--[[
	Cerebro - UI Sandbox
	Visual development framework for testing UI components
	
	Usage: /cb testui
	
	This module provides a tabbed showcase of all reusable UI patterns.
	Use this for rapid iteration during UI development.
]]--

local AddonName, PrivateTable = ...
local Cerebro = LibStub("AceAddon-3.0"):GetAddon(AddonName)
local AceGUI = LibStub("AceGUI-3.0")
local UISandbox = Cerebro:NewModule("UISandbox", "AceEvent-3.0")

-- Register module reference
Cerebro.modules.UISandbox = UISandbox

-- Frame state
local sandboxFrame = nil

function UISandbox:OnInitialize()
	self.nativeFrames = {}        -- Structural frames (persist across tabs)
	self.contentNativeFrames = {} -- Content frames (cleared on tab switch)
	self.currentTab = "tiles"
end

function UISandbox:OnEnable()
	-- No events needed for sandbox
end

--[[=============================================
	Session Confirmation Popups
===============================================]]

-- Store references for tooltip updates (set during sidebar creation)
UISandbox.sessionTooltipText = nil
UISandbox.sessionTooltip = nil

StaticPopupDialogs["CEREBRO_CONFIRM_START_SESSION"] = {
	text = "Start a new raid session?\n\n|cff888888This will begin tracking attendance and enable EP/GP awards.|r",
	button1 = "Start Raid",
	button2 = "Cancel",
	OnAccept = function()
		Cerebro.modules.Session:StartSession()
		Cerebro:Print("|cff00ff00Raid session started.|r")
		-- Update tooltip
		if UISandbox.sessionTooltipText then
			UISandbox.sessionTooltipText:SetText("|cffFFD700End Raid|r")
			if UISandbox.sessionTooltip then
				UISandbox.sessionTooltip:SetWidth(UISandbox.sessionTooltipText:GetStringWidth() + 20)
			end
		end
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 3,
}

StaticPopupDialogs["CEREBRO_CONFIRM_END_SESSION"] = {
	text = "End the current raid session?\n\n|cff888888This will finalize attendance and stop EP/GP awards.|r",
	button1 = "End Raid",
	button2 = "Cancel",
	OnAccept = function()
		local session = Cerebro.modules.Session and Cerebro.modules.Session:GetCurrentSession() -- luacheck: ignore
		Cerebro.modules.Session:EndSession()
		Cerebro:Print("|cffff8800Raid session ended.|r")
		-- Update tooltip
		if UISandbox.sessionTooltipText then
			UISandbox.sessionTooltipText:SetText("|cffFFD700Start Raid|r")
			if UISandbox.sessionTooltip then
				UISandbox.sessionTooltip:SetWidth(UISandbox.sessionTooltipText:GetStringWidth() + 20)
			end
		end
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 3,
}

--[[=============================================
	Standings Context Menu - EP/GP Adjustment Popups
===============================================]]

-- Store the target player for adjustment dialogs
UISandbox.adjustmentTarget = nil
UISandbox.adjustmentOperation = nil  -- "add_ep", "sub_ep", "add_gp", "sub_gp"

StaticPopupDialogs["CEREBRO_ADJUST_EP_GP"] = {
	text = "%s",  -- Will be formatted with player name and operation
	button1 = "Apply",
	button2 = "Cancel",
	hasEditBox = true,
	editBoxWidth = 100,
	OnShow = function(self)
		self.EditBox:SetText("")
		self.EditBox:SetFocus()
		self.EditBox:SetNumeric(true)
	end,
	OnAccept = function(self)
		local amount = tonumber(self.EditBox:GetText())
		local target = UISandbox.adjustmentTarget
		local op = UISandbox.adjustmentOperation
		
		if not amount or amount <= 0 then
			Cerebro:Print("|cffff0000Invalid amount.|r")
			return
		end
		
		if not target or not op then
			Cerebro:Print("|cffff0000No target player selected.|r")
			return
		end
		
		local epgp = Cerebro:GetModule("EPGP")
		if not epgp then
			Cerebro:Print("|cffff0000EPGP module not loaded.|r")
			return
		end
		
		local reason = "Manual Adjust (UI)"
		if op == "add_ep" then
			epgp:AwardEP(target, amount, "OFFICER_ADJUST", reason)
			Cerebro:Print(string.format("|cff00ff00Added %d EP to %s|r", amount, Cerebro:ColorPlayerName(target)))
		elseif op == "sub_ep" then
			epgp:AwardEP(target, -amount, "OFFICER_ADJUST", reason)
			Cerebro:Print(string.format("|cffff8800Subtracted %d EP from %s|r", amount, Cerebro:ColorPlayerName(target)))
		elseif op == "add_gp" then
			epgp:ChargeGP(target, amount, "OFFICER_ADJUST", reason)
			Cerebro:Print(string.format("|cff00ff00Added %d GP to %s|r", amount, Cerebro:ColorPlayerName(target)))
		elseif op == "sub_gp" then
			epgp:ChargeGP(target, -amount, "OFFICER_ADJUST", reason)
			Cerebro:Print(string.format("|cffff8800Subtracted %d GP from %s|r", amount, Cerebro:ColorPlayerName(target)))
		end
		
		pcall(function()
			Cerebro.Ledger:AddAuditLog("MANUAL_ADJUST", string.format("%s %d to %s (%s)", op, amount, target, reason))
		end)
		
		-- Refresh standings
		if UISandbox.RefreshStandingsRows then
			UISandbox:RefreshStandingsRows()
		end
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 3,
}

-- Purge player confirmation popup (type-to-confirm, matches StandingsFrame)
StaticPopupDialogs["CEREBRO_SANDBOX_CONFIRM_PURGE"] = {
	text = "Confirm action on this player?",
	button1 = "Confirm",
	button2 = "Cancel",
	hasEditBox = true,
	editBoxWidth = 160,
	OnShow = function(dialog)
		local action = UISandbox._pendingPurge
		-- Modern WoW uses dialog.Buttons[1] instead of dialog.button1
		local confirmBtn = (dialog.Buttons and dialog.Buttons[1]) or dialog.button1
		if action and action.action == "block" then
			dialog.Text:SetText("Are you sure you want to |cffff0000BLOCK|r " .. (action.player or "this player") .. "?\n\nThis permanently removes them and blocks re-sync.\nUse /cb unblock to reverse.")
			dialog.EditBox:Hide()
			if confirmBtn then confirmBtn:Enable() end
		else
			dialog.Text:SetText("Are you sure you want to |cffff8800PURGE|r " .. (action and action.player or "this player") .. "?\n\nThis |cffff4444permanently deletes|r all EP/GP/attendance data.\nType |cffff8800PURGE|r to confirm:")
			dialog.EditBox:SetText("")
			dialog.EditBox:Show()
			dialog.EditBox:SetFocus()
			if confirmBtn then confirmBtn:Disable() end
		end
	end,
	EditBoxOnTextChanged = function(editBox)
		local parent = editBox:GetParent()
		local action = UISandbox._pendingPurge
		local confirmBtn = (parent.Buttons and parent.Buttons[1]) or parent.button1
		if action and action.action ~= "block" and confirmBtn then
			if editBox:GetText():upper() == "PURGE" then
				confirmBtn:Enable()
			else
				confirmBtn:Disable()
			end
		end
	end,
	OnAccept = function()
		local action = UISandbox._pendingPurge
		UISandbox._pendingPurge = nil
		if not action then return end
		local fullName = action.player

		if action.action == "block" then
			Cerebro:BlockPlayer(fullName)
		else
			Cerebro:PurgePlayer(fullName)
		end
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 4,
}

-- Context menu dropdown frame (reused)
local standingsContextMenu = nil
-- Invisible anchor frame for cursor positioning
local cursorAnchor = nil
-- Pending purge action
UISandbox._pendingPurge = nil

function UISandbox:ShowStandingsContextMenu(anchor, playerName, playerData)
	if not Cerebro:IsOfficer() then return end
	
	-- Create dropdown frame if needed
	if not standingsContextMenu then
		standingsContextMenu = CreateFrame("Frame", "CerebroStandingsContextMenu", UIParent, "UIDropDownMenuTemplate")
	end
	
	-- Create invisible cursor anchor frame (always parented to UIParent)
	if not cursorAnchor then
		cursorAnchor = CreateFrame("Frame", nil, UIParent)
		cursorAnchor:SetSize(1, 1)
	end
	
	-- Position anchor at current cursor location
	local cursorX, cursorY = GetCursorPosition()
	local scale = UIParent:GetEffectiveScale()
	cursorAnchor:ClearAllPoints()
	cursorAnchor:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cursorX / scale, cursorY / scale)
	
	local status = playerData.status or "Core"
	
	local menuList = {
		{ text = "|cffFFD700" .. playerName .. "|r", isTitle = true, notCheckable = true },
		{ text = "", isTitle = true, notCheckable = true },  -- Spacer
	}
	
	-- Admit to Core (only for Pending players)
	if status == "Pending" then
		table.insert(menuList, {
			text = "Admit to Core",
			notCheckable = true,
			func = function()
				local data = Cerebro:GetPlayerData(playerName)
				data.status = "Core"
				data.coreAdmittedAt = time()
				data.lastUpdate = time()
				
				-- Try to get class from guild roster if missing
				if not data.class then
					for i = 1, GetNumGuildMembers() do
						local gName, _, _, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(i)
						if gName and Cerebro:GetFullPlayerName(gName) == playerName then
							data.class = classFileName
							break
						end
					end
				end
				
				Cerebro:Print(string.format("Admitted %s to Core.", Cerebro:ColorPlayerName(playerName)))
				Cerebro.Ledger:AddAuditLog("ADMIT_PLAYER", "Promoted " .. playerName .. " to Core status.")
				
				-- Broadcast update via V2 (full payload)
				if Cerebro.modules.CommunicationV2 and Cerebro.modules.CommunicationV2.BroadcastPlayerUpdate then
					Cerebro.modules.CommunicationV2:BroadcastPlayerUpdate(playerName, {
						status = "Core",
						class = data.class,
						coreAdmittedAt = data.coreAdmittedAt,
						lastUpdate = data.lastUpdate
					})
				end
				
				UISandbox:RefreshStandingsRows()
			end,
		})
		table.insert(menuList, { text = "", isTitle = true, notCheckable = true })  -- Spacer
	end
	
	-- EP adjustments
	table.insert(menuList, {
		text = "|cff00ff00Add EP|r",
		notCheckable = true,
		func = function()
			UISandbox.adjustmentTarget = playerName
			UISandbox.adjustmentOperation = "add_ep"
			StaticPopup_Show("CEREBRO_ADJUST_EP_GP", string.format("Add EP to %s\n\nEnter amount:", Cerebro:ColorPlayerName(playerName)))
		end,
	})
	table.insert(menuList, {
		text = "|cffff8800Subtract EP|r",
		notCheckable = true,
		func = function()
			UISandbox.adjustmentTarget = playerName
			UISandbox.adjustmentOperation = "sub_ep"
			StaticPopup_Show("CEREBRO_ADJUST_EP_GP", string.format("Subtract EP from %s\n\nEnter amount:", Cerebro:ColorPlayerName(playerName)))
		end,
	})
	
	table.insert(menuList, { text = "", isTitle = true, notCheckable = true })  -- Spacer
	
	-- GP adjustments
	table.insert(menuList, {
		text = "|cff00ff00Add GP|r",
		notCheckable = true,
		func = function()
			UISandbox.adjustmentTarget = playerName
			UISandbox.adjustmentOperation = "add_gp"
			StaticPopup_Show("CEREBRO_ADJUST_EP_GP", string.format("Add GP to %s\n\nEnter amount:", Cerebro:ColorPlayerName(playerName)))
		end,
	})
	table.insert(menuList, {
		text = "|cffff8800Subtract GP|r",
		notCheckable = true,
		func = function()
			UISandbox.adjustmentTarget = playerName
			UISandbox.adjustmentOperation = "sub_gp"
			StaticPopup_Show("CEREBRO_ADJUST_EP_GP", string.format("Subtract GP from %s\n\nEnter amount:", Cerebro:ColorPlayerName(playerName)))
		end,
	})
	
	table.insert(menuList, { text = "", isTitle = true, notCheckable = true })  -- Spacer
	
	-- Purge Player (reset EP/GP) — locked during active sessions
	local sessionActive = Cerebro.modules.Session and Cerebro.modules.Session:IsActive()
	if sessionActive then
		table.insert(menuList, {
			text = "|cff666666Purge Player (locked during session)|r",
			notCheckable = true,
			disabled = true,
		})
	else
		table.insert(menuList, {
			text = "|cffff8800Purge Player (reset)|r",
			notCheckable = true,
			func = function()
				UISandbox._pendingPurge = { player = playerName, action = "purge" }
				StaticPopup_Show("CEREBRO_SANDBOX_CONFIRM_PURGE")
			end,
		})
	end
	
	-- Block Player (permanent sync block)
	table.insert(menuList, {
		text = "|cffff0000Block Player (permanent)|r",
		notCheckable = true,
		func = function()
			UISandbox._pendingPurge = { player = playerName, action = "block" }
			StaticPopup_Show("CEREBRO_SANDBOX_CONFIRM_PURGE")
		end,
	})
	
	table.insert(menuList, { text = "", isTitle = true, notCheckable = true })  -- Spacer
	table.insert(menuList, { text = "Cancel", notCheckable = true })
	
	-- Use UIDropDownMenu pattern (EasyMenu may not be available)
	UIDropDownMenu_Initialize(standingsContextMenu, function(self, level)
		for _, item in ipairs(menuList) do
			local info = UIDropDownMenu_CreateInfo()
			info.text = item.text
			info.isTitle = item.isTitle
			info.notCheckable = item.notCheckable or true
			info.func = item.func
			UIDropDownMenu_AddButton(info, level)
		end
	end, "MENU")
	
	-- Capture cursor position before toggle (WoW may change it during toggle)
	local cx, cy = GetCursorPosition()
	
	-- Use invisible anchor frame positioned at cursor
	ToggleDropDownMenu(1, nil, standingsContextMenu, cursorAnchor, 0, 0)
	
	-- Force reposition after WoW's dropdown positioning (override it with delay)
	-- Use C_Timer to run after WoW's internal positioning logic completes
	C_Timer.After(0.01, function()
		local dropdownList = _G["DropDownList1"]
		if dropdownList and dropdownList:IsShown() then
			-- Use the dropdown's own effective scale for accurate positioning
			local scale = dropdownList:GetEffectiveScale()
			dropdownList:ClearAllPoints()
			dropdownList:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / scale, cy / scale)
		end
	end)
end

--[[=============================================
	Toggle / Show / Hide
===============================================]]

function UISandbox:Toggle()
	if sandboxFrame and sandboxFrame:IsShown() then
		self:Hide()
	else
		self:Show()
	end
end

function UISandbox:Show()
	-- Always create a fresh frame to avoid stale state issues
	-- First, clean up any existing frame
	if sandboxFrame then
		-- Remove OnHide temporarily to prevent recursive/double cleanup
		sandboxFrame:SetScript("OnHide", nil)
		
		-- Clean up AceGUI content
		if self.contentFrame then
			AceGUI:Release(self.contentFrame)
			self.contentFrame = nil
		end
		
		-- Clean up native frames (will hide sandboxFrame as part of cleanup)
		self:CleanupNativeFrames()
		sandboxFrame = nil
		self.sidebarButtons = {}
	end
	
	self:CreateFrame()
	
	-- Start sync status refresh timer (30s = V2 heartbeat cadence)
	self:StartSyncStatusTimer()
end

function UISandbox:Hide()
	-- Cancel sync status timer
	self:StopSyncStatusTimer()
	
	if sandboxFrame then
		-- Release AceGUI content frame
		if self.contentFrame then
			AceGUI:Release(self.contentFrame)
			self.contentFrame = nil
		end
		-- Cleanup all tracked native frames (includes sandboxFrame itself)
		self:CleanupNativeFrames()
		sandboxFrame = nil
		self.sidebarButtons = {}
	end
end

--[[=============================================
	Frame Creation
===============================================]]

