-- UI/HelpPages/Overview.lua
-- What Farm? :: HelpWindow - Overview page (split out)
-- Right panel order: Random Farm -> Top Farms -> WoW Token -> Bag Worth
-- Bag Worth is MANUAL ONLY (runs ONLY when you press "Refresh Worth")

local ADDON_NAME, ns = ...
local WF = ns.WF

WF.UI = WF.UI or {}
WF.UI.HelpPages = WF.UI.HelpPages or {}

local BODY_FONT = (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.body) or ("Interface\\AddOns\\" .. tostring(ADDON_NAME) .. "\\Media\\Fonts\\PTSansNarrow.ttf")
local DISPLAY_FONT = (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.display) or ("Interface\\AddOns\\" .. tostring(ADDON_NAME) .. "\\Media\\Fonts\\Expressway.ttf")

local function WF_IsElvUIEnabled()
  if _G and _G.ElvUI then
    return true
  end
  if type(IsAddOnLoaded) == "function" then
    local ok, loaded = pcall(IsAddOnLoaded, "ElvUI")
    if ok and loaded then
      return true
    end
  end
  return false
end

local function WF_FontTemplate(wfName, blizzardName)
  if (not WF_IsElvUIEnabled()) and wfName and _G[wfName] then
    return wfName
  end
  if blizzardName and _G[blizzardName] then
    return blizzardName
  end
  return wfName or blizzardName
end

local RF_EXPANSION_DISPLAY = {
  ["BfA"] = "Battle for Azeroth",
  ["TWW"] = "The War Within",
  ["MoP"] = "Mists of Pandaria",
  ["WoD"] = "Warlords of Draenor",
  ["WotLK"] = "Wrath of the Lich King",
  ["Wrath"] = "Wrath of the Lich King",
  ["Cata"] = "Cataclysm",
  ["DF"] = "Dragonflight",
  ["SL"] = "Shadowlands",
  ["TBC"] = "The Burning Crusade",
  ["BC"] = "The Burning Crusade",
}

