-- ============================================================
--  UI.lua  |  v2.1.17  —  "Iskaara Fishing Village" Theme
--  AzerothAngler — Complete UI rebuild
--
--  Design language:
--    • Charred driftwood panels with whale-oil lantern accents
--    • Carved-bone typography (MORPHEUS for headers, FRIZQT for body)
--    • Arctic harbor teal highlights on hover / accent elements
--    • Rich data-tip tooltips on every stat card and row element
--    • All tabs and features from v2.1.16 preserved and working
--
--  Uses only ForgeDMC Toolkit (ForgeTimer/ForgeComm) — no Ace/lib.
-- ============================================================
local _, ns = ...
local Theme = ns.Theme
local M -- lazily assigned in CreateMainFrame after Media.lua loads

ns.UI = {}
local UI = ns.UI

-- ── Font paths ────────────────────────────────────────────────────────
local TITLE_FONT = "Fonts\\MORPHEUS.TTF"    -- WoW's carved runic font
local BODY_FONT  = "Fonts\\FRIZQT__.TTF"    -- WoW's clean serif UI font
local VALUE_FONT = "Fonts\\SKURRI.TTF"      -- WoW's large score/value font
local MONO_FONT  = "Fonts\\ARIALN.TTF"      -- Thin mono for debug/data

-- ── Asset paths ───────────────────────────────────────────────────────
local GEN = "Interface\\AddOns\\AzerothAngler\\Media\\generated\\"

-- ── Tab definitions (order matters for layout) ───────────────────────
local TAB_LIST = { "Overview", "Watcher", "Atlas", "Equipment", "Tackle", "Session", "Database", "Settings" }

-- ─────────────────────────────────────────────────────────────────────
--  SHARED LOW-LEVEL HELPERS
-- ─────────────────────────────────────────────────────────────────────

local function applyTex(texture, path, alpha, r, g, b)
    if not texture or not path then return end
    texture:SetTexture(path)
    texture:SetVertexColor(r or 1, g or 1, b or 1, alpha or 1)
end

local function addIcon(parent, asset, sz, anchor, rel, relAnchor, x, y, alpha, r, g, b)
    if not parent or not asset then return end
    local t = parent:CreateTexture(nil, "OVERLAY")
    t:SetTexture(asset)
    t:SetSize(sz or 20, sz or 20)
    t:SetPoint(anchor or "TOPLEFT", rel or parent, relAnchor or anchor or "TOPLEFT", x or 0, y or 0)
    t:SetVertexColor(r or 1, g or 1, b or 1, alpha or 0.85)
    return t
end

local function addBg(parent, asset, alpha, r, g, b, sublevel)
    if not parent or not asset then return end
    local t = parent:CreateTexture(nil, "BACKGROUND", nil, sublevel or -8)
    t:SetTexture(asset)
    t:SetAllPoints(parent)
    t:SetVertexColor(r or 1, g or 1, b or 1, alpha or 0.08)
    return t
end

local function createText(parent, text, size, anchor, rel, relAnchor, x, y, color, font, flags, justify)
    local fs = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fs:SetFont(font or BODY_FONT, size or 12, flags or "")
    fs:SetPoint(anchor, rel or parent, relAnchor or anchor, x or 0, y or 0)
    fs:SetJustifyH(justify or "LEFT")
    fs:SetJustifyV("MIDDLE")
    fs:SetWordWrap(false)
    if fs.SetMaxLines then fs:SetMaxLines(1) end
    fs:SetTextColor(unpack(color or Theme.colors.soft))
    fs:SetText(text or "")
    return fs
end

local function enableWrap(fs, width, maxLines)
    if not fs then return end
    if width and fs.SetWidth then fs:SetWidth(width) end
    if fs.SetWordWrap then fs:SetWordWrap(true) end
    if fs.SetMaxLines and maxLines then fs:SetMaxLines(maxLines) end
end

local function createButton(parent, text, width, height, anchor, rel, relAnchor, x, y, onClick, emphasis)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(width or 120, height or 28)
    btn:SetPoint(anchor, rel or parent, relAnchor or anchor, x or 0, y or 0)
    btn.isEmphasis = emphasis and true or false
    btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btn.text:SetFont(BODY_FONT, 11, "OUTLINE")
    btn.text:SetPoint("CENTER", btn, "CENTER", 0, 0)
    btn.text:SetJustifyH("CENTER")
    btn.text:SetText(text or "")
    Theme:StyleButton(btn)
    btn:SetScript("OnEnter", function(self)
        self.isHovered = true
        Theme:StyleButton(self)
    end)
    btn:SetScript("OnLeave", function(self)
        self.isHovered = false
        Theme:StyleButton(self)
    end)
    if onClick then btn:SetScript("OnClick", onClick) end
    return btn
end

local function setButtonText(button, text)
    if button and button.text then button.text:SetText(text or "") end
end

local function createCard(parent, width, height, anchor, rel, relAnchor, x, y, mood)
    local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    if width  then f:SetWidth(width)   end
    if height then f:SetHeight(height) end
    if anchor then f:SetPoint(anchor, rel, relAnchor, x or 0, y or 0) end
    f:SetClipsChildren(true)
    Theme:ApplyPanel(f, mood or "panel")
    return f
end

-- ── Decorative carved-wood divider line ──────────────────────────────
local function addDivider(parent, anchor, rel, relAnchor, x, y, width)
    local line = parent:CreateTexture(nil, "BORDER")
    line:SetHeight(1)
    line:SetPoint(anchor or "TOPLEFT", rel or parent, relAnchor or anchor or "TOPLEFT", x or 0, y or 0)
    if width then
        line:SetWidth(width)
    else
        line:SetPoint("RIGHT", parent, "RIGHT", -8, 0)
    end
    line:SetColorTexture(0.941, 0.627, 0.125, 0.35) -- amber wood grain
    return line
end

-- ─────────────────────────────────────────────────────────────────────
--  RICH TOOLTIP SYSTEM
--  createTip(frame, title, lines)   — shows on hover, hides on leave
--  Each line: { label, value, color }   (color optional, defaults to foam)
-- ─────────────────────────────────────────────────────────────────────
local function createTip(frame, titleFn, linesFn)
    if not frame then return end
    frame:EnableMouse(true)
    frame:SetScript("OnEnter", function(self)
        local title = type(titleFn) == "function" and titleFn() or tostring(titleFn or "")
        local lines = type(linesFn) == "function" and linesFn() or (linesFn or {})
        if title == "" and #lines == 0 then return end
        GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
        GameTooltip:ClearLines()
        if title ~= "" then
            GameTooltip:AddLine(title, 0.941, 0.627, 0.125, true)
        end
        for _, row in ipairs(lines) do
            if type(row) == "string" then
                GameTooltip:AddLine(row, 0.608, 0.749, 0.792, true)
            elseif type(row) == "table" then
                local label = row[1] or row.label or ""
                local value = row[2] or row.value or ""
                local r, g, b = 0.608, 0.749, 0.792
                if row.color then r, g, b = unpack(row.color) end
                if value ~= "" then
                    GameTooltip:AddDoubleLine(label, value, 0.608, 0.749, 0.792, r, g, b)
                else
                    GameTooltip:AddLine(label, r, g, b, true)
                end
            end
        end
        GameTooltip:Show()
    end)
    frame:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
end

-- ─────────────────────────────────────────────────────────────────────
--  STAT CARD  (used on Overview tab — 5 across)
--  Rich tooltip attached via createTip
-- ─────────────────────────────────────────────────────────────────────
local function createStatCard(parent, label, anchor, rel, relAnchor, x, y, tipTitle, tipLinesFn)
    local card = createCard(parent, 158, 88, anchor, rel, relAnchor, x, y, "alt")

    -- Lantern flicker accent — top-left amber line
    local accent = card:CreateTexture(nil, "BORDER")
    accent:SetPoint("TOPLEFT",  card, "TOPLEFT",  4, -4)
    accent:SetPoint("TOPRIGHT", card, "TOPRIGHT", -4, -4)
    accent:SetHeight(2)
    accent:SetColorTexture(0.941, 0.627, 0.125, 0.55)

    card.label = createText(card, label, 9, "TOPLEFT", card, "TOPLEFT",
        12, -14, Theme.colors.muted, BODY_FONT, "OUTLINE")
    card.value = createText(card, "-", 20, "TOPLEFT", card.label, "BOTTOMLEFT",
        0, -6, Theme.colors.bone, VALUE_FONT, "OUTLINE")
    card.sub = createText(card, "", 9, "TOPLEFT", card.value, "BOTTOMLEFT",
        0, -5, Theme.colors.soft)
    enableWrap(card.sub, 136, 2)

    -- Subtle fish icon watermark (bottom-right)
    if M and M.icon then
        local wm = card:CreateTexture(nil, "BACKGROUND", nil, -7)
        wm:SetTexture(M.icon.fishCommon)
        wm:SetSize(32, 32)
        wm:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -6, 6)
        wm:SetVertexColor(0.118, 0.812, 0.808, 0.12)
    end

    if tipTitle then
        createTip(card, tipTitle, tipLinesFn or {})
    end

    return card
end

-- ─────────────────────────────────────────────────────────────────────
--  ROW / LINE HELPERS
-- ─────────────────────────────────────────────────────────────────────
local function createLineStack(parent, count, x, y, width, spacing, color, maxLines)
    local lines = {}
    local lineMax = maxLines or 2
    for i = 1, count do
        lines[i] = createText(parent, "", 11, "TOPLEFT", parent, "TOPLEFT",
            x, y - ((i - 1) * (spacing or 24)), color or Theme.colors.foam)
        enableWrap(lines[i], width or 300, lineMax)
        if lineMax <= 1 and lines[i].SetWordWrap then
            lines[i]:SetWordWrap(false)
        end
    end
    return lines
end

local function setLines(lines, values)
    for i = 1, #lines do
        lines[i]:SetText(values[i] or "")
    end
end

local function getItemIDFromLink(itemLink)
    if type(itemLink) ~= "string" then return nil end
    local id = itemLink:match("item:(%d+)")
    return tonumber(id)
end

local function findFishRowByCatchKey(catchKey)
    if not (ns.FishDB and ns.FishDB.byName and ns.FishDB.byItemID) then return nil end

    local itemID = getItemIDFromLink(catchKey)
    if itemID and ns.FishDB.byItemID[itemID] then
        return ns.FishDB.byItemID[itemID]
    end

    local clean = string.lower(ns.Utils:CleanDisplayText(catchKey or ""))
    if clean ~= "" and ns.FishDB.byName[clean] then
        return ns.FishDB.byName[clean]
    end
    return nil
end

local function abbreviateWithSuffix(baseText, suffix, maxLen)
    local base = ns.Utils:CleanDisplayText(baseText or "")
    local limit = tonumber(maxLen) or 36
    if not suffix or suffix == "" then
        return ns.Utils:AbbrevText(base, limit)
    end

    local suffixLen = #suffix + 1
    if limit <= suffixLen + 3 then
        return ns.Utils:AbbrevText(base .. " " .. suffix, limit)
    end

    local baseLimit = limit - suffixLen
    if #base > baseLimit then
        base = base:sub(1, math.max(1, baseLimit - 3)) .. "..."
    end
    return base .. " " .. suffix
end

local function formatCatchDisplayWithMetrics(catchKey, fallbackName, maxLen)
    local clean = ns.Utils:CleanDisplayText(fallbackName or catchKey or "")
    local metricSuffix = clean:match("(%[%d+%.?%d* lb, %d+%.?%d* in%])")
    if metricSuffix then
        local base = clean:gsub("%s*%[%d+%.?%d* lb, %d+%.?%d* in%]", "")
        return abbreviateWithSuffix(base, metricSuffix, maxLen)
    end

    local row = findFishRowByCatchKey(catchKey)
    if row then
        local suffix = string.format("[%.1f lb, %.1f in]", tonumber(row.weightLb) or 0, tonumber(row.sizeIn) or 0)
        local baseName = row.name or clean
        return abbreviateWithSuffix(baseName, suffix, maxLen)
    end

    return ns.Utils:AbbrevText(clean, maxLen or 36)
end

local function formatCompactMetricLine(catchText)
    local text = ns.Utils:CleanDisplayText(catchText or "")
    local w, l = text:match("%[(%d+%.?%d*) lb, (%d+%.?%d*) in%]")
    if w and l then
        return string.format("Size: %s lb | %s in", w, l)
    end

    local row = findFishRowByCatchKey(catchText)
    if row then
        return string.format("Size: %.1f lb | %.1f in", tonumber(row.weightLb) or 0, tonumber(row.sizeIn) or 0)
    end
    return "Size: -"
end

-- ── Utility ──────────────────────────────────────────────────────────
local function formatTimeStamp(at)
    if not at or at <= 0 then return "--:--" end
    return date("%H:%M", at)
end

local function getSuccessRate(catches, casts)
    return ns.Utils:FormatPercent(catches or 0, casts or 0)
end

local function getToolModeLabel(state)
    if not state then return "Unavailable" end
    if state.hasMainhandPoleEquipped then
        return state.hasLegacyPoleEquipped and "Legacy Rod" or "Main-hand Pole"
    end
    if state.hasModernFishingTool then return "Profession Tool" end
    if state.canCastFishing         then return "Spell-based"   end
    return "Unavailable"
end

local function getAddonVersion()
    local n = ns.addonName or "AzerothAngler"
    local v = (C_AddOns and C_AddOns.GetAddOnMetadata and C_AddOns.GetAddOnMetadata(n, "Version"))
           or (GetAddOnMetadata and GetAddOnMetadata(n, "Version"))
    return tostring(v or "?")
end

-- ── Section header inside a content panel ────────────────────────────
local function addSectionHeader(parent, text, anchor, rel, relAnchor, x, y)
    local hdr = createText(parent, text, 15, anchor, rel, relAnchor, x, y,
        Theme.colors.lantern, TITLE_FONT, "OUTLINE")
    addDivider(parent, "TOPLEFT", hdr, "BOTTOMLEFT", 0, -4, nil)
    return hdr
end