function UISandbox:CreateFrame()
	-- Cleanup any existing
	self:CleanupNativeFrames()
	
	-- ================================
	-- PREMIUM FRAME SIZING (25% smaller than original 750x550)
	-- ================================
	local FRAME_WIDTH = 602
	local FRAME_HEIGHT = 452
	local SIDEBAR_WIDTH = 50
	local ICON_SIZE = 36
	local ICON_SPACING = 6
	
	-- Create main frame (basic container)
	local frame = self:TrackFrame(CreateFrame("Frame", "CerebroUISandboxFrame", UIParent, "BackdropTemplate"))
	frame:SetSize(FRAME_WIDTH, FRAME_HEIGHT)
	frame:SetPoint("CENTER")
	frame:SetBackdrop({
		bgFile = "Interface\\AddOns\\Cerebro\\Media\\sandbox_frame_hybrid",
		edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
		edgeSize = 24,
		insets = { left = 6, right = 6, top = 6, bottom = 6 }
	})
	frame:SetBackdropColor(1, 1, 1, 1)  -- White to show the full color of our hybrid texture
	frame:SetBackdropBorderColor(0.3, 0.35, 0.4, 1)
	
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetScript("OnDragStart", frame.StartMoving)
	frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
	frame:SetFrameStrata("HIGH")
	
	sandboxFrame = frame
	
	-- Hook OnHide to ensure cleanup happens even when closed via ESC key
	-- This fixes blank content issue by ensuring full recreation on next Show()
	frame:SetScript("OnHide", function()
		-- Cancel sync status timer
		self:StopSyncStatusTimer()
		
		-- Release AceGUI content frame
		if self.contentFrame then
			AceGUI:Release(self.contentFrame)
			self.contentFrame = nil
		end
		-- Cleanup all tracked native frames
		self:CleanupNativeFrames()
		sandboxFrame = nil
		self.sidebarButtons = {}
	end)
	
	-- Register for ESC key closing
	_G["CerebroUISandbox"] = frame
	tinsert(UISpecialFrames, "CerebroUISandbox")
	
	-- ================================
	-- TITLE BAR
	-- ================================
	local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
	title:SetPoint("TOP", 0, -16)
	title:SetText("|cffFFD700Cerebro UI Sandbox|r")
	
	-- Close button
	local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
	closeBtn:SetPoint("TOPRIGHT", -8, -8)
	closeBtn:SetScript("OnClick", function()
		self:Hide()
	end)
	
	-- ================================
	-- VERTICAL SIDEBAR (LEFT Edge - Full Height)
	-- The background texture already includes sidebar area and divider
	-- ================================
	
	-- Invisible sidebar frame for button anchoring
	local sidebar = self:TrackFrame(CreateFrame("Frame", nil, frame))
	sidebar:SetPoint("TOPLEFT", 10, -45)
	sidebar:SetPoint("BOTTOMLEFT", 10, 12)
	sidebar:SetWidth(SIDEBAR_WIDTH)
	
	-- Tab definitions with atlas icons
	local tabs = {
		{id = "progress", icon = "UI-LFG-RoleIcon-Generic", tooltip = "My Progress"},
		{id = "standings", icon = "pvptalents-tab-pvptalents", tooltip = "Standings"},
		{id = "loot_history", icon = "Auctioneer", tooltip = "Loot History"},
		{id = "history", icon = "communities-icon-chat", tooltip = "History"},
	}
	
	-- Officer-only tabs (appear at bottom)
	if Cerebro:IsOfficer() then
		table.insert(tabs, {id = "session", icon = "groupfinder-icon-flag", tooltip = "Start Raid", officerOnly = true})
		table.insert(tabs, {id = "mplus_history", icon = "ChallengeMode-icon-chest", tooltip = "Guild M+ History", officerOnly = true})
		table.insert(tabs, {id = "epgp_settings", icon = "Professions-Crafting-Orders-Icon", tooltip = "EP/GP Settings", officerOnly = true})
		table.insert(tabs, {id = "system_logs", icon = "GarrMission_MissionIcon-Patrol", tooltip = "System Logs", officerOnly = true})
		table.insert(tabs, {id = "help", icon = "QuestTurnin", tooltip = "Cerebro Help", officerOnly = true})
	end
	
	self.sidebarButtons = {}
	
	for i, tabDef in ipairs(tabs) do
		local btn = self:TrackFrame(CreateFrame("Button", nil, sidebar, "BackdropTemplate"))
		btn:SetSize(ICON_SIZE, ICON_SIZE)
		btn:SetPoint("TOP", 0, -ICON_SPACING - (i - 1) * (ICON_SIZE + ICON_SPACING))
		
		-- Button background
		btn:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 10,
		})
		btn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
		btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
		
		-- Icon
		local icon = btn:CreateTexture(nil, "ARTWORK")
		icon:SetSize(24, 24)
		icon:SetPoint("CENTER")
		icon:SetAtlas(tabDef.icon)
		btn.icon = icon
		
		-- Custom tooltip frame (appears LEFT of button, centered vertically)
		local tooltip = self:TrackFrame(CreateFrame("Frame", nil, btn, "BackdropTemplate"))
		tooltip:SetSize(100, 28)
		tooltip:SetPoint("RIGHT", btn, "LEFT", -8, 0)  -- Left of button, vertically centered
		tooltip:SetFrameStrata("TOOLTIP")
		tooltip:SetAlpha(0)
		tooltip:Hide()
		
		-- Tooltip background
		tooltip:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 10,
		})
		tooltip:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
		tooltip:SetBackdropBorderColor(1, 0.82, 0, 1)  -- Gold border
		
		-- Tooltip text
		local tooltipText = tooltip:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		tooltipText:SetPoint("CENTER", 0, 0)
		tooltipText:SetText("|cffFFD700" .. tabDef.tooltip .. "|r")
		
		-- Adjust tooltip width to fit text
		tooltip:SetWidth(tooltipText:GetStringWidth() + 20)
		
		btn.customTooltip = tooltip
		
		-- Highlight on hover with fade animation
		btn:SetScript("OnEnter", function(self)
			self:SetBackdropBorderColor(1, 0.82, 0, 1) -- Gold
			self.customTooltip:Show()
			UIFrameFadeIn(self.customTooltip, 0.15, self.customTooltip:GetAlpha(), 1)
		end)
		btn:SetScript("OnLeave", function(self)
			if UISandbox.currentTab ~= tabDef.id then
				self:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
			end
			UIFrameFadeOut(self.customTooltip, 0.1, self.customTooltip:GetAlpha(), 0)
		end)
		
		-- Click handler
		btn:SetScript("OnClick", function()
			-- Special handling for session tab - show confirmation popup
			if tabDef.id == "session" then
				-- Store references for tooltip updates
				UISandbox.sessionTooltipText = tooltipText
				UISandbox.sessionTooltip = tooltip
				
				local session = Cerebro.modules.Session and Cerebro.modules.Session:GetCurrentSession()
				if session then
					StaticPopup_Show("CEREBRO_CONFIRM_END_SESSION")
				else
					StaticPopup_Show("CEREBRO_CONFIRM_START_SESSION")
				end
				return -- Don't navigate to a tab
			end
			
			UISandbox:SelectTab(tabDef.id)
		end)
		
		btn.tabId = tabDef.id
		self.sidebarButtons[tabDef.id] = btn
	end
	
	-- ================================
	-- STATUS BAR (Bottom, after sidebar, text right-aligned)
	-- ================================
	local STATUS_HEIGHT = 20
	
	local statusBg = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
	statusBg:SetPoint("BOTTOMLEFT", sidebar, "BOTTOMRIGHT", 20, 2)
	statusBg:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 12)
	statusBg:SetHeight(STATUS_HEIGHT)
	statusBg:SetColorTexture(0.05, 0.05, 0.08, 0.6)
	
	-- Sync status dot (left side of status bar)
	local syncDot = frame:CreateTexture(nil, "OVERLAY")
	syncDot:SetSize(8, 8)
	syncDot:SetPoint("LEFT", statusBg, "LEFT", 6, 0)
	syncDot:SetColorTexture(0.5, 0.5, 0.5, 1)  -- Default gray
	self.syncDot = syncDot
	
	local syncLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	syncLabel:SetPoint("LEFT", syncDot, "RIGHT", 4, 0)
	syncLabel:SetText("|cff888888No Peers|r")
	self.syncLabel = syncLabel
	
	-- Update sync status immediately
	self:UpdateSyncIndicator()
	
	local statusText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	statusText:SetPoint("RIGHT", statusBg, "RIGHT", -5, 0)
	statusText:SetText("|cff666688v" .. Cerebro.version .. "|r")
	
	-- ================================
	-- CONTENT AREA (Right of divider, above status bar)
	-- ================================

	local content = AceGUI:Create("SimpleGroup")  -- Changed from ScrollFrame to prevent dual scroll bars
	content:SetLayout("Flow")
	
	-- Parent to our native frame (positioned after divider glow)
	content.frame:SetParent(frame)
	content.frame:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 22, -5)  -- Reduced from -28 to remove empty space
	content.frame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, STATUS_HEIGHT + 24)  -- Increased for bottom clearance
	content:SetFullWidth(true)
	content.frame:Show()  -- Explicitly show the content frame
	
	self.contentFrame = content

	
	-- Select first tab (My Progress)
	self:SelectTab("progress")
end

function UISandbox:SelectTab(tabId)

	self.currentTab = tabId
	
	-- Cleanup ONLY content frames from previous tab
	self:CleanupContentFrames()
	
	-- Update button highlights
	for id, btn in pairs(self.sidebarButtons or {}) do
		if id == tabId then
			btn:SetBackdropBorderColor(1, 0.82, 0, 1)  -- Gold = selected
			btn:SetBackdropColor(0.15, 0.12, 0.05, 0.9)
		else
			btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)  -- Gray = unselected
			btn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
		end
	end
	
	-- Redraw content
	if self.contentFrame then

		self.contentFrame:ReleaseChildren()
		self:DrawTab(self.contentFrame, tabId)
	end
end

function UISandbox:DrawTab(container, tab)
	if tab == "progress" then
		self:DrawProgressTab(container)
	elseif tab == "standings" then
		self:DrawStandingsTab(container)
	elseif tab == "loot_history" then
		self:DrawLootHistoryTab(container)
	elseif tab == "history" then
		self:DrawHistoryTab(container)
	-- Officer-only tabs (RBAC enforced at sidebar level)
	elseif tab == "session" then
		self:DrawSessionTab(container)
	elseif tab == "mplus_history" then
		self:DrawGuildMPlusHistoryTab(container)
	elseif tab == "epgp_settings" then
		self:DrawEPGPSettingsTab(container)
	elseif tab == "system_logs" then
		self:DrawSystemLogsTab(container)
	elseif tab == "help" then
		self:DrawHelpTab(container)
	end
end

--[[=============================================
	Sync Status Indicator
===============================================]]

-- Update the sync status dot and label in the status bar
function UISandbox:UpdateSyncIndicator()
	if not self.syncDot or not self.syncLabel then return end
	
	local commV2 = Cerebro:GetModule("CommunicationV2", true)
	if not commV2 then
		self.syncDot:SetColorTexture(0.5, 0.5, 0.5, 1)
		self.syncLabel:SetText("|cff888888Offline|r")
		return
	end
	
	local state, details = commV2:GetSyncStatus()
	
	if state == "synced" then
		self.syncDot:SetColorTexture(0, 1, 0, 1)
		self.syncLabel:SetText("|cff00ff00Synced|r")
	elseif state == "syncing" then
		self.syncDot:SetColorTexture(1, 1, 0, 1)
		self.syncLabel:SetText("|cffffff00Syncing...|r")
	elseif state == "diverged" then
		self.syncDot:SetColorTexture(1, 0.2, 0.2, 1)
		local count = details and details.divergedCount or 0
		self.syncLabel:SetText(string.format("|cffff4444Diverged (%d)|r", count))
	else -- no_peers
		self.syncDot:SetColorTexture(0.5, 0.5, 0.5, 1)
		self.syncLabel:SetText("|cff888888No Peers|r")
	end
end

-- Start repeating timer to refresh sync indicator (30s = V2 heartbeat)
function UISandbox:StartSyncStatusTimer()
	self:StopSyncStatusTimer()
	self.syncStatusTimer = Cerebro:ScheduleRepeatingTimer(function()
		self:UpdateSyncIndicator()
	end, 30)
end

-- Cancel sync status refresh timer
function UISandbox:StopSyncStatusTimer()
	if self.syncStatusTimer then
		Cerebro:CancelTimer(self.syncStatusTimer)
		self.syncStatusTimer = nil
	end
end

--[[=============================================
	Tab: My Progress (Real Player Data)
===============================================]]

function UISandbox:DrawProgressTab(container)
	-- Get actual player data
	local myName = Cerebro:GetFullPlayerName(UnitName("player"))
	local player = Cerebro.Player:New(myName)
	local playerData = Cerebro:GetPlayerData(myName) or {}
	
	local pr = player:GetPR() or 0
	local ep = playerData.ep or 0
	local gp = playerData.gp or 200
	
	-- Calculate PR rank (GetAllPlayers returns sorted by PR descending)
	local allPlayers = Cerebro:GetAllPlayers()
	local prRank = #allPlayers
	for i, p in ipairs(allPlayers) do
		if p.name == myName then
			prRank = i
			break
		end
	end
	
	-- Get M+ history data upfront for total count
	local MPlus = Cerebro:GetModule("MythicPlus", true)
	local totalRuns = 0
	if Cerebro.db.global.mplusRuns then
		totalRuns = #Cerebro.db.global.mplusRuns
	end
	
	-- ============================================
	-- HERO SECTION (Compact - Fixed, No Scroll)
	-- ============================================
	local heroWrapper = AceGUI:Create("SimpleGroup")
	heroWrapper:SetLayout("Fill")
	heroWrapper:SetFullWidth(true)
	heroWrapper:SetHeight(115)
	container:AddChild(heroWrapper)
	
	local heroFrame = self:TrackContentFrame(CreateFrame("Frame", nil, heroWrapper.frame, "BackdropTemplate"))
	heroFrame:SetAllPoints()
	heroFrame:SetBackdrop({
		bgFile = "Interface\\AddOns\\Cerebro\\Media\\sandbox_frame_hybrid",
		edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
		edgeSize = 12,
		insets = { left = 3, right = 3, top = 3, bottom = 3 }
	})
	heroFrame:SetBackdropColor(1, 1, 1, 0.95)
	heroFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
	
	-- Portrait dimensions
	local PORTRAIT_SIZE = 95
	local PORTRAIT_PADDING = 8
	
	-- 3D Character Portrait (Far Left with Border)
	local portraitFrame = self:TrackContentFrame(CreateFrame("Frame", nil, heroFrame, "BackdropTemplate"))
	portraitFrame:SetSize(PORTRAIT_SIZE + 6, PORTRAIT_SIZE + 6)  -- +6 for border padding
	portraitFrame:SetPoint("LEFT", PORTRAIT_PADDING, 0)
	portraitFrame:SetBackdrop({
		bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
		edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
		edgeSize = 10,
		insets = { left = 2, right = 2, top = 2, bottom = 2 }
	})
	portraitFrame:SetBackdropColor(0.05, 0.05, 0.05, 1)
	portraitFrame:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
	
	local model = self:TrackContentFrame(CreateFrame("PlayerModel", nil, portraitFrame))
	model:SetSize(PORTRAIT_SIZE - 4, PORTRAIT_SIZE - 4)
	model:SetPoint("CENTER", 0, 0)
	model:SetUnit("player")
	model:SetPortraitZoom(0.9)
	model:SetCamDistanceScale(0.8)
	
	-- Calculate content area (right of portrait, centered in remaining space)
	-- Full width = heroFrame width, portrait takes PORTRAIT_SIZE + PORTRAIT_PADDING*2
	-- Content area starts after portrait and centers in remaining space
	local contentAreaStart = PORTRAIT_SIZE + PORTRAIT_PADDING * 2 + 10  -- 10 = gap after portrait frame
	
	-- Content block width: EP(70) + div(20) + GP(70) + eq(20) + PR(80) = ~260
	-- Plus readiness bar = 260
	local CONTENT_WIDTH = 280
	
	-- Create a content container to hold everything (for centering)
	local contentFrame = self:TrackContentFrame(CreateFrame("Frame", nil, heroFrame))
	contentFrame:SetPoint("LEFT", contentAreaStart, 0)
	contentFrame:SetPoint("RIGHT", -10, 0)
	contentFrame:SetPoint("TOP", 0, 0)
	contentFrame:SetPoint("BOTTOM", 0, 0)
	
	-- Player Name/Rank/Badge - SINGLE LINE at top (centered in content area)
	local nameStr = UnitName("player") or "Player"
	local _, guildRank = GetGuildInfo("player")
	guildRank = guildRank or "No Guild"
	
	local headerLine = Cerebro:GetClassColorString(nameStr) .. nameStr .. "|r"
	headerLine = headerLine .. " |cff888888·|r " .. guildRank
	if Cerebro:IsOfficer() then
		headerLine = headerLine .. " |cff888888·|r |cffFFD700[Officer]|r"
	end
	
	local nameFS = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	nameFS:SetPoint("TOP", 0, -8)
	nameFS:SetText(headerLine)
	
	-- Stats Block - EP ÷ GP = PR equation (centered)
	-- Tooltip definitions (same as classic UI)
	local tooltipEP = "Effort Points\n\nEarned by raiding, killing bosses,\nand completing Mythic+ dungeons.\n\n|cff00ff00Higher EP = more contribution|r"
	local tooltipGP = "Gear Points\n\nIncreases when you receive loot.\n\n|cffff8800Higher GP = more gear received|r"
	local tooltipPR = "Priority Rating\n\nCalculated as EP ÷ GP.\n\n|cffFFD700Higher PR = higher loot priority|r"
	
	-- Create tiles anchored to center
	-- EP Tile (anchor point for the row)
	local tileEP = self:CreateDemoTile(contentFrame, "EP", tostring(ep), {1, 1, 1}, false, tooltipEP)
	
	-- Division Symbol
	local divSymbol = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
	divSymbol:SetPoint("LEFT", tileEP, "RIGHT", 6, 0)
	divSymbol:SetText("|cffFFD700÷|r")
	
	-- GP Tile
	local tileGP = self:CreateDemoTile(contentFrame, "GP", tostring(gp), {0.5, 0.5, 0.5}, false, tooltipGP)
	tileGP:SetPoint("LEFT", divSymbol, "RIGHT", 6, 0)
	
	-- Equals Symbol
	local eqSymbol = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
	eqSymbol:SetPoint("LEFT", tileGP, "RIGHT", 6, 0)
	eqSymbol:SetText("|cffFFD700=|r")
	
	-- PR Tile (Emphasized)
	local tilePR = self:CreateDemoTile(contentFrame, "PR", string.format("%.2f", pr), {1, 0.82, 0}, true, tooltipPR)
	tilePR:SetPoint("LEFT", eqSymbol, "RIGHT", 6, 0)
	
	-- PR Rank display (right of PR tile)
	-- Huge "Rank" label
	local rankLabel = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
	rankLabel:SetPoint("TOPLEFT", tilePR, "TOPRIGHT", 15, -2)
	rankLabel:SetText("|cffFFD700Rank|r")
	
	-- Massive Rank Number (1.5x Huge)
	local rankValue = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
	rankValue:SetPoint("TOP", rankLabel, "BOTTOM", 0, -4)
	rankValue:SetText("|cffFFD700" .. prRank .. "|r")
	
	-- Force larger size (32pt)
	local fontPath = GameFontNormalHuge:GetFont()
	rankValue:SetFont(fontPath, 32, "OUTLINE")
	
	-- Calculate total equation width and center it
	-- EP(70) + gap(6) + div(20) + gap(6) + GP(70) + gap(6) + eq(20) + gap(6) + PR(80) = 284
	local EQUATION_WIDTH = 284
	tileEP:SetPoint("TOP", -EQUATION_WIDTH/2 + 35, -28)  -- Center the row, offset for tile center
	
	-- Readiness Bar (below equation, centered)
	local epgp = Cerebro:GetModule("EPGP", true)
	local readyCur = epgp and epgp:GetReadiness(myName) or 0
	local readyCap = (Cerebro.db.global.config and Cerebro.db.global.config.epgp and Cerebro.db.global.config.epgp.readinessCap) or 1000
	local readyPct = (readyCap > 0) and math.min(readyCur / readyCap, 1.0) or 0
	
	local bar = self:TrackContentFrame(CreateFrame("StatusBar", nil, contentFrame, "BackdropTemplate"))
	bar:SetSize(EQUATION_WIDTH, 20)
	bar:SetPoint("TOP", tileEP, "BOTTOM", EQUATION_WIDTH/2 - 35, -6)  -- Center under equation
	bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
	bar:SetStatusBarColor(0.2, 0.8, 0.2)
	bar:SetMinMaxValues(0, 1)
	bar:SetValue(readyPct)
	
	local barBg = bar:CreateTexture(nil, "BACKGROUND")
	barBg:SetAllPoints()
	barBg:SetColorTexture(0.1, 0.1, 0.1, 0.8)
	
	bar:SetBackdrop({edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8})
	bar:SetBackdropBorderColor(0.4, 0.33, 0.1, 1)
	
	local barText = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	barText:SetPoint("CENTER")
	barText:SetText(string.format("Readiness: %d%% (%d/%d)", readyPct * 100, readyCur, readyCap))
	
	-- Readiness bar tooltip
	bar:EnableMouse(true)
	bar:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_TOP")
		GameTooltip:AddLine("Readiness", 1, 0.82, 0)
		GameTooltip:AddLine(" ")
		GameTooltip:AddLine("EP earned from Mythic+ and other", 1, 1, 1, true)
		GameTooltip:AddLine("activities outside of raid.", 1, 1, 1, true)
		GameTooltip:AddLine(" ")
		GameTooltip:AddLine("|cffff8800Resets weekly.|r", 1, 1, 1, true)
		GameTooltip:AddLine(" ")
		GameTooltip:AddDoubleLine("Current:", readyCur, 1, 1, 1, 0.2, 1, 0.2)
		GameTooltip:AddDoubleLine("Cap:", readyCap, 1, 1, 1, 1, 0.82, 0)
		GameTooltip:Show()
	end)
	bar:SetScript("OnLeave", function()
		GameTooltip:Hide()
	end)
	
	-- ============================================
	-- M+ HISTORY SECTION (Exact MyProgress.lua pattern)
	-- ============================================
	
	-- Determine how many runs to show
	local runLimit = (self.showAllRuns and totalRuns > 5) and 100 or 5
	local history = MPlus and MPlus:GetHistory(runLimit, true) or {}
	local needScroll = self.showAllRuns and #history > 5
	
	-- Main group (exactly like MyProgress.lua)
	local group = AceGUI:Create("InlineGroup")
	group:SetTitle("Recent Mythic+ Runs" .. (totalRuns > 5 and "  |cffFFD700[Click to " .. (self.showAllRuns and "collapse" or "expand") .. "]|r" or ""))
	group:SetFullWidth(true)
	if needScroll then
		group:SetHeight(250)  -- Explicit height for scroll mode (nested scroll fix)
		group:SetLayout("Fill")
	else
		group:SetLayout("List")
	end
	container:AddChild(group)
	
	-- Make the group title clickable to toggle show all
	if totalRuns > 5 then
		group.frame:EnableMouse(true)
		group.frame:SetScript("OnMouseUp", function()
			self.showAllRuns = not self.showAllRuns
			self:SelectTab("progress")
		end)
	end
	
	-- Choose container for rows: scroll or direct
	local rowContainer
	if needScroll then
		local scroll = AceGUI:Create("ScrollFrame")
		scroll:SetLayout("Flow")
		scroll:SetFullWidth(true)
		scroll:SetFullHeight(true)
		group:AddChild(scroll)
		rowContainer = scroll
	else
		rowContainer = group
	end
	
	-- Column header row
	self:CreateMPlusRow(rowContainer, nil, true, false)
	
	if #history == 0 then
		local noRuns = AceGUI:Create("Label")
		noRuns:SetText("|cff888888No runs recorded yet. Complete a dungeon to see history.|r")
		noRuns:SetFullWidth(true)
		rowContainer:AddChild(noRuns)
	else
		-- Render runs
		for i, run in ipairs(history) do
			self:CreateMPlusRow(rowContainer, run, false, (i % 2 == 0))
		end
	end
