-- Picker.lua
-- City picker / welcome / settings window (WoW-side widgets). Selection,
-- grouping, search, validation and saved "My Cities" live in the pure modules
-- (Selection, Cities); this file renders them and wires clicks. Logic entry
-- points are exposed so the runner can drive them under the mock.

local Cities = require("Cities")
local Selection = require("Selection")
local Window = require("Window")
local Methods = require("Methods")
local Icons = require("Icons")
local Theme = require("Theme")

local VISIBLE_ROWS = 14
local ROW_HEIGHT = 16

-- Colours come from the Theme module (ADR-0009); see ui/Theme.lua.

local Picker = {}

function Picker.init(db)
  Picker.db = db
  if db then
    if not db.notify then
      db.notify = { beforeMinutes = 10, atTime = true, sound = true, fired = {} }
    end
    db.savedCities = db.savedCities or {}
    Selection.normalizeSavedNames(db) -- clean any legacy names (defence in depth)
  end
end

-- Combined picker rows: "My Cities" (saved) first, then the built-in list.
-- Pure (no widgets) so the runner can verify it.
function Picker.buildRows(db, query)
  local rows = {}
  local saved = Selection.getSavedCities(db)
  local matches = {}
  if not query or query == "" then
    matches = saved
  else
    local q = query:lower()
    for _, c in ipairs(saved) do
      if c.name:lower():find(q, 1, true) then matches[#matches + 1] = c end
    end
  end
  if #matches > 0 then
    rows[#rows + 1] = { kind = "header", label = "My Cities" }
    for _, c in ipairs(matches) do rows[#rows + 1] = { kind = "saved", city = c } end
  end
  for _, r in ipairs(Cities.displayList(query)) do rows[#rows + 1] = r end
  return rows
end

local function tzText(entry)
  if entry.tz == "fixed" then
    return string.format("UTC%+d", math.floor((entry.baseUtcOffset or 0) / 60))
  end
  return "my timezone"
end

-- "Selected: ..." indicator text for the active selection.
function Picker.currentSelectionText(db)
  local sel = Selection.get(db)
  if not sel then return "Selected: Rotterdam (default)" end
  if sel.kind == "city" then
    local c = Cities.findByName(sel.name)
    return "Selected: " .. (c and (c.name .. ", " .. c.country) or sel.name)
  elseif sel.kind == "saved" then
    local s = Selection.findSaved(db, sel.name)
    return "Selected: " .. sel.name .. " (saved, " .. (s and tzText(s) or "?") .. ")"
  end
  return string.format("Selected: Manual %.4f, %.4f (%s)", sel.latitude, sel.longitude,
    sel.tz == "fixed" and string.format("UTC%+d", math.floor((sel.baseUtcOffset or 0) / 60)) or "my timezone")
end

function Picker.shouldAutoOpen(db)
  return Selection.get(db) == nil
end

function Picker.clearError()
  if Picker.errorLabel then Picker.errorLabel:SetText("") end
end

function Picker.updateSelected()
  if Picker.selectedLabel then
    Picker.selectedLabel:SetText(Picker.currentSelectionText(Picker.db))
  end
  if Picker.headerLoc then
    Picker.headerLoc:SetText(Picker.headerLocationText(Picker.db))
  end
  -- Current-location card (city big, country/source on the right).
  local sel = Selection.get(Picker.db)
  local city, country = "Rotterdam", ""
  if sel then
    if sel.kind == "city" then
      local c = Cities.findByName(sel.name)
      if c then city, country = c.name, c.country else city = sel.name end
    elseif sel.kind == "saved" then
      city, country = sel.name, "saved"
    else
      city, country = "Manual location", ""
    end
  end
  if Picker.cardCity then Picker.cardCity:SetText(city) end
  if Picker.cardCountry then Picker.cardCountry:SetText(country) end
end

-- Selection actions (persist + update main window + indicator + list).
local function afterSelectionChange()
  if Window.refresh then Window.refresh() end
  Picker.updateSelected()
  if Picker.refreshLocation then Picker.refreshLocation() end
end

function Picker.selectCity(name)
  Selection.setCity(Picker.db, name)
  afterSelectionChange()
end

function Picker.selectSaved(name)
  Selection.setSavedCity(Picker.db, name)
  afterSelectionChange()
end

function Picker.deleteSaved(name)
  Selection.deleteCity(Picker.db, name)
  afterSelectionChange()
end

function Picker.selectCityByName(name)
  local city = Cities.findByName(name)
  if not city then return nil end
  Picker.selectCity(city.name)
  return city.name
end

-- One-off manual location (not saved). Empty fields = no-op (not an error).
function Picker.applyManual(latText, lonText, offsetText)
  if (not latText or latText == "") and (not lonText or lonText == "") then
    Picker.clearError()
    return false
  end
  local lat, lon = tonumber(latText), tonumber(lonText)
  local opts = {}
  if offsetText and offsetText ~= "" then
    local hours = tonumber(offsetText)
    if not hours then return false, "Offset must be a number (hours from UTC)" end
    opts.tz, opts.baseUtcOffset, opts.dstRule = "fixed", math.floor(hours * 60 + 0.5), "none"
  end
  local ok, err = Selection.setManual(Picker.db, lat, lon, opts)
  if ok then
    Picker.clearError(); afterSelectionChange()
  elseif Picker.errorLabel then
    Picker.errorLabel:SetText(err or "Invalid coordinates")
  end
  return ok, err
end

-- Save a named "My City". euDst applies only when an offset is given.
function Picker.saveManual(name, latText, lonText, offsetText, euDst)
  local lat, lon = tonumber(latText), tonumber(lonText)
  local opts = {}
  if offsetText and offsetText ~= "" then
    local hours = tonumber(offsetText)
    if not hours then
      if Picker.errorLabel then Picker.errorLabel:SetText("Offset must be a number") end
      return false, "Offset must be a number"
    end
    opts.tz, opts.baseUtcOffset = "fixed", math.floor(hours * 60 + 0.5)
    opts.dstRule = euDst and "EU" or "none"
  end
  local ok, err, savedName = Selection.saveCity(Picker.db, name, lat, lon, opts)
  if ok then
    Picker.clearError()
    Selection.setSavedCity(Picker.db, savedName)
    afterSelectionChange()
  elseif Picker.errorLabel then
    Picker.errorLabel:SetText(err or "Could not save")
  end
  return ok, err
end

-- ===== Master-detail city picker (3S-2) ===================================
-- Pure row builders (no widgets): the master column lists My Cities + countries
-- (with counts); the detail column lists the selected country's cities, or flat
-- search matches when the search box has text. The old buildRows (combined list)
-- is kept above for callers/tests that still use it.

local function citiesInCountry(country)
  local out = {}
  for _, c in ipairs(Cities.all()) do
    if c.country == country then out[#out + 1] = c end
  end
  table.sort(out, function(a, b) return a.name < b.name end)
  return out
end

function Picker.masterRows(db)
  local rows = {}
  local saved = Selection.getSavedCities(db)
  if #saved > 0 then
    rows[#rows + 1] = { kind = "myheader", label = "MY CITIES" }
    for _, c in ipairs(saved) do rows[#rows + 1] = { kind = "saved", city = c } end
  end
  rows[#rows + 1] = { kind = "cheader", label = "COUNTRIES" }
  for _, g in ipairs(Cities.byCountry()) do
    rows[#rows + 1] = { kind = "country", country = g.country, count = #g.cities }
  end
  return rows
end

-- Returns (rows, searching). Searching = the query drove a flat cross-city list.
function Picker.detailRows(db, query, country)
  local rows = {}
  if query and query ~= "" then
    for _, c in ipairs(Cities.search(query)) do rows[#rows + 1] = { kind = "city", city = c } end
    return rows, true
  end
  if country then
    for _, c in ipairs(citiesInCountry(country)) do rows[#rows + 1] = { kind = "city", city = c } end
  end
  return rows, false
end

-- Country to pre-select: the current city's country, else the first country.
function Picker.defaultCountry(db)
  local sel = Selection.get(db)
  if sel and sel.kind == "city" then
    local c = Cities.findByName(sel.name)
    if c then return c.country end
  end
  local g = Cities.byCountry()[1]
  return g and g.country
end

-- Recompute + render both columns from the current query/selected country.
function Picker.refreshLocation(query)
  query = query or (Picker.searchBox and Picker.searchBox:GetText()) or ""
  if not Picker.selectedCountry then Picker.selectedCountry = Picker.defaultCountry(Picker.db) end
  Picker.masterData = Picker.masterRows(Picker.db)
  local rows, searching = Picker.detailRows(Picker.db, query, Picker.selectedCountry)
  Picker.detailData, Picker.detailSearching = rows, searching
  Picker.refreshMaster()
  Picker.refreshDetail()
  Picker.updateSelected()
end

function Picker.selectCountry(country)
  Picker.selectedCountry = country
  Picker.dScroll = 0
  if Picker.searchBox then Picker.searchBox:SetText("") end -- fires refreshLocation
  Picker.refreshLocation("")
end

function Picker.refreshMaster()
  if not Picker.masterPool then return end
  local data, vis = Picker.masterData or {}, #Picker.masterPool
  Picker.mScroll = math.min(Picker.mScroll or 0, math.max(0, #data - vis))
  local sel = Selection.get(Picker.db)
  local selSaved = sel and sel.kind == "saved" and sel.name or nil
  for i = 1, vis do
    local row = Picker.masterPool[i]
    local e = data[Picker.mScroll + i]
    row.delBtn:Hide(); row.count:SetText(""); row.hl:Hide()
    row.kind, row.country, row.name, row._selected = nil, nil, nil, false
    if not e then
      row:Hide()
    elseif e.kind == "myheader" or e.kind == "cheader" then
      row.label:SetText("|cff8a8275" .. e.label .. "|r"); row.kind = "header"; row:Show()
    elseif e.kind == "saved" then
      local isSel = (e.city.name == selSaved)
      row.label:SetText(e.city.name); row.kind, row.name, row._selected = "saved", e.city.name, isSel
      if isSel then row.hl:Show() end
      row.delBtn:SetScript("OnClick", function() Picker.deleteSaved(e.city.name) end); row.delBtn:Show()
      row:Show()
    elseif e.kind == "country" then
      local isSel = (e.country == Picker.selectedCountry)
      row.label:SetText(e.country); row.count:SetText("|cff8a8275" .. e.count .. "|r")
      row.kind, row.country, row._selected = "country", e.country, isSel
      if isSel then row.hl:Show() end
      row:Show()
    end
  end
  if Picker.masterSB then Picker.masterSB:update() end
end

function Picker.refreshDetail()
  if not Picker.detailPool then return end
  local data, vis = Picker.detailData or {}, #Picker.detailPool
  Picker.dScroll = math.min(Picker.dScroll or 0, math.max(0, #data - vis))
  local sel = Selection.get(Picker.db)
  local selCity = sel and sel.kind == "city" and sel.name or nil
  if Picker.detailHeader then
    Picker.detailHeader:SetText("|cffb89254"
      .. (Picker.detailSearching and "SEARCH RESULTS" or (Picker.selectedCountry or "")) .. "|r")
  end
  for i = 1, vis do
    local row = Picker.detailPool[i]
    local e = data[Picker.dScroll + i]
    row.mark:Hide(); row.hl:Hide()
    row.name, row._selected = nil, false
    if not e then
      row:Hide()
    else
      local isSel = (e.city.name == selCity)
      row.label:SetText(e.city.name); row.name, row._selected = e.city.name, isSel
      if isSel then row.hl:Show(); row.mark:Show() end
      row:Show()
    end
  end
  if Picker.detailSB then Picker.detailSB:update() end
end

function Picker.scrollMaster(delta)
  Picker.mScroll = math.max(0, (Picker.mScroll or 0) - delta)
  Picker.refreshMaster()
end

function Picker.scrollDetail(delta)
  Picker.dScroll = math.max(0, (Picker.dScroll or 0) - delta)
  Picker.refreshDetail()
end

-- The "Add custom location" form replaces the master-detail browse view while
-- open (browse hidden so nothing shows through behind the cream form).
function Picker.openAddPanel()
  Picker.clearError()
  if Picker.browse then Picker.browse:Hide() end
  if Picker.addPanel then Picker.addPanel:Show() end
end

function Picker.closeAddPanel()
  if Picker.addPanel then Picker.addPanel:Hide() end
  if Picker.browse then Picker.browse:Show() end
end

-- Notification settings (wired to db.notify, read live by the Notifier).
function Picker.setBeforeMinutes(n)
  if not (Picker.db and Picker.db.notify) then return end
  Picker.db.notify.beforeMinutes = math.max(0, math.floor(tonumber(n) or 0))
end
function Picker.setAtTime(on)
  if Picker.db and Picker.db.notify then Picker.db.notify.atTime = on and true or false end
end
function Picker.setSound(on)
  if Picker.db and Picker.db.notify then Picker.db.notify.sound = on and true or false end
end
function Picker.updateNotifyControls()
  local n = Picker.db and Picker.db.notify
  if not n then return end
  if Picker.beforeValue then
    local m = n.beforeMinutes or 0
    Picker.beforeValue:SetText(m == 0 and "Off" or (m .. " min"))
  end
  if Picker.atToggle then Picker.atToggle:update() end
  if Picker.soundToggle then Picker.soundToggle:update() end
  if Picker.themeToggle then Picker.themeToggle:update() end
end

-- Step the before-minutes value (stepper buttons); clamps at 0 = Off.
function Picker.stepBeforeMinutes(delta)
  local n = Picker.db and Picker.db.notify
  if not n then return end
  Picker.setBeforeMinutes((n.beforeMinutes or 0) + delta)
  Picker.updateNotifyControls()
end

-- Calculation method + Asr school (persisted in the shared DB, read live by
-- Cities.times via the Methods registry). All pure; the widgets just call these
-- and re-render. Changing either re-runs the engine and refreshes the window.
local function afterCalcChange()
  if Window.refresh then Window.refresh() end
  Picker.updateCalcControls()
end

function Picker.setMethod(key)
  if not Picker.db then return end
  Picker.db.method = Methods.resolveMethod(key)
  afterCalcChange()
end

-- Step through the ordered method list (dir = +1 next / -1 prev), wrapping.
function Picker.cycleMethod(dir)
  local list = Methods.list()
  local cur = Methods.resolveMethod(Picker.db and Picker.db.method)
  local idx = 1
  for i, m in ipairs(list) do if m.key == cur then idx = i; break end end
  idx = ((idx - 1 + (dir or 1)) % #list) + 1
  Picker.setMethod(list[idx].key)
end

function Picker.setMadhab(key)
  if not Picker.db then return end
  Picker.db.madhab = Methods.resolveMadhab(key)
  afterCalcChange()
end

function Picker.toggleMadhab()
  local cur = Methods.resolveMadhab(Picker.db and Picker.db.madhab)
  Picker.setMadhab(cur == "hanafi" and "shafi" or "hanafi")
end

function Picker.updateCalcControls()
  local db = Picker.db
  if Picker.methodDropdown then Picker.methodDropdown:updateButton() end
  if Picker.asrCards then
    local cur = Methods.resolveMadhab(db and db.madhab)
    for _, c in ipairs(Picker.asrCards) do
      local on = (c.key == cur)
      c._selected = on
      if c.bg then c.bg:SetColorTexture(unpack(on and Theme.color("cardSel") or Theme.color("cardOff"))) end
      if c.border then if on then c.border:Show() else c.border:Hide() end end
      if c.title then c.title:SetTextColor(unpack(on and Theme.color("cardTitleOn") or Theme.color("cardTitleOff"))) end
      if c.desc then c.desc:SetTextColor(unpack(on and Theme.color("cardDescOn") or Theme.color("cardDescOff"))) end
    end
  end
end

local function makeColLabel(f, text, x, y)
  local fs = f:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
  fs:SetPoint("TOPLEFT", x, y)
  fs:SetText(text)
end

-- Reusable row-pool dropdown (no Blizzard dropdown widget -- stable on Retail +
-- Classic). A button shows the current value; clicking opens an anchored,
-- scrollable list built from the same pooled-row + highlight pattern as the
-- city list, with a full-screen click-catcher to close on click-away. All
-- state lives on the returned table and the option list / current value are
-- supplied by callbacks, so the runner can drive open/close/select/scroll under
-- the mock. opts = { width, rows, getOptions()->{{key,label}..},
-- getCurrent()->key, onSelect(key) }.
local DD_ROW_H = 16
local function makeDropdown(parent, opts)
  local dd = { isOpen = false, scrollOffset = 0 }
  local vis = opts.rows or 8
  local width = opts.width or 240

  -- Flat cream button: border + cream fill + left label + a v arrow. (No
  -- Blizzard button template, so it matches the cream palette.)
  local button = CreateFrame("Button", nil, parent)
  button:SetSize(width, 26)
  local bborder = button:CreateTexture(nil, "BACKGROUND"); bborder:SetAllPoints(); Theme.tex(bborder, "line")
  local bfill = button:CreateTexture(nil, "BORDER")
  bfill:SetPoint("TOPLEFT", 1, -1); bfill:SetPoint("BOTTOMRIGHT", -1, 1); Theme.tex(bfill, "cardOff")
  local blabel = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  blabel:SetPoint("LEFT", 10, 0); blabel:SetPoint("RIGHT", -24, 0); blabel:SetJustifyH("LEFT"); Theme.txt(blabel, "text")
  dd.labelFS = blabel
  -- Down-arrow texture (a Unicode glyph renders as a "tofu" box in WoW's font).
  local barrow = button:CreateTexture(nil, "OVERLAY")
  barrow:SetSize(13, 13); barrow:SetPoint("RIGHT", -8, 0)
  Icons.setUI(barrow, "chevron", unpack(Theme.color("arrow")))
  button:SetScript("OnEnter", function() bfill:SetColorTexture(unpack(Theme.color("cardSel"))) end)
  button:SetScript("OnLeave", function() bfill:SetColorTexture(unpack(Theme.color("cardOff"))) end)
  dd.button = button

  -- Full-screen catcher: clicking outside the open list closes it. Parented to
  -- the button so it disappears when the button (tab/window) is hidden.
  local catcher = CreateFrame("Button", nil, button)
  catcher:SetPoint("TOPLEFT", UIParent, "TOPLEFT")
  catcher:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT")
  catcher:SetFrameStrata("FULLSCREEN")
  catcher:Hide()
  catcher:SetScript("OnClick", function() dd:close() end)
  dd.catcher = catcher

  local popup = CreateFrame("Frame", nil, button)
  popup:SetSize(width, vis * DD_ROW_H + 8)
  popup:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2)
  popup:SetFrameStrata("FULLSCREEN_DIALOG")
  popup:EnableMouseWheel(true)
  popup:SetScript("OnMouseWheel", function(_, delta) dd:scroll(delta) end)
  local pborder = popup:CreateTexture(nil, "BACKGROUND")
  pborder:SetAllPoints(); Theme.tex(pborder, "line")
  local pbg = popup:CreateTexture(nil, "BORDER")
  pbg:SetPoint("TOPLEFT", 1, -1); pbg:SetPoint("BOTTOMRIGHT", -1, 1); Theme.tex(pbg, "cardOff")
  popup:Hide()
  dd.popup = popup

  dd.rows = {}
  for i = 1, vis do
    local row = CreateFrame("Button", nil, popup)
    row:SetSize(width - 8, DD_ROW_H)
    row:SetPoint("TOPLEFT", 4, -4 - (i - 1) * DD_ROW_H)
    local hl = row:CreateTexture(nil, "BACKGROUND")
    hl:SetAllPoints(); Theme.tex(hl, "rowHl"); hl:Hide()
    row.hl = hl
    -- Check icon marks the current option (replaces a ">" text prefix).
    local mark = row:CreateTexture(nil, "OVERLAY")
    mark:SetSize(12, 12); mark:SetPoint("LEFT", 6, 0)
    Icons.setUI(mark, "check", unpack(Theme.color("gold"))); mark:Hide(); row.mark = mark
    local label = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", 24, 0); label:SetJustifyH("LEFT"); Theme.txt(label, "text")
    row.label = label
    row:SetScript("OnClick", function(self) if self.key then dd:select(self.key) end end)
    dd.rows[i] = row
  end

  function dd:currentLabel()
    local cur = opts.getCurrent()
    for _, o in ipairs(opts.getOptions()) do
      if o.key == cur then return o.label end
    end
    return "Select..."
  end

  function dd:updateButton() dd.labelFS:SetText(dd:currentLabel()) end

  function dd:renderRows()
    local options = opts.getOptions()
    local cur = opts.getCurrent()
    local maxOffset = math.max(0, #options - vis)
    dd.scrollOffset = math.min(dd.scrollOffset, maxOffset)
    for i = 1, vis do
      local row = dd.rows[i]
      local o = options[dd.scrollOffset + i]
      if not o then
        row.key, row._selected = nil, false
        row.hl:Hide(); row.mark:Hide(); row:Hide()
      else
        local isSel = (o.key == cur)
        row.key, row._selected = o.key, isSel
        row.label:SetText(o.label)
        if isSel then row.hl:Show(); row.mark:Show() else row.hl:Hide(); row.mark:Hide() end
        row:Show()
      end
    end
  end

  function dd:open()
    dd.isOpen = true
    dd.scrollOffset = 0
    dd:renderRows()
    dd.catcher:Show(); dd.popup:Show()
  end

  function dd:close()
    dd.isOpen = false
    dd.popup:Hide(); dd.catcher:Hide()
  end

  function dd:toggle() if dd.isOpen then dd:close() else dd:open() end end

  function dd:scroll(delta)
    dd.scrollOffset = math.max(0, dd.scrollOffset - delta)
    dd:renderRows()
  end

  function dd:select(key)
    dd:close()
    if opts.onSelect then opts.onSelect(key) end
    dd:updateButton()
  end

  button:SetScript("OnClick", function() dd:toggle() end)
  dd:updateButton()
  return dd
end

-- Reusable faux-pill toggle (no Blizzard art). State is shown three ways at once
-- so it reads unambiguously, including for red-green colourblind users (never
-- colour alone): ON = green track, knob right, "ON" text; OFF = red track, knob
-- left, "OFF" text. getter()/onToggle(bool) wire it to the DB; state lives on the
-- returned table so the runner can drive it under the mock.
local TOGGLE_ON  = { 0.27, 0.55, 0.30, 1 } -- green
local TOGGLE_OFF = { 0.72, 0.27, 0.24, 1 } -- red
local function makeToggle(parent, getter, onToggle)
  local t = {}
  local btn = CreateFrame("Button", nil, parent)
  btn:SetSize(54, 22)
  local track = btn:CreateTexture(nil, "BACKGROUND"); track:SetAllPoints()
  local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  Theme.txt(label, "onDark")
  local thumb = btn:CreateTexture(nil, "ARTWORK"); thumb:SetSize(14, 14)
  t.btn, t.track, t.thumb, t.label = btn, track, thumb, label
  function t:update()
    local on = getter() and true or false
    t.on = on
    track:SetColorTexture(unpack(on and TOGGLE_ON or TOGGLE_OFF))
    -- 14px knob in a 22px track -> 4px gap top/bottom; match it left/right.
    thumb:ClearAllPoints(); thumb:SetPoint(on and "RIGHT" or "LEFT", on and -4 or 4, 0)
    thumb:SetColorTexture(unpack(Theme.color("knob")))
    -- Text sits on the open side of the track, opposite the knob.
    label:ClearAllPoints()
    if on then
      label:SetText("ON"); label:SetPoint("LEFT", 7, 0)
    else
      label:SetText("OFF"); label:SetPoint("RIGHT", -7, 0)
    end
  end
  btn:SetScript("OnClick", function() onToggle(not (getter() and true or false)); t:update() end)
  t:update()
  return t
end

-- Reusable proportional scrollbar for a row-pool list. A faint track with a
-- gold thumb sized by visible/total and positioned by the scroll offset; the
-- thumb is draggable (cursor-delta mapping) and hides itself when everything
-- fits. getCount/getOffset/setOffset wire it to the list's state.
local function makeScrollbar(listFrame, vis, height, getCount, getOffset, setOffset)
  local sb = {}
  local track = listFrame:CreateTexture(nil, "BACKGROUND")
  track:SetPoint("TOPRIGHT", listFrame, "TOPRIGHT", 10, 0)
  track:SetPoint("BOTTOMRIGHT", listFrame, "BOTTOMRIGHT", 10, 0)
  track:SetWidth(6); Theme.tex(track, "divider")
  local thumb = CreateFrame("Button", nil, listFrame)
  thumb:SetWidth(6)
  local tt = thumb:CreateTexture(nil, "ARTWORK"); tt:SetAllPoints(); Theme.tex(tt, "gold")
  sb.track, sb.thumb = track, thumb

  function sb:update()
    local count = getCount() or 0
    local maxOff = math.max(0, count - vis)
    if maxOff <= 0 then track:Hide(); thumb:Hide(); return end
    track:Show(); thumb:Show()
    local th = math.max(16, height * vis / count)
    local off = math.min(getOffset() or 0, maxOff)
    local y = -(off / maxOff) * (height - th)
    thumb:SetSize(6, th)
    thumb:ClearAllPoints(); thumb:SetPoint("TOPRIGHT", listFrame, "TOPRIGHT", 10, y)
    sb.thumbH = th
  end

  thumb:RegisterForDrag("LeftButton")
  thumb:SetScript("OnMouseDown", function()
    local _, sy = GetCursorPosition()
    sb._startY, sb._startOff = sy, getOffset() or 0
    thumb:SetScript("OnUpdate", function()
      -- Self-terminate if the button was released anywhere off the thumb (then
      -- OnMouseUp never fired) -- otherwise the drag leaks and the list would
      -- keep scrolling as the mouse merely moves over it.
      if IsMouseButtonDown and not IsMouseButtonDown("LeftButton") then
        thumb:SetScript("OnUpdate", nil); return
      end
      local _, cy = GetCursorPosition()
      local scale = UIParent:GetEffectiveScale() or 1
      local dy = (sb._startY - cy) / scale
      local count = getCount() or 0
      local maxOff = math.max(0, count - vis)
      local th = math.max(16, height * vis / count)
      local range = math.max(1, height - th)
      local off = math.floor(sb._startOff + (dy / range) * maxOff + 0.5)
      setOffset(math.max(0, math.min(maxOff, off)))
    end)
  end)
  thumb:SetScript("OnMouseUp", function() thumb:SetScript("OnUpdate", nil) end)

  sb:update()
  return sb
end

-- Flat button matching the cream/gold palette (replaces the red Blizzard
-- UIPanelButtonTemplate). primary=true -> gold fill; otherwise cream. Returns a
-- plain Button; the caller sets size/point/OnClick.
-- iconName (optional): show a centered Lucide icon instead of text (e.g. a
-- minus/plus stepper or a trash button), tinted dark to match the button text.
local function makeFlatButton(parent, text, primary, iconName)
  local b = CreateFrame("Button", nil, parent)
  local border = b:CreateTexture(nil, "BACKGROUND"); border:SetAllPoints(); Theme.tex(border, "line")
  -- Fill on the BORDER layer (one above BACKGROUND) so it always draws over the
  -- outline regardless of texture creation order.
  local fill = b:CreateTexture(nil, "BORDER")
  fill:SetPoint("TOPLEFT", 1, -1); fill:SetPoint("BOTTOMRIGHT", -1, 1)
  -- Gold buttons (primary = deeper gold, secondary = lighter gold), themed.
  local baseRole = primary and "btnPrimary" or "btnSecondary"
  local hovRole = primary and "btnPrimaryHover" or "btnSecondaryHover"
  Theme.tex(fill, baseRole) -- registered so it repaints on a theme change
  local fs = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  fs:SetPoint("CENTER"); fs:SetText(text)
  Theme.txt(fs, "btnText")
  if iconName then
    fs:SetText("")
    local ic = b:CreateTexture(nil, "OVERLAY"); ic:SetPoint("CENTER"); ic:SetSize(13, 13)
    Icons.setUI(ic, iconName, unpack(Theme.color("btnText")))
    b.icon = ic
  end
  b:SetScript("OnEnter", function() fill:SetColorTexture(unpack(Theme.color(hovRole))) end)
  b:SetScript("OnLeave", function() fill:SetColorTexture(unpack(Theme.color(baseRole))) end)
  b.fill, b.label = fill, fs
  return b
end

-- Flat cream input box (replaces the gray Blizzard InputBoxTemplate skin).
local function makeFlatEditBox(parent)
  local eb = CreateFrame("EditBox", nil, parent)
  eb:SetAutoFocus(false)
  eb:SetFontObject("GameFontHighlight"); Theme.txt(eb, "text"); eb:SetTextInsets(8, 8, 0, 0)
  eb:SetScript("OnEscapePressed", eb.ClearFocus)
  local bd = eb:CreateTexture(nil, "BACKGROUND"); bd:SetAllPoints(); Theme.tex(bd, "line")
  local fl = eb:CreateTexture(nil, "BORDER")
  fl:SetPoint("TOPLEFT", 1, -1); fl:SetPoint("BOTTOMRIGHT", -1, 1); Theme.tex(fl, "cardOff")
  return eb
end

-- Flat cream checkbox (replaces UICheckButtonTemplate). A gold inner square
-- shows when checked; exposes GetChecked/SetChecked like a CheckButton.
local function makeFlatCheck(parent)
  local c = CreateFrame("Button", nil, parent)
  c:SetSize(18, 18)
  local bd = c:CreateTexture(nil, "BACKGROUND"); bd:SetAllPoints(); Theme.tex(bd, "line")
  local fl = c:CreateTexture(nil, "BORDER")
  fl:SetPoint("TOPLEFT", 1, -1); fl:SetPoint("BOTTOMRIGHT", -1, 1); Theme.tex(fl, "cardOff")
  local mark = c:CreateTexture(nil, "OVERLAY")
  mark:SetPoint("CENTER"); mark:SetSize(16, 16); Icons.setUI(mark, "check", unpack(Theme.color("gold"))); mark:Hide()
  c._checked = false
  function c:GetChecked() return self._checked end
  function c:SetChecked(v) self._checked = v and true or false; if self._checked then mark:Show() else mark:Hide() end end
  c:SetScript("OnClick", function(self) self:SetChecked(not self._checked) end)
  return c
end

-- Settings redesign (ADR-0005, Approach B): a dark header, a persistent left
-- sidebar (title + subtitle per section, active item marked with a gold bar +
-- lighter background), and a content area on the right hosting one panel at a
-- time. 3S-1 builds this chrome; each section's content is restyled in 3S-2..6.
-- Skin is the approximation pass: solid-colour textures only, no bundled art.
local TABS = {
  { key = "location",      label = "Location",      sub = "Pick where you are" },
  { key = "calculation",   label = "Calculation",   sub = "Method & Asr" },
  { key = "notifications", label = "Notifications", sub = "Alerts & sound" },
}

-- Switch sections: show one panel, hide the others, and mark the active sidebar
-- item (gold bar + lighter background). Runner-drivable via IsShown().
function Picker.showTab(name)
  if not Picker.panels then return end
  if not Picker.panels[name] then name = "location" end
  Picker.activeTab = name
  for key, panel in pairs(Picker.panels) do
    if key == name then panel:Show() else panel:Hide() end
  end
  if Picker.navItems then
    for key, n in pairs(Picker.navItems) do
      if key == name then n.hl:Show(); n.bar:Show() else n.hl:Hide(); n.bar:Hide() end
    end
  end
end

-- Compact "City . Country" for the header. Reuses the selection model.
function Picker.headerLocationText(db)
  local sel = Selection.get(db)
  if not sel then return "Rotterdam" end
  if sel.kind == "city" then
    local c = Cities.findByName(sel.name)
    return c and (c.name .. " \194\183 " .. c.country) or sel.name
  elseif sel.kind == "saved" then
    return sel.name
  end
  return "Manual location"
end

function Picker.create()
  if Picker.frame then return Picker.frame end

  local f = CreateFrame("Frame", "PrayerTimesPicker", UIParent)
  f:SetSize(660, 560)
  f:SetPoint("CENTER", UIParent, "CENTER", 150, 0)
  f:SetFrameStrata("DIALOG")
  f:SetMovable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", f.StopMovingOrSizing)

  local bg = f:CreateTexture(nil, "BACKGROUND")
  bg:SetAllPoints(); Theme.tex(bg, "bg")

  -- Dark header bar: wordmark + "SETTINGS", current location (right), close X.
  local header = f:CreateTexture(nil, "BACKGROUND")
  header:SetPoint("TOPLEFT", 0, 0); header:SetPoint("TOPRIGHT", 0, 0); header:SetHeight(46)
  Theme.tex(header, "header")

  -- Header text: wordmark | SETTINGS (left), current location (right), close X.
  -- All three are the same font size and vertically centred on the header,
  -- in line with the X button. A thin vertical bar separates the wordmark.
  local HFONT = 14
  local wm = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  wm:SetFont("Fonts\\FRIZQT__.TTF", HFONT, "")
  wm:SetPoint("LEFT", header, "LEFT", 16, 0); wm:SetText("MyPrayerTimes"); Theme.txt(wm, "gold")

  local wmBar = f:CreateTexture(nil, "OVERLAY")
  wmBar:SetSize(1, 16); wmBar:SetPoint("LEFT", wm, "RIGHT", 9, 0); Theme.tex(wmBar, "line")

  local wmSub = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  wmSub:SetFont("Fonts\\FRIZQT__.TTF", HFONT, "")
  wmSub:SetPoint("LEFT", wmBar, "RIGHT", 9, 0); wmSub:SetText("SETTINGS"); Theme.txt(wmSub, "dimText")

  local x = CreateFrame("Button", nil, f)
  x:SetSize(26, 26); x:SetPoint("RIGHT", header, "RIGHT", -10, 0)
  local xi = x:CreateTexture(nil, "OVERLAY"); xi:SetPoint("CENTER"); xi:SetSize(16, 16)
  Icons.setUI(xi, "close", unpack(Theme.color("gold")))
  x:SetScript("OnEnter", function() xi:SetVertexColor(1, 0.9, 0.6) end)
  x:SetScript("OnLeave", function() xi:SetVertexColor(unpack(Theme.color("gold"))) end)
  x:SetScript("OnClick", function() Picker.close() end)

  Picker.headerLoc = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  Picker.headerLoc:SetFont("Fonts\\FRIZQT__.TTF", HFONT, "")
  Picker.headerLoc:SetPoint("RIGHT", x, "LEFT", -12, 0); Theme.txt(Picker.headerLoc, "gold")

  -- Left sidebar. On the BORDER layer (above the dark BACKGROUND fill) so the
  -- cream always draws over it -- same deterministic fix as the other panels.
  local side = f:CreateTexture(nil, "BORDER")
  side:SetPoint("TOPLEFT", 0, -46); side:SetPoint("BOTTOMLEFT", 0, 0); side:SetWidth(188)
  Theme.tex(side, "sidebar")

  -- Branding logo above the footer text. Full coin + banner art (the minimap
  -- button uses the same Media/logo.tga, circle-masked); shown unmasked here so
  -- the "MyPrayerTimes" banner reads. Centred in the 188px sidebar.
  local logo = f:CreateTexture(nil, "ARTWORK")
  logo:SetSize(120, 120)
  logo:SetPoint("BOTTOM", f, "BOTTOMLEFT", 94, 46)
  logo:SetTexture("Interface\\AddOns\\MyPrayerTimes\\Media\\logo.tga")

  -- Sidebar footer: addon version + author, centred under the logo. Retail moved GetAddOnMetadata into
  -- the C_AddOns namespace (the global was removed); Classic still has the
  -- global. Prefer the namespaced one, fall back to the global, then a literal.
  local getMeta = (C_AddOns and C_AddOns.GetAddOnMetadata) or GetAddOnMetadata
  local ver = (getMeta and getMeta("MyPrayerTimes", "Version")) or "1.0.2"
  local author = f:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
  author:SetPoint("BOTTOM", f, "BOTTOMLEFT", 94, 24); author:SetText("MyPrayerTimes v" .. ver); Theme.txt(author, "muted")
  local credit = f:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
  credit:SetPoint("BOTTOM", f, "BOTTOMLEFT", 94, 10); credit:SetText("by Morhak"); Theme.txt(credit, "muted")

  -- Sidebar nav items + content panels.
  Picker.navItems, Picker.tabButtons, Picker.panels = {}, {}, {}
  for i, t in ipairs(TABS) do
    local btn = CreateFrame("Button", nil, f)
    btn:SetSize(188, 58)
    btn:SetPoint("TOPLEFT", 0, -46 - (i - 1) * 58)
    local hl = btn:CreateTexture(nil, "BACKGROUND")
    hl:SetAllPoints(); Theme.tex(hl, "navHl"); hl:Hide()
    local bar = btn:CreateTexture(nil, "ARTWORK")
    bar:SetPoint("TOPLEFT", 0, 0); bar:SetPoint("BOTTOMLEFT", 0, 0); bar:SetWidth(4)
    Theme.tex(bar, "gold"); bar:Hide()
    local title = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", 16, -12); title:SetText(t.label); Theme.txt(title, "navText")
    local sub = btn:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    sub:SetPoint("TOPLEFT", 16, -30); sub:SetText(t.sub); Theme.txt(sub, "navSub")
    btn:SetScript("OnClick", function() Picker.showTab(t.key) end)
    Picker.navItems[t.key] = { btn = btn, hl = hl, bar = bar }
    Picker.tabButtons[t.key] = btn -- back-compat alias

    local panel = CreateFrame("Frame", nil, f)
    panel:SetPoint("TOPLEFT", f, "TOPLEFT", 189, -46)
    panel:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
    Picker.panels[t.key] = panel
  end
  local locP, calcP, notifP = Picker.panels.location, Picker.panels.calculation, Picker.panels.notifications

  -- Thin divider between the cream sidebar and the cream content.
  local vdiv = f:CreateTexture(nil, "ARTWORK")
  vdiv:SetPoint("TOPLEFT", 188, -46); vdiv:SetPoint("BOTTOMLEFT", 188, 0)
  vdiv:SetWidth(1); Theme.tex(vdiv, "divider")

  -- ===== Location tab (master-detail: country -> city, search, card) =====
  local MVIS, DVIS, RH = 20, 18, 18
  local locBg = locP:CreateTexture(nil, "BACKGROUND")
  locBg:SetAllPoints(); Theme.tex(locBg, "content")

  -- Legacy indicator kept (hidden) so updateSelected + older tests still work.
  Picker.selectedLabel = locP:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  Picker.selectedLabel:SetPoint("TOPLEFT", 0, 0); Picker.selectedLabel:Hide()

  -- Current-location card. (Panel content uses a consistent 10px left/right
  -- margin: card, search, lists, the add form and its buttons all line up.)
  -- ARTWORK (above the BACKGROUND tab fill) so the dark card always shows.
  local card = locP:CreateTexture(nil, "ARTWORK")
  card:SetPoint("TOPLEFT", 10, -4); card:SetSize(451, 46); Theme.tex(card, "card")
  local cl = locP:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  cl:SetPoint("TOPLEFT", 20, -10); cl:SetText("CURRENT LOCATION"); Theme.txt(cl, "gold")
  Picker.cardCity = locP:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  Picker.cardCity:SetPoint("TOPLEFT", 20, -24); Theme.txt(Picker.cardCity, "onDark")
  Picker.cardCountry = locP:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  Picker.cardCountry:SetPoint("TOPRIGHT", -16, -26); Theme.txt(Picker.cardCountry, "dimText")

  -- Search across all cities -- a flat cream field (no Blizzard InputBox skin)
  -- with a placeholder shown while empty.
  local search = CreateFrame("EditBox", "PrayerTimesPickerSearch", locP)
  search:SetSize(451, 24); search:SetPoint("TOPLEFT", 10, -58); search:SetAutoFocus(false)
  search:SetFontObject("GameFontHighlight"); Theme.txt(search, "text")
  search:SetTextInsets(10, 10, 0, 0)
  search:SetScript("OnEscapePressed", search.ClearFocus)
  local sborder = search:CreateTexture(nil, "BACKGROUND"); sborder:SetAllPoints(); Theme.tex(sborder, "line")
  local sfill = search:CreateTexture(nil, "BORDER")
  sfill:SetPoint("TOPLEFT", 1, -1); sfill:SetPoint("BOTTOMRIGHT", -1, 1); Theme.tex(sfill, "cardOff")
  local ph = search:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
  ph:SetPoint("LEFT", 10, 0); ph:SetText("Search all cities..."); Theme.txt(ph, "muted")
  search:SetScript("OnTextChanged", function(self)
    ph:SetShown(self:GetText() == "")
    Picker.dScroll = 0; Picker.refreshLocation(self:GetText())
  end)
  Picker.searchBox = search

  -- Browse container (master + detail). Hidden as a unit when the add-custom
  -- form is open, so nothing shows through behind the form.
  local browse = CreateFrame("Frame", nil, locP)
  browse:SetAllPoints(locP)
  Picker.browse = browse

  -- Master column: My Cities + countries (with counts).
  local mlist = CreateFrame("Frame", nil, browse)
  mlist:SetPoint("TOPLEFT", 10, -90); mlist:SetSize(186, MVIS * RH)
  mlist:EnableMouseWheel(true); mlist:SetScript("OnMouseWheel", function(_, d) Picker.scrollMaster(d) end)
  Picker.masterPool = {}
  for i = 1, MVIS do
    local row = CreateFrame("Button", nil, mlist)
    row:SetSize(186, RH); row:SetPoint("TOPLEFT", 0, -(i - 1) * RH)
    local hl = row:CreateTexture(nil, "BACKGROUND")
    hl:SetAllPoints(); Theme.tex(hl, "rowHl"); hl:Hide(); row.hl = hl
    local label = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", 8, 0); label:SetPoint("RIGHT", row, "RIGHT", -24, 0)
    label:SetJustifyH("LEFT"); label:SetWordWrap(false); Theme.txt(label, "text"); row.label = label
    local count = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    count:SetPoint("RIGHT", -8, 0); row.count = count
    local del = makeFlatButton(row, "", false, "trash")
    del:SetSize(18, 16); del:SetPoint("RIGHT", -4, 0); del:Hide(); row.delBtn = del
    row:SetScript("OnClick", function(self)
      if self.kind == "country" then Picker.selectCountry(self.country)
      elseif self.kind == "saved" then Picker.selectSaved(self.name) end
    end)
    Picker.masterPool[i] = row
  end
  Picker.masterSB = makeScrollbar(mlist, MVIS, MVIS * RH,
    function() return #(Picker.masterData or {}) end,
    function() return Picker.mScroll or 0 end,
    function(o) Picker.mScroll = o; Picker.refreshMaster() end)

  -- Detail column: cities of the selected country (or search results). Pushed
  -- clear of the master scrollbar (no separator line between the columns).
  Picker.detailHeader = browse:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  Picker.detailHeader:SetPoint("TOPLEFT", 230, -92)
  local dlist = CreateFrame("Frame", nil, browse)
  dlist:SetPoint("TOPLEFT", 230, -110); dlist:SetSize(221, DVIS * RH)
  dlist:EnableMouseWheel(true); dlist:SetScript("OnMouseWheel", function(_, d) Picker.scrollDetail(d) end)
  Picker.detailPool = {}
  for i = 1, DVIS do
    local row = CreateFrame("Button", nil, dlist)
    row:SetSize(221, RH); row:SetPoint("TOPLEFT", 0, -(i - 1) * RH)
    local hl = row:CreateTexture(nil, "BACKGROUND")
    hl:SetAllPoints(); Theme.tex(hl, "rowHl"); hl:Hide(); row.hl = hl
    local mark = row:CreateTexture(nil, "OVERLAY")
    mark:SetSize(16, 16); mark:SetPoint("RIGHT", -6, 0)
    Icons.setUI(mark, "check", unpack(Theme.color("gold"))); mark:Hide(); row.mark = mark
    local label = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", 10, 0); label:SetJustifyH("LEFT"); Theme.txt(label, "text"); row.label = label
    row:SetScript("OnClick", function(self) if self.name then Picker.selectCity(self.name) end end)
    Picker.detailPool[i] = row
  end
  Picker.detailSB = makeScrollbar(dlist, DVIS, DVIS * RH,
    function() return #(Picker.detailData or {}) end,
    function() return Picker.dScroll or 0 end,
    function(o) Picker.dScroll = o; Picker.refreshDetail() end)

  -- Global "Add custom location": a full-width bar below BOTH columns, with a
  -- separator, so it reads as "enter your own coordinates" rather than "add a
  -- city to the selected country".
  local addBtn = makeFlatButton(browse, "+ Add custom location", true)
  addBtn:SetHeight(26)
  addBtn:SetPoint("BOTTOMLEFT", browse, "BOTTOMLEFT", 10, 10)
  addBtn:SetPoint("BOTTOMRIGHT", browse, "BOTTOMRIGHT", -10, 10)
  addBtn:SetScript("OnClick", function() Picker.openAddPanel() end)
  local addSep = browse:CreateTexture(nil, "ARTWORK")
  addSep:SetPoint("BOTTOMLEFT", addBtn, "TOPLEFT", 0, 10); addSep:SetPoint("BOTTOMRIGHT", addBtn, "TOPRIGHT", 0, 10)
  addSep:SetHeight(1); Theme.tex(addSep, "divider")

  -- Add-custom-location form (overlay; logic unchanged from 3R-3). Opaque cream
  -- background, raised above the browse container so nothing shows through.
  -- Flush to the panel so the form's fields share the same 10px margins as the
  -- card/search above (the cream bg matches the tab, so flush is invisible).
  local addPanel = CreateFrame("Frame", nil, locP)
  addPanel:SetPoint("TOPLEFT", 0, -86); addPanel:SetPoint("BOTTOMRIGHT", 0, 8)
  addPanel:SetFrameLevel(locP:GetFrameLevel() + 10)
  local apbg = addPanel:CreateTexture(nil, "BACKGROUND"); apbg:SetAllPoints(); Theme.tex(apbg, "content")
  addPanel:Hide(); Picker.addPanel = addPanel

  local at = addPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  at:SetPoint("TOPLEFT", 10, -8); at:SetText("ADD CUSTOM LOCATION"); Theme.txt(at, "gold")

  -- Coordinates row, spread across the width; EU DST to the right. Labels sit
  -- directly above their boxes (10 / 96 / 182).
  makeColLabel(addPanel, "Lat", 10, -32)
  makeColLabel(addPanel, "Lon", 96, -32)
  makeColLabel(addPanel, "UTC+/-", 182, -32)
  local boxY = -46
  local latBox = makeFlatEditBox(addPanel)
  latBox:SetSize(76, 22); latBox:SetPoint("TOPLEFT", 10, boxY)
  local lonBox = makeFlatEditBox(addPanel)
  lonBox:SetSize(76, 22); lonBox:SetPoint("TOPLEFT", 96, boxY)
  local offBox = makeFlatEditBox(addPanel)
  offBox:SetSize(62, 22); offBox:SetPoint("TOPLEFT", 182, boxY)
  local euCheck = makeFlatCheck(addPanel)
  euCheck:SetPoint("TOPLEFT", 258, boxY - 2)
  Picker.euCheck = euCheck
  local euText = addPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  euText:SetPoint("LEFT", euCheck, "RIGHT", 6, 0); euText:SetText("EU DST"); Theme.txt(euText, "text")

  -- Name on its own full-width line.
  local nameLabelY = boxY - 30
  makeColLabel(addPanel, "Name", 10, nameLabelY)
  local nameBox = makeFlatEditBox(addPanel)
  nameBox:SetHeight(22)
  nameBox:SetPoint("TOPLEFT", 10, nameLabelY - 16); nameBox:SetPoint("RIGHT", addPanel, "RIGHT", -10, 0)
  Picker.nameBox, Picker.latBox, Picker.lonBox, Picker.offsetBox = nameBox, latBox, lonBox, offBox

  local clearErr = function() Picker.clearError() end
  nameBox:SetScript("OnTextChanged", clearErr)
  latBox:SetScript("OnTextChanged", clearErr)
  lonBox:SetScript("OnTextChanged", clearErr)
  offBox:SetScript("OnTextChanged", clearErr)

  -- Buttons spread across the width: Save (left), Use once (mid), Back (right).
  local btnY = nameLabelY - 46
  local saveBtn = makeFlatButton(addPanel, "Save as My City", true)
  saveBtn:SetSize(170, 26); saveBtn:SetPoint("TOPLEFT", 10, btnY)
  saveBtn:SetScript("OnClick", function()
    local ok = Picker.saveManual(nameBox:GetText(), latBox:GetText(), lonBox:GetText(),
      offBox:GetText(), euCheck:GetChecked())
    if ok then Picker.closeAddPanel() end
  end)
  local backBtn = makeFlatButton(addPanel, "Back")
  backBtn:SetSize(90, 26); backBtn:SetPoint("TOPRIGHT", addPanel, "TOPRIGHT", -10, btnY)
  backBtn:SetScript("OnClick", function() Picker.closeAddPanel() end)
  local useBtn = makeFlatButton(addPanel, "Use once")
  useBtn:SetSize(120, 26); useBtn:SetPoint("LEFT", saveBtn, "RIGHT", 10, 0)
  useBtn:SetScript("OnClick", function()
    local ok = Picker.applyManual(latBox:GetText(), lonBox:GetText(), offBox:GetText())
    if ok then Picker.closeAddPanel() end
  end)

  -- Tab order within the add form: Lat -> Lon -> UTC -> Name -> Lat.
  local function tabTo(a, b) a:SetScript("OnTabPressed", function() b:SetFocus() end) end
  tabTo(latBox, lonBox); tabTo(lonBox, offBox); tabTo(offBox, nameBox); tabTo(nameBox, latBox)

  Picker.errorLabel = addPanel:CreateFontString(nil, "OVERLAY", "GameFontRed")
  Picker.errorLabel:SetPoint("TOPLEFT", 10, btnY - 24)

  -- Short how-to for the empty space below the form: getting exact coordinates
  -- from Google Maps (mirrors the wizard's custom-location help).
  local helpHead = addPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  helpHead:SetPoint("TOPLEFT", 10, btnY - 52); helpHead:SetText("HOW TO FIND COORDINATES")
  Theme.txt(helpHead, "gold")
  local help = addPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  help:SetPoint("TOPLEFT", 10, btnY - 72); help:SetPoint("RIGHT", addPanel, "RIGHT", -10, 0)
  help:SetJustifyH("LEFT"); help:SetJustifyV("TOP"); help:SetSpacing(3)
  help:SetText(
    "1. Open Google Maps and right-click your city (or search for it).\n"
    .. "2. Click the latitude, longitude numbers at the top of the menu \226\128\148 "
    .. "the first number is Lat, the second is Lon.\n"
    .. "3. UTC+/- is your offset from GMT (Central Europe = 1). Tick EU DST if "
    .. "your country follows European summer time.")
  Theme.txt(help, "muted")

  -- ===== Calculation tab (method dropdown + Asr description cards) =====
  local calcBg = calcP:CreateTexture(nil, "BACKGROUND")
  calcBg:SetAllPoints(); Theme.tex(calcBg, "content")

  local mLabel = calcP:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  mLabel:SetPoint("TOPLEFT", 10, -16); mLabel:SetText("CALCULATION METHOD"); Theme.txt(mLabel, "gold")

  Picker.methodDropdown = makeDropdown(calcP, {
    width = 451, rows = 10,
    getOptions = function() return Methods.list() end,
    getCurrent = function() return Methods.resolveMethod(Picker.db and Picker.db.method) end,
    onSelect = function(key) Picker.setMethod(key) end,
  })
  Picker.methodDropdown.button:SetPoint("TOPLEFT", 10, -34)

  local aLabel = calcP:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  aLabel:SetPoint("TOPLEFT", 10, -78); aLabel:SetText("ASR SCHOOL"); Theme.txt(aLabel, "gold")

  -- Two selectable description cards, one per school (mutually exclusive).
  local ASR_DESC = {
    shafi = "Shafi'i, Maliki, Hanbali -- shadow = object length.",
    hanafi = "Shadow = twice the object length.",
  }
  Picker.asrCards = {}
  local cardW = 220
  for i, a in ipairs(Methods.asrList()) do
    local card = CreateFrame("Button", nil, calcP)
    card:SetSize(cardW, 72); card:SetPoint("TOPLEFT", 10 + (i - 1) * (cardW + 11), -96)
    card.key = a.key
    local cbg = card:CreateTexture(nil, "BACKGROUND"); cbg:SetAllPoints(); Theme.tex(cbg, "cardOff")
    card.bg = cbg
    local barT = card:CreateTexture(nil, "ARTWORK")
    barT:SetPoint("TOPLEFT", 0, 0); barT:SetPoint("BOTTOMLEFT", 0, 0); barT:SetWidth(3)
    Theme.tex(barT, "gold"); barT:Hide(); card.border = barT
    local ct = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    ct:SetPoint("TOPLEFT", 12, -12); ct:SetText(a.label); Theme.txt(ct, "text"); card.title = ct
    local cd = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    cd:SetPoint("TOPLEFT", 12, -32); cd:SetWidth(cardW - 24); cd:SetJustifyH("LEFT")
    cd:SetText(ASR_DESC[a.key] or ""); Theme.txt(cd, "cardDescOff"); card.desc = cd
    card:SetScript("OnClick", function() Picker.setMadhab(a.key) end)
    Picker.asrCards[i] = card
  end

  -- ===== Notifications tab (stepper + toggle switches) =====
  local notifBg = notifP:CreateTexture(nil, "BACKGROUND")
  notifBg:SetAllPoints(); Theme.tex(notifBg, "content")

  -- Row helper: title + description, returns the row's y for the control.
  local function notifRow(y, title, desc)
    local t = notifP:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    t:SetPoint("TOPLEFT", 10, y); t:SetText(title); Theme.txt(t, "text")
    local d = notifP:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    d:SetPoint("TOPLEFT", 10, y - 18); d:SetWidth(300); d:SetJustifyH("LEFT")
    d:SetText(desc); Theme.txt(d, "muted")
  end
  local function separator(y)
    local s = notifP:CreateTexture(nil, "ARTWORK")
    s:SetPoint("TOPLEFT", 10, y); s:SetPoint("TOPRIGHT", -10, y); s:SetHeight(1)
    Theme.tex(s, "divider")
  end

  local nlabel = notifP:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  nlabel:SetPoint("TOPLEFT", 10, -16); nlabel:SetText("REMINDER BEFORE PRAYER"); Theme.txt(nlabel, "gold")

  -- Before-prayer minutes stepper.
  notifRow(-36, "Alert before each prayer", "Applies to all five daily prayers. Set to Off to disable.")
  local minusBtn = makeFlatButton(notifP, "", false, "minus")
  minusBtn:SetSize(28, 24); minusBtn:SetPoint("TOPRIGHT", -114, -34)
  minusBtn:SetScript("OnClick", function() Picker.stepBeforeMinutes(-1) end)
  Picker.beforeValue = notifP:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  Picker.beforeValue:SetPoint("TOPRIGHT", -50, -40); Picker.beforeValue:SetWidth(58); Picker.beforeValue:SetJustifyH("CENTER")
  Theme.txt(Picker.beforeValue, "text")
  local plusBtn = makeFlatButton(notifP, "", false, "plus")
  plusBtn:SetSize(28, 24); plusBtn:SetPoint("TOPRIGHT", -10, -34)
  plusBtn:SetScript("OnClick", function() Picker.stepBeforeMinutes(1) end)

  separator(-84)

  -- Alert exactly at prayer time.
  notifRow(-100, "Alert exactly at prayer time", "Fire a notice the moment each prayer enters.")
  Picker.atToggle = makeToggle(notifP,
    function() return Picker.db and Picker.db.notify and Picker.db.notify.atTime end,
    function(v) Picker.setAtTime(v) end)
  Picker.atToggle.btn:SetPoint("TOPRIGHT", -10, -100)

  separator(-148)

  -- Notification sound.
  notifRow(-164, "Notification sound", "Play a chime with each alert.")
  Picker.soundToggle = makeToggle(notifP,
    function() return Picker.db and Picker.db.notify and Picker.db.notify.sound ~= false end,
    function(v) Picker.setSound(v) end)
  Picker.soundToggle.btn:SetPoint("TOPRIGHT", -10, -164)

  separator(-212)

  -- Appearance: dark-theme toggle (ON = dark). Flipping it re-themes everything
  -- live via Theme.set's hooks.
  notifRow(-228, "Dark theme", "Use a dark colour palette across the addon.")
  Picker.themeToggle = makeToggle(notifP,
    function() return Theme.isDark() end,
    function(v) Theme.set(v and "dark" or "light") end)
  Picker.themeToggle.btn:SetPoint("TOPRIGHT", -10, -228)

  Picker.frame = f
  Picker.mScroll, Picker.dScroll = 0, 0
  Picker.showTab("location")
  Picker.updateSelected()
  Picker.updateNotifyControls()
  Picker.updateCalcControls()
  Picker.refreshLocation("")
  return f
end

function Picker.open()
  Picker.create()
  Picker.clearError()
  Picker.showTab(Picker.activeTab or "location")
  Picker.updateSelected()
  Picker.updateNotifyControls()
  Picker.updateCalcControls()
  Picker.closeAddPanel()
  Picker.refreshLocation(Picker.searchBox and Picker.searchBox:GetText() or "")
  Picker.frame:Show()
end

function Picker.close()
  if Picker.frame then Picker.frame:Hide() end
end

function Picker.toggle()
  if Picker.frame and Picker.frame:IsShown() then Picker.close() else Picker.open() end
end

-- Repaint the non-registry, state-dependent bits on a theme change: re-run the
-- updaters that read Theme.color live (selection card, calc cards, notify
-- toggles, the city lists). The static chrome is repainted by Theme.apply's
-- registry; this hook covers the dynamic colours.
function Picker.applyTheme()
  if not Picker.frame then return end
  if Picker.updateCalcControls then Picker.updateCalcControls() end
  if Picker.updateNotifyControls then Picker.updateNotifyControls() end
  if Picker.refreshLocation then Picker.refreshLocation() end
  Picker.updateSelected()
end
Theme.addHook(function() Picker.applyTheme() end)

-- Styled component factories, exposed so the welcome wizard (ADR-0006) reuses
-- the exact same widgets. Picker.COL is a live proxy onto the Theme palette
-- (kept until the wizard is routed through Theme directly in T-3).
Picker.COL = setmetatable({}, { __index = function(_, role) return Theme.color(role) end })
Picker.ui = {
  flatButton = makeFlatButton,
  flatEditBox = makeFlatEditBox,
  flatCheck = makeFlatCheck,
  scrollbar = makeScrollbar,
  dropdown = makeDropdown,
  toggle = makeToggle,
  colLabel = makeColLabel,
}

if PrayerTimesNS then PrayerTimesNS.modules.Picker = Picker end
return Picker