-- ── Check row for Settings ───────────────────────────────────────────
local function createCheck(parent, label, desc, point, rel, relPoint, x, y, getter, setter, width)
    local holder = CreateFrame("Frame", nil, parent)
    local rowWidth = width or 380
    holder:SetSize(rowWidth, 44)
    holder:SetPoint(point, rel, relPoint, x, y)
    holder:SetClipsChildren(true)
    local cb = CreateFrame("CheckButton", nil, holder, "UICheckButtonTemplate")
    cb:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, -3)
    local title = createText(holder, label, 11, "TOPLEFT", cb, "TOPRIGHT", 6, -2, Theme.colors.foam)
    local sub   = createText(holder, desc,  9,  "TOPLEFT", title, "BOTTOMLEFT", 0, -2, Theme.colors.soft)
    enableWrap(sub, math.max(160, rowWidth - 54), 1)
    cb:SetScript("OnClick", function(self)
        setter(self:GetChecked() and true or false)
        UI:RefreshAll()
    end)
    holder.check = cb
    holder.title = title
    holder.sub   = sub
    holder.getter = getter
    return holder
end

-- ═════════════════════════════════════════════════════════════════════
--  MAIN FRAME  — "Harbormaster's Chart Room"
-- ═════════════════════════════════════════════════════════════════════
function UI:CreateMainFrame()
    if ns.state.frame then return end
    M = ns.Media

    -- ── Outer shell ──────────────────────────────────────────────
    local frame = CreateFrame("Frame", "AzerothAnglerMainFrame", UIParent, "BackdropTemplate")
    frame:SetSize(1110, 720)
    local uiScale = tonumber(ns.DB and ns.DB.profile and ns.DB.profile.ui and ns.DB.profile.ui.scale) or 1
    frame:SetScale(math.max(0.2, uiScale))
    frame:SetFrameStrata("HIGH")
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    Theme:ApplyPanel(frame, "frame")
    frame:SetScript("OnDragStart", function(self)
        if not ns.DB.profile.ui.locked then self:StartMoving() end
    end)
    frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        ns.Core:SaveFramePosition(self, "point")
    end)

    -- Watermark logo (dim, top-right)
    frame.wmLogo = frame:CreateTexture(nil, "BACKGROUND", nil, -6)
    frame.wmLogo:SetTexture(GEN .. "logo_primary.png")
    frame.wmLogo:SetSize(340, 340)
    frame.wmLogo:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -20, -16)
    frame.wmLogo:SetVertexColor(1, 1, 1, 0.04)

    -- ── Header bar ───────────────────────────────────────────────
    local header = createCard(frame, nil, 106, "TOPLEFT",  frame, "TOPLEFT",  12, -12, "hero")
    header:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -12)
    frame.header = header

    -- Wide banner art behind header
    if M and M.custom then
        header.banner = header:CreateTexture(nil, "BACKGROUND", nil, -7)
        header.banner:SetTexture(M.custom.banner)
        header.banner:SetAllPoints(header)
        header.banner:SetVertexColor(1, 1, 1, 0.45)
        header.banner:SetTexCoord(0, 1, 0, 1)
    end

    -- Harbor scene amber fog overlay
    local fog = header:CreateTexture(nil, "BACKGROUND", nil, -5)
    fog:SetTexture("Interface\\Buttons\\WHITE8x8")
    fog:SetAllPoints(header)
    fog:SetVertexColor(0.10, 0.06, 0.02, 0.52)

    -- Carved hook icon
    if M and M.custom then
        -- Forge-Certified badge in header top-right
        local hdrBadge = header:CreateTexture(nil, "ARTWORK")
        hdrBadge:SetTexture(M.custom.forgeCertified and M.custom.forgeCertified or M.custom.iconHook)
        hdrBadge:SetSize(82, 82)
        hdrBadge:SetPoint("RIGHT", header, "RIGHT", -14, 0)
        hdrBadge:SetVertexColor(1, 1, 1, 0.90)
    end

    header.title = createText(header, "Azeroth Angler", 30,
        "TOPLEFT", header, "TOPLEFT", 20, -12,
        Theme.colors.lantern, TITLE_FONT, "THICKOUTLINE")
    header.subtitle = createText(header, "Tuskarr Fishing Companion · Fast cast · Lure awareness · Zone memory · Session tracking", 11,
        "TOPLEFT", header.title, "BOTTOMLEFT", 0, -6,
        Theme.colors.soft)
    enableWrap(header.subtitle, 680, 1)

    -- Status pill
    header.statusPill = CreateFrame("Frame", nil, header, "BackdropTemplate")
    header.statusPill:SetSize(198, 32)
    header.statusPill:SetPoint("TOPRIGHT", header, "TOPRIGHT", -110, -14)
    Theme:ApplyPill(header.statusPill, "warn")
    header.statusText = createText(header.statusPill, "Harbor Ready", 13,
        "CENTER", header.statusPill, "CENTER", 0, 0,
        Theme.colors.foam, BODY_FONT, "OUTLINE", "CENTER")
    header.statusMeta = createText(header, "Fish pace: 0/hr", 10,
        "TOPRIGHT", header.statusPill, "BOTTOMRIGHT", 0, -6,
        Theme.colors.soft)
    ns.Utils:SafeSetWidth(header.statusMeta, 260)

    -- Debug button
    header.debugButton = createButton(header, "Debug", 72, 24,
        "RIGHT", header.statusPill, "LEFT", -10, 0,
        function() UI:ToggleDebug() end)

    -- Zone + last catch line
    header.zone = createText(header, "Waters: -  |  Last: None", 10,
        "TOPLEFT", header.subtitle, "BOTTOMLEFT", 0, -8,
        Theme.colors.muted)
    ns.Utils:SafeSetWidth(header.zone, 780)

    -- Close button
    local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4)

    -- ── Nav rail ─────────────────────────────────────────────────
    local rail = createCard(frame, 186, nil, "TOPLEFT", frame, "TOPLEFT", 12, -130, "inset")
    rail:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 46)
    frame.rail = rail

    -- Tuskarr face watermark
    if M and M.custom then
        addBg(rail, M.custom.iconFace, 0.09, 1, 1, 1, -8)
    end

    -- Rail header
    local railIcon = rail:CreateTexture(nil, "ARTWORK")
    if M and M.icon then railIcon:SetTexture(M.icon.fishing) end
    railIcon:SetSize(36, 36)
    railIcon:SetPoint("TOPLEFT", rail, "TOPLEFT", 14, -12)
    railIcon:SetVertexColor(1, 0.82, 0.22, 0.90)

    local railTitle = createText(rail, "Navigator", 14,
        "TOPLEFT", railIcon, "TOPRIGHT", 8, 0,
        Theme.colors.lanternSoft, TITLE_FONT, "OUTLINE")
    local railSub = createText(rail, "Tabs · Sessions · Gear", 9,
        "TOPLEFT", railTitle, "BOTTOMLEFT", 0, -4,
        Theme.colors.muted)
    enableWrap(railSub, 130, 2)

    -- Tab buttons (with icons and tooltips)
    local tabIcons = M and M.tabIcons or {}
    local tabTips = {
        Overview  = "Session overview, stat cards, recent catches",
        Watcher   = "Zone ledger, hotspot waters, rare archive",
        Atlas     = "Per-zone fish rates, recipes, requirements",
        Equipment = "Gear sets — save, cycle, equip, restore",
        Tackle    = "Lure status, bag inventory, auto-apply",
        Session   = "Full session log, history, catch ledger",
        Database  = "Search full fish database and inspect stats",
        Settings  = "Cast flow, alerts, tracker preferences",
    }
    frame.tabButtons = {}
    frame.tabs = {}

    local content = createCard(frame, nil, nil, "TOPLEFT", rail, "TOPRIGHT", 12, 0, "inset")
    content:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 46)
    frame.content = content

    for i, name in ipairs(TAB_LIST) do
        local btn = createButton(rail, name, 152, 32,
            "TOPLEFT", rail, "TOPLEFT", 14, -106 - ((i-1) * 38),
            function() UI:ShowTab(name) end, i == 1)
        -- small icon on button left
        if tabIcons[name] then
            local ico = btn:CreateTexture(nil, "OVERLAY")
            ico:SetTexture(tabIcons[name])
            ico:SetSize(14, 14)
            ico:SetPoint("LEFT", btn, "LEFT", 8, 0)
            ico:SetVertexColor(1, 1, 1, 0.75)
        end
        createTip(btn, name, { tabTips[name] or "" })
        frame.tabButtons[name] = btn

        local panel = CreateFrame("Frame", nil, content)
        panel:SetAllPoints()
        panel:Hide()
        frame.tabs[name] = panel
    end

    -- Quick-action buttons at bottom of rail
    rail.sessionButton = createButton(rail, "Start Session", 152, 28,
        "BOTTOMLEFT", rail, "BOTTOMLEFT", 14, 94,
        function()
            if ns.DB.currentSession.active then ns.Session:Stop(true) else ns.Session:Start() end
            UI:RefreshAll()
        end, true)
    createTip(rail.sessionButton,
        function() return ns.DB.currentSession.active and "End Session" or "Start Session" end,
        function()
            if ns.DB.currentSession.active then
                return {
                    { "Elapsed",  ns.Utils:FormatDuration(ns.Session:GetElapsed()) },
                    { "Catches",  tostring(ns.DB.currentSession.catches or 0) },
                    { "Casts",    tostring(ns.DB.currentSession.casts or 0) },
                    "Click to end and save this session.",
                }
            end
            return { "Start logging casts and catches.", "Auto-starts with each cast if enabled in Settings." }
        end)

    rail.gearButton = createButton(rail, "Equip Fishing Set", 152, 28,
        "BOTTOMLEFT", rail, "BOTTOMLEFT", 14, 60,
        function() ns.Fishing:EquipPreferredSet(); UI:RefreshAll() end)
    createTip(rail.gearButton, "Equip Fishing Set",
        function()
            local pref = ns.DB.profile.gear.preferredSet or ""
            return {
                { "Preferred", pref ~= "" and pref or "None" },
                { "Equipped",  ns.Fishing:GetEquippedSetName() ~= "" and ns.Fishing:GetEquippedSetName() or "None" },
                "Switches to your saved fishing equipment set.",
            }
        end)

    rail.compactButton = createButton(rail, "Compact Tracker", 152, 28,
        "BOTTOMLEFT", rail, "BOTTOMLEFT", 14, 26,
        function() UI:ToggleCompact() end)
    createTip(rail.compactButton, "Compact Tracker",
        { "Toggle the small floating HUD.", "Shows casts, catches, pace, lure, and last catch." })

    -- ── Footer bar ────────────────────────────────────────────────
    local footer = createCard(frame, nil, 28, "BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 10, "panel")
    footer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 10)
    frame.footer = footer

    footer.zone      = createText(footer, "Waters: -",        10, "LEFT",  footer,       "LEFT",  10, 0, Theme.colors.soft)
    footer.skill     = createText(footer, "Skill: 0",         10, "LEFT",  footer.zone,  "RIGHT", 18, 0, Theme.colors.soft)
    footer.rate      = createText(footer, "Pace: 0/hr",       10, "LEFT",  footer.skill, "RIGHT", 18, 0, Theme.colors.soft)
    footer.lure      = createText(footer, "Lure: Missing",    10, "LEFT",  footer.rate,  "RIGHT", 18, 0, Theme.colors.soft)
    footer.lastCatch = createText(footer, "Last: None",       10, "RIGHT", footer,       "RIGHT", -10, 0, Theme.colors.soft)
    ns.Utils:SafeSetWidth(footer.zone,  170)
    ns.Utils:SafeSetWidth(footer.skill,  80)
    ns.Utils:SafeSetWidth(footer.rate,  110)
    ns.Utils:SafeSetWidth(footer.lure,  150)
    ns.Utils:SafeSetWidth(footer.lastCatch, 360)

    createTip(footer, "Session Status",
        function()
            local cur = ns.DB.currentSession
            local lure = ns.Fishing:GetLureState()
            return {
                { "Zone",     cur.zone or "-" },
                { "Skill",    tostring(cur.fishingSkill or 0) },
                { "Pace",     ns.Tracking:GetCatchRate() },
                { "Lure",     lure.label },
                { "Last",     ns.Utils:AbbrevText(cur.lastCatch or "None", 40) },
            }
        end)

    -- Build all tab panels
    self:BuildOverview(frame.tabs.Overview)
    self:BuildWatcher(frame.tabs.Watcher)
    self:BuildAtlas(frame.tabs.Atlas)
    self:BuildEquipment(frame.tabs.Equipment)
    self:BuildTackle(frame.tabs.Tackle)
    self:BuildSession(frame.tabs.Session)
    self:BuildDatabase(frame.tabs.Database)
    self:BuildSettings(frame.tabs.Settings)
    self:CreateCompactFrame()

    frame:Hide()
    ns.state.frame = frame
    self:ShowTab("Overview")
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: OVERVIEW
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildOverview(panel)
    -- ── Hero card ────────────────────────────────────────────────
    local hero = createCard(panel, nil, 118, "TOPLEFT", panel, "TOPLEFT", 12, -12, "hero")
    hero:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -12)
    panel.hero = hero

    if M and M.custom then
        -- Portrait right edge watermark
        local portrait = hero:CreateTexture(nil, "ARTWORK")
        portrait:SetTexture(M.custom.portrait)
        portrait:SetSize(108, 108)
        portrait:SetPoint("RIGHT", hero, "RIGHT", -12, 0)
        portrait:SetVertexColor(1, 1, 1, 0.88)

        -- Wide emblem watermark
        local emblem = hero:CreateTexture(nil, "BACKGROUND", nil, -7)
        emblem:SetTexture(M.custom.emblem)
        emblem:SetSize(118, 118)
        emblem:SetPoint("RIGHT", hero, "RIGHT", -10, 0)
        emblem:SetVertexColor(1, 1, 1, 0.06)
    end

    panel.heroTitle = createText(hero, "Command the shoreline", 22,
        "TOPLEFT", hero, "TOPLEFT", 16, -12,
        Theme.colors.lantern, TITLE_FONT, "THICKOUTLINE")
    panel.heroSub = createText(hero, "", 11, "TOPLEFT", panel.heroTitle, "BOTTOMLEFT",
        0, -6, Theme.colors.soft)
    enableWrap(panel.heroSub, 720, 2)
    panel.heroNote = createText(hero, "", 10, "TOPLEFT", panel.heroSub, "BOTTOMLEFT",
        0, -8, Theme.colors.muted)
    ns.Utils:SafeSetWidth(panel.heroNote, 730)

    -- Quick-action buttons inside hero
    panel.heroButtons = {}
    panel.heroButtons.session = createButton(hero, "Start Session", 128, 28,
        "BOTTOMLEFT", hero, "BOTTOMLEFT", 14, 8,
        function()
            if ns.DB.currentSession.active then ns.Session:Stop(true) else ns.Session:Start() end
            UI:RefreshAll()
        end, true)
    panel.heroButtons.gear = createButton(hero, "Equip Set", 110, 28,
        "LEFT", panel.heroButtons.session, "RIGHT", 8, 0,
        function() ns.Fishing:EquipPreferredSet(); UI:RefreshAll() end)
    panel.heroButtons.restore = createButton(hero, "Restore Gear", 110, 28,
        "LEFT", panel.heroButtons.gear, "RIGHT", 8, 0,
        function() ns.Fishing:RestorePreviousSet(); UI:RefreshAll() end)
    panel.heroButtons.compact = createButton(hero, "Compact HUD", 110, 28,
        "LEFT", panel.heroButtons.restore, "RIGHT", 8, 0,
        function() UI:ToggleCompact() end)

    -- ── Five stat cards ───────────────────────────────────────────
    panel.stats = {}

    panel.stats[1] = createStatCard(panel, "Current Waters",
        "TOPLEFT", hero, "BOTTOMLEFT", 0, -10,
        "Current Fishing Zone",
        function()
            local z = ns.DB.currentSession.zone or ns.Utils:GetZoneName()
            local zs = ns.Tracking:GetZoneStats(z)
            return {
                { "Zone",          z },
                { "Lifetime casts",  tostring(zs.casts   or 0) },
                { "Lifetime catches",tostring(zs.catches or 0) },
                { "Rare catches",    tostring(zs.rares   or 0) },
            }
        end)

    panel.stats[2] = createStatCard(panel, "Fishing Skill",
        "LEFT", panel.stats[1], "RIGHT", 8, 0,
        "Your Fishing Skill",
        function()
            local ts = ns.Fishing:GetFishingToolState()
            return {
                { "Skill",     tostring(ns.DB.currentSession.fishingSkill or 0) },
                { "Tool",      ts.toolLabel or "Unknown" },
                { "Tool mode", getToolModeLabel(ts) },
            }
        end)

    panel.stats[3] = createStatCard(panel, "Fish Pace",
        "LEFT", panel.stats[2], "RIGHT", 8, 0,
        "Catch Pace (per hour)",
        function()
            local c = ns.DB.currentSession
            return {
                { "Catch pace",  ns.Tracking:GetCatchRate() },
                { "Cast pace",   ns.Tracking:GetCastRate() },
                { "Success rate",getSuccessRate(c.catches, c.casts) },
                { "Elapsed",     ns.Utils:FormatDuration(ns.Session:GetElapsed()) },
            }
        end)

    panel.stats[4] = createStatCard(panel, "Unique Catches",
        "LEFT", panel.stats[3], "RIGHT", 8, 0,
        "Unique Species This Session",
        function()
            local c = ns.DB.currentSession
            local top = ns.Tracking:GetSessionTopItems(3)
            local rows = {
                { "Unique",       tostring(ns.Utils:CountTableEntries(c.items or {})) },
                { "Rare catches", tostring(c.rareCatches or 0) },
                { "Pool catches", tostring(c.poolCatches or 0) },
                "",
                "Top catches this session:",
            }
            for i, row in ipairs(top) do
                rows[#rows+1] = string.format("  %d. %s x%d", i, ns.Utils:AbbrevText(row.display,20), row.count)
            end
            return rows
        end)

    panel.stats[5] = createStatCard(panel, "Lure Status",
        "LEFT", panel.stats[4], "RIGHT", 8, 0,
        "Fishing Lure Status",
        function()
            local ls = ns.Fishing:GetLureState()
            local bl = ns.Fishing:GetBestAvailableLure()
            local rows = {
                { "Active",     ls.active and "|cFF4CD96AYes|r" or "|cFFE05C3ANo|r" },
                { "Remaining",  ls.active and ns.Utils:FormatDuration(ls.remaining) or "--:--" },
            }
            if bl then
                rows[#rows+1] = { "Best in bags", bl.name .. " (+" .. tostring(bl.bonus) .. ")" }
                rows[#rows+1] = { "Count",        tostring(bl.count) }
            else
                rows[#rows+1] = "|cFFE05C3ANo lure consumables in bags.|r"
            end
            return rows
        end)

    -- ── Live Deck card ────────────────────────────────────────────
    panel.activity = createCard(panel, 310, nil, "TOPLEFT", panel.stats[1], "BOTTOMLEFT", 0, -12, "panel")
    panel.activity:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)
    addSectionHeader(panel.activity, "Live Deck", "TOPLEFT", panel.activity, "TOPLEFT", 12, -12)
    local actSub = createText(panel.activity, "Session stats and recent catches", 9,
        "TOPLEFT", panel.activity, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.activityLines = createLineStack(panel.activity, 9, 12, -54, 286, 26)

    -- ── Fish Watcher mini-card ─────────────────────────────────
    panel.watcher = createCard(panel, nil, nil, "TOPLEFT", panel.activity, "TOPRIGHT", 10, 0, "alt")
    panel.watcher:SetPoint("TOPRIGHT",    panel, "TOPRIGHT",  -12, -130)
    panel.watcher:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.watcher, "Fish Watcher", "TOPLEFT", panel.watcher, "TOPLEFT", 12, -12)
    panel.watcherSub = createText(panel.watcher, "", 9,
        "TOPLEFT", panel.watcher, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.watcherLines = createLineStack(panel.watcher, 10, 12, -54, 480, 20, nil, 1)

    -- Tooltip on watcher lines
    createTip(panel.watcher, "Fish Watcher",
        function()
            local z = ns.DB.currentSession.zone or ns.Utils:GetZoneName()
            local zs = ns.Tracking:GetZoneStats(z)
            return {
                { "Zone",    z },
                { "Catches", tostring(zs.catches or 0) },
                { "Rares",   tostring(zs.rares   or 0) },
                "",
                "Lifetime recorded catches in this zone, sorted by frequency.",
            }
        end)
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: WATCHER
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildWatcher(panel)
    -- ── Summary banner ────────────────────────────────────────────
    local summary = createCard(panel, nil, 80, "TOPLEFT", panel, "TOPLEFT", 12, -12, "hero")
    summary:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -12)
    panel.summary = summary

    if M and M.custom then
        local rodArt = summary:CreateTexture(nil, "ARTWORK")
        rodArt:SetTexture(M.custom.rodSmall)
        rodArt:SetSize(180, 44)
        rodArt:SetPoint("TOPRIGHT", summary, "TOPRIGHT", -14, -18)
        rodArt:SetVertexColor(1, 1, 1, 0.70)
    end

    panel.summaryTitle = createText(summary, "Fish Watcher", 22,
        "TOPLEFT", summary, "TOPLEFT", 16, -12,
        Theme.colors.lantern, TITLE_FONT, "THICKOUTLINE")
    panel.summarySub = createText(summary, "Zone memory, hotspot waters, and rare catch archive.", 11,
        "TOPLEFT", panel.summaryTitle, "BOTTOMLEFT", 0, -6,
        Theme.colors.soft)
    enableWrap(panel.summarySub, 720, 1)
    panel.summaryMeta = createText(summary, "", 10,
        "TOPLEFT", panel.summarySub, "BOTTOMLEFT", 0, -6,
        Theme.colors.muted)
    ns.Utils:SafeSetWidth(panel.summaryMeta, 800)

    -- ── Zone Ledger ───────────────────────────────────────────────
    panel.zoneCard = createCard(panel, 436, nil, "TOPLEFT", summary, "BOTTOMLEFT", 0, -10, "panel")
    panel.zoneCard:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)
    addSectionHeader(panel.zoneCard, "Current Zone Ledger", "TOPLEFT", panel.zoneCard, "TOPLEFT", 12, -12)
    local zoneSub = createText(panel.zoneCard, "Your most common catches here, ordered by memory.", 9,
        "TOPLEFT", panel.zoneCard, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.zoneLines = createLineStack(panel.zoneCard, 11, 12, -54, 410, 22, nil, 1)

    createTip(panel.zoneCard, "Zone Ledger",
        function()
            local z = ns.DB.currentSession.zone or ns.Utils:GetZoneName()
            local zs = ns.Tracking:GetZoneStats(z)
            local c, u, r = ns.Tracking:GetZoneQualityBreakdown(z)
            return {
                { "Zone",     z },
                { "Catches",  tostring(zs.catches or 0) },
                { "Common",   tostring(c) },
                { "Uncommon", tostring(u) },
                { "Rare",     tostring(r) },
            }
        end)

    -- ── Hotspot Waters ────────────────────────────────────────────
    panel.hotspots = createCard(panel, nil, 234, "TOPLEFT", panel.zoneCard, "TOPRIGHT", 10, 0, "alt")
    panel.hotspots:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -92)
    addSectionHeader(panel.hotspots, "Hotspot Waters", "TOPLEFT", panel.hotspots, "TOPLEFT", 12, -12)
    local hotSub = createText(panel.hotspots, "Lifetime catch leaders across your journey.", 9,
        "TOPLEFT", panel.hotspots, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.hotspotLines = createLineStack(panel.hotspots, 7, 12, -54, 400, 22, nil, 1)

    createTip(panel.hotspots, "Hotspot Waters",
        { "Zones ranked by total lifetime catches.", "Shows all zones you have fished in." })

    -- ── Rare Archive ──────────────────────────────────────────────
    panel.rares = createCard(panel, nil, nil, "TOPLEFT", panel.hotspots, "BOTTOMLEFT", 0, -10, "panel")
    panel.rares:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.rares, "Rare Archive", "TOPLEFT", panel.rares, "TOPLEFT", 12, -12)
    local rareSub = createText(panel.rares, "Recent quality catches and their zone of origin.", 9,
        "TOPLEFT", panel.rares, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.rareLines = createLineStack(panel.rares, 7, 12, -54, 400, 22, nil, 1)

    createTip(panel.rares, "Rare Archive",
        { "Uncommon/Rare-quality items caught this account.", "Sorted by time of catch, most recent first." })
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: ATLAS
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildAtlas(panel)
    local hero = createCard(panel, nil, 84, "TOPLEFT", panel, "TOPLEFT", 12, -12, "hero")
    hero:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -12)
    panel.hero = hero

    if M and M.icon then
        local compass = hero:CreateTexture(nil, "ARTWORK")
        compass:SetTexture(M.icon.compass)
        compass:SetSize(64, 64)
        compass:SetPoint("RIGHT", hero, "RIGHT", -18, 0)
        compass:SetVertexColor(0.70, 0.90, 1.00, 0.80)
    end

    panel.heroTitle = createText(hero, "Azeroth Angler Atlas", 22,
        "TOPLEFT", hero, "TOPLEFT", 16, -12,
        Theme.colors.lantern, TITLE_FONT, "THICKOUTLINE")
    panel.heroSub = createText(hero, "Per-zone fish, catch rates, pools, recipes, requirements, and gear notes.", 11,
        "TOPLEFT", panel.heroTitle, "BOTTOMLEFT", 0, -6, Theme.colors.soft)
    enableWrap(panel.heroSub, 760, 1)
    panel.heroMeta = createText(hero, "Source: -", 9,
        "TOPLEFT", panel.heroSub, "BOTTOMLEFT", 0, -6, Theme.colors.muted)
    ns.Utils:SafeSetWidth(panel.heroMeta, 760)

    panel.left = createCard(panel, 450, nil, "TOPLEFT", hero, "BOTTOMLEFT", 0, -10, "panel")
    panel.left:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)
    addSectionHeader(panel.left, "Zone Details", "TOPLEFT", panel.left, "TOPLEFT", 12, -12)
    createText(panel.left, "Fishing level, fish rates, and pool info for your current waters.", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.zoneLines = createLineStack(panel.left, 14, 12, -54, 418, 20, nil, 1)

    createTip(panel.left, "Zone Details",
        { "Embedded atlas data — no external addon required.",
          "Data covers fish catch rates, target skill level,",
          "pool counts, and map identification." })

    panel.topRight = createCard(panel, nil, 234, "TOPLEFT", panel.left, "TOPRIGHT", 10, 0, "alt")
    panel.topRight:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -96)
    addSectionHeader(panel.topRight, "Recipes and Prices", "TOPLEFT", panel.topRight, "TOPLEFT", 12, -12)
    createText(panel.topRight, "Recipe outputs with optional Auctionator market snapshots.", 9,
        "TOPLEFT", panel.topRight, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.recipeLines = createLineStack(panel.topRight, 8, 12, -54, 430, 20, nil, 1)

    createTip(panel.topRight, "Recipes and Prices",
        { "Shows cooking recipe outputs for fish caught in this zone.",
          "Price data populated by Auctionator (if installed).",
          "Falls back to market averages when data is unavailable." })

    panel.bottomRight = createCard(panel, nil, nil, "TOPLEFT", panel.topRight, "BOTTOMLEFT", 0, -10, "panel")
    panel.bottomRight:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.bottomRight, "Requirements and Equipment", "TOPLEFT", panel.bottomRight, "TOPLEFT", 12, -12)
    createText(panel.bottomRight, "Time/season restrictions, map IDs, and recommended fishing gear.", 9,
        "TOPLEFT", panel.bottomRight, "TOPLEFT", 12, -30, Theme.colors.muted)

    local detailScroll = CreateFrame("ScrollFrame", "AzerothAnglerDetailScroll", panel.bottomRight, "UIPanelScrollFrameTemplate")
    detailScroll:SetPoint("TOPLEFT",     panel.bottomRight, "TOPLEFT",      8, -52)
    detailScroll:SetPoint("BOTTOMRIGHT", panel.bottomRight, "BOTTOMRIGHT", -26,   8)

    local detailChild = CreateFrame("Frame", nil, detailScroll)
    detailChild:SetWidth(400)
    detailChild:SetHeight(20)
    detailScroll:SetScrollChild(detailChild)

    local detailText = detailChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailText:SetFont(BODY_FONT, 11, "")
    detailText:SetPoint("TOPLEFT", detailChild, "TOPLEFT", 4, -4)
    detailText:SetWidth(388)
    detailText:SetJustifyH("LEFT")
    detailText:SetJustifyV("TOP")
    detailText:SetWordWrap(true)
    detailText:SetTextColor(unpack(Theme.colors.foam))

    panel.detailScroll     = detailScroll
    panel.detailScrollChild = detailChild
    panel.detailText       = detailText
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: EQUIPMENT
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildEquipment(panel)
    local hero = createCard(panel, nil, 88, "TOPLEFT", panel, "TOPLEFT", 12, -12, "hero")
    hero:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -12)
    panel.hero = hero

    if M and M.custom then
        local rodArt = hero:CreateTexture(nil, "ARTWORK")
        rodArt:SetTexture(M.custom.rodLarge)
        rodArt:SetSize(80, 80)
        rodArt:SetPoint("RIGHT", hero, "RIGHT", -14, 0)
        rodArt:SetVertexColor(1, 1, 1, 0.88)
    end

    panel.heroTitle = createText(hero, "Gear Studio", 22,
        "TOPLEFT", hero, "TOPLEFT", 16, -12,
        Theme.colors.lantern, TITLE_FONT, "THICKOUTLINE")
    panel.heroSub = createText(hero, "Save fishing set, cycle equipment sets, equip preferred, restore previous outfit.", 11,
        "TOPLEFT", panel.heroTitle, "BOTTOMLEFT", 0, -6, Theme.colors.soft)
    enableWrap(panel.heroSub, 760, 1)

    -- Left: current gear readout
    panel.left = createCard(panel, 450, nil, "TOPLEFT", hero, "BOTTOMLEFT", 0, -10, "panel")
    panel.left:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)
    addSectionHeader(panel.left, "Current Gear", "TOPLEFT", panel.left, "TOPLEFT", 12, -12)
    createText(panel.left, "Live readout of rod, lure, sets, and restore target.", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.leftLines = createLineStack(panel.left, 9, 12, -54, 416, 24, nil, 1)

    createTip(panel.left, "Current Gear",
        function()
            local ts = ns.Fishing:GetFishingToolState()
            local ls = ns.Fishing:GetLureState()
            return {
                { "Tool",     ts.toolLabel or "Unknown" },
                { "Mode",     getToolModeLabel(ts) },
                { "Skill",    tostring(ns.DB.currentSession.fishingSkill or 0) },
                { "Lure",     ls.label },
                { "Enchant ID", tostring(ts.enchantID or 0) },
            }
        end)

    -- Right: set manager + buttons
    panel.right = createCard(panel, nil, nil, "TOPLEFT", panel.left, "TOPRIGHT", 10, 0, "alt")
    panel.right:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.right, "Gear Studio", "TOPLEFT", panel.right, "TOPLEFT", 12, -12)
    createText(panel.right, "Equipment sets detected from Equipment Manager.", 9,
        "TOPLEFT", panel.right, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.setLines = createLineStack(panel.right, 9, 12, -54, 430, 24, nil, 1)

    createTip(panel.right, "Equipment Sets",
        function()
            local sets = ns.Fishing:GetEquipmentSets()
            local rows = { string.format("%d sets found.", #sets), "" }
            for i = 1, math.min(5, #sets) do
                rows[#rows+1] = sets[i].name
            end
            return rows
        end)

    panel.buttons = {}
    panel.buttons.save    = createButton(panel.right, "Save Equipped Set", 150, 28,
        "BOTTOMLEFT", panel.right, "BOTTOMLEFT", 12, 48,
        function() ns.Fishing:SaveCurrentSetAsPreferred(); UI:RefreshAll() end, true)
    panel.buttons.prev    = createButton(panel.right, "Prev",  80, 28,
        "LEFT", panel.buttons.save, "RIGHT", 8, 0,
        function() ns.Fishing:CyclePreferredSet(-1); UI:RefreshAll() end)
    panel.buttons.next    = createButton(panel.right, "Next",  80, 28,
        "LEFT", panel.buttons.prev, "RIGHT", 8, 0,
        function() ns.Fishing:CyclePreferredSet(1); UI:RefreshAll() end)
    panel.buttons.equip   = createButton(panel.right, "Equip Preferred", 128, 28,
        "BOTTOMLEFT", panel.right, "BOTTOMLEFT", 12, 14,
        function() ns.Fishing:EquipPreferredSet(); UI:RefreshAll() end)
    panel.buttons.restore = createButton(panel.right, "Restore Previous", 128, 28,
        "LEFT", panel.buttons.equip, "RIGHT", 8, 0,
        function() ns.Fishing:RestorePreviousSet(); UI:RefreshAll() end)
    panel.buttons.compact = createButton(panel.right, "Compact HUD", 118, 28,
        "LEFT", panel.buttons.restore, "RIGHT", 8, 0,
        function() UI:ToggleCompact() end)

    createTip(panel.buttons.save,    "Save Equipped Set",    { "Saves whichever equipment set you currently have equipped", "as your preferred fishing set." })
    createTip(panel.buttons.equip,   "Equip Preferred Set",  { "Immediately switches to your saved preferred fishing set." })
    createTip(panel.buttons.restore, "Restore Previous Set", { "Switches back to the outfit you had before equipping your", "fishing gear. Useful for returning to combat or crafting sets." })
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: TACKLE
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildTackle(panel)
    local hero = createCard(panel, nil, 88, "TOPLEFT", panel, "TOPLEFT", 12, -12, "hero")
    hero:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -12)
    panel.hero = hero

    if M and M.custom then
        local hookArt = hero:CreateTexture(nil, "ARTWORK")
        hookArt:SetTexture(M.custom.hookDisc)
        hookArt:SetSize(80, 80)
        hookArt:SetPoint("RIGHT", hero, "RIGHT", -12, 0)
        hookArt:SetVertexColor(1, 1, 1, 0.90)
    end

    panel.heroTitle = createText(hero, "Tackle Deck", 22,
        "TOPLEFT", hero, "TOPLEFT", 16, -12,
        Theme.colors.lantern, TITLE_FONT, "THICKOUTLINE")
    panel.heroSub = createText(hero, "Lure status, bag inventory, and one-click lure application.", 11,
        "TOPLEFT", panel.heroTitle, "BOTTOMLEFT", 0, -6, Theme.colors.soft)
    enableWrap(panel.heroSub, 760, 1)

    panel.left = createCard(panel, 450, nil, "TOPLEFT", hero, "BOTTOMLEFT", 0, -10, "panel")
    panel.left:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)
    addSectionHeader(panel.left, "Tackle Snapshot", "TOPLEFT", panel.left, "TOPLEFT", 12, -12)
    createText(panel.left, "Live lure and rod status with auto-lure state.", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.leftLines = createLineStack(panel.left, 11, 12, -54, 416, 22, nil, 1)

    createTip(panel.left, "Tackle Snapshot",
        function()
            local ls = ns.Fishing:GetLureState()
            local bl = ns.Fishing:GetBestAvailableLure()
            local rows = {
                { "Lure active",  ls.active and "|cFF4CD96AYes|r" or "|cFFE05C3ANo|r" },
                { "Remaining",    ls.active and ns.Utils:FormatDuration(ls.remaining) or "--:--" },
            }
            if bl then
                rows[#rows+1] = { "Best available", bl.name }
                rows[#rows+1] = { "Skill bonus",    "+" .. tostring(bl.bonus) }
                rows[#rows+1] = { "In bags",        tostring(bl.count) }
            end
            return rows
        end)

    panel.right = createCard(panel, nil, nil, "TOPLEFT", panel.left, "TOPRIGHT", 10, 0, "alt")
    panel.right:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.right, "Available Lures", "TOPLEFT", panel.right, "TOPLEFT", 12, -12)
    createText(panel.right, "Known lure consumables detected in your bags.", 9,
        "TOPLEFT", panel.right, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.lureLines = createLineStack(panel.right, 9, 12, -54, 400, 22, nil, 1)

    createTip(panel.right, "Available Lures",
        { "Lure consumables currently in your bags.",
          "[S] = selected/preferred lure.",
          "Use Lure Prev/Next to change preferred lure.",
          "Use Apply Selected to apply immediately." })

    panel.buttons = {}
    panel.buttons.apply   = createButton(panel.right, "Apply Selected", 124, 28,
        "BOTTOMLEFT", panel.right, "BOTTOMLEFT", 12, 48,
        function()
            if not ns.Fishing:ApplyBestLure("manual") then
                ns.Utils:Print("No lure was applied. Check that a lure consumable is in your bags.")
            end
            UI:RefreshAll()
        end, true)
    panel.buttons.prevLure= createButton(panel.right, "Lure Prev", 90, 28,
        "BOTTOMLEFT", panel.right, "BOTTOMLEFT", 12, 14,
        function() ns.Fishing:CyclePreferredLure(-1); UI:RefreshAll() end)
    panel.buttons.nextLure= createButton(panel.right, "Lure Next", 90, 28,
        "LEFT", panel.buttons.prevLure, "RIGHT", 8, 0,
        function() ns.Fishing:CyclePreferredLure(1); UI:RefreshAll() end)
    panel.buttons.equip   = createButton(panel.right, "Equipment", 90, 28,
        "LEFT", panel.buttons.nextLure, "RIGHT", 8, 0,
        function() UI:Open("Equipment") end)
    panel.buttons.settings= createButton(panel.right, "Settings", 90, 28,
        "LEFT", panel.buttons.equip, "RIGHT", 8, 0,
        function() UI:Open("Settings") end)

    createTip(panel.buttons.apply, "Apply Selected Lure",
        function()
            local bl = ns.Fishing:GetBestAvailableLure()
            if bl then
                return {
                    "Will attempt to apply:",
                    { "Lure",  bl.name },
                    { "Bonus", "+" .. tostring(bl.bonus) .. " fishing skill" },
                    { "Count", tostring(bl.count) .. " in bags" },
                }
            end
            return { "|cFFE05C3ANo lure consumables found in bags.|r" }
        end)
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: SESSION
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildSession(panel)
    -- Left: session summary
    panel.summary = createCard(panel, 292, nil, "TOPLEFT", panel, "TOPLEFT", 12, -12, "panel")
    panel.summary:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)

    if M and M.custom then
        addBg(panel.summary, M.custom.standing, 0.14, 1, 1, 1, -8)
        local hookIcon = panel.summary:CreateTexture(nil, "ARTWORK")
        hookIcon:SetTexture(M.custom.iconHook)
        hookIcon:SetSize(40, 40)
        hookIcon:SetPoint("TOPRIGHT", panel.summary, "TOPRIGHT", -10, -10)
        hookIcon:SetVertexColor(1, 1, 1, 0.80)
    end

    addSectionHeader(panel.summary, "Session Summary", "TOPLEFT", panel.summary, "TOPLEFT", 12, -12)
    createText(panel.summary, "Current run at a glance.", 9,
        "TOPLEFT", panel.summary, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.summaryLines = createLineStack(panel.summary, 11, 12, -54, 262, 26)

    createTip(panel.summary, "Session Summary",
        function()
            local c = ns.DB.currentSession
            return {
                { "Status",  c.active and "|cFF4CD96AActive|r" or "Idle" },
                { "Elapsed", ns.Utils:FormatDuration(ns.Session:GetElapsed()) },
                { "Pace",    ns.Tracking:GetCatchRate() },
                { "Hit rate",getSuccessRate(c.catches, c.casts) },
                { "Rares",   tostring(c.rareCatches or 0) },
            }
        end)

    -- Middle: catch ledger
    panel.catches = createCard(panel, 300, nil, "TOPLEFT", panel.summary, "TOPRIGHT", 10, 0, "alt")
    panel.catches:SetPoint("BOTTOMLEFT", panel.summary, "BOTTOMRIGHT", 10, 0)
    addSectionHeader(panel.catches, "Session Catch Ledger", "TOPLEFT", panel.catches, "TOPLEFT", 12, -12)
    createText(panel.catches, "Top catches and most recent pulls this session.", 9,
        "TOPLEFT", panel.catches, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.catchLines = createLineStack(panel.catches, 11, 12, -54, 270, 24, nil, 1)

    -- Right: session history
    panel.history = createCard(panel, nil, nil, "TOPLEFT", panel.catches, "TOPRIGHT", 10, 0, "panel")
    panel.history:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.history, "Recent Voyages", "TOPLEFT", panel.history, "TOPLEFT", 12, -12)
    createText(panel.history, "Saved sessions with catch pace and rare totals.", 9,
        "TOPLEFT", panel.history, "TOPLEFT", 12, -30, Theme.colors.muted)
    panel.historyLines = createLineStack(panel.history, 11, 12, -54, 250, 24, nil, 1)

    createTip(panel.history, "Recent Voyages",
        function()
            local count = #(ns.DB.history.sessions or {})
            return {
                string.format("%d sessions saved (max 20).", count),
                "Sessions are saved when you click End Session.",
                "Format: Zone | Catches/Casts | Pace | Rares",
            }
        end)
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: DATABASE
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildDatabase(panel)
    local hero = createCard(panel, nil, 88, "TOPLEFT", panel, "TOPLEFT", 12, -12, "hero")
    hero:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -12)
    panel.hero = hero

    panel.heroTitle = createText(hero, "Fish Database", 20,
        "TOPLEFT", hero, "TOPLEFT", 12, -10,
        Theme.colors.lantern, TITLE_FONT, "OUTLINE")
    panel.heroSub = createText(hero, "Search imported fish master data and inspect detailed fish attributes.", 11,
        "TOPLEFT", panel.heroTitle, "BOTTOMLEFT", 0, -5, Theme.colors.soft)
    enableWrap(panel.heroSub, 760, 1)
    panel.heroMeta = createText(hero, "Matches: 0", 9,
        "TOPLEFT", panel.heroSub, "BOTTOMLEFT", 0, -6, Theme.colors.muted)

    panel.left = createCard(panel, 450, nil, "TOPLEFT", hero, "BOTTOMLEFT", 0, -10, "panel")
    panel.left:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 12, 12)
    addSectionHeader(panel.left, "Search", "TOPLEFT", panel.left, "TOPLEFT", 12, -12)
    createText(panel.left, "Filter by fish name.", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -30, Theme.colors.muted)

    panel.searchBox = CreateFrame("EditBox", nil, panel.left, "InputBoxTemplate")
    panel.searchBox:SetSize(300, 26)
    panel.searchBox:SetPoint("TOPLEFT", panel.left, "TOPLEFT", 12, -54)
    panel.searchBox:SetAutoFocus(false)
    panel.searchBox:SetMaxLetters(64)
    panel.searchBox:SetScript("OnTextChanged", function()
        UI:RefreshDatabase()
    end)
    panel.searchBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)

    panel.clearBtn = createButton(panel.left, "Clear", 70, 24,
        "LEFT", panel.searchBox, "RIGHT", 8, 0,
        function()
            panel.searchBox:SetText("")
            panel.searchBox:ClearFocus()
            UI:RefreshDatabase()
        end)

    panel.dbSortKey = panel.dbSortKey or "weightLb"
    if panel.dbSortDesc == nil then panel.dbSortDesc = true end
    panel.dbSelectedItemID = panel.dbSelectedItemID or nil
    panel.dbSelectedName = panel.dbSelectedName or nil

    local function setSort(sortKey)
        if panel.dbSortKey == sortKey then
            panel.dbSortDesc = not panel.dbSortDesc
        else
            panel.dbSortKey = sortKey
            panel.dbSortDesc = true
        end
        UI:RefreshDatabase()
    end

    panel.sortWeightBtn = createButton(panel.left, "Weight", 82, 22,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -84,
        function() setSort("weightLb") end)
    panel.sortLengthBtn = createButton(panel.left, "Length", 82, 22,
        "LEFT", panel.sortWeightBtn, "RIGHT", 6, 0,
        function() setSort("sizeIn") end)
    panel.sortNameBtn = createButton(panel.left, "Name", 72, 22,
        "LEFT", panel.sortLengthBtn, "RIGHT", 6, 0,
        function() setSort("name") end)

    panel.listHeader = createText(panel.left, "Weight | Length | Name", 10,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -112, Theme.colors.lanternSoft, BODY_FONT, "OUTLINE")
    panel.colIndex = createText(panel.left, "#", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 12, -130, Theme.colors.muted, BODY_FONT, "OUTLINE")
    panel.colWeight = createText(panel.left, "Weight", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 30, -130, Theme.colors.muted, BODY_FONT, "OUTLINE")
    panel.colLength = createText(panel.left, "Length", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 108, -130, Theme.colors.muted, BODY_FONT, "OUTLINE")
    panel.colName = createText(panel.left, "Name", 9,
        "TOPLEFT", panel.left, "TOPLEFT", 184, -130, Theme.colors.muted, BODY_FONT, "OUTLINE")

    panel.rowHeight = 19
    panel.visibleRowCount = 14
    panel.resultScroll = CreateFrame("ScrollFrame", nil, panel.left, "FauxScrollFrameTemplate")
    panel.resultScroll:SetPoint("TOPLEFT", panel.left, "TOPLEFT", 12, -148)
    panel.resultScroll:SetPoint("BOTTOMRIGHT", panel.left, "BOTTOMRIGHT", -26, 12)
    panel.resultScroll:SetScript("OnVerticalScroll", function(self, offset)
        if FauxScrollFrame_OnVerticalScroll then
            FauxScrollFrame_OnVerticalScroll(self, offset, panel.rowHeight, function() UI:RefreshDatabase() end)
        end
    end)

    panel.resultRows = {}
    for i = 1, panel.visibleRowCount do
        local rowBtn = CreateFrame("Button", nil, panel.left)
        rowBtn:SetSize(420, 18)
        rowBtn:SetPoint("TOPLEFT", panel.resultScroll, "TOPLEFT", 0, -((i - 1) * panel.rowHeight))

        rowBtn.bg = rowBtn:CreateTexture(nil, "BACKGROUND")
        rowBtn.bg:SetAllPoints(rowBtn)
        rowBtn.bg:SetColorTexture(0, 0, 0, 0.18)

        rowBtn.index = createText(rowBtn, "", 9, "LEFT", rowBtn, "LEFT", 2, 0, Theme.colors.muted, BODY_FONT, "OUTLINE")
        rowBtn.weight = createText(rowBtn, "", 10, "LEFT", rowBtn, "LEFT", 20, 0, Theme.colors.foam, BODY_FONT)
        rowBtn.length = createText(rowBtn, "", 10, "LEFT", rowBtn, "LEFT", 98, 0, Theme.colors.foam, BODY_FONT)
        rowBtn.name = createText(rowBtn, "", 10, "LEFT", rowBtn, "LEFT", 174, 0, Theme.colors.bone, BODY_FONT)

        rowBtn:SetScript("OnEnter", function(self)
            if not self.isSelected then
                self.bg:SetColorTexture(0.074, 0.180, 0.196, 0.34)
            end
        end)
        rowBtn:SetScript("OnLeave", function(self)
            if self.isSelected then
                self.bg:SetColorTexture(0.114, 0.302, 0.322, 0.58)
            else
                self.bg:SetColorTexture(0, 0, 0, 0.18)
            end
        end)
        rowBtn:SetScript("OnClick", function(self)
            if not self.rowData then return end
            panel.dbSelectedItemID = tonumber(self.rowData.itemID) or nil
            panel.dbSelectedName = string.lower(ns.Utils:CleanDisplayText(self.rowData.name or ""))
            UI:RefreshDatabase()
        end)

        panel.resultRows[i] = rowBtn
    end

    panel.right = createCard(panel, nil, nil, "TOPLEFT", panel.left, "TOPRIGHT", 10, 0, "alt")
    panel.right:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)
    addSectionHeader(panel.right, "Details", "TOPLEFT", panel.right, "TOPLEFT", 12, -12)
    createText(panel.right, "Selected fish record from the imported spreadsheet database.", 9,
        "TOPLEFT", panel.right, "TOPLEFT", 12, -30, Theme.colors.muted)

    panel.detailIcon = panel.right:CreateTexture(nil, "ARTWORK")
    panel.detailIcon:SetSize(28, 28)
    panel.detailIcon:SetPoint("TOPRIGHT", panel.right, "TOPRIGHT", -14, -42)
    panel.detailIcon:Hide()

    panel.detailLines = createLineStack(panel.right, 14, 12, -64, 400, 22, nil, 1)
end

-- ═════════════════════════════════════════════════════════════════════
--  TAB: SETTINGS
-- ═════════════════════════════════════════════════════════════════════
function UI:BuildSettings(panel)
    panel.card = createCard(panel, nil, nil, "TOPLEFT", panel, "TOPLEFT", 12, -12, "panel")
    panel.card:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -12, 12)

    -- Tuskarr — bottom-right art
    if M and M.custom then
        local tuskarr = panel.card:CreateTexture(nil, "ARTWORK")
        tuskarr:SetTexture(M.custom.standing)
        tuskarr:SetSize(210, 226)
        tuskarr:SetPoint("BOTTOMRIGHT", panel.card, "BOTTOMRIGHT", -8, 6)
        tuskarr:SetVertexColor(1, 1, 1, 0.92)
    end

    -- Panel header
    local hdrTitle = createText(panel.card, "Settings", 20,
        "TOPLEFT", panel.card, "TOPLEFT", 16, -14,
        Theme.colors.lantern, TITLE_FONT, "OUTLINE")
    createText(panel.card, "Tune cast flow, alerts, lure behavior, and interface preferences.", 10,
        "TOPLEFT", hdrTitle, "BOTTOMLEFT", 0, -4, Theme.colors.soft)
    panel.version = createText(panel.card, "v" .. getAddonVersion() .. "  \xc2\xb7  ForgeDMC", 9,
        "TOPLEFT", hdrTitle, "BOTTOMLEFT", 0, -18, Theme.colors.muted)
    addDivider(panel.card, "TOPLEFT", panel.card, "TOPLEFT", 12, -58, nil)

    -- ── 3-column layout ───────────────────────────────────────────────
    -- Each column: C1=14, C2=304, C3=594  |  W=278 per column
    -- Row height: 36px + 1px gap = 37px stride
    -- 12 rows max per column fits cleanly in the card height
    local C1, C2, C3 = 14, 304, 594
    local W = 278
    local rows = {}

    -- Amber group label + thin rule
    local function grp(text, anchor, rel, relAnchor, ox, oy)
        local lbl = createText(panel.card, text, 10,
            anchor, rel, relAnchor, ox, oy,
            Theme.colors.lanternSoft, BODY_FONT, "OUTLINE")
        local rule = panel.card:CreateTexture(nil, "BORDER")
        rule:SetHeight(1)
        rule:SetPoint("TOPLEFT",  lbl, "BOTTOMLEFT",  0, -3)
        rule:SetWidth(W - 10)
        rule:SetColorTexture(0.941, 0.627, 0.125, 0.30)
        return lbl
    end

    -- Compact check row (36px height) anchored below rel
    local function row(idx, label, desc, rel, ox, getter, setter)
        local holder = CreateFrame("Frame", nil, panel.card)
        holder:SetSize(W, 36)
        holder:SetPoint("TOPLEFT", rel, "BOTTOMLEFT", ox, -1)
        holder:SetClipsChildren(true)
        local cb = CreateFrame("CheckButton", nil, holder, "UICheckButtonTemplate")
        cb:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, -1)
        local title = createText(holder, label, 10, "TOPLEFT", cb, "TOPRIGHT", 4, -1, Theme.colors.foam)
        local sub   = createText(holder, desc,   9, "TOPLEFT", title, "BOTTOMLEFT", 0, -1, Theme.colors.soft)
        enableWrap(sub, W - 46, 1)
        cb:SetScript("OnClick", function(self)
            setter(self:GetChecked() and true or false)
            UI:RefreshAll()
        end)
        holder.check  = cb
        holder.title  = title
        holder.sub    = sub
        holder.getter = getter
        rows[idx] = holder
        return holder
    end

    -- ── COLUMN 1: Casting & Interaction ───────────────────────────────
    local gCI = grp("Casting & Interaction", "TOPLEFT", panel.card, "TOPLEFT", C1, -66)
    row( 1, "Double-right-click cast",   "Quick double-right-click to cast fishing.",          gCI,     0, function() return ns.DB.profile.interaction.doubleClickEnabled end,   function(v) ns.DB.profile.interaction.doubleClickEnabled = v end)
    row( 2, "Recast while fishing",      "Double-click recast while already channeling.",      rows[1], 0, function() return ns.DB.profile.interaction.recastOnDoubleClick end,  function(v) ns.DB.profile.interaction.recastOnDoubleClick = v end)
    row( 3, "Enable Recast keybind",     "Dedicated keybind that always recasts fishing.",     rows[2], 0, function() return ns.DB.profile.interaction.recastKeyEnabled end,     function(v) ns.DB.profile.interaction.recastKeyEnabled = v end)
    row( 4, "Dismount with cast key",    "Pressing cast while mounted dismounts first.",       rows[3], 0, function() return ns.DB.profile.interaction.dismountWithKey end,      function(v) ns.DB.profile.interaction.dismountWithKey = v end)
    row( 5, "Override Lunker cast",      "Allow casting during Lunker's Lure channel.",        rows[4], 0, function() return ns.DB.profile.interaction.overrideLunker end,       function(v) ns.DB.profile.interaction.overrideLunker = v end)
    row( 6, "Auto-deploy raft",          "Use raft item before casting while swimming.",       rows[5], 0, function() return ns.DB.profile.interaction.raftEnabled end,          function(v) ns.DB.profile.interaction.raftEnabled = v end)

    -- ── COLUMN 1 continued: Sleep & Tools ─────────────────────────────
    local gST = grp("Sleep & Tools", "TOPLEFT", rows[6], "BOTTOMLEFT", 0, -10)
    row( 7, "Sleep mode",               "Pause all cast helpers without disabling addon.",     gST,     0, function() return ns.DB.profile.interaction.sleepingMode end,          function(v) if ns.Fishing then ns.Fishing:SetSleeping(v, "settings") end end)
    row( 8, "Sleep when no tool",       "Auto-sleep when no fishing rod is equipped.",         rows[7], 0, function() return ns.DB.profile.interaction.sleepWhenNoTool end,       function(v) ns.DB.profile.interaction.sleepWhenNoTool = v end)

    -- ── COLUMN 2: Alerts & Sounds ─────────────────────────────────────
    local gAS = grp("Alerts & Sounds", "TOPLEFT", panel.card, "TOPLEFT", C2, -66)
    row( 9, "Bite alert sound",          "Play a sound when a catch is detected.",              gAS,     0, function() return ns.DB.profile.alerts.soundOnBite end,               function(v) ns.DB.profile.alerts.soundOnBite = v end)
    row(10, "Enhanced sounds",           "Temporarily boost sound channels while casting.",     rows[9], 0, function() return ns.DB.profile.alerts.enhanceSounds end,             function(v) ns.DB.profile.alerts.enhanceSounds = v end)
    row(11, "Flash on rare catch",       "Flash UI header when a rare item is caught.",         rows[10], 0, function() return ns.DB.profile.alerts.flashOnRare end,              function(v) ns.DB.profile.alerts.flashOnRare = v end)
    row(12, "Milestone alerts",          "Celebrate major lifetime catch totals in chat.",      rows[11], 0, function() return ns.DB.profile.alerts.milestoneAlerts end,          function(v) ns.DB.profile.alerts.milestoneAlerts = v end)
    row(13, "Patient Chest alert",       "Flash when a Patient Chest is detected nearby.",      rows[12], 0, function() return ns.DB.profile.alerts.patientChestAlert end,        function(v) ns.DB.profile.alerts.patientChestAlert = v end)

    -- ── COLUMN 2 continued: Lure Management ───────────────────────────
    local gLM = grp("Lure Management", "TOPLEFT", rows[13], "BOTTOMLEFT", 0, -10)
    row(15, "Missing lure reminder",     "Warn before casting when no lure is active.",         gLM,     0, function() return ns.DB.profile.alerts.lureReminder end,              function(v) ns.DB.profile.alerts.lureReminder = v end)
    row(16, "Lure reminder on zone chg.","Remind to apply lure when entering a new zone.",      rows[15], 0, function() return ns.DB.profile.alerts.lureReminderOnZoneChange end, function(v) ns.DB.profile.alerts.lureReminderOnZoneChange = v end)

    -- ── COLUMN 3: Interface & Display ─────────────────────────────────
    local gUI = grp("Interface & Display", "TOPLEFT", panel.card, "TOPLEFT", C3, -66)
    row(19, "Show compact tracker",      "Show floating HUD when dashboard is closed.",         gUI,     0, function() return ns.DB.profile.ui.compactMode end,                   function(v) ns.DB.profile.ui.compactMode = v if ns.state.compactFrame then ns.state.compactFrame:SetShown(v) end end)
    row(20, "Show dashboard on login",   "Open the dashboard automatically at login.",           rows[19], 0, function() return ns.DB.profile.ui.showMainOnLogin end,             function(v) ns.DB.profile.ui.showMainOnLogin = v end)
    row(21, "Show minimap button",       "Display the Azeroth Angler minimap button.",           rows[20], 0, function() return ns.DB.profile.ui.showMinimapButton end,           function(v) ns.DB.profile.ui.showMinimapButton = v if ns.Minimap then ns.Minimap:RefreshButton() end end)
    row(22, "Lock main window",          "Prevent dragging the main dashboard frame.",           rows[21], 0, function() return ns.DB.profile.ui.locked end,                     function(v) ns.DB.profile.ui.locked = v end)
    row(23, "Nearby auto-ping",          "Broadcast position in /say to reveal nearby users.",  rows[22], 0, function() return ns.DB.profile.nearby.enabled and ns.DB.profile.nearby.autoPing end, function(v) ns.DB.profile.nearby.enabled = v ns.DB.profile.nearby.autoPing = v end)
    row(24, "Auto-restore gear on end",  "Restore previous outfit when ending a session.",       rows[23], 0, function() return ns.DB.profile.gear.autoRestoreOnStop end,        function(v) ns.DB.profile.gear.autoRestoreOnStop = v end)

    panel.rows = rows

    panel.tip = createText(panel.card,
        "Tip: manage gear sets on the Equipment tab  \xc2\xb7  configure preferred lure on the Tackle tab", 9,
        "BOTTOMLEFT", panel.card, "BOTTOMLEFT", 106, 14, Theme.colors.muted)
    enableWrap(panel.tip, 560, 1)
end

-- ═════════════════════════════════════════════════════════════════════
function UI:CreateCompactFrame()
    if ns.state.compactFrame then return end

    local frame = CreateFrame("Frame", "AzerothAnglerCompactFrame", UIParent, "BackdropTemplate")
    frame:SetSize(292, 312)
    frame:SetFrameStrata("HIGH")
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetClampedToScreen(true)
    Theme:ApplyPanel(frame, "hero")

    frame:SetScript("OnDragStart", function(self)
        if not ns.DB.profile.ui.locked then self:StartMoving() end
    end)
    frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        ns.Core:SaveFramePosition(self, "compactPoint")
    end)

    -- Amber top accent bar
    local topBar = frame:CreateTexture(nil, "BORDER")
    topBar:SetPoint("TOPLEFT",  frame, "TOPLEFT",  4, -4)
    topBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4)
    topBar:SetHeight(2)
    topBar:SetColorTexture(0.941, 0.627, 0.125, 0.70)

    -- Hook icon top-right
    if M and M.custom then
        frame.hookIcon = frame:CreateTexture(nil, "ARTWORK")
        frame.hookIcon:SetTexture(M.custom.iconHook)
        frame.hookIcon:SetSize(44, 44)
        frame.hookIcon:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -10, -10)
        frame.hookIcon:SetVertexColor(1, 1, 1, 0.85)
    end

    frame.title = createText(frame, "Azeroth Angler", 15,
        "TOPLEFT", frame, "TOPLEFT", 12, -10,
        Theme.colors.lanternSoft, TITLE_FONT, "OUTLINE")

    frame.statusPill = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    frame.statusPill:SetSize(126, 26)
    frame.statusPill:SetPoint("TOPLEFT", frame.title, "BOTTOMLEFT", 0, -6)
    Theme:ApplyPill(frame.statusPill, "warn")
    frame.statusText = createText(frame.statusPill, "Idle", 11,
        "CENTER", frame.statusPill, "CENTER", 0, 0,
        Theme.colors.foam, BODY_FONT, "OUTLINE", "CENTER")

    -- Stat lines
    local linesY = -62
    local lineH  = 22
    local function cLine(y) return createText(frame, "", 12, "TOPLEFT", frame, "TOPLEFT", 12, y, Theme.colors.foam) end
    local function cLineSub(y) return createText(frame, "", 12, "TOPLEFT", frame, "TOPLEFT", 12, y, Theme.colors.soft) end

    frame.casts     = cLine(linesY)
    frame.catches   = cLine(linesY - lineH)
    frame.rate      = cLineSub(linesY - lineH*2)
    frame.lure      = cLineSub(linesY - lineH*3)
    frame.zone      = cLineSub(linesY - lineH*4)
    frame.lastCatch = cLineSub(linesY - lineH*5)

    frame.recentTitle = createText(frame, "Recent (last 5):", 10,
        "TOPLEFT", frame, "TOPLEFT", 12, linesY - lineH*6, Theme.colors.muted)
    frame.recentCatchLines = {}
    for i = 1, 5 do
        frame.recentCatchLines[i] = createText(frame, "", 10,
            "TOPLEFT", frame, "TOPLEFT", 12, linesY - lineH*6 - (i * 14), Theme.colors.soft)
    end

    for _, fs in ipairs({ frame.casts, frame.catches, frame.rate, frame.lure, frame.zone, frame.lastCatch }) do
        ns.Utils:SafeSetWidth(fs, 262)
    end
    ns.Utils:SafeSetWidth(frame.recentTitle, 262)
    for i = 1, 5 do
        ns.Utils:SafeSetWidth(frame.recentCatchLines[i], 262)
    end

    frame.buttons = {}
    frame.buttons.lure = createButton(frame, "Attach Lure", 126, 24,
        "BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 10,
        function()
            if ns.DB and ns.DB.profile and ns.DB.profile.alerts
                and not ns.DB.profile.alerts.autoAttachLure then
                ns.DB.profile.alerts.autoAttachLure = true
                ns.Utils:Print("Auto-attach lure enabled.")
            end
            if not ns.Fishing:ApplyBestLure("compact-button") then
                ns.Utils:Print("No lure was applied.")
            end
            UI:RefreshAll()
        end, true)
    if frame.buttons.lure.text then
        frame.buttons.lure.text:SetPoint("CENTER", frame.buttons.lure, "CENTER", 7, 0)
    end
    if M and M.icon and M.icon.lure then
        frame.buttons.lure.icon = frame.buttons.lure:CreateTexture(nil, "OVERLAY")
        frame.buttons.lure.icon:SetTexture(M.icon.lure)
        frame.buttons.lure.icon:SetSize(13, 13)
        frame.buttons.lure.icon:SetPoint("LEFT", frame.buttons.lure, "LEFT", 8, 0)
        frame.buttons.lure.icon:SetVertexColor(1, 1, 1, 0.86)
    end
    createTip(frame.buttons.lure, "Attach Lure",
        { "Applies your selected/best available lure.", "Matches the Tackle tab apply action." })

    frame.buttons.equip = createButton(frame, "Equip Gear", 126, 24,
        "LEFT", frame.buttons.lure, "RIGHT", 8, 0,
        function()
            ns.Fishing:EquipPreferredSet()
            UI:RefreshAll()
        end)
    if frame.buttons.equip.text then
        frame.buttons.equip.text:SetPoint("CENTER", frame.buttons.equip, "CENTER", 7, 0)
    end
    if M and M.icon and M.icon.poleNice then
        frame.buttons.equip.icon = frame.buttons.equip:CreateTexture(nil, "OVERLAY")
        frame.buttons.equip.icon:SetTexture(M.icon.poleNice)
        frame.buttons.equip.icon:SetSize(13, 13)
        frame.buttons.equip.icon:SetPoint("LEFT", frame.buttons.equip, "LEFT", 8, 0)
        frame.buttons.equip.icon:SetVertexColor(1, 1, 1, 0.86)
    end
    createTip(frame.buttons.equip, "Equip Gear",
        { "Equips your preferred fishing equipment set.", "Matches the Equipment tab equip action." })

    -- Click to open full UI
    frame:SetScript("OnMouseDown", function(self, btn)
        if btn == "RightButton" then
            UI:Open("Overview")
        end
    end)

    createTip(frame, "Azeroth Angler — Compact HUD",
        { "Right-click to open the full dashboard.", "Left-click and drag to move." })

    frame:Hide()
    ns.state.compactFrame = frame