end

-- M+ Row rendering (adapted from MyProgress:CreateRowFrame)
function UISandbox:CreateMPlusRow(container, run, isHeader, isStripe)
	local ROW_HEIGHT = 32  -- Reduced from 40 to fit more rows
	
	-- AceGUI Wrapper to reserve space in the layout
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetLayout("Fill")
	wrapper:SetFullWidth(true)
	wrapper:SetHeight(isHeader and 20 or ROW_HEIGHT)
	container:AddChild(wrapper)
	
	-- Native Frame (tracked for cleanup)
	local f = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame, "BackdropTemplate"))
	f:SetAllPoints()
	
	-- Background Stripe
	if isStripe and not isHeader then
		local bg = f:CreateTexture(nil, "BACKGROUND")
		bg:SetAllPoints()
		bg:SetColorTexture(1, 1, 1, 0.05)
	end
	
	if isHeader then
		-- Header Row
		local fsDung = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsDung:SetPoint("LEFT", 10, 0)
		fsDung:SetText("|cffFFD700Level / Dungeon|r")
		
		local fsGrp = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsGrp:SetPoint("LEFT", 145, 0)  -- Moved left from 180
		fsGrp:SetText("|cffFFD700Party|r")
		
		local fsRes = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsRes:SetPoint("RIGHT", -10, 0)
		fsRes:SetText("|cffFFD700Result|r")
		return
	end
	
	-- == DATA ROW ==
	
	-- A. Level Badge
	local level = run.level or 0
	local levelColor = "|cffFFFFFF"
	if level >= 20 then levelColor = "|cffFF8000" -- Orange
	elseif level >= 15 then levelColor = "|cffA335EE" -- Purple
	elseif level >= 10 then levelColor = "|cff0070DD" -- Blue
	end
	
	local lvlFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")  -- Smaller font
	lvlFS:SetPoint("LEFT", 10, 0)
	lvlFS:SetText(levelColor .. "+" .. level .. "|r")
	
	-- B. Dungeon Name (with fallback for simulated runs)
	local dungeonNames = {
		[2660] = "Ara-Kara",
		[2662] = "The Dawnbreaker",
		[2651] = "The Stonevault",
		[2649] = "Priory",
		[2669] = "Floodgate",
		[2773] = "Eco-Dome",
		[2287] = "Halls of Atonement",
		[2441] = "Tazavesh: Streets",
		[375] = "Mists",
		[376] = "Necrotic Wake",
		[377] = "De Other Side",
		[378] = "Halls of Atonement",
		[379] = "Plaguefall",
	}
	
	local mapName = nil
	if run.mapID and run.mapID > 0 then
		if run.mapName and run.mapName ~= "" then
			mapName = run.mapName
		end
		if not mapName or mapName == "" then
			mapName = dungeonNames[run.mapID]
		end
		if not mapName or mapName == "" then
			mapName = C_ChallengeMode.GetMapUIInfo(run.mapID)
		end
	end
	mapName = mapName or "Unknown"
	
	local nameFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")  -- Smaller font
	nameFS:SetPoint("LEFT", lvlFS, "RIGHT", 6, 0)
	nameFS:SetWidth(80)  -- Reduced from 100
	nameFS:SetText(mapName)
	nameFS:SetJustifyH("LEFT")
	nameFS:SetWordWrap(false)
	
	-- C. Party Icons
	local startX = 145  -- Moved left from 180
	local iconSize = 20  -- Reduced from 22
	
	-- Sort group: Tank > Healer > Damage
	local roleOrder = { TANK = 1, HEALER = 2, HEALS = 2, DAMAGER = 3, DPS = 3, DMG = 3 }
	local sortedGroup = {}
	for _, m in ipairs(run.group or {}) do table.insert(sortedGroup, m) end
	table.sort(sortedGroup, function(a, b) 
		return (roleOrder[a.role] or 4) < (roleOrder[b.role] or 4) 
	end)
	
	for i, member in ipairs(sortedGroup) do
		local iframe = self:TrackContentFrame(CreateFrame("Frame", nil, f))
		iframe:SetSize(iconSize, iconSize)
		iframe:SetPoint("LEFT", startX + ((i - 1) * (iconSize + 3)), 0)
		
		-- Background circle with class color
		local bgCircle = iframe:CreateTexture(nil, "BACKGROUND")
		bgCircle:SetAllPoints()
		bgCircle:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
		
		local classKey = string.upper(member.class or "PRIEST")
		local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[classKey]
		if cc then
			bgCircle:SetVertexColor(cc.r, cc.g, cc.b, 1)
		else
			bgCircle:SetVertexColor(0.5, 0.5, 0.5, 1)
		end
		
		-- Role icon overlay
		local roleIcon = iframe:CreateTexture(nil, "ARTWORK")
		roleIcon:SetSize(13, 13)
		roleIcon:SetPoint("CENTER")
		
		local role = member.role
		if role == "TANK" then
			roleIcon:SetAtlas("roleicon-tiny-tank")
		elseif role == "HEALER" or role == "HEALS" then
			roleIcon:SetAtlas("roleicon-tiny-healer")
		else
			roleIcon:SetAtlas("roleicon-tiny-dps")
		end
		
		-- Tooltip (with role normalization matching MyProgress.lua)
		iframe:EnableMouse(true)
		iframe:SetScript("OnEnter", function(self)
			GameTooltip:SetOwner(self, "ANCHOR_TOP")
			local colorStr = member.class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[string.upper(member.class)]
				and "|c" .. RAID_CLASS_COLORS[string.upper(member.class)].colorStr or "|cffFFFFFF"
			GameTooltip:AddLine(colorStr .. (member.name or "Unknown") .. "|r")
			
			-- Role normalization (exact match with MyProgress.lua)
			local roleDisplay = member.role or "DPS"
			if roleDisplay == "DAMAGER" or roleDisplay == "DMG" then roleDisplay = "DPS" end
			if roleDisplay == "HEALER" then roleDisplay = "HEALS" end
			
			GameTooltip:AddLine(roleDisplay, 1, 1, 1)
			if member.mplusRating then
				GameTooltip:AddLine("M+ Rating: " .. member.mplusRating, 0.5, 0.5, 0.5)
			end
			GameTooltip:Show()
		end)
		iframe:SetScript("OnLeave", function() GameTooltip:Hide() end)
	end
	
	-- D. Result (Right Aligned) - EP Award only, no status icon (time +upgrade shows completion)
	local epBtn = self:TrackContentFrame(CreateFrame("Button", nil, f))
	epBtn:SetSize(80, 20)
	epBtn:SetPoint("RIGHT", -5, 0)  -- Pushed further right
	
	local epFS = epBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	epFS:SetAllPoints()
	epFS:SetText("|cff00ff00+" .. (run.totalEP or 0) .. " EP|r")
	
	-- EP Breakdown Tooltip (exact match with MyProgress.lua)
	epBtn:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
		GameTooltip:AddLine("EP Breakdown", 1, 1, 1)
		GameTooltip:AddLine(" ")
		GameTooltip:AddDoubleLine("Base Reward:", run.baseEP or 0, 1, 1, 1, 1, 1, 1)
		GameTooltip:AddDoubleLine("Keystone Level (+" .. (run.level or 0) .. "):", run.levelEP or 0, 1, 1, 1, 0.2, 1, 0.2)
		GameTooltip:AddDoubleLine("Time/Guild Bonus:", run.bonusEP or 0, 1, 1, 1, 1, 0.82, 0)
		GameTooltip:AddLine(" ")
		GameTooltip:AddDoubleLine("Total:", (run.totalEP or 0) .. " EP", 1, 1, 1, 0.2, 1, 0.2)
		GameTooltip:AddLine(" ")
		GameTooltip:AddDoubleLine("Status:", run.status or "Pending", 0.5, 0.5, 0.5, 0.5, 0.5, 0.5)
		GameTooltip:Show()
	end)
	epBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
	
	-- Time
	local mins = math.floor((run.duration or 0) / 60)
	local secs = (run.duration or 0) % 60
	local timeStr = string.format("%d:%02d", mins, secs)
	if run.upgrade and run.upgrade > 0 then 
		timeStr = timeStr .. " (+" .. run.upgrade .. ")" 
	end
	
	local timeFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	timeFS:SetPoint("RIGHT", epFS, "LEFT", -10, 0)
	timeFS:SetText(timeStr)
end

--[[=============================================
	Tab: RCLC Loot History
	3-region frame: fixed header (dropdowns + Starting GP),
	scrollable loot rows (GP + Cumulative GP columns),
	anchored End GP footer
===============================================]]

-- Filter state for Loot History tab
UISandbox.lootHistoryFilters = UISandbox.lootHistoryFilters or {
	date = nil,   -- selected date string
	player = nil, -- selected player name or nil for "All Players"
}

function UISandbox:DrawLootHistoryTab(container)
	local importModule = Cerebro:GetModule("RCLC_Import", true)
	if not importModule then
		local errLabel = AceGUI:Create("Label")
		errLabel:SetText("|cffff0000RCLC Import module not available.|r\n\nEnsure RCLootCouncil is installed.")
		errLabel:SetFullWidth(true)
		container:AddChild(errLabel)
		return
	end
	
	-- Detect new dates from RCLC auto-sync
	importModule:DetectNewRCLCDates()
	
	-- Get available dates
	local dates = importModule:GetLootDates()
	
	if #dates == 0 then
		local emptyLabel = AceGUI:Create("Label")
		emptyLabel:SetText("\n\n|cff666666No RCLC loot data found.\n\nLoot history will appear here after items are awarded via RCLootCouncil.|r")
		emptyLabel:SetFullWidth(true)
		container:AddChild(emptyLabel)
		return
	end
	
	-- Default to latest date if none selected
	local dateStillValid = false
	if self.lootHistoryFilters.date then
		for _, d in ipairs(dates) do
			if d == self.lootHistoryFilters.date then dateStillValid = true; break end
		end
	end
	if not dateStillValid then
		self.lootHistoryFilters.date = dates[1] -- Most recent
	end
	local selectedDate = self.lootHistoryFilters.date
	
	-- Get players for selected date
	local playersForDate = importModule:GetPlayersForDate(selectedDate)
	
	-- Default to current player if they have entries, otherwise "All Players"
	local myName = Cerebro:GetPlayerFullName()
	if self.lootHistoryFilters.player == nil then
		local hasMyData = false
		for _, p in ipairs(playersForDate) do
			if p == myName then hasMyData = true; break end
		end
		self.lootHistoryFilters.player = hasMyData and myName or ""
	end
	local selectedPlayer = self.lootHistoryFilters.player
	
	-- ================================
	-- REGION 1: Fixed Header (Dropdowns + Starting GP)
	-- ================================
	local headerGroup = AceGUI:Create("SimpleGroup")
	headerGroup:SetFullWidth(true)
	headerGroup:SetLayout("Flow")
	container:AddChild(headerGroup)
	
	-- Date dropdown
	local dateList = {}
	for _, d in ipairs(dates) do
		dateList[d] = d
	end
	
	local dateDD = AceGUI:Create("Dropdown")
	dateDD:SetLabel("")
	dateDD:SetWidth(120)
	dateDD:SetList(dateList, dates) -- Ordered by most recent first
	dateDD:SetValue(selectedDate)
	dateDD:SetCallback("OnValueChanged", function(_, _, val)
		self.lootHistoryFilters.date = val
		self.lootHistoryFilters.player = nil -- Reset player filter on date change
		self:SelectTab("loot_history")
	end)
	headerGroup:AddChild(dateDD)
	
	-- Name dropdown
	local nameList = {[""] = "All Players"}
	local nameOrder = {""}
	for _, p in ipairs(playersForDate) do
		local shortName = Cerebro:GetShortPlayerName(p) or p
		nameList[p] = shortName
		table.insert(nameOrder, p)
	end
	
	local nameDD = AceGUI:Create("Dropdown")
	nameDD:SetLabel("")
	nameDD:SetWidth(140)
	nameDD:SetList(nameList, nameOrder)
	nameDD:SetValue(selectedPlayer)
	nameDD:SetCallback("OnValueChanged", function(_, _, val)
		self.lootHistoryFilters.player = val
		self:SelectTab("loot_history")
	end)
	headerGroup:AddChild(nameDD)
	
	-- Get loot entries for selected date + player
	local playerFilter = (selectedPlayer ~= "") and selectedPlayer or nil
	local entries = importModule:GetLootEntries(selectedDate, playerFilter)
	
	-- Parse the date to a Unix timestamp for GP calculations
	local dateMonth, dateDay, dateYear = selectedDate:match("(%d+)/(%d+)/(%d+)")
	local dayTimestamp = 0
	if dateMonth and dateDay and dateYear then
		dayTimestamp = time({year = tonumber(dateYear), month = tonumber(dateMonth), day = tonumber(dateDay), hour = 0, min = 0, sec = 0})
	end
	
	-- Calculate Starting GP (only meaningful for single player view)
	local isSinglePlayer = (playerFilter ~= nil)
	local startingGP = 0
	if isSinglePlayer and dayTimestamp > 0 then
		startingGP = importModule:GetStartingGP(selectedPlayer, dayTimestamp)
	end
	
	-- Starting GP header (only shown for single player)
	if isSinglePlayer then
		local startGPLabel = AceGUI:Create("Label")
		startGPLabel:SetText(string.format("|cffFFD700Starting GP:|r |cffffffff%s|r", Cerebro:FormatNumber(startingGP)))
		startGPLabel:SetFullWidth(true)
		container:AddChild(startGPLabel)
	end
	
	-- ================================
	-- Column Header
	-- ================================
	local colHeaderWrapper = AceGUI:Create("SimpleGroup")
	colHeaderWrapper:SetLayout("Fill")
	colHeaderWrapper:SetFullWidth(true)
	colHeaderWrapper:SetHeight(22)
	container:AddChild(colHeaderWrapper)
	
	local hf = self:TrackContentFrame(CreateFrame("Frame", nil, colHeaderWrapper.frame))
	hf:SetAllPoints()
	
	local colName = hf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	colName:SetPoint("LEFT", 8, 0)
	colName:SetText("|cffFFD700Name|r")
	
	local colTime = hf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	colTime:SetPoint("LEFT", 90, 0)
	colTime:SetText("|cffFFD700Time|r")
	
	local colItem = hf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	colItem:SetPoint("LEFT", 150, 0)
	colItem:SetText("|cffFFD700Item|r")
	
	local colResp = hf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	colResp:SetPoint("LEFT", 310, 0)
	colResp:SetText("|cffFFD700Response|r")
	
	local colGP = hf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	colGP:SetPoint("RIGHT", -80, 0)
	colGP:SetText("|cffFFD700GP|r")
	
	if isSinglePlayer then
		local colCumGP = hf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
		colCumGP:SetPoint("RIGHT", -10, 0)
		colCumGP:SetText("|cffFFD700Cum. GP|r")
	end
	
	-- ================================
	-- REGION 2: Scrollable Loot Rows
	-- ================================
	local mainGroup = AceGUI:Create("InlineGroup")
	mainGroup:SetTitle("")
	mainGroup:SetFullWidth(true)
	mainGroup:SetFullHeight(true)
	mainGroup:SetLayout("Fill")
	container:AddChild(mainGroup)
	
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	mainGroup:AddChild(scroll)
	
	-- Empty state
	if #entries == 0 then
		local emptyLabel = AceGUI:Create("Label")
		emptyLabel:SetText("\n\n|cff666666No loot entries for this date" .. (playerFilter and (" for " .. Cerebro:GetShortPlayerName(playerFilter)) or "") .. ".|r")
		emptyLabel:SetFullWidth(true)
		scroll:AddChild(emptyLabel)
	else
		-- Render each loot row
		local cumulativeGP = startingGP
		
		for i, entry in ipairs(entries) do
			local gp = importModule:CalculateGP(entry)
			cumulativeGP = cumulativeGP + gp
			
			self:CreateLootHistoryRow(scroll, entry, gp, cumulativeGP, isSinglePlayer, (i % 2 == 0))
		end
	end
	
	-- ================================
	-- REGION 3: Anchored End GP Footer (only for single player)
	-- ================================
	if isSinglePlayer and #entries > 0 then
		local endGP = startingGP
		for _, entry in ipairs(entries) do
			endGP = endGP + importModule:CalculateGP(entry)
		end
		
		local footerGroup = AceGUI:Create("SimpleGroup")
		footerGroup:SetFullWidth(true)
		footerGroup:SetLayout("Fill")
		footerGroup:SetHeight(28)
		container:AddChild(footerGroup)
		
		local footerFrame = self:TrackContentFrame(CreateFrame("Frame", nil, footerGroup.frame, "BackdropTemplate"))
		footerFrame:SetAllPoints()
		footerFrame:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 10,
			insets = { left = 3, right = 3, top = 3, bottom = 3 }
		})
		footerFrame:SetBackdropColor(0.08, 0.08, 0.12, 0.95)
		footerFrame:SetBackdropBorderColor(0.6, 0.5, 0, 1) -- Gold border
		
		local endGPLabel = footerFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		endGPLabel:SetPoint("RIGHT", -12, 0)
		endGPLabel:SetText(string.format("|cffFFD700End GP:|r |cffffffff%s|r", Cerebro:FormatNumber(endGP)))
	end
end