local function RF_Trim(v)
  v = tostring(v or "")
  return (v:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function RF_HumanizeLabel(v)
  v = RF_Trim(v)
  if v == "" then return v end
  v = v:gsub("_", " ")
  v = v:gsub("%-", " ")
  v = v:gsub("(%l)(%u)", "%1 %2")
  v = v:gsub("(%a)(%d)", "%1 %2")
  v = v:gsub("(%d)(%a)", "%1 %2")
  v = v:gsub("%s+", " ")
  return v
end

local function RF_AimLabel(Util, v)
  local value = RF_Trim(v)
  if value == "" then value = "Unknown" end
  if Util and Util.PrettyToken then
    return Util:PrettyToken(value)
  end
  return tostring(value)
end

local function RF_ExpansionLabel(v)
  local value = RF_Trim(v)
  if value == "" then value = "Unknown" end
  return RF_EXPANSION_DISPLAY[value] or RF_HumanizeLabel(value)
end

local function RF_BuildExpansionItems(expansions)
  local items = {}
  local src = expansions or {}
  for i = 1, #src do
    local v = tostring(src[i] or "")
    if v ~= "" then
      local display = RF_ExpansionLabel(v)
      local body
      if v == "Any" then
        body = "Show farms from every expansion in the Random Farm pool."
      elseif v == "Unknown" then
        body = "Only show farms that do not have an expansion assigned yet."
      else
        body = "Only show farms from " .. display .. "."
      end
      items[#items + 1] = { text = display, value = v, tooltipTitle = "Expansion", tooltipBody = body }
    end
  end
  return items
end

local function BW_IsGuildBankOpen()
  if IsGuildBankFrameOpen then
    return IsGuildBankFrameOpen()
  end
  if GuildBankFrame and GuildBankFrame.IsShown and GuildBankFrame:IsShown() then
    return true
  end
  return false
end

local function BW_SourceLabel(providerStr)
  providerStr = tostring(providerStr or "")
  local prov, extra = providerStr:match("^([%a_]+)%:(.+)$")
  if not prov then prov = providerStr end
  prov = tostring(prov or ""):lower()
  extra = tostring(extra or "")

  if prov == "tsm" then
    local lbl = extra
    if TSMStringToLabel then
      lbl = TSMStringToLabel(extra) or extra
    end
    if lbl == "" then lbl = "Default" end
    return "TSM • " .. lbl
  end

  if prov == "auctionator" then
    return "Auctionator"
  end

  if prov == "vendor" or prov == "" then
    return "Vendor"
  end

  return providerStr
end

local function RF_BuildAimItems(WF, Util, farmAims)
  local items = {}
  local seen = {}

  local function add(value, body)
    value = RF_Trim(value)
    if value == "" then value = "Unknown" end
    local key = value:lower()
    if seen[key] then return end
    seen[key] = true
    items[#items + 1] = {
      text = RF_AimLabel(Util, value),
      value = value,
      tooltipTitle = "Aim",
      tooltipBody = body,
    }
  end

  add("Any", "Show every farm aim in the Random Farm pool.")
  add("Unknown", "Only show farms that do not have an aim assigned yet.")

  for i = 1, #(farmAims or {}) do
    local v = tostring(farmAims[i] or "")
    if v ~= "" and v ~= "Any" then
      add(v, "Only show farms aimed at " .. RF_AimLabel(Util, v) .. ".")
    end
  end

  local db = (WF and WF.DB and WF.DB.Get and WF.DB:Get()) or nil
  local farms = (db and db.farms) or nil
  if farms then
    local dynamic = {}
    for _, farm in pairs(farms) do
      if type(farm) == "table" then
        local aim = RF_Trim(farm.aim)
        if aim ~= "" and aim ~= "Any" then
          dynamic[#dynamic + 1] = aim
        end
      end
    end

    table.sort(dynamic, function(a, b)
      return RF_AimLabel(Util, a):lower() < RF_AimLabel(Util, b):lower()
    end)

    for i = 1, #dynamic do
      local v = dynamic[i]
      add(v, "Only show farms aimed at " .. RF_AimLabel(Util, v) .. ".")
    end
  end

  return items
end

WF.UI.HelpPages.Overview = function(ctx)
  local UI = ctx.UI
  local self = UI

  local WF = ctx.WF
  local Widgets = ctx.Widgets
  local Theme = ctx.Theme
  local Util = ctx.Util

  -- format helpers (provided by HelpWindow ctx)
  local Money = ctx.Money
  local FormatTime = ctx.FormatTime
  local SessionGPH = ctx.SessionGPH
  local SessionLIV = ctx.SessionLIV
  local SafeNow = ctx.SafeNow
  local SetWaypoint = ctx.SetWaypoint

  local C4 = ctx.C4
  local TEXT = ctx.TEXT
  local MUTED = ctx.MUTED
  local PANEL = ctx.PANEL
  local BORDER = (Theme and Theme.border) or { 0.12, 0.12, 0.12, 1.00 }
  local ACCENT = (Theme and Theme.accent) or { 0.35, 0.78, 1.00, 1.00 }

  local function DisplayProfileName(name)
    name = tostring(name or "")
    if name == "" then
      return "Default"
    end
    return name
  end

-- Checkboxes: force WhatFarm accent blue checks (not Blizzard green)
local function WF_ApplyBlueCheck(btn)
  if not btn or btn._wfBlueCheck then return end
  btn._wfBlueCheck = true

  local function tint(tex, a)
    if not tex or not tex.SetVertexColor then return end
    if tex.SetDesaturated then tex:SetDesaturated(true) end
    tex:SetVertexColor(ACCENT[1], ACCENT[2], ACCENT[3], a or 1)
  end

  local function apply()
    tint(btn.GetCheckedTexture and btn:GetCheckedTexture(), 1)
    tint(btn.GetDisabledCheckedTexture and btn:GetDisabledCheckedTexture(), 0.65)
    if btn.Check then tint(btn.Check, 1) end
  end

  apply()
  if btn.HookScript then
    btn:HookScript("OnShow", apply)
  end
end


  -- -----------------------------------------------------
  -- Options gear icon helper (Overview right-rail headers)
  -- Replaces the "Options" text button with a clickable gear icon.
  -- -----------------------------------------------------
  local function _WF_ApplyGearOptionButton(btn)
    if not btn or btn._wfGearOpt then return end
    btn._wfGearOpt = true

    -- compact footprint, but keep click target generous
    if btn.SetSize then btn:SetSize(18, 18) end
    if btn.SetHitRectInsets then
      -- negative values expand the clickable region
      btn:SetHitRectInsets(-10, -10, -6, -6)
    end

    if btn.txt then
      if btn.txt.SetText then btn.txt:SetText("") end
      if btn.txt.Hide then btn.txt:Hide() end
    end

    if not btn.icon then
      btn.icon = btn:CreateTexture(nil, "OVERLAY")
    end
    btn.icon:SetPoint("CENTER", 0, 0)
    btn.icon:SetSize(16, 16)
    -- Use WhatFarm's own gear icon from Media (white silhouette so tinting works).
    -- (User has these in: WhatFarm/Media/Icons/WF_Gear*.tga)
    btn.icon:SetTexture("Interface/AddOns/WhatFarm/Media/Icons/WF_Gear_White2.tga")
    if btn.icon.SetTexCoord then
      btn.icon:SetTexCoord(0, 1, 0, 1)
    end

    local mr, mg, mb = C4(MUTED, MUTED)
    btn.icon:SetVertexColor(mr, mg, mb, 1)
  end

  local function _WF_GearOpt_SetHover(btn, hover)
    if not btn then return end
    if btn.bg then btn.bg:SetAlpha(0) end

    if btn.icon then
      if hover then
        local ar, ag, ab = C4(ACCENT, ACCENT)
        btn.icon:SetVertexColor(ar, ag, ab, 1)
      else
        local mr, mg, mb = C4(MUTED, MUTED)
        btn.icon:SetVertexColor(mr, mg, mb, 1)
      end
    end
  end

  local MakePage = ctx.MakePage
  local MakePanel = ctx.MakePanel
  local MakeCard = ctx.MakeCard
  local MakeSectionTitle = ctx.MakeSectionTitle
  local MakeMutedText = ctx.MakeMutedText
  local MakeBodyText = ctx.MakeBodyText
  local MakeDivider = ctx.MakeDivider

  local TSMStringToLabel = ctx.TSMStringToLabel

  local EnsureSettingsDefaults = ctx.EnsureSettingsDefaults

  -- -----------------------------------------------------
  -- Local helpers
  -- -----------------------------------------------------
  local function _Trim(s)
    s = tostring(s or "")
    s = s:gsub("^%s+", ""):gsub("%s+$", "")
    return s
  end

  local function _DurationText(sec)
    sec = tonumber(sec)
    if not sec or sec <= 0 then return "—" end
    -- Floor (don't round) so durations match the values stored/shown elsewhere.
    -- Sessions often store a precise duration (float) but only coarse startedAt/endedAt
    -- timestamps (whole seconds). Rounding can drift by 1-3s across UIs.
    sec = math.floor(sec + 0.0001)

    local h = math.floor(sec / 3600)
    sec = sec - (h * 3600)
    local m = math.floor(sec / 60)
    local s = sec - (m * 60)

    if h > 0 then
      return string.format("%dh %dm %ds", h, m, s)
    end
    if m > 0 then
      return string.format("%dm %ds", m, s)
    end
    return string.format("%ds", s)
  end

  local function _DurationLongText(sec)
    sec = tonumber(sec)
    if not sec or sec <= 0 then return "—" end
    sec = math.floor(sec + 0.0001)

    local h = math.floor(sec / 3600)
    sec = sec - (h * 3600)
    local m = math.floor(sec / 60)
    local s = sec - (m * 60)

    local parts = {}
    if h > 0 then parts[#parts + 1] = string.format("%d Hr", h) end
    if m > 0 then parts[#parts + 1] = string.format("%d Min", m) end
    if s > 0 or #parts == 0 then parts[#parts + 1] = string.format("%d Sec", s) end

    return table.concat(parts, " ")
  end

  local function _SessDuration(sess)
    if not sess then return nil end
    -- Prefer an explicit duration field if present. Some session records store
    -- a precise duration (float seconds) while startedAt/endedAt are coarse (whole seconds).
    local dPrecise = tonumber(
      sess.durationSec
      or sess.durationSeconds
      or sess.elapsedSec
      or sess.elapsedSeconds
      or sess.runtimeSec
      or sess.runTimeSec
      or sess.duration
      or sess.seconds
      or sess.elapsed
      or sess.runtime
      or sess.runTime
      or sess.durSec
      or sess.dur
    )

    local a = tonumber(sess.startedAt or 0) or 0
    local b = tonumber(sess.endedAt or sess.endAt or sess.ended or sess.endTime or 0) or 0
    local dWall = (a > 0 and b > a) and (b - a) or nil

    local function _Valid(d)
      d = tonumber(d)
      if not d or d <= 0 then return nil end
      -- Guard against wrong units (ms) or corrupted values.
      if d > (7 * 24 * 3600) then return nil end
      return d
    end

    dPrecise = _Valid(dPrecise)
    dWall = _Valid(dWall)

    if dPrecise and dWall then
      -- If they're close, prefer the precise value.
      if math.abs(dPrecise - dWall) <= 5 then
        return dPrecise
      end
      -- Otherwise, pick the smaller one (avoid overstating duration).
      return math.min(dPrecise, dWall)
    end

    return dPrecise or dWall
  end

  local function _SessDurationText(sess)
    if not sess then return "—" end
    local t = _Trim(sess.durationText or sess.durationStr or sess.timeText or "")
    if t ~= "" then return t end
    local d = _SessDuration(sess)
    if not d then return "—" end
    return _DurationText(d)
  end


  local function _SessTotals(sess)
    local total = tonumber(sess and (sess.totalValue or sess.total or sess.value)) or 0
    local gold =
        tonumber(sess and sess.moneyGained)
        or tonumber(sess and sess.lootedGold)
        or tonumber(sess and sess.goldLooted)
        or tonumber(sess and sess.rawGold)
        or tonumber(sess and sess.gold)
        or 0
    gold = tonumber(gold) or 0
    if gold < 0 then gold = 0 end

    local livTotal = total - gold
    if livTotal < 0 then livTotal = 0 end

    return total, gold, livTotal
  end

  local function _DisplayMetricValue(metric, sess)
    metric = tostring(metric or "gph"):lower()
    if metric == "liv" then
      local _, _, livTotal = _SessTotals(sess)
      return livTotal
    end
    return SessionGPH(sess)
  end
  -- -----------------------------------------------------
  -- OVERVIEW
  -- -----------------------------------------------------
  local pOverview = MakePage("Overview", "Overview")
  do
    if pOverview.SetClipsChildren then pOverview:SetClipsChildren(true) end

    local c1 = MakeCard(pOverview, "Last 5 avg GPH")

    -- Metric toggle (GPH <-> LIV)
    do
      local btn = CreateFrame("Button", nil, c1)
      btn:SetSize(46, 18)
      btn:SetPoint("TOPRIGHT", -10, -10)

      btn.txt = btn:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      btn.txt:SetPoint("CENTER")
      btn.txt:SetTextColor(C4(MUTED, MUTED))
      btn.txt:SetText("GPH")

      btn:RegisterForClicks("LeftButtonUp")
      btn:SetScript("OnClick", function()
        if not WF or not WF.DB or not WF.DB.GetSetting or not WF.DB.SetSetting then return end
        local cur = tostring(WF.DB:GetSetting("overviewLast5Metric") or "gph"):lower()
        local nxt = (cur == "liv") and "gph" or "liv"
        WF.DB:SetSetting("overviewLast5Metric", nxt)

        -- Refresh any visible pages that depend on the display metric.
        if UI and UI.RefreshOverview then UI:RefreshOverview() end
        if UI and UI.RefreshHistory then UI:RefreshHistory() end
        if UI and UI.RefreshFarms then UI:RefreshFarms() end
      end)

      c1._metricBtn = btn

      -- Make the whole card clickable (users tend to click the card, not the tiny label).
      local hit = CreateFrame("Button", nil, c1)
      hit:SetAllPoints(c1)
      hit:SetFrameLevel(c1:GetFrameLevel() + 1)

      if btn and btn.SetFrameLevel then
        btn:SetFrameLevel(c1:GetFrameLevel() + 5)
      end

      hit:RegisterForClicks("LeftButtonUp")
      hit:SetScript("OnClick", function()
        if btn and btn.Click then btn:Click() end
      end)

      -- Prefer hover/click on the full card (including the label).
      if btn and btn.EnableMouse then btn:EnableMouse(false) end

      local function _ShowLast5MetricTip(owner)
        if not owner or not GameTooltip then return end

        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end

        local metric = "gph"
        if WF and WF.DB and WF.DB.GetSetting then
          metric = tostring(WF.DB:GetSetting("overviewLast5Metric") or "gph"):lower()
        end
        if metric ~= "liv" then metric = "gph" end

        local curLabel = (metric == "liv") and "LIV" or "GPH"
        local nxtLabel = (metric == "liv") and "GPH" or "LIV"

        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine("Metric", ar, ag, ab)
        GameTooltip:AddLine("Currently: " .. curLabel, 0.95, 0.95, 0.95)
        GameTooltip:AddLine("Click to switch to " .. nxtLabel .. ".", 0.90, 0.90, 0.90, true)
        GameTooltip:AddLine(" ", 1, 1, 1)
        GameTooltip:AddLine("LIV: Looted item value (excludes looted gold).", 0.75, 0.75, 0.75, true)
        GameTooltip:AddLine("GPH: Gold per hour based on session time.", 0.75, 0.75, 0.75, true)
        GameTooltip:Show()
      end

      hit:SetScript("OnEnter", function(self)
        _ShowLast5MetricTip(self)
      end)
      hit:SetScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
      end)

      c1._metricHit = hit
    end

    local c2 = MakeCard(pOverview, "Best run (last 5)")
    local c3 = MakeCard(pOverview, "Sessions saved")
    local c4 = MakeCard(pOverview, "Farms Loaded")

    -- ---------------------------
    -- Left panel: Recent runs
    -- ---------------------------
    local left = MakePanel(pOverview, PANEL)

    local leftTitle = MakeSectionTitle(left, "Recent runs")
    leftTitle:SetPoint("TOPLEFT", 12, -10)

    -- Keep refs for refresh styling
    UI._ovLeftTitle = leftTitle

    local leftSub = left:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    leftSub:SetTextColor(C4(MUTED, MUTED))
    leftSub:SetText("Left-click: History  •  Right-click: Options")
    do
      local f, _, fl = leftSub:GetFont()
      if f then
        leftSub:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
      end
    end
    leftSub:SetPoint("LEFT", leftTitle, "RIGHT", 10, 0)

    local div = MakeDivider(left)
    div:SetPoint("TOPLEFT", 12, -34)
    div:SetPoint("TOPRIGHT", -12, -34)

    -- Column header bar (Farm | <metric> | Mode | Time | Date)
    local runsHeader = CreateFrame("Frame", nil, left)
    runsHeader:SetPoint("TOPLEFT", 6, -40)
    runsHeader:SetPoint("TOPRIGHT", -26, -40)
    runsHeader:SetHeight(22)

    runsHeader.bg = runsHeader:CreateTexture(nil, "BACKGROUND")
    runsHeader.bg:SetAllPoints(runsHeader)
    runsHeader.bg:SetTexture("Interface/Buttons/WHITE8x8")
    runsHeader.bg:SetVertexColor(1, 1, 1, 0.06)

    runsHeader.farm = runsHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    runsHeader.farm:SetPoint("LEFT", 12, 0)
    runsHeader.farm:SetTextColor(C4(ACCENT, ACCENT))
    do -- _wfHdrFont
      local f, _, fl = runsHeader.farm:GetFont()
      if f then
        runsHeader.farm:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
      end
    end
    runsHeader.farm:SetText("Farm")
    runsHeader.farm:SetWordWrap(false)
    if runsHeader.farm.SetMaxLines then runsHeader.farm:SetMaxLines(1) end

    runsHeader.time = runsHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    runsHeader.time:SetPoint("RIGHT", runsHeader, "RIGHT", -12, 0)
    runsHeader.time:SetTextColor(C4(ACCENT, ACCENT))
    do -- _wfHdrFont
      local f, _, fl = runsHeader.time:GetFont()
      if f then
        runsHeader.time:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
      end
    end
    runsHeader.time:SetJustifyH("CENTER")
    runsHeader.time:SetWidth(92)
    runsHeader.time:SetText("Date")
    runsHeader.time:SetWordWrap(false)
    if runsHeader.time.SetMaxLines then runsHeader.time:SetMaxLines(1) end

    runsHeader.dur = runsHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    runsHeader.dur:SetPoint("RIGHT", runsHeader.time, "LEFT", -20, 0)
    runsHeader.dur:SetTextColor(C4(ACCENT, ACCENT))
    do -- _wfHdrFont
      local f, _, fl = runsHeader.dur:GetFont()
      if f then
        runsHeader.dur:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
      end
    end
    runsHeader.dur:SetJustifyH("CENTER")
    runsHeader.dur:SetWidth(72)
    runsHeader.dur:SetText("Time")
    runsHeader.dur:SetWordWrap(false)
    if runsHeader.dur.SetMaxLines then runsHeader.dur:SetMaxLines(1) end

    runsHeader.mode = runsHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    runsHeader.mode:SetPoint("RIGHT", runsHeader.dur, "LEFT", -20, 0)
    runsHeader.mode:SetTextColor(C4(ACCENT, ACCENT))
    do -- _wfHdrFont
      local f, _, fl = runsHeader.mode:GetFont()
      if f then
        runsHeader.mode:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
      end
    end
    runsHeader.mode:SetJustifyH("CENTER")
    runsHeader.mode:SetWidth(100)
    runsHeader.mode:SetText("Mode")
    runsHeader.mode:SetWordWrap(false)
    if runsHeader.mode.SetMaxLines then runsHeader.mode:SetMaxLines(1) end

    runsHeader.metric = runsHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    runsHeader.metric:SetPoint("RIGHT", runsHeader.mode, "LEFT", -20, 0)
    runsHeader.metric:SetTextColor(C4(ACCENT, ACCENT))
    do -- _wfHdrFont
      local f, _, fl = runsHeader.metric:GetFont()
      if f then
        runsHeader.metric:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
      end
    end
    runsHeader.metric:SetJustifyH("RIGHT")
    runsHeader.metric:SetWidth(140)
    runsHeader.metric:SetText("GPH")
    runsHeader.metric:SetWordWrap(false)
    if runsHeader.metric.SetMaxLines then runsHeader.metric:SetMaxLines(1) end

-- Make the column headers clickable (sort Recent Runs)
    -- Farm: A-Z, Metric: High-Low, Time: Long-Short, Date: Newest-Oldest. Clicking again toggles direction.
    if not runsHeader._wfHeaderButtons then
      runsHeader._wfHeaderButtons = true
      UI._ovRunsHeader = runsHeader

      local function _HeaderHover(btn, on)
        if not btn or not btn._hl then return end
        btn._hl:SetAlpha(on and 0.08 or 0)
      end

      local function _MakeHeaderBtn(key, w, anchorRegion)
        local b = CreateFrame("Button", nil, runsHeader)
        b:SetHeight(22)
        if w then b:SetWidth(w) end
        if anchorRegion then
          b:SetPoint("CENTER", anchorRegion, "CENTER", 0, 0)
        end

        b._hl = b:CreateTexture(nil, "BACKGROUND")
        b._hl:SetAllPoints(b)
        b._hl:SetTexture("Interface/Buttons/WHITE8x8")
        b._hl:SetVertexColor(1, 1, 1, 1)
        b._hl:SetAlpha(0)

        b:SetScript("OnEnter", function(self) _HeaderHover(self, true) end)
        b:SetScript("OnLeave", function(self) _HeaderHover(self, false) end)
        b:SetScript("OnClick", function()
          local curKey = UI._ovRecentSortKey or "date"
          local curDir = UI._ovRecentSortDir or -1

          if curKey == key then
            curDir = -curDir
          else
            curKey = key
            if key == "farm" then
              curDir = 1
            elseif key == "metric" then
              curDir = -1
            else
              curDir = -1
            end
          end

          UI._ovRecentSortKey = curKey
          UI._ovRecentSortDir = curDir
          if UI.RefreshOverview then UI:RefreshOverview() end
        end)

        return b
      end

      -- Buttons sized to match column widths
      runsHeader._btnDate = _MakeHeaderBtn("date", 110, runsHeader.time)
      runsHeader._btnDur = _MakeHeaderBtn("dur", 90, runsHeader.dur)
      runsHeader._btnMetric = _MakeHeaderBtn("metric", 160, runsHeader.metric)

      -- Mode header: Left-click sorts by Mode, Right-click opens the Mode filter menu
      runsHeader._btnMode = CreateFrame("Button", nil, runsHeader)
      runsHeader._btnMode:SetHeight(22)
      runsHeader._btnMode:SetWidth(120)
      runsHeader._btnMode:SetPoint("CENTER", runsHeader.mode, "CENTER", 0, 0)

      runsHeader._btnMode._hl = runsHeader._btnMode:CreateTexture(nil, "BACKGROUND")
      runsHeader._btnMode._hl:SetAllPoints(runsHeader._btnMode)
      runsHeader._btnMode._hl:SetTexture("Interface/Buttons/WHITE8x8")
      runsHeader._btnMode._hl:SetVertexColor(1, 1, 1, 1)
      runsHeader._btnMode._hl:SetAlpha(0)

      runsHeader._btnMode:RegisterForClicks("LeftButtonUp", "RightButtonUp")
      runsHeader._btnMode:SetScript("OnEnter", function(self)
        _HeaderHover(self, true)

        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(self, "ANCHOR_TOPRIGHT")
        GameTooltip:AddLine("Mode", ACCENT[1], ACCENT[2], ACCENT[3], true)
        GameTooltip:AddLine("Left-click: sort by Mode", 0.70, 0.70, 0.70)
        GameTooltip:AddLine("Right-click: open Mode filter", 0.70, 0.70, 0.70)
        GameTooltip:Show()
      end)

      runsHeader._btnMode:SetScript("OnLeave", function(self)
        _HeaderHover(self, false)
        GameTooltip:Hide()
      end)
      runsHeader._btnMode:SetScript("OnClick", function(_, button)
        if button == "LeftButton" and IsShiftKeyDown and IsShiftKeyDown() then
          if WF and WF.Share and WF.Share.InsertRunLink then
            WF.Share:InsertRunLink(sess)
          elseif WF and WF.Share and WF.Share.ShowShareDialog then
            WF.Share:ShowShareDialog(sess)
          end
          return
        end

        if button == "RightButton" then
          if UI and UI.OpenModeFilterMenu then UI:OpenModeFilterMenu() end
          return
        end

        local curKey = UI._ovRecentSortKey or "date"
        local curDir = UI._ovRecentSortDir or -1

        if curKey == "mode" then
          curDir = -curDir
        else
          curKey = "mode"
          curDir = 1
        end

        UI._ovRecentSortKey = curKey
        UI._ovRecentSortDir = curDir
        if UI.RefreshOverview then UI:RefreshOverview() end
      end)

      runsHeader._btnFarm = CreateFrame("Button", nil, runsHeader)
      runsHeader._btnFarm:SetHeight(22)
      runsHeader._btnFarm:SetPoint("LEFT", 0, 0)
      runsHeader._btnFarm:SetPoint("RIGHT", runsHeader._btnMetric, "LEFT", 0, 0)

      runsHeader._btnFarm._hl = runsHeader._btnFarm:CreateTexture(nil, "BACKGROUND")
      runsHeader._btnFarm._hl:SetAllPoints(runsHeader._btnFarm)
      runsHeader._btnFarm._hl:SetTexture("Interface/Buttons/WHITE8x8")
      runsHeader._btnFarm._hl:SetVertexColor(1, 1, 1, 1)
      runsHeader._btnFarm._hl:SetAlpha(0)

      runsHeader._btnFarm:RegisterForClicks("LeftButtonUp")

      runsHeader._btnFarm:SetScript("OnEnter", function(self) _HeaderHover(self, true) end)
      runsHeader._btnFarm:SetScript("OnLeave", function(self) _HeaderHover(self, false) end)
      runsHeader._btnFarm:SetScript("OnClick", function()
        local curKey = UI._ovRecentSortKey or "date"
        local curDir = UI._ovRecentSortDir or -1

        if curKey == "farm" then
          curDir = -curDir
        else
          curKey = "farm"
          curDir = 1
        end

        UI._ovRecentSortKey = curKey
        UI._ovRecentSortDir = curDir
        if UI.RefreshOverview then UI:RefreshOverview() end
      end)
    end

    -- --------------------------------------------
    -- Recent Runs (stable) + About (scrollable box)
    -- --------------------------------------------
    UI._ovAboutCollapsed = UI._ovAboutCollapsed and true or false

    local about = CreateFrame("Frame", nil, left)
    about:SetPoint("BOTTOMLEFT", 6, 10)
    about:SetPoint("BOTTOMRIGHT", -6, 10)
    -- Keep the expanded About / FAQ / Changelog box to roughly five collapsed rows before scrolling.
    -- Collapsed mode leaves the tab strip visible and gives the space back to Recent Runs.
    about:SetHeight(UI._ovAboutCollapsed and 32 or 165)

    -- Dedicated tutorial/highlight target for the Recent Runs area only.
    -- This intentionally stops above the About box so the tutorial does not
    -- spotlight both sections at the same time.
    local recentRunsFocus = CreateFrame("Frame", nil, left)
    recentRunsFocus:SetPoint("TOPLEFT", left, "TOPLEFT", 6, -6)
    recentRunsFocus:SetPoint("TOPRIGHT", left, "TOPRIGHT", -6, -6)
    recentRunsFocus:SetPoint("BOTTOMLEFT", about, "TOPLEFT", 0, 4)
    recentRunsFocus:SetPoint("BOTTOMRIGHT", about, "TOPRIGHT", -6, 4)

    UI._ovAboutActiveTab = UI._ovAboutActiveTab or "about"
    if UI._ovAboutActiveTab ~= "about" and UI._ovAboutActiveTab ~= "faq" and UI._ovAboutActiveTab ~= "changelog" then
      UI._ovAboutActiveTab = "about"
    end

    about._wfTabs = CreateFrame("Frame", nil, about)
    about._wfTabs:SetPoint("TOPLEFT", 6, -4)
    about._wfTabs:SetPoint("TOPRIGHT", -34, -4)
    about._wfTabs:SetHeight(22)

    do
      local function MakeAboutTab(key, label)
        local b = CreateFrame("Button", nil, about._wfTabs)
        b:SetSize((label == "Changelog" and 118) or 92, 22)
        b._wfTabKey = key

        -- Segmented tab rendering.
        -- The old button-box look used four separate 1px borders, which could
        -- disappear or look uneven at certain UI scales. This avoids boxed
        -- borders entirely: inactive tabs sit on a dark strip, and the active
        -- tab is shown with a blue underline + slightly brighter fill.
        b.bg = b:CreateTexture(nil, "BACKGROUND")
        b.bg:SetTexture("Interface/Buttons/WHITE8x8")
        b.bg:SetAllPoints(b)

        b.activeLine = b:CreateTexture(nil, "ARTWORK")
        b.activeLine:SetTexture("Interface/Buttons/WHITE8x8")
        b.activeLine:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
        b.activeLine:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
        b.activeLine:SetHeight(2)

        b.topSoftLine = b:CreateTexture(nil, "BORDER")
        b.topSoftLine:SetTexture("Interface/Buttons/WHITE8x8")
        b.topSoftLine:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
        b.topSoftLine:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
        b.topSoftLine:SetHeight(1)

        if Widgets and Widgets.MakePixelPerfect then
          Widgets:MakePixelPerfect(b.activeLine, b, { mode = "h" })
          Widgets:MakePixelPerfect(b.topSoftLine, b, { mode = "h" })
        end

        b:SetHighlightTexture("Interface/Buttons/WHITE8x8")
        local ht = b.GetHighlightTexture and b:GetHighlightTexture()
        if ht then
          ht:SetBlendMode("BLEND")
          ht:ClearAllPoints()
          ht:SetAllPoints(b)
          ht:SetVertexColor(1, 1, 1, 0.035)
        end

        b.text = b:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
        b.text:SetPoint("CENTER", 0, 0)
        b.text:SetText(label)

        do
          local ar, ag, ab = C4(ACCENT, ACCENT)
          local mr, mg, mb = C4(MUTED, MUTED)
          local active = (UI._ovAboutActiveTab or "about") == key
          if active then
            b.bg:SetColorTexture(ar, ag, ab, 0.13)
            b.activeLine:SetColorTexture(ar, ag, ab, 1)
            b.activeLine:Show()
            b.topSoftLine:SetColorTexture(ar, ag, ab, 0.18)
            b.text:SetTextColor(1, 1, 1, 1)
          else
            b.bg:SetColorTexture(0, 0, 0, 0.26)
            b.activeLine:Hide()
            b.topSoftLine:SetColorTexture(1, 1, 1, 0.035)
            b.text:SetTextColor(mr, mg, mb, 1)
          end
        end

        b:SetScript("OnClick", function()
          local sameTab = UI._ovAboutActiveTab == key
          UI._ovAboutActiveTab = key

          if UI._ovAboutCollapsed and UI._SetOverviewAboutCollapsed then
            UI:_SetOverviewAboutCollapsed(false)
            if about._wfScroll and about._wfScroll.ScrollBar then about._wfScroll.ScrollBar:SetValue(0) end
            return
          end

          if sameTab then return end
          if about._wfScroll and about._wfScroll.ScrollBar then about._wfScroll.ScrollBar:SetValue(0) end
          if UI._UpdateOverviewAboutTabs then UI:_UpdateOverviewAboutTabs() end
          if UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
        end)

        return b
      end

      about._wfTabAbout = MakeAboutTab("about", "About")
      about._wfTabAbout:SetPoint("TOPLEFT", about._wfTabs, "TOPLEFT", 0, 0)

      about._wfTabFAQ = MakeAboutTab("faq", "FAQ")
      about._wfTabFAQ:SetPoint("LEFT", about._wfTabAbout, "RIGHT", 8, 0)

      about._wfTabChangelog = MakeAboutTab("changelog", "Changelog")
      about._wfTabChangelog:SetPoint("LEFT", about._wfTabFAQ, "RIGHT", 8, 0)
    end

    about._wfCollapseButton = CreateFrame("Button", nil, about)
    about._wfCollapseButton:SetSize(22, 22)
    about._wfCollapseButton:SetPoint("TOPRIGHT", about, "TOPRIGHT", -6, -4)

    about._wfCollapseButton.bg = about._wfCollapseButton:CreateTexture(nil, "BACKGROUND")
    about._wfCollapseButton.bg:SetTexture("Interface/Buttons/WHITE8x8")
    about._wfCollapseButton.bg:SetAllPoints(about._wfCollapseButton)

    about._wfCollapseButton.line = about._wfCollapseButton:CreateTexture(nil, "ARTWORK")
    about._wfCollapseButton.line:SetTexture("Interface/Buttons/WHITE8x8")
    about._wfCollapseButton.line:SetPoint("BOTTOMLEFT", about._wfCollapseButton, "BOTTOMLEFT", 0, 0)
    about._wfCollapseButton.line:SetPoint("BOTTOMRIGHT", about._wfCollapseButton, "BOTTOMRIGHT", 0, 0)
    about._wfCollapseButton.line:SetHeight(2)

    if Widgets and Widgets.MakePixelPerfect then
      Widgets:MakePixelPerfect(about._wfCollapseButton.line, about._wfCollapseButton, { mode = "h" })
    end

    about._wfCollapseButton:SetHighlightTexture("Interface/Buttons/WHITE8x8")
    do
      local ht = about._wfCollapseButton.GetHighlightTexture and about._wfCollapseButton:GetHighlightTexture()
      if ht then
        ht:SetBlendMode("BLEND")
        ht:ClearAllPoints()
        ht:SetAllPoints(about._wfCollapseButton)
        ht:SetVertexColor(1, 1, 1, 0.045)
      end
    end

    about._wfCollapseButton.text = about._wfCollapseButton:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
    about._wfCollapseButton.text:SetPoint("CENTER", 0, 0)

    about._wfCollapseButton:SetScript("OnClick", function()
      if UI and UI._SetOverviewAboutCollapsed then
        UI:_SetOverviewAboutCollapsed(not UI._ovAboutCollapsed)
      end
    end)

    local aboutDiv = MakeDivider(about)
    aboutDiv:SetPoint("TOPLEFT", 6, -30)
    aboutDiv:SetPoint("TOPRIGHT", -6, -30)
    aboutDiv:SetAlpha(0.25)

    local aboutScroll = CreateFrame("ScrollFrame", nil, about, "UIPanelScrollFrameTemplate")
    about._wfScroll = aboutScroll
  if Widgets and Widgets.SkinScrollBar and aboutScroll.ScrollBar then Widgets:SkinScrollBar(aboutScroll.ScrollBar) end
    aboutScroll:SetPoint("TOPLEFT", 0, -36)
    aboutScroll:SetPoint("BOTTOMRIGHT", -26, 4)

    local aboutSC = CreateFrame("Frame", nil, aboutScroll)
    aboutSC:SetPoint("TOPLEFT", 0, 0)
    aboutSC:SetSize(1, 1)
    aboutScroll:SetScrollChild(aboutSC)

    -- Recent Runs scroll sits ABOVE About, anchored to About's TOP.
    local scroll = CreateFrame("ScrollFrame", nil, left, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
    scroll:SetPoint("TOPLEFT", 6, -66)
    scroll:SetPoint("BOTTOMLEFT", about, "TOPLEFT", 0, 4)
    scroll:SetPoint("BOTTOMRIGHT", about, "TOPRIGHT", -26, 4)

    local sc = CreateFrame("Frame", nil, scroll)
    sc:SetSize(1, 1)
    scroll:SetScrollChild(sc)

    -- Keep the scrollChild width synced with the ScrollFrame width.
    -- Without this, if the Help window is resized wider, the rows stay "stuck" at the old width,
    -- causing the GPH/Date columns to drift and leaving dead space on the right.
    local function _UpdateRunsWidth()
      local w = scroll:GetWidth()
      if w and w > 1 then
        sc:SetWidth(w)
      end
      if UI and UI._LayoutOverviewRecentRunsColumns then
        UI:_LayoutOverviewRecentRunsColumns()
      end
    end

    _UpdateRunsWidth()
    if C_Timer and C_Timer.After then
      C_Timer.After(0, _UpdateRunsWidth)
    end

    if not scroll._wfRunsSizeHooked then
      scroll._wfRunsSizeHooked = true
      scroll:HookScript("OnSizeChanged", _UpdateRunsWidth)
    end

    -- Empty state (Recent runs)
    local emptyRuns = scroll:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    emptyRuns:SetPoint("CENTER", scroll, "CENTER", 0, 0)
    emptyRuns:SetJustifyH("CENTER")
    emptyRuns:SetTextColor(C4(MUTED, MUTED))
    emptyRuns:SetText("No runs yet.\nStart a session to see your recent runs here.")
    emptyRuns:Hide()

    local function _UpdateEmptyRunsWidth()
      local w = scroll:GetWidth() or 0
      if w > 60 then
        emptyRuns:SetWidth(w - 40)
      end
    end
    _UpdateEmptyRunsWidth()
    scroll:HookScript("OnSizeChanged", _UpdateEmptyRunsWidth)
    UI._ovEmptyRuns = emptyRuns

    -- About dropdowns live inside aboutSC, and can scroll when everything is open.
    local aboutDrops = {}

    function UI:_PaintOverviewAboutCollapseButton()
      local btn = about and about._wfCollapseButton
      if not btn then return end

      local collapsed = UI._ovAboutCollapsed and true or false
      local ar, ag, ab = C4(ACCENT, ACCENT)
      local mr, mg, mb = C4(MUTED, MUTED)

      if btn.bg then
        if collapsed then
          btn.bg:SetColorTexture(0, 0, 0, 0.26)
        else
          btn.bg:SetColorTexture(ar, ag, ab, 0.13)
        end
      end

      if btn.line then
        if collapsed then
          btn.line:SetColorTexture(1, 1, 1, 0.06)
        else
          btn.line:SetColorTexture(ar, ag, ab, 1)
        end
      end

      if btn.text then
        btn.text:SetText(collapsed and "+" or "-")
        if collapsed then
          btn.text:SetTextColor(mr, mg, mb, 1)
        else
          btn.text:SetTextColor(1, 1, 1, 1)
        end
      end
    end

    function UI:_SetOverviewAboutCollapsed(collapsed)
      UI._ovAboutCollapsed = collapsed and true or false

      if about and about.SetHeight then
        about:SetHeight(UI._ovAboutCollapsed and 32 or 165)
      end

      if aboutDiv then
        if UI._ovAboutCollapsed then aboutDiv:Hide() else aboutDiv:Show() end
      end
      if aboutScroll then
        if UI._ovAboutCollapsed then aboutScroll:Hide() else aboutScroll:Show() end
      end

      if UI._ovAboutCollapsed then
        for i = 1, #aboutDrops do
          local d = aboutDrops[i]
          if d.btn then d.btn:Hide() end
          if d.body then d.body:Hide() end
        end
      end

      if UI._PaintOverviewAboutCollapseButton then UI:_PaintOverviewAboutCollapseButton() end

      if UI._UpdateOverviewAboutTabs then UI:_UpdateOverviewAboutTabs() end
      if UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
      if UI._LayoutOverviewRecentRunsColumns then UI:_LayoutOverviewRecentRunsColumns() end

      if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
          if UI and UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
          if UI and UI._LayoutOverviewRecentRunsColumns then UI:_LayoutOverviewRecentRunsColumns() end
        end)
      end
    end

    function UI:_UpdateOverviewAboutTabs()
      local activeTab = UI._ovAboutActiveTab or "about"
      local ar, ag, ab = C4(ACCENT, ACCENT)
      local tr, tg, tb = C4(TEXT, TEXT)
      local mr, mg, mb = C4(MUTED, MUTED)
      local br, bg, bb = C4(BORDER, BORDER)
      local activeDrop = nil

      local function PaintTab(btn, active)
        if not btn then return end

        if btn.bg then
          if active then
            btn.bg:SetColorTexture(ar, ag, ab, 0.13)
          else
            btn.bg:SetColorTexture(0, 0, 0, 0.26)
          end
        end

        if btn.activeLine then
          if active then
            btn.activeLine:SetColorTexture(ar, ag, ab, 1)
            btn.activeLine:Show()
          else
            btn.activeLine:Hide()
          end
        end

        if btn.topSoftLine then
          if active then
            btn.topSoftLine:SetColorTexture(ar, ag, ab, 0.18)
          else
            btn.topSoftLine:SetColorTexture(1, 1, 1, 0.035)
          end
        end

        if btn.text then
          if active then
            btn.text:SetTextColor(tr, tg, tb)
          else
            btn.text:SetTextColor(mr, mg, mb)
          end
        end
      end

      PaintTab(about._wfTabAbout, activeTab == "about")
      PaintTab(about._wfTabFAQ, activeTab == "faq")
      PaintTab(about._wfTabChangelog, activeTab == "changelog")
      if UI._PaintOverviewAboutCollapseButton then UI:_PaintOverviewAboutCollapseButton() end

      if UI._ovAboutCollapsed then
        for i = 1, #aboutDrops do
          local d = aboutDrops[i]
          if d.ApplyTitleStyle then d.ApplyTitleStyle() end
          d.btn:Hide()
          d.body:Hide()
        end
        return
      end

      for i = 1, #aboutDrops do
        local d = aboutDrops[i]
        if d.tab == activeTab and d.open then
          activeDrop = d
          break
        end
      end

      for i = 1, #aboutDrops do
        local d = aboutDrops[i]
        if d.ApplyTitleStyle then d.ApplyTitleStyle() end

        if d.tab ~= activeTab then
          d.btn:Hide()
          d.body:Hide()
        elseif activeDrop then
          d.btn:SetShown(d == activeDrop)
          if d == activeDrop then d.body:Show() else d.body:Hide() end
        else
          d.btn:Show()
          d.body:Hide()
        end
      end
    end

    local function _ScrollWheelSB(sf, delta)
      local sb = sf and sf.ScrollBar
      if not sb or not sb.GetValue then return end
      local cur = sb:GetValue() or 0
      local minV, maxV = sb:GetMinMaxValues()
      local step = 28
      local nxt = cur - (delta * step)
      if nxt < minV then nxt = minV end
      if nxt > maxV then nxt = maxV end
      sb:SetValue(nxt)
    end

    about:EnableMouseWheel(true)
    about:SetScript("OnMouseWheel", function(_, delta) _ScrollWheelSB(aboutScroll, delta) end)
    aboutScroll:EnableMouseWheel(true)
    aboutScroll:SetScript("OnMouseWheel", function(_, delta) _ScrollWheelSB(aboutScroll, delta) end)
    aboutScroll:HookScript("OnSizeChanged", function()
      if UI and UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
    end)

    -- FIX: layout dropdowns by absolute y-offset so open bodies never indent later headers
    function UI:_LayoutOverviewAbout()
      if not aboutSC then return end

      if UI._ovAboutCollapsed then
        for i = 1, #aboutDrops do
          local d = aboutDrops[i]
          if d.btn then d.btn:Hide() end
          if d.body then d.body:Hide() end
        end
        aboutSC:SetHeight(1)
        if aboutScroll and aboutScroll.UpdateScrollChildRect then
          aboutScroll:UpdateScrollChildRect()
        end
        return
      end

      local w = aboutScroll:GetWidth() or 0
      if w > 1 then
        aboutSC:SetWidth(w)
      end

      -- About / FAQ / Changelog spacing.
      -- Keep this subtle: enough padding to breathe without making the body text feel detached.
      local bodyLeft = 22
      local bodyRight = 8
      local titleH = 18
      local titleGap = 7
      local bodyBottomGap = 11
      local bodyW = math.max(1, w - bodyLeft - bodyRight)

      local y = -2
      for i = 1, #aboutDrops do
        local d = aboutDrops[i]

        if d.tab ~= (UI._ovAboutActiveTab or "about") or not d.btn:IsShown() then
          d.body:Hide()
        else
          d.btn:ClearAllPoints()
          d.btn:SetPoint("TOPLEFT", aboutSC, "TOPLEFT", 6, y)
          d.btn:SetPoint("TOPRIGHT", aboutSC, "TOPRIGHT", -6, y)
          y = y - titleH - titleGap

          d.body:ClearAllPoints()
          if d.open then
            d.body:Show()
            if d.body.SetWidth then d.body:SetWidth(bodyW) end
            d.body:SetPoint("TOPLEFT", aboutSC, "TOPLEFT", bodyLeft, y)

            local bh = (d.body.GetStringHeight and d.body:GetStringHeight()) or 0
            bh = bh + 2
            if bh < 14 then bh = 14 end

            y = y - bh - bodyBottomGap
          else
            d.body:Hide()
          end
        end
      end

      local h = (aboutScroll:GetHeight() or 1)
      local need = (-y) + 10
      if need > h then h = need end

      aboutSC:SetHeight(h)

      if aboutScroll.UpdateScrollChildRect then
        aboutScroll:UpdateScrollChildRect()
      end
    end

    local function MakeAboutDrop(title, body, defaultOpen, tabKey)
      local d = {}
      d.open = (defaultOpen and true) or false
      d.tab = tabKey or "about"

      d.btn = CreateFrame("Button", nil, aboutSC)
      d.btn:SetHeight(18)
      d.btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
	do
  local hl = (d.btn.GetHighlightTexture and d.btn:GetHighlightTexture()) or nil
  if hl then
    hl:ClearAllPoints()
    hl:SetPoint("TOPLEFT", d.btn, "TOPLEFT", 6, 0)
    hl:SetPoint("BOTTOMRIGHT", d.btn, "BOTTOMRIGHT", -6, 0)

    -- shave off the hard edge baked into the texture so it fades cleanly
    if hl.SetTexCoord then hl:SetTexCoord(0.02, 0.98, 0, 1) end

    -- remove baked-in yellow so the tint reads true-blue
    if hl.SetDesaturation then
      hl:SetDesaturation(1)
    elseif hl.SetDesaturated then
      hl:SetDesaturated(true)
    end

    local ar, ag, ab = C4(ACCENT, ACCENT)
    hl:SetVertexColor(ar, ag, ab, 0.22)
  end
end


      d.arrow = d.btn:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
      d.arrow:SetPoint("LEFT", 0, 0)
      d.arrow:SetTextColor(C4(MUTED, MUTED))

      d.title = d.btn:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      d.title:SetPoint("LEFT", d.arrow, "RIGHT", 6, 0)
      d.title:SetPoint("RIGHT", 0, 0)
      d.title:SetJustifyH("LEFT")
      d.title:SetTextColor(C4(TEXT, TEXT))
      d.title:SetText(title)

      local function ApplyTitleStyle()
        if d.open then
          -- Title centered, minus sits immediately left of it
          d.title:ClearAllPoints()
          d.title:SetPoint("CENTER", d.btn, "CENTER", 6, 0)
          d.title:SetJustifyH("CENTER")
          d.title:SetTextColor(C4(ACCENT, ACCENT))
          d.arrow:ClearAllPoints()
          d.arrow:SetPoint("RIGHT", d.title, "LEFT", -2, 0)
          d.arrow:SetText("-")
          d.arrow:SetTextColor(C4(MUTED, MUTED))
        else
          d.arrow:ClearAllPoints()
          d.arrow:SetPoint("LEFT", 0, 0)
          d.arrow:SetText("+")
          d.arrow:SetTextColor(C4(MUTED, MUTED))
          d.title:ClearAllPoints()
          d.title:SetPoint("LEFT", d.arrow, "RIGHT", 6, 0)
          d.title:SetPoint("RIGHT", 0, 0)
          d.title:SetJustifyH("LEFT")
          d.title:SetTextColor(C4(TEXT, TEXT))
        end
      end

      d.body = MakeBodyText(aboutSC, body)
      d.body:SetJustifyH("LEFT")
      if d.body.SetWordWrap then d.body:SetWordWrap(true) end
      d.body:SetTextColor(C4(TEXT, TEXT))

      local function Apply()
        -- Close all other drops in the same tab first
        for i = 1, #aboutDrops do
          local other = aboutDrops[i]
          if other ~= d and other.tab == d.tab then
            other.open = false
            other.body:Hide()
            -- Reset other titles to default style
            other.arrow:SetText("+")
            other.arrow:SetTextColor(C4(MUTED, MUTED))
            other.title:ClearAllPoints()
            other.title:SetPoint("LEFT", other.arrow, "RIGHT", 6, 0)
            other.title:SetPoint("RIGHT", 0, 0)
            other.title:SetJustifyH("LEFT")
            other.title:SetTextColor(C4(TEXT, TEXT))
          end
        end

        if d.open then d.body:Show() else d.body:Hide() end
        ApplyTitleStyle()

        if UI and UI._UpdateOverviewAboutTabs then UI:_UpdateOverviewAboutTabs() end
        if UI and UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
      end

      d.btn:SetScript("OnClick", function()
        d.open = not d.open
        Apply()
      end)

      d.ApplyTitleStyle = ApplyTitleStyle
      aboutDrops[#aboutDrops + 1] = d
      Apply()
      return d
    end

MakeAboutDrop("Overview",
  [=[Overview is the first page you see when you open the addon. It brings recent runs, best farms, value cards, and utility tools together in one place.

Pressing the top card in Overview flips the page between GPH and LIV. Gold per Hour (GPH) is best for steady material farms where income is more predictable. RNG heavy farms like patterns, pets, mounts, and gear are better measured with Looted Item Value (LIV).

Recent Runs gives you a quick view of your latest saved runs without opening the full History page. Right click a run for a full context menu including pin to top, rename, view in History, and remove. Shift click any run to share it directly. Right click the Mode column to filter Recent Runs by profile, so you are only comparing runs that were set up the same way.

The right side of Overview holds the utility tools: Random Farm, Top Farms, WoW Token, Bag Worth, and the Farm Timer.]=],
  false
)

MakeAboutDrop("Overview Utilities",
  [=[
Random Farm helps when you do not want to keep repeating the same farms. You can randomize your next farm, use filters to narrow what can be picked, and let the addon load that farm into your Farm Window for you. When you start the session from that setup, the run can be saved under the picked farm instead of falling back to a generic zone name.

Top Farms shows your best saved farms based on the current Overview metric. When you flip the top card between GPH and LIV, Top Farms follows that metric so the list matches how you are judging the page.

Farm Timer is built for grouped farms that run on repeat cycles, especially kill, loot, and reset style farms. You can set up the cycle around how your group farms, such as killing for 1:45, looting for 15 seconds, then taking a short reset break before the next pull. Group announcements can call out the current phase so everyone knows when to kill, loot, reset, or get ready for the next cycle.

Bag Worth manually checks the value of items detected by your current filters. You can compare your character bags, personal bank, and guild bank, then use the breakdown window to see where your value is coming from. The result respects the filters you set, so ignored qualities and item types do not get mixed into the total.

WoW Token gives you a quick token reference without leaving the addon.]=],
  false
)

	MakeAboutDrop("Farms",
  [=[The Farms page is your route library. Expansion, Type, and Group filters let you go from a huge list to a short set of believable options without the page feeling noisy. Search is faster when you already know the name or zone. The filters are better when you know the kind of farm but not the exact route yet.

Right click any farm row for the full action menu including Edit, Share, Waypoint, Archive, and Delete. The Details column has a tooltip so you can get a quick reminder of what a farm is for without opening it. The Zone column shows coordinates so you can see where the route actually lives at a glance.

Right click the GPH column header to view averages for a specific profile. That means you can compare speed sets, route variations, or different profiles without mixing farming rules together.

To turn a saved session into a reusable farm, open History, right click the session, and choose Promote to Farm. The session needs waypoint or location data so the addon knows where the farm belongs. Promoting creates or updates the Farm entry, links that History run to it, and recalculates the farm averages so it can start tracking accurately instead of staying as a loose session.

After promoting, open the Farms page and use Edit if you need to clean up the name, expansion, type, aim, notes, or waypoint. Once the farm is in your library and tagged correctly, Random Farm can include it when your filters match.

Archiving is for farms you do not want cluttering the active library but still want to keep the data for. The Resync button forces farm averages to recalculate from your actual History if the numbers ever drift out of sync.]=],
  false
)

	MakeAboutDrop("Farm Window",
  [=[The Farm Window is the tracker you use while a run is active. It shows your current farm, timer, raw gold, selected profile, session controls, and the loot window with tracked items and values.

In Compact layout, the Start button toggles between Start, Pause, and Resume. Shift click Start to hard stop and save immediately instead of pausing. The Stop button pauses by default, and Shift click Stop ends the run. When a session is ended, it saves straight to History as long as it meets the requirements you set in Settings.

The gear icon opens Settings from the Farm Window, so you can adjust tracking rules, price source, history requirements, and notifications without leaving the tracker.

The layout icon controls the Farm Window layout and related tracking windows. Shift click it to change the Farm Window layout. Left click it to open the Compare window, and right click it to open the Competitive window. Compare and Competitive let you monitor side by side data without interrupting the active session.

Right click the profile bar to cycle profiles backwards instead of forwards. This lets you swap farming setups from the tracker without opening Settings.

While a session is running, you can right click the Item column header in the loot window to manually add an item you missed. You can also right click a tracked item to delete it from the current session if something was captured by mistake. These tools help clean up small tracking issues before the run gets saved to History.]=],
  false
)

	MakeAboutDrop("Competitive",
  [=[Competitive is the live challenge window for farming races. Open it from the Farm Window by right clicking the layout icon, then use it when you want a shared match instead of separate solo sessions.

The host creates the session, invites players, and starts the match. When the match starts, the host's active profile and price source become the official scoring rules, so everyone is judged under the same setup instead of mixing Vendor, TSM, Auctionator, or different loot filters together.

The Leaderboard tab shows the session state, host, required profile, scoring context, player rows, and match controls. The Loot tab shows the competitive loot breakdown so you can see which drops are carrying the score.

Invites can be sent from the Competitive window, and players can join by invite or by session details when your Competitive Settings allow it. The settings gear in the Competitive window controls invite behavior, open joining, and the shared challenge channel fallback.

During an active Competitive match, the Farm Window protects the shared run by locking normal controls where needed. The host controls the race, and participants stay synced instead of accidentally starting, stopping, or resetting their own separate session.]=],
  false
)

    MakeAboutDrop("History",
  [=[History is the audit page. When a number looks wrong, this is where you go to understand what actually drove it. Clicking a session opens the details side, which shows the full breakdown: date, duration, items looted, item types, raw gold, looted value, and the run's actual GPH.

Right click any item in the top loot list to delete it from the session view, ignore it, or both. Right click the Top Loot header to see what has been ignored or deleted. Shift click the Top Loot header to jump straight to the ignored view without going through the menu.

Right click the Mode column to filter the full list by profile. This is useful when you only want to compare sessions that were run under the same farming setup.

Right click Clear History to open targeted cleanup options. You can delete runs under a certain time, under a certain GPH, or under a certain LIV instead of wiping everything at once.

Sessions can be merged together when two runs really belong as one. In the merge menu, hold Shift to narrow the candidate list to sessions from the same zone only, which makes it easier to find the right match without scrolling through unrelated runs.

You can also share sessions from History. Right clicking a run gives you both the full share and a shorter summary version depending on how much detail you want to send.]=],
  false
)
	MakeAboutDrop("Notepad",
  [=[Notepad is for route reminders, setup notes, farm ideas, item links, and anything else you want to keep connected to your farming.

Use the note list on the left to find saved notes, and the editor on the right to write or update them. Search helps once the list starts filling up, and right click a note for extra options like duplicate or delete.

While the editor is active, you can Shift click supported links into a note. This makes it easier to save item references, route reminders, or useful details without trying to remember them later.]=],
  false
)


MakeAboutDrop("Settings",
  [=[Settings control how What Farm? records, filters, alerts, displays, and cleans up your data. This is where you set the rules the addon follows while tracking sessions.

The Window section controls how the Farm Window looks and behaves. Window Layout changes the tracker style, and the Pill options choose which value cards appear in the window, such as Time, Estimated GPH, or Looted Item Value. You can enable a fourth pill if you want more data visible at once. Window options also control things like the minimap button, double-tap keybind, auto-start, auto-resume on loot, locking mode changes while recording, auto-splitting when mode changes, and UI Scale.

History settings control the requirements a session has to meet before it saves. Minimum session duration, minimum GPH, minimum LIV, and session pruning help keep accidental starts, test runs, and low-value junk data out of your long term averages. These settings are your first safeguard against messy History data.

Loot Tracking controls what gets captured during a session. Minimum value, quality floor, category toggles, and the soulbound filter all decide what makes it into your saved data and what gets ignored. Getting these rules right early keeps your History cleaner, because bad data is easier to prevent than it is to fix later.

What Farm? also gives you cleanup safeguards when something slips through. While a session is running, you can right click a tracked item in the Farm Window to delete it from the current run before it saves. In History, you can remove or ignore individual items from a saved session, and Clear History has targeted cleanup options instead of forcing you to wipe everything at once.

Notifications let you choose when What Farm? alerts you about valuable drops. You can set the gold threshold, then control chat messages, sounds, and raid warning toasts separately. Use the Test button to preview the notification before committing to the setup.]=],
  false
)

MakeAboutDrop("Sessions",
  [=[Sessions are the saved runs created by the Farm Window. A session starts when you press Start, records what happens during the run, and becomes part of your data once it is saved.

Each saved session can include loot, raw gold, time, mode, price source, profile, and farm assignment. That saved data is what powers your Recent Runs, History details, farm averages, and long term comparisons.

When a session is saved, Overview can show it in Recent Runs, History can show the full breakdown, and the Farms page can use it when recalculating averages.

Profiles help keep sessions separated by setup. A transmog run, material run, and raw gold run can all be useful, but they should not always be compared under the same rules.

Good session data starts before the run begins. Choosing the right farm, profile, price source, and loot tracking rules makes the saved result cleaner and saves cleanup later.]=],
  false
)

MakeAboutDrop("Profiles",
  [=[Profiles are saved farming setups. Each profile can store its own loot rules, price source, quality filters, category toggles, and capture settings so you can switch between Transmog, Mats, Raw Gold, or any custom setup without rebuilding everything.

The profile bar in the Farm Window cycles through your profiles in one click. Right click the profile bar to cycle backwards instead of forwards.

Profiles matter because they keep your data honest. A fast raw gold farm, a slow transmog farm, and a material farm can all be valuable, but they should not always be compared under the same rules.

Several pages let you filter or compare by profile. Recent Runs can be filtered from the Mode column, History can be filtered by profile, and Farms can show profile specific averages from the GPH header. This keeps your results tied to the setup that actually created them.]=],
  false
)

MakeAboutDrop("Pricing",
  [=[Pricing decides how item values are calculated across What Farm?. This affects Overview, Recent Runs, History, Farms, Bag Worth, notifications, GPH, LIV, and any place where the addon needs to turn an item into a gold value.

Vendor is the safest and most stable source. It uses the value an item has when sold directly to a vendor, so it does not depend on auction scans, market movement, or another addon. It is best for raw gold farming and for players who want conservative values that are always immediately real. The downside is that it can undervalue items that are worth much more on the auction house.

Auctionator uses your latest auction house scan data. This is better when you care about what items may sell for on your realm, but it depends on your scans being current. If your scan data is old, missing, or inflated, your session values can look wrong.

TSM is the strongest option if you already use TSM for gold-making. It can use market-based and regional pricing, which is useful for transmog, recipes, pets, materials, and other auction-focused farms. It is powerful, but your results depend on the TSM price string and the data behind it.

The same session can look very different depending on the Price Source you choose. A raw gold run usually makes the most sense with Vendor, while an RNG or transmog run may make more sense with Auctionator or TSM. Pick the source that matches what you are trying to measure, because that choice decides how What Farm? values the run.]=],
  false
)
    MakeAboutDrop("What does What Farm? track?",
  [=[What Farm? tracks farming sessions, gold per hour, looted item value, recent runs, farm history, route notes, bag value, and utility tools like token tracking and random farm picking.

The main idea is simple: start a run, farm, stop the run, then compare that session against your own history instead of guessing whether the route was good.]=],
  false,
  "faq"
)

    MakeAboutDrop("How are item values calculated?",
  [=[Item values follow the price source you pick. Vendor uses immediate sell value. Auctionator uses your local auction scan data. TSM uses your TSM pricing string, which is usually better for market-focused farming if your TSM setup is current.

Changing the price source can change how strong or weak a run looks, so pick the source that matches the kind of farming you are doing.]=],
  false,
  "faq"
)

    MakeAboutDrop("Why is an item showing 0 gold?",
  [=[An item usually shows 0 gold when the addon cannot find usable pricing for it, the selected price source has no data for it, the item is filtered out, or the item does not have a meaningful market/vendor value.

For Auctionator or TSM pricing, make sure your price data is current. Old or missing scan data can make good items look worthless until your source has a usable value.]=],
  false,
  "faq"
)

    MakeAboutDrop("What is Bag Worth?",
  [=[Bag Worth estimates the value of items currently in your bags, bank, and guild bank based on your active filters and price source.

It is a snapshot of what you are holding right now. It is not the same thing as session profit, and it does not mean every item will instantly sell for that amount.]=],
  false,
  "faq"
)

    MakeAboutDrop("Why does Bag Worth not match my farm total?",
  [=[Bag Worth checks what is currently in your bags and storage. Farm totals track what was captured during a specific session.

Those numbers can differ when you sell items, move items, delete items, open containers, change filters, change price source, or scan your bank and guild bank separately.]=],
  false,
  "faq"
)

    MakeAboutDrop("How do filters work?",
  [=[Filters decide which items count toward your totals. Quality filters control item rarity. Category filters control item types like gear, pets, patterns, consumables, gems, materials, and other tracked item groups.

Use tighter filters for clean data and looser filters when you want the addon to show more of what you picked up.]=],
  false,
  "faq"
)

    MakeAboutDrop("Does What Farm? work with ElvUI?",
  [=[Yes. What Farm? detects ElvUI and uses compatible styling where needed while keeping its own What Farm? look.

The goal is for the addon to feel consistent with itself without fighting your UI setup.]=],
  false,
  "faq"
)

    MakeAboutDrop("Where should I start?",
  [=[Start with the Tutorial if you want the guided version. Start with the Farm Window if you already know the basics and just want to run a session.

After a few runs, use Overview and History to compare results, then tighten your profiles and filters once you know what kind of farming you care about most.]=],
  false,
  "faq"
)

    MakeAboutDrop("6/12/26 Reset Breakdown + Merge Polish",
  [=[This update adds automatic instance reset breakdown support, improves merged session handling, cleans up History details, and adds more notification sound options.

New:
- Sessions now automatically record instance reset breakdown data when resets occur.
- Reset data is stored inside the saved History session instead of creating separate session entries.
- Session Details now shows a dedicated Reset Breakdown section when reset data exists.
- Reset Breakdown shows total resets, average length, average GPH, and each reset's length, GPH, LIV, and item count.
- Added a Group Top Loot by reset setting.
- When enabled, Top Loot can be viewed grouped by reset.
- When disabled, Top Loot stays as one flat session list while reset stats are still saved.
- Added Auction Window Open as a valuable loot notification sound using LibSharedMedia.

Improvements:
- Merged sessions now preserve reset breakdowns.
- Reset sections are automatically renumbered after merging sessions.
- Merge picker value display now matches the active History value metric.
- Merge candidate rows no longer repeat the profile/mode text.
- Merge picker rows now stay on one line instead of wrapping long profile names.
- Merge picker header now separates mode/filter info more clearly.
- Added What Farm color formatting to merge picker labels and values.
- Improved reset breakdown spacing in Session Details so it reads like its own section.
- Improved grouped Top Loot layout, reset header alignment, and expand/collapse behavior.
- Grouped Top Loot sorting now sorts items inside each reset group instead of moving items between reset groups.
- Large Top Loot values now use cleaner comma formatting.
- Reset header values now use compact formatting with full values available where needed.

Fixes:
- Fixed Reset 1 sometimes showing 0g or missing items because the baseline was created too late.
- Fixed reset LIV displaying like an hourly rate instead of actual looted item value.
- Fixed merged reset breakdown data not being preserved correctly.
- Fixed grouped Top Loot item sorting jumping across reset groups.
- Fixed quality/icon display issues in grouped Top Loot rows.
- Fixed reset header clipping with large GPH/LIV values.
- Removed the unnecessary total reset time line from Session Details.
- Removed noisy reset tooltip behavior from History rows.
- Fixed shared tooltip styling so What Farm no longer forces other game/addon tooltips into a solid black backdrop.
- Fixed notification sound playback for the Auction Window Open sound by registering and fetching it through LibSharedMedia.]=],
  false,
  "changelog"
)

    MakeAboutDrop("6/7/26 Beta Release - v0.1.0",
  [=[What Farm? is now in beta release. This version is meant to get the addon into players' hands while the remaining polish issues are cleaned up through real use and feedback.

Core farming tools are included: session tracking, loot tracking, raw gold, total value, GPH, LIV, History, Farms, Recent Runs, Bag Worth, farm utilities, profiles, settings, sharing, competitive, and the tutorial.

Note about the farm library: The farm library is currently not populated, I will be adding entries as time goes on, but for now you are still able to create your own farm and track the average GPH yourself by right clicking a session in your history, and promoting it to farm. 

Known beta notes: some UI scaling, resize behavior, tutorial positioning, and layout polish may still vary depending on screen size, UI scale, and addon setup.

If you run into a bug, please report it on CurseForge with what window you were using, what happened, and your UI scale or screen resolution if possible.]=],
  false,
  "changelog"
)
    if UI and UI._SetOverviewAboutCollapsed then
      UI:_SetOverviewAboutCollapsed(UI._ovAboutCollapsed and true or false)
    elseif UI and UI._UpdateOverviewAboutTabs then
      UI:_UpdateOverviewAboutTabs()
    end

if C_Timer and C_Timer.After then
      C_Timer.After(0, function()
        if UI and UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
        if UI and UI._LayoutOverviewRecentRunsColumns then UI:_LayoutOverviewRecentRunsColumns() end
      end)
      C_Timer.After(0.1, function()
        if UI and UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
        if UI and UI._LayoutOverviewRecentRunsColumns then UI:_LayoutOverviewRecentRunsColumns() end
      end)
    end

    -- ---------------------------------------------------
    -- Recent Runs rows (clean + 2-line)
    -- ---------------------------------------------------
    local rows = {}
    local rowH = 26

    function UI:_LayoutOverviewRecentRunsColumns()
      local rh = UI._ovRunsHeader or runsHeader
      if not rh then return end

      local width = (rh.GetWidth and rh:GetWidth()) or 0
      if (not width or width < 1) and scroll and scroll.GetWidth then
        width = scroll:GetWidth() or 0
      end
      if not width or width < 1 then return end

      local s = {
        pad = 12,
        gap = 20,
        nameGap = 10,
        date = 92,
        dur = 72,
        mode = 100,
        metric = 140,
        farmMin = 140,
      }

      local innerWidth = math.max(0, width - (s.pad * 2))
      local farmWidth = innerWidth - (s.date + s.dur + s.mode + s.metric + (s.gap * 3) + s.nameGap)

      local function _shrink(key, minValue, need)
        if need <= 0 then return need end
        local cur = s[key] or 0
        if cur <= minValue then return need end
        local take = math.min(cur - minValue, need)
        s[key] = cur - take
        return need - take
      end

      if farmWidth < s.farmMin then
        local need = s.farmMin - farmWidth
        need = _shrink("mode", 74, need)
        need = _shrink("metric", 112, need)
        need = _shrink("gap", 10, need)
        need = _shrink("dur", 58, need)
        need = _shrink("date", 78, need)
        need = _shrink("nameGap", 6, need)
      end

      if rh.time then
        rh.time:ClearAllPoints()
        rh.time:SetPoint("RIGHT", rh, "RIGHT", -s.pad, 0)
        rh.time:SetWidth(s.date)
      end
      if rh.dur and rh.time then
        rh.dur:ClearAllPoints()
        rh.dur:SetPoint("RIGHT", rh.time, "LEFT", -s.gap, 0)
        rh.dur:SetWidth(s.dur)
      end
      if rh.mode and rh.dur then
        rh.mode:ClearAllPoints()
        rh.mode:SetPoint("RIGHT", rh.dur, "LEFT", -s.gap, 0)
        rh.mode:SetWidth(s.mode)
      end
      if rh.metric and rh.mode then
        rh.metric:ClearAllPoints()
        rh.metric:SetPoint("RIGHT", rh.mode, "LEFT", -s.gap, 0)
        rh.metric:SetWidth(s.metric)
      end
      if rh.farm and rh.metric then
        rh.farm:ClearAllPoints()
        rh.farm:SetPoint("LEFT", rh, "LEFT", s.pad, 0)
        rh.farm:SetPoint("RIGHT", rh.metric, "LEFT", -s.nameGap, 0)
      end

      if rh._btnDate and rh.time then
        rh._btnDate:ClearAllPoints()
        rh._btnDate:SetWidth(s.date + 16)
        rh._btnDate:SetPoint("CENTER", rh.time, "CENTER", 0, 0)
      end
      if rh._btnDur and rh.dur then
        rh._btnDur:ClearAllPoints()
        rh._btnDur:SetWidth(s.dur + 16)
        rh._btnDur:SetPoint("CENTER", rh.dur, "CENTER", 0, 0)
      end
      if rh._btnMode and rh.mode then
        rh._btnMode:ClearAllPoints()
        rh._btnMode:SetWidth(s.mode + 16)
        rh._btnMode:SetPoint("CENTER", rh.mode, "CENTER", 0, 0)
      end
      if rh._btnMetric and rh.metric then
        rh._btnMetric:ClearAllPoints()
        rh._btnMetric:SetWidth(s.metric + 18)
        rh._btnMetric:SetPoint("CENTER", rh.metric, "CENTER", 0, 0)
      end
      if rh._btnFarm and rh._btnMetric then
        rh._btnFarm:ClearAllPoints()
        rh._btnFarm:SetPoint("LEFT", rh, "LEFT", 0, 0)
        rh._btnFarm:SetPoint("RIGHT", rh._btnMetric, "LEFT", 0, 0)
      end

      for i = 1, #rows do
        local row = rows[i]
        if row and row.time and row.dur and row.mode and row.meta and row.name then
          row.time:ClearAllPoints()
          row.time:SetPoint("RIGHT", row, "RIGHT", -s.pad, 0)
          row.time:SetWidth(s.date)

          row.dur:ClearAllPoints()
          row.dur:SetPoint("RIGHT", row.time, "LEFT", -s.gap, 0)
          row.dur:SetWidth(s.dur)

          row.mode:ClearAllPoints()
          row.mode:SetPoint("RIGHT", row.dur, "LEFT", -s.gap, 0)
          row.mode:SetWidth(s.mode)

          row.meta:ClearAllPoints()
          row.meta:SetPoint("RIGHT", row.mode, "LEFT", -s.gap, 0)
          row.meta:SetWidth(s.metric)

          row.name:ClearAllPoints()
          row.name:SetPoint("LEFT", row, "LEFT", s.pad, 0)
          row.name:SetPoint("RIGHT", row.meta, "LEFT", -s.nameGap, 0)
        end
      end
    end

    if not UI._EnsureHistoryPopups then
    local POP_RENAME = "WF_OV_RENAME_RUN"
    local POP_NOTE   = "WF_OV_RUN_NOTE"

    function UI:_EnsureRunPopups()
      if UI._ovRunPopupsReady then return end
      UI._ovRunPopupsReady = true
      if not StaticPopupDialogs then return end

      if not StaticPopupDialogs[POP_RENAME] then
        StaticPopupDialogs[POP_RENAME] = {
          text = "Rename farm",
          button1 = "Save",
          button2 = "Cancel",
          hasEditBox = true,
          editBoxWidth = 240,
          maxLetters = 64,
          timeout = 0,
          whileDead = true,
          hideOnEscape = true,
          preferredIndex = 3,
          OnShow = function(self, data)
            if self.text then self.text:SetText("Rename farm") end
            local v = (data and data.initial) or ""
            local eb = self.editBox or self.wideEditBox
            if eb then
              eb:SetText(v)
              eb:HighlightText()
              eb:SetFocus()
            end
          end,
          OnAccept = function(self, data)
            local d = data or self.data or (self.GetParent and self:GetParent() and self:GetParent().data)
            if not d or not d.ui or not d.sess then return end
            local eb = self.editBox or self.wideEditBox
            local v = (eb and eb:GetText()) or ""
            d.ui:RenameFarmFromSession(d.sess, v)
          end,
          EditBoxOnEnterPressed = function(self)
            local parent = self:GetParent()
            if parent and parent.button1 and parent.button1:IsEnabled() then
              parent.button1:Click()
            end
          end,
          EditBoxOnEscapePressed = function(self)
            local parent = self:GetParent()
            if parent and parent.button2 then parent.button2:Click() end
          end,
        }
      end

      if not StaticPopupDialogs[POP_NOTE] then
        StaticPopupDialogs[POP_NOTE] = {
          text = "Run note",
          button1 = "Save",
          button2 = "Cancel",
          hasEditBox = true,
          editBoxWidth = 240,
          maxLetters = 160,
          timeout = 0,
          whileDead = true,
          hideOnEscape = true,
          preferredIndex = 3,
          OnShow = function(self, data)
            if self.text then self.text:SetText("Run note") end
            local v = (data and data.initial) or ""
            local eb = self.editBox or self.wideEditBox
            if eb then
              eb:SetText(v)
              eb:HighlightText()
              eb:SetFocus()
            end
          end,
          OnAccept = function(self, data)
            if not data or not data.ui or not data.sess then return end
            local eb = self.editBox or self.wideEditBox
            local v = (eb and eb:GetText()) or ""
            data.ui:SetHistorySessionNote(data.sess, v)
          end,
          EditBoxOnEnterPressed = function(self)
            local parent = self:GetParent()
            if parent and parent.button1 and parent.button1:IsEnabled() then
              parent.button1:Click()
            end
          end,
          EditBoxOnEscapePressed = function(self)
            local parent = self:GetParent()
            if parent and parent.button2 then parent.button2:Click() end
          end,
        }
      end
    end

-- Safe display name helper (Overview uses this for the Rename popup default text)
function UI:GetSessionDisplayName(sess)
  if not sess then return nil end

  -- 1) Session-level custom name wins
  local n = sess._wfCustomFarmName or sess.farmName
  if type(n) == "string" then
    n = _Trim(n)
    if n ~= "" then return n end
  end

  -- 2) If session is bound to a saved farm, prefer the farm name
  if WF and WF.DB and WF.DB.GetFarm and sess.farmId then
    local farm = WF.DB:GetFarm(sess.farmId)
    if farm then
      local fn = farm.name
      if type(fn) == "string" then
        fn = _Trim(fn)
        if fn ~= "" then return fn end
      end

      local fz = farm.zone
      if type(fz) == "string" then
        fz = _Trim(fz)
        if fz ~= "" then return fz end
      end
    end
  end

  -- 3) Fallback to zone
  local z = sess.zone
  if type(z) == "string" then
    z = _Trim(z)
    if z ~= "" then return z end
  end

  return "Farm"
end


    function UI:PromptRenameFarm(sess)
      if not sess then return end
      self:_EnsureRunPopups()
      if not StaticPopup_Show then return end

      local data = {
        ui = self,
        sess = sess,
        initial = tostring(self:GetSessionDisplayName(sess) or ""),
      }

      local frame = StaticPopup_Show(POP_RENAME, nil, nil, data)
      if frame then frame.data = data end
    end

    function UI:PromptHistorySessionNote(sess)
      if not sess then return end
      self:_EnsureRunPopups()
      if not StaticPopup_Show then return end
      StaticPopup_Show(POP_NOTE, nil, nil, {
        ui = self,
        sess = sess,
        initial = tostring(sess.note or ""),
      })
    end

    function UI:RenameFarmFromSession(sess, newName)
      if not sess then return end
      local v = _Trim(newName)
      if v == "" then return end

      -- Prefer renaming the underlying saved farm (so it reflects everywhere in History).
      if WF and WF.DB then
        -- If this session isn't bound to a farm yet but has coords, create/upsert one first.
        if (not sess.farmId) and sess.mapId and sess.x and sess.y then
          local id, farm = WF.DB:UpsertFarm({
            mapId = sess.mapId,
            x = sess.x,
            y = sess.y,
            zone = sess.zone,
            name = v,
          })
          sess.farmId = id
        end

        if sess.farmId and WF.DB.GetFarm then
          local farm = WF.DB:GetFarm(sess.farmId)
          if farm then
            -- Preserve the original name for reset (stored on the farm, not the session).
            if not farm._wfOrigName then
              local orig = tostring(farm.name or farm.zone or sess.zone or "")
              orig = _Trim(orig)
              if orig ~= "" then
                farm._wfOrigName = orig
              end
            end

            farm.name = v
            farm.updatedAt = SafeNow()
          else
            sess._wfCustomFarmName = v
          end
        else
          sess._wfCustomFarmName = v
        end
      else
        sess._wfCustomFarmName = v
      end

      sess.farmName = v

      if self.RefreshOverview then self:RefreshOverview() end
      if self.RefreshHistory then self:RefreshHistory() end
    end

    function UI:ResetFarmNameFromSession(sess)
      if not sess then return end
      if WF and WF.DB and WF.DB.GetFarm and sess.farmId then
        local farm = WF.DB:GetFarm(sess.farmId)
        if farm then
          farm.name = nil
          farm._wfOrigName = nil
          farm.updatedAt = SafeNow()
        end
      end

      if self.RefreshOverview then self:RefreshOverview() end
      if self.RefreshHistory then self:RefreshHistory() end
    end

    function UI:RenameHistorySessionName(sess, newName)
      if not sess then return end
      local v = _Trim(newName)
      if v == "" then return end

      if not sess._wfOrigFarmName then
        local orig = tostring(sess.farmName or sess.zone or "")
        if orig ~= "" then
          sess._wfOrigFarmName = orig
        end
      end

      sess.farmName = v
      sess._wfCustomFarmName = v

      if self.RefreshOverview then self:RefreshOverview() end
      if self.RefreshHistory then self:RefreshHistory() end
    end

    function UI:ResetHistorySessionName(sess)
      if not sess then return end
      if sess._wfOrigFarmName and sess._wfOrigFarmName ~= "" then
        sess.farmName = sess._wfOrigFarmName
        sess._wfCustomFarmName = nil
      end
      sess._wfOrigFarmName = nil
      sess._wfCustomFarmName = nil

      if self.RefreshOverview then self:RefreshOverview() end
      if self.RefreshHistory then self:RefreshHistory() end
    end

    function UI:SetHistorySessionNote(sess, note)
      if not sess then return end
      local v = _Trim(note)
      if v == "" then
        sess.note = nil
      else
        sess.note = v
      end

      if self.RefreshOverview then self:RefreshOverview() end
      if self.RefreshHistory then self:RefreshHistory() end
    end

    function UI:ToggleHistorySessionPinned(sess)
      if not sess then return end
      if sess.pinned then
        sess.pinned = nil
      else
        sess.pinned = true
      end

      if self.RefreshOverview then self:RefreshOverview() end
    end

    end
    -- Shared helpers (row hover + tooltips)
    local function _WF_ApplyBlueQuestHighlight(btn, alpha)
      if not btn or not btn.SetHighlightTexture then return end

      btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
      local hl = (btn.GetHighlightTexture and btn:GetHighlightTexture()) or nil
      if not hl then return end

      hl:ClearAllPoints()
      hl:SetPoint("TOPLEFT", btn, "TOPLEFT", 6, 0)
      hl:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -6, 0)

      -- shave off the hard edge baked into the texture
      if hl.SetTexCoord then
        hl:SetTexCoord(0.02, 0.98, 0, 1)
      end

      -- remove baked-in yellow so the tint reads true-blue
      if hl.SetDesaturation then
        hl:SetDesaturation(1)
      elseif hl.SetDesaturated then
        hl:SetDesaturated(true)
      end

      local ar, ag, ab
      if C4 then ar, ag, ab = C4(ACCENT, ACCENT) end
      if not ar then
        ar = (ACCENT and ACCENT[1]) or 0.25
        ag = (ACCENT and ACCENT[2]) or 0.75
        ab = (ACCENT and ACCENT[3]) or 1.00
      end

      hl:SetVertexColor(ar, ag, ab, alpha or 0.22)
    end

        -- Tooltip anchoring: keep Overview tooltips locked to the row so they behave like the rest of the addon.
    local function _WF_TooltipFollow_Stop()
    end

    local function _WF_TooltipFollow_Reposition()
    end

    local function _WF_TooltipFollow_Start()
    end

    local function _WF_TooltipAnchorRow(frame)
      if not (GameTooltip and frame) then return end
      if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
      GameTooltip:SetOwner(frame, "ANCHOR_RIGHT")
      if GameTooltip.SetClampedToScreen then GameTooltip:SetClampedToScreen(true) end
      if GameTooltip.ClearLines then GameTooltip:ClearLines() end
    end

    local function _WF_TooltipFlipAboveIfNeeded(frame)
    end


    local function EnsureRow(i)
      local r = rows[i]
      if r then return r end

      r = CreateFrame("Button", nil, sc)
      r:SetHeight(rowH)
      r:SetPoint("LEFT", 0, 0)
      r:SetPoint("RIGHT", 0, 0)
      r:RegisterForClicks("LeftButtonUp", "RightButtonUp")
      _WF_ApplyBlueQuestHighlight(r, 0.22)

      r.bg = r:CreateTexture(nil, "BACKGROUND")
      r.bg:SetAllPoints(r)
      r.bg:SetTexture("Interface/Buttons/WHITE8x8")
      r.bg:SetAlpha(0)

      r.name = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      r.name:SetPoint("LEFT", 12, 0)
      r.name:SetTextColor(C4(TEXT, TEXT))
  r.name:SetWordWrap(false)
  if r.name.SetMaxLines then r.name:SetMaxLines(1) end
      r.name:SetJustifyH("LEFT")

      -- Date column
      r.time = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.time:SetPoint("RIGHT", -12, 0)
      r.time:SetTextColor(C4(MUTED, MUTED))
      r.time:SetJustifyH("CENTER")
      r.time:SetWidth(92)
      r.time:SetWordWrap(false)
      if r.time.SetMaxLines then r.time:SetMaxLines(1) end

      -- Duration column
      r.dur = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.dur:SetPoint("RIGHT", r.time, "LEFT", -20, 0)
      r.dur:SetTextColor(C4(MUTED, MUTED))
      r.dur:SetJustifyH("CENTER")
      r.dur:SetWidth(72)
      r.dur:SetWordWrap(false)
      if r.dur.SetMaxLines then r.dur:SetMaxLines(1) end

      -- Mode column (profile used when the session started)
      r.mode = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.mode:SetPoint("RIGHT", r.dur, "LEFT", -20, 0)
      r.mode:SetWidth(100)
      r.mode:SetJustifyH("CENTER")
      r.mode:SetTextColor(C4(MUTED, MUTED))
      r.mode:SetWordWrap(false)
      if r.mode.SetMaxLines then r.mode:SetMaxLines(1) end

      -- Value column (matches the header metric)
      -- Right-align within a fixed width so the column looks uniform.
      r.meta = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.meta:SetPoint("RIGHT", r.mode, "LEFT", -20, 0)
      r.meta:SetWidth(140)
      r.meta:SetJustifyH("RIGHT")
      r.meta:SetTextColor(C4(TEXT, TEXT))
      r.meta:SetWordWrap(false)
      if r.meta.SetMaxLines then r.meta:SetMaxLines(1) end


      r.name:SetPoint("RIGHT", r.meta, "LEFT", -10, 0)

r.sep = r:CreateTexture(nil, "BACKGROUND")
      r.sep:SetHeight((Widgets and Widgets.PixelSize) and Widgets:PixelSize(r) or 1)
      if Widgets and Widgets.MakePixelPerfect then Widgets:MakePixelPerfect(r.sep, r) end
      r.sep:SetPoint("BOTTOMLEFT", 12, 0)
      r.sep:SetPoint("BOTTOMRIGHT", -12, 0)
      r.sep:SetColorTexture(1, 1, 1, 0.06)

      r:SetScript("OnEnter", function(self)
          self.bg:SetAlpha(0) -- QuestTitleHighlight handles hover now (no box edges)

        local sess = self._sess
        if not sess or not GameTooltip then return end

        local metric = tostring(UI._ovMetric or "gph"):lower()
        if metric ~= "liv" then metric = "gph" end
        local metricLabel = (metric == "liv") and "LIV" or "GPH"

        local displayVal = 0
        if metric == "liv" then
          local _, _, livTotal = _SessTotals(sess)
          displayVal = livTotal
        else
          displayVal = SessionGPH(sess)
        end

        local total, gold, livTotal = _SessTotals(sess)

        _WF_TooltipAnchorRow(self)
        local z = _Trim(sess.zone or "")
        if z == "" and WF and WF.DB and WF.DB.GetFarm and sess.farmId then
          local f = WF.DB:GetFarm(sess.farmId)
          if f then z = _Trim(f.zone or "") end
        end

        local farmName = _Trim(sess._wfCustomFarmName or sess.farmName or "")
        if farmName == "" and WF and WF.DB and WF.DB.GetFarm and sess.farmId then
          local f = WF.DB:GetFarm(sess.farmId)
          if f then farmName = _Trim(f.name or "") end
        end
        if farmName == "" then farmName = z end

        local titleName = (farmName ~= "" and farmName or (z ~= "" and z or "Unknown"))

        GameTooltip:AddLine(titleName, ACCENT[1], ACCENT[2], ACCENT[3], true)
        if _Trim(sess._wfCustomFarmName or "") ~= "" then
          GameTooltip:AddLine("Zone: " .. (z ~= "" and z or "—"), 0.95, 0.95, 0.95)
        end
        GameTooltip:AddLine(metricLabel .. ": " .. Money(Util:Round(displayVal)), 0.95, 0.95, 0.95)
        -- Extra context (matches the Mode/Time columns)
        local modeText = _Trim(sess.mode or sess.profile or sess.profileName or sess.modeName or "")
        if modeText == "" then modeText = "—" end
        local durText = _SessDurationText(sess)
        if _Trim(durText) == "" then durText = "—" end
        GameTooltip:AddDoubleLine("Mode:", modeText, 0.75, 0.75, 0.75, 1, 1, 1)
        GameTooltip:AddDoubleLine("Duration:", durText, 0.75, 0.75, 0.75, 1, 1, 1)

        GameTooltip:AddLine(" ", 1, 1, 1)
        GameTooltip:AddDoubleLine("Looted Item Value:", Money(Util:Round(livTotal)), 0.75, 0.75, 0.75, 1, 1, 1)
        GameTooltip:AddDoubleLine("Looted Gold:", Money(Util:Round(gold)), 0.75, 0.75, 0.75, 1, 1, 1)
        GameTooltip:AddDoubleLine("Total Value:", Money(Util:Round(total)), 0.75, 0.75, 0.75, 1, 1, 1)

        local note = _Trim(sess.note or "")
        local isPinned = sess.pinned and true or false

        if isPinned or note ~= "" then
          GameTooltip:AddLine(" ", 1, 1, 1)
          if isPinned then
            GameTooltip:AddLine("Pinned", ACCENT[1], ACCENT[2], ACCENT[3], true)
          end
          if note ~= "" then
            GameTooltip:AddLine(note, TEXT[1], TEXT[2], TEXT[3], true)
          end
        else
          -- Keep breathing room above the click hint when there is no note/pin.
          GameTooltip:AddLine(" ", 1, 1, 1)
        end

        GameTooltip:AddLine("Click to view in History", 0.65, 0.65, 0.65)
        GameTooltip:AddLine("Shift-click to share", 0.65, 0.65, 0.65)

        GameTooltip:Show()
        _WF_TooltipFlipAboveIfNeeded(self)
      end)

      r:SetScript("OnLeave", function(self)
        self.bg:SetAlpha(0)
        if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
      end)

      r:SetScript("OnMouseUp", function(self, button)
        local sess = self._sess
        if not sess then return end

        if button == "LeftButton" and IsShiftKeyDown and IsShiftKeyDown() then
          if WF and WF.Share and WF.Share.InsertRunLink then
            WF.Share:InsertRunLink(sess)
          elseif WF and WF.Share and WF.Share.ShowShareDialog then
            WF.Share:ShowShareDialog(sess)
          end
          return
        end

        if button == "RightButton" then
          local function doRemove()
            UI:RemoveHistorySession(sess)
          end

          local function askRemove()
            if WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("confirmDestructive") then
              UI:Confirm("Remove run", "Remove this run from history? This also removes it from Recent Runs.", doRemove)
            else
              doRemove()
            end
          end

          local items = {
            { text = "View in History", fn = function()
                UI._pendingSelectSession = sess
                UI:Show("History")
                if UI.RefreshHistory then UI:RefreshHistory() end
              end
            },
            { text = (sess.pinned and "Unpin from top" or "Pin to top"), fn = function()
                UI:ToggleHistorySessionPinned(sess)
              end
            },
            { sep = true },
            { text = "Rename farm", fn = function()
                UI:PromptRenameFarm(sess)
              end
            },
          }

          do
            local hasCustom = false

            if _Trim(sess._wfCustomFarmName) ~= "" then hasCustom = true end
            if _Trim(sess._wfOrigFarmName) ~= "" then hasCustom = true end

            -- Covers older data or edge cases where farmName differs from zone (custom) but custom fields aren't present.
            if not hasCustom then
              local fn = _Trim(sess.farmName)
              local zn = _Trim(sess.zone)
              if fn ~= "" and zn ~= "" and fn ~= zn then
                hasCustom = true
              end
            end

            if hasCustom then
              items[#items + 1] = { text = "Reset farm name", fn = function()
                UI:ResetFarmNameFromSession(sess)
              end }
            end
          end

          items[#items + 1] = { text = (sess.note and "Edit note…" or "Add note"), fn = function()
            UI:PromptHistorySessionNote(sess)
          end }

          if sess.note then
            items[#items + 1] = { text = "Clear note", fn = function()
              UI:SetHistorySessionNote(sess, "")
            end }
          end

          if WF and WF.Share and WF.Share.ShowShareDialog then
            items[#items + 1] = { text = "Share", fn = function()
              WF.Share:ShowShareDialog(sess)
            end }
          end

          items[#items + 1] = { sep = true }
          items[#items + 1] = { text = "Remove run", fn = askRemove }

          UI:OpenContextMenu(items, 240)
        else
          UI._pendingSelectSession = sess
          UI:Show("History")
          if UI.RefreshHistory then UI:RefreshHistory() end
        end
      end)

      rows[i] = r
      if UI and UI._LayoutOverviewRecentRunsColumns then
        UI:_LayoutOverviewRecentRunsColumns()
      end
      return r
    end

    -- ---------------------------
    -- Right panel
    -- ---------------------------
    local right = MakePanel(pOverview, PANEL)

    -- Scroll the whole right rail so long sections can't push Token/Bag Worth off-screen.
    local rScroll = CreateFrame("ScrollFrame", nil, right, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and rScroll.ScrollBar then Widgets:SkinScrollBar(rScroll.ScrollBar) end
    rScroll:SetPoint("TOPLEFT", 6, -8)
    rScroll:SetPoint("BOTTOMRIGHT", -28, 8)

    local rSC = CreateFrame("Frame", nil, rScroll)
    rSC:SetPoint("TOPLEFT", 0, 0)
    rSC:SetSize(1, 1)
    rScroll:SetScrollChild(rSC)

    local function _ScrollWheel(scrollFrame, delta)
      local sb = scrollFrame and scrollFrame.ScrollBar
      if not sb or not sb.GetValue then return end

      local cur = sb:GetValue() or 0
      local minV, maxV = sb:GetMinMaxValues()
      local step = 28

      local nxt = cur - (delta * step)
      if nxt < minV then nxt = minV end
      if nxt > maxV then nxt = maxV end
      sb:SetValue(nxt)
    end

    right:EnableMouseWheel(true)
    right:SetScript("OnMouseWheel", function(_, delta) _ScrollWheel(rScroll, delta) end)

    rScroll:EnableMouseWheel(true)
    rScroll:SetScript("OnMouseWheel", function(_, delta) _ScrollWheel(rScroll, delta) end)

    -- Random Farm
    local rfHeader = CreateFrame("Frame", nil, rSC)
    rfHeader:SetPoint("TOPLEFT", 12, -10)
    rfHeader:SetPoint("TOPRIGHT", rSC, "TOPRIGHT", -12, -10)
    rfHeader:SetHeight(20)

    local rTitle = MakeSectionTitle(rfHeader, "Random Farm")
    rTitle:SetPoint("LEFT", 0, 0)

    -- Match Farm Timer options style (small text toggle)
    local rfOptBtn = CreateFrame("Button", nil, rfHeader)
    rfOptBtn:SetSize(64, 18)
    rfOptBtn:SetPoint("RIGHT", 0, 0)

    rfOptBtn.bg = rfOptBtn:CreateTexture(nil, "BACKGROUND")
    rfOptBtn.bg:SetAllPoints(rfOptBtn)
    rfOptBtn.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    rfOptBtn.bg:SetVertexColor(1, 1, 1, 1)
    rfOptBtn.bg:SetAlpha(0)

    rfOptBtn.txt = rfOptBtn:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    rfOptBtn.txt:SetPoint("CENTER", 0, 0)
    rfOptBtn.txt:SetText("Options")
    do
      local mr, mg, mb = C4(MUTED, MUTED)
      if rfOptBtn.txt then rfOptBtn.txt:SetTextColor(mr, mg, mb, 1) end
    end


    _WF_ApplyGearOptionButton(rfOptBtn)
    rfOptBtn:SetScript("OnEnter", function()
      _WF_GearOpt_SetHover(rfOptBtn, true)
      if GameTooltip then
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(rfOptBtn, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end
        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine("Random Farm Options", ar, ag, ab)
        GameTooltip:AddLine("Adjust the pool filters used by Random Farm.", TEXT[1], TEXT[2], TEXT[3], true)
        GameTooltip:AddLine("Notification style and sound live in Settings > Notifications.", 0.75, 0.75, 0.75, true)
        GameTooltip:Show()
        _WF_TooltipFlipAboveIfNeeded(self)
      end
    end)
    rfOptBtn:SetScript("OnLeave", function()
      _WF_GearOpt_SetHover(rfOptBtn, false)
      if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
    end)

    local rMeta = MakeMutedText(rSC, "")
    rMeta:SetPoint("TOPLEFT", rfHeader, "BOTTOMLEFT", 0, -8)
    rMeta:SetJustifyH("LEFT")

    local rfOptPanel = CreateFrame("Frame", nil, rSC, "BackdropTemplate")
    rfOptPanel:SetPoint("TOPLEFT", rMeta, "BOTTOMLEFT", 0, -10)
    rfOptPanel:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    rfOptPanel:Hide()
    rfOptPanel:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 10,
      insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    rfOptPanel:SetBackdropColor(0, 0, 0, 0.16)
    rfOptPanel:SetBackdropBorderColor(1, 1, 1, 0.10)

    local function _RF_Ensure()
      if EnsureSettingsDefaults then EnsureSettingsDefaults() end
    end

    local function _RF_Set(key, v)
      _RF_Ensure()
      if WF and WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting(key, v)
      end
      if UI and UI.RefreshOverview then
        UI:RefreshOverview()
      end
      if UI and UI._UpdateOverviewRightScroll then
        UI:_UpdateOverviewRightScroll()
      end
    end

    local comboAim = ctx.MakeCombo(
      rfOptPanel,
      "Aim",
      function() _RF_Ensure(); if WF and WF.DB and WF.DB.GetSetting then return WF.DB:GetSetting("randomAim") or "Any" end return "Any" end,
      function(v) _RF_Set("randomAim", v) end,
      function() return RF_BuildAimItems(WF, Util, ctx.FARM_AIMS) end
    )

    local comboExp = ctx.MakeCombo(
      rfOptPanel,
      "Expansion",
      function() _RF_Ensure(); if WF and WF.DB and WF.DB.GetSetting then return WF.DB:GetSetting("randomExpansion") or "Any" end return "Any" end,
      function(v) _RF_Set("randomExpansion", v) end,
      function() return RF_BuildExpansionItems(ctx.EXPANSIONS) end
    )

    local comboType = ctx.MakeCombo(
      rfOptPanel,
      "Type",
      function() _RF_Ensure(); if WF and WF.DB and WF.DB.GetSetting then return WF.DB:GetSetting("randomFarmType") or "Any" end return "Any" end,
      function(v) _RF_Set("randomFarmType", v) end,
      function() return ctx.FARM_TYPES end
    )

    comboExp._refresh = function()
      if comboExp and comboExp._txt then
        local value = "Any"
        if WF and WF.DB and WF.DB.GetSetting then
          value = WF.DB:GetSetting("randomExpansion") or "Any"
        end
        comboExp._txt:SetText(RF_ExpansionLabel(value))
      end
    end

    local chkZone = ctx.MakeCheck(
      rfOptPanel,
      "Current zone only",
      function() _RF_Ensure(); if WF and WF.DB and WF.DB.GetSetting then return WF.DB:GetSetting("randomCurrentZoneOnly") end return false end,
      function(v) _RF_Set("randomCurrentZoneOnly", v) end
    )

    local chkUnknown = ctx.MakeCheck(
      rfOptPanel,
      "Include uncategorized farms",
      function() _RF_Ensure(); if WF and WF.DB and WF.DB.GetSetting then return WF.DB:GetSetting("randomIncludeUnknown") end return false end,
      function(v) _RF_Set("randomIncludeUnknown", v) end
    )

    local function _RF_SetComboWidth(combo, w)
      if not combo then return end
      w = tonumber(w) or 240
      if combo.SetWidth then combo:SetWidth(w) end
      if combo._btn and combo._btn.SetWidth then combo._btn:SetWidth(w) end
    end

    local function _RF_Layout()
      local w = rfOptPanel:GetWidth() or 0
      local pad = 12
      local gap = 12
      local fullW = (w > 0) and (w - (pad * 2)) or 240
      if fullW < 140 then fullW = 140 end
      local colW = math.floor((fullW - gap) / 2)

      comboAim:ClearAllPoints()
      comboExp:ClearAllPoints()
      comboType:ClearAllPoints()
      chkZone:ClearAllPoints()
      chkUnknown:ClearAllPoints()

      if colW >= 150 then
        _RF_SetComboWidth(comboAim, colW)
        _RF_SetComboWidth(comboExp, colW)
        _RF_SetComboWidth(comboType, fullW)

        comboAim:SetPoint("TOPLEFT", rfOptPanel, "TOPLEFT", pad, -10)
        comboExp:SetPoint("TOPLEFT", comboAim, "TOPRIGHT", gap, 0)
        comboType:SetPoint("TOPLEFT", comboAim, "BOTTOMLEFT", 0, -10)


        chkZone:SetPoint("TOPLEFT", comboType, "BOTTOMLEFT", 0, -8)
        chkUnknown:SetPoint("TOPLEFT", comboType, "BOTTOMLEFT", colW + gap, -8)

        rfOptPanel:SetHeight(10 + 44 + 10 + 44 + 8 + 26 + 12)
      else
        _RF_SetComboWidth(comboAim, fullW)
        _RF_SetComboWidth(comboExp, fullW)
        _RF_SetComboWidth(comboType, fullW)

        comboAim:SetPoint("TOPLEFT", rfOptPanel, "TOPLEFT", pad, -10)
        comboExp:SetPoint("TOPLEFT", comboAim, "BOTTOMLEFT", 0, -10)
        comboType:SetPoint("TOPLEFT", comboExp, "BOTTOMLEFT", 0, -10)


        chkZone:SetPoint("TOPLEFT", comboType, "BOTTOMLEFT", 0, -8)
        chkUnknown:SetPoint("TOPLEFT", chkZone, "BOTTOMLEFT", 0, -8)

        rfOptPanel:SetHeight(10 + 44 + 10 + 44 + 10 + 44 + 8 + 26 + 8 + 26 + 12)
      end
    end

    rfOptPanel:SetScript("OnShow", function()
      -- Defer one frame so rSC has its real width before backdrop renders
      C_Timer.After(0, function()
        if not rfOptPanel:IsShown() then return end
        rfOptPanel:ClearAllPoints()
        rfOptPanel:SetPoint("TOPLEFT", rMeta, "BOTTOMLEFT", 0, -10)
        rfOptPanel:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
        _RF_Layout()
        if comboAim and comboAim._refresh then comboAim._refresh() end
        if comboExp and comboExp._refresh then comboExp._refresh() end
        if comboType and comboType._refresh then comboType._refresh() end
      end)
    end)
    rfOptPanel:SetScript("OnSizeChanged", function()
      if rfOptPanel:IsShown() then _RF_Layout() end
    end)

    local bRoll = Widgets:Button(rSC, "Roll + Waypoint", 160, 26)

    -- Tooltip for the Random Farm roll button
    if bRoll and bRoll.HookScript then
      bRoll:HookScript("OnEnter", function()
        if not GameTooltip then return end
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(bRoll, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end
        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine("Roll + Waypoint", ar, ag, ab)
        GameTooltip:AddLine("Rolls a random farm from your current filters.", TEXT[1], TEXT[2], TEXT[3], true)
        GameTooltip:AddLine("Sets a map waypoint to the selected farm.", 0.75, 0.75, 0.75, true)
        GameTooltip:AddLine("Tip: tweak the pool via Options.", 0.65, 0.65, 0.65, true)
        GameTooltip:Show()
        _WF_TooltipFlipAboveIfNeeded(self)
      end)
      bRoll:HookScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
      end)
    end

    local rDiv = MakeDivider(rSC)
    rDiv:SetPoint("LEFT", rSC, "LEFT", 12, 0)
    rDiv:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    rDiv:SetDrawLayer("OVERLAY", 7)
    if rDiv.SetHeight then rDiv:SetHeight(2) end
    rDiv:SetAlpha(0.30)

    local function _RF_Reflow()
      bRoll:ClearAllPoints()
      if rfOptPanel:IsShown() then
        bRoll:SetPoint("TOPLEFT", rfOptPanel, "BOTTOMLEFT", 0, -10)
      else
        bRoll:SetPoint("TOPLEFT", rMeta, "BOTTOMLEFT", 0, -10)
      end

      rDiv:ClearAllPoints()
      rDiv:SetPoint("LEFT", rSC, "LEFT", 12, 0)
      rDiv:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
      rDiv:SetPoint("TOP", bRoll, "BOTTOM", 0, -14)

      if UI and UI._UpdateOverviewRightScroll then
        UI:_UpdateOverviewRightScroll()
      end
    end

    rfOptBtn:SetScript("OnClick", function()
      if rfOptPanel:IsShown() then
        rfOptPanel:Hide()
      else
        rfOptPanel:Show()
      end
      _RF_Reflow()
    end)

    -- initial layout (panel collapsed)
    _RF_Reflow()

-- Top Farms (clean rows + hover details)
    local tTitle = MakeSectionTitle(rSC, "Top Farms")
    tTitle:SetPoint("TOPLEFT", rDiv, "BOTTOMLEFT", 0, -18)

    -- Stored for metric refresh (we render "Top Farms • <metric>")
    UI._ovTopTitle = tTitle

    local tMetric = MakeMutedText(rSC, "")
    tMetric:SetPoint("TOPRIGHT", rDiv, "BOTTOMRIGHT", 0, -18)
    tMetric:SetJustifyH("RIGHT")

    local TOP_FARMS_LIMIT = 5

    local tList = CreateFrame("Frame", nil, rSC)
    tList:SetPoint("TOPLEFT", tTitle, "BOTTOMLEFT", 0, -8)
    tList:SetPoint("TOPRIGHT", rSC, "TOPRIGHT", -12, 0)
    tList:SetHeight(108)

    -- Empty state (Top Farms)
    local emptyTop = tList:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    emptyTop:SetPoint("CENTER", tList, "CENTER", 0, 0)
    emptyTop:SetJustifyH("CENTER")
    emptyTop:SetTextColor(C4(MUTED, MUTED))
    emptyTop:SetText("No farm data yet.\nComplete a session to populate Top Farms.")
    emptyTop:Hide()
    UI._ovEmptyTopFarms = emptyTop

    local tRows = {}
    local function EnsureTopRow(i)
      if tRows[i] then return tRows[i] end

      local b = CreateFrame("Button", nil, tList)
      b:SetHeight(18)
      b:SetPoint("LEFT", 0, 0)
      b:SetPoint("RIGHT", 0, 0)
      _WF_ApplyBlueQuestHighlight(b, 0.20)

      b.rank = b:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      b.rank:SetPoint("LEFT", 0, 0)
      b.rank:SetTextColor(C4(MUTED, MUTED))
      b.rank:SetText(i .. ".")

      b.name = b:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      b.name:SetPoint("LEFT", b.rank, "RIGHT", 6, 0)
      b.name:SetTextColor(C4(TEXT, TEXT))
      b.name:SetJustifyH("LEFT")

      b.value = b:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      b.value:SetPoint("RIGHT", 0, 0)
      b.value:SetTextColor(C4(TEXT, TEXT))
      b.value:SetJustifyH("RIGHT")

      b.sep = b:CreateTexture(nil, "BACKGROUND")
      b.sep:SetHeight((Widgets and Widgets.PixelSize) and Widgets:PixelSize(b) or 1)
  if Widgets and Widgets.MakePixelPerfect then Widgets:MakePixelPerfect(b.sep, b) end
      b.sep:SetPoint("BOTTOMLEFT", 0, -2)
      b.sep:SetPoint("BOTTOMRIGHT", 0, -2)
      b.sep:SetColorTexture(1, 1, 1, 0.06)

      b:RegisterForClicks("LeftButtonUp")
      b:SetScript("OnMouseUp", function(self, button)
        if button ~= "LeftButton" then return end
        local d = self._data
        if not d or not d.bestSess then return end

        UI._pendingSelectSession = d.bestSess
        UI:Show("History")
        if UI.RefreshHistory then UI:RefreshHistory() end
      end)

      b:SetScript("OnEnter", function(self)
        local d = self._data
        if not d or not GameTooltip then return end

        _WF_TooltipAnchorRow(self)
        GameTooltip:AddLine(d.label or "Farm", ACCENT[1], ACCENT[2], ACCENT[3], true)
        local bestSess = d.bestSess
        if bestSess and _Trim(bestSess._wfCustomFarmName or "") ~= "" then
          local z = _Trim(bestSess.zone or "")
          if z == "" and WF and WF.DB and WF.DB.GetFarm and bestSess.farmId then
            local f = WF.DB:GetFarm(bestSess.farmId)
            if f then z = _Trim(f.zone or "") end
          end
          GameTooltip:AddLine("Zone: " .. (z ~= "" and z or "—"), 0.95, 0.95, 0.95)
        end
        GameTooltip:AddLine((d.metricLabel or "GPH") .. ": " .. Money(Util:Round(d.metricValue or 0)), 0.95, 0.95, 0.95)
        if bestSess then
          local modeText = _Trim(bestSess.mode or bestSess.profile or bestSess.profileName or bestSess.modeName or "")
          if modeText == "" then modeText = "—" end
          GameTooltip:AddDoubleLine("Mode:", modeText, 0.75, 0.75, 0.75, 1, 1, 1)
        end

        GameTooltip:AddLine(" ", 1, 1, 1)

        if d.bestRunText and d.bestRunText ~= "" then
          GameTooltip:AddDoubleLine("Best run:", tostring(d.bestRunText), 0.75, 0.75, 0.75, 1, 1, 1)
        end

        if d.durationText and d.durationText ~= "" then
          GameTooltip:AddDoubleLine("Duration:", tostring(d.durationText), 0.75, 0.75, 0.75, 1, 1, 1)
        end

        if d.itemValue then
          GameTooltip:AddDoubleLine("Looted Item Value:", Money(Util:Round(d.itemValue)), 0.75, 0.75, 0.75, 1, 1, 1)
        end
        if d.goldValue then
          GameTooltip:AddDoubleLine("Looted Gold:", Money(Util:Round(d.goldValue)), 0.75, 0.75, 0.75, 1, 1, 1)
        end
        if d.totalValue then
          GameTooltip:AddDoubleLine("Total Value:", Money(Util:Round(d.totalValue)), 0.75, 0.75, 0.75, 1, 1, 1)
        end
        if d.runsCount then
          GameTooltip:AddLine(" ", 1, 1, 1)
          GameTooltip:AddDoubleLine("Runs:", tostring(d.runsCount), 0.75, 0.75, 0.75, 1, 1, 1)
        end

        local note = bestSess and _Trim(bestSess.note or "") or ""
        local isPinned = bestSess and bestSess.pinned and true or false

        if isPinned or note ~= "" then
          GameTooltip:AddLine(" ", 1, 1, 1)
          if isPinned then
            GameTooltip:AddLine("Pinned", ACCENT[1], ACCENT[2], ACCENT[3], true)
          end
          if note ~= "" then
            GameTooltip:AddLine(note, TEXT[1], TEXT[2], TEXT[3], true)
          end
        else
          -- Keep the CTA separated even when there's no note/pin.
          GameTooltip:AddLine(" ", 1, 1, 1)
        end

        GameTooltip:AddLine("Click to view in History", 0.65, 0.65, 0.65)
        GameTooltip:Show()
        _WF_TooltipFlipAboveIfNeeded(self)
      end)

      b:SetScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
      end)

      tRows[i] = b
      return b
    end

    for i = 1, TOP_FARMS_LIMIT do
      local r = EnsureTopRow(i)
      r:SetPoint("TOPLEFT", 0, -((i - 1) * 22))
      r:SetPoint("TOPRIGHT", 0, -((i - 1) * 22))
      r.name:SetPoint("RIGHT", r.value, "LEFT", -10, 0) -- prevents wrapping into value
    end

    local tDiv = MakeDivider(rSC)
    tDiv:SetPoint("LEFT", rSC, "LEFT", 12, 0)
    tDiv:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    tDiv:SetPoint("TOP", tList, "BOTTOM", 0, -14)

    -- WoW Token
    local tokTitle = MakeSectionTitle(rSC, "WoW Token")
    tokTitle:SetPoint("TOPLEFT", tDiv, "BOTTOMLEFT", 0, -18)

    local tokPrice = MakeBodyText(rSC, "Price: —")
    tokPrice:SetPoint("TOPLEFT", tokTitle, "BOTTOMLEFT", 0, -10)
    tokPrice:SetJustifyH("LEFT")
    tokPrice:SetTextColor(C4(TEXT, TEXT))

    local tokUpdated = MakeMutedText(rSC, "Updated: —")
    tokUpdated:SetPoint("TOPLEFT", tokPrice, "BOTTOMLEFT", 0, -4)
    tokUpdated:SetJustifyH("LEFT")

    local tokGoal = MakeBodyText(rSC, "Per Day Goal: —")
    tokGoal:SetPoint("TOPLEFT", tokUpdated, "BOTTOMLEFT", 0, -8)
    tokGoal:SetJustifyH("LEFT")
    tokGoal:SetTextColor(C4(TEXT, TEXT))

    local tokRemain = MakeBodyText(rSC, "Remaining: —")
    tokRemain:SetPoint("TOPLEFT", tokGoal, "BOTTOMLEFT", 0, -4)
    tokRemain:SetJustifyH("LEFT")
    tokRemain:SetTextColor(C4(TEXT, TEXT))

    local bTok = Widgets:Button(rSC, "Refresh Token", 160, 26)
    bTok:SetPoint("TOPLEFT", tokRemain, "BOTTOMLEFT", 0, -8)

    local tokDiv = MakeDivider(rSC)
    tokDiv:SetPoint("LEFT", rSC, "LEFT", 12, 0)
    tokDiv:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    tokDiv:SetPoint("TOP", bTok, "BOTTOM", 0, -14)

    -- Bag Worth (manual only)

    -- Bag Worth (manual only)
    local bwHeader = CreateFrame("Frame", nil, rSC)
    bwHeader:SetPoint("TOPLEFT", tokDiv, "BOTTOMLEFT", 0, -18)
    bwHeader:SetPoint("TOPRIGHT", tokDiv, "BOTTOMRIGHT", 0, -18)
    bwHeader:SetHeight(20)

    local bwTitle = MakeSectionTitle(bwHeader, "Bag Worth")
    bwTitle:SetPoint("LEFT", 0, 0)

    local bwOptBtn = CreateFrame("Button", nil, bwHeader)
    bwOptBtn:SetSize(64, 18)
    bwOptBtn:SetPoint("RIGHT", 0, 0)

    bwOptBtn.bg = bwOptBtn:CreateTexture(nil, "BACKGROUND")
    bwOptBtn.bg:SetAllPoints(bwOptBtn)
    bwOptBtn.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    bwOptBtn.bg:SetVertexColor(1, 1, 1, 1)
    bwOptBtn.bg:SetAlpha(0)

    bwOptBtn.txt = bwOptBtn:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    bwOptBtn.txt:SetPoint("CENTER", 0, 0)
    bwOptBtn.txt:SetText("Options")
    do
      local mr, mg, mb = C4(MUTED, MUTED)
      if bwOptBtn.txt then bwOptBtn.txt:SetTextColor(mr, mg, mb, 1) end
    end


    _WF_ApplyGearOptionButton(bwOptBtn)
    bwOptBtn:SetScript("OnEnter", function()
      _WF_GearOpt_SetHover(bwOptBtn, true)
      if GameTooltip then
        GameTooltip:SetOwner(bwOptBtn, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end
        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine("Bag Worth Options", ar, ag, ab)
        GameTooltip:AddLine("Choose what Bag Worth counts when you press Refresh Worth.", TEXT[1], TEXT[2], TEXT[3], true)
        GameTooltip:Show()
      end
    end)
    bwOptBtn:SetScript("OnLeave", function()
      _WF_GearOpt_SetHover(bwOptBtn, false)
      if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
    end)

    local function _BW_Ensure()
      if EnsureSettingsDefaults then EnsureSettingsDefaults() end
    end

    local function _BW_Get(key, default)
      _BW_Ensure()
      if WF and WF.DB and WF.DB.GetSetting then
        local v = WF.DB:GetSetting(key)
        if v == nil then return default end
        return v
      end
      return default
    end

    local function _BW_Set(key, v)
      _BW_Ensure()
      if WF and WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting(key, v)
      end
    end

    local function _BW_ClearRemembered()
      if WF and WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting("bwLastPayload", nil)
        WF.DB:SetSetting("bwLastProvider", nil)
        WF.DB:SetSetting("bwLastAt", nil)
      end
      UI._bwLastPayload = nil
      if UI and UI.RefreshBagWorth then
        UI:RefreshBagWorth({ force = false })
      end
    end

    local MakeCheck = ctx.MakeCheck

    local bwOptPanel = CreateFrame("Frame", nil, rSC, "BackdropTemplate")
    bwOptPanel:SetPoint("TOPLEFT", bwHeader, "BOTTOMLEFT", 0, -4)
    bwOptPanel:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    bwOptPanel:Hide()
    bwOptPanel:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 10,
      insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    bwOptPanel:SetBackdropColor(0, 0, 0, 0.16)
    bwOptPanel:SetBackdropBorderColor(0, 0, 0, 0.55)

    -- Section label: muted text under the same WhatFarm divider style used throughout this page.
    local function MakeSectionLabel(parent, text, showRule)
      local f = CreateFrame("Frame", nil, parent)
      f:SetHeight(showRule ~= false and 18 or 14)

      if showRule ~= false then
        local div = MakeDivider(f)
        div:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
        div:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
        if div._wfSyncDividerGeometry then
          div:_wfSyncDividerGeometry()
        end
      end

      local fs = f:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      do
        local mr, mg, mb = C4(MUTED, MUTED)
        fs:SetTextColor(mr, mg, mb, 0.78)
      end
      fs:SetText(text)
      fs:SetJustifyH("LEFT")
      fs:SetPoint("TOPLEFT", f, "TOPLEFT", 0, showRule ~= false and -7 or 0)

      return f
    end

    -- Section label frames
    local lblCategories = MakeSectionLabel(bwOptPanel, "Item Types", false)
    local lblQuality    = MakeSectionLabel(bwOptPanel, "Quality")
    local lblDisplay    = MakeSectionLabel(bwOptPanel, "Display")

    -- Category checks
    local chkMats = MakeCheck(bwOptPanel, "Include Mats",
      function() return _BW_Get("bwIncludeMats", true) end,
      function(v) _BW_Set("bwIncludeMats", v) end)
    local chkGear = MakeCheck(bwOptPanel, "Include Gear",
      function() return _BW_Get("bwIncludeGear", true) end,
      function(v) _BW_Set("bwIncludeGear", v) end)
    local chkPatterns = MakeCheck(bwOptPanel, "Include Patterns",
      function() return _BW_Get("bwIncludePatterns", true) end,
      function(v) _BW_Set("bwIncludePatterns", v) end)
    local chkToys = MakeCheck(bwOptPanel, "Include Toys",
      function() return _BW_Get("bwIncludeToys", true) end,
      function(v) _BW_Set("bwIncludeToys", v) end)
    local chkPets = MakeCheck(bwOptPanel, "Include Pets",
      function() return _BW_Get("bwIncludePets", true) end,
      function(v) _BW_Set("bwIncludePets", v) end)
    local chkConsumables = MakeCheck(bwOptPanel, "Include Consumables",
      function() return _BW_Get("bwIncludeConsumables", true) end,
      function(v) _BW_Set("bwIncludeConsumables", v) end)
    local chkGems = MakeCheck(bwOptPanel, "Include Gems",
      function() return _BW_Get("bwIncludeGems", true) end,
      function(v) _BW_Set("bwIncludeGems", v) end)

    local chkGreys = MakeCheck(bwOptPanel, "Include Greys",
      function() return _BW_Get("bwIncludeGreys", false) end,
      function(v) _BW_Set("bwIncludeGreys", v) end)
    local chkWhites = MakeCheck(bwOptPanel, "Include Whites",
      function() return _BW_Get("bwIncludeWhites", false) end,
      function(v) _BW_Set("bwIncludeWhites", v) end)
    local chkGreens = MakeCheck(bwOptPanel, "Include Greens",
      function() return _BW_Get("bwIncludeGreens", true) end,
      function(v) _BW_Set("bwIncludeGreens", v) end)
    local chkBlues = MakeCheck(bwOptPanel, "Include Blues",
      function() return _BW_Get("bwIncludeBlues", true) end,
      function(v) _BW_Set("bwIncludeBlues", v) end)
    local chkEpics = MakeCheck(bwOptPanel, "Include Epics",
      function() return _BW_Get("bwIncludeEpics", true) end,
      function(v) _BW_Set("bwIncludeEpics", v) end)
    local chkRemember = MakeCheck(bwOptPanel, "Remember Worth",
      function() return _BW_Get("bwRememberWorth", false) end,
      function(v)
        _BW_Set("bwRememberWorth", v)
        if not v then _BW_ClearRemembered() end
      end)

    local function _BW_AttachTip(frame, title, body)
      if not (frame and frame.HookScript) then return end

      -- Layout controls the hover/click area via explicit widths so tooltips don't overlap columns.

      frame:HookScript("OnEnter", function(selfTip)
        if not GameTooltip then return end
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(selfTip, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end
        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine(tostring(title or ""), ACCENT[1], ACCENT[2], ACCENT[3], true)
        body = tostring(body or "")
        if body ~= "" then
          GameTooltip:AddLine(body, TEXT[1], TEXT[2], TEXT[3], true)
        end
        GameTooltip:Show()
      end)

      frame:HookScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
      end)
    end

    _BW_AttachTip(chkMats,        "Incl. Mats",         "Counts trade goods and crafting reagents (cloth, ore, herbs, leather, etc.).")
    _BW_AttachTip(chkGear,        "Incl. Gear",         "Counts weapons, armor, and trinkets.")
    _BW_AttachTip(chkPatterns,    "Incl. Patterns",     "Counts recipes, patterns, plans, schematics, and similar learnables.")
    _BW_AttachTip(chkToys,        "Incl. Toys",         "Counts toys from your bags and bank.")
    _BW_AttachTip(chkPets,        "Incl. Pets",         "Counts caged battle pets and pet-related items.")
    _BW_AttachTip(chkConsumables, "Incl. Consumables",  "Counts potions, flasks, food, elixirs, scrolls, and other consumables.")
    _BW_AttachTip(chkGems,        "Incl. Gems",         "Counts gems and gem-like items.")
    _BW_AttachTip(chkGreys,       "Incl. Greys",        "Includes grey (poor) quality items across all categories.")
    _BW_AttachTip(chkWhites,      "Incl. Whites",       "Includes white (common) quality items across all categories.")
    _BW_AttachTip(chkGreens,      "Incl. Greens",       "Includes uncommon (green) quality items across all categories.")
    _BW_AttachTip(chkBlues,       "Incl. Blues",        "Includes rare (blue) quality items across all categories.")
    _BW_AttachTip(chkEpics,       "Incl. Epics",        "Includes epic (purple) and better items across all categories.")
    _BW_AttachTip(chkRemember,    "Remember Worth",     "Show the last scanned totals after reloads. Disable to clear the saved display.")


    local function _BW_LayoutOptions()
      bwOptPanel:ClearAllPoints()
      bwOptPanel:SetPoint("TOPLEFT", bwHeader, "BOTTOMLEFT", 0, -4)
      bwOptPanel:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)

      local w = bwOptPanel:GetWidth() or 0
      if w <= 1 then w = 200 end

      local xPad  = 12
      local chkW  = w - (xPad * 2)      -- checkbox label width
      local lblW  = w - (xPad * 2) - 2  -- section label width
      local ROW_H = 20
      local GAP   = 5
      local SEC_H = 18  -- compact section label frame height (divider + text)
      local CAT_SEC_H = 14 -- Item Types has no divider, so keep its label tighter
      local SEC_GAP_BELOW = 4

      local function SetChkW(chk)
        if chk and chk.Text and chk.Text.SetWidth then
          chk.Text:SetWidth(chkW - 24) -- 24 = checkbox texture width
        end
      end

      -- Position helpers
      local y = -4

      -- ITEM TYPES section
      lblCategories:ClearAllPoints()
      lblCategories:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      lblCategories:SetPoint("TOPRIGHT", bwOptPanel, "TOPRIGHT", -xPad, y)
      lblCategories:SetHeight(CAT_SEC_H)
      y = y - CAT_SEC_H - SEC_GAP_BELOW

      chkMats:ClearAllPoints()
      chkMats:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkMats)
      y = y - ROW_H - GAP

      chkGear:ClearAllPoints()
      chkGear:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkGear)
      y = y - ROW_H - GAP

      chkPatterns:ClearAllPoints()
      chkPatterns:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkPatterns)
      y = y - ROW_H - GAP

      chkToys:ClearAllPoints()
      chkToys:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkToys)
      y = y - ROW_H - GAP

      chkPets:ClearAllPoints()
      chkPets:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkPets)
      y = y - ROW_H - GAP

      chkConsumables:ClearAllPoints()
      chkConsumables:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkConsumables)
      y = y - ROW_H - GAP

      chkGems:ClearAllPoints()
      chkGems:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkGems)
      y = y - ROW_H - 4

      -- QUALITY section
      lblQuality:ClearAllPoints()
      lblQuality:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      lblQuality:SetPoint("TOPRIGHT", bwOptPanel, "TOPRIGHT", -xPad, y)
      lblQuality:SetHeight(SEC_H)
      y = y - SEC_H - SEC_GAP_BELOW

      chkGreys:ClearAllPoints()
      chkGreys:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkGreys)
      y = y - ROW_H - GAP

      chkWhites:ClearAllPoints()
      chkWhites:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkWhites)
      y = y - ROW_H - GAP

      chkGreens:ClearAllPoints()
      chkGreens:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkGreens)
      y = y - ROW_H - GAP

      chkBlues:ClearAllPoints()
      chkBlues:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkBlues)
      y = y - ROW_H - GAP

      chkEpics:ClearAllPoints()
      chkEpics:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkEpics)
      y = y - ROW_H - 4

      -- DISPLAY section
      lblDisplay:ClearAllPoints()
      lblDisplay:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      lblDisplay:SetPoint("TOPRIGHT", bwOptPanel, "TOPRIGHT", -xPad, y)
      lblDisplay:SetHeight(SEC_H)
      y = y - SEC_H - SEC_GAP_BELOW

      chkRemember:ClearAllPoints()
      chkRemember:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", xPad, y)
      SetChkW(chkRemember)
      y = y - ROW_H - 10

      bwOptPanel:SetHeight(-y)
    end

    bwOptPanel:SetScript("OnShow", function()
      -- Defer one frame so rSC has its real width before the backdrop renders,
      -- preventing the clipped/missing top-left corner visual glitch.
      C_Timer.After(0, function()
        if bwOptPanel:IsShown() then
          _BW_LayoutOptions()
        end
      end)
    end)

    bwOptPanel:SetScript("OnSizeChanged", function()
      if bwOptPanel:IsShown() then
        _BW_LayoutOptions()
      end
    end)

    -- Display
    local bwTotal = MakeBodyText(rSC, "Total - —")
    bwTotal:SetJustifyH("LEFT")
    bwTotal:SetTextColor(C4(TEXT, TEXT))
    bwTotal:SetWordWrap(true)

    local bwBags = MakeMutedText(rSC, "Bags - —")
    bwBags:SetJustifyH("LEFT")
    bwBags:SetWordWrap(true)

    local bwPB = MakeMutedText(rSC, "Personal Bank - open bank + refresh")
    bwPB:SetJustifyH("LEFT")
    bwPB:SetWordWrap(true)

    local bwGB = MakeMutedText(rSC, "Guild Bank - open guild bank + refresh")
    bwGB:SetJustifyH("LEFT")
    bwGB:SetWordWrap(true)

    local bwSource = MakeMutedText(rSC, "Source - —")
    bwSource:SetJustifyH("LEFT")
    bwSource:SetWordWrap(true)

    local bwProgress = MakeMutedText(rSC, "")
    bwProgress:SetJustifyH("LEFT")
    bwProgress:SetWordWrap(true)
    bwProgress:Hide()

    local function _BW_Reflow()
      -- Right margin: 12px from rSC right edge
      local rightInset = -12

      bwTotal:ClearAllPoints()
      if bwOptPanel:IsShown() then
        bwTotal:SetPoint("TOPLEFT", bwOptPanel, "BOTTOMLEFT", 0, -10)
      else
        bwTotal:SetPoint("TOPLEFT", bwHeader, "BOTTOMLEFT", 0, -10)
      end
      bwTotal:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwBags:ClearAllPoints()
      bwBags:SetPoint("TOPLEFT", bwTotal, "BOTTOMLEFT", 0, -8)
      bwBags:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwPB:ClearAllPoints()
      bwPB:SetPoint("TOPLEFT", bwBags, "BOTTOMLEFT", 0, -6)
      bwPB:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwGB:ClearAllPoints()
      bwGB:SetPoint("TOPLEFT", bwPB, "BOTTOMLEFT", 0, -6)
      bwGB:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwSource:ClearAllPoints()
      bwSource:SetPoint("TOPLEFT", bwGB, "BOTTOMLEFT", 0, -6)
      bwSource:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwProgress:ClearAllPoints()
      bwProgress:SetPoint("TOPLEFT", bwSource, "BOTTOMLEFT", 0, -6)
      bwProgress:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      if UI and UI._UpdateOverviewRightScroll then
        UI:_UpdateOverviewRightScroll()
      end
    end

    bwOptBtn:SetScript("OnClick", function()
      if bwOptPanel:IsShown() then
        bwOptPanel:Hide()
      else
        bwOptPanel:Show()
      end
      _BW_Reflow()
    end)

    _BW_Reflow()

    local bBW = Widgets:Button(rSC, "Refresh Worth", 160, 26)