end

-- ═════════════════════════════════════════════════════════════════════
--  DEBUG FRAME
-- ═════════════════════════════════════════════════════════════════════
function UI:CreateDebugFrame()
    if ns.state.debugFrame then return end

    local frame = createCard(UIParent, 860, 500, "CENTER", UIParent, "CENTER", 0, 0, "frame")
    frame:SetFrameStrata("DIALOG")
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", function(self) self:StartMoving() end)
    frame:SetScript("OnDragStop",  function(self) self:StopMovingOrSizing() end)

    addSectionHeader(frame, "Azeroth Angler Debug Console", "TOPLEFT", frame, "TOPLEFT", 16, -12)
    frame.sub = createText(frame, "Live error log for startup issues, event failures, and UI callback problems.", 11,
        "TOPLEFT", frame, "TOPLEFT", 16, -34, Theme.colors.soft)
    enableWrap(frame.sub, 700, 1)
    frame.summary = createText(frame, "Entries: 0", 10,
        "TOPLEFT", frame.sub, "BOTTOMLEFT", 0, -8, Theme.colors.muted)

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4)

    frame.refreshButton = createButton(frame, "Refresh", 90, 26,
        "TOPRIGHT", frame, "TOPRIGHT", -40, -44,
        function() UI:RefreshDebug() end)
    frame.clearButton = createButton(frame, "Clear", 90, 26,
        "RIGHT", frame.refreshButton, "LEFT", -8, 0,
        function()
            if ns.Debug and ns.Debug.Clear then ns.Debug:Clear() end
            UI:RefreshDebug()
        end, true)
    frame.mainButton = createButton(frame, "Open Main", 110, 26,
        "RIGHT", frame.clearButton, "LEFT", -8, 0,
        function() UI:Open("Overview") end)

    frame.scroll = CreateFrame("ScrollFrame", "AzerothAnglerDebugScroll", frame, "UIPanelScrollFrameTemplate")
    frame.scroll:SetPoint("TOPLEFT",     frame, "TOPLEFT",     16, -114)
    frame.scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -36, 14)

    frame.output = CreateFrame("EditBox", nil, frame.scroll)
    frame.output:SetMultiLine(true)
    frame.output:SetAutoFocus(false)
    frame.output:SetFont(MONO_FONT, 11, "")
    frame.output:SetWidth(796)
    frame.output:SetTextInsets(6, 6, 6, 6)
    frame.output:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
        frame:Hide()
    end)
    frame.scroll:SetScrollChild(frame.output)

    frame:Hide()
    ns.state.debugFrame = frame