-- Create a single loot history row
function UISandbox:CreateLootHistoryRow(container, entry, gp, cumulativeGP, showCumGP, isStripe)
	local ROW_HEIGHT = 28
	
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetLayout("Fill")
	wrapper:SetFullWidth(true)
	wrapper:SetHeight(ROW_HEIGHT)
	container:AddChild(wrapper)
	
	local f = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame, "BackdropTemplate"))
	f:SetAllPoints()
	
	-- Stripe background
	if isStripe then
		local bg = f:CreateTexture(nil, "BACKGROUND")
		bg:SetAllPoints()
		bg:SetColorTexture(1, 1, 1, 0.04)
	end
	
	-- Player name (class-colored)
	local shortName = Cerebro:GetShortPlayerName(entry.player) or entry.player
	local classColor = "|cffFFFFFF"
	local playerData = Cerebro.db.global.players and Cerebro.db.global.players[entry.player]
	local playerClass = (playerData and playerData.class) or entry.class
	if playerClass then
		local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[playerClass:upper()]
		if cc then classColor = "|c" .. cc.colorStr end
	end
	
	local nameFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	nameFS:SetPoint("LEFT", 8, 0)
	nameFS:SetWidth(78)
	nameFS:SetText(classColor .. shortName .. "|r")
	nameFS:SetJustifyH("LEFT")
	nameFS:SetWordWrap(false)
	
	-- Time
	local timeFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	timeFS:SetPoint("LEFT", 90, 0)
	timeFS:SetWidth(55)
	local timeStr = entry.time or ""
	if timeStr == "" and entry.servertime > 0 then
		timeStr = date("%H:%M", entry.servertime)
	end
	timeFS:SetText("|cff888888" .. timeStr .. "|r")
	timeFS:SetJustifyH("LEFT")
	
	-- Item name (truncated, colored by quality if we have the link)
	local itemFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	itemFS:SetPoint("LEFT", 150, 0)
	itemFS:SetWidth(155)
	itemFS:SetJustifyH("LEFT")
	itemFS:SetWordWrap(false)
	
	local itemText = entry.itemName or entry.itemLink or "Unknown"
	if entry.itemLink and entry.itemLink:find("item:") then
		-- Try to color by rarity
		local _, _, rarity = C_Item.GetItemInfo(entry.itemLink)
		if rarity and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[rarity] then
			itemText = ITEM_QUALITY_COLORS[rarity].hex .. (entry.itemName or entry.itemLink) .. "|r"
		end
	end
	itemFS:SetText(itemText)
	
	-- Item tooltip on hover
	if entry.itemLink and entry.itemLink:find("item:") then
		f:EnableMouse(true)
		f:SetScript("OnEnter", function(self)
			GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
			GameTooltip:SetHyperlink(entry.itemLink)
			GameTooltip:Show()
		end)
		f:SetScript("OnLeave", function()
			GameTooltip:Hide()
		end)
	end
	
	-- Response
	local respFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	respFS:SetPoint("LEFT", 310, 0)
	respFS:SetWidth(70)
	respFS:SetText("|cffaaaaaa" .. (entry.response or "?") .. "|r")
	respFS:SetJustifyH("LEFT")
	respFS:SetWordWrap(false)
	
	-- GP column
	local isGuildPlayer = Cerebro.db.global.players and Cerebro.db.global.players[entry.player]
	local gpText
	if not isGuildPlayer then
		gpText = "|cff666666N/A|r"
	elseif gp == 0 then
		gpText = "|cff4444440|r"
	else
		gpText = "|cffff8800" .. Cerebro:FormatNumber(gp) .. "|r"
	end
	
	local gpFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	gpFS:SetPoint("RIGHT", -80, 0)
	gpFS:SetWidth(45)
	gpFS:SetText(gpText)
	gpFS:SetJustifyH("RIGHT")
	
	-- Cumulative GP column (only for single player)
	if showCumGP then
		local cumText
		if not isGuildPlayer then
			cumText = "|cff666666N/A|r"
		else
			cumText = "|cffffffff" .. Cerebro:FormatNumber(cumulativeGP) .. "|r"
		end
		
		local cumFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
		cumFS:SetPoint("RIGHT", -10, 0)
		cumFS:SetWidth(60)
		cumFS:SetText(cumText)
		cumFS:SetJustifyH("RIGHT")
	end
end

--[[=============================================
	Tab: History (Transaction Log)
===============================================]]

-- Search text storage for history tab
UISandbox.historySearchText = ""

function UISandbox:DrawHistoryTab(container)
	local transactions = Cerebro.db.global.transactions or {}
	
	-- Header
	local header = AceGUI:Create("Heading")
	header:SetText("Transaction History")
	header:SetFullWidth(true)
	container:AddChild(header)
	
	-- Search bar
	local searchGroup = AceGUI:Create("SimpleGroup")
	searchGroup:SetFullWidth(true)
	searchGroup:SetLayout("Flow")
	container:AddChild(searchGroup)
	
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("")
	searchBox:SetWidth(250)
	searchBox:SetText(self.historySearchText or "")
	searchBox:DisableButton(true)
	searchGroup:AddChild(searchBox)
	
	local searchBtn = AceGUI:Create("Button")
	searchBtn:SetText("Search")
	searchBtn:SetWidth(80)
	searchBtn:SetCallback("OnClick", function()
		self.historySearchText = searchBox:GetText()
		self:SelectTab("history")
	end)
	searchGroup:AddChild(searchBtn)
	
	local clearBtn = AceGUI:Create("Button")
	clearBtn:SetText("Clear")
	clearBtn:SetWidth(60)
	clearBtn:SetCallback("OnClick", function()
		self.historySearchText = ""
		searchBox:SetText("")
		self:SelectTab("history")
	end)
	searchGroup:AddChild(clearBtn)
	
	-- Count display
	local countLabel = AceGUI:Create("Label")
	countLabel:SetWidth(120)
	searchGroup:AddChild(countLabel)
	
	-- Empty state
	if #transactions == 0 then
		local spacer = AceGUI:Create("Label")
		spacer:SetText("\n\n")
		spacer:SetFullWidth(true)
		container:AddChild(spacer)
		
		local emptyLabel = AceGUI:Create("Label")
		emptyLabel:SetText("|cff666666No transactions recorded yet.\n\nEP/GP changes will appear here.|r")
		emptyLabel:SetFullWidth(true)
		container:AddChild(emptyLabel)
		return
	end
	
	-- Filter transactions
	local searchFilter = (self.historySearchText or ""):lower()
	local filtered = {}
	
	-- Process in reverse order (newest first)
	for i = #transactions, 1, -1 do
		local tx = transactions[i]
		
		-- Build searchable text
		local playerName = tx.player or "Unknown"
		local reason = tx.reason or "Unknown"
		local currency = tx.currency or tx.type or "?"
		local admin = tx.admin or "System"
		local details = ""
		if type(tx.details) == "string" then
			details = tx.details
		elseif type(tx.details) == "table" then
			if tx.details.item then details = tx.details.item end
			if tx.details.reason then details = tx.details.reason end
		end
		
		local searchText = string.format("%s %s %s %s %s", 
			playerName, reason, currency, admin, details):lower()
		
		if not tx.voided and (searchFilter == "" or searchText:find(searchFilter, 1, true)) then
			table.insert(filtered, tx)
			if #filtered >= 100 then break end
		end
	end
	
	-- Update count
	countLabel:SetText(string.format("|cff888888%d of %d|r", #filtered, #transactions))
	
	-- Scrollable content
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	container:AddChild(scroll)
	
	-- Header row
	self:CreateHistoryRow(scroll, nil, true, false)
	
	-- Transaction rows
	for i, tx in ipairs(filtered) do
		self:CreateHistoryRow(scroll, tx, false, (i % 2 == 0))
	end
	
	if #filtered == 0 and searchFilter ~= "" then
		local noMatch = AceGUI:Create("Label")
		noMatch:SetText("\n|cff666666No transactions match your search.|r")
		noMatch:SetFullWidth(true)
		scroll:AddChild(noMatch)
	end
end

function UISandbox:CreateHistoryRow(container, tx, isHeader, isStripe)
	local ROW_HEIGHT = 28
	
	-- AceGUI wrapper
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetLayout("Fill")
	wrapper:SetFullWidth(true)
	wrapper:SetHeight(isHeader and 22 or ROW_HEIGHT)
	container:AddChild(wrapper)
	
	-- Native frame
	local f = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame, "BackdropTemplate"))
	f:SetAllPoints()
	
	-- Stripe background
	if isStripe and not isHeader then
		local bg = f:CreateTexture(nil, "BACKGROUND")
		bg:SetAllPoints()
		bg:SetColorTexture(1, 1, 1, 0.05)
	end
	
	if isHeader then
		-- Header labels
		local fsDate = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsDate:SetPoint("LEFT", 5, 0)
		fsDate:SetText("|cffFFD700Date|r")
		
		local fsPlayer = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsPlayer:SetPoint("LEFT", 70, 0)
		fsPlayer:SetText("|cffFFD700Player|r")
		
		local fsAmount = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsAmount:SetPoint("LEFT", 175, 0)
		fsAmount:SetText("|cffFFD700Change|r")
		
		local fsReason = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsReason:SetPoint("LEFT", 260, 0)
		fsReason:SetText("|cffFFD700Reason|r")
		
		local fsAdmin = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		fsAdmin:SetPoint("RIGHT", -5, 0)
		fsAdmin:SetText("|cffFFD700By|r")
		return
	end
	
	-- == DATA ROW ==
	
	-- Date/Time
	local dateFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	dateFS:SetPoint("LEFT", 5, 0)
	dateFS:SetWidth(60)
	dateFS:SetText("|cff888888" .. date("%m/%d %H:%M", tx.timestamp or 0) .. "|r")
	dateFS:SetJustifyH("LEFT")
	
	-- Player name with class color
	local playerName = tx.player or "Unknown"
	local shortName = Cerebro:GetShortPlayerName(playerName)
	local playerData = Cerebro.db.global.players and Cerebro.db.global.players[playerName]
	local classColor = "|cffFFFFFF"
	if playerData and playerData.class then
		local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[playerData.class]
		if cc then classColor = "|c" .. cc.colorStr end
	end
	
	local playerFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	playerFS:SetPoint("LEFT", 70, 0)
	playerFS:SetWidth(100)
	playerFS:SetText(classColor .. shortName .. "|r")
	playerFS:SetJustifyH("LEFT")
	playerFS:SetWordWrap(false)
	
	-- Amount with color (green for EP+, red for GP+/EP-)
	local amount = tx.amount or 0
	local currency = tx.currency or tx.type or "?"
	local amountColor = "|cff00ff00"  -- Default green
	local sign = "+"
	
	if amount < 0 then
		amountColor = "|cffff4444"  -- Red for negative
		sign = ""
	elseif currency == "GP" then
		amountColor = "|cffff8800"  -- Orange for GP charges
	end
	
	local amountFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	amountFS:SetPoint("LEFT", 175, 0)
	amountFS:SetWidth(75)
	amountFS:SetText(string.format("%s%s%d %s|r", amountColor, sign, amount, currency))
	amountFS:SetJustifyH("LEFT")
	
	-- Reason
	local reason = tx.reason or "Unknown"
	local reasonFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	reasonFS:SetPoint("LEFT", 260, 0)
	reasonFS:SetWidth(130)
	reasonFS:SetText(reason)
	reasonFS:SetJustifyH("LEFT")
	reasonFS:SetWordWrap(false)
	
	-- Admin (right-aligned)
	local admin = Cerebro:GetShortPlayerName(tx.admin or "System")
	local adminFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	adminFS:SetPoint("RIGHT", -5, 0)
	adminFS:SetWidth(80)
	adminFS:SetText("|cff00ccff" .. admin .. "|r")
	adminFS:SetJustifyH("RIGHT")
	adminFS:SetWordWrap(false)
	
	-- Tooltip on hover with full details
	f:EnableMouse(true)
	f:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
		GameTooltip:AddLine("Transaction Details", 1, 0.82, 0)
		GameTooltip:AddLine(" ")
		GameTooltip:AddDoubleLine("Player:", playerName, 1, 1, 1, 0.7, 0.7, 0.7)
		GameTooltip:AddDoubleLine("Amount:", string.format("%s%d %s", sign, amount, currency), 1, 1, 1, 0.7, 0.7, 0.7)
		GameTooltip:AddDoubleLine("Reason:", reason, 1, 1, 1, 0.7, 0.7, 0.7)
		GameTooltip:AddDoubleLine("By:", tx.admin or "System", 1, 1, 1, 0, 0.8, 1)
		GameTooltip:AddDoubleLine("Date:", date("%Y-%m-%d %H:%M:%S", tx.timestamp or 0), 1, 1, 1, 0.5, 0.5, 0.5)
		
		-- Session info if present
		if tx.sessionName then
			GameTooltip:AddLine(" ")
			GameTooltip:AddDoubleLine("Session:", tx.sessionName, 1, 1, 1, 0.7, 0.7, 0.7)
		end
		
		-- Details if present
		if type(tx.details) == "table" then
			GameTooltip:AddLine(" ")
			if tx.details.item then
				GameTooltip:AddLine("Item: " .. tx.details.item, 1, 1, 1)
			end
			if tx.details.preGP and tx.details.postGP then
				GameTooltip:AddDoubleLine("GP Change:", 
					string.format("%d → %d", tx.details.preGP, tx.details.postGP), 
					1, 1, 1, 1, 0.5, 0)
			end
			if tx.details.mapID then
				GameTooltip:AddDoubleLine("M+ Level:", tx.details.level or "?", 1, 1, 1, 0.7, 0.7, 0.7)
			end
		elseif type(tx.details) == "string" and tx.details ~= "" then
			GameTooltip:AddLine(" ")
			GameTooltip:AddLine("Details: " .. tx.details, 0.7, 0.7, 0.7, true)
		end
		
		-- Legacy marker
		if not tx.logicVersion then
			GameTooltip:AddLine(" ")
			GameTooltip:AddLine("|cffff8800* Legacy transaction|r", 1, 0.5, 0)
		end
		
		GameTooltip:Show()
	end)
	f:SetScript("OnLeave", function()
		GameTooltip:Hide()
	end)
end

--[[=============================================
	Stat Tile Helper (used by DrawProgressTab)
===============================================]]

function UISandbox:CreateDemoTile(parent, label, value, color, emphasized, tooltip)
	local width = emphasized and 80 or 70
	local height = emphasized and 55 or 50
	local borderR, borderG, borderB = 0.4, 0.4, 0.4
	if emphasized then borderR, borderG, borderB = 1, 0.82, 0 end
	
	local f = self:TrackContentFrame(CreateFrame("Frame", nil, parent, "BackdropTemplate"))
	f:SetSize(width, height)
	f:SetBackdrop({
		bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
		edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
		edgeSize = 12,
		insets = { left = 2, right = 2, top = 2, bottom = 2 }
	})
	f:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
	f:SetBackdropBorderColor(borderR, borderG, borderB, 1)
	
	local v = f:CreateFontString(nil, "OVERLAY", emphasized and "GameFontNormalHuge" or "GameFontNormalLarge")
	v:SetPoint("CENTER", 0, 5)
	v:SetText(value)
	v:SetTextColor(unpack(color))
	
	local l = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	l:SetPoint("BOTTOM", 0, 5)
	l:SetText(string.upper(label))
	l:SetTextColor(0.6, 0.6, 0.6)
	
	-- Add tooltip on hover
	if tooltip then
		f:EnableMouse(true)
		f:SetScript("OnEnter", function(self)
			GameTooltip:SetOwner(self, "ANCHOR_TOP")
			-- Split tooltip into title and body
			local lines = {strsplit("\n", tooltip)}
			for i, line in ipairs(lines) do
				if i == 1 then
					GameTooltip:AddLine(line, 1, 0.82, 0) -- Gold title
				else
					GameTooltip:AddLine(line, 1, 1, 1, true) -- White body, wrap
				end
			end
			GameTooltip:Show()
		end)
		f:SetScript("OnLeave", function()
			GameTooltip:Hide()
		end)
	end
	
	return f
end


function UISandbox:DrawIconsTab(container)
	local desc = AceGUI:Create("Label")
	desc:SetText("|cffFFD700Party Icons|r - LFG-style composite icons (class circle + role overlay)")
	desc:SetFullWidth(true)
	container:AddChild(desc)
	
	local spacer = AceGUI:Create("Label")
	spacer:SetText(" ")
	spacer:SetFullWidth(true)
	container:AddChild(spacer)
	
	-- Icon grid
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetLayout("Fill")
	wrapper:SetFullWidth(true)
	wrapper:SetHeight(200)
	container:AddChild(wrapper)
	
	local host = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame))
	host:SetAllPoints()
	
	-- Classes to demo
	local classes = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "DEATHKNIGHT", 
	                 "SHAMAN", "MAGE", "WARLOCK", "MONK", "DRUID", "DEMONHUNTER"}
	local roles = {"TANK", "HEALER", "DPS"}
	
	local iconSize = 32
	local spacing = 8
	local startX = 30
	local startY = -20
	
	for row, role in ipairs(roles) do
		-- Role label
		local roleLabel = host:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		roleLabel:SetPoint("TOPLEFT", startX - 25, startY - (row - 1) * (iconSize + spacing + 10))
		roleLabel:SetText(role)
		
		for col, class in ipairs(classes) do
			local x = startX + (col - 1) * (iconSize + spacing) + 30
			local y = startY - (row - 1) * (iconSize + spacing + 10) - 15
			
			local iframe = self:TrackContentFrame(CreateFrame("Frame", nil, host))
			iframe:SetSize(iconSize, iconSize)
			iframe:SetPoint("TOPLEFT", x, y)
			
			-- Class-colored circle
			local circle = iframe:CreateTexture(nil, "BACKGROUND")
			circle:SetAllPoints()
			circle:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
			local cc = RAID_CLASS_COLORS[class]
			if cc then
				circle:SetVertexColor(cc.r, cc.g, cc.b, 1)
			end
			
			-- Role icon overlay
			local roleIcon = iframe:CreateTexture(nil, "ARTWORK")
			roleIcon:SetSize(20, 20)
			roleIcon:SetPoint("CENTER")
			if role == "TANK" then
				roleIcon:SetAtlas("roleicon-tiny-tank")
			elseif role == "HEALER" then
				roleIcon:SetAtlas("roleicon-tiny-healer")
			else
				roleIcon:SetAtlas("roleicon-tiny-dps")
			end
			
			-- Tooltip
			iframe:EnableMouse(true)
			iframe:SetScript("OnEnter", function(self)
				GameTooltip:SetOwner(self, "ANCHOR_TOP")
				local colorStr = cc and "|c" .. cc.colorStr or "|cffffffff"
				GameTooltip:AddLine(colorStr .. class .. "|r")
				GameTooltip:AddLine(role, 1, 1, 1)
				GameTooltip:Show()
			end)
			iframe:SetScript("OnLeave", function() GameTooltip:Hide() end)
		end
	end
end

--[[=============================================
	Tab: Typography
===============================================]]

function UISandbox:DrawTypographyTab(container)
	local desc = AceGUI:Create("Label")
	desc:SetText("|cffFFD700Typography Standards|r - Font hierarchy and color codes")
	desc:SetFullWidth(true)
	container:AddChild(desc)
	
	local spacer = AceGUI:Create("Label")
	spacer:SetText(" ")
	spacer:SetFullWidth(true)
	container:AddChild(spacer)
	
	-- Typography samples
	local samples = {
		{font = "GameFontNormalHuge", label = "Primary Headers", color = "|cffFFD700", code = "GameFontNormalHuge + FFD700"},
		{font = "GameFontNormalLarge", label = "Section Headers", color = "|cffFFFFFF", code = "GameFontNormalLarge + FFFFFF"},
		{font = "GameFontHighlight", label = "Primary Body Text", color = "", code = "GameFontHighlight (default gray)"},
		{font = "GameFontNormal", label = "Emphasis / Delta Values", color = "|cff00ff00", code = "GameFontNormal + 00ff00"},
		{font = "GameFontNormalSmall", label = "Disclaimers and Labels", color = "|cff888888", code = "GameFontNormalSmall + 888888"},
	}
	
	for _, sample in ipairs(samples) do
		local wrapper = AceGUI:Create("SimpleGroup")
		wrapper:SetLayout("Fill")
		wrapper:SetFullWidth(true)
		wrapper:SetHeight(40)
		container:AddChild(wrapper)
		
		local host = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame))
		host:SetAllPoints()
		
		local fs = host:CreateFontString(nil, "OVERLAY", sample.font)
		fs:SetPoint("LEFT", 20, 0)
		fs:SetText(sample.color .. sample.label .. "|r")
		
		local code = host:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
		code:SetPoint("LEFT", 300, 0)
		code:SetText("|cff888888" .. sample.code .. "|r")
	end