bBW:SetPoint("TOPLEFT", bwSource, "BOTTOMLEFT", 0, -12)

-- Combined Refresh/Details action:
--   Click         = Refresh Worth
--   Shift + Click = Details breakdown (shows what's being counted)
UI._bwDetailsReady = UI._bwDetailsReady or false

local function BW_ShowActionTooltip()
  if not (bBW and GameTooltip) then return end

  local isShift = false
  if IsShiftKeyDown then
    isShift = (IsShiftKeyDown() == true)
  end

  if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
  GameTooltip:SetOwner(bBW, "ANCHOR_RIGHT")
  if GameTooltip.ClearLines then GameTooltip:ClearLines() end

  local ar, ag, ab = C4(ACCENT, ACCENT)
  if isShift then
    GameTooltip:AddLine("Bag Worth Details", ar, ag, ab)
    GameTooltip:AddLine("Shows which items are being counted, their quantities, and how much they contribute to the total.", TEXT[1], TEXT[2], TEXT[3], true)
    if not UI._bwDetailsReady then
      GameTooltip:AddLine(" ")
      GameTooltip:AddLine("Press Refresh Worth first to generate a breakdown.", 0.35, 0.78, 1.00, true)
    end
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("Tip: release Shift to refresh worth.", 0.75, 0.75, 0.75, true)
  else
    GameTooltip:AddLine("Refresh Worth", ar, ag, ab)
    GameTooltip:AddLine("Runs a manual Bag Worth scan using your selected filters.", TEXT[1], TEXT[2], TEXT[3], true)
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("Tip: hold Shift to open Details instead.", 0.75, 0.75, 0.75, true)
  end

  GameTooltip:Show()
end

local function BW_UpdateActionButton()
  if not bBW then return end

  local isShift = false
  if IsShiftKeyDown then
    isShift = (IsShiftKeyDown() == true)
  end

  local label = nil
  if UI._bwScanRunning then
    label = "Scanning…"
  else
    label = isShift and "Details" or "Refresh Worth"
  end
  if bBW.label and bBW.label.SetText then
    bBW.label:SetText(label)
  elseif bBW.SetText then
    bBW:SetText(label)
  end

  -- Subtle cue: if you're holding Shift but no breakdown exists yet, mute the label.
  if bBW.label and bBW.label.SetTextColor and Theme then
    if isShift and not UI._bwDetailsReady then
      local mr, mg, mb = C4(MUTED, MUTED)
      bBW.label:SetTextColor(mr, mg, mb, 1)
    elseif Theme.text then
      bBW.label:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
    end
  end

  -- If tooltip is showing, refresh it so the text matches the current modifier state.
  if GameTooltip and GameTooltip.IsOwned and GameTooltip:IsOwned(bBW) then
    BW_ShowActionTooltip()
  end
end

local function BW_SetDetailsEnabled(on)
  UI._bwDetailsReady = (on == true)
  BW_UpdateActionButton()
end

local function BW_SetScanButtonState(scanning)
  UI._bwScanRunning = (scanning == true)
  if bBW then
    if UI._bwScanRunning then
      if bBW.Disable then bBW:Disable() end
    else
      if bBW.Enable then bBW:Enable() end
    end
  end
  BW_UpdateActionButton()
end

local function BW_FinishScanState(message)
  BW_SetScanButtonState(false)
  if type(message) == "string" then
    bwProgress:SetText(message)
  end
end

-- Update label when Shift is pressed/released (even without hovering).
if bBW and bBW.RegisterEvent and bBW.SetScript then
  bBW:RegisterEvent("MODIFIER_STATE_CHANGED")
  bBW:SetScript("OnEvent", function()
    BW_UpdateActionButton()
  end)
end

if bBW and bBW.HookScript then
  bBW:HookScript("OnShow", BW_UpdateActionButton)
  bBW:HookScript("OnEnter", function()
    BW_UpdateActionButton()
    BW_ShowActionTooltip()
  end)
  bBW:HookScript("OnLeave", function()
    if GameTooltip then GameTooltip:Hide() end
    _WF_TooltipFollow_Stop()
  end)
end

BW_UpdateActionButton()

-- -----------------------------------------------------
-- Bag Worth breakdown popup (shows what is being counted)
-- -----------------------------------------------------
local function BW_NormalizeCategoryKey(category)
      category = tostring(category or ""):lower()
      if category == "gear" then return "gear" end
      if category == "mats" or category == "material" or category == "materials" then return "mats" end
      if category == "patterns" or category == "pattern" then return "patterns" end
      if category == "toys" or category == "toy" then return "toys" end
      if category == "pets" or category == "pet" then return "pets" end
      if category == "consumables" or category == "consumable" then return "consumables" end
      if category == "gems" or category == "gem" then return "gems" end
      if category == "other" then return "other" end
      return nil
    end

    local function BW_DeriveCategoryKeyFromLink(itemLink)
      local classID
      local subClassID
      local equipLoc
      local isCraftingReagent

      if itemLink then
        local _, _, _, instantEquipLoc, _, instantClassID, instantSubClassID = GetItemInfoInstant(itemLink)
        if instantEquipLoc and instantEquipLoc ~= "" then
          equipLoc = instantEquipLoc
        end
        if instantClassID ~= nil then
          classID = tonumber(instantClassID) or classID
        end
        if instantSubClassID ~= nil then
          subClassID = tonumber(instantSubClassID) or subClassID
        end

        local itemName, _, _, _, _, _, _, _, fullEquipLoc, _, _, fullClassID, fullSubClassID, _, _, _, craftingReagent = GetItemInfo(itemLink)
        if itemName then
          if fullEquipLoc and fullEquipLoc ~= "" then
            equipLoc = fullEquipLoc
          end
          if fullClassID ~= nil then
            classID = tonumber(fullClassID) or classID
          end
          if fullSubClassID ~= nil then
            subClassID = tonumber(fullSubClassID) or subClassID
          end
          if craftingReagent ~= nil then
            isCraftingReagent = craftingReagent and true or false
          end
        end
      end

      if classID == 2 or classID == 4 then
        return "gear"
      end

      if equipLoc and equipLoc ~= "" and equipLoc ~= "INVTYPE_BAG" and equipLoc ~= "INVTYPE_QUIVER" then
        if tostring(equipLoc):find("PROFESSION") then
          return "gear"
        end
      end

      if isCraftingReagent == true or classID == 7 then
        return "mats"
      end

      if classID == 9 then
        return "patterns"
      end

      if classID == 17 or (classID == 15 and subClassID == 2) or (type(itemLink) == "string" and itemLink:find("battlepet:")) then
        return "pets"
      end

      if classID == 0 then
        return "consumables"
      end

      if classID == 3 then
        return "gems"
      end

      return "other"
    end

    local function BW_ResolveCategoryKey(category, itemLink)
      return BW_NormalizeCategoryKey(category) or BW_DeriveCategoryKeyFromLink(itemLink) or "other"
    end

    local function BW_FormatCategoryLabel(category, itemLink)
      local key = BW_ResolveCategoryKey(category, itemLink)
      if key == "gear" then return "Gear" end
      if key == "mats" then return "Materials" end
      if key == "patterns" then return "Patterns" end
      if key == "toys" then return "Toys" end
      if key == "pets" then return "Pets" end
      if key == "consumables" then return "Consumables" end
      if key == "gems" then return "Gems" end
      return "Other"
    end

    -- Memory cap: Bag Worth should not keep thousands of item rows alive.
    -- The scan can see everything, but the UI/remembered payload only keeps
    -- the top rows needed for the details window.
    local BW_DETAILS_LIMIT = 150

    local function BW_BuildBreakdownFromByItem(byItem, limit)
      local out = {}
      if type(byItem) ~= "table" then return out end
      for key, e in pairs(byItem) do
        if type(e) == "table" then
          local c = tonumber(e.count) or 0
          local v = tonumber(e.value) or 0
          local link = e.link or (type(key) == "string" and key or nil)
          if c > 0 and v > 0 then
            local each = math.floor((v / c) + 0.5)
            out[#out + 1] = {
              link = link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = c,
              each = each,
              value = v,
              category = BW_FormatCategoryLabel(e.category, link),
            }
          end
        end
      end
      table.sort(out, function(a, b) return (a.value or 0) > (b.value or 0) end)
      if limit and #out > limit then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end
      return out
    end

    local function BW_BuildTopForSave(byItem, limit)
      local out = {}
      if type(byItem) ~= "table" then return out end
      for key, e in pairs(byItem) do
        if type(e) == "table" then
          local c = tonumber(e.count) or 0
          local v = tonumber(e.value) or 0
          local link = e.link or (type(key) == "string" and key or nil)
          if c > 0 and v > 0 then
            out[#out + 1] = {
              link = link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = c,
              value = v,
              category = BW_ResolveCategoryKey(e.category, link),
            }
          end
        end
      end
      table.sort(out, function(a, b)
        local av = tonumber(a.value) or 0
        local bv = tonumber(b.value) or 0
        if av == bv then
          local an = tostring(a.link or "")
          local bn = tostring(b.link or "")
          return an < bn
        end
        return av > bv
      end)
      if limit and #out > limit then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end
      return out
    end

    local function BW_BuildSkippedBreakdown(skippedItems, limit)
      local out = {}
      if type(skippedItems) ~= "table" then return out end
      for i = 1, #skippedItems do
        local e = skippedItems[i]
        if type(e) == "table" then
          local c = tonumber(e.count) or 0
          local reason = tostring(e.reason or "Skipped")
          if c > 0 then
            out[#out + 1] = {
              itemID = tonumber(e.itemID),
              link = e.link,
              name = e.name,
              count = c,
              reason = reason,
              source = e.source,
            }
          end
        end
      end
      table.sort(out, function(a, b)
        local ar = tostring(a.reason or "")
        local br = tostring(b.reason or "")
        if ar == br then
          local ac = tonumber(a.count) or 0
          local bc = tonumber(b.count) or 0
          if ac == bc then
            local an = BW_LinkName and BW_LinkName(a.link) or tostring(a.name or a.link or "")
            local bn = BW_LinkName and BW_LinkName(b.link) or tostring(b.name or b.link or "")
            return an < bn
          end
          return ac > bc
        end
        return ar < br
      end)
      if limit and #out > limit then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end
      return out
    end

    local function BW_BuildSkippedForSave(skippedItems, limit)
      local tmp = BW_BuildSkippedBreakdown(skippedItems, limit)
      local out = {}
      for i = 1, #tmp do
        local d = tmp[i]
        out[#out + 1] = {
          itemID = tonumber(d.itemID),
          link = d.link,
          name = d.name,
          count = d.count,
          reason = d.reason,
          source = d.source,
        }
      end
      return out
    end

    local function BW_MakeCompactPayload(payload)
      payload = payload or {}

      local top = nil
      if type(payload.top) == "table" then
        top = {}
        for i = 1, #payload.top do
          local e = payload.top[i]
          if type(e) == "table" then
            top[#top + 1] = {
              link = e.link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = tonumber(e.count) or 0,
              value = tonumber(e.value) or 0,
              category = BW_ResolveCategoryKey(e.category, e.link),
            }
          end
        end
      else
        top = BW_BuildTopForSave(payload.byItem, BW_DETAILS_LIMIT)
      end

      local skippedTop = nil
      if type(payload.skippedTop) == "table" then
        skippedTop = {}
        for i = 1, #payload.skippedTop do
          local e = payload.skippedTop[i]
          if type(e) == "table" then
            skippedTop[#skippedTop + 1] = {
              itemID = tonumber(e.itemID),
              link = e.link,
              name = e.name,
              count = tonumber(e.count) or 0,
              reason = e.reason,
              source = e.source,
            }
          end
        end
      else
        skippedTop = BW_BuildSkippedForSave(payload.skippedItems, BW_DETAILS_LIMIT)
      end

      local breakdown = nil
      if type(payload.breakdown) == "table" then
        breakdown = {}
        for i = 1, #payload.breakdown do
          local e = payload.breakdown[i]
          if type(e) == "table" then
            breakdown[#breakdown + 1] = {
              link = e.link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = tonumber(e.count) or 0,
              each = tonumber(e.each) or 0,
              value = tonumber(e.value) or 0,
              category = BW_ResolveCategoryKey(e.category, e.link),
            }
          end
        end
      else
        breakdown = BW_BuildBreakdownFromByItem(payload.byItem, BW_DETAILS_LIMIT)
      end

      local skippedBreakdown = nil
      if type(payload.skippedBreakdown) == "table" then
        skippedBreakdown = BW_BuildSkippedBreakdown(payload.skippedBreakdown, BW_DETAILS_LIMIT)
      else
        skippedBreakdown = BW_BuildSkippedBreakdown(payload.skippedItems or payload.skippedTop, BW_DETAILS_LIMIT)
      end

      return {
        scope = payload.scope,
        source = payload.source,
        total = payload.total,
        bagsTotal = payload.bagsTotal,
        bankTotal = payload.bankTotal,
        bankScanned = payload.bankScanned,
        gbTotal = payload.gbTotal,
        pricedStacks = payload.pricedStacks,
        pricedQty = payload.pricedQty,
        skippedStacks = payload.skippedStacks,
        skippedQty = payload.skippedQty,
        note = payload.note,
        pending = payload.pending,
        top = top,
        skippedTop = skippedTop,
        breakdown = breakdown,
        skippedBreakdown = skippedBreakdown,
        bagsBreakdown  = type(payload.bagsByItem)  == "table" and BW_BuildBreakdownFromByItem(payload.bagsByItem,  BW_DETAILS_LIMIT) or nil,
        bankBreakdown  = type(payload.bankByItem)  == "table" and BW_BuildBreakdownFromByItem(payload.bankByItem,  BW_DETAILS_LIMIT) or nil,
        gbBreakdown    = type(payload.gbByItem)    == "table" and BW_BuildBreakdownFromByItem(payload.gbByItem,    BW_DETAILS_LIMIT) or nil,
        bagsSkippedItems = type(payload.bagsSkippedItems) == "table" and BW_BuildSkippedForSave(payload.bagsSkippedItems, BW_DETAILS_LIMIT) or nil,
        bankSkippedItems = type(payload.bankSkippedItems) == "table" and BW_BuildSkippedForSave(payload.bankSkippedItems, BW_DETAILS_LIMIT) or nil,
        gbSkippedItems   = type(payload.gbSkippedItems)   == "table" and BW_BuildSkippedForSave(payload.gbSkippedItems,   BW_DETAILS_LIMIT) or nil,
      }
    end

    local function BW_ApplyFlatBagPanelBackdrop(frame, bgAlpha, borderAlpha, isHeader)
      if not frame then return end

      local br, bg, bb = C4(BORDER, BORDER)
      br = br or 0.18; bg = bg or 0.18; bb = bb or 0.18

      -- Keep the Bag Breakdown popup on the same dark stack as Help/Main.
      -- The header uses Theme.bg instead of Theme.panel2 so it does not read
      -- brighter than the Help/Main header strip.
      local src = Theme and Theme.bg
      local fallback = { 0.05, 0.05, 0.05, 0.98 }
      local pr, pg, pb = C4(src, fallback)
      pr = pr or fallback[1]; pg = pg or fallback[2]; pb = pb or fallback[3]

      if frame.SetBackdrop then
        frame:SetBackdrop({
          bgFile = "Interface/Buttons/WHITE8X8",
          edgeFile = "Interface/Buttons/WHITE8X8",
          edgeSize = 1,
          insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(pr, pg, pb, tonumber(bgAlpha) or 1)
        frame:SetBackdropBorderColor(br, bg, bb, tonumber(borderAlpha) or 1)
      elseif frame.SetBackdropColor then
        frame:SetBackdropColor(pr, pg, pb, tonumber(bgAlpha) or 1)
      end
    end

    local function BW_EnsureDarkOutline(frame)
      if not frame or not frame.CreateTexture then return end

      local br, bg, bb = C4(BORDER, BORDER)
      br = br or 0.10
      bg = bg or 0.10
      bb = bb or 0.10

      if not frame._wfDarkEdgeTop then
        frame._wfDarkEdgeTop = frame:CreateTexture(nil, "BORDER")
        frame._wfDarkEdgeTop:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
        frame._wfDarkEdgeTop:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
        frame._wfDarkEdgeTop:SetHeight(1)

        frame._wfDarkEdgeBottom = frame:CreateTexture(nil, "BORDER")
        frame._wfDarkEdgeBottom:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
        frame._wfDarkEdgeBottom:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
        frame._wfDarkEdgeBottom:SetHeight(1)

        frame._wfDarkEdgeLeft = frame:CreateTexture(nil, "BORDER")
        frame._wfDarkEdgeLeft:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
        frame._wfDarkEdgeLeft:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
        frame._wfDarkEdgeLeft:SetWidth(1)

        frame._wfDarkEdgeRight = frame:CreateTexture(nil, "BORDER")
        frame._wfDarkEdgeRight:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
        frame._wfDarkEdgeRight:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
        frame._wfDarkEdgeRight:SetWidth(1)
      end

      local a = 1
      frame._wfDarkEdgeTop:SetColorTexture(br, bg, bb, a)
      frame._wfDarkEdgeBottom:SetColorTexture(br, bg, bb, a)
      frame._wfDarkEdgeLeft:SetColorTexture(br, bg, bb, a)
      frame._wfDarkEdgeRight:SetColorTexture(br, bg, bb, a)
    end

    local function BW_RegisterEscapeClose(frame)
      if not frame then return end

      local specialName = "WF_BagWorthBreakdownFrame"
      if _G[specialName] ~= frame then
        _G[specialName] = frame
      end

      for i = 1, #UISpecialFrames do
        if UISpecialFrames[i] == specialName then
          return
        end
      end

      UISpecialFrames[#UISpecialFrames + 1] = specialName
    end

    local function BW_ClaimUtilityWindowFocus(frame)
      local focus = WF and WF.UI and WF.UI.UtilityWindowFocus or nil
      if focus and focus.Claim then
        focus:Claim("bagWorthBreakdown", frame)
        return
      end

      if frame and frame.Raise then
        frame:Raise()
      end
    end

    local function BW_EnsureDetailsFrame()
      if UI._bwDetailsFrame then return UI._bwDetailsFrame end

      local f = Widgets:Panel(UIParent)
      BW_ApplyFlatBagPanelBackdrop(f, 1, 1)
      BW_EnsureDarkOutline(f)
      f:SetFrameStrata("DIALOG")
      f:SetSize(820, 560)
      f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
      if f.SetClampedToScreen then f:SetClampedToScreen(true) end
      f:Hide()
      BW_RegisterEscapeClose(f)

      if f.HookScript then
        f:HookScript("OnShow", function(frame)
          BW_ClaimUtilityWindowFocus(frame)
        end)
        f:HookScript("OnMouseDown", function(frame)
          BW_ClaimUtilityWindowFocus(frame)
        end)
      end

      if f.EnableMouse then f:EnableMouse(true) end
      if f.EnableKeyboard then f:EnableKeyboard(true) end
      if f.SetPropagateKeyboardInput then f:SetPropagateKeyboardInput(true) end
      if f.SetScript then
        f:SetScript("OnKeyDown", function(self, key)
          if self and self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(key ~= "ESCAPE")
          end

          if key == "ESCAPE" and self and self.IsShown and self:IsShown() then
            self:Hide()
            if self and self.SetPropagateKeyboardInput then
              self:SetPropagateKeyboardInput(true)
            end
          end
        end)
        f:SetScript("OnKeyUp", function(self)
          if self and self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(true)
          end
        end)
        f:SetScript("OnHide", function(self)
          if self and self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(true)
          end
        end)
      end

      BW_ApplyFlatBagPanelBackdrop(f, 1, 1)
      BW_EnsureDarkOutline(f)

      if f.bg and f.bg.SetAlpha then f.bg:SetAlpha(1) end
      if f.Bg and f.Bg.SetAlpha then f.Bg:SetAlpha(1) end
      if f.background and f.background.SetAlpha then f.background:SetAlpha(1) end

      do
        if not f._wfSolidBG then
          local solidBG = f:CreateTexture(nil, "BACKGROUND")
          solidBG:SetAllPoints(f)
          local sr, sg, sb = C4((Theme and Theme.bg) or { 0.05, 0.05, 0.05, 0.98 }, { 0.05, 0.05, 0.05, 0.98 })
          solidBG:SetColorTexture(sr or 0.05, sg or 0.05, sb or 0.05, 1)
          f._wfSolidBG = solidBG
        else
          local sr, sg, sb = C4((Theme and Theme.bg) or { 0.05, 0.05, 0.05, 0.98 }, { 0.05, 0.05, 0.05, 0.98 })
          f._wfSolidBG:SetColorTexture(sr or 0.05, sg or 0.05, sb or 0.05, 1)
          if f._wfSolidBG.SetAlpha then f._wfSolidBG:SetAlpha(1) end
        end
      end

      local header = Widgets:Header(f, "WHAT FARM?", "Bag Breakdown")
      BW_ApplyFlatBagPanelBackdrop(header, 0.98, 1, true)
      BW_EnsureDarkOutline(header)
      header:SetPoint("TOPLEFT", 0, 0)
      header:SetPoint("TOPRIGHT", 0, 0)
      header:SetHeight(34)

      if header and header.close then
        header.close:SetScript("OnClick", function()
          if f and f.Hide then f:Hide() end
        end)
      end

      if f.SetMovable then f:SetMovable(true) end
      if header and header.EnableMouse then header:EnableMouse(true) end
      if header and header.RegisterForDrag then header:RegisterForDrag("LeftButton") end
      if header and header.SetScript then
        header:SetScript("OnDragStart", function()
          BW_ClaimUtilityWindowFocus(f)
          if f and f.StartMoving then f:StartMoving() end
        end)
        header:SetScript("OnDragStop", function()
          if f and f.StopMovingOrSizing then f:StopMovingOrSizing() end
        end)
      end

      if f.SetResizable then f:SetResizable(true) end

      local BAGWORTH_MIN_W, BAGWORTH_MIN_H = 820, 520
      local BAGWORTH_MAX_W, BAGWORTH_MAX_H = 1600, 1000

      local function BW_GetDetailsResizeBounds()
        local minW, minH = BAGWORTH_MIN_W, BAGWORTH_MIN_H
        local maxW, maxH = BAGWORTH_MAX_W, BAGWORTH_MAX_H

        if UIParent and UIParent.GetWidth and UIParent.GetHeight then
          local parentW = UIParent:GetWidth() or maxW
          local parentH = UIParent:GetHeight() or maxH
          maxW = math.max(minW, math.floor(parentW - 40))
          maxH = math.max(minH, math.floor(parentH - 40))
        end

        return minW, minH, maxW, maxH
      end

      local function BW_ApplyDetailsResizeBounds()
        local minW, minH, maxW, maxH = BW_GetDetailsResizeBounds()

        if f.SetResizeBounds then
          f:SetResizeBounds(minW, minH, maxW, maxH)
        else
          if f.SetMinResize then f:SetMinResize(minW, minH) end
          if f.SetMaxResize then f:SetMaxResize(maxW, maxH) end
        end
      end

      BW_ApplyDetailsResizeBounds()

      if f.HookScript then
        f:HookScript("OnSizeChanged", function(frame, w, h)
          if frame and frame._wfBagWorthApplyingSize then return end

          local minW, minH, maxW, maxH = BW_GetDetailsResizeBounds()
          local nextW = math.min(math.max(w or minW, minW), maxW)
          local nextH = math.min(math.max(h or minH, minH), maxH)

          if nextW ~= w or nextH ~= h then
            frame._wfBagWorthApplyingSize = true
            frame:SetSize(nextW, nextH)
            frame._wfBagWorthApplyingSize = nil
          end
        end)
      end

      local grip = nil
      if Widgets and Widgets.AttachCornerLockGrip then
        grip = Widgets:AttachCornerLockGrip(f, {
          lockKey = "uiLocked_bagWorthBreakdown",
          size = 16,
          x = -6,
          y = 6,
          alpha = 0.60,
          resizeText = "FRAME BEING RESIZED",
          onResizeStart = function()
            BW_ApplyDetailsResizeBounds()
          end,
          onResizeStop = function(frame)
            BW_ApplyDetailsResizeBounds()
            if Widgets and Widgets.RefreshPixelPerfect then
              Widgets:RefreshPixelPerfect(frame)
            end
            if frame and frame._bwSizeScrollChild then
              frame:_bwSizeScrollChild()
            end
          end,
        })
      end

      local meta = CreateFrame("Frame", nil, f)
      meta:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 14, -10)
      meta:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", -14, -10)
      meta:SetHeight(206)

      local ar, ag, ab = C4(ACCENT, ACCENT)
      local tr, tg, tb = C4(TEXT, TEXT)
      local mr, mg, mb = C4(MUTED, MUTED)

      local metaLabel = MakeMutedText(meta, "TOTAL WORTH")
      metaLabel:SetPoint("TOPLEFT", meta, "TOPLEFT", 0, 0)
      metaLabel:SetJustifyH("LEFT")
      metaLabel:SetTextColor(1, 1, 1)

      local totalBox = CreateFrame("Frame", nil, meta, (BackdropTemplateMixin and "BackdropTemplate") or nil)
      totalBox:SetPoint("TOPLEFT", metaLabel, "BOTTOMLEFT", 0, -4)
      totalBox:SetSize(238, 34)
      if totalBox.SetBackdrop then
        totalBox:SetBackdrop({
          bgFile = "Interface/Buttons/WHITE8X8",
          edgeFile = "Interface/Buttons/WHITE8X8",
          edgeSize = 1,
          insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        local pr, pg, pb = C4((Theme and Theme.panel) or { 0.07, 0.07, 0.07, 0.98 }, { 0.07, 0.07, 0.07, 0.98 })
        totalBox:SetBackdropColor(pr or 0.07, pg or 0.07, pb or 0.07, 0.98)
        totalBox:SetBackdropBorderColor(ar or 0.18, ag or 0.62, ab or 1.00, 0.65)
      end

      local metaTotal = totalBox:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontNormalLarge", "GameFontNormalLarge"))
      metaTotal:SetPoint("LEFT", totalBox, "LEFT", 10, 0)
      metaTotal:SetPoint("RIGHT", totalBox, "RIGHT", -10, 0)
      metaTotal:SetJustifyH("LEFT")
      metaTotal:SetTextColor(1, 1, 1)

      local function MakeMetaField(anchor, labelText)
        local label = MakeMutedText(meta, labelText)
        label:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -8)
        label:SetWidth(112)
        label:SetJustifyH("LEFT")
        label:SetTextColor(ar, ag, ab)

        local value = meta:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        value:SetPoint("LEFT", label, "RIGHT", 8, 0)
        value:SetPoint("RIGHT", meta, "RIGHT", -4, 0)
        value:SetJustifyH("LEFT")
        value:SetTextColor(tr, tg, tb)

        return label, value
      end

      local scannedLabel, scannedValue = MakeMetaField(totalBox, "Scanned:")
      local sourceLabel, sourceValue = MakeMetaField(scannedLabel, "Price Source:")
      local bagsLabel, bagsValue = MakeMetaField(sourceLabel, "Bags:")
      local pbLabel, pbValue = MakeMetaField(bagsLabel, "Personal Bank:")
      local gbLabel, gbValue = MakeMetaField(pbLabel, "Guild Bank:")
      local pricedLabel, pricedValue = MakeMetaField(gbLabel, "Priced Stacks:")
      local skippedLabel, skippedValue = MakeMetaField(pricedLabel, "Skipped Stacks:")

      local tabBar = CreateFrame("Frame", nil, f)
      tabBar:SetPoint("TOPLEFT", meta, "BOTTOMLEFT", 0, -18)
      tabBar:SetPoint("TOPRIGHT", meta, "BOTTOMRIGHT", 0, -18)
      tabBar:SetHeight(24)

      local function MakeTabButton(parent, width)
        local b = CreateFrame("Button", nil, parent)
        b:SetSize(width, 22)

        b.bg = b:CreateTexture(nil, "BACKGROUND")
        b.bg:SetAllPoints(b)
        b.bg:SetColorTexture(0, 0, 0, 0.35)

        b.borderTop = b:CreateTexture(nil, "BORDER")
        b.borderTop:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
        b.borderTop:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
        b.borderTop:SetHeight(1)

        b.borderBottom = b:CreateTexture(nil, "BORDER")
        b.borderBottom:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
        b.borderBottom:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
        b.borderBottom:SetHeight(1)

        b.borderLeft = b:CreateTexture(nil, "BORDER")
        b.borderLeft:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
        b.borderLeft:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
        b.borderLeft:SetWidth(1)

        b.borderRight = b:CreateTexture(nil, "BORDER")
        b.borderRight:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
        b.borderRight:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
        b.borderRight:SetWidth(1)

        if Widgets and Widgets.MakePixelPerfect then
          Widgets:MakePixelPerfect(b.borderTop, b, { mode = "h" })
          Widgets:MakePixelPerfect(b.borderBottom, b, { mode = "h" })
          Widgets:MakePixelPerfect(b.borderLeft, b, { mode = "v" })
          Widgets:MakePixelPerfect(b.borderRight, b, { mode = "v" })
        end

        b.text = b:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        b.text:SetPoint("CENTER")

        return b
      end

      local tabCounted = MakeTabButton(tabBar, 136)
      tabCounted:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0)

      local tabSkipped = MakeTabButton(tabBar, 136)
      tabSkipped:SetPoint("LEFT", tabCounted, "RIGHT", 8, 0)

      local PAD_R = 18
      local COL_GAP = 12
      local COL_TOTAL_W = 132
      local COL_EACH_W = 108
      local COL_QTY_W = 54
      local COL_CATEGORY_W = 102
      local COL_REASON_W = 248
      local SKIPPED_QTY_SHIFT = 30
      local SKIPPED_COLUMNS_LEFT_SHIFT = 112

      local cols = CreateFrame("Frame", nil, f)
      cols:SetPoint("TOPLEFT", tabBar, "BOTTOMLEFT", 0, -8)
      cols:SetPoint("TOPRIGHT", tabBar, "BOTTOMRIGHT", 0, -8)
      cols:SetHeight(18)

      local cItem = MakeMutedText(cols, "Item")
      cItem:SetJustifyH("LEFT")

      local cCategory = MakeMutedText(cols, "Category")
      cCategory:SetJustifyH("LEFT")
      cCategory:SetWidth(COL_CATEGORY_W)

      local cQty = MakeMutedText(cols, "Quantity")
      cQty:SetJustifyH("RIGHT")
      cQty:SetWidth(COL_QTY_W)

      local cEach = MakeMutedText(cols, "Each")
      cEach:SetJustifyH("RIGHT")
      cEach:SetWidth(COL_EACH_W)

      local cTotal = MakeMutedText(cols, "Total")
      cTotal:SetJustifyH("RIGHT")
      cTotal:SetWidth(COL_TOTAL_W)

      local div = MakeDivider(f)
      div:SetPoint("TOPLEFT", cols, "BOTTOMLEFT", 0, -6)
      div:SetPoint("TOPRIGHT", cols, "BOTTOMRIGHT", 0, -6)

      local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
      if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
      scroll:SetPoint("TOPLEFT", div, "BOTTOMLEFT", 0, -6)
      scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -28, 18)

      local sc = CreateFrame("Frame", nil, scroll)
      sc:SetSize(1, 1)
      scroll:SetScrollChild(sc)

      local emptyText = MakeMutedText(sc, "No items to show.")
      emptyText:SetPoint("TOP", sc, "TOP", 0, -18)
      emptyText:SetJustifyH("CENTER")
      emptyText:Hide()

      f._bwMeta = meta
      f._bwMetaTotal = metaTotal
      f._bwMetaScanned = scannedValue
      f._bwMetaSource = sourceValue
      f._bwMetaBags = bagsValue
      f._bwMetaPB = pbValue
      f._bwMetaGB = gbValue
      f._bwMetaPriced = pricedValue
      f._bwMetaSkipped = skippedValue
      f._bwScroll = scroll
      f._bwScrollChild = sc
      f._bwRows = {}
      f._bwCountedData = {}
      f._bwSkippedData = {}
      f._bwData = {}
      f._bwCollapsedSources = {}
      f._bwView = "counted"
      f._bwSort = {
        counted = { key = "value", asc = false },
        skipped = { key = "reason", asc = true },
      }
      f._bwEmptyText = emptyText
      f._bwTabCounted = tabCounted
      f._bwTabSkipped = tabSkipped

      local ROW_H = 18

      local function Row_OnEnter(self)
        if self and self._hl then self._hl:SetAlpha(0.10) end
      end

      local function Row_OnLeave(self)
        if self and self._hl then self._hl:SetAlpha(0) end
      end

      local function Row_OnClick(self)
        if not self then return end
        if self._isDivider and self._sourceKey then
          f._bwCollapsedSources = f._bwCollapsedSources or {}
          f._bwCollapsedSources[self._sourceKey] = not f._bwCollapsedSources[self._sourceKey]
          if f._BW_Rerender then
            f:_BW_Rerender()
          end
          return
        end
        if ChatEdit_InsertLink then
          -- Resolve to a full colored hyperlink before inserting into chat.
          -- row._link may be: a proper |c...|H...|h|r link, a bare |Hitem:...|h
          -- without color, or a plain name/bracket string like "[Item Name]" used
          -- as a table key fallback. Only the first form is safe to pass to
          -- SendChatMessage. Anything without |H is not a real hyperlink and must
          -- be resolved via GetItemInfo or itemID, or skipped entirely.
          local link = self._link
          local resolved = nil

          if type(link) == "string" and link:find("|H", 1, true) then
            -- Has a hyperlink token — try to get the colored version from cache
            if GetItemInfo then
              local _, rl = GetItemInfo(link)
              if rl and rl ~= "" then resolved = rl end
            end
            -- If GetItemInfo didn't return a colored link, use as-is only if it already has |c
            if not resolved and link:find("^|c") then
              resolved = link
            end
          end

          -- If still no resolved link, try via stored itemID
          if not resolved and self._itemID and self._itemID > 0 and GetItemInfo then
            local _, rl = GetItemInfo(self._itemID)
            if rl and rl ~= "" then resolved = rl end
          end

          if resolved then
            ChatEdit_InsertLink(resolved)
          end
        end
      end

      local function BW_LinkName(link)
        if type(link) ~= "string" then return "" end
        local n = link:match("%[(.-)%]")
        if n and n ~= "" then return n end
        return link
      end

      local function BW_DisplayName(d)
        d = d or {}
        if type(d.link) == "string" and d.link ~= "" then
          -- Prefer the original hyperlink. It already contains the quality color,
          -- even when item info has not finished loading yet.
          if d.link:find("|H", 1, true) then
            return d.link, true
          end

          local itemName, itemLink = nil, nil
          if GetItemInfo then
            itemName, itemLink = GetItemInfo(d.link)
          end
          if itemLink and itemLink ~= "" then
            return itemLink, true
          end
          if itemName and itemName ~= "" then
            return itemName, false
          end
          return BW_LinkName(d.link), false
        end
        if d.name and d.name ~= "" then
          return tostring(d.name), false
        end
        return "—", false
      end

      local SORT_ICON_UP = "Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up"
      local SORT_ICON_DOWN = "Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up"

      local function BW_EnsureSortIcon(btn, anchorRegion)
        if not btn then return nil end
        if btn._bwSortIcon then return btn._bwSortIcon end

        local icon = btn:CreateTexture(nil, "OVERLAY")
        icon:SetSize(12, 12)
        if anchorRegion then
          icon:SetPoint("LEFT", anchorRegion, "RIGHT", 2, 0)
        else
          icon:SetPoint("RIGHT", btn, "RIGHT", 0, 0)
        end
        icon:SetAlpha(0.92)
        if icon.SetDesaturation then
          icon:SetDesaturation(1)
        elseif icon.SetDesaturated then
          icon:SetDesaturated(true)
        end
        icon:Hide()

        btn._bwSortIcon = icon
        return icon
      end

      local function BW_SetSortIcon(btn, anchorRegion, active, asc)
        local icon = BW_EnsureSortIcon(btn, anchorRegion)
        if not icon then return end

        if not active then
          icon:Hide()
          return
        end

        if anchorRegion and icon.ClearAllPoints then
          icon:ClearAllPoints()
          local justify = anchorRegion.GetJustifyH and anchorRegion:GetJustifyH()
          if justify == "LEFT" and anchorRegion.GetStringWidth and anchorRegion.GetWidth then
            local textW = tonumber(anchorRegion:GetStringWidth()) or 0
            local regionW = tonumber(anchorRegion:GetWidth()) or 0
            local offset = textW + 4
            if regionW > 12 and offset > (regionW - 12) then
              offset = regionW - 12
            end
            if offset < 2 then offset = 2 end
            icon:SetPoint("LEFT", anchorRegion, "LEFT", offset, 0)
          else
            icon:SetPoint("LEFT", anchorRegion, "RIGHT", 2, 0)
          end
        end

        icon:SetTexture(asc and SORT_ICON_UP or SORT_ICON_DOWN)
        local ir, ig, ib = C4(ACCENT, ACCENT)
        icon:SetVertexColor(ir or 0.18, ig or 0.62, ib or 1.00, 1)
        icon:Show()
      end

      local function BW_GetActiveSortState(self)
        local view = self._bwView or "counted"
        self._bwSort = self._bwSort or {}
        if not self._bwSort[view] then
          if view == "skipped" then
            self._bwSort[view] = { key = "reason", asc = true }
          else
            self._bwSort[view] = { key = "value", asc = false }
          end
        end
        return self._bwSort[view]
      end

      local function MakeHeaderButton(parent)
        local b = CreateFrame("Button", nil, cols)
        b:SetPoint("TOPLEFT", parent, "TOPLEFT", -8, 8)
        b:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 8, -8)
        b:EnableMouse(true)
        return b
      end

      local bItem = MakeHeaderButton(cItem)
      local bCategory = MakeHeaderButton(cCategory)
      local bQty = MakeHeaderButton(cQty)
      local bEach = MakeHeaderButton(cEach)
      local bTotal = MakeHeaderButton(cTotal)

      BW_EnsureSortIcon(bItem, cItem)
      BW_EnsureSortIcon(bCategory, cCategory)
      BW_EnsureSortIcon(bQty, cQty)
      BW_EnsureSortIcon(bEach, cEach)
      BW_EnsureSortIcon(bTotal, cTotal)

      function f:_BW_UpdateTabVisuals()
        local activeView = self._bwView or "counted"
        local activeR, activeG, activeB = C4(ACCENT, ACCENT)
        local textR, textG, textB = C4(TEXT, TEXT)
        local mutR, mutG, mutB = C4(MUTED, MUTED)
        local borderR, borderG, borderB = C4(BORDER, BORDER)

        local function PaintTab(btn, active)
          if not btn then return end
          if btn.bg then
            if active then
              btn.bg:SetColorTexture(activeR or 0.18, activeG or 0.62, activeB or 1.00, 0.18)
            else
              btn.bg:SetColorTexture(0, 0, 0, 0.28)
            end
          end
          if btn.text then
            if active then
              btn.text:SetTextColor(textR, textG, textB)
            else
              btn.text:SetTextColor(mutR, mutG, mutB)
            end
          end
          local lineR, lineG, lineB = borderR, borderG, borderB
          if active then
            lineR, lineG, lineB = activeR, activeG, activeB
          end
          if btn.borderTop then btn.borderTop:SetColorTexture(lineR, lineG, lineB, 1) end
          if btn.borderBottom then btn.borderBottom:SetColorTexture(lineR, lineG, lineB, 1) end
          if btn.borderLeft then btn.borderLeft:SetColorTexture(lineR, lineG, lineB, 1) end
          if btn.borderRight then btn.borderRight:SetColorTexture(lineR, lineG, lineB, 1) end
        end

        PaintTab(tabCounted, activeView == "counted")
        PaintTab(tabSkipped, activeView == "skipped")
      end

      function f:_BW_UpdateTabCounts()
        local countedN = 0
        for _, e in ipairs(self._bwCountedData or {}) do
          if type(e) == "table" and e.kind ~= "divider" and e.kind ~= "section" and e.kind ~= "spacer" then
            countedN = countedN + 1
          end
        end
        local skippedN = 0
        for _, e in ipairs(self._bwSkippedData or {}) do
          if type(e) == "table" and e.kind ~= "divider" and e.kind ~= "section" and e.kind ~= "spacer" then
            skippedN = skippedN + 1
          end
        end
        if tabCounted and tabCounted.text then
          tabCounted.text:SetText("Counted (" .. tostring(countedN) .. ")")
        end
        if tabSkipped and tabSkipped.text then
          tabSkipped.text:SetText("Skipped (" .. tostring(skippedN) .. ")")
        end
      end

      function f:_BW_UpdateSortHeader()
        local state = BW_GetActiveSortState(self)
        local key = state.key or "value"
        local asc = (state.asc == true)
        local skippedView = (self._bwView == "skipped")
        local ar, ag, ab = C4(ACCENT, ACCENT)
        local mr, mg, mb = C4(MUTED, MUTED)

        local function SetHeader(fs, btn, anchorRegion, text, active)
          if not fs then return end
          fs:SetText(text)
          if active then
            fs:SetTextColor(ar, ag, ab)
          else
            fs:SetTextColor(mr, mg, mb)
          end
          BW_SetSortIcon(btn, anchorRegion, active, asc)
        end

        SetHeader(cItem, bItem, cItem, "Item", key == "name")
        SetHeader(cQty, bQty, cQty, "Quantity", key == "count")

        if skippedView then
          SetHeader(cCategory, bCategory, cCategory, "Category", false)
          SetHeader(cEach, bEach, cEach, "Why", key == "reason")
        else
          SetHeader(cCategory, bCategory, cCategory, "Category", key == "category")
          SetHeader(cEach, bEach, cEach, "Each", key == "each")
        end

        SetHeader(cTotal, bTotal, cTotal, "Total", key == "value")
      end

      local function _BW_AlignHeaderColumns()
        local sbw = (scroll and scroll.ScrollBar and scroll.ScrollBar.GetWidth and scroll.ScrollBar:GetWidth()) or 16
        local pad = (sbw or 16) + 6
        local skippedView = (f._bwView == "skipped")

        if skippedView then
          cTotal:Hide()
          bTotal:Hide()
          cCategory:Hide()
          bCategory:Hide()

          cEach:Show()
          bEach:Show()
          cEach:SetJustifyH("LEFT")
          cEach:ClearAllPoints()
          cEach:SetPoint("RIGHT", cols, "RIGHT", -(PAD_R + pad + 6), 0)
          cEach:SetWidth(COL_REASON_W)

          cQty:ClearAllPoints()
          cQty:SetPoint("RIGHT", cEach, "LEFT", -(COL_GAP + SKIPPED_QTY_SHIFT), 0)
          cQty:SetWidth(COL_QTY_W)
        else
          cTotal:Show()
          bTotal:Show()
          cCategory:Show()
          bCategory:Show()

          cTotal:SetJustifyH("RIGHT")
          cTotal:ClearAllPoints()
          cTotal:SetPoint("RIGHT", cols, "RIGHT", -(PAD_R + pad + 6), 0)
          cTotal:SetWidth(COL_TOTAL_W)

          cEach:Show()
          bEach:Show()
          cEach:SetJustifyH("RIGHT")
          cEach:ClearAllPoints()
          cEach:SetPoint("RIGHT", cTotal, "LEFT", -COL_GAP, 0)
          cEach:SetWidth(COL_EACH_W)

          cQty:ClearAllPoints()
          cQty:SetPoint("RIGHT", cEach, "LEFT", -COL_GAP, 0)
          cQty:SetWidth(COL_QTY_W)

          cCategory:ClearAllPoints()
          cCategory:SetPoint("RIGHT", cQty, "LEFT", -COL_GAP, 0)
          cCategory:SetWidth(COL_CATEGORY_W)
        end

        cItem:ClearAllPoints()
        cItem:SetPoint("LEFT", cols, "LEFT", 28, 0)
        if skippedView then
          cItem:SetPoint("RIGHT", cQty, "LEFT", -14, 0)
        else
          cItem:SetPoint("RIGHT", cCategory, "LEFT", -12, 0)
        end
        cItem:SetJustifyH("LEFT")

        bItem:ClearAllPoints()
        bItem:SetPoint("TOPLEFT", cols, "TOPLEFT", 6, 8)
        if skippedView then
          bItem:SetPoint("BOTTOMRIGHT", cQty, "BOTTOMLEFT", -8, -8)
        else
          bItem:SetPoint("BOTTOMRIGHT", cCategory, "BOTTOMLEFT", -8, -8)
        end
      end

      local function _BW_SizeScrollChild()
        local w = (scroll and scroll.GetWidth and scroll:GetWidth()) or 0
        local sbw = (scroll and scroll.ScrollBar and scroll.ScrollBar.GetWidth and scroll.ScrollBar:GetWidth()) or 16
        w = (w or 0) - (sbw or 16) - 6
        if w < 1 then w = 1 end
        sc:SetWidth(w)
        _BW_AlignHeaderColumns()
      end

      _BW_SizeScrollChild()
      if scroll and scroll.HookScript then
        scroll:HookScript("OnShow", _BW_SizeScrollChild)
        scroll:HookScript("OnSizeChanged", _BW_SizeScrollChild)
      end
      if f and f.HookScript then
        f:HookScript("OnShow", _BW_SizeScrollChild)
        f:HookScript("OnSizeChanged", _BW_SizeScrollChild)
      end

      local function EnsureRow(i)
        local row = f._bwRows[i]
        if row then return row end

        row = CreateFrame("Button", nil, sc)
        row:SetHeight(ROW_H)
        row:SetPoint("TOPLEFT", sc, "TOPLEFT", 0, -((i - 1) * ROW_H))
        row:SetPoint("TOPRIGHT", sc, "TOPRIGHT", 0, -((i - 1) * ROW_H))

        row._bg = row:CreateTexture(nil, "BACKGROUND")
        row._bg:SetAllPoints(row)
        row._bg:SetColorTexture(0.18, 0.62, 1.00, 0.0)

        row._hl = row:CreateTexture(nil, "HIGHLIGHT")
        row._hl:SetAllPoints(row)
        row._hl:SetColorTexture(0.18, 0.62, 1.00, 0.0)

        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(16, 16)
        row.icon:SetPoint("LEFT", 6, 0)

        row.name = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        row.name:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
        row.name:SetJustifyH("LEFT")

        row.category = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.category:SetJustifyH("LEFT")
        row.category:SetWidth(COL_CATEGORY_W)

        row.qty = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.qty:SetJustifyH("RIGHT")
        row.qty:SetWidth(COL_QTY_W)

        row.each = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.each:SetJustifyH("RIGHT")
        row.each:SetWidth(COL_EACH_W)

        row.total = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        row.total:SetJustifyH("RIGHT")
        row.total:SetWidth(COL_TOTAL_W)
        do
          local ar, ag, ab = C4(ACCENT, ACCENT)
          row.total:SetTextColor(ar, ag, ab)
        end

        row.reason = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.reason:SetJustifyH("LEFT")
        row.reason:SetWidth(COL_REASON_W)

        row:SetScript("OnEnter", Row_OnEnter)
        row:SetScript("OnLeave", Row_OnLeave)
        row:SetScript("OnClick", Row_OnClick)

        f._bwRows[i] = row
        return row
      end

      local function _BW_GetNum(d, key)
        d = d or {}
        if key == "count" then
          return tonumber(d.count) or 0
        end
        if key == "each" then
          return tonumber(d.each) or 0
        end
        if key == "value" then
          return tonumber(d.value) or 0
        end
        return 0
      end

      function f:_BW_ApplySort(nameFunc)
        local data = self._bwData or {}
        if #data < 2 and not (type(self._bwRawSections) == "table" and #self._bwRawSections > 0) then return end

        local state = BW_GetActiveSortState(self)
        local key = state.key or "value"
        local asc = (state.asc == true)
        local getName = nameFunc or function(link)
          if type(link) ~= "string" then return "" end
          local n = link:match("%[(.-)%]")
          if n and n ~= "" then return n end
          return link
        end

        local function RowName(row)
          row = row or {}
          if row.link then
            return tostring(getName(row.link) or "")
          end
          return tostring(row.name or "")
        end

        local function RowLess(a, b)
          a = a or {}
          b = b or {}

          if key == "name" then
            local an = RowName(a)
            local bn = RowName(b)
            if an == bn then
              local ac = tonumber(a.count) or 0
              local bc = tonumber(b.count) or 0
              if ac == bc then
                local av = tonumber(a.value) or 0
                local bv = tonumber(b.value) or 0
                if asc then return av < bv end
                return av > bv
              end
              if asc then return ac < bc end
              return ac > bc
            end
            if asc then return an < bn end
            return an > bn
          end

          if key == "category" then
            local acat = tostring(a.category or "")
            local bcat = tostring(b.category or "")
            if acat == bcat then
              local av = tonumber(a.value) or 0
              local bv = tonumber(b.value) or 0
              if av == bv then
                local an = RowName(a)
                local bn = RowName(b)
                if asc then return an < bn end
                return an > bn
              end
              if asc then return av < bv end
              return av > bv
            end
            if asc then return acat < bcat end
            return acat > bcat
          end

          if key == "reason" then
            local ar = tostring(a.reason or "")
            local br = tostring(b.reason or "")
            if ar == br then
              local ac = tonumber(a.count) or 0
              local bc = tonumber(b.count) or 0
              if ac == bc then
                local an = RowName(a)
                local bn = RowName(b)
                if asc then return an < bn end
                return an > bn
              end
              if asc then return ac < bc end
              return ac > bc
            end
            if asc then return ar < br end
            return ar > br
          end

          local av = _BW_GetNum(a, key)
          local bv = _BW_GetNum(b, key)
          if av == bv then
            local an = RowName(a)
            local bn = RowName(b)
            if an == bn then
              local ac = tonumber(a.count) or 0
              local bc = tonumber(b.count) or 0
              if asc then return ac < bc end
              return ac > bc
            end
            if asc then return an < bn end
            return an > bn
          end
          if asc then return av < bv end
          return av > bv
        end

        local function SortRows(rows)
          if type(rows) ~= "table" or #rows < 2 then return end
          table.sort(rows, RowLess)
        end

        -- New section model: keep the Bags / Personal Bank / Guild Bank headers in place,
        -- but sort the items inside each section. Only add a blank spacer after a section
        -- that is actually open and showing items, so collapsed headers do not look huge.
        if self._bwView ~= "skipped" and type(self._bwRawSections) == "table" and #self._bwRawSections > 0 then
          local flat = {}
          local previousSectionHadVisibleItems = false
          for i = 1, #self._bwRawSections do
            local sec = self._bwRawSections[i]
            if type(sec) == "table" then
              local items = sec.items or {}
              SortRows(items)
              if #flat > 0 and previousSectionHadVisibleItems then
                flat[#flat + 1] = { kind = "spacer" }
              end
              flat[#flat + 1] = sec
              previousSectionHadVisibleItems = false
              if not sec.collapsed then
                for j = 1, #items do
                  flat[#flat + 1] = items[j]
                end
                previousSectionHadVisibleItems = (#items > 0)
              end
            end
          end
          self._bwCountedData = flat
          self._bwData = flat
          return
        end

        local function IsSectionRow(row)
          if type(row) ~= "table" then return false end
          local kind = row.kind
          return kind == "divider" or kind == "section" or kind == "spacer"
        end

        local hasSections = false
        for i = 1, #data do
          if IsSectionRow(data[i]) then
            hasSections = true
            break
          end
        end

        if not hasSections then
          SortRows(data)
          return
        end

        -- Legacy section model: preserve each source header/spacer and sort only the
        -- item rows between headers. This keeps bank items inside bank, guild bank
        -- items inside guild bank, etc.
        local sorted = {}
        local chunk = {}

        local function FlushChunk()
          SortRows(chunk)
          for i = 1, #chunk do
            sorted[#sorted + 1] = chunk[i]
          end
          chunk = {}
        end

        for i = 1, #data do
          local row = data[i]
          if IsSectionRow(row) then
            FlushChunk()
            sorted[#sorted + 1] = row
          else
            chunk[#chunk + 1] = row
          end
        end
        FlushChunk()

        for i = 1, #sorted do
          data[i] = sorted[i]
        end
        for i = #sorted + 1, #data do
          data[i] = nil
        end
      end

      local function BW_RequestSort(key)
        local data = f._bwData or {}
        if #data < 1 then return end

        local state = BW_GetActiveSortState(f)
        if state.key ~= key then
          state.key = key
          state.asc = (key == "name" or key == "reason" or key == "category")
        else
          state.asc = not state.asc
        end

        f:_BW_ApplySort(BW_LinkName)
        f:_BW_UpdateSortHeader()
        if f._BW_Rerender then
          f:_BW_Rerender()
        end
      end

      bItem:SetScript("OnClick", function() BW_RequestSort("name") end)
      bCategory:SetScript("OnClick", function()
        if f._bwView ~= "skipped" then
          BW_RequestSort("category")
        end
      end)
      bQty:SetScript("OnClick", function() BW_RequestSort("count") end)
      bEach:SetScript("OnClick", function()
        if f._bwView == "skipped" then
          BW_RequestSort("reason")
        else
          BW_RequestSort("each")
        end
      end)
      bTotal:SetScript("OnClick", function() BW_RequestSort("value") end)

      function f:_BW_GetRenderData()
        local src = self._bwData or {}
        if self._bwView ~= "skipped" then
          return src
        end

        local out = {}
        local currentSource = nil
        local currentCollapsed = false
        local collapsed = self._bwCollapsedSources or {}
        local previousSourceHadVisibleItems = false

        for i = 1, #src do
          local d = src[i]
          if type(d) == "table" and d.kind == "spacer" then
            -- Spacing is generated here so collapsed skipped sections do not leave huge gaps.
          elseif type(d) == "table" and d.kind == "divider" then
            if #out > 0 and previousSourceHadVisibleItems then
              out[#out + 1] = { kind = "spacer" }
            end
            currentSource = tostring(d.sourceKey or d.label or i)
            currentCollapsed = (collapsed[currentSource] == true)
            previousSourceHadVisibleItems = false
            out[#out + 1] = d
          elseif not currentSource or not currentCollapsed then
            out[#out + 1] = d
            if currentSource then
              previousSourceHadVisibleItems = true
            end
          end
        end

        return out
      end

      local function BW_RenderRows()
        local data = f:_BW_GetRenderData()
        local n = #data
        local skippedView = (f._bwView == "skipped")

        if n < 1 then
          f._bwScrollChild:SetHeight(1)
          if f._bwEmptyText then
            if skippedView then
              f._bwEmptyText:SetText("No skipped items to show.")
            else
              f._bwEmptyText:SetText("No counted items to show.")
            end
            f._bwEmptyText:Show()
          end
        else
          f._bwScrollChild:SetHeight(n * ROW_H)
          if f._bwEmptyText then
            f._bwEmptyText:Hide()
          end
        end

        local ar, ag, ab = C4(ACCENT, ACCENT)
        local mr, mg, mb = C4(MUTED, MUTED)

        for i = 1, n do
          local d = data[i]
          local row = EnsureRow(i)

          if d.kind == "spacer" then
            row._link = nil
            row._isDivider = false
            row._sourceKey = nil
            row._bg:SetAlpha(0)
            row._hl:SetAlpha(0)
            row:EnableMouse(false)
            row:SetScript("OnClick", nil)
            row:SetScript("OnEnter", nil)
            row:SetScript("OnLeave", nil)

            row.icon:SetTexture(nil)
            row.icon:Hide()
            row.name:SetText("")
            row.name:Hide()
            row.qty:Hide()
            row.each:Hide()
            row.total:Hide()
            row.category:Hide()
            row.reason:Hide()
            row:Show()

          -- Divider row: collapsible source header, used by skipped items.
          elseif d.kind == "divider" then
            row._link = nil
            row._isDivider = true
            row._sourceKey = tostring(d.sourceKey or d.label or i)
            row._bg:SetAlpha(0.12)
            row._hl:SetAlpha(0)
            row:EnableMouse(true)
            row:SetScript("OnClick", Row_OnClick)
            row:SetScript("OnEnter", Row_OnEnter)
            row:SetScript("OnLeave", Row_OnLeave)

            row.icon:SetTexture(nil)
            row.icon:Hide()

            row.name:Show()
            row.name:ClearAllPoints()
            row.name:SetPoint("LEFT", row, "LEFT", 6, 0)
            row.name:SetPoint("RIGHT", row, "RIGHT", -6, 0)
            row.name:SetJustifyH("LEFT")
            row.name:SetTextColor(ar, ag, ab)

            local label = tostring(d.label or "Skipped Items")
            local count = tonumber(d.count) or 0
            local collapsed = f._bwCollapsedSources and f._bwCollapsedSources[row._sourceKey]
            label = (collapsed and "+  " or "-  ") .. label
            if count > 0 then
              label = label .. "  •  " .. tostring(count) .. " items"
            end
            row.name:SetText(label)

            row.qty:Hide()
            row.each:Hide()
            row.total:Hide()
            row.category:Hide()
            row.reason:Hide()
            row:Show()

          -- Divider row: collapsible section header between sources.
          elseif d.kind == "section" then
            row._link = nil
            row._isDivider = false
            row._sourceKey = nil
            row._bg:SetAlpha(0.08)
            row._hl:SetAlpha(0)
            row:EnableMouse(true)

            row.icon:SetTexture(nil)
            row.icon:Hide()

            row.name:Show()
            row.name:ClearAllPoints()
            row.name:SetPoint("LEFT", row, "LEFT", 6, 0)
            row.name:SetPoint("RIGHT", row, "RIGHT", -6, 0)
            row.name:SetJustifyH("LEFT")
            row.name:SetTextColor(ar, ag, ab)

            local toggle = d.collapsed and "+" or "-"
            local label = toggle .. "  " .. tostring(d.label or "")
            local total = tonumber(d.total) or 0
            if total > 0 then
              label = label .. "   " .. Money(Util:Round(total))
            end
            row.name:SetText(label)

            row.qty:Hide()
            row.each:Hide()
            row.total:Hide()
            row.category:Hide()
            row.reason:Hide()

            -- Click toggles collapse
            row:SetScript("OnClick", function()
              if not f._bwRawSections then return end
              d.collapsed = not d.collapsed
              if f._bwSectionState then
                f._bwSectionState[d.label] = d.collapsed
              end
              -- Reflatten and re-render. Only leave the spacer after an open section
              -- with visible items; closed headers should stack tightly.
              local flat = {}
              local previousSectionHadVisibleItems = false
              for si = 1, #f._bwRawSections do
                local sec = f._bwRawSections[si]
                local items = sec.items or {}
                if #flat > 0 and previousSectionHadVisibleItems then
                  flat[#flat + 1] = { kind = "spacer" }
                end
                flat[#flat + 1] = sec
                previousSectionHadVisibleItems = false
                if not sec.collapsed then
                  for ji = 1, #items do flat[#flat + 1] = items[ji] end
                  previousSectionHadVisibleItems = (#items > 0)
                end
              end
              f._bwCountedData = flat
              f._bwData = flat
              f:_BW_UpdateTabCounts()
              f:_BW_Rerender()
            end)
            row:SetScript("OnEnter", function(self)
              if self._hl then self._hl:SetAlpha(0.08) end
            end)
            row:SetScript("OnLeave", function(self)
              if self._hl then self._hl:SetAlpha(0) end
            end)
            row:Show()

          else
            row:EnableMouse(true)
            row:SetScript("OnClick", Row_OnClick)
            row:SetScript("OnEnter", Row_OnEnter)
            row:SetScript("OnLeave", Row_OnLeave)
            if row._bg and row._bg.SetAlpha then
              row._bg:SetAlpha((i % 2) == 0 and 0.03 or 0.0)
            end

            row._link = d.link
            row._itemID = d.itemID
            row._isDivider = false
            row._sourceKey = nil

            row.name:Show()
            row.qty:Show()

            local text, isLink = BW_DisplayName(d)
            -- Apply quality color when the text is a plain name (not a pre-colored |c hyperlink).
            -- Without this, items whose links weren't cached yet show in white regardless of rarity.
            if isLink and type(text) == "string" and text:find("^|c") then
              row.name:SetTextColor(1, 1, 1, 1) -- color is embedded in the hyperlink escape
            elseif type(d.quality) == "number" and GetItemQualityColor then
              local qr, qg, qb = GetItemQualityColor(d.quality)
              row.name:SetTextColor(qr or 1, qg or 1, qb or 1, 1)
            else
              row.name:SetTextColor(C4(TEXT, TEXT))
            end
            row.name:SetText(text)

            row.qty:SetText(tostring(d.count or 0))

            if skippedView then
              row.total:Hide()
              row.each:Hide()
              row.category:Hide()
              row.reason:Show()

              row.reason:ClearAllPoints()
              row.reason:SetPoint("RIGHT", row, "RIGHT", -(PAD_R - 8), 0)
              row.reason:SetWidth(COL_REASON_W)
              row.reason:SetText(tostring(d.reason or "—"))

              row.qty:ClearAllPoints()
              row.qty:SetPoint("RIGHT", row.reason, "LEFT", -(COL_GAP + SKIPPED_QTY_SHIFT), 0)
              row.qty:SetWidth(COL_QTY_W)

              row.name:ClearAllPoints()
              row.name:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
              row.name:SetPoint("RIGHT", row.qty, "LEFT", -14, 0)
            else
              row.reason:Hide()
              row.each:Show()
              row.total:Show()
              row.category:Show()

              row.total:ClearAllPoints()
              row.total:SetPoint("RIGHT", row, "RIGHT", -(PAD_R - 8), 0)
              row.total:SetWidth(COL_TOTAL_W)
              row.total:SetText(Money(Util:Round(d.value or 0)))

              row.each:ClearAllPoints()
              row.each:SetPoint("RIGHT", row.total, "LEFT", -COL_GAP, 0)
              row.each:SetWidth(COL_EACH_W)
              row.each:SetText(Money(Util:Round(d.each or 0)))

              row.qty:ClearAllPoints()
              row.qty:SetPoint("RIGHT", row.each, "LEFT", -COL_GAP, 0)
              row.qty:SetWidth(COL_QTY_W)

              row.category:ClearAllPoints()
              row.category:SetPoint("RIGHT", row.qty, "LEFT", -COL_GAP, 0)
              row.category:SetWidth(COL_CATEGORY_W)
              row.category:SetText(tostring(d.category or "Other"))

              row.name:ClearAllPoints()
              row.name:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
              row.name:SetPoint("RIGHT", row.category, "LEFT", -12, 0)
            end

            local icon = nil
            if type(d.link) == "string" and d.link ~= "" then
              if C_Item and C_Item.GetItemIconByID and GetItemInfoInstant then
                local itemID = select(1, GetItemInfoInstant(d.link))
                if itemID then
                  icon = C_Item.GetItemIconByID(itemID)
                end
              end
              if not icon and GetItemIcon and GetItemInfoInstant then
                local itemID = select(1, GetItemInfoInstant(d.link))
                if itemID then icon = GetItemIcon(itemID) end
              end
            end

            if icon then
              row.icon:SetTexture(icon)
              row.icon:Show()
            else
              row.icon:SetTexture(nil)
              row.icon:Hide()
              row.name:ClearAllPoints()
              row.name:SetPoint("LEFT", row, "LEFT", 6, 0)
              if skippedView then
                row.name:SetPoint("RIGHT", row.qty, "LEFT", -14, 0)
              else
                row.name:SetPoint("RIGHT", row.category, "LEFT", -12, 0)
              end
            end

            row:Show()
          end
        end

        for i = n + 1, #f._bwRows do
          f._bwRows[i]:Hide()
        end
      end

      function f:_BW_Rerender()
        _BW_SizeScrollChild()
        BW_RenderRows()
      end

      function f:_BW_SetView(view)
        if view ~= "skipped" then
          view = "counted"
        end
        self._bwView = view
        if view == "skipped" then
          self._bwData = self._bwSkippedData or {}
        else
          self._bwData = self._bwCountedData or {}
        end
        self:_BW_ApplySort(BW_LinkName)
        self:_BW_UpdateTabVisuals()
        self:_BW_UpdateSortHeader()
        self:_BW_Rerender()
        if self._bwScroll and self._bwScroll.SetVerticalScroll then
          self._bwScroll:SetVerticalScroll(0)
        end
      end

      function f:_BW_SetData(payload)
        payload = payload or {}
        local total = tonumber(payload.total) or 0
        local bags = tonumber(payload.bagsTotal) or 0
        local bank = tonumber(payload.bankTotal) or 0
        local gb = tonumber(payload.gbTotal) or 0
        local scope = tostring(payload.scope or "bags")

        local function BW_FormatScopeLabel(scopeValue)
          scopeValue = tostring(scopeValue or "bags")
          if scopeValue == "bank" then
            return "Personal Bank"
          end
          if scopeValue == "bagsbank" then
            return "Bags + Personal Bank"
          end
          if scopeValue == "gb" then
            return "Guild Bank"
          end
          if scopeValue == "all" then
            return "Bags + Personal Bank + Guild Bank"
          end
          return "Bags"
        end

        local function BW_FormatSourceLabel(providerStr)
          providerStr = tostring(providerStr or "")
          providerStr = providerStr:gsub("^%s+", ""):gsub("%s+$", "")
          if providerStr == "" then
            return "—"
          end

          local prov, extra = providerStr:match("^([%a_]+)%:(.+)$")
          if not prov then
            prov = providerStr
          end

          prov = tostring(prov or ""):lower()
          extra = tostring(extra or ""):gsub("^%s+", ""):gsub("%s+$", "")

          if prov == "tsm" then
            if extra ~= "" then
              return "TSM (" .. extra .. ")"
            end
            return "TSM"
          end

          if prov == "auctionator" then
            if extra ~= "" then
              return "Auctionator (" .. extra .. ")"
            end
            return "Auctionator"
          end

          if prov == "vendor" then
            return "Vendor"
          end

          if extra ~= "" then
            return tostring(prov) .. " (" .. extra .. ")"
          end

          return providerStr
        end

        local countedList = nil

        -- Build a per-source sectioned list if we have per-scope breakdowns.
        -- Each section gets a divider entry (kind="divider") followed by its items.
        -- Build a per-source sectioned list with collapsible headers.
        -- Each section header is { kind="section", label, total, collapsed, items }
        -- BW_RenderRows flattens visible items inline.
        local function BuildSectionedList(bagsBreakdown, bankBreakdown, gbBreakdown, scope)
          local out = {}
          local function AddSection(label, totalVal, items)
            items = items or {}
            if #items > 1 then
              table.sort(items, function(a, b) return (a.value or 0) > (b.value or 0) end)
            end
            -- Preserve existing collapsed state if section already exists
            local collapsed = false
            if f._bwSectionState and f._bwSectionState[label] ~= nil then
              collapsed = f._bwSectionState[label]
            end
            out[#out + 1] = { kind = "section", label = label, total = totalVal, collapsed = collapsed, items = items }
          end
          local bags = bagsBreakdown or {}
          local bank = bankBreakdown or {}
          local gb   = gbBreakdown   or {}
          if #bags > 0 or #bank > 0 or #gb > 0 then
            local scanScope = tostring(scope or "bags")
            local includeBags = (scanScope == "bags" or scanScope == "bagsbank" or scanScope == "all")
            local includeBank = (scanScope == "bank" or scanScope == "bagsbank" or (scanScope == "all" and payload.bankScanned == true))
            local includeGB = (scanScope == "gb" or scanScope == "all")

            if includeBags then AddSection("Bags", tonumber(payload.bagsTotal) or 0, bags) end
            if includeBank then AddSection("Personal Bank", tonumber(payload.bankTotal) or 0, bank) end
            if includeGB then AddSection("Guild Bank", tonumber(payload.gbTotal) or 0, gb) end
            return out
          end
          return nil
        end

        if type(payload.bagsBreakdown) == "table" or type(payload.bankBreakdown) == "table" or type(payload.gbBreakdown) == "table" then
          local scope = tostring(payload.scope or "bags")
          -- Always section source-aware payloads, even when only one source has
          -- counted items. Bank-only, guild-bank-only, and bags-only scans should
          -- still show the source divider/header instead of flattening the list.
          countedList = BuildSectionedList(payload.bagsBreakdown, payload.bankBreakdown, payload.gbBreakdown, scope)
        end

        if not countedList then
          if type(payload.breakdown) == "table" then
            countedList = {}
            for i = 1, #payload.breakdown do
              local e = payload.breakdown[i]
              if type(e) == "table" and (tonumber(e.value) or 0) > 0 then
                countedList[#countedList + 1] = {
                  link = e.link,
                  name = e.name,
                  itemID = tonumber(e.itemID),
                  count = tonumber(e.count) or 0,
                  each = tonumber(e.each) or 0,
                  value = tonumber(e.value) or 0,
                  category = BW_FormatCategoryLabel(e.category, e.link),
                }
              end
            end
          elseif type(payload.byItem) == "table" then
            countedList = BW_BuildBreakdownFromByItem(payload.byItem, BW_DETAILS_LIMIT)
          elseif type(payload.top) == "table" then
            countedList = {}
            for _, e in ipairs(payload.top) do
              if type(e) == "table" then
                local link = e.link
                local c = tonumber(e.count) or 0
                local v = tonumber(e.value) or 0
                if type(link) == "string" and c > 0 and v > 0 then
                  local each = math.floor((v / c) + 0.5)
                  countedList[#countedList + 1] = {
                    link = link,
                    count = c,
                    each = each,
                    value = v,
                    category = BW_FormatCategoryLabel(e.category, link),
                  }
                end
              end
            end
          else
            countedList = {}
          end
        end

        local skippedList = {}
        local function AddSkippedSection(sourceKey, label, sourceItems)
          if type(sourceItems) ~= "table" or #sourceItems == 0 then return end
          local items = BW_BuildSkippedBreakdown(sourceItems, BW_DETAILS_LIMIT)
          if #items == 0 then return end
          local sectionLabel = "Skipped from " .. tostring(label or "Unknown")
          if #skippedList > 0 then
            skippedList[#skippedList + 1] = { kind = "spacer" }
          end
          skippedList[#skippedList + 1] = {
            kind = "divider",
            sourceKey = sourceKey,
            label = sectionLabel,
            count = #items,
            total = 0,
          }
          for i = 1, #items do
            items[i].source = label
            skippedList[#skippedList + 1] = items[i]
          end
        end

        local hasSourceSkippedPayload = type(payload.bagsSkippedItems) == "table" or type(payload.bankSkippedItems) == "table" or type(payload.gbSkippedItems) == "table"
        if hasSourceSkippedPayload then
          AddSkippedSection("skipped_bags", "Bags", payload.bagsSkippedItems)
          AddSkippedSection("skipped_bank", "Personal Bank", payload.bankSkippedItems)
          AddSkippedSection("skipped_guildbank", "Guild Bank", payload.gbSkippedItems)
        else
          local skippedSrc = payload.skippedBreakdown or payload.skippedItems or payload.skippedTop
          if type(skippedSrc) == "table" then
            local items = BW_BuildSkippedBreakdown(skippedSrc, BW_DETAILS_LIMIT)
            if #items > 0 then
              local fallbackLabel = "Skipped Items"
              if scope == "bags" then
                fallbackLabel = "Skipped from Bags"
              elseif scope == "bank" then
                fallbackLabel = "Skipped from Personal Bank"
              elseif scope == "gb" then
                fallbackLabel = "Skipped from Guild Bank"
              elseif scope == "bagsbank" then
                fallbackLabel = "Skipped from Bags + Personal Bank"
              elseif scope == "all" then
                fallbackLabel = "Skipped from Selected Sources"
              end
              skippedList[#skippedList + 1] = { kind = "divider", sourceKey = "skipped", label = fallbackLabel, count = #items, total = 0 }
              for i = 1, #items do
                skippedList[#skippedList + 1] = items[i]
              end
            end
          end
        end

        -- Flatten sectioned list into _bwData including section header rows
        local function FlattenSections(sectioned)
          local flat = {}
          local previousSectionHadVisibleItems = false
          for i = 1, #sectioned do
            local sec = sectioned[i]
            local items = sec.items or {}
            if #flat > 0 and previousSectionHadVisibleItems then
              flat[#flat + 1] = { kind = "spacer" }
            end
            flat[#flat + 1] = sec  -- section header row (kind="section")
            previousSectionHadVisibleItems = false
            if not sec.collapsed then
              for j = 1, #items do
                flat[#flat + 1] = items[j]
              end
              previousSectionHadVisibleItems = (#items > 0)
            end
          end
          return flat
        end

        if not f._bwSectionState then f._bwSectionState = {} end

        -- Store raw sections for collapse toggling
        if type(countedList) == "table" and #countedList > 0 and countedList[1].kind == "section" then
          f._bwRawSections = countedList
          countedList = FlattenSections(countedList)
        else
          f._bwRawSections = nil
        end

        self._bwCountedData = countedList or {}
        self._bwSkippedData = skippedList or {}
        self:_BW_UpdateTabCounts()

        local priced = tonumber(payload.pricedStacks) or 0
        local skipped = tonumber(payload.skippedStacks) or 0
        local scopeLabel = BW_FormatScopeLabel(scope)
        local sourceLabel = BW_FormatSourceLabel(payload.source)
        local bankScanned = (payload.bankScanned == true) or scope == "bank" or scope == "bagsbank" or scope == "all"
        local gbScanned = (scope == "gb" or scope == "all")
        local pbText = bankScanned and Money(Util:Round(bank)) or "Not scanned"
        local gbText = gbScanned and Money(Util:Round(gb)) or "Not scanned"

        if self._bwMetaTotal and self._bwMetaTotal.SetText then
          self._bwMetaTotal:SetText(Money(Util:Round(total)))
        end
        if self._bwMetaScanned and self._bwMetaScanned.SetText then
          self._bwMetaScanned:SetText(scopeLabel)
        end
        if self._bwMetaSource and self._bwMetaSource.SetText then
          self._bwMetaSource:SetText(sourceLabel)
        end
        if self._bwMetaBags and self._bwMetaBags.SetText then
          self._bwMetaBags:SetText(Money(Util:Round(bags)))
        end
        if self._bwMetaPB and self._bwMetaPB.SetText then
          self._bwMetaPB:SetText(pbText)
        end
        if self._bwMetaGB and self._bwMetaGB.SetText then
          self._bwMetaGB:SetText(gbText)
        end
        if self._bwMetaPriced and self._bwMetaPriced.SetText then
          local pricedQty = tonumber(payload.pricedQty) or 0
          self._bwMetaPriced:SetText(tostring(priced) .. " stacks / " .. tostring(pricedQty) .. " qty")
        end
        if self._bwMetaSkipped and self._bwMetaSkipped.SetText then
          local skippedQty = tonumber(payload.skippedQty) or 0
          self._bwMetaSkipped:SetText(tostring(skipped) .. " stacks / " .. tostring(skippedQty) .. " qty")
        end

        if self._bwView ~= "skipped" and self._bwView ~= "counted" then
          self._bwView = "counted"
        end
        self:_BW_SetView(self._bwView or "counted")
      end

      tabCounted:SetScript("OnClick", function()
        f:_BW_SetView("counted")
      end)
      tabSkipped:SetScript("OnClick", function()
        f:_BW_SetView("skipped")
      end)

      scroll:EnableMouseWheel(true)
      scroll:SetScript("OnMouseWheel", function(_, delta)
        local cur = scroll:GetVerticalScroll() or 0
        local step = 36
        local nextV = cur - (delta * step)
        if nextV < 0 then nextV = 0 end
        local range = scroll:GetVerticalScrollRange() or 0
        if nextV > range then nextV = range end
        scroll:SetVerticalScroll(nextV)
      end)

      f:_BW_UpdateTabCounts()
      f:_BW_UpdateTabVisuals()
      f:_BW_UpdateSortHeader()

      UI._bwDetailsFrame = f
      return f
    end
    local function BW_OpenDetails()
      if InCombatLockdown and InCombatLockdown() then
        if DEFAULT_CHAT_FRAME then
          DEFAULT_CHAT_FRAME:AddMessage("|cff66c2ffWhatFarm:|r BagWorth details will open after combat.")
        end
        local frame = CreateFrame("Frame")
        frame:RegisterEvent("PLAYER_REGEN_ENABLED")
        frame:SetScript("OnEvent", function(self)
          self:UnregisterAllEvents()
          BW_OpenDetails()
        end)
        return
      end

      local payload = UI._bwLastPayload
      if type(payload) ~= "table" then
        if UIErrorsFrame and UIErrorsFrame.AddMessage then
          UIErrorsFrame:AddMessage("WhatFarm: Press 'Refresh Worth' first to generate a breakdown.", 0.35, 0.78, 1.00)
        end
        return
      end

      local f = BW_EnsureDetailsFrame()
      if f and f._BW_SetData then
        f:_BW_SetData(payload)
      end
      f:Show()
      BW_ClaimUtilityWindowFocus(f)
    end

    BW_SetDetailsEnabled(false)



    -- -----------------------------------------------------
    -- Farm Timer (under Bag Worth)
    -- Self-contained: does NOT touch token/bag worth/random farm wiring
    -- -----------------------------------------------------
    local rightBottom = bBW

    local FT = WF.FarmTimer
    if not FT then
      FT = {}
      WF.FarmTimer = FT
    end

    if not FT.cfg then
      FT.cfg = {
        kill = "1:45",
        loot = "0:15",
        grace = "0:05",
        loop = true,
        party = false,
        hud = true,
        hudUnlock = false,
        warn = 10,
      }
    end
    FT.state = FT.state or { running = false, paused = false, pauseRem = nil, phase = "Kill", endsAt = 0, warned = false }

    if not FT._inited then
      FT._inited = true

      local function _Trim(s)
        s = tostring(s or "")
        s = s:gsub("^%s+", ""):gsub("%s+$", "")
        return s
      end

      function FT:ParseTime(s)
        s = _Trim(s)
        if s == "" then return 0 end

        local a, b, c = s:match("^(%d+)%:(%d+)%:(%d+)$")
        if a then
          return (tonumber(a) or 0) * 3600 + (tonumber(b) or 0) * 60 + (tonumber(c) or 0)
        end

        local m, ss = s:match("^(%d+)%:(%d+)$")
        if m then
          return (tonumber(m) or 0) * 60 + (tonumber(ss) or 0)
        end

        local n = tonumber(s)
        if n then return math.floor(n + 0.00001) end
        return 0
      end

      function FT:FmtClock(sec)
        sec = tonumber(sec) or 0
        if sec < 0 then sec = 0 end
        sec = math.floor(sec + 0.00001)
        local m = math.floor(sec / 60)
        local s = sec % 60
        return string.format("%d:%02d", m, s)
      end

      FT._hudLockKey = FT._hudLockKey or "uiLocked_farmTimerHUD"

      function FT:IsHUDUnlocked()
        if Widgets and Widgets.IsLocked and WF and WF.DB and WF.DB.GetSetting then
          local locked = WF.DB:GetSetting(self._hudLockKey)
          if locked == nil then
            return self.cfg and self.cfg.hudUnlock and true or false
          end
          return not locked
        end
        return self.cfg and self.cfg.hudUnlock and true or false
      end

      function FT:SetHUDUnlocked(unlocked)
        unlocked = unlocked and true or false
        if self.cfg then
          self.cfg.hudUnlock = unlocked
        end
        if Widgets and Widgets.SetLocked then
          Widgets:SetLocked(self._hudLockKey, not unlocked)
        end
      end

      function FT:_PersistHUDSize()
        if not (self and self.hud and self.cfg) then return end
        local w, h = self.hud:GetSize()
        self.cfg.hudSize = {
          math.floor((w or 0) + 0.5),
          math.floor((h or 0) + 0.5),
        }
      end

      function FT:_SyncHUDLockCheckbox()
        if self._uiCbUnlock and self._uiCbUnlock.SetChecked then
          self._uiCbUnlock:SetChecked(self:IsHUDUnlocked() and true or false)
        end
      end

      function FT:_EnsureTicker()
        if FT._ticker then return end
        local f = CreateFrame("Frame")
        f:Hide()
        f:SetScript("OnUpdate", function(_, _)
          FT:Tick()
        end)
        FT._ticker = f
      end

      function FT:_DoFlash()
        if not UIParent then return end

        local ar, ag, ab = C4(ACCENT, TEXT)
        ar = tonumber(ar) or 0.20
        ag = tonumber(ag) or 0.55
        ab = tonumber(ab) or 1.00

        if not self._flashFrame then
          local flash = CreateFrame("Frame", nil, UIParent)
          flash:SetAllPoints(UIParent)
          flash:SetFrameStrata("FULLSCREEN")
          flash:SetFrameLevel(2)
          flash:EnableMouse(false)
          flash:SetAlpha(0)
          flash:Hide()

          local tex = flash:CreateTexture(nil, "BACKGROUND")
          tex:SetAllPoints(flash)
          tex:SetTexture("Interface/Buttons/WHITE8X8")
          tex:SetBlendMode("ADD")
          tex:SetVertexColor(ar, ag, ab, 1)

          local anim = flash:CreateAnimationGroup()

          local pulseIn1 = anim:CreateAnimation("Alpha")
          pulseIn1:SetOrder(1)
          pulseIn1:SetFromAlpha(0.00)
          pulseIn1:SetToAlpha(0.12)
          pulseIn1:SetDuration(0.08)

          local pulseOut1 = anim:CreateAnimation("Alpha")
          pulseOut1:SetOrder(2)
          pulseOut1:SetFromAlpha(0.12)
          pulseOut1:SetToAlpha(0.04)
          pulseOut1:SetDuration(0.10)

          local pulseIn2 = anim:CreateAnimation("Alpha")
          pulseIn2:SetOrder(3)
          pulseIn2:SetFromAlpha(0.04)
          pulseIn2:SetToAlpha(0.09)
          pulseIn2:SetDuration(0.07)

          local pulseOut2 = anim:CreateAnimation("Alpha")
          pulseOut2:SetOrder(4)
          pulseOut2:SetFromAlpha(0.09)
          pulseOut2:SetToAlpha(0.00)
          pulseOut2:SetDuration(0.24)

          anim:SetScript("OnPlay", function()
            flash:SetAlpha(0)
            flash:Show()
          end)
          anim:SetScript("OnFinished", function()
            flash:SetAlpha(0)
            flash:Hide()
          end)

          self._flashFrame = flash
          self._flashTex = tex
          self._flashAnim = anim
        end

        if self._flashTex and self._flashTex.SetVertexColor then
          self._flashTex:SetVertexColor(ar, ag, ab, 1)
        end

        if self._flashAnim and self._flashAnim.IsPlaying and self._flashAnim:IsPlaying() then
          self._flashAnim:Stop()
        end

        if self._flashFrame then
          self._flashFrame:SetAlpha(0)
          self._flashFrame:Show()
        end

        if self._flashAnim then
          self._flashAnim:Play()
        end
      end
            function FT:_EnsureHUD()
        if FT.hud then return end

        local tr, tg, tb = C4(TEXT, TEXT)
        local mr, mg, mb = C4(MUTED, MUTED)
        local ar, ag, ab = C4(ACCENT, TEXT)

        local bodyFont = (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.body) or BODY_FONT
        local displayFont = (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.display) or DISPLAY_FONT

        if Widgets and Widgets.SetLocked and WF and WF.DB and WF.DB.GetSetting then
          local savedLock = WF.DB:GetSetting(FT._hudLockKey)
          if savedLock == nil then
            Widgets:SetLocked(FT._hudLockKey, not (FT.cfg and FT.cfg.hudUnlock and true or false))
          else
            FT.cfg.hudUnlock = not savedLock
          end
        end

        local hud = CreateFrame("Frame", "WhatFarm_FarmTimerHUD", UIParent, (BackdropTemplateMixin and "BackdropTemplate") or nil)
        hud:SetSize(268, 44)
        hud:SetFrameStrata("HIGH")
        hud:SetFrameLevel(50)
        hud:SetClampedToScreen(true)
        hud:SetMovable(true)
        hud:SetResizable(true)
        if hud.SetMinResize then
          hud:SetMinResize(236, 40)
        end
        if hud.SetMaxResize then
          hud:SetMaxResize(360, 56)
        end
        hud:EnableMouse(true)

        if hud.SetBackdrop then
          hud:SetBackdrop({
            bgFile = "Interface/Buttons/WHITE8X8",
            edgeFile = "Interface/Buttons/WHITE8X8",
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
          })
          local pr, pg, pb = C4(PANEL, PANEL)
          pr = pr or 0.10
          pg = pg or 0.10
          pb = pb or 0.10
          hud:SetBackdropColor(pr, pg, pb, 0.96)
          hud:SetBackdropBorderColor(mr, mg, mb, 0.28)
        end

        hud.topAccent = hud:CreateTexture(nil, "BORDER")
        hud.topAccent:SetHeight(1)
        hud.topAccent:SetPoint("TOPLEFT", 1, -1)
        hud.topAccent:SetPoint("TOPRIGHT", -1, -1)
        hud.topAccent:SetColorTexture(mr, mg, mb, 0.22)

        hud.dragZone = CreateFrame("Frame", nil, hud)
        hud.dragZone:SetPoint("TOPLEFT", 8, -5)
        hud.dragZone:SetPoint("TOPRIGHT", -24, -5)
        hud.dragZone:SetHeight(13)
        hud.dragZone:EnableMouse(true)
        hud.dragZone:RegisterForDrag("LeftButton")

        hud.title = hud:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
        hud.title:SetPoint("TOPLEFT", 10, -7)
        hud.title:SetJustifyH("LEFT")
        hud.title:SetTextColor(tr, tg, tb, 0.92)
        hud.title:SetText("WF Timer")
        if hud.title.SetFont then
          hud.title:SetFont(bodyFont, 10, "")
        end

        hud.phaseText = hud:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
        hud.phaseText:SetPoint("LEFT", hud.title, "RIGHT", 10, 0)
        hud.phaseText:SetJustifyH("LEFT")
        hud.phaseText:SetTextColor(mr, mg, mb, 0.92)
        hud.phaseText:SetText("")
        if hud.phaseText.SetFont then
          hud.phaseText:SetFont(bodyFont, 10, "")
        end

        hud.stateText = hud:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
        hud.stateText:SetPoint("TOPRIGHT", -10, -7)
        hud.stateText:SetJustifyH("RIGHT")
        hud.stateText:SetTextColor(ar, ag, ab, 0.96)
        hud.stateText:SetText("")
        if hud.stateText.SetFont then
          hud.stateText:SetFont(bodyFont, 10, "")
        end

        hud.timeText = hud:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
        hud.timeText:SetPoint("BOTTOMLEFT", 10, 6)
        hud.timeText:SetJustifyH("LEFT")
        hud.timeText:SetTextColor(ar, ag, ab, 1)
        hud.timeText:SetText("0:00")
        if hud.timeText.SetFont then
          hud.timeText:SetFont(displayFont, 18, "")
        end
        if hud.timeText.SetShadowColor then
          hud.timeText:SetShadowColor(0, 0, 0, 0.55)
        end
        if hud.timeText.SetShadowOffset then
          hud.timeText:SetShadowOffset(1, -1)
        end

        local function _HUDButton(parent, label, w)
          local b = CreateFrame("Button", nil, parent, (BackdropTemplateMixin and "BackdropTemplate") or nil)
          b:SetSize(w or 52, 18)
          b:EnableMouse(true)

          if b.SetBackdrop then
            b:SetBackdrop({
              bgFile = "Interface/Buttons/WHITE8X8",
              edgeFile = "Interface/Buttons/WHITE8X8",
              edgeSize = 1,
              insets = { left = 1, right = 1, top = 1, bottom = 1 },
            })
            local pr, pg, pb = C4(PANEL, PANEL)
            pr = pr or 0.10
            pg = pg or 0.10
            pb = pb or 0.10
            b:SetBackdropColor(pr + 0.02, pg + 0.02, pb + 0.02, 0.98)
            b:SetBackdropBorderColor(mr, mg, mb, 0.24)
          end

          b.txt = b:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
          b.txt:SetPoint("CENTER", 0, 0)
          b.txt:SetJustifyH("CENTER")
          b.txt:SetTextColor(tr, tg, tb, 0.94)
          b.txt:SetText(label)
          if b.txt.SetFont then
            b.txt:SetFont(bodyFont, 10, "")
          end

          b:SetScript("OnEnter", function()
            local pr, pg, pb = C4(PANEL, PANEL)
            pr = pr or 0.10
            pg = pg or 0.10
            pb = pb or 0.10
            if b.SetBackdropColor then b:SetBackdropColor(pr + 0.04, pg + 0.04, pb + 0.04, 1) end
            if b.SetBackdropBorderColor then b:SetBackdropBorderColor(ar, ag, ab, 0.38) end
          end)
          b:SetScript("OnLeave", function()
            local pr, pg, pb = C4(PANEL, PANEL)
            pr = pr or 0.10
            pg = pg or 0.10
            pb = pb or 0.10
            if b.SetBackdropColor then b:SetBackdropColor(pr + 0.02, pg + 0.02, pb + 0.02, 0.98) end
            if b.SetBackdropBorderColor then b:SetBackdropBorderColor(mr, mg, mb, 0.24) end
            GameTooltip:Hide()
          end)

          return b
        end

        hud.btnStop = _HUDButton(hud, "Stop", 46)
        hud.btnStop:SetPoint("BOTTOMRIGHT", -22, 6)

        hud.btnPause = _HUDButton(hud, "Pause", 54)
        hud.btnPause:SetPoint("RIGHT", hud.btnStop, "LEFT", -6, 0)

        hud.btnStop:SetScript("OnClick", function()
          FT:Stop()
        end)

        hud.btnPause:SetScript("OnClick", function()
          if not (FT.state and FT.state.running) then return end
          if FT.state.paused then
            FT:Resume()
          else
            FT:Pause()
          end
        end)

        hud.dragZone:SetScript("OnDragStart", function()
          local unlocked = FT:IsHUDUnlocked()
          if unlocked and hud.StartMoving then
            hud._wfDragging = true
            hud:StartMoving()
          end
        end)

        hud.dragZone:SetScript("OnDragStop", function()
          if not hud._wfDragging then return end
          hud._wfDragging = nil
          hud:StopMovingOrSizing()
          if FT.cfg then
            local p, rel, rp, x, y = hud:GetPoint(1)
            FT.cfg.hudPoint = { p or "CENTER", rp or "CENTER", x or 0, y or 0 }
          end
        end)

        hud:SetScript("OnDragStop", function()
          if not hud._wfDragging then return end
          hud._wfDragging = nil
          hud:StopMovingOrSizing()
          if FT.cfg then
            local p, rel, rp, x, y = hud:GetPoint(1)
            FT.cfg.hudPoint = { p or "CENTER", rp or "CENTER", x or 0, y or 0 }
          end
        end)

        if Widgets and Widgets.AttachCornerLockGrip then
          hud.resizeGrip = Widgets:AttachCornerLockGrip(hud, {
            lockKey = FT._hudLockKey,
            size = 14,
            x = -4,
            y = 4,
            alpha = 0.55,
            noLockText = true,
            onResizeStart = function()
              if hud.StopMovingOrSizing then
                hud:StopMovingOrSizing()
              end
            end,
            onResizeStop = function()
              FT:_PersistHUDSize()
            end,
          })

          if hud.resizeGrip and hud.resizeGrip.HookScript then
            local function _SyncHUDGripState()
              if FT.cfg then
                FT.cfg.hudUnlock = FT:IsHUDUnlocked()
              end
              FT:_SyncHUDLockCheckbox()
            end

            hud.resizeGrip:HookScript("OnMouseDown", _SyncHUDGripState)
            hud.resizeGrip:HookScript("OnMouseUp", _SyncHUDGripState)
            hud.resizeGrip:HookScript("OnEnter", function(selfBtn)
              if not GameTooltip or not GameTooltip.SetOwner then return end
              if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
              GameTooltip:SetOwner(selfBtn, "ANCHOR_LEFT")
              GameTooltip:ClearLines()
              GameTooltip:AddLine("Farm Timer HUD", ACCENT[1], ACCENT[2], ACCENT[3], true)
              if FT:IsHUDUnlocked() then
                GameTooltip:AddLine("Drag the corner grip to resize. Drag the top bar to move. Click the grip again to lock it.", 0.7, 0.7, 0.7, true)
              else
                GameTooltip:AddLine("Click the corner grip to unlock moving and resizing.", 0.7, 0.7, 0.7, true)
              end
              GameTooltip:Show()
            end)
            hud.resizeGrip:HookScript("OnLeave", function()
              GameTooltip:Hide()
            end)
          end
        end

        hud:Hide()
        FT.hud = hud

        if FT.cfg and FT.cfg.hudSize then
          local w = tonumber(FT.cfg.hudSize[1]) or 268
          local h = tonumber(FT.cfg.hudSize[2]) or 44
          if w < 236 then w = 236 end
          if h < 40 then h = 40 end
          if w > 360 then w = 360 end
          if h > 56 then h = 56 end
          hud:SetSize(w, h)
        end

        if FT.cfg and FT.cfg.hudPoint then
          local p = FT.cfg.hudPoint[1] or "CENTER"
          local rp = FT.cfg.hudPoint[2] or "CENTER"
          local x = FT.cfg.hudPoint[3] or 0
          local y = FT.cfg.hudPoint[4] or 0
          hud:ClearAllPoints()
          hud:SetPoint(p, UIParent, rp, x, y)
        else
          hud:SetPoint("CENTER", UIParent, "CENTER", 0, 220)
        end

        if FT.cfg then
          FT.cfg.hudUnlock = FT:IsHUDUnlocked()
        end
      end

      function FT:_ShowHUD(show)
        FT:_EnsureHUD()
        if not FT.hud then return end
        if show then
          FT.hud:Show()
        else
          FT.hud:Hide()
        end
      end

      function FT:_RaidWarn(msg)
        msg = tostring(msg or "")
        if msg == "" then return end

        local ar, ag, ab = C4(ACCENT, TEXT)

        if RaidNotice_AddMessage and RaidWarningFrame then
          RaidNotice_AddMessage(RaidWarningFrame, msg, { r = ar, g = ag, b = ab }, 1.2)
        end

        if PlaySound and SOUNDKIT and SOUNDKIT.RAID_WARNING then
          pcall(PlaySound, SOUNDKIT.RAID_WARNING, "Master")
        end
      end

      function FT:_GroupAnnounce(msg)
        if not (FT.cfg and FT.cfg.party) then return end
        if not SendChatMessage then return end

        msg = tostring(msg or "")
        if msg == "" then return end

        local inRaid = (IsInRaid and IsInRaid()) and true or false
        local inGroup = (IsInGroup and IsInGroup()) and true or false
        if not (inRaid or inGroup) then return end

        local channels = {}
        if inRaid then
          local isLead = (UnitIsGroupLeader and UnitIsGroupLeader("player")) or false
          local isAssist = (UnitIsGroupAssistant and UnitIsGroupAssistant("player")) or false
          if isLead or isAssist then
            channels[#channels + 1] = "RAID_WARNING"
          end
          channels[#channels + 1] = "RAID"
        elseif inGroup then
          channels[#channels + 1] = "PARTY"
        end

        if #channels <= 0 then return end

        local attempts = 2
        local delays = { 0.00, 0.75 }

        local function _Send(ch)
          for i = 1, #ch do
            pcall(SendChatMessage, msg, ch[i])
          end
        end

        _Send(channels)

        for i = 2, attempts do
          local delay = delays[i] or ((i - 1) * 0.75)
          if C_Timer and C_Timer.After then
            C_Timer.After(delay, function()
              if not (FT and FT.cfg and FT.cfg.party) then return end

              local stillInRaid = (IsInRaid and IsInRaid()) and true or false
              local stillInGroup = (IsInGroup and IsInGroup()) and true or false
              if not (stillInRaid or stillInGroup) then return end

              local stillChannels = {}
              if stillInRaid then
                local stillLead = (UnitIsGroupLeader and UnitIsGroupLeader("player")) or false
                local stillAssist = (UnitIsGroupAssistant and UnitIsGroupAssistant("player")) or false
                if stillLead or stillAssist then
                  stillChannels[#stillChannels + 1] = "RAID_WARNING"
                end
                stillChannels[#stillChannels + 1] = "RAID"
              elseif stillInGroup then
                stillChannels[#stillChannels + 1] = "PARTY"
              end

              if #stillChannels > 0 then
                _Send(stillChannels)
              end
            end)
          end
        end
      end

      -- Human-friendly announce strings (raid warning + optional party announce)
      function FT:_PhaseStartMsg(phase)
        phase = tostring(phase or "Kill")
        if phase == "Kill" then return "WhatFarm: Start Killing" end
        if phase == "Loot" then return "WhatFarm: Start Looting" end
        if phase == "Grace" then return "WhatFarm: Reset" end
        return "WhatFarm: " .. phase
      end

      function FT:_PhaseWarnMsg(phase, sec)
        phase = tostring(phase or "")
        sec = tonumber(sec) or 0

        if phase == "Kill" then return "WhatFarm: Killing ends in " .. sec .. "s" end
        if phase == "Loot" then return "WhatFarm: Looting ends in " .. sec .. "s" end
        if phase == "Grace" then return "WhatFarm: Reset ends in " .. sec .. "s" end

        if phase == "" then
          return "WhatFarm: " .. sec .. "s"
        end

        return "WhatFarm: " .. phase .. " ends in " .. sec .. "s"
      end

      function FT:_SetPhase(phase, dur)
        self.state.phase = phase or "Kill"
        self.state.endsAt = (GetTime() or 0) + (tonumber(dur) or 0)
        self.state.warned = false
        self:UpdateUI(true)
      end

      function FT:Start()
        local cfg = self.cfg or {}
        local k = self:ParseTime(cfg.kill)
        local l = self:ParseTime(cfg.loot)
        local g = self:ParseTime(cfg.grace)

        if k < 1 then k = 1 end
        if l < 0 then l = 0 end
        if g < 0 then g = 0 end

        self.state.running = true
        self.state.paused = false
        self.state.pauseRem = nil
        self.state.k = k
        self.state.l = l
        self.state.g = g

        self:_EnsureTicker()
        self._ticker:Show()

        if cfg.hud then
          self:_ShowHUD(true)
        else
          self:_ShowHUD(false)
        end

        self:_SetPhase("Kill", k)
        local msg = self:_PhaseStartMsg("Kill")
        self:_RaidWarn(msg)
        self:_GroupAnnounce(msg)
      end

      function FT:Stop()
        self.state.running = false
        self.state.paused = false
        self.state.pauseRem = nil
        self.state.warned = false
        if self._ticker then self._ticker:Hide() end
        self:_ShowHUD(false)
        self:UpdateUI(true)
      end

      function FT:Pause()
        if not (self.state and self.state.running) then return end
        if self.state.paused then return end

        local now = GetTime() or 0
        local rem = (self.state.endsAt or 0) - now
        if rem < 0 then rem = 0 end

        self.state.paused = true
        self.state.pauseRem = rem

        if self._ticker then self._ticker:Hide() end
        self:UpdateUI(true)
      end

      function FT:Resume()
        if not (self.state and self.state.running) then return end
        if not self.state.paused then return end

        local now = GetTime() or 0
        local rem = tonumber(self.state.pauseRem) or 0
        if rem < 0 then rem = 0 end

        self.state.paused = false
        self.state.pauseRem = nil
        self.state.endsAt = now + rem

        self:_EnsureTicker()
        if self._ticker then self._ticker:Show() end
        self:UpdateUI(true)
      end

      function FT:NextPhase()
        local st = self.state
        local cfg = self.cfg or {}
        if st.phase == "Kill" then
          if (st.l or 0) > 0 then
            self:_SetPhase("Loot", st.l)
            local msg = self:_PhaseStartMsg("Loot")
            self:_RaidWarn(msg)
            self:_GroupAnnounce(msg)
            return
          end
          if (st.g or 0) > 0 then
            self:_SetPhase("Grace", st.g)
            local msg = self:_PhaseStartMsg("Grace")
            self:_RaidWarn(msg)
            self:_GroupAnnounce(msg)
            return
          end
          if cfg.loop then
            self:_SetPhase("Kill", st.k)
            local msg = self:_PhaseStartMsg("Kill")
            self:_RaidWarn(msg)
            self:_GroupAnnounce(msg)
          else
            self:Stop()
          end
          return
        end

        if st.phase == "Loot" then
          if (st.g or 0) > 0 then
            self:_SetPhase("Grace", st.g)
            local msg = self:_PhaseStartMsg("Grace")
            self:_RaidWarn(msg)
            self:_GroupAnnounce(msg)
            return
          end
          if cfg.loop then
            self:_SetPhase("Kill", st.k)
            local msg = self:_PhaseStartMsg("Kill")
            self:_RaidWarn(msg)
            self:_GroupAnnounce(msg)
          else
            self:Stop()
          end
          return
        end

        -- Grace -> back to Kill (loop) or stop
        if cfg.loop then
          self:_SetPhase("Kill", st.k)
          local msg = self:_PhaseStartMsg("Kill")
          self:_RaidWarn(msg)
          self:_GroupAnnounce(msg)
        else
          self:Stop()
        end
      end

      function FT:UpdateUI(force)
        local st = self.state
        if not st then return end

        local phase = st.phase or "Kill"
        local rem = 0

        if st.running then
          if st.paused then
            rem = tonumber(st.pauseRem)
            if not rem then
              rem = (st.endsAt or 0) - (GetTime() or 0)
            end
          else
            rem = (st.endsAt or 0) - (GetTime() or 0)
          end
        end

        if rem < 0 then rem = 0 end

        local ar, ag, ab = C4(ACCENT, TEXT)

        local mr, mg, mb = C4(MUTED, MUTED)
        local now = GetTime() or 0
        local pulse = (math.floor(now * 2) % 2) == 0

        if self._uiStatus and self._uiStatus.SetText then
          if st.running then
            local ar8 = math.floor((ar or 0) * 255 + 0.5)
            local ag8 = math.floor((ag or 0) * 255 + 0.5)
            local ab8 = math.floor((ab or 0) * 255 + 0.5)
            local mr8 = math.floor((mr or 0) * 255 + 0.5)
            local mg8 = math.floor((mg or 0) * 255 + 0.5)
            local mb8 = math.floor((mb or 0) * 255 + 0.5)

            local timeCode
            if st.paused then
              local pa = pulse and 255 or 140
              timeCode = string.format("|c%02x%02x%02x%02x", pa, mr8, mg8, mb8)
            else
              timeCode = string.format("|cff%02x%02x%02x", ar8, ag8, ab8)
            end

            local pausedCode = string.format("|cff%02x%02x%02x", mr8, mg8, mb8)
            local line = tostring(phase) .. " • " .. timeCode .. self:FmtClock(rem) .. "|r"
            if st.paused then
              line = line .. " • " .. pausedCode .. "Paused|r"
            end

            self._uiStatus:SetText(line)
            if self._uiStatus.SetTextColor then
              self._uiStatus:SetTextColor(C4(TEXT, TEXT))
            end
          else
            self._uiStatus:SetText("")
            if self._uiStatus.SetTextColor then
              self._uiStatus:SetTextColor(C4(MUTED, MUTED))
            end
          end
        end

        -- update overview buttons (Start -> Pause/Resume + Stop)
        if self._uiLayoutButtons then
          local key = 0
          if st.running then key = 10 end
          if st.paused then key = key + 1 end

          if force or (self._uiBtnState ~= key) then
            self._uiBtnState = key
            self._uiLayoutButtons(st.running)
          end
        end

        if self._uiStart and self._uiStart.SetText then
          if st.running then
            if st.paused then
              self._uiStart:SetText("Resume")
            else
              self._uiStart:SetText("Pause")
            end
          else
            self._uiStart:SetText("Start Timer")
          end
        end

        if self._uiStop then
          if st.running then
            if self._uiStop.Show then self._uiStop:Show() end
          else
            if self._uiStop.Hide then self._uiStop:Hide() end
          end
        end

        -- HUD update
        if self.hud then
          if st.running then
            if self.hud.title and self.hud.title.SetText then
              self.hud.title:SetText("WF Timer")
            end

            if self.hud.phaseText and self.hud.phaseText.SetText then
              if phase and phase ~= "" then
                self.hud.phaseText:SetText("Phase: " .. tostring(phase))
              else
                self.hud.phaseText:SetText("")
              end
            end

            if self.hud.stateText and self.hud.stateText.SetText then
              if st.paused then
                self.hud.stateText:SetText("Paused")
              else
                self.hud.stateText:SetText("")
              end
            end

            if self.hud.timeText and self.hud.timeText.SetText then
              self.hud.timeText:SetText(self:FmtClock(rem))
              if self.hud.timeText.SetTextColor then
                if st.paused then
                  self.hud.timeText:SetTextColor(mr, mg, mb, 0.88)
                else
                  self.hud.timeText:SetTextColor(ar, ag, ab, 1)
                end
              end
            end

            if self.hud.btnPause and self.hud.btnPause.txt and self.hud.btnPause.txt.SetText then
              if st.paused then
                self.hud.btnPause.txt:SetText("Resume")
              else
                self.hud.btnPause.txt:SetText("Pause")
              end
            end

            if self.hud.btnPause then
              if self.hud.btnPause.Show then self.hud.btnPause:Show() end
            end
            if self.hud.btnStop then
              if self.hud.btnStop.Show then self.hud.btnStop:Show() end
            end
          else
            if self.hud.phaseText and self.hud.phaseText.SetText then
              self.hud.phaseText:SetText("")
            end
            if self.hud.stateText and self.hud.stateText.SetText then
              self.hud.stateText:SetText("")
            end
            if self.hud.btnPause then
              if self.hud.btnPause.Hide then self.hud.btnPause:Hide() end
            end
            if self.hud.btnStop then
              if self.hud.btnStop.Hide then self.hud.btnStop:Hide() end
            end
          end
        end
      end

      function FT:Tick()
        if not (self.state and self.state.running) then return end
        if self.state.paused then
          self:UpdateUI(false)
          return
        end

        local now = GetTime() or 0
        local rem = (self.state.endsAt or 0) - now
        if rem < 0 then rem = 0 end

        local warnAt = tonumber((self.cfg and self.cfg.warn) or 10) or 10
        if warnAt < 1 then warnAt = 1 end

        if (not self.state.warned) and rem <= warnAt then
          self.state.warned = true
          local msg = self:_PhaseWarnMsg(self.state.phase, warnAt)
          self:_RaidWarn(msg)
          self:_GroupAnnounce(msg)
          self:_DoFlash()
        end

        if rem <= 0 then
          self:NextPhase()
          return
        end

        self:UpdateUI(false)
      end
    end

        -- UI section ----------------------------------------------------------
    local ftDiv = MakeDivider(rSC)
    ftDiv:SetPoint("LEFT", rSC, "LEFT", 12, 0)
    ftDiv:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    ftDiv:SetPoint("TOP", rightBottom, "BOTTOM", 0, -14)

    local ftHeader = CreateFrame("Frame", nil, rSC)
    ftHeader:SetPoint("TOPLEFT", ftDiv, "BOTTOMLEFT", 0, -14)
    ftHeader:SetPoint("TOPRIGHT", ftDiv, "BOTTOMRIGHT", 0, -14)
    ftHeader:SetHeight(20)

    local ftTitle = MakeSectionTitle(ftHeader, "Farm Timer")
    ftTitle:SetPoint("LEFT", 0, 0)

    local ftOptBtn = CreateFrame("Button", nil, ftHeader)
    ftOptBtn:SetSize(64, 18)
    ftOptBtn:SetPoint("RIGHT", 0, 0)

    ftOptBtn.bg = ftOptBtn:CreateTexture(nil, "BACKGROUND")
    ftOptBtn.bg:SetAllPoints(ftOptBtn)
    ftOptBtn.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    ftOptBtn.bg:SetVertexColor(1, 1, 1, 1)
    ftOptBtn.bg:SetAlpha(0)

    ftOptBtn.txt = ftOptBtn:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    ftOptBtn.txt:SetPoint("CENTER", 0, 0)
    ftOptBtn.txt:SetText("Options")
    do
      local mr, mg, mb = C4(MUTED, MUTED)
      if ftOptBtn.txt then ftOptBtn.txt:SetTextColor(mr, mg, mb, 1) end
    end


    _WF_ApplyGearOptionButton(ftOptBtn)
    ftOptBtn:SetScript("OnEnter", function()
      _WF_GearOpt_SetHover(ftOptBtn, true)
      if GameTooltip then
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(ftOptBtn, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end

        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine("Farm Timer Options", ar, ag, ab)
        GameTooltip:AddLine("Toggle the options panel below.", TEXT[1], TEXT[2], TEXT[3], true)
        GameTooltip:AddLine("Edit phase durations, warnings, and HUD settings.", 0.75, 0.75, 0.75, true)
        GameTooltip:Show()
      end
    end)
    ftOptBtn:SetScript("OnLeave", function()
      _WF_GearOpt_SetHover(ftOptBtn, false)
      if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
    end)

    local ftStatus = MakeMutedText(ftHeader, "")
    ftStatus:SetPoint("LEFT", ftTitle, "RIGHT", 10, 0)
    ftStatus:SetPoint("RIGHT", ftOptBtn, "LEFT", -8, 0)
    ftStatus:SetJustifyH("RIGHT")
    if ftStatus.SetWordWrap then ftStatus:SetWordWrap(false) end
    -- Distinct phase cards (Kill / Loot / Grace)
    
    -- Farm Timer styling: rounded corners for phase cards / buttons (match addon look)
    local function _FT_SetRoundedBackdrop(frame, bgA, borderA, edgeSize, inset)
      if not (frame and frame.SetBackdrop) then return end
      edgeSize = tonumber(edgeSize) or 10
      inset = tonumber(inset) or 3

      frame:SetBackdrop({
        bgFile = "Interface/Buttons/WHITE8X8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = edgeSize,
        insets = { left = inset, right = inset, top = inset, bottom = inset },
      })

      if frame.SetBackdropColor then
        frame:SetBackdropColor(0, 0, 0, tonumber(bgA) or 0.16)
      end
      if frame.SetBackdropBorderColor then
        frame:SetBackdropBorderColor(1, 1, 1, tonumber(borderA) or 0.10)
      end
    end

    local function _FT_RoundExistingBackdrop(frame, edgeSize, inset)
      if not (frame and frame.SetBackdrop) then return end

      local bd = (frame.GetBackdrop and frame:GetBackdrop()) or nil
      local bgFile = (bd and bd.bgFile) or "Interface/Buttons/WHITE8X8"

      -- Preserve current colors if supported
      local br, bg, bb, ba
      local r, g, b, a
      if frame.GetBackdropColor then
        r, g, b, a = frame:GetBackdropColor()
      end
      if frame.GetBackdropBorderColor then
        br, bg, bb, ba = frame:GetBackdropBorderColor()
      end

      edgeSize = tonumber(edgeSize) or 9
      inset = tonumber(inset) or 3

      frame:SetBackdrop({
        bgFile = bgFile,
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = edgeSize,
        insets = { left = inset, right = inset, top = inset, bottom = inset },
      })

      if r and frame.SetBackdropColor then
        frame:SetBackdropColor(r, g, b, a)
      end
      if br and frame.SetBackdropBorderColor then
        frame:SetBackdropBorderColor(br, bg, bb, ba)
      end
    end

    local function MakePhaseCard(parent, label, w)
      local card = CreateFrame("Frame", nil, parent, (BackdropTemplateMixin and "BackdropTemplate") or nil)
      card:SetSize(w or 68, 40)

      local tr, tg, tb = C4(TEXT, TEXT)
      local mr, mg, mb = C4(MUTED, MUTED)

      -- Subtle dark panel with rounded corners
      _FT_SetRoundedBackdrop(card, 0.16, 0.10, 9, 3)

      card.lbl = card:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      card.lbl:SetPoint("TOP", 0, -5)
      card.lbl:SetJustifyH("CENTER")
      card.lbl:SetTextColor(mr, mg, mb)
      card.lbl:SetText(label or "")

      card.edit = CreateFrame("EditBox", nil, card)
      card.edit:SetSize((w or 68) - 16, 18)
      card.edit:SetPoint("BOTTOM", 0, 6)
      card.edit:SetFontObject("WF_GameFontHighlightSmall")
      card.edit:SetAutoFocus(false)
      card.edit:SetJustifyH("CENTER")
      card.edit:SetTextInsets(4, 4, 0, 0)
      card.edit:SetTextColor(tr, tg, tb)

      do
        local f, _, fl = card.edit:GetFont()
        if f then
          card.edit:SetFont(f, 14, fl or "OUTLINE")
        end
      end

      card.edit:SetScript("OnEscapePressed", card.edit.ClearFocus)
      card.edit:SetScript("OnEnterPressed", card.edit.ClearFocus)

      card.edit:SetScript("OnEditFocusGained", function()
        if card.SetBackdropBorderColor then card:SetBackdropBorderColor(1, 1, 1, 0.18) end
        if card.SetBackdropColor then card:SetBackdropColor(0, 0, 0, 0.22) end
      end)

      card.edit:SetScript("OnEditFocusLost", function()
        if card.SetBackdropBorderColor then card:SetBackdropBorderColor(1, 1, 1, 0.10) end
        if card.SetBackdropColor then card:SetBackdropColor(0, 0, 0, 0.16) end
      end)

      return card
    end

    local cardKill = MakePhaseCard(rSC, "Kill", 68)
    cardKill:SetPoint("TOPLEFT", ftHeader, "BOTTOMLEFT", 0, -10)

    local cardLoot = MakePhaseCard(rSC, "Loot", 68)
    cardLoot:SetPoint("LEFT", cardKill, "RIGHT", 6, 0)

    local cardGrace = MakePhaseCard(rSC, "Grace", 68)
    cardGrace:SetPoint("LEFT", cardLoot, "RIGHT", 6, 0)
    cardKill.edit:SetText(tostring(FT.cfg.kill or "1:45"))
    cardLoot.edit:SetText(tostring(FT.cfg.loot or "0:15"))
    cardGrace.edit:SetText(tostring(FT.cfg.grace or "0:05"))

    -- Options panel (collapsed by default)
    FT.cfg.uiOpen = FT.cfg.uiOpen and true or false

    local optPanel = CreateFrame("Frame", nil, rSC, (BackdropTemplateMixin and "BackdropTemplate") or nil)
    optPanel:SetPoint("TOP", cardKill, "BOTTOM", 0, -10)
    optPanel:SetPoint("LEFT", ftDiv, "LEFT", 0, 0)
    optPanel:SetPoint("RIGHT", ftDiv, "RIGHT", 0, 0)
    optPanel:SetHeight(82)

    _FT_SetRoundedBackdrop(optPanel, 0.10, 0.08, 9, 3)

    local function MakeCheck(parent, label)
      local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
      cb:SetSize(20, 20)

      WF_ApplyBlueCheck(cb)
      local t = cb:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      t:SetPoint("LEFT", cb, "RIGHT", 4, 1)
      t:SetText(label or "")
      cb._label = t

      return cb
    end

    local cbLoop = MakeCheck(optPanel, "Loop cycle")
    local col1X, col2X = 8, 118

    cbLoop:SetPoint("TOPLEFT", optPanel, "TOPLEFT", col1X, -8)

    local cbParty = MakeCheck(optPanel, "Party mode")
    cbParty:SetPoint("TOPLEFT", optPanel, "TOPLEFT", col2X, -8)

    local cbHud = MakeCheck(optPanel, "Mini timer HUD")
    cbHud:SetPoint("TOPLEFT", optPanel, "TOPLEFT", col1X, -30)

    local function _FT_AttachTip(frame, title, body)
      if not (frame and frame.HookScript) then return end

      frame:HookScript("OnEnter", function(selfTip)
        if not GameTooltip then return end
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(selfTip, "ANCHOR_RIGHT")
        GameTooltip:AddLine(tostring(title or ""), ACCENT[1], ACCENT[2], ACCENT[3], true)
        body = tostring(body or "")
        if body ~= "" then
          GameTooltip:AddLine(body, TEXT[1], TEXT[2], TEXT[3], true)
        end
        GameTooltip:Show()
      end)

      frame:HookScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
      end)
    end

    _FT_AttachTip(cbLoop, "Loop cycle", "Automatically restarts after the Grace phase.")
    _FT_AttachTip(cbParty, "Party mode", "Announce phase changes and warnings in group chat (Party/Raid/Raid Warning when appropriate).")
    _FT_AttachTip(cbHud, "Mini timer HUD", "Show a small on-screen timer overlay while running.")

    local warnLbl = MakeMutedText(optPanel, "Warn at")
    warnLbl:SetPoint("TOPLEFT", optPanel, "TOPLEFT", col1X + 2, -54)
    warnLbl:SetJustifyH("LEFT")

    local warnBox = CreateFrame("Frame", nil, optPanel, (BackdropTemplateMixin and "BackdropTemplate") or nil)
    warnBox:SetSize(42, 20)
    warnBox:SetPoint("LEFT", warnLbl, "RIGHT", 8, 0)

    if warnBox.SetBackdrop then
      local ar, ag, ab = C4(ACCENT, TEXT)
      warnBox:SetBackdrop({
        bgFile = "Interface/Buttons/WHITE8X8",
        edgeFile = "Interface/Buttons/WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
      })
      warnBox:SetBackdropColor(0, 0, 0, 0.22)
      warnBox:SetBackdropBorderColor(1, 1, 1, 0.12)
    end

    local warnEB = CreateFrame("EditBox", nil, warnBox)
    warnEB:SetAllPoints(warnBox)
    warnEB:SetFontObject("WF_GameFontHighlightSmall")
    warnEB:SetAutoFocus(false)
    warnEB:SetJustifyH("CENTER")
    warnEB:SetMaxLetters(3)
    warnEB:SetTextInsets(6, 6, 0, 0)
    if warnEB.SetNumeric then warnEB:SetNumeric(true) end
    warnEB:SetScript("OnEscapePressed", warnEB.ClearFocus)
    warnEB:SetScript("OnEnterPressed", warnEB.ClearFocus)

    local warnSuf = MakeMutedText(optPanel, "s")
    warnSuf:SetPoint("LEFT", warnBox, "RIGHT", 6, 0)

    _FT_AttachTip(warnBox, "Warn at", "Seconds before the phase ends to trigger the warning.")
    _FT_AttachTip(warnEB, "Warn at", "Seconds before the phase ends to trigger the warning.")


    local function _ReflowFTOptions()
      if not (optPanel and optPanel.GetWidth) then return end
      local w = optPanel:GetWidth() or 0
      if w <= 0 then return end

      local pad  = 8
      local rowH = 22
      local single = (w < 220)

      -- Clear anchors (we re-anchor based on available width)
      cbLoop:ClearAllPoints()
      cbParty:ClearAllPoints()
      cbHud:ClearAllPoints()
      warnLbl:ClearAllPoints()
      warnBox:ClearAllPoints()
      warnSuf:ClearAllPoints()

      local function _SetLabelWidth(cb, width)
        if not (cb and cb._label and cb._label.SetWidth) then return end
        if width < 60 then width = 60 end
        cb._label:SetWidth(width)
        if cb._label.SetWordWrap then cb._label:SetWordWrap(false) end
        if cb._label.SetJustifyH then cb._label:SetJustifyH("LEFT") end
      end

      if single then
        cbLoop:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad, -8)
        cbParty:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad, -8 - rowH)
        cbHud:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad, -8 - (rowH * 2))

        local warnY = -8 - (rowH * 3) - 8
        warnLbl:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad + 2, warnY)

        local boxW = 42
        if w < 220 then boxW = 36 end
        if warnBox.SetWidth then warnBox:SetWidth(boxW) end

        warnBox:SetPoint("LEFT", warnLbl, "RIGHT", 8, 0)
        warnSuf:SetPoint("LEFT", warnBox, "RIGHT", 6, 0)

        if optPanel.SetHeight then optPanel:SetHeight(132) end

        local labelW = w - (pad * 2) - 28
        _SetLabelWidth(cbLoop, labelW)
        _SetLabelWidth(cbParty, labelW)
        _SetLabelWidth(cbHud, labelW)
      else
        local inner = w - (pad * 2)
        local colW  = math.floor(inner / 2)
        local col2X = pad + colW

        cbLoop:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad, -8)
        cbParty:SetPoint("TOPLEFT", optPanel, "TOPLEFT", col2X, -8)
        cbHud:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad, -30)

        warnLbl:SetPoint("TOPLEFT", optPanel, "TOPLEFT", pad + 2, -54)

        local boxW = 42
        if w < 280 then boxW = 38 end
        if warnBox.SetWidth then warnBox:SetWidth(boxW) end

        warnBox:SetPoint("LEFT", warnLbl, "RIGHT", 8, 0)
        warnSuf:SetPoint("LEFT", warnBox, "RIGHT", 6, 0)

        if optPanel.SetHeight then optPanel:SetHeight(82) end

        local labelW = colW - 28
        _SetLabelWidth(cbLoop, labelW)
        _SetLabelWidth(cbParty, labelW)
        _SetLabelWidth(cbHud, labelW)
      end
    end

    -- Make the options panel responsive so nothing gets clipped on narrow widths
    optPanel:HookScript("OnSizeChanged", function() _ReflowFTOptions() end)
    optPanel:HookScript("OnShow", function() _ReflowFTOptions() end)
    _ReflowFTOptions()


    cbLoop:SetChecked(FT.cfg.loop and true or false)
    cbParty:SetChecked(FT.cfg.party and true or false)
    cbHud:SetChecked(FT.cfg.hud and true or false)
    FT._uiCbUnlock = nil

    warnEB:SetText(tostring(tonumber(FT.cfg.warn or 10) or 10))

    if FT.cfg.uiOpen then optPanel:Show() else optPanel:Hide() end

    local ftHint = MakeMutedText(rSC, "")
    ftHint:SetJustifyH("LEFT")

    local ftBtnRow = CreateFrame("Frame", nil, rSC)
    ftBtnRow:SetSize(220, 26)

    local bFT = Widgets:Button(ftBtnRow, "Start Timer", 220, 26)
    bFT:SetPoint("LEFT", 0, 0)

    -- Round the timer buttons (only if they use backdrops)
    _FT_RoundExistingBackdrop(bFT, 9, 3)

    local bFTStop = Widgets:Button(ftBtnRow, "Stop", 106, 26)
    bFTStop:SetPoint("LEFT", bFT, "RIGHT", 8, 0)
    bFTStop:Hide()

    _FT_RoundExistingBackdrop(bFTStop, 9, 3)

    -- Some custom button templates don't expose :SetText, but FT:UpdateUI expects it.
    local function _FT_EnsureButtonSetText(btn)
      if not btn or btn._wfPatchedSetText then return end

      local fs = nil
      if btn.GetFontString then
        fs = btn:GetFontString()
      end
      if not fs then
        fs = btn.label or btn.text or btn._text or btn._label or btn.Text
      end

      if fs and fs.SetText then
        btn.SetText = function(selfBtn, s)
          fs:SetText(tostring(s or ""))
        end
        btn._wfPatchedSetText = true
      end
    end

    _FT_EnsureButtonSetText(bFT)
    _FT_EnsureButtonSetText(bFTStop)

    local function _LayoutTimerButtons(running)
      if running then
        if bFT.SetWidth then bFT:SetWidth(106) end
        bFT:SetPoint("LEFT", 0, 0)

        if bFTStop.SetWidth then bFTStop:SetWidth(106) end
        bFTStop:ClearAllPoints()
        bFTStop:SetPoint("LEFT", bFT, "RIGHT", 8, 0)
        bFTStop:Show()
      else
        bFTStop:Hide()
        if bFT.SetWidth then bFT:SetWidth(220) end
        bFT:SetPoint("LEFT", 0, 0)
      end
    end


    local function _SaveTimerEdits()
      FT.cfg.kill = tostring(cardKill.edit:GetText() or "")
      FT.cfg.loot = tostring(cardLoot.edit:GetText() or "")
      FT.cfg.grace = tostring(cardGrace.edit:GetText() or "")

      local w = tonumber(warnEB:GetText() or "")
      if not w then w = tonumber(FT.cfg.warn) or 10 end
      w = math.floor(w + 0.00001)
      if w < 1 then w = 1 end
      if w > 999 then w = 999 end
      FT.cfg.warn = w
      warnEB:SetText(tostring(w))
    end

    local function _UpdateHint()
      local w = tonumber(FT.cfg.warn or 10) or 10
      ftHint:SetText("Warn at " .. tostring(w) .. "s: raid warning + repeat announce")
    end

    local function _LayoutFarmTimer()
      ftHint:ClearAllPoints()
      ftBtnRow:ClearAllPoints()

      if optPanel:IsShown() then
        ftHint:SetPoint("TOPLEFT", optPanel, "BOTTOMLEFT", 6, -6)
      else
        ftHint:SetPoint("TOPLEFT", cardKill, "BOTTOMLEFT", 2, -6)
      end

      ftBtnRow:SetPoint("TOPLEFT", ftHint, "BOTTOMLEFT", -2, -6)
    end

    _UpdateHint()
    _LayoutFarmTimer()

    rightBottom = ftBtnRow

    -- Wire UI -> timer
    FT._uiStatus = ftStatus
    FT._uiStart = bFT
    FT._uiStop = bFTStop
    FT._uiLayoutButtons = _LayoutTimerButtons
    FT:UpdateUI(true)

    ftOptBtn:SetScript("OnClick", function()
      FT.cfg.uiOpen = not FT.cfg.uiOpen
      if FT.cfg.uiOpen then
        optPanel:Show()
        _ReflowFTOptions()
      else
        optPanel:Hide()
      end
      _LayoutFarmTimer()

      if UI and UI._UpdateOverviewRightScroll then
        UI:_UpdateOverviewRightScroll()
      end
    end)

    local function _AfterOptionChange()
      _SaveTimerEdits()
      _UpdateHint()
      if FT and FT.UpdateUI then FT:UpdateUI(true) end
    end

    cbLoop:SetScript("OnClick", function(selfBtn)
      FT.cfg.loop = selfBtn:GetChecked() and true or false
    end)
    cbParty:SetScript("OnClick", function(selfBtn)
      FT.cfg.party = selfBtn:GetChecked() and true or false
    end)
    cbHud:SetScript("OnClick", function(selfBtn)
      FT.cfg.hud = selfBtn:GetChecked() and true or false
      if FT.state and FT.state.running then
        FT:_ShowHUD(FT.cfg.hud)
      end
    end)
    warnEB:SetScript("OnEditFocusLost", function()
      _AfterOptionChange()
    end)

    bFT:SetScript("OnClick", function()
      if FT.state and FT.state.running then
        if FT.state.paused then
          FT:Resume()
        else
          FT:Pause()
        end
      else
        _SaveTimerEdits()
        _UpdateHint()
        FT:Start()
      end

      if UI and UI._UpdateOverviewRightScroll then
        UI:_UpdateOverviewRightScroll()
      end
    end)

    bFTStop:SetScript("OnClick", function()
      if FT.state and FT.state.running then
        FT:Stop()
        if UI and UI._UpdateOverviewRightScroll then
          UI:_UpdateOverviewRightScroll()
        end
      end
    end)



    function UI:_UpdateOverviewRightScroll()
      if not (rScroll and rScroll:IsShown()) then return end

      local w = rScroll:GetWidth() or 0
      if w > 1 then
        rSC:SetWidth(w)
      end

      local top = (rTitle and rTitle.GetTop and rTitle:GetTop()) or (rSC.GetTop and rSC:GetTop())
      local bottom = (rightBottom and rightBottom.GetBottom and rightBottom:GetBottom()) or (bBW and bBW.GetBottom and bBW:GetBottom())

      if top and bottom then
        local h = (top - bottom) + 20
        if h < 1 then h = 1 end
        rSC:SetHeight(h)
        if rScroll.UpdateScrollChildRect then rScroll:UpdateScrollChildRect() end
      end
    end

    function UI:RefreshBagWorth(opt)
      opt = opt or {}

      -- Bag Worth saved options (and optional remembered worth)
      local bwRemember = true
      if _BW_Get then
        bwRemember = (_BW_Get("bwRememberWorth", false) == true)
      elseif WF and WF.DB and WF.DB.GetSetting then
        local v = WF.DB:GetSetting("bwRememberWorth")
        if v == false then bwRemember = false end
      end

      if bwRemember and not UI._bwLastPayload and WF and WF.DB and WF.DB.GetSetting then
        local saved = WF.DB:GetSetting("bwLastPayload")
        if type(saved) == "table" then
          UI._bwLastPayload = saved
        end
      end

      local providerStr = (WF.Price and WF.Price.GetProvider) and WF.Price:GetProvider() or "vendor"
      bwSource:SetText("Source: " .. BW_SourceLabel(providerStr))
      bwPB:SetText("Personal Bank - open bank + refresh")

      -- Manual-only display (no scan)
      if not opt.force then
        local last = UI._bwLastPayload
        BW_SetDetailsEnabled(last and (type(last.byItem) == "table" or type(last.top) == "table" or type(last.skippedTop) == "table"))
        if last then
          local total = tonumber(last.total) or 0
          local bagsTotal = tonumber(last.bagsTotal) or 0
          local bankTotal = tonumber(last.bankTotal) or 0
          local bankScanned = (last.bankScanned == true)
          local gbTotal = tonumber(last.gbTotal) or 0

          bwTotal:SetText("Total - " .. Money(Util:Round(total)))
          bwBags:SetText("Bags - " .. Money(Util:Round(bagsTotal)))

          if bankScanned then
            bwPB:SetText("Personal Bank - " .. Money(Util:Round(bankTotal)))
          else
            bwPB:SetText("Personal Bank - open bank + refresh")
          end

          if gbTotal > 0 then
            bwGB:SetText("Guild Bank - " .. Money(Util:Round(gbTotal)))
          else
            bwGB:SetText("Guild Bank - open guild bank + refresh")
          end

          bwProgress:SetText("")
        else
          bwTotal:SetText("Total - —")
          bwBags:SetText("Bags - —")
          bwPB:SetText("Personal Bank - —")
          bwGB:SetText("Guild Bank - —")
          bwProgress:SetText("") -- removed "Press Refresh Worth"
        end
        return
      end

      -- Forced scan (button)
      if not (WF and WF.BagWorth and WF.BagWorth.Init and WF.BagWorth.Scan) then
        bwTotal:SetText("Total - —")
        bwBags:SetText("Bags - —")
        bwGB:SetText("Guild Bank - —")
        bwProgress:SetText("Bag Worth module not loaded")
        return
      end

      WF.BagWorth:Init()

      local function IsBankOpen()
        if C_PlayerInteractionManager and Enum and Enum.PlayerInteractionType and Enum.PlayerInteractionType.Banker and C_PlayerInteractionManager.IsInteractingWithNpcOfType then
          local ok, isBanker = pcall(C_PlayerInteractionManager.IsInteractingWithNpcOfType, Enum.PlayerInteractionType.Banker)
          if ok and isBanker then return true end
        end
        if BankFrame and BankFrame.IsShown and BankFrame:IsShown() then return true end
        return false
      end

      local gbOpen = BW_IsGuildBankOpen()
      local bankOpen = IsBankOpen()

      local scope = "bags"
      if gbOpen then
        scope = "all"
      elseif bankOpen then
        scope = "bagsbank"
      end

      bwProgress:SetText("Scanning…")
      BW_SetDetailsEnabled(false)
      BW_SetScanButtonState(true)
      do
        local df = UI._bwDetailsFrame
        if df and df.IsShown and df:IsShown() then
          if df._bwMetaTotal and df._bwMetaTotal.SetText then df._bwMetaTotal:SetText("—") end
          if df._bwMetaScanned and df._bwMetaScanned.SetText then df._bwMetaScanned:SetText("Scanning…") end
          if df._bwMetaSource and df._bwMetaSource.SetText then df._bwMetaSource:SetText("—") end
          if df._bwMetaBags and df._bwMetaBags.SetText then df._bwMetaBags:SetText("—") end
          if df._bwMetaPB and df._bwMetaPB.SetText then df._bwMetaPB:SetText("Not scanned") end
          if df._bwMetaGB and df._bwMetaGB.SetText then df._bwMetaGB:SetText("Not scanned") end
          if df._bwMetaPriced and df._bwMetaPriced.SetText then df._bwMetaPriced:SetText("—") end
          if df._bwMetaSkipped and df._bwMetaSkipped.SetText then df._bwMetaSkipped:SetText("—") end
        end
      end

      if bankOpen then
        bwPB:SetText("Personal Bank - scanning…")
      else
        bwPB:SetText("Personal Bank - open bank + refresh")
      end

      local scanOpt = { force = true }
      do
        local function getBW(key, default)
          if _BW_Get then return _BW_Get(key, default) end
          if WF and WF.DB and WF.DB.GetSetting then
            local v = WF.DB:GetSetting(key)
            if v ~= nil then return v end
          end
          return default
        end
        scanOpt.filters = {
          greys        = getBW("bwIncludeGreys",        false),
          whites       = getBW("bwIncludeWhites",       false),
          greens       = getBW("bwIncludeGreens",       true),
          blues        = getBW("bwIncludeBlues",        true),
          epics        = getBW("bwIncludeEpics",        true),
          mats         = getBW("bwIncludeMats",         true),
          gear         = getBW("bwIncludeGear",         true),
          patterns     = getBW("bwIncludePatterns",     true),
          toys         = getBW("bwIncludeToys",         true),
          pets         = getBW("bwIncludePets",         true),
          consumables  = getBW("bwIncludeConsumables",  true),
          gems         = getBW("bwIncludeGems",         true),
        }
      end

      local function BW_HandleScanEvent(event, payload)
        if type(event) ~= "string" then
          BW_FinishScanState("Bag Worth scan failed")
          return
        end

        if type(payload) ~= "table" then
          if event == "error" then
            BW_FinishScanState("Bag Worth scan failed")
          end
          return
        end

        if event == "progress" then
          local tab = tonumber(payload.tab) or 0
          local tabs = tonumber(payload.tabs) or 0
          if tabs > 0 and tab > 0 then
            bwProgress:SetText("Scanning guild bank " .. tab .. "/" .. tabs .. "…")
          else
            bwProgress:SetText("Scanning…")
          end
          return
        end

        if event == "error" then
          BW_FinishScanState(tostring(payload.message or "Bag Worth scan failed"))
          return
        end

        if event == "done" then
          local livePayload = BW_MakeCompactPayload(payload)

          UI._bwLastPayload = livePayload

          if bwRemember and WF and WF.DB and WF.DB.SetSetting then
            WF.DB:SetSetting("bwLastPayload", livePayload)
            WF.DB:SetSetting("bwLastProvider", providerStr)
            if time then WF.DB:SetSetting("bwLastAt", time()) end
          end

          BW_SetDetailsEnabled(true)
          do
            local df = UI._bwDetailsFrame
            if df and df.IsShown and df:IsShown() and df._BW_SetData then
              df:_BW_SetData(livePayload)
            end
          end

          local total = tonumber(payload.total) or 0
          local bagsTotal = tonumber(payload.bagsTotal) or 0
          local bankTotal = tonumber(payload.bankTotal) or 0
          local bankScanned = (payload.bankScanned == true)
          local gbTotal = tonumber(payload.gbTotal) or 0

          bwTotal:SetText("Total - " .. Money(Util:Round(total)))
          bwBags:SetText("Bags - " .. Money(Util:Round(bagsTotal)))

          if bankScanned then
            bwPB:SetText("Personal Bank - " .. Money(Util:Round(bankTotal)))
          else
            bwPB:SetText("Personal Bank - open bank + refresh")
          end

          if gbTotal > 0 then
            bwGB:SetText("Guild Bank - " .. Money(Util:Round(gbTotal)))
          else
            bwGB:SetText("Guild Bank - open guild bank + refresh")
          end

          if payload.pending == true then
            bwProgress:SetText("Scanning guild bank…")
          else
            BW_FinishScanState("")
          end
          return
        end

        BW_FinishScanState("Bag Worth scan failed")
      end

      local ok, err = xpcall(function()
        WF.BagWorth:Scan(scope, providerStr, BW_HandleScanEvent, scanOpt)
      end, geterrorhandler())

      if not ok then
        BW_FinishScanState("Bag Worth scan failed")
        return
      end

      -- Safety net: if the scan never fires done/error (e.g. stalled GB scan),
      -- release the button after 30 seconds so it doesn't stay stuck forever.
      local scanToken = (UI._bwScanToken or 0) + 1
      UI._bwScanToken = scanToken
      C_Timer.After(30, function()
        if UI._bwScanToken == scanToken and UI._bwScanRunning then
          BW_FinishScanState("Scan timed out")
        end
      end)
    end

    function UI:RefreshToken()
      if not pOverview then return end

      local supported = (WF.Token and WF.Token.IsSupported and WF.Token:IsSupported())
      if not supported then
        tokPrice:SetText("Price: —")
        tokUpdated:SetText("Updated: — (token API unavailable)")
        tokGoal:SetText("")
        tokRemain:SetText("")
        if bTok.Disable then bTok:Disable() end
        return
      end

      if bTok.Enable then bTok:Enable() end
      if WF.Token and WF.Token.Init then WF.Token:Init() end

      local price, ts = nil, nil
      if WF.Token and WF.Token.GetCached then
        price, ts = WF.Token:GetCached()
      end

      if not price or price <= 0 then
        tokPrice:SetText("Price: …")
      else
        tokPrice:SetText("Price: " .. Money(price))
      end

      local at = ts and date("%m/%d %H:%M", ts) or "—"
      tokUpdated:SetText("Updated: " .. at)

      if price and price > 0 then
        local my = (GetMoney and GetMoney()) or 0
        local remaining = price - my
        if remaining < 0 then remaining = 0 end
        local perDay = Util:Round(remaining / 30)
        tokGoal:SetText("Per Day Goal: " .. Money(perDay) .. " ")
        tokRemain:SetText("Remaining: " .. Money(remaining))
      else
        tokGoal:SetText("Per Day Goal: —")
        tokRemain:SetText("Remaining: —")
      end

      local n = SafeNow()
      if (not ts) or ((n - ts) > 300) then
        if WF.Token and WF.Token.RequestUpdate then
          WF.Token:RequestUpdate()
        end
      end
    end
if WF and WF.Token and WF.Token.RegisterCallback and (not WF.Token._wfOverviewTokenCB) then
  WF.Token._wfOverviewTokenCB = true
  WF.Token:RegisterCallback(function()
    local ui = nil
    if WF and WF.UI then
      ui = WF.UI.HelpWindow or WF.UI.helpWindow or WF.UI.help or WF.UI.Help
    end
    if ui and ui.frame and ui.frame.IsShown and ui.frame:IsShown() then
      local ov = ui._layout and ui._layout.ov and ui._layout.ov.page
      if ov and ov.IsShown and ov:IsShown() and ui.RefreshToken then
        ui:RefreshToken()
      end
    end
  end)
end


    bTok:SetScript("OnClick", function()
      if WF.Token and WF.Token.RequestUpdate then
        if WF.Token.Init then WF.Token:Init() end
        WF.Token:RequestUpdate()
      end
      UI:RefreshToken()
    end)

    bBW:SetScript("OnClick", function()
      if IsShiftKeyDown and IsShiftKeyDown() then
        BW_OpenDetails()
      else
        UI:RefreshBagWorth({ force = true })
      end
      BW_UpdateActionButton()
    end)

    bRoll:SetScript("OnClick", function()
  local main = (WF and WF.UI and WF.UI.Main) or nil

  -- Main window owns the random roll + waypoint (and respects filters).
  local farm = nil
  if main and main.RollRandomFarmAndWaypoint then
    farm = main:RollRandomFarmAndWaypoint()
  elseif UI and UI.PickRandomFarm then
    -- Fallback: pick without the main window (still respects Settings filters)
    farm = UI:PickRandomFarm()
    if farm then
      local wx, wy = farm.x, farm.y
      -- Do not fake 50/50 coords from the fallback path. Dungeon/raid maps can
      -- resolve that fake pin to a random-looking location.
      if farm.mapId and type(wx) == "number" and type(wy) == "number" then
        SetWaypoint(farm.mapId, wx, wy)
      end
    end
  end

  if not farm then
    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      UIErrorsFrame:AddMessage("What Farm?: No farms match your Random Farm filters.", 0.35, 0.78, 1.00, 1)
    end
    return
  end

  if WF.DB and WF.DB.SetSetting and farm.id ~= nil then
    WF.DB:SetSetting("lastRolledFarmId", farm.id)
  end

  if main and main.Show then
    main:Show()
  elseif WF.UI.Main and WF.UI.Main.Show then
    WF.UI.Main:Show()
  end
end)


    function UI:RefreshOverview()
      if not pOverview then return end

      local db = WF.DB:Get()
      local sessions = (db.history and db.history.sessions) or {}
      local sessionsAll = sessions
      local farms = db.farms or {}

      -- Farms Loaded is the unfiltered farm directory count.
      -- Random Farm Pool is a filtered subset of that directory.
      local pool, cfg = UI:GetRandomPool()
      pool = pool or {}
      cfg = cfg or { aim = "Any", expansion = "Any", farmType = "Any" }

      local loaded = (UI.GetFarmsLoadedCount and UI:GetFarmsLoadedCount()) or 0
      c4._value:SetText(tostring(loaded))


      -- Mode filter (profile used when the session started)
      local modeFilter = nil
      if UI and UI.GetModeFilter then
        modeFilter = UI:GetModeFilter()
      end

      if modeFilter then
        local filtered = UI._ovModeSessions
        if not filtered then
          filtered = {}
          UI._ovModeSessions = filtered
        end

        if wipe then
          wipe(filtered)
        else
          for k in pairs(filtered) do
            filtered[k] = nil
          end
        end

        for i = 1, #sessionsAll do
          local s = sessionsAll[i]
          if s and tostring(s.mode or "") == modeFilter then
            filtered[#filtered + 1] = s
          end
        end

        sessions = filtered
      end

      -- Session count becomes dynamic under a mode filter
      c3._value:SetText(tostring(#sessions))

      -- Recent runs sort (header click)
      -- Keys: "date" (default newest first), "farm" (A-Z), "metric" (high-low), "mode" (A-Z), "dur" (long-short)
      if not UI._ovRecentSortKey then UI._ovRecentSortKey = "date" end
      if not UI._ovRecentSortDir then UI._ovRecentSortDir = -1 end

      local metric = "gph"
      if WF.DB and WF.DB.GetSetting then
        metric = tostring(WF.DB:GetSetting("overviewLast5Metric") or "gph"):lower()
      end
      if metric ~= "liv" then metric = "gph" end

      local metricLabel = (metric == "liv") and "LIV" or "GPH"

      -- Persist for hover tooltips
      UI._ovMetric = metric
      UI._ovMetricLabel = metricLabel

      -- Section titles (Recent Runs • <metric>, Top Farms • <metric>)
      if UI._ovLeftTitle and UI._ovLeftTitle.SetText then
        local mf = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
      local ms = mf and (" • " .. DisplayProfileName(mf)) or ""
      UI._ovLeftTitle:SetText("Recent runs • " .. metricLabel .. ms)
      else
        local mf = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
        local ms = mf and (" • " .. DisplayProfileName(mf)) or ""
        leftTitle:SetText("Recent runs • " .. metricLabel .. ms)
      end
      if runsHeader and runsHeader.metric then
        runsHeader.metric:SetText(metricLabel)
      end

      -- Highlight active Recent Runs sort column
      local rh = UI._ovRunsHeader or runsHeader
      if rh then
        local rH, gH, bH = C4(ACCENT, ACCENT)
        if rh.farm then rh.farm:SetTextColor(rH, gH, bH, 0.95) end
        if rh.metric then rh.metric:SetTextColor(rH, gH, bH, 0.95) end
        if rh.mode then rh.mode:SetTextColor(rH, gH, bH, 0.95) end
        if rh.dur then rh.dur:SetTextColor(rH, gH, bH, 0.95) end
        if rh.time then rh.time:SetTextColor(rH, gH, bH, 0.95) end
      end
      if UI._ovTopTitle and UI._ovTopTitle.SetText then
        local mf = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
      local ms = mf and (" • " .. DisplayProfileName(mf)) or ""
      UI._ovTopTitle:SetText("Top Farms • " .. metricLabel .. ms)
      else
        local mf = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
      local ms = mf and (" • " .. DisplayProfileName(mf)) or ""
      tTitle:SetText("Top Farms • " .. metricLabel .. ms)
      end

      -- Keep the old right-side metric label hidden/empty (we include it in the title now)
      tMetric:SetText("")

      if c1 and c1._title then
        c1._title:SetText(metric == "liv" and "Last 5 avg LIV" or "Last 5 avg GPH")
      end
      if c1 and c1._metricBtn and c1._metricBtn.txt then
        c1._metricBtn.txt:SetText(metricLabel)
      end

      local function _SessTotals(sess)
        local total = tonumber(sess and (sess.totalValue or sess.total or sess.value)) or 0
        local gold =
            tonumber(sess and sess.moneyGained)
            or tonumber(sess and sess.lootedGold)
            or tonumber(sess and sess.goldLooted)
            or tonumber(sess and sess.rawGold)
            or tonumber(sess and sess.gold)
            or 0
        gold = tonumber(gold) or 0
        if gold < 0 then gold = 0 end
        local livTotal = total - gold
        if livTotal < 0 then livTotal = 0 end
        return total, gold, livTotal
      end

      local function _DisplayValue(sess)
        if metric == "liv" then
          local _, _, livTotal = _SessTotals(sess)
          return livTotal
        end
        return SessionGPH(sess)
      end

      local last5 = {}
      for i = #sessions, math.max(#sessions - 4, 1), -1 do
        last5[#last5 + 1] = sessions[i]
      end

      local sum, best = 0, 0
      for i = 1, #last5 do
        local v = _DisplayValue(last5[i])
        sum = sum + v
        if v > best then best = v end
      end

      local avg = 0
      if #last5 > 0 then avg = sum / #last5 end
      c1._value:SetText(Money(Util:Round(avg)))
      c2._value:SetText(Money(Util:Round(best)))

      if not pOverview:IsShown() then return end

      -- Recent runs display list (avoid full-table copy/sort on every open)
      local display = UI._ovRecentDisplay
      if not display then
        display = {}
        UI._ovRecentDisplay = display
      end

      if wipe then
        wipe(display)
      else
        for k in pairs(display) do
          display[k] = nil
        end
      end

      local showCap = math.min(#sessions, 20)

      -- Pinned (newest first)
      for i = #sessions, 1, -1 do
        local s = sessions[i]
        if s and s.pinned then
          display[#display + 1] = s
          if #display >= showCap then
            break
          end
        end
      end

      -- Then newest unpinned
      if #display < showCap then
        for i = #sessions, 1, -1 do
          local s = sessions[i]
          if s and (not s.pinned) then
            display[#display + 1] = s
            if #display >= showCap then
              break
            end
          end
        end

      -- Sort the display list (within pinned/unpinned) based on the clicked column.
      local sortKey = UI._ovRecentSortKey or "date"
      local sortDir = UI._ovRecentSortDir or -1

      local function _SessName(s)
        local dn = nil
        if UI and UI.GetSessionDisplayName then
          dn = UI:GetSessionDisplayName(s)
        end
        local v = dn or (s and (s._wfCustomFarmName or s.farmName or s.zone) or "")
        return string.lower(tostring(v or ""))
      end


local function _SessMode(s)
  local m = tostring(s and s.mode or "")
  m = m:gsub("^%s+", ""):gsub("%s+$", "")
  local empty = (m == "" or m == "—")
  return string.lower(m), empty
end

      local function _SessDate(s)
        return tonumber(s and s.startedAt) or 0
      end

      local function _SessMetricVal(s)
        return tonumber(_DisplayValue(s)) or 0
      end

      local function _SessDurVal(s)
        return tonumber(_SessDuration(s)) or 0
      end

      local function _Cmp(a, b)
        if not a then return false end
        if not b then return true end

        local av, bv = nil, nil
        if sortKey == "farm" then
          av = _SessName(a)
          bv = _SessName(b)
        elseif sortKey == "mode" then
          local am, ae = _SessMode(a)
          local bm, be = _SessMode(b)

          -- Empty/legacy mode always sorts last (regardless of direction)
          if ae ~= be then
            if ae then return false end
            return true
          end

          av = am
          bv = bm
        elseif sortKey == "metric" then
          av = _SessMetricVal(a)
          bv = _SessMetricVal(b)
        elseif sortKey == "dur" then
          av = _SessDurVal(a)
          bv = _SessDurVal(b)
        else
          av = _SessDate(a)
          bv = _SessDate(b)
        end

        if av == bv then
          -- Tie-breaker: newest first, then name
          local da = _SessDate(a)
          local db = _SessDate(b)
          if da ~= db then
            return da > db
          end
          local na = _SessName(a)
          local nb = _SessName(b)
          return na < nb
        end

        if sortDir < 0 then
          return av > bv
        end
        return av < bv
      end

      local pin = UI._ovRecentPinnedScratch
      if not pin then
        pin = {}
        UI._ovRecentPinnedScratch = pin
      end

      local norm = UI._ovRecentNormalScratch
      if not norm then
        norm = {}
        UI._ovRecentNormalScratch = norm
      end

      if wipe then
        wipe(pin)
        wipe(norm)
      else
        for k in pairs(pin) do pin[k] = nil end
        for k in pairs(norm) do norm[k] = nil end
      end

      for i = 1, #display do
        local s = display[i]
        if s and s.pinned then
          pin[#pin + 1] = s
        elseif s then
          norm[#norm + 1] = s
        end
      end

      table.sort(pin, _Cmp)
      table.sort(norm, _Cmp)

      if wipe then
        wipe(display)
      else
        for k in pairs(display) do display[k] = nil end
      end
      for i = 1, #pin do display[#display + 1] = pin[i] end
      for i = 1, #norm do display[#display + 1] = norm[i] end

      end

      local w = scroll:GetWidth()
      if w and w > 1 then sc:SetWidth(w) end

      local showN = #display
      for i = 1, math.max(showN, #rows) do
        local row = rows[i]
        local sess = display[i]

        if sess and i <= showN then
          row = EnsureRow(i)
          row:Show()
          row:SetPoint("TOPLEFT", 0, -((i - 1) * rowH))
          row:SetPoint("TOPRIGHT", 0, -((i - 1) * rowH))
          row._sess = sess

          local v = _DisplayValue(sess)
          local dur = _SessDuration(sess)

          local dn = nil
          if UI and UI.GetSessionDisplayName then
            dn = UI:GetSessionDisplayName(sess)
          end
          row.name:SetText(dn or sess._wfCustomFarmName or sess.farmName or sess.zone or "Session")
          row.meta:SetText(Money(Util:Round(v)))
          local mode = _Trim(sess.mode or "")
          if mode == "" then mode = "—" end
          if row.mode then row.mode:SetText(mode) end
          if row.dur then row.dur:SetText(_SessDurationText(sess)) end
          row.time:SetText(FormatTime(sess.startedAt))
        elseif row then
          row:Hide()
        end
      end
      sc:SetHeight(showN * rowH)
      if UI and UI._LayoutOverviewRecentRunsColumns then
        UI:_LayoutOverviewRecentRunsColumns()
      end

      -- Empty state (Recent runs)
      if UI._ovEmptyRuns then
        if showN and showN > 0 then UI._ovEmptyRuns:Hide() else UI._ovEmptyRuns:Show() end
      end


      -- Random pool meta
      local poolLine = "Pool: " .. tostring(#pool) .. " Farms"

      local lines = {
        poolLine,
        "Aim: " .. (Util and Util.PrettyToken and Util:PrettyToken(cfg.aim) or tostring(cfg.aim)),
        "Expansion: " .. (Util and Util.PrettyToken and Util:PrettyToken(cfg.expansion) or tostring(cfg.expansion)),
        "Type: " .. (Util and Util.PrettyToken and Util:PrettyToken(cfg.farmType) or tostring(cfg.farmType)),
      }
      if cfg.currentZoneOnly then lines[#lines + 1] = "Scope: Current Zone Only" end
      if cfg.includeUnknown then lines[#lines + 1] = "Includes: Uncategorized Farms" end
      rMeta:SetText(table.concat(lines, "\n"))

      -- Top Farms (best per farm by current display metric)
local scratch = UI._ovScratch
if not scratch then
  scratch = {}
  UI._ovScratch = scratch
end

local function _wipe(t)
  if wipe then
    wipe(t)
  else
    for k in pairs(t) do
      t[k] = nil
    end
  end
end

local bestByKey = scratch.bestByKey or {}
local labelByKey = scratch.labelByKey or {}
local bestSessByKey = scratch.bestSessByKey or {}
local countByKey = scratch.countByKey or {}

scratch.bestByKey = bestByKey
scratch.labelByKey = labelByKey
scratch.bestSessByKey = bestSessByKey
scratch.countByKey = countByKey

_wipe(bestByKey)
_wipe(labelByKey)
_wipe(bestSessByKey)
_wipe(countByKey)

for i = 1, #sessions do
  local s = sessions[i]
  if s then
    local rawKey = s.farmId
    local label = nil

    if rawKey ~= nil then
      local farm = farms[rawKey]
      label = (farm and farm.name) or s.farmName or s.zone
    else
      label = s.farmName or s.zone
      rawKey = label
    end

    label = tostring(label or "")
    if label ~= "" and rawKey ~= nil then
      local key = label:lower()
      countByKey[key] = (countByKey[key] or 0) + 1

      local v = _DisplayValue(s)
      local cur = bestByKey[key]
      if (cur == nil) or (v > cur) then
        bestByKey[key] = v
        labelByKey[key] = label
        bestSessByKey[key] = s
      end
    end
  end
end

-- Select top 5 without allocating/sorting a full list
local topKeys = scratch.topKeys or {}
local topVals = scratch.topVals or {}

scratch.topKeys = topKeys
scratch.topVals = topVals

_wipe(topKeys)
_wipe(topVals)

for k, v in pairs(bestByKey) do
  v = tonumber(v) or 0
  for pos = 1, TOP_FARMS_LIMIT do
    local curV = topVals[pos]
    if (not curV) or (v > curV) then
      for j = TOP_FARMS_LIMIT, pos + 1, -1 do
        topVals[j] = topVals[j - 1]
        topKeys[j] = topKeys[j - 1]
      end
      topVals[pos] = v
      topKeys[pos] = k
      break
    end
  end
end

-- Empty state (Top Farms)
if UI._ovEmptyTopFarms then
  if topKeys[1] then UI._ovEmptyTopFarms:Hide() else UI._ovEmptyTopFarms:Show() end
end

for i = 1, TOP_FARMS_LIMIT do
  local r = tRows[i]
  local k = topKeys[i]
  if k then
    local sess = bestSessByKey[k]
    local label = labelByKey[k] or "—"
    local v = topVals[i] or 0
    local runs = countByKey[k] or 0

    r:Show()
    r.name:SetText(label)
    r.value:SetText(Money(Util:Round(v)))

    local bestDur = _SessDuration(sess)
    local total, gold, itemValue = _SessTotals(sess)

    r._data = {
      label = label,
      metricLabel = metricLabel,
      metricValue = v,

      bestSess = sess,
      bestRunText = sess and FormatTime(sess.startedAt) or nil,
      durationText = bestDur and _DurationLongText(bestDur) or nil,

      totalValue = total,
      goldValue = gold,
      itemValue = itemValue,

      runsCount = runs,
    }
  else
    r:Hide()
    r._data = nil
  end
end

-- Token can refresh automatically
      self:RefreshToken()

      -- Bag Worth: manual-only (no scan here)
      self:RefreshBagWorth({ force = false })

      if self._UpdateOverviewRightScroll then
        self:_UpdateOverviewRightScroll()
        if C_Timer and C_Timer.After then
          C_Timer.After(0, function()
            if UI and UI.frame and UI.frame:IsShown() and pOverview and pOverview:IsShown() then
              UI:_UpdateOverviewRightScroll()
            end
          end)
        end
      end
    end

    pOverview:SetScript("OnShow", function()
      UI:_Layout()
      if UI._LayoutOverviewAbout then UI:_LayoutOverviewAbout() end
      UI:RefreshOverview()
    end)

    -- NOTE: When opening the Help window via slash (e.g. /wf help), the Overview page can already
    -- be shown *before* this OnShow handler is attached. If that happens, run the OnShow handler once
    -- immediately after attaching it so the page is populated on first open (no extra frame hooks/timers).
    if pOverview and pOverview.IsShown and pOverview:IsShown() then
      -- Avoid declaring one more top-level local here; this giant page builder is already
      -- near Lua 5.1's active-local limit on some clients.
      if pOverview.GetScript and pOverview:GetScript("OnShow") then
        pOverview:GetScript("OnShow")(pOverview)
      end
    end

    -- IMPORTANT: HelpWindow._Layout expects these refs to exist
    self._layout.ov = {
      page = pOverview,
      cards = { c1, c2, c3, c4 },
      left = left,
      recentRuns = recentRunsFocus,
      about = about,
      right = right,
      rightScroll = rScroll,

      rMeta = rMeta,
      tList = tList,

      tokPrice = tokPrice,
      tokUpdated = tokUpdated,
      tokGoal = tokGoal,
      tokRemain = tokRemain,
    }
  end
end