end

-- ═════════════════════════════════════════════════════════════════════
--  NAVIGATION
-- ═════════════════════════════════════════════════════════════════════
function UI:ShowTab(tabName)
    if not ns.state.frame then return end
    for name, panel in pairs(ns.state.frame.tabs) do
        panel:SetShown(name == tabName)
    end
    for name, button in pairs(ns.state.frame.tabButtons) do
        button.isActive = (name == tabName)
        Theme:StyleButton(button)
    end
    ns.state.activeTab = tabName
end

function UI:Open(tabName)
    if not ns.state.frame then return end
    ns.state.frame:Show()
    self:ShowTab(tabName or ns.state.activeTab or "Overview")
    self:RefreshAll()
end

function UI:ToggleMain()
    if not ns.state.frame then return end
    if ns.state.frame:IsShown() then
        ns.state.frame:Hide()
    else
        self:Open(ns.state.activeTab or "Overview")
    end
end

function UI:ToggleCompact()
    if not ns.state.compactFrame then return end
    ns.DB.profile.ui.compactMode = not ns.DB.profile.ui.compactMode
    ns.state.compactFrame:SetShown(ns.DB.profile.ui.compactMode)
    self:RefreshAll()
end

function UI:RefreshDebug()
    local frame = ns.state.debugFrame
    if not frame then return end
    local lines = (ns.Debug and ns.Debug.GetLines and ns.Debug:GetLines()) or {}
    local text  = (ns.Debug and ns.Debug.GetText  and ns.Debug:GetText())  or "No debug module available."
    frame.output:SetText(text)
    frame.output:SetCursorPosition(0)
    frame.summary:SetText(string.format("Entries: %d", #lines))
end

function UI:ToggleDebug()
    self:CreateDebugFrame()
    if ns.state.debugFrame:IsShown() then
        ns.state.debugFrame:Hide()
    else
        ns.state.debugFrame:Show()
        self:RefreshDebug()
    end
end

-- ═════════════════════════════════════════════════════════════════════
--  REFRESH FUNCTIONS
-- ═════════════════════════════════════════════════════════════════════

function UI:RefreshOverview()
    local panel = ns.state.frame and ns.state.frame.tabs.Overview
    if not panel then return end

    local current     = ns.DB.currentSession
    local zoneName    = current.zone or ns.Utils:GetZoneName()
    local zoneStats   = ns.Tracking:GetZoneStats(zoneName)
    local toolState   = ns.Fishing:GetFishingToolState()
    local lureState   = ns.Fishing:GetLureState()
    local zoneTop     = ns.Tracking:GetZoneTopItems(zoneName, 10)
    local preferredSet = ns.DB.profile.gear.preferredSet or ""
    local equippedSet  = ns.Fishing:GetEquippedSetName()
    local zoneShort    = ns.Utils:AbbrevText(zoneName, 24)
    local lureShort    = ns.Utils:AbbrevText(lureState.label, 22)

    panel.heroTitle:SetText(current.active and "Command the shoreline" or "Ready for the next cast")
    panel.heroSub:SetText(current.active
        and "Your session is live. Pace, lure state, gear, and zone memory are updating in real time."
        or  "Open water is quiet. Gear up, pick a shoreline, and let the watcher build a catch ledger.")
    panel.heroNote:SetText(string.format(
        "Set: %s   |   Tool: %s   |   Lure: %s",
        ns.Utils:AbbrevText(preferredSet ~= "" and preferredSet or "None", 18),
        getToolModeLabel(toolState),
        lureShort))

    setButtonText(panel.heroButtons.session, current.active and "End Session" or "Start Session")

    -- Stat cards
    panel.stats[1].value:SetText(ns.Utils:AbbrevText(zoneName, 16))
    panel.stats[1].sub:SetText(string.format("%d lifetime catches", zoneStats.catches or 0))

    current.fishingSkill = ns.Utils:GetFishingSkill() or 0
    panel.stats[2].value:SetText(tostring(current.fishingSkill))
    panel.stats[2].sub:SetText(ns.Utils:AbbrevText(getToolModeLabel(toolState), 22))

    panel.stats[3].value:SetText(ns.Tracking:GetCatchRate())
    panel.stats[3].sub:SetText("Casts: " .. ns.Tracking:GetCastRate())

    panel.stats[4].value:SetText(tostring(ns.Utils:CountTableEntries(current.items or {})))
    panel.stats[4].sub:SetText(string.format("%d rare • %d pool", current.rareCatches or 0, current.poolCatches or 0))

    panel.stats[5].value:SetText(ns.Utils:AbbrevText(lureState.label, 16))
    panel.stats[5].sub:SetText(lureState.active
        and ("Time: " .. ns.Utils:FormatDuration(lureState.remaining))
        or  "Apply lure for bonus skill")

    -- Live Deck
    local recentCatches = current.recentCatches or {}
    local activityContent = {
        string.format("Status: %s  |  Elapsed: %s",
            current.active and "|cFF4CD96AActive|r" or "Idle",
            ns.Utils:FormatDuration(ns.Session:GetElapsed())),
        string.format("Casts: %d  |  Catches: %d  (%s)",
            current.casts or 0, current.catches or 0,
            getSuccessRate(current.catches, current.casts)),
        string.format("Pace: %s  |  Rares: %d  |  Pool: %d",
            ns.Tracking:GetCatchRate(), current.rareCatches or 0, current.poolCatches or 0),
        "|cFFF0A020Recent Catches:|r",
    }
    for i = 1, 5 do
        local link = recentCatches[i]
        activityContent[#activityContent+1] = link
            and formatCatchDisplayWithMetrics(link, link, 38)
            or (i == 1 and "|cFF9BBFCA No catches this session.|r" or "")
    end
    setLines(panel.activityLines, activityContent)

    -- Fish Watcher
    panel.watcherSub:SetText(string.format(
        "%s — %d catches, %d rares",
        zoneName, zoneStats.catches or 0, zoneStats.rares or 0))
    local watcherLines = {}
    for i = 1, #panel.watcherLines do
        local row = zoneTop[i]
        if row then
            watcherLines[i] = string.format("%d. %s  ×%d  (%s)",
                i, ns.Utils:AbbrevText(row.display, 26), row.count,
                ns.Utils:FormatPercent(row.count, zoneStats.catches or 0))
        elseif i == 1 then
            watcherLines[i] = "|cFF9BBFCA No zone catches recorded yet.|r"
        else
            watcherLines[i] = ""
        end
    end
    setLines(panel.watcherLines, watcherLines)

    -- Header
    local hdr = ns.state.frame.header
    Theme:ApplyPill(hdr.statusPill, current.active and "good" or "warn")
    hdr.statusText:SetText(current.active and "Session Active" or "Harbor Ready")
    hdr.zone:SetText(string.format("Waters: %s  |  Last: %s  |  Pace: %s",
        zoneShort,
        formatCatchDisplayWithMetrics(current.lastCatch, current.lastCatch, 42),
        ns.Tracking:GetCatchRate()))
    hdr.statusMeta:SetText("Fish pace: " .. ns.Tracking:GetCatchRate())

    setButtonText(ns.state.frame.rail.sessionButton, current.active and "End Session" or "Start Session")
end

function UI:RefreshWatcher()
    local panel = ns.state.frame and ns.state.frame.tabs.Watcher
    if not panel then return end

    local zoneName  = ns.DB.currentSession.zone or ns.Utils:GetZoneName()
    local zoneStats = ns.Tracking:GetZoneStats(zoneName)
    local zoneTop   = ns.Tracking:GetZoneTopItems(zoneName, 11)
    local hotspots  = ns.Tracking:GetHotspotZones(7)
    local rares     = ns.Tracking:GetRecentRares(7)
    local c, u, r   = ns.Tracking:GetZoneQualityBreakdown(zoneName)

    panel.summaryMeta:SetText(string.format(
        "Waters: %s  |  Casts: %d  |  Catches: %d  |  Rares: %d  |  Quality C/U/R: %d/%d/%d",
        ns.Utils:AbbrevText(zoneName, 20),
        zoneStats.casts or 0, zoneStats.catches or 0, zoneStats.rares or 0,
        c or 0, u or 0, r or 0))

    local zoneLines = {}
    for i = 1, 11 do
        local row = zoneTop[i]
        if row then
            zoneLines[i] = string.format("%d. %s  ×%d  (%s of zone)",
                i, formatCatchDisplayWithMetrics(row.key, row.display, 38), row.count,
                ns.Utils:FormatPercent(row.count, zoneStats.catches or 0))
        elseif i == 1 then
            zoneLines[i] = "|cFF9BBFCA No zone catches recorded yet.|r"
        else
            zoneLines[i] = ""
        end
    end
    setLines(panel.zoneLines, zoneLines)

    local hotspotLines = {}
    for i = 1, 7 do
        local row = hotspots[i]
        if row then
            hotspotLines[i] = string.format("%d. %s  |  %d / %d",
                i, ns.Utils:AbbrevText(row.zone, 20), row.catches, row.casts)
        elseif i == 1 then
            hotspotLines[i] = "|cFF9BBFCA No hotspot data yet.|r"
        else
            hotspotLines[i] = ""
        end
    end
    setLines(panel.hotspotLines, hotspotLines)

    local rareLines = {}
    for i = 1, 7 do
        local row = rares[i]
        if row then
            rareLines[i] = string.format("%s  |  %s  |  %s",
                formatTimeStamp(row.at),
                formatCatchDisplayWithMetrics(row.itemLink, row.displayName or row.itemLink, 36),
                ns.Utils:AbbrevText(row.zone or "Unknown", 14))
        elseif i == 1 then
            rareLines[i] = "|cFF9BBFCA No rare catches stored yet.|r"
        else
            rareLines[i] = ""
        end
    end
    setLines(panel.rareLines, rareLines)
end

function UI:RefreshAtlas()
    local panel = ns.state.frame and ns.state.frame.tabs.Atlas
    if not panel or not ns.Atlas then return end

    local currentZone = ns.Utils:GetZoneName()
    local summary     = ns.Atlas:BuildZoneSummary(currentZone, ns.Atlas:GetCurrentMapID())
    local fishRates   = summary.fishRates    or {}
    local pools       = summary.pools        or {}
    local recipes     = summary.recipes      or {}
    local requirements= summary.requirements or {}
    local equipment   = summary.equipment    or {}
    local dataZone    = summary.usedFallbackZone
        and ((summary.zoneName or "Unknown") .. " (fallback)")
        or  (summary.zoneName or "No data")

    panel.heroMeta:SetText(string.format(
        "Source: %s  |  Current: %s  |  Data zone: %s",
        summary.source or "Embedded", currentZone, dataZone))

    local zoneLines = {
        "Current zone: "   .. ns.Utils:AbbrevText(currentZone, 36),
        "Data zone: "      .. ns.Utils:AbbrevText(dataZone, 36),
        "Zone ID: "        .. tostring(summary.zoneID  or "Unknown"),
        "Map ID: "         .. tostring(summary.mapID   or "Unknown"),
        "Target skill: "   .. tostring(summary.fishingLevel or "Unknown"),
        "|cFFF0A020Fish catch rates:|r",
    }
    for i = 1, 6 do
        local row = fishRates[i]
        if row then
            local rate = string.format("%.1f%%", (tonumber(row.catchChance) or 0) * 100)
            local price = ns.Atlas:FormatAuctionPrice(row.itemID)
            zoneLines[#zoneLines+1] = string.format("  %d. %s  %s  |  raw %s",
                i, ns.Utils:AbbrevText(row.name, 18), rate, price)
        elseif i == 1 then
            zoneLines[#zoneLines+1] = "  |cFF9BBFCA No fish-rate data for current zone.|r"
        else
            zoneLines[#zoneLines+1] = ""
        end
    end
    zoneLines[#zoneLines+1] = "|cFFF0A020Pool counts:|r"
    for i = 1, 2 do
        local pool = pools[i]
        zoneLines[#zoneLines+1] = pool
            and string.format("  %s ×%d", ns.Utils:AbbrevText(pool.name, 22), pool.count or 0)
            or (i == 1 and "  |cFF9BBFCA No pool data.|r" or "")
    end
    setLines(panel.zoneLines, zoneLines)

    local recipeLines = { "|cFFF0A020Primary fish recipes:|r" }
    for i = 1, 7 do
        local row = recipes[i]
        if row then
            local price = ns.Atlas:FormatAuctionPrice(row.productID)
            recipeLines[#recipeLines+1] = string.format("  %d. %s  |  %s",
                i, ns.Utils:AbbrevText(row.recipeName, 26), price)
        elseif i == 1 then
            recipeLines[#recipeLines+1] = "  |cFF9BBFCA No recipes for current zone.|r"
        else
            recipeLines[#recipeLines+1] = ""
        end
    end
    setLines(panel.recipeLines, recipeLines)

    local detailParts = { "|cFFF0A020Requirements:|r" }
    if #requirements == 0 then
        detailParts[#detailParts+1] = "  · No special restrictions."
    else
        for _, req in ipairs(requirements) do
            detailParts[#detailParts+1] = "  · " .. req
        end
    end
    detailParts[#detailParts+1] = " "
    detailParts[#detailParts+1] = "|cFFF0A020Equipment:|r"
    if #equipment == 0 then
        detailParts[#detailParts+1] = "  · No equipment entries."
    else
        for _, row in ipairs(equipment) do
            local cat  = row.category and ("[" .. ns.Utils:AbbrevText(row.category, 8) .. "] ") or ""
            local desc = row.desc     and ns.Utils:AbbrevText(row.desc, 52)                      or ""
            detailParts[#detailParts+1] = "  " .. cat .. desc
        end
    end
    if panel.detailText then
        panel.detailText:SetText(table.concat(detailParts, "\n"))
        local h = panel.detailText:GetStringHeight()
        panel.detailScrollChild:SetHeight(math.max(h + 16, 20))
    end
end

function UI:RefreshEquipment()
    local panel = ns.state.frame and ns.state.frame.tabs.Equipment
    if not panel then return end

    local preferredSet = ns.DB.profile.gear.preferredSet or ""
    local equippedSet  = ns.Fishing:GetEquippedSetName()
    local toolState    = ns.Fishing:GetFishingToolState()
    local lureState    = ns.Fishing:GetLureState()
    local sets         = ns.Fishing:GetEquipmentSets()

    setLines(panel.leftLines, {
        "Fishing tool: "     .. ns.Utils:AbbrevText(toolState.toolLabel or ns.Fishing:GetPoleName(), 44),
        "Tool mode: "        .. getToolModeLabel(toolState),
        "Fishing skill: "    .. tostring(ns.DB.currentSession.fishingSkill or 0),
        "Lure: "             .. ns.Utils:AbbrevText(lureState.label, 44),
        "Lure active: "      .. (lureState.active and "|cFF4CD96AYes|r" or "|cFFE05C3ANo|r")
                             .. (lureState.active and ("  Remaining: " .. ns.Utils:FormatDuration(lureState.remaining)) or ""),
        "Preferred set: "    .. ns.Utils:AbbrevText(preferredSet ~= "" and preferredSet or "None", 36),
        "Equipped set: "     .. ns.Utils:AbbrevText(equippedSet  ~= "" and equippedSet  or "None", 36),
        "Return set: "       .. ns.Utils:AbbrevText((ns.DB.profile.gear.returnSet or "") ~= "" and ns.DB.profile.gear.returnSet or "None", 36),
        string.format("Sets found: %d  (Prev/Next to cycle, Equip to apply)", #sets),
    })

    local setLineText = {}
    for i = 1, #panel.setLines do
        local entry = sets[i]
        if entry then
            local marker = "    "
            if entry.name == equippedSet and entry.name == preferredSet then
                marker = "|cFFF0A020[E/P]|r "
            elseif entry.name == equippedSet then
                marker = "|cFF4CD96A[E] |r "
            elseif entry.name == preferredSet then
                marker = "|cFFF0A020[P] |r "
            end
            setLineText[i] = marker .. ns.Utils:AbbrevText(entry.name, 48)
        elseif i == 1 then
            setLineText[i] = "|cFF9BBFCA No equipment sets found in Equipment Manager.|r"
        else
            setLineText[i] = ""
        end
    end
    setLines(panel.setLines, setLineText)
end

function UI:RefreshTackle()
    local panel = ns.state.frame and ns.state.frame.tabs.Tackle
    if not panel then return end

    local current      = ns.DB.currentSession
    local toolState    = ns.Fishing:GetFishingToolState()
    local lureState    = ns.Fishing:GetLureState()
    local lures        = ns.Fishing:GetAvailableLures(12)
    local selectedID   = ns.Fishing:GetPreferredLureItemID()
    local bestLure     = ns.Fishing:GetBestAvailableLure()
    local sleepState   = ns.DB.profile.interaction.sleepingMode and "|cFFE05C3ASleeping|r" or "|cFF4CD96AAwake|r"

    local respawnLine = "Pool respawn: no pool cast yet"
    if ns.state.lastPoolCastAt and ns.state.lastPoolCastAt > 0 then
        local elapsed   = math.max(0, (GetTime and GetTime() or 0) - ns.state.lastPoolCastAt)
        local remaining = math.max(0, 60 - math.floor(elapsed))
        respawnLine = remaining > 0
            and ("Pool respawn: ~" .. tostring(remaining) .. "s")
            or  "Pool respawn: |cFF4CD96Aready|r"
    end

    local autoLure = ns.DB.profile.alerts.autoAttachLure
    setLines(panel.leftLines, {
        "Fishing tool: "   .. ns.Utils:AbbrevText(toolState.toolLabel or ns.Fishing:GetPoleName(), 42),
        "Tool mode: "      .. getToolModeLabel(toolState),
        "Skill: "          .. tostring(current.fishingSkill or 0),
        "Lure status: "    .. ns.Utils:AbbrevText(lureState.label, 36),
        "Lure active: "    .. (lureState.active and "|cFF4CD96AYes|r" or "|cFFE05C3ANo|r")
                           .. (lureState.active and ("  " .. ns.Utils:FormatDuration(lureState.remaining)) or ""),
        "Selected lure: "  .. (bestLure and (ns.Utils:AbbrevText(bestLure.name, 22) .. " (+" .. tostring(bestLure.bonus or 0) .. ")") or "None"),
        "Source: "         .. (bestLure and ns.Utils:AbbrevText(bestLure.source or "Unknown", 30) or "--"),
        "Auto-attach: "    .. (autoLure and "|cFF4CD96AEnabled|r" or "Disabled"),
        "Sleep mode: "     .. sleepState,
        "",
        respawnLine,
    })

    local lureLines = {}
    for i = 1, #panel.lureLines do
        local row = lures[i]
        if row then
            local marker = (tonumber(row.itemID) == tonumber(selectedID)) and "|cFFF0A020[S]|r " or "    "
            lureLines[i] = string.format("%s%s ×%d  +%d  |  %s",
                marker, ns.Utils:AbbrevText(row.name, 16), row.count or 0, row.bonus or 0,
                ns.Utils:AbbrevText(row.source or "Unknown", 14))
        elseif i == 1 then
            lureLines[i] = "|cFF9BBFCA No known lure consumables found in bags.|r"
        else
            lureLines[i] = ""
        end
    end
    setLines(panel.lureLines, lureLines)
end

function UI:RefreshSession()
    local panel = ns.state.frame and ns.state.frame.tabs.Session
    if not panel then return end

    local current       = ns.DB.currentSession
    local sessionTop    = ns.Tracking:GetSessionTopItems(10)
    local recentCatches = current.recentCatches or {}

    setLines(panel.summaryLines, {
        "Status: "     .. (current.active and "|cFF4CD96AActive|r" or "Idle"),
        "Zone: "       .. ns.Utils:AbbrevText(current.zone or "-", 28),
        "Elapsed: "    .. ns.Utils:FormatDuration(ns.Session:GetElapsed()),
        "Skill: "      .. tostring(current.fishingSkill or 0),
        "Casts: "      .. tostring(current.casts   or 0),
        "Catches: "    .. tostring(current.catches or 0),
        "Success: "    .. getSuccessRate(current.catches, current.casts),
        "Pool: "       .. tostring(current.poolCatches      or 0)
                       .. "  |  Open water: " .. tostring(current.openWaterCatches or 0),
        "Unique: "     .. tostring(ns.Utils:CountTableEntries(current.items or {})),
        "Rares: "      .. tostring(current.rareCatches or 0),
        "Pace: "       .. ns.Tracking:GetCatchRate(),
    })

    local catchLines = {}
    for i = 1, 11 do
        if i <= 6 then
            local row = sessionTop[i]
            if row then
                catchLines[i] = string.format("%d. %s  ×%d", i, formatCatchDisplayWithMetrics(row.key, row.display, 38), row.count)
            elseif i == 1 then
                catchLines[i] = "|cFF9BBFCA No catches yet.|r"
            else
                catchLines[i] = ""
            end
        elseif i == 7 then
            catchLines[i] = #recentCatches > 0 and "|cFFF0A020── Recent ──|r" or ""
        else
            local idx    = i - 7
            local recent = recentCatches[idx]
            if recent then
                catchLines[i] = string.format("R%d. %s", idx, formatCatchDisplayWithMetrics(recent, recent, 38))
            else
                catchLines[i] = ""
            end
        end
    end
    setLines(panel.catchLines, catchLines)

    local historyLines = {}
    for i = 1, 11 do
        local entry = ns.DB.history.sessions[i]
        if entry then
            historyLines[i] = string.format("%s  |  %d/%d  |  %s  |  %d★",
                ns.Utils:AbbrevText(entry.zone or "Unknown", 14),
                entry.catches or 0, entry.casts or 0,
                entry.fishPerHour or "0/hr",
                entry.rareCatches or 0)
        elseif i == 1 then
            historyLines[i] = "|cFF9BBFCA No saved sessions yet.|r"
        else
            historyLines[i] = ""
        end
    end
    setLines(panel.historyLines, historyLines)
end

function UI:RefreshDatabase()
    local panel = ns.state.frame and ns.state.frame.tabs.Database
    if not panel then return end

    local query = panel.searchBox and panel.searchBox:GetText() or ""
    query = string.lower(ns.Utils:CleanDisplayText(query or ""))

    local rows = {}
    for key, row in pairs((ns.FishDB and ns.FishDB.byName) or {}) do
        local name = string.lower(ns.Utils:CleanDisplayText(row and row.name or key or ""))
        if query == "" or name:find(query, 1, true) then
            rows[#rows + 1] = row
        end
    end
    local sortKey = panel.dbSortKey or "weightLb"
    local sortDesc = panel.dbSortDesc ~= false
    table.sort(rows, function(a, b)
        local an = ns.Utils:CleanDisplayText(a and a.name or "")
        local bn = ns.Utils:CleanDisplayText(b and b.name or "")

        local av, bv
        if sortKey == "weightLb" then
            av = tonumber(a and a.weightLb) or 0
            bv = tonumber(b and b.weightLb) or 0
        elseif sortKey == "sizeIn" then
            av = tonumber(a and a.sizeIn) or 0
            bv = tonumber(b and b.sizeIn) or 0
        else
            av = an
            bv = bn
        end

        if av == bv then return an < bn end
        if sortDesc then
            return av > bv
        end
        return av < bv
    end)

    if panel.sortWeightBtn then
        local wText = "Weight"
        local lText = "Length"
        local nText = "Name"
        if sortKey == "weightLb" then
            wText = sortDesc and "Weight v" or "Weight ^"
        elseif sortKey == "sizeIn" then
            lText = sortDesc and "Length v" or "Length ^"
        else
            nText = sortDesc and "Name v" or "Name ^"
        end
        setButtonText(panel.sortWeightBtn, wText)
        setButtonText(panel.sortLengthBtn, lText)
        setButtonText(panel.sortNameBtn, nText)
    end

    if panel.heroMeta then
        panel.heroMeta:SetText(string.format("Matches: %d  |  Database: %d fish  |  Sort: %s %s",
            #rows,
            ns.Utils:CountTableEntries((ns.FishDB and ns.FishDB.byName) or {}),
            (sortKey == "weightLb" and "Weight") or (sortKey == "sizeIn" and "Length") or "Name",
            sortDesc and "desc" or "asc"))
    end

    local selected = nil
    local selectedItemID = panel.dbSelectedItemID
    local selectedName = panel.dbSelectedName

    if selectedItemID or selectedName then
        for i = 1, #rows do
            local row = rows[i]
            local rowItemID = tonumber(row and row.itemID) or 0
            local rowName = string.lower(ns.Utils:CleanDisplayText(row and row.name or ""))
            if (selectedItemID and rowItemID == selectedItemID) or (selectedName and rowName == selectedName) then
                selected = row
                break
            end
        end
    end
    if not selected then
        selected = rows[1]
        panel.dbSelectedItemID = tonumber(selected and selected.itemID) or nil
        panel.dbSelectedName = selected and string.lower(ns.Utils:CleanDisplayText(selected.name or "")) or nil
    end

    local rowOffset = 0
    if panel.resultScroll and FauxScrollFrame_GetOffset then
        rowOffset = FauxScrollFrame_GetOffset(panel.resultScroll) or 0
    end

    for i = 1, #panel.resultRows do
        local rowBtn = panel.resultRows[i]
        local rowIndex = i + rowOffset
        local row = rows[rowIndex]
        if row then
            local rowName = string.lower(ns.Utils:CleanDisplayText(row.name or ""))
            local rowItemID = tonumber(row.itemID) or 0
            local isSelected = selected and (
                (selected.itemID and tonumber(selected.itemID) == rowItemID) or
                string.lower(ns.Utils:CleanDisplayText(selected.name or "")) == rowName)

            rowBtn.rowData = row
            rowBtn.index:SetText(string.format("%d.", rowIndex))
            rowBtn.weight:SetText(string.format("%.1f lb", tonumber(row.weightLb) or 0))
            rowBtn.length:SetText(string.format("%.1f in", tonumber(row.sizeIn) or 0))
            rowBtn.name:SetText(ns.Utils:AbbrevText(row.name or "Unknown", 30))
            rowBtn.isSelected = isSelected and true or false

            if rowBtn.isSelected then
                rowBtn.bg:SetColorTexture(0.114, 0.302, 0.322, 0.58)
                rowBtn.name:SetTextColor(unpack(Theme.colors.lanternSoft))
            else
                rowBtn.bg:SetColorTexture(0, 0, 0, 0.18)
                rowBtn.name:SetTextColor(unpack(Theme.colors.bone))
            end
            rowBtn:Show()
        elseif i == 1 and #rows == 0 and rowOffset == 0 then
            rowBtn.rowData = nil
            rowBtn.index:SetText("")
            rowBtn.weight:SetText("")
            rowBtn.length:SetText("")
            rowBtn.name:SetText("|cFF9BBFCA No fish match current search.|r")
            rowBtn.name:SetTextColor(unpack(Theme.colors.soft))
            rowBtn.isSelected = false
            rowBtn.bg:SetColorTexture(0, 0, 0, 0.18)
            rowBtn:Show()
        else
            rowBtn.rowData = nil
            rowBtn.index:SetText("")
            rowBtn.weight:SetText("")
            rowBtn.length:SetText("")
            rowBtn.name:SetText("")
            rowBtn.name:SetTextColor(unpack(Theme.colors.bone))
            rowBtn.isSelected = false
            rowBtn.bg:SetColorTexture(0, 0, 0, 0.18)
            rowBtn:Hide()
        end
    end

    if panel.resultScroll and FauxScrollFrame_Update then
        FauxScrollFrame_Update(panel.resultScroll, #rows, #panel.resultRows, panel.rowHeight)
    end

    local detailText = {}
    if selected then
        local minW, maxW, minL, maxL
        if ns.FishMetrics and ns.FishMetrics.GetRangesForRow then
            minW, maxW, minL, maxL = ns.FishMetrics:GetRangesForRow(selected)
        end

        local weightLine = string.format("Weight: %.1f lb", tonumber(selected.weightLb) or 0)
        local lengthLine = string.format("Length: %.1f in", tonumber(selected.sizeIn) or 0)
        if minW and maxW then
            weightLine = string.format("%s  (Range %.1f-%.1f lb)", weightLine, minW, maxW)
        end
        if minL and maxL then
            lengthLine = string.format("%s  (Range %.1f-%.1f in)", lengthLine, minL, maxL)
        end

        detailText = {
            weightLine,
            lengthLine,
            "Name: " .. tostring(selected.name or "-"),
            "ItemID: " .. tostring(selected.itemID or "-"),
            "Quality: " .. tostring(selected.quality or "-"),
            "Location: " .. tostring(selected.location or "-"),
            "Expansion: " .. tostring(selected.expansion or "-"),
            "Bait: " .. tostring(selected.bait or "-"),
            "Weather: " .. tostring(selected.weather or "-"),
            "Type: " .. tostring(selected.fishType or "-"),
            "Value: " .. tostring(selected.gold or "-") .. "g",
            "Drop: " .. tostring(selected.dropRate or "-"),
            "WoWhead: " .. ns.Utils:AbbrevText(selected.wowhead or "-", 52),
            "Image Ref: " .. ns.Utils:AbbrevText(selected.imageURL or "-", 49),
        }

        local icon = nil
        if selected.itemID and GetItemInfo then
            icon = select(10, GetItemInfo(selected.itemID))
        end
        if icon then
            panel.detailIcon:SetTexture(icon)
            panel.detailIcon:Show()
        else
            panel.detailIcon:Hide()
        end
    else
        detailText = {
            "Weight: -",
            "Length: -",
            "Name: -",
            "ItemID: -",
            "Quality: -",
            "Location: -",
            "Expansion: -",
            "Bait: -",
            "Weather: -",
            "Type: -",
            "Value: -",
            "Drop: -",
            "WoWhead: -",
            "Image Ref: -",
        }
        panel.detailIcon:Hide()
    end
    setLines(panel.detailLines, detailText)
end

function UI:RefreshSettings()
    local panel = ns.state.frame and ns.state.frame.tabs.Settings
    if not panel or not panel.rows then return end
    for _, row in pairs(panel.rows) do
        row.check:SetChecked(row.getter())
    end
end

function UI:RefreshCompact()
    local frame = ns.state.compactFrame
    if not frame then return end

    local current   = ns.DB.currentSession
    local lureState = ns.Fishing:GetLureState()
    Theme:ApplyPill(frame.statusPill, current.active and "good" or "warn")
    frame.statusText:SetText(current.active and "Fishing" or "Idle")
    frame.casts:SetText("|cFFF0A020Casts:|r "   .. tostring(current.casts   or 0))
    frame.catches:SetText("|cFFF0A020Catches:|r " .. tostring(current.catches or 0)
        .. "  (" .. getSuccessRate(current.catches, current.casts) .. ")")
    frame.rate:SetText("Pace: " .. ns.Tracking:GetCatchRate() .. "  |  " .. formatCompactMetricLine(current.lastCatch))
    frame.lure:SetText("Lure: " .. ns.Utils:AbbrevText(lureState.label, 30))
    frame.zone:SetText("Waters: " .. ns.Utils:AbbrevText(current.zone or "-", 24))
    frame.lastCatch:SetText("Last: " .. formatCatchDisplayWithMetrics(current.lastCatch, current.lastCatch, 42))

    local recent = current.recentCatches or {}
    for i = 1, 5 do
        local text = ""
        if recent[i] then
            text = string.format("%d. %s", i, formatCatchDisplayWithMetrics(recent[i], recent[i], 42))
        elseif i == 1 then
            text = "No recent catches."
        end
        frame.recentCatchLines[i]:SetText(text)
    end
end

function UI:RefreshFooter()
    local footer = ns.state.frame and ns.state.frame.footer
    if not footer then return end

    local current   = ns.DB.currentSession
    local lureState = ns.Fishing:GetLureState()
    footer.zone:SetText("Waters: "     .. ns.Utils:AbbrevText(current.zone or "-", 16))
    footer.skill:SetText("Skill: "     .. tostring(current.fishingSkill or 0))
    footer.rate:SetText("Pace: "       .. ns.Tracking:GetCatchRate())
    footer.lure:SetText("Lure: "       .. ns.Utils:AbbrevText(lureState.label, 22))
    footer.lastCatch:SetText("Last: "  .. ns.Utils:AbbrevText(current.lastCatch or "None", 36))
end

function UI:RefreshAll()
    if not ns.DB then return end
    if ns.state.frame then
        self:RefreshOverview()
        self:RefreshWatcher()
        self:RefreshAtlas()
        self:RefreshEquipment()
        self:RefreshTackle()
        self:RefreshSession()
        self:RefreshDatabase()
        self:RefreshSettings()
        self:RefreshFooter()
    end
    if ns.state.compactFrame then
        self:RefreshCompact()
    end
    if ns.state.debugFrame and ns.state.debugFrame:IsShown() then
        self:RefreshDebug()
    end
    if ns.Minimap and ns.Minimap.RefreshButton      then ns.Minimap:RefreshButton()  end
end