end

--[[=============================================
	Tab: Data Rows
===============================================]]

function UISandbox:DrawRowsTab(container)
	local desc = AceGUI:Create("Label")
	desc:SetText("|cffFFD700Data Rows|r - Native frame rows with stripe alternation")
	desc:SetFullWidth(true)
	container:AddChild(desc)
	
	local spacer = AceGUI:Create("Label")
	spacer:SetText(" ")
	spacer:SetFullWidth(true)
	container:AddChild(spacer)
	
	-- Header row
	self:CreateDemoRow(container, nil, true, false)
	
	-- Data rows with striping
	local demoData = {
		{level = 15, dungeon = "Ara-Kara", ep = 125, success = true},
		{level = 12, dungeon = "The Stonevault", ep = 95, success = true},
		{level = 18, dungeon = "The Dawnbreaker", ep = 155, success = false},
		{level = 20, dungeon = "Priory", ep = 175, success = true},
	}
	
	for i, data in ipairs(demoData) do
		self:CreateDemoRow(container, data, false, (i % 2 == 0))
	end
	
	-- Code snippet
	local codeLabel = AceGUI:Create("Label")
	codeLabel:SetText("\n|cff888888Pattern:|r AceGUI wrapper (SetHeight) + native CreateFrame + ROW_HEIGHT = 40")
	codeLabel:SetFullWidth(true)
	container:AddChild(codeLabel)
end

function UISandbox:CreateDemoRow(container, data, isHeader, isStripe)
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetLayout("Fill")
	wrapper:SetFullWidth(true)
	wrapper:SetHeight(isHeader and 24 or 40)
	container:AddChild(wrapper)
	
	local f = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame, "BackdropTemplate"))
	f:SetAllPoints()
	
	if isStripe and not isHeader then
		local bg = f:CreateTexture(nil, "BACKGROUND")
		bg:SetAllPoints()
		bg:SetColorTexture(1, 1, 1, 0.05)
	end
	
	if isHeader then
		local col1 = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		col1:SetPoint("LEFT", 10, 0)
		col1:SetText("|cffFFD700Level / Dungeon|r")
		
		local col2 = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		col2:SetPoint("LEFT", 200, 0)
		col2:SetText("|cffFFD700EP Award|r")
		
		local col3 = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		col3:SetPoint("RIGHT", -10, 0)
		col3:SetText("|cffFFD700Result|r")
	else
		-- Level badge
		local levelColor = "|cffFFFFFF"
		if data.level >= 20 then levelColor = "|cffFF8000"
		elseif data.level >= 15 then levelColor = "|cffA335EE"
		elseif data.level >= 10 then levelColor = "|cff0070DD"
		end
		
		local lvl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
		lvl:SetPoint("LEFT", 10, 0)
		lvl:SetText(levelColor .. "+" .. data.level .. "|r")
		
		local name = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		name:SetPoint("LEFT", 60, 0)
		name:SetText(data.dungeon)
		
		local ep = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		ep:SetPoint("LEFT", 200, 0)
		ep:SetText("|cff00ff00+" .. data.ep .. " EP|r")
		
		local status = data.success 
			and "|TInterface\\RaidFrame\\ReadyCheck-Ready:20|t"
			or "|TInterface\\RaidFrame\\ReadyCheck-NotReady:20|t"
		local statusFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
		statusFS:SetPoint("RIGHT", -10, 0)
		statusFS:SetText(status)
	end
end

--[[=============================================
	Tab: Color Palette
===============================================]]

function UISandbox:DrawColorsTab(container)
	local desc = AceGUI:Create("Label")
	desc:SetText("|cffFFD700Color Palette|r - Standard Cerebro colors")
	desc:SetFullWidth(true)
	container:AddChild(desc)
	
	local spacer = AceGUI:Create("Label")
	spacer:SetText(" ")
	spacer:SetFullWidth(true)
	container:AddChild(spacer)
	
	local colors = {
		{name = "Gold (PR/Headers)", hex = "FFD700", r = 1, g = 0.82, b = 0},
		{name = "White (EP)", hex = "FFFFFF", r = 1, g = 1, b = 1},
		{name = "Gray (GP)", hex = "808080", r = 0.5, g = 0.5, b = 0.5},
		{name = "Green (Success)", hex = "00FF00", r = 0, g = 1, b = 0},
		{name = "Orange (Caution)", hex = "FF8800", r = 1, g = 0.53, b = 0},
		{name = "Muted Gray (Disclaimers)", hex = "888888", r = 0.53, g = 0.53, b = 0.53},
	}
	
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetLayout("Fill")
	wrapper:SetFullWidth(true)
	wrapper:SetHeight(250)
	container:AddChild(wrapper)
	
	local host = self:TrackContentFrame(CreateFrame("Frame", nil, wrapper.frame))
	host:SetAllPoints()
	
	local swatchSize = 50
	local startY = -10
	
	for i, color in ipairs(colors) do
		local y = startY - (i - 1) * 38
		
		-- Color swatch
		local swatch = self:TrackContentFrame(CreateFrame("Frame", nil, host, "BackdropTemplate"))
		swatch:SetSize(swatchSize, 30)
		swatch:SetPoint("TOPLEFT", 20, y)
		swatch:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 8,
		})
		swatch:SetBackdropColor(color.r, color.g, color.b, 1)
		swatch:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
		
		-- Label
		local label = host:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		label:SetPoint("LEFT", swatch, "RIGHT", 15, 0)
		label:SetText("|cff" .. color.hex .. color.name .. "|r")
		
		-- Hex code
		local hex = host:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
		hex:SetPoint("LEFT", 350, y - 5)
		hex:SetText("|cff888888#" .. color.hex .. "|r")
	end
end

--[[=============================================
	Tab: Standings (Player Rankings)
===============================================]]

-- Standings state (persistent across tab switches)
UISandbox.standingsSortKey = "pr"
UISandbox.standingsSortAsc = false
UISandbox.standingsFilter = ""

-- Column Schema (matching StandingsFrame.lua exactly)
local STANDINGS_COLUMNS = {
	{ id = "rank", label = "#", width = 25, sortKey = "rank", numeric = true },
	{ id = "name", label = "Player", width = 110, sortKey = "name", numeric = false },
	{ id = "guildRank", label = "Guild Rank", width = 70, sortKey = "guildRank", numeric = false },
	{ id = "pr", label = "PR", width = 50, sortKey = "pr", numeric = true },
	{ id = "ep", label = "EP", width = 40, sortKey = "ep", numeric = true },
	{ id = "gp", label = "GP", width = 40, sortKey = "gp", numeric = true },
	{ id = "status", label = "Status", width = 50, sortKey = "status", numeric = false },
	{ id = "attendance", label = "Attend", width = 55, sortKey = "attendancePct", numeric = true },
}

-- Attendance calculation (ported from StandingsFrame.lua)
function UISandbox:CalculateAttendanceDisplay(playerData)
	local bossKillsPresent = playerData.bossKillsPresent or 0
	local coreAdmittedAt = playerData.coreAdmittedAt
	
	if not coreAdmittedAt then
		return "- (NEW)", {r=0.7, g=0.7, b=0.7}
	end
	
	-- Use eligibleBossKills (pre-computed by officer, synced to all clients)
	local eligibleBosses = playerData.eligibleBossKills or 0
	
	if eligibleBosses == 0 then
		return "- (NEW)", {r=0.7, g=0.7, b=0.7}
	end
	
	-- Cap at 100% for display sanity
	local pct = math.min(100, math.floor((bossKillsPresent / eligibleBosses) * 100))
	local displayText = string.format("%d%%", pct)
	
	local color
	if pct >= 95 then
		color = {r=0.0, g=1.0, b=0.0}
	elseif pct >= 80 then
		color = {r=1.0, g=1.0, b=0.0}
	else
		color = {r=1.0, g=0.0, b=0.0}
	end
	
	return displayText, color, pct
end

function UISandbox:DrawStandingsTab(container)
	local isOfficer = Cerebro:IsOfficer()
	
	-- Check if current player is Pending (non-officers can't see standings if pending)
	local myName = Cerebro:GetFullPlayerName(UnitName("player"))
	local myData = Cerebro:GetPlayerData(myName) or {}
	
	if myData.status == "Pending" and not isOfficer then
		local awaitLabel = AceGUI:Create("Label")
		awaitLabel:SetText("\n\n|cffff0000Awaiting Officer Approval...|r\n\nYou are currently in 'Pending' status.\nYour EP/GP data is visible in 'My Progress',\nbut you will not appear in the Standings until an officer\nadmits you or starts a raid session.")
		awaitLabel:SetFullWidth(true)
		container:AddChild(awaitLabel)
		return
	end
	
	-- Store reference for refresh
	self.standingsContainer = container
	
	-- ============================================
	-- FILTER/SEARCH BAR (Using AceGUI for proper focus)
	-- ============================================
	local filterGroup = AceGUI:Create("SimpleGroup")
	filterGroup:SetFullWidth(true)
	filterGroup:SetLayout("Flow")
	container:AddChild(filterGroup)
	
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("")
	searchBox:SetWidth(150)
	searchBox:SetText(self.standingsFilter or "")
	searchBox:DisableButton(true)
	searchBox:SetCallback("OnTextChanged", function(widget, event, text)
		self.standingsFilter = (text or ""):lower()
		-- Just refresh the rows, don't rebuild entire tab
		self:RefreshStandingsRows()
	end)
	filterGroup:AddChild(searchBox)
	
	local searchHint = AceGUI:Create("Label")
	searchHint:SetText("  |cff888888Search...|r")
	searchHint:SetWidth(80)
	filterGroup:AddChild(searchHint)
	
	-- Spacer to push sync button right
	local spacer = AceGUI:Create("Label")
	spacer:SetText("")
	spacer:SetWidth(80)
	filterGroup:AddChild(spacer)
	
	-- Sync Button (Officer: also broadcasts discovery to guild addon users)
	local syncBtn = AceGUI:Create("Button")
	syncBtn:SetText(isOfficer and "Sync" or "Request")
	syncBtn:SetWidth(80)
	syncBtn:SetCallback("OnClick", function()
		if isOfficer then
			-- Existing sync functionality
			Cerebro:RepairDatabase()
			if Cerebro.modules.Communication then
				local sent = Cerebro.modules.CommunicationV2:SyncToRaid()
				
				-- Enhanced: Scan guild for addon users (BroadcastDiscovery)
				-- Only guild members WITH the addon will respond and be added as Pending
				Cerebro.modules.CommunicationV2:BroadcastDiscovery()
				
				if sent then
					Cerebro:Print("Data synced. Scanning guild for addon users...")
				else
					Cerebro:Print("|cffff8800Sync deferred until combat ends.|r Scanning guild for addon users...")
				end
			end
		else
			if Cerebro.modules.Communication then
				Cerebro.modules.CommunicationV2:RequestSync()
			end
			Cerebro:Print("Sync requested from officers...")
		end
		self:RefreshStandingsRows()
	end)
	filterGroup:AddChild(syncBtn)
	
	-- Small spacer
	local spacer2 = AceGUI:Create("Label")
	spacer2:SetText(" ")
	spacer2:SetFullWidth(true)
	container:AddChild(spacer2)
	
	-- ============================================
	-- HEADER ROW (Clickable for sorting)
	-- ============================================
	local headerGroup = AceGUI:Create("SimpleGroup")
	headerGroup:SetFullWidth(true)
	headerGroup:SetLayout("Flow")
	container:AddChild(headerGroup)
	
	-- Store reference for header refresh
	self.standingsHeaderGroup = headerGroup
	
	self:RenderStandingsHeaders()
	
	-- ============================================
	-- SCROLLABLE DATA AREA
	-- ============================================
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	container:AddChild(scroll)
	
	self.standingsScrollFrame = scroll
	self.standingsIsOfficer = isOfficer
	
	-- Initial render
	self:RefreshStandingsRows()
end

function UISandbox:RenderStandingsHeaders()
	local headerGroup = self.standingsHeaderGroup
	if not headerGroup then return end
	
	headerGroup:ReleaseChildren()
	
	for _, col in ipairs(STANDINGS_COLUMNS) do
		local btn = AceGUI:Create("InteractiveLabel")
		
		-- Build label: gold column name + gray sort indicator (if this column is sorted)
		local labelText = "|cffFFD700" .. col.label .. "|r"
		if self.standingsSortKey == col.sortKey then
			local arrow = self.standingsSortAsc and " ^" or " v"
			labelText = labelText .. "|cff666666" .. arrow .. "|r"
		end
		
		btn:SetText(labelText)
		btn:SetWidth(col.width)
		btn:SetCallback("OnClick", function()
			if self.standingsSortKey == col.sortKey then
				self.standingsSortAsc = not self.standingsSortAsc
			else
				self.standingsSortKey = col.sortKey
				-- Numeric columns default to descending, string columns to ascending
				self.standingsSortAsc = (col.numeric == false)
			end
			-- Refresh both headers (for sort indicator) and rows (for data)
			self:RenderStandingsHeaders()
			self:RefreshStandingsRows()
		end)
		headerGroup:AddChild(btn)
	end
end

function UISandbox:RefreshStandingsRows()
	local scroll = self.standingsScrollFrame
	if not scroll then return end
	
	scroll:ReleaseChildren()
	
	local isOfficer = self.standingsIsOfficer
	
	-- Get and filter players
	local players = Cerebro:GetAllPlayers() or {}
	local filtered = {}
	
	for _, p in ipairs(players) do
		local name = p.name or ""
		local status = p.status or "Core"
		
		-- RBAC: Exclude Pending players for non-officers
		if not (status == "Pending" and not isOfficer) then
			-- Apply filter text
			if self.standingsFilter == "" or name:lower():find(self.standingsFilter, 1, true) then
				-- Compute attendance percentage for sorting
				local _, _, pct = self:CalculateAttendanceDisplay(p)
				p.attendancePct = pct or 0
				table.insert(filtered, p)
			end
		end
	end
	
	-- Sort players
	local columns = STANDINGS_COLUMNS
	table.sort(filtered, function(a, b)
		local valA = a[self.standingsSortKey]
		local valB = b[self.standingsSortKey]
		
		if valA == nil then valA = 0 end
		if valB == nil then valB = 0 end
		
		local isNumeric = false
		for _, col in ipairs(columns) do
			if col.sortKey == self.standingsSortKey then
				isNumeric = col.numeric
				break
			end
		end
		
		if not isNumeric then
			valA = tostring(valA):lower()
			valB = tostring(valB):lower()
		end
		
		if valA ~= valB then
			if self.standingsSortAsc then
				return valA < valB
			else
				return valA > valB
			end
		end
		
		local nameA = (a.name or ""):lower()
		local nameB = (b.name or ""):lower()
		return nameA < nameB
	end)
	
	-- Render rows
	if #filtered == 0 then
		local noResults = AceGUI:Create("Label")
		noResults:SetText("|cff888888No players found matching filter.|r")
		noResults:SetFullWidth(true)
		scroll:AddChild(noResults)
	else
		for i, data in ipairs(filtered) do
			self:CreateStandingsRowAceGUI(scroll, data, i, (i % 2 == 0))
		end
		
	end
end

function UISandbox:CreateStandingsRowAceGUI(container, data, rank, isStripe)
	-- ============================================
	-- DATA EXTRACTION (full Name-Realm always)
	-- ============================================
	local name = data.name or "Unknown"  -- FULL Name-Realm, never strip!
	local class = data.class or "Unknown"
	local pr = data.pr or 0
	local ep = data.ep or 0
	local gp = data.gp or 0
	local status = data.status or "Core"
	
	-- Guild rank
	local guildRank = "Unknown"
	local success, rank_fetch = pcall(function()
		return Cerebro:GetPlayerGuildRank(name)
	end)
	if success and rank_fetch then
		guildRank = rank_fetch
	end
	
	-- Attendance
	local attendText, attendColor = self:CalculateAttendanceDisplay(data)
	
	-- Row group
	local rowGroup = AceGUI:Create("SimpleGroup")
	rowGroup:SetFullWidth(true)
	rowGroup:SetLayout("Flow")
	container:AddChild(rowGroup)
	
	-- ============================================
	-- RIGHT-CLICK HANDLER (Officer Context Menu)
	-- ============================================
	if Cerebro:IsOfficer() and rowGroup.frame then
		rowGroup.frame:EnableMouse(true)
		rowGroup.frame:SetScript("OnMouseUp", function(self, button)
			if button == "RightButton" then
				UISandbox:ShowStandingsContextMenu(self, name, data)
			end
		end)
	end
	
	-- ============================================
	-- COLUMNS (matching STANDINGS_COLUMNS order)
	-- ============================================
	
	-- Rank
	local rankLabel = AceGUI:Create("Label")
	rankLabel:SetText("|cff888888" .. rank .. "|r")
	rankLabel:SetWidth(25)
	rowGroup:AddChild(rankLabel)
	
	-- Player Name (FULL Name-Realm with class color)
	-- NOTE: Use GetClassColor directly, NOT GetClassColorString
	-- GetClassColorString incorrectly calls GetPlayerData(name), which would
	-- auto-create entries if passed a class name like "WARRIOR"
	local nameLabel = AceGUI:Create("Label")
	local cc = Cerebro:GetClassColor(class)
	local colorStr = string.format("|cff%02x%02x%02x", cc.r * 255, cc.g * 255, cc.b * 255)
	nameLabel:SetText(colorStr .. name .. "|r")
	nameLabel:SetWidth(110)
	rowGroup:AddChild(nameLabel)
	
	-- Guild Rank
	local guildLabel = AceGUI:Create("Label")
	guildLabel:SetText("|cffaaaaaa" .. guildRank .. "|r")
	guildLabel:SetWidth(70)
	rowGroup:AddChild(guildLabel)
	
	-- PR (gold)
	local prLabel = AceGUI:Create("Label")
	prLabel:SetText("|cffFFD700" .. string.format("%.2f", pr) .. "|r")
	prLabel:SetWidth(50)
	rowGroup:AddChild(prLabel)
	
	-- EP
	local epLabel = AceGUI:Create("Label")
	epLabel:SetText(string.format("%d", ep))
	epLabel:SetWidth(40)
	rowGroup:AddChild(epLabel)
	
	-- GP
	local gpLabel = AceGUI:Create("Label")
	gpLabel:SetText(string.format("%d", gp == 0 and 1 or gp))
	gpLabel:SetWidth(40)
	rowGroup:AddChild(gpLabel)
	
	-- Status
	local statusLabel = AceGUI:Create("Label")
	if status == "Pending" then
		statusLabel:SetText("|cffff0000Pending|r")
	else
		statusLabel:SetText("|cff00ff00" .. status .. "|r")
	end
	statusLabel:SetWidth(50)
	rowGroup:AddChild(statusLabel)
	
	-- Attendance (color-coded)
	local attendLabel = AceGUI:Create("Label")
	local attendColorStr = string.format("|cff%02x%02x%02x", 
		math.floor(attendColor.r * 255), 
		math.floor(attendColor.g * 255), 
		math.floor(attendColor.b * 255))
	attendLabel:SetText(attendColorStr .. attendText .. "|r")
	attendLabel:SetWidth(55)
	rowGroup:AddChild(attendLabel)
end

--[[=============================================
	Frame Lifecycle Management
===============================================]]

function UISandbox:CleanupNativeFrames()
	-- Cleanup structural frames (called on window close)
	for _, frame in ipairs(self.nativeFrames or {}) do
		if frame and frame.Hide then
			frame:Hide()
			frame:SetParent(nil)
			frame:ClearAllPoints()
		end
	end
	self.nativeFrames = {}
	
	-- Also cleanup content frames
	self:CleanupContentFrames()
end

function UISandbox:CleanupContentFrames()
	-- Cleanup content frames (called on tab switch)
	for _, frame in ipairs(self.contentNativeFrames or {}) do
		if frame and frame.Hide then
			frame:Hide()
			frame:SetParent(nil)
			frame:ClearAllPoints()
		end
	end
	self.contentNativeFrames = {}
end

function UISandbox:TrackFrame(frame)
	-- Track structural frames (sidebar, main frame, etc.)
	if frame then
		table.insert(self.nativeFrames, frame)
	end
	return frame
end

function UISandbox:TrackContentFrame(frame)
	-- Track content frames (cleared on tab switch)
	if frame then
		table.insert(self.contentNativeFrames, frame)
	end
	return frame
end

--[[=============================================
	Officer Tab: Session Management
===============================================]]

function UISandbox:DrawSessionTab(container)
	-- RBAC check (belt & suspenders - sidebar already gated)
	if not Cerebro:IsOfficer() then
		local label = AceGUI:Create("Label")
		label:SetText("|cffff4444Officer access required.|r")
		label:SetFullWidth(true)
		container:AddChild(label)
		return
	end
	
	local session = Cerebro.modules.Session and Cerebro.modules.Session:GetCurrentSession()
	
	-- Header
	local header = AceGUI:Create("Heading")
	header:SetText("Session Management")
	header:SetFullWidth(true)
	container:AddChild(header)
	
	if session then
		-- Active session info
		local activeLabel = AceGUI:Create("Label")
		activeLabel:SetText(string.format("|cff00ff00Active Session:|r %s", session.raidName or "Raid"))
		activeLabel:SetFullWidth(true)
		container:AddChild(activeLabel)
		
		local spacer = AceGUI:Create("Label")
		spacer:SetText("\n")
		spacer:SetFullWidth(true)
		container:AddChild(spacer)
		
		-- End Session button
		local endBtn = AceGUI:Create("Button")
		endBtn:SetText("End Raid Session")
		endBtn:SetWidth(180)
		endBtn:SetCallback("OnClick", function()
			Cerebro.modules.Session:EndSession()
			self:SelectTab("session") -- Refresh
		end)
		container:AddChild(endBtn)
	else
		-- No active session
		local inactiveLabel = AceGUI:Create("Label")
		inactiveLabel:SetText("|cff888888No active session.|r")
		inactiveLabel:SetFullWidth(true)
		container:AddChild(inactiveLabel)
		
		local spacer = AceGUI:Create("Label")
		spacer:SetText("\n")
		spacer:SetFullWidth(true)
		container:AddChild(spacer)
		
		-- Start Session button
		local startBtn = AceGUI:Create("Button")
		startBtn:SetText("Start Raid Session")
		startBtn:SetWidth(180)
		startBtn:SetCallback("OnClick", function()
			Cerebro.modules.Session:StartSession()
			self:SelectTab("session") -- Refresh
		end)
		container:AddChild(startBtn)
	end
	
	-- Additional officer tools
	local spacer2 = AceGUI:Create("Label")
	spacer2:SetText("\n")
	spacer2:SetFullWidth(true)
	container:AddChild(spacer2)
	
	-- Prep Check button
	local prepBtn = AceGUI:Create("Button")
	prepBtn:SetText("Run Prep Check")
	prepBtn:SetWidth(150)
	prepBtn:SetCallback("OnClick", function()
		if Cerebro.modules.Preparation then
			Cerebro.modules.Preparation:CheckRaid()
		end
	end)
	container:AddChild(prepBtn)
	
	-- Decay button
	local decayBtn = AceGUI:Create("Button")
	decayBtn:SetText("Run Weekly Decay")
	decayBtn:SetWidth(150)
	decayBtn:SetCallback("OnClick", function()
		StaticPopup_Show("CEREBRO_CONFIRM_DECAY")
	end)
	container:AddChild(decayBtn)
end

--[[=============================================
	Officer Tab: Guild M+ History
===============================================]]

function UISandbox:DrawGuildMPlusHistoryTab(container)
	if not Cerebro:IsOfficer() then
		local label = AceGUI:Create("Label")
		label:SetText("|cffff4444Officer access required.|r")
		label:SetFullWidth(true)
		container:AddChild(label)
		return
	end
	
	-- Cleanup native frames from previous render
	self.guildMplusNativeFrames = self.guildMplusNativeFrames or {}
	for _, frame in ipairs(self.guildMplusNativeFrames) do
		if frame and frame.Hide then
			frame:Hide()
			frame:SetParent(nil)
		end
	end
	self.guildMplusNativeFrames = {}
	
	local function TrackFrame(frame)
		if frame then table.insert(self.guildMplusNativeFrames, frame) end
		return frame
	end
	
	local ROW_HEIGHT = 36
	
	-- Dungeon lookup table
	local DUNGEON_NAMES = {
		[2660] = "Ara-Kara", [2662] = "Dawnbreaker", [2651] = "Stonevault",
		[2649] = "Priory", [2669] = "Floodgate", [2773] = "Eco-Dome",
		[2287] = "Halls of Atonement", [2441] = "Tazavesh: Streets",
	}
	
	local function GetDungeonName(run)
		if run.mapName and run.mapName ~= "" then return run.mapName end
		if run.mapID and DUNGEON_NAMES[run.mapID] then return DUNGEON_NAMES[run.mapID] end
		if run.mapID then
			local name = C_ChallengeMode.GetMapUIInfo(run.mapID)
			if name then return name end
		end
		return "Unknown"
	end
	
	local runs = Cerebro.db.global.guildMplusRuns or {}
	
	-- Initialize filter state
	self.guildMplusFilters = self.guildMplusFilters or { player = "", dungeon = "", level = "", result = "" }
	
	-- Build unique values for dropdowns
	local players = { [""] = "All Players" }
	local dungeons = { [""] = "All Dungeons" }
	local levels = { [""] = "All Levels" }
	local results = { [""] = "All Results", ["success"] = "Timed", ["fail"] = "Untimed" }
	
	for _, run in ipairs(runs) do
		local p = Cerebro:GetFullPlayerName(run.uploadedBy or "Unknown")
		players[p] = p
		local d = GetDungeonName(run)
		dungeons[d] = d
		local l = "+" .. (run.level or 0)
		levels[l] = l
	end
	
	-- Filter bar
	local filterGroup = AceGUI:Create("SimpleGroup")
	filterGroup:SetFullWidth(true)
	filterGroup:SetLayout("Flow")
	container:AddChild(filterGroup)
	
	local playerDD = AceGUI:Create("Dropdown")
	playerDD:SetLabel("")
	playerDD:SetWidth(150)
	playerDD:SetList(players)
	playerDD:SetValue(self.guildMplusFilters.player or "")
	playerDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.player = val
		container:ReleaseChildren()
		self:DrawGuildMPlusHistoryTab(container)
	end)
	filterGroup:AddChild(playerDD)
	
	local dungeonDD = AceGUI:Create("Dropdown")
	dungeonDD:SetLabel("")
	dungeonDD:SetWidth(120)
	dungeonDD:SetList(dungeons)
	dungeonDD:SetValue(self.guildMplusFilters.dungeon or "")
	dungeonDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.dungeon = val
		container:ReleaseChildren()
		self:DrawGuildMPlusHistoryTab(container)
	end)
	filterGroup:AddChild(dungeonDD)
	
	local levelDD = AceGUI:Create("Dropdown")
	levelDD:SetLabel("")
	levelDD:SetWidth(70)
	levelDD:SetList(levels)
	levelDD:SetValue(self.guildMplusFilters.level or "")
	levelDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.level = val
		container:ReleaseChildren()
		self:DrawGuildMPlusHistoryTab(container)
	end)
	filterGroup:AddChild(levelDD)
	
	local resultDD = AceGUI:Create("Dropdown")
	resultDD:SetLabel("")
	resultDD:SetWidth(90)
	resultDD:SetList(results)
	resultDD:SetValue(self.guildMplusFilters.result or "")
	resultDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.result = val
		container:ReleaseChildren()
		self:DrawGuildMPlusHistoryTab(container)
	end)
	filterGroup:AddChild(resultDD)
	
	-- Apply filters
	local filtered = {}
	for _, run in ipairs(runs) do
		local pass = true
		if self.guildMplusFilters.player ~= "" then
			local p = Cerebro:GetFullPlayerName(run.uploadedBy or "Unknown")
			if p ~= self.guildMplusFilters.player then pass = false end
		end
		if pass and self.guildMplusFilters.dungeon ~= "" then
			if GetDungeonName(run) ~= self.guildMplusFilters.dungeon then pass = false end
		end
		if pass and self.guildMplusFilters.level ~= "" then
			if "+" .. (run.level or 0) ~= self.guildMplusFilters.level then pass = false end
		end
		if pass and self.guildMplusFilters.result ~= "" then
			if self.guildMplusFilters.result == "success" and not run.success then pass = false end
			if self.guildMplusFilters.result == "fail" and run.success then pass = false end
		end
		if pass then table.insert(filtered, run) end
	end
	
	-- Main content area
	local mainGroup = AceGUI:Create("InlineGroup")
	mainGroup:SetTitle("")
	mainGroup:SetFullWidth(true)
	mainGroup:SetFullHeight(true)
	mainGroup:SetLayout("Fill")
	container:AddChild(mainGroup)
	
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	mainGroup:AddChild(scroll)
	
	-- Empty state
	if #filtered == 0 then
		local lbl = AceGUI:Create("Label")
		if #runs == 0 then
			lbl:SetText("\n\nNo guild M+ runs recorded yet.\n\nRuns will appear here as guild members complete M+ dungeons.")
		else
			lbl:SetText("\n\nNo runs match the selected filters.")
		end
		lbl:SetColor(0.5, 0.5, 0.5)
		lbl:SetFullWidth(true)
		scroll:AddChild(lbl)
		return
	end
	
	-- Sort by timestamp descending
	table.sort(filtered, function(a, b)
		return (a.timestamp or 0) > (b.timestamp or 0)
	end)
	
	-- Header row
	local headerWrapper = AceGUI:Create("SimpleGroup")
	headerWrapper:SetLayout("Fill")
	headerWrapper:SetFullWidth(true)
	headerWrapper:SetHeight(24)
	scroll:AddChild(headerWrapper)
	
	local hf = TrackFrame(CreateFrame("Frame", nil, headerWrapper.frame))
	hf:SetAllPoints()
	
	local fsPlayer = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsPlayer:SetPoint("LEFT", 10, 0)
	fsPlayer:SetText("|cffFFD700Player|r")
	
	local fsLevel = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsLevel:SetPoint("LEFT", 180, 0)
	fsLevel:SetText("|cffFFD700Level / Dungeon|r")
	
	local fsParty = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsParty:SetPoint("LEFT", 380, 0)
	fsParty:SetText("|cffFFD700Party|r")
	
	local fsResult = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsResult:SetPoint("RIGHT", -10, 0)
	fsResult:SetText("|cffFFD700Result|r")
	
	-- Data rows (max 100)
	local displayCount = math.min(#filtered, 100)
	local roleOrder = { TANK = 1, HEALER = 2, HEALS = 2, DAMAGER = 3, DPS = 3, DMG = 3 }
	
	for i = 1, displayCount do
		local run = filtered[i]
		local isStripe = (i % 2 == 0)
		
		local wrapper = AceGUI:Create("SimpleGroup")
		wrapper:SetLayout("Fill")
		wrapper:SetFullWidth(true)
		wrapper:SetHeight(ROW_HEIGHT)
		scroll:AddChild(wrapper)
		
		local f = TrackFrame(CreateFrame("Frame", nil, wrapper.frame, "BackdropTemplate"))
		f:SetAllPoints()
		
		if isStripe then
			local bg = f:CreateTexture(nil, "BACKGROUND")
			bg:SetAllPoints()
			bg:SetColorTexture(1, 1, 1, 0.05)
		end
		
		-- A. Player Name (class-colored)
		local fullName = Cerebro:GetFullPlayerName(run.uploadedBy or "Unknown")
		local playerClass = nil
		if run.group then
			for _, member in ipairs(run.group) do
				if (member.name or ""):find("^" .. (run.uploadedBy or "")) then
					playerClass = member.class and string.upper(member.class)
					break
				end
			end
		end
		if not playerClass then
			local playerData = Cerebro.db.global.players and Cerebro.db.global.players[fullName]
			playerClass = playerData and playerData.class
		end
		
		local classColor = "|cffFFFFFF"
		if playerClass and RAID_CLASS_COLORS and RAID_CLASS_COLORS[playerClass] then
			classColor = "|c" .. RAID_CLASS_COLORS[playerClass].colorStr
		end
		
		local playerFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		playerFS:SetPoint("LEFT", 10, 0)
		playerFS:SetWidth(160)
		playerFS:SetText(classColor .. fullName .. "|r")
		playerFS:SetJustifyH("LEFT")
		
		-- B. Level Badge
		local level = run.level or 0
		local levelColor = level >= 20 and "|cffFF8000" or level >= 15 and "|cffA335EE" or level >= 10 and "|cff0070DD" or "|cffFFFFFF"
		local lvlFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
		lvlFS:SetPoint("LEFT", 180, 0)
		lvlFS:SetText(levelColor .. "+" .. level .. "|r")
		
		-- C. Dungeon Name
		local nameFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		nameFS:SetPoint("LEFT", 225, 0)
		nameFS:SetWidth(140)
		nameFS:SetText(GetDungeonName(run))
		nameFS:SetJustifyH("LEFT")
		nameFS:SetWordWrap(false)
		
		-- D. Party Icons
		local startX = 380
		local iconSize = 24
		local sortedGroup = {}
		for _, m in ipairs(run.group or {}) do table.insert(sortedGroup, m) end
		table.sort(sortedGroup, function(a, b)
			return (roleOrder[a.role] or 4) < (roleOrder[b.role] or 4)
		end)
		
		for j, member in ipairs(sortedGroup) do
			local iframe = TrackFrame(CreateFrame("Frame", nil, f))
			iframe:SetSize(iconSize, iconSize)
			iframe:SetPoint("LEFT", startX + ((j - 1) * (iconSize + 4)), 0)
			
			local bgCircle = iframe:CreateTexture(nil, "BACKGROUND")
			bgCircle:SetAllPoints()
			bgCircle:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
			
			local classKey = string.upper(member.class or "PRIEST")
			local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[classKey]
			if cc then bgCircle:SetVertexColor(cc.r, cc.g, cc.b, 1)
			else bgCircle:SetVertexColor(0.5, 0.5, 0.5, 1) end
			
			local roleIcon = iframe:CreateTexture(nil, "ARTWORK")
			roleIcon:SetSize(15, 15)
			roleIcon:SetPoint("CENTER")
			
			local role = member.role
			if role == "TANK" then roleIcon:SetAtlas("roleicon-tiny-tank")
			elseif role == "HEALER" or role == "HEALS" then roleIcon:SetAtlas("roleicon-tiny-healer")
			else roleIcon:SetAtlas("roleicon-tiny-dps") end
			
			iframe:SetScript("OnEnter", function(self)
				GameTooltip:SetOwner(self, "ANCHOR_TOP")
				local cStr = member.class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[string.upper(member.class)]
					and "|c" .. RAID_CLASS_COLORS[string.upper(member.class)].colorStr or "|cffFFFFFF"
				GameTooltip:AddLine(cStr .. (member.name or "Unknown") .. "|r")
				GameTooltip:Show()
			end)
			iframe:SetScript("OnLeave", function() GameTooltip:Hide() end)
		end
		
		-- E. Result
		local statusIcon = run.success
			and "|TInterface\\RaidFrame\\ReadyCheck-Ready:20|t"
			or "|TInterface\\RaidFrame\\ReadyCheck-NotReady:20|t"
		
		local statusFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
		statusFS:SetPoint("RIGHT", -10, 0)
		statusFS:SetText(statusIcon)
		
		-- F. EP Award
		local epFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		epFS:SetPoint("RIGHT", statusFS, "LEFT", -10, 0)
		epFS:SetText("|cff00ff00+" .. (run.totalEP or 0) .. " EP|r")
		
		-- G. Time
		local mins = math.floor((run.duration or 0) / 60)
		local secs = (run.duration or 0) % 60
		local timeStr = string.format("%d:%02d", mins, secs)
		if run.upgrade and run.upgrade > 0 then
			timeStr = timeStr .. " |cff00ff00(+" .. run.upgrade .. ")|r"
		end
		
		local timeFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		timeFS:SetPoint("RIGHT", epFS, "LEFT", -20, 0)
		timeFS:SetText(timeStr)
	end
	
	if #filtered > displayCount then
		local moreLabel = AceGUI:Create("Label")
		moreLabel:SetText(string.format("\n|cff888888Showing %d of %d runs.|r", displayCount, #filtered))
		moreLabel:SetFullWidth(true)
		scroll:AddChild(moreLabel)
	end
end

--[[=============================================
	Officer Tab: EP/GP Settings (Combined)
===============================================]]

-- Track which sub-tab is selected (EP = default)
UISandbox.epgpSubTab = UISandbox.epgpSubTab or "EP"

function UISandbox:DrawEPGPSettingsTab(container)
	if not Cerebro:IsOfficer() then
		local label = AceGUI:Create("Label")
		label:SetText("|cffff4444Officer access required.|r")
		label:SetFullWidth(true)
		container:AddChild(label)
		return
	end
	
	-- Sub-tab button bar
	local buttonBar = AceGUI:Create("SimpleGroup")
	buttonBar:SetFullWidth(true)
	buttonBar:SetHeight(40)
	buttonBar:SetLayout("Flow")
	container:AddChild(buttonBar)
	
	-- Create native frame for buttons (need more control over styling)
	local barWrapper = AceGUI:Create("SimpleGroup")
	barWrapper:SetLayout("Fill")
	barWrapper:SetWidth(200)
	barWrapper:SetHeight(32)
	buttonBar:AddChild(barWrapper)
	
	local barFrame = self:TrackContentFrame(CreateFrame("Frame", nil, barWrapper.frame))
	barFrame:SetAllPoints()
	
	-- EP Button
	local epBtn = self:TrackContentFrame(CreateFrame("Button", nil, barFrame, "BackdropTemplate"))
	epBtn:SetSize(80, 28)
	epBtn:SetPoint("LEFT", 0, 0)
	epBtn:SetBackdrop({
		bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
		edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
		edgeSize = 10,
		insets = { left = 2, right = 2, top = 2, bottom = 2 }
	})
	
	local epText = epBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	epText:SetPoint("CENTER")
	epText:SetText("EP")
	
	-- GP Button
	local gpBtn = self:TrackContentFrame(CreateFrame("Button", nil, barFrame, "BackdropTemplate"))
	gpBtn:SetSize(80, 28)
	gpBtn:SetPoint("LEFT", epBtn, "RIGHT", 4, 0)
	gpBtn:SetBackdrop({
		bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
		edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
		edgeSize = 10,
		insets = { left = 2, right = 2, top = 2, bottom = 2 }
	})
	
	local gpText = gpBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	gpText:SetPoint("CENTER")
	gpText:SetText("GP")
	
	-- Content container (this is what gets refreshed on sub-tab switch)
	local contentContainer = AceGUI:Create("SimpleGroup")
	contentContainer:SetFullWidth(true)
	contentContainer:SetFullHeight(true)
	contentContainer:SetLayout("Fill")
	container:AddChild(contentContainer)
	
	-- Style buttons based on selection
	local function UpdateButtonStyles()
		if self.epgpSubTab == "EP" then
			epBtn:SetBackdropBorderColor(1, 0.82, 0, 1)     -- Gold = selected
			epBtn:SetBackdropColor(0.2, 0.15, 0, 0.8)
			epText:SetTextColor(1, 0.82, 0)
			gpBtn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)  -- Gray = unselected
			gpBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
			gpText:SetTextColor(0.6, 0.6, 0.6)
		else
			gpBtn:SetBackdropBorderColor(1, 0.82, 0, 1)     -- Gold = selected
			gpBtn:SetBackdropColor(0.2, 0.15, 0, 0.8)
			gpText:SetTextColor(1, 0.82, 0)
			epBtn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)  -- Gray = unselected
			epBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
			epText:SetTextColor(0.6, 0.6, 0.6)
		end
	end
	
	-- Function to refresh content only (not the buttons)
	local function RefreshContent()
		contentContainer:ReleaseChildren()
		UpdateButtonStyles()
		if self.epgpSubTab == "EP" then
			self:DrawEPSettingsContent(contentContainer)
		else
			self:DrawGPSettingsContent(contentContainer)
		end
	end
	
	UpdateButtonStyles()
	
	-- Button click handlers - only refresh content, not entire tab
	epBtn:SetScript("OnClick", function()
		if self.epgpSubTab ~= "EP" then
			self.epgpSubTab = "EP"
			RefreshContent()
		end
	end)
	
	gpBtn:SetScript("OnClick", function()
		if self.epgpSubTab ~= "GP" then
			self.epgpSubTab = "GP"
			RefreshContent()
		end
	end)
	
	-- Draw initial content based on selected sub-tab
	if self.epgpSubTab == "EP" then
		self:DrawEPSettingsContent(contentContainer)
	else
		self:DrawGPSettingsContent(contentContainer)
	end
end

--[[=============================================
	Officer Tab: EP Settings (Content Only)
===============================================]]

function UISandbox:DrawEPSettingsContent(container)
	
	-- Create scrolling container
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	container:AddChild(scroll)
	
	-- Helper: Create a slider row for a config item
	local function CreateSliderRow(parent, item)
		local group = AceGUI:Create("SimpleGroup")
		group:SetFullWidth(true)
		group:SetLayout("Flow")
		parent:AddChild(group)
		
		-- Label with unit suffix if specified
		local labelText = item.label
		local label = AceGUI:Create("Label")
		label:SetText(labelText)
		label:SetWidth(160)
		group:AddChild(label)
		
		local slider = AceGUI:Create("Slider")
		local sliderMin = item.min or 0
		local sliderMax = item.max or 100
		slider:SetSliderValues(sliderMin, sliderMax, item.step or 1)
		slider:SetWidth(200)
		-- Don't use SetIsPercent - we handle display with unit suffixes instead
		
		-- Get current value and convert for display
		local currentValue = _G.UI_ConfigSchema:GetValue(item.key) or 0
		local displayValue = currentValue
		
		-- Handle percent display for normalized storage (stored 0-1, display 0-100)
		if item.isPercent and item.percentStorage == "normalized" then
			displayValue = currentValue * 100
		end
		
		-- Clamp to slider range (defensive against corrupted stored values)
		displayValue = math.max(sliderMin, math.min(sliderMax, displayValue))
		slider:SetValue(displayValue)
		
		-- Helper to convert slider value to storage value
		local function toStorageValue(sliderValue)
			if item.isPercent and item.percentStorage == "normalized" then
				return sliderValue / 100
			end
			return sliderValue
		end
		
		-- Save on mouse release (for slider drag)
		slider:SetCallback("OnMouseUp", function(widget, event)
			local sliderValue = widget:GetValue()
			_G.UI_ConfigSchema:SetValue(item.key, toStorageValue(sliderValue))
		end)
		
		-- Save on enter pressed (for manual editbox input)
		slider:SetCallback("OnEnterPressed", function(widget, event, value)
			_G.UI_ConfigSchema:SetValue(item.key, toStorageValue(value))
		end)
		group:AddChild(slider)
		
		-- Unit suffix label (e.g., "%", "EP", "min")
		if item.unit then
			local unitLabel = AceGUI:Create("Label")
			unitLabel:SetText(" " .. item.unit)
			unitLabel:SetWidth(40)
			group:AddChild(unitLabel)
		end
		
		-- Description tooltip
		if item.desc then
			local desc = AceGUI:Create("Label")
			desc:SetText("|cff888888" .. item.desc .. "|r")
			desc:SetFullWidth(true)
			parent:AddChild(desc)
		end
	end
	
	-- ============================================
	-- SECTION: General Settings
	-- ============================================
	local generalHeader = AceGUI:Create("Heading")
	generalHeader:SetText("General Settings")
	generalHeader:SetFullWidth(true)
	scroll:AddChild(generalHeader)
	
	CreateSliderRow(scroll, {
		key = "config.epgp.decayRate",
		label = "Weekly Decay Rate",
		desc = "Percentage to decay EP and GP each week",
		min = 0, max = 50, step = 1,
		isPercent = true, percentStorage = "normalized",
		unit = "%"
	})
	
	-- Spacer
	local spacer1 = AceGUI:Create("Label")
	spacer1:SetText(" ")
	spacer1:SetFullWidth(true)
	scroll:AddChild(spacer1)
	
	-- ============================================
	-- SECTION: Raid EP Awards
	-- ============================================
	local raidHeader = AceGUI:Create("Heading")
	raidHeader:SetText("Raid EP Awards")
	raidHeader:SetFullWidth(true)
	scroll:AddChild(raidHeader)
	
	CreateSliderRow(scroll, {
		key = "config.epgp.epFarmBoss",
		label = "Farm Boss EP",
		desc = "EP awarded per farm boss kill",
		min = 0, max = 1000, step = 10,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.epProgBoss",
		label = "Prog Boss EP",
		desc = "EP awarded per progression boss kill",
		min = 0, max = 1000, step = 10,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.epOnTime",
		label = "Raid Start EP",
		desc = "EP awarded at session start (after grace period)",
		min = 0, max = 1000, step = 10,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.onTimeGracePeriod",
		label = "Grace Period",
		desc = "Minutes after session start where arrivals count as on-time",
		min = 0, max = 30, step = 1,
		unit = "min"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.epFullAttendance",
		label = "Full Attendance",
		desc = "EP awarded for full raid attendance",
		min = 0, max = 1000, step = 10,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.epProgressionHourly",
		label = "Progression (30 min)",
		desc = "EP awarded every 30 minutes of progression",
		min = 0, max = 1000, step = 10,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.benchMultiplier",
		label = "Bench Multiplier",
		desc = "EP multiplier for benched raiders (0% = no EP, 100% = full EP)",
		min = 0, max = 100, step = 5,
		isPercent = true, percentStorage = "normalized",
		unit = "%"
	})
	
	-- Spacer
	local spacer2 = AceGUI:Create("Label")
	spacer2:SetText(" ")
	spacer2:SetFullWidth(true)
	scroll:AddChild(spacer2)
	
	-- ============================================
	-- SECTION: Mythic+ EP Settings
	-- ============================================
	local mplusHeader = AceGUI:Create("Heading")
	mplusHeader:SetText("Mythic+ EP Settings")
	mplusHeader:SetFullWidth(true)
	scroll:AddChild(mplusHeader)
	
	CreateSliderRow(scroll, {
		key = "config.epgp.mplusBase",
		label = "M+ Base",
		desc = "Base EP for completing a Mythic+ dungeon",
		min = 0, max = 1000, step = 10,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.mplusPerLevel",
		label = "Per Level",
		desc = "Additional EP per keystone level",
		min = 0, max = 100, step = 1,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.mplusMaxLevel",
		label = "Max Level Cap",
		desc = "Maximum keystone level for EP scaling",
		min = 1, max = 50, step = 1,
		unit = "+"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.mplusGuildBonus",
		label = "Guild Bonus",
		desc = "Bonus EP for all-guild groups",
		min = 0, max = 100, step = 5,
		unit = "EP"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.readinessCap",
		label = "Readiness Cap",
		desc = "Maximum readiness EP per week",
		min = 0, max = 10000, step = 100,
		unit = "EP"
	})
	
	-- Spacer
	local spacer3 = AceGUI:Create("Label")
	spacer3:SetText(" ")
	spacer3:SetFullWidth(true)
	scroll:AddChild(spacer3)
	
	-- ============================================
	-- SECTION: Preparation EP
	-- ============================================
	local prepHeader = AceGUI:Create("Heading")
	prepHeader:SetText("Preparation")
	prepHeader:SetFullWidth(true)
	scroll:AddChild(prepHeader)
	
	CreateSliderRow(scroll, {
		key = "config.prep.prepAwardAmount",
		label = "Prep Award",
		desc = "EP awarded for passing prep check",
		min = 0, max = 500, step = 10,
		unit = "EP"
	})
end

--[[=============================================
	Officer Tab: GP Settings (Content Only)
===============================================]]

function UISandbox:DrawGPSettingsContent(container)
	
	-- Create scrolling container
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	container:AddChild(scroll)
	
	-- Helper: Create a slider row for a config item (same as EP Settings)
	local function CreateSliderRow(parent, item)
		local group = AceGUI:Create("SimpleGroup")
		group:SetFullWidth(true)
		group:SetLayout("Flow")
		parent:AddChild(group)
		
		local labelText = item.label
		local label = AceGUI:Create("Label")
		label:SetText(labelText)
		label:SetWidth(180)
		group:AddChild(label)
		
		local slider = AceGUI:Create("Slider")
		local sliderMin = item.min or 0
		local sliderMax = item.max or 100
		slider:SetSliderValues(sliderMin, sliderMax, item.step or 1)
		slider:SetWidth(180)
		
		-- Get current value and convert for display
		local currentValue = _G.UI_ConfigSchema:GetValue(item.key) or 0
		local displayValue = currentValue
		
		if item.isPercent and item.percentStorage == "normalized" then
			displayValue = currentValue * 100
		end
		
		displayValue = math.max(sliderMin, math.min(sliderMax, displayValue))
		slider:SetValue(displayValue)
		
		-- Helper to convert slider value to storage value
		local function toStorageValue(sliderValue)
			if item.isPercent and item.percentStorage == "normalized" then
				return sliderValue / 100
			end
			return sliderValue
		end
		
		slider:SetCallback("OnMouseUp", function(widget, event)
			local sliderValue = widget:GetValue()
			_G.UI_ConfigSchema:SetValue(item.key, toStorageValue(sliderValue))
		end)
		
		slider:SetCallback("OnEnterPressed", function(widget, event, value)
			_G.UI_ConfigSchema:SetValue(item.key, toStorageValue(value))
		end)
		group:AddChild(slider)
		
		-- Unit suffix
		if item.unit then
			local unitLabel = AceGUI:Create("Label")
			unitLabel:SetText(" " .. item.unit)
			unitLabel:SetWidth(40)
			group:AddChild(unitLabel)
		end
		
		-- Description
		if item.desc then
			local desc = AceGUI:Create("Label")
			desc:SetText("|cff888888" .. item.desc .. "|r")
			desc:SetFullWidth(true)
			parent:AddChild(desc)
		end
	end
	
	-- ============================================
	-- SECTION: General GP Settings
	-- ============================================
	local generalHeader = AceGUI:Create("Heading")
	generalHeader:SetText("General GP Settings")
	generalHeader:SetFullWidth(true)
	scroll:AddChild(generalHeader)
	
	CreateSliderRow(scroll, {
		key = "config.epgp.baseGP",
		label = "Base GP",
		desc = "Minimum GP floor for all players (prevents division issues)",
		min = 0, max = 10000, step = 10,
		unit = "GP"
	})
	
	-- Spacer
	local spacer1 = AceGUI:Create("Label")
	spacer1:SetText(" ")
	spacer1:SetFullWidth(true)
	scroll:AddChild(spacer1)
	
	-- ============================================
	-- SECTION: GP Modifiers
	-- ============================================
	local modifiersHeader = AceGUI:Create("Heading")
	modifiersHeader:SetText("GP Modifiers")
	modifiersHeader:SetFullWidth(true)
	scroll:AddChild(modifiersHeader)
	
	CreateSliderRow(scroll, {
		key = "config.epgp.offspecMultiplier",
		label = "Off-Spec Multiplier",
		desc = "GP multiplier for off-spec items (50% means half GP cost)",
		min = 0, max = 100, step = 5,
		isPercent = true, percentStorage = "normalized",
		unit = "%"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.minorUpgradeMultiplier",
		label = "Minor Upgrade Multiplier",
		desc = "GP multiplier for minor upgrades",
		min = 0, max = 100, step = 5,
		isPercent = true, percentStorage = "normalized",
		unit = "%"
	})
	
	CreateSliderRow(scroll, {
		key = "config.epgp.minorUpgradeIlvlDelta",
		label = "Minor Upgrade iLvl Delta",
		desc = "Max iLvl difference to consider as minor upgrade",
		min = 0, max = 50, step = 1,
		unit = "iLvl"
	})
	
	-- Spacer
	local spacer2 = AceGUI:Create("Label")
	spacer2:SetText(" ")
	spacer2:SetFullWidth(true)
	scroll:AddChild(spacer2)
	
	-- ============================================
	-- SECTION: GP Base Values Configuration (Paper Doll)
	-- ============================================
	local slotHeader = AceGUI:Create("Heading")
	slotHeader:SetText("GP Base Values Configuration")
	slotHeader:SetFullWidth(true)
	scroll:AddChild(slotHeader)
	
	-- Description
	local slotDesc = AceGUI:Create("Label")
	slotDesc:SetText("|cff888888Set the default GP cost for each item slot. Press Enter to save.|r")
	slotDesc:SetFullWidth(true)
	scroll:AddChild(slotDesc)
	
	-- Paper doll container (native frame for precise positioning)
	local dollWrapper = AceGUI:Create("SimpleGroup")
	dollWrapper:SetFullWidth(true)
	dollWrapper:SetHeight(340)
	dollWrapper:SetLayout("Fill")
	scroll:AddChild(dollWrapper)
	
	local dollFrame = self:TrackContentFrame(CreateFrame("Frame", nil, dollWrapper.frame, "BackdropTemplate"))
	dollFrame:SetAllPoints()
	dollFrame:SetBackdrop({
		bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
		edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
		edgeSize = 12,
		insets = { left = 2, right = 2, top = 2, bottom = 2 }
	})
	dollFrame:SetBackdropColor(0.05, 0.05, 0.05, 0.8)
	dollFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
	
	-- Get slot weights from config
	local config = Cerebro.db.global.config.epgp.slotWeights or {}
	
	-- Slot icon atlas mappings (use WoW's paper doll slot textures)
	local slotAtlas = {
		INVTYPE_HEAD = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Head",
		INVTYPE_NECK = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Neck",
		INVTYPE_SHOULDER = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Shoulder",
		INVTYPE_CLOAK = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest",  -- No cloak slot icon
		INVTYPE_CHEST = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest",
		INVTYPE_WRIST = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Wrists",
		INVTYPE_HAND = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Hands",
		INVTYPE_WAIST = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Waist",
		INVTYPE_LEGS = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Legs",
		INVTYPE_FEET = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Feet",
		INVTYPE_FINGER = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Finger",
		INVTYPE_TRINKET = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Trinket",
		INVTYPE_WEAPON = "Interface\\PaperDoll\\UI-PaperDoll-Slot-MainHand",
		INVTYPE_SHIELD = "Interface\\PaperDoll\\UI-PaperDoll-Slot-SecondaryHand",
		INVTYPE_RANGED = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Ranged",
		INVTYPE_2HWEAPON = "Interface\\PaperDoll\\UI-PaperDoll-Slot-MainHand",
		INVTYPE_WEAPONMAINHAND = "Interface\\PaperDoll\\UI-PaperDoll-Slot-MainHand",
		INVTYPE_WEAPONOFFHAND = "Interface\\PaperDoll\\UI-PaperDoll-Slot-SecondaryHand",
		INVTYPE_HOLDABLE = "Interface\\PaperDoll\\UI-PaperDoll-Slot-SecondaryHand",
	}
	
	-- Slot size
	local SLOT_SIZE = 30
	local SLOT_GAP = 6
	local EDIT_WIDTH = 40
	local EDIT_HEIGHT = 18
	local SLOT_TOTAL_HEIGHT = SLOT_SIZE + SLOT_GAP
	
	-- Helper: Create left-side slot (edit box on LEFT of icon)
	local function CreateLeftSlot(parent, slotKey, slotLabel, x, y)
		local slotFrame = self:TrackContentFrame(CreateFrame("Frame", nil, parent))
		slotFrame:SetSize(EDIT_WIDTH + SLOT_SIZE + 10, SLOT_SIZE)
		slotFrame:SetPoint("TOPLEFT", x, -y)
		
		-- Edit box on left
		local editFrame = self:TrackContentFrame(CreateFrame("EditBox", nil, slotFrame, "BackdropTemplate"))
		editFrame:SetSize(EDIT_WIDTH, EDIT_HEIGHT)
		editFrame:SetPoint("LEFT", 0, 0)
		editFrame:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 8,
			insets = { left = 2, right = 2, top = 2, bottom = 2 }
		})
		editFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
		editFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
		editFrame:SetFontObject("GameFontHighlightSmall")
		editFrame:SetJustifyH("CENTER")
		editFrame:SetAutoFocus(false)
		editFrame:SetText(tostring(config[slotKey] or 0))
		
		-- Icon on right of edit
		local icon = slotFrame:CreateTexture(nil, "ARTWORK")
		icon:SetSize(SLOT_SIZE, SLOT_SIZE)
		icon:SetPoint("LEFT", editFrame, "RIGHT", 4, 0)
		icon:SetTexture(slotAtlas[slotKey] or "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest")
		
		-- Border around icon
		local border = self:TrackContentFrame(CreateFrame("Frame", nil, slotFrame, "BackdropTemplate"))
		border:SetSize(SLOT_SIZE + 4, SLOT_SIZE + 4)
		border:SetPoint("CENTER", icon, "CENTER", 0, 0)
		border:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8 })
		border:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
		
		-- Save handlers
		editFrame:SetScript("OnEnterPressed", function(self)
			local val = tonumber(self:GetText())
			if val and val >= 0 then
				if not Cerebro.db.global.config.epgp.slotWeights then Cerebro.db.global.config.epgp.slotWeights = {} end
				Cerebro.db.global.config.epgp.slotWeights[slotKey] = val
				Cerebro.Ledger:AddAuditLog("GP_CONFIG", string.format("Updated %s GP to %d", slotLabel, val))
				Cerebro:Print(string.format("Updated %s GP to %d", slotLabel, val))
				border:SetBackdropBorderColor(0, 1, 0, 1)
				C_Timer.After(0.5, function() border:SetBackdropBorderColor(0.4, 0.4, 0.4, 1) end)
			else
				self:SetText(tostring(config[slotKey] or 0))
			end
			self:ClearFocus()
		end)
		editFrame:SetScript("OnEscapePressed", function(self) self:SetText(tostring(config[slotKey] or 0)); self:ClearFocus() end)
		
		-- Tooltip
		slotFrame:EnableMouse(true)
		slotFrame:SetScript("OnEnter", function() GameTooltip:SetOwner(slotFrame, "ANCHOR_RIGHT"); GameTooltip:AddLine(slotLabel, 1, 0.82, 0); GameTooltip:Show() end)
		slotFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
		icon:EnableMouse(true)
		icon:SetScript("OnEnter", function() GameTooltip:SetOwner(slotFrame, "ANCHOR_RIGHT"); GameTooltip:AddLine(slotLabel, 1, 0.82, 0); GameTooltip:Show() end)
		icon:SetScript("OnLeave", function() GameTooltip:Hide() end)
		
		return slotFrame
	end
	
	-- Helper: Create right-side slot (edit box on RIGHT of icon)
	local function CreateRightSlot(parent, slotKey, slotLabel, x, y)
		local slotFrame = self:TrackContentFrame(CreateFrame("Frame", nil, parent))
		slotFrame:SetSize(EDIT_WIDTH + SLOT_SIZE + 10, SLOT_SIZE)
		slotFrame:SetPoint("TOPRIGHT", x, -y)
		
		-- Icon on left
		local icon = slotFrame:CreateTexture(nil, "ARTWORK")
		icon:SetSize(SLOT_SIZE, SLOT_SIZE)
		icon:SetPoint("LEFT", 0, 0)
		icon:SetTexture(slotAtlas[slotKey] or "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest")
		
		-- Border around icon
		local border = self:TrackContentFrame(CreateFrame("Frame", nil, slotFrame, "BackdropTemplate"))
		border:SetSize(SLOT_SIZE + 4, SLOT_SIZE + 4)
		border:SetPoint("CENTER", icon, "CENTER", 0, 0)
		border:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8 })
		border:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
		
		-- Edit box on right of icon
		local editFrame = self:TrackContentFrame(CreateFrame("EditBox", nil, slotFrame, "BackdropTemplate"))
		editFrame:SetSize(EDIT_WIDTH, EDIT_HEIGHT)
		editFrame:SetPoint("LEFT", icon, "RIGHT", 4, 0)
		editFrame:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 8,
			insets = { left = 2, right = 2, top = 2, bottom = 2 }
		})
		editFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
		editFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
		editFrame:SetFontObject("GameFontHighlightSmall")
		editFrame:SetJustifyH("CENTER")
		editFrame:SetAutoFocus(false)
		editFrame:SetText(tostring(config[slotKey] or 0))
		
		-- Save handlers
		editFrame:SetScript("OnEnterPressed", function(self)
			local val = tonumber(self:GetText())
			if val and val >= 0 then
				if not Cerebro.db.global.config.epgp.slotWeights then Cerebro.db.global.config.epgp.slotWeights = {} end
				Cerebro.db.global.config.epgp.slotWeights[slotKey] = val
				Cerebro.Ledger:AddAuditLog("GP_CONFIG", string.format("Updated %s GP to %d", slotLabel, val))
				Cerebro:Print(string.format("Updated %s GP to %d", slotLabel, val))
				border:SetBackdropBorderColor(0, 1, 0, 1)
				C_Timer.After(0.5, function() border:SetBackdropBorderColor(0.4, 0.4, 0.4, 1) end)
			else
				self:SetText(tostring(config[slotKey] or 0))
			end
			self:ClearFocus()
		end)
		editFrame:SetScript("OnEscapePressed", function(self) self:SetText(tostring(config[slotKey] or 0)); self:ClearFocus() end)
		
		-- Tooltip
		slotFrame:EnableMouse(true)
		slotFrame:SetScript("OnEnter", function() GameTooltip:SetOwner(slotFrame, "ANCHOR_RIGHT"); GameTooltip:AddLine(slotLabel, 1, 0.82, 0); GameTooltip:Show() end)
		slotFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
		icon:EnableMouse(true)
		icon:SetScript("OnEnter", function() GameTooltip:SetOwner(slotFrame, "ANCHOR_RIGHT"); GameTooltip:AddLine(slotLabel, 1, 0.82, 0); GameTooltip:Show() end)
		icon:SetScript("OnLeave", function() GameTooltip:Hide() end)
		
		return slotFrame
	end
	
	-- Helper: Create weapon slot (icon with overlay text + edit below)
	local function CreateWeaponSlot(parent, slotKey, slotLabel, overlay, x, y)
		local slotFrame = self:TrackContentFrame(CreateFrame("Frame", nil, parent))
		slotFrame:SetSize(SLOT_SIZE + 4, SLOT_SIZE + EDIT_HEIGHT + 4)
		slotFrame:SetPoint("TOPLEFT", x, -y)
		
		-- Icon
		local icon = slotFrame:CreateTexture(nil, "ARTWORK")
		icon:SetSize(SLOT_SIZE, SLOT_SIZE)
		icon:SetPoint("TOP", 0, 0)
		icon:SetTexture(slotAtlas[slotKey] or "Interface\\PaperDoll\\UI-PaperDoll-Slot-MainHand")
		
		-- Overlay text (1H, 2H, MH, etc.)
		local overlayText = slotFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
		overlayText:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -2, 2)
		overlayText:SetText("|cffFFD700" .. overlay .. "|r")
		
		-- Border
		local border = self:TrackContentFrame(CreateFrame("Frame", nil, slotFrame, "BackdropTemplate"))
		border:SetSize(SLOT_SIZE + 4, SLOT_SIZE + 4)
		border:SetPoint("CENTER", icon, "CENTER", 0, 0)
		border:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8 })
		border:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
		
		-- Edit box below
		local editFrame = self:TrackContentFrame(CreateFrame("EditBox", nil, slotFrame, "BackdropTemplate"))
		editFrame:SetSize(SLOT_SIZE + 4, EDIT_HEIGHT)
		editFrame:SetPoint("TOP", icon, "BOTTOM", 0, -2)
		editFrame:SetBackdrop({
			bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
			edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
			edgeSize = 8,
			insets = { left = 2, right = 2, top = 2, bottom = 2 }
		})
		editFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
		editFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
		editFrame:SetFontObject("GameFontHighlightSmall")
		editFrame:SetJustifyH("CENTER")
		editFrame:SetAutoFocus(false)
		editFrame:SetText(tostring(config[slotKey] or 0))
		
		-- Save handlers
		editFrame:SetScript("OnEnterPressed", function(self)
			local val = tonumber(self:GetText())
			if val and val >= 0 then
				if not Cerebro.db.global.config.epgp.slotWeights then Cerebro.db.global.config.epgp.slotWeights = {} end
				Cerebro.db.global.config.epgp.slotWeights[slotKey] = val
				Cerebro.Ledger:AddAuditLog("GP_CONFIG", string.format("Updated %s GP to %d", slotLabel, val))
				Cerebro:Print(string.format("Updated %s GP to %d", slotLabel, val))
				border:SetBackdropBorderColor(0, 1, 0, 1)
				C_Timer.After(0.5, function() border:SetBackdropBorderColor(0.4, 0.4, 0.4, 1) end)
			else
				self:SetText(tostring(config[slotKey] or 0))
			end
			self:ClearFocus()
		end)
		editFrame:SetScript("OnEscapePressed", function(self) self:SetText(tostring(config[slotKey] or 0)); self:ClearFocus() end)
		
		-- Tooltip
		slotFrame:EnableMouse(true)
		slotFrame:SetScript("OnEnter", function() GameTooltip:SetOwner(slotFrame, "ANCHOR_TOP"); GameTooltip:AddLine(slotLabel, 1, 0.82, 0); GameTooltip:Show() end)
		slotFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
		
		return slotFrame
	end
	
	-- Layout constants
	local LEFT_X = 10
	local RIGHT_X = -10
	local TOP_Y = 10
	
	-- LEFT COLUMN: Matches WoW character frame (Head → Wrist, top to bottom)
	local leftSlots = {
		{ key = "INVTYPE_HEAD", label = "Head" },
		{ key = "INVTYPE_NECK", label = "Neck" },
		{ key = "INVTYPE_SHOULDER", label = "Shoulder" },
		{ key = "INVTYPE_CLOAK", label = "Back" },
		{ key = "INVTYPE_CHEST", label = "Chest" },
		{ key = "INVTYPE_WRIST", label = "Wrist" },
	}
	
	for i, slot in ipairs(leftSlots) do
		CreateLeftSlot(dollFrame, slot.key, slot.label, LEFT_X, TOP_Y + (i-1) * SLOT_TOTAL_HEIGHT)
	end
	
	-- RIGHT COLUMN: Matches WoW character frame (Hands → Trinket, top to bottom)
	local rightSlots = {
		{ key = "INVTYPE_HAND", label = "Hands" },
		{ key = "INVTYPE_WAIST", label = "Waist" },
		{ key = "INVTYPE_LEGS", label = "Legs" },
		{ key = "INVTYPE_FEET", label = "Feet" },
		{ key = "INVTYPE_FINGER", label = "Ring" },
		{ key = "INVTYPE_TRINKET", label = "Trinket" },
	}
	
	for i, slot in ipairs(rightSlots) do
		CreateRightSlot(dollFrame, slot.key, slot.label, RIGHT_X, TOP_Y + (i-1) * SLOT_TOTAL_HEIGHT)
	end
	
	-- CENTER: 3D Character Model (fill the space between columns)
	local modelWidth = 140
	local modelHeight = 180  -- Taller to fill vertical space
	local modelFrame = self:TrackContentFrame(CreateFrame("PlayerModel", nil, dollFrame))
	modelFrame:SetSize(modelWidth, modelHeight)
	modelFrame:SetPoint("CENTER", dollFrame, "CENTER", 0, 60)  -- Move higher
	modelFrame:SetUnit("player")
	modelFrame:SetCamDistanceScale(0.8)  -- Zoom in
	modelFrame:SetPosition(0, 0, -0.3)   -- Raise model in frame
	modelFrame:SetRotation(math.rad(15))
	modelFrame:SetAnimation(0)  -- Idle (prevents drift)
	
	-- BOTTOM ROW: Weapons with overlays (in-line with 6th slot row)
	local bottomY = TOP_Y + 5 * SLOT_TOTAL_HEIGHT  -- Same Y as 6th slot row
	local weaponSlots = {
		{ key = "INVTYPE_WEAPON", label = "Weapon (1H)", overlay = "1H" },
		{ key = "INVTYPE_2HWEAPON", label = "Weapon (2H)", overlay = "2H" },
		{ key = "INVTYPE_WEAPONMAINHAND", label = "Main Hand", overlay = "MH" },
		{ key = "INVTYPE_WEAPONOFFHAND", label = "Off Hand", overlay = "OH" },
		{ key = "INVTYPE_SHIELD", label = "Shield", overlay = "SH" },
		{ key = "INVTYPE_RANGED", label = "Ranged", overlay = "RD" },
		{ key = "INVTYPE_HOLDABLE", label = "Off Hand (Frill)", overlay = "FR" },
	}
	
	local weaponWidth = SLOT_SIZE + 8
	local totalWeaponsWidth = #weaponSlots * weaponWidth
	-- Create a container frame for weapons, centered in dollFrame
	local weaponContainer = self:TrackContentFrame(CreateFrame("Frame", nil, dollFrame))
	weaponContainer:SetSize(totalWeaponsWidth, SLOT_SIZE + EDIT_HEIGHT + 4)
	weaponContainer:SetPoint("TOP", dollFrame, "TOP", 0, -bottomY)
	
	for i, slot in ipairs(weaponSlots) do
		CreateWeaponSlot(weaponContainer, slot.key, slot.label, slot.overlay, (i-1) * weaponWidth, 0)
	end
end

--[[=============================================
	Officer Tab: System Logs
===============================================]]

function UISandbox:DrawSystemLogsTab(container)
	if not Cerebro:IsOfficer() then
		local label = AceGUI:Create("Label")
		label:SetText("|cffff4444Officer access required.|r")
		label:SetFullWidth(true)
		container:AddChild(label)
		return
	end
	
	-- Search Bar
	local searchGroup = AceGUI:Create("SimpleGroup")
	searchGroup:SetFullWidth(true)
	searchGroup:SetLayout("Flow")
	container:AddChild(searchGroup)
	
	-- Initialize search state if needed
	self.searchText = self.searchText or {}
	
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("Search (press Enter)")
	searchBox:SetWidth(300)
	searchBox:SetText(self.searchText["system_logs"] or "")
	searchBox:SetCallback("OnEnterPressed", function(widget, event, text)
		self.searchText["system_logs"] = text
		-- Refresh the tab
		container:ReleaseChildren()
		self:DrawSystemLogsTab(container)
	end)
	searchGroup:AddChild(searchBox)
	
	-- Content Area
	local contentGroup = AceGUI:Create("SimpleGroup")
	contentGroup:SetFullWidth(true)
	contentGroup:SetFullHeight(true)
	contentGroup:SetLayout("Fill")
	container:AddChild(contentGroup)
	
	local editBox = AceGUI:Create("MultiLineEditBox")
	editBox:SetLabel("")
	editBox:SetFullWidth(true)
	editBox:SetFullHeight(true)
	editBox:DisableButton(true)
	contentGroup:AddChild(editBox)
	
	-- Build audit log text
	local auditText = ""
	local list = Cerebro.db.global.audit or {}
	local searchFilter = (self.searchText["system_logs"] or ""):lower()
	
	if #list == 0 then
		editBox:SetText("No audit logs found.")
		return
	end
	
	-- Show last 100 (filtered), newest first
	local count = 0
	local currentEpoch = Cerebro.db.global.resetEpoch or 0
	for i = #list, 1, -1 do
		local entry = list[i]
		
		-- Epoch marker: show [Epoch N] for entries from previous epochs
		local epochTag = ""
		if entry.epoch and entry.epoch ~= currentEpoch then
			epochTag = string.format("|cff888888[Epoch %d]|r ", entry.epoch)
		end
		
		-- Format: [DATE TIME] [Epoch N] Admin: ACTION - Details
		local line = string.format("[%s] %s|cff00ccff%s|r: |cffffaa00%s|r - %s",
			date("%m/%d %H:%M", entry.timestamp),
			epochTag,
			Cerebro:GetShortPlayerName(entry.admin),
			entry.action or "ACTION",
			entry.details or ""
		)
		
		-- Apply search filter (case-insensitive)
		if searchFilter == "" or line:lower():find(searchFilter, 1, true) then
			auditText = auditText .. line .. "\n"
			count = count + 1
			if count >= 250 then break end
		end
	end
	
	if count == 0 then
		auditText = "No matching entries found."
	end
	
	editBox:SetText(auditText)
	
	-- Make Read-Only (Revert on change)
	editBox:SetCallback("OnTextChanged", function(widget)
		if widget:GetText() ~= auditText then
			widget:SetText(auditText)
		end
	end)
end

--[[=============================================
	Officer Tab: Cerebro Help
===============================================]]

function UISandbox:DrawHelpTab(container)
	if not Cerebro:IsOfficer() then
		local label = AceGUI:Create("Label")
		label:SetText("|cffff4444Officer access required.|r")
		label:SetFullWidth(true)
		container:AddChild(label)
		return
	end
	
	if not _G.UI_CommandRegistry then
		local label = AceGUI:Create("Label")
		label:SetText("|cffff4444Command Registry not available.|r")
		label:SetFullWidth(true)
		container:AddChild(label)
		return
	end
	
	-- Search bar (fixed at top, outside scroll)
	local searchGroup = AceGUI:Create("SimpleGroup")
	searchGroup:SetFullWidth(true)
	searchGroup:SetLayout("Flow")
	container:AddChild(searchGroup)
	
	self.helpSearchText = self.helpSearchText or ""
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("Search Commands (Enter/Esc to exit)")
	searchBox:SetWidth(300)
	searchBox:DisableButton(true)
	searchBox:SetText(self.helpSearchText)
	searchGroup:AddChild(searchBox)
	
	-- Main scrolling content area
	local mainGroup = AceGUI:Create("InlineGroup")
	mainGroup:SetTitle("")
	mainGroup:SetFullWidth(true)
	mainGroup:SetFullHeight(true)
	mainGroup:SetLayout("Fill")
	container:AddChild(mainGroup)
	
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	scroll:SetLayout("Flow")
	mainGroup:AddChild(scroll)
	
	-- Render initial command list
	self:RenderHelpCommandList(scroll, self.helpSearchText)
	
	-- Store scroll reference for refresh
	self.helpScrollFrame = scroll
	
	-- Now set callbacks (after scroll exists)
	searchBox:SetCallback("OnTextChanged", function(widget, event, text)
		self.helpSearchText = text or ""
		-- Only refresh the scroll content, not the whole tab
		if self.helpScrollFrame then
			self.helpScrollFrame:ReleaseChildren()
			self:RenderHelpCommandList(self.helpScrollFrame, text or "")
		end
	end)
	searchBox:SetCallback("OnEnterPressed", function(widget)
		widget:ClearFocus()
	end)
	searchBox:SetCallback("OnEscapePressed", function(widget)
		widget:ClearFocus()
	end)
end

function UISandbox:RenderHelpCommandList(container, searchText)
	if not _G.UI_CommandRegistry then return end
	
	local commands = _G.UI_CommandRegistry:GetFiltered(searchText or "")
	
	for _, cmd in ipairs(commands) do
		local cmdGroup = AceGUI:Create("InlineGroup")
		cmdGroup:SetTitle(cmd.displayName or cmd.id)
		cmdGroup:SetFullWidth(true)
		cmdGroup:SetLayout("Flow")
		container:AddChild(cmdGroup)
		
		-- Description and slash syntax
		local descText = cmd.description or ""
		if cmd.slash and cmd.slash ~= "" then
			descText = descText .. "\n|cffaaaaaa" .. cmd.slash .. "|r"
		end
		
		local desc = AceGUI:Create("Label")
		desc:SetText(descText)
		desc:SetFullWidth(true)
		cmdGroup:AddChild(desc)
		
		-- Run button
		local runBtn = AceGUI:Create("Button")
		runBtn:SetText("Run")
		runBtn:SetWidth(100)
		runBtn:SetCallback("OnClick", function()
			-- Click-time RBAC check
			if cmd.officerOnly and not Cerebro:IsOfficerSafe() then
				Cerebro:Print("This command requires officer permissions.")
				return
			end
			
			local success, err = _G.UI_CommandRegistry:Execute(cmd.id, {})
			if not success then
				Cerebro:Print("Command failed: " .. tostring(err))
			end
		end)
		cmdGroup:AddChild(runBtn)
	end
	
	if #commands == 0 then
		local noResults = AceGUI:Create("Label")
		noResults:SetText("No commands found matching search.")
		noResults:SetColor(0.7, 0.7, 0.7)
		noResults:SetFullWidth(true)
		container:AddChild(noResults)
	end
	
	-- Bottom spacer
	local bottomSpacer = AceGUI:Create("Label")
	bottomSpacer:SetText(" ")
	bottomSpacer:SetFullWidth(true)
	container:AddChild(bottomSpacer)
end

return UISandbox
