Module:MissionaryDailyOpsBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
Undo revision 387459 by NewZimiaGov (talk)
Tag: Undo
Line 1: Line 1:
-- Module:WarLeagueDivisionOpsBVR
-- Module:MissionaryDailyOpsBVR (REWRITE + FLAG RESOLVER + CULT ICONS)
-- Daily, in-world War League divisional operations log.
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Integrates Module:BassaridianDailyEvents by parsing today’s city-event/urgency table output.
-- Deterministic output (no math.random / no os.time seeding).
-- Includes division + city flag icons, with a safe fallback to the Imperial Trade Union flag
--
-- (same behavior as Module:MissionaryDailyOpsBVR).
-- Features:
--  • City profiles (culture/spiritual tags + per-city order bias)
--  • Economic signals parsed from General Port of Lake Morovia "Company Table"
--   (counts by sector + sample HQ companies) and used to bias order + task.
-- • Optional Threat Watch integration from Module:ReligiousEvents (opt-in).
-- • City flags: existence-checked; fallback to Imperial Trade Union flag when missing.
-- • Cult/Order icons: from Module:GeneralPortOperations addLogo(...) mapping.
--
-- Usage:
--  {{#invoke:MissionaryDailyOpsBVR|makeTable}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|showEcon=yes}}
--  {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
--  {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}}


local p = {}
local p = {}
local cal = require('Module:BassaridianCalendar')
local DAYS_IN_YEAR = 183


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (A) PSSC DATE (same logic used by Module:BassaridianDailyEvents)
-- 0) SMALL UTILS
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function getPSSCDate()
local function trim(s)
    local startDate    = os.time({year = 1999, month = 8, day = 6})
if s == nil then return "" end
    local secondsInDay  = 86400
s = tostring(s)
    local daysPerYear  = 183
s = s:gsub("^%s+", ""):gsub("%s+$", "")
    local now          = os.time()
return s
end
 
local function safeTitleContent(titleText)
local ok, t = pcall(mw.title.new, titleText)
if not ok or not t then return nil end
local ok2, content = pcall(function() return t:getContent() end)
if not ok2 then return nil end
return content
end
 
local function normalizeSpaces(s)
return (s or ""):gsub("[%s\u00A0]+", " ")
end


    local totalDays    = math.floor((now - startDate) / secondsInDay)
local function stripWiki(s)
    local yearFraction  = totalDays / daysPerYear
s = tostring(s or "")
    local psscYear      = math.floor(yearFraction)
-- Remove files/templates quickly
    local dayOfYear    = math.floor((yearFraction - psscYear) * daysPerYear) + 1
s = s:gsub("%[%[File:[^%]]+%]%]", "")
s = s:gsub("%{%b{}%}", "")
-- Links: [[A|B]] => B, [[A]] => A
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
-- Bold/italics
s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
return trim(s)
end


    local month, dayOfMonth
local function containsAny(s, words)
    if dayOfYear <= 61 then
s = (s or ""):lower()
        month = 1; dayOfMonth = dayOfYear
for _, w in ipairs(words) do
    elseif dayOfYear <= 122 then
if s:find(w, 1, true) then return true end
        month = 2; dayOfMonth = dayOfYear - 61
end
    else
return false
        month = 3; dayOfMonth = dayOfYear - 122
end
    end


    local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
local function joinTags(tags)
    return {
if not tags or #tags == 0 then return "" end
        text      = dateString,
local out = {}
        year      = psscYear,
for _, t in ipairs(tags) do out[#out+1] = t end
        dayOfYear = dayOfYear,
return table.concat(out, ", ")
        month    = month,
        dom      = dayOfMonth
    }
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (B) HELPERS
-- 1) CALENDAR PARSING
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function pick(t) return t[math.random(#t)] end
local function parseCalendarString(s)
if type(s) ~= "string" then return nil end
local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
if dayOfYear and year then
return tonumber(year), tonumber(dayOfYear), month
end
return nil
end
 
local function extractEventText(s)
if type(s) ~= "string" then return "" end
local ev = s:match("PSSC%s+–%s+(.-)%s+–%s+Proverb:")
return ev or ""
end


local function pickWeighted(list)
local function doyToMonthDom(doy)
    local total = 0
if doy <= 61 then
    for _, it in ipairs(list) do total = total + (it.weight or 1) end
return 1, doy
    local r = math.random() * total
elseif doy <= 122 then
    local acc = 0
return 2, doy - 61
    for _, it in ipairs(list) do
else
        acc = acc + (it.weight or 1)
return 3, doy - 122
        if r <= acc then return it end
end
    end
    return list[#list]
end
end


local function bulletList(items)
local function getDateInfo()
    if not items or #items == 0 then return "" end
local raw = cal.getCurrentDate()
    return "<ul><li>" .. table.concat(items, "</li><li>") .. "</li></ul>"
local y, doy = parseCalendarString(raw)
if not y or not doy then
y, doy = 0, 1
raw = tostring(raw or "")
end
local m, dom = doyToMonthDom(doy)
return {
year = y,
dayOfYear = doy,
monthNum = m,
dom = dom,
text = string.format("%d/%d/%d PSSC", dom, m, y),
raw = raw,
calendarEvent = extractEventText(raw),
}
end
 
--------------------------------------------------------------------------------
-- 2) DETERMINISTIC RNG
--------------------------------------------------------------------------------
local function hash31(str)
local h = 0
for i = 1, #str do
h = (h * 131 + str:byte(i)) % 2147483647
end
return h
end
end


local function cityLink(cityName, anchorOverride)
local function lcg(seed)
    local anchor = anchorOverride or cityName
local s = seed % 2147483647
    return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", anchor, cityName)
if s <= 0 then s = s + 2147483646 end
return function()
s = (1103515245 * s + 12345) % 2147483647
return s
end
end
end


local function opId(di, divCode, idx)
local function pickFromList(nextInt, list, avoidValue)
    return string.format("WL-%03d.%03d-%s-%02d", di.year, di.dayOfYear, divCode, idx)
if #list == 0 then return nil end
if #list == 1 then return list[1] end
local tries = 0
while tries < 12 do
local idx = (nextInt() % #list) + 1
local v = list[idx]
if v ~= avoidValue then return v end
tries = tries + 1
end
for _, v in ipairs(list) do
if v ~= avoidValue then return v end
end
return list[1]
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (C) FLAG FALLBACK (matches the "use ITU flag when missing" pattern)
-- 3) COLOR CODING (Hatch-style)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DEFAULT_FLAG = "Imperial_Trade_Union_flag.png"
local function sevStyle(sev)
 
if sev == "red" then
local function flagIcon(fileName, sizePx)
return ' style="background-color:#f8d7da;"'
    local size = tonumber(sizePx) or 20
elseif sev == "yellow" then
    local f = (fileName and fileName ~= "") and fileName or DEFAULT_FLAG
return ' style="background-color:#fff3cd;"'
    return string.format("[[File:%s|%dpx]]", f, size)
else
return ' style="background-color:#d4edda;"'
end
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (D) INTEGRATE DAILY EVENTS (parse Module:BassaridianDailyEvents.makeTable())
-- 4) CITY NETWORK (canonical list)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function buildEventMapForToday()
local function normalizeForFlag(name)
    local ok, daily = pcall(require, "Module:BassaridianDailyEvents")
return (name or "")
    if not ok or not daily or type(daily.makeTable) ~= "function" then
:gsub("[%s%-’]", "")
         return {}, false
:gsub("[Áá]", "A"):gsub("[Éé]", "E")
    end
:gsub("[Íí]", "I"):gsub("[Óó]", "O")
:gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end
 
local function cityLink(name)
return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", name, name)
end
 
-- 54 canonical cities; tier used for "major city" baseline.
local cityOrder = {
-- Core majors
{name="Vaeringheim",      region="Core", tier="major"},
{name="Luminaria",        region="Core", tier="major"},
{name="Serena",          region="Core", tier="major"},
{name="Pyralis",          region="Core", tier="major"},
{name="Symphonara",      region="Core", tier="major"},
{name="Aurelia",          region="Core", tier="major"},
{name="Somniumpolis",    region="Core", tier="major"},
{name="Nexa",            region="Core", tier="major"},
{name="Lunalis Sancta",  region="Core", tier="major"},
{name="Sylvapolis",      region="Core", tier="major"},
 
-- Core minors
{name="Saluria",          region="Core", tier="minor"},
{name="Aetherium",        region="Core", tier="minor"},
{name="Ferrum Citadel",  region="Core", tier="minor"},
{name="Acheron",          region="Core", tier="minor"},
{name="Erythros",        region="Core", tier="minor"},
{name="Catonis Atrium",  region="Core", tier="minor"},
{name="Delphica",         region="Core", tier="minor"},
{name="Koinonía",        region="Core", tier="minor"},
{name="Aureum",          region="Core", tier="minor"},


    local tbl = daily.makeTable() -- rows: "| date || city || event || urgency"
-- Alperkin
    local eventMap = {}
{name="The Alpazkigz",    region="Alperkin", tier="minor"},
{name="Odiferia",        region="Alperkin", tier="minor"},


    for line in tostring(tbl):gmatch("[^\n]+") do
-- New South Jangsong
        if line:match("^|%s") and not line:match("^|%-%s*$") then
{name="Skýrophos",        region="New South Jangsong", tier="major"},
            local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
{name="Bjornopolis",      region="New South Jangsong", tier="major"},
            if #parts >= 4 then
{name="Aegirheim",       region="New South Jangsong", tier="major"},
                local city = parts[2]
{name="Norsolyra",        region="New South Jangsong", tier="major"},
                local ev  = parts[3]
{name="Thorsalon",        region="New South Jangsong", tier="major"},
                local urg  = parts[4]
{name="Pelagia",          region="New South Jangsong", tier="minor"},
                if city and city ~= "" and not eventMap[city] then
{name="Myrene",          region="New South Jangsong", tier="minor"},
                    eventMap[city] = { desc = ev, urgency = urg }
{name="Thyrea",          region="New South Jangsong", tier="minor"},
                end
{name="Ephyra",           region="New South Jangsong", tier="minor"},
            end
{name="Halicarn",         region="New South Jangsong", tier="minor"},
         end
    end


    return eventMap, true
-- Haifan Bassaridia
end
{name="Keybir-Aviv",      region="Haifan Bassaridia", tier="major"},
{name="Tel-Amin",        region="Haifan Bassaridia", tier="major"},
{name="Diamandis",        region="Haifan Bassaridia", tier="major"},
{name="Jogi",            region="Haifan Bassaridia", tier="major"},
{name="Lewisburg",        region="Haifan Bassaridia", tier="major"},
{name="Thermosalem",      region="Haifan Bassaridia", tier="minor"},
{name="Akróstadium",      region="Haifan Bassaridia", tier="minor"},
{name="Sufriya",          region="Haifan Bassaridia", tier="minor"},
{name="Lykopolis",        region="Haifan Bassaridia", tier="minor"},
 
-- Normark
{name="Ardclach",        region="Normark", tier="major"},
{name="Riddersborg",      region="Normark", tier="major"},
 
-- Ouriana
{name="Bashkim",          region="Ouriana", tier="major"},
{name="Ourid",            region="Ouriana", tier="major"},
{name="Tonar",            region="Ouriana", tier="minor"},
 
-- Caledonia
{name="Fanghorn",        region="Caledonia", tier="major"},
{name="Eikbu",            region="Caledonia", tier="major"},
{name="Galvø",            region="Caledonia", tier="major"},
{name="Sårensby",        region="Caledonia", tier="minor"},
{name="Slevik",          region="Caledonia", tier="minor"},
{name="Sjøsborg",        region="Caledonia", tier="minor"},
{name="Storesund",        region="Caledonia", tier="minor"},
{name="Notranskja",      region="Caledonia", tier="minor"},
{name="Kaledonija",      region="Caledonia", tier="minor"},
}


local function urgencyScore(urg)
local function regionPass(filter, cityRegion)
    if urg == "CRITICAL" then return 4 end
if not filter or filter == "" or filter == "All" then return true end
    if urg == "HIGH"     then return 3 end
if filter == "Core" then return cityRegion == "Core" end
    if urg == "MEDIUM"   then return 2 end
if filter == "NSJ" then return cityRegion == "New South Jangsong" end
    if urg == "LOW"     then return 1 end
if filter == "Haifan" then return cityRegion == "Haifan Bassaridia" end
    return 0
if filter == "Caledonian" then return cityRegion == "Caledonia" end
return cityRegion == filter
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (E) FORCE ORGANIZATION
-- 4.1) CITY FLAGS (existence-checked + fallback)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local GROUND_ECHELONS = {
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"
    { name="Turmae",       weight=5 },
 
    { name="Contubernia",   weight=5 },
local function asciiNormalizeForFlag(name)
    { name="Ordinis",       weight=4 },
return (name or "")
    { name="Manipuli",     weight=3 },
:gsub("[%s%-’]", "")
    { name="Centuriae",     weight=2 },
:gsub("[ÁáÀàÂâÄäÃãÅåĀā]", "A")
    { name="Cohortes",     weight=1 },
:gsub("[ÉéÈèÊêËëĒē]", "E")
:gsub("[ÍíÌìÎîÏïĪī]", "I")
:gsub("[ÓóÒòÔôÖöÕõØøŌō]", "O")
:gsub("[ÚúÙùÛûÜüŪū]", "U")
:gsub("[ÝýŸÿ]", "Y")
:gsub("[Ææ]", "AE")
end
 
local fileExistsCache = {}
local function fileExists(fileName)
if not fileName or fileName == "" then return false end
if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
local ok, t = pcall(mw.title.new, "File:" .. fileName)
local exists = (ok and t and t.exists == true) or false
fileExistsCache[fileName] = exists
return exists
end
 
local citySet = {}
for _, c in ipairs(cityOrder) do citySet[c.name] = true end
 
-- Explicit odd filenames you noted (plus Skyrophos).
local cityFlagOverrides = {
["Skýrophos"] = "SkyrophosFlag.png",
["Sårensby"]  = "SårensbyFlag.png",
["Slevik"]    = "SlevikFlag.png.png",
["Kaledonija"]= "KaledonijaFlag.png",
}
}


local NAVAL_ECHELONS = {
local cityFlagMarkupCache = {}
    { name="Birlik-i Gemi (Patrol Element)", weight=5 },
local function getCityFlagMarkup(cityName)
    { name="Tabur-i Derya (Task Unit)",     weight=3 },
if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end
    { name="Ordu (Task Group)",             weight=1 },
 
-- 1) explicit override
local override = cityFlagOverrides[cityName]
if override and fileExists(override) then
local mk = string.format("[[File:%s|20px]]", override)
cityFlagMarkupCache[cityName] = mk
return mk
end
 
-- 2) try normalized candidates
local norm = normalizeForFlag(cityName)
local ascii = asciiNormalizeForFlag(cityName)
local candidates = {
norm .. "Flag.png",
ascii .. "Flag.png",
}
 
for _, fn in ipairs(candidates) do
if fileExists(fn) then
local mk = string.format("[[File:%s|20px]]", fn)
cityFlagMarkupCache[cityName] = mk
return mk
end
end
 
-- 3) fallback to ITU
local mk = string.format("[[File:%s|20px]]", DEFAULT_CITY_FLAG_FILE)
cityFlagMarkupCache[cityName] = mk
return mk
end
 
--------------------------------------------------------------------------------
-- 5) CORRIDORS / ROLE HINTS (hard-coded)
--------------------------------------------------------------------------------
local RAIL = {
["Lykopolis"]=true, ["Lewisburg"]=true, ["Bashkim"]=true, ["Somniumpolis"]=true,
["Erythros"]=true, ["Vaeringheim"]=true, ["Symphonara"]=true, ["Skýrophos"]=true,
["Norsolyra"]=true, ["Pelagia"]=true, ["Riddersborg"]=true, ["Sårensby"]=true,
}
}
local FERRY = {["Jogi"]=true, ["Ardclach"]=true}


local AIR_ECHELONS = {
--------------------------------------------------------------------------------
     { name="Zeygi (Element)",  weight=5 },
-- 6) MISSIONARY ORGANIZATION (sizes)
    { name="Ptisis (Flight)",   weight=3 },
--------------------------------------------------------------------------------
    { name="Moira (Squadron)", weight=1 },
local org = {
Lampade     = {size=3,  code="L"},
Hetairoi    = {size=6,  code="H"},
Dodekade    = {size=12,  code="D"},
Kleisthenes = {size=25,  code="K"},
Stoa        = {size=50,  code="S"},
Thiasos    = {size=100, code="T"},
}
}


local function pickEchelonForUrgency(echelons, uScore)
local function pickOrgUnit(nextInt, sev)
    local tuned = {}
if sev == "green" then
    for _, e in ipairs(echelons) do
return pickFromList(nextInt, {"Lampade","Hetairoi"}, nil)
        local w = e.weight or 1
elseif sev == "yellow" then
        if uScore >= 3 then
return pickFromList(nextInt, {"Hetairoi","Dodekade","Kleisthenes"}, nil)
            if e.name:match("Cohortes") or e.name:match("Ordu %(") or e.name:match("Moira") then
else
                w = w + 3
return pickFromList(nextInt, {"Kleisthenes","Stoa","Thiasos"}, nil)
            elseif e.name:match("Centuriae") or e.name:match("Tabur%-i Derya") or e.name:match("Ptisis") then
end
                w = w + 2
            end
        elseif uScore == 2 then
            if e.name:match("Centuriae") or e.name:match("Tabur%-i Derya") or e.name:match("Ptisis") then
                w = w + 1
            end
        end
        table.insert(tuned, { name = e.name, weight = w })
    end
    return pickWeighted(tuned).name
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (F) FLAGS (only specify known files; everything else auto-falls back to ITU)
-- 7) ORDERS + TASKS (tagged)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local CITY_FLAGS = {
local function orderLink(pageTitle, anchor, label)
    -- Vaeringheim investor belt
return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
    ["Vaeringheim"]      = "VaeringheimFlag.png",
end
    ["Lunalis Sancta"]  = "LunalisSanctaFlag.png",
 
    ["Luminaria"]        = "LuminariaFlag.png",
local function T(text, sev, tags)
    ["Serena"]          = "SerenaFlag.png",
return {text=text, sev=sev, tags=tags or {}}
    ["Symphonara"]      = "SymphonaraFlag.png",
end
    ["Aurelia"]          = "AureliaFlag.png",
 
    ["Sylvapolis"]      = "SylvapolisFlag.png",
local orders = {
    ["Delphica"]        = "DelphicaFlag.png",
{key="OSI", name="Ordo Solis Invicti",
    ["Catonis Atrium"]  = "CatonisAtriumFlag.png",
link=orderLink("Atos (Reformed Stripping Path)","Ordo_Solis_Invicti","Ordo Solis Invicti"),
    ["Ferrum Citadel"]  = "FerrumCitadelFlag.png",
tasks={
T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education","service"}),
T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine","service"}),
T("Solar route inspection at major junctions; traffic etiquette posted for pilgrims and commuters.","green",{"transit","rail","public"}),
T("Sun-pledge renewal for civic clerks; oath sheets sealed and logged with Temple Bank scribes.","green",{"governance","audit","service"}),
T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit","industry"}),
T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit","service"}),
T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd","public"}),
T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security","crowd"}),
T("Emergency morale procession after night unrest; hymn leadership and calm restoration at transit choke points.","red",{"unrest","crowd","transit"}),
}
},
 
{key="OAM", name="Order of Aurora Mystica",
link=orderLink("Eos (Reformed Stripping Path)","Order_of_Aurora_Mystica","Order of Aurora Mystica"),
tasks={
T("Sunrise vigil and reconciliation rite for competing guilds; signed harmony pledges archived by Temple Bank clerks.","green",{"mediation","guild","service"}),
T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public","service"}),
T("Quiet home-visits to recent migrants; orientation blessings and sanctioned neighborhood guides issued.","green",{"public","service","mediation"}),
T("Rooftop shrine upkeep and dawn-banner replacement; civic lanes kept clear for morning processions.","green",{"shrine","infrastructure","public"}),
T("Pilgrim flow timing review for a dawn festival; route markers placed to prevent crush points.","green",{"festival","crowd","public"}),
T("Civic blessing for new housing blocks; routing of foot-traffic to prevent crush points and misdirection.","yellow",{"crowd","infrastructure","service"}),
T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
T("Dawn mediation under escort after a workplace dispute threatens walkouts; settlement notarized.","yellow",{"mediation","industry","governance"}),
T("Omen-risk advisory for coastal weather or fog days; port stewards briefed and signage posted.","yellow",{"port","public","service"}),
T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.","red",{"unrest","arson","crowd"}),
}
},
 
{key="HSA", name="Harmony Sanctum",
link=orderLink("Micras (Reformed Stripping Path)","Harmony_Sanctum","Harmony Sanctum"),
tasks={
T("Neighborhood mediation session for family disputes and trade-feud spillover; binding accords notarized.","green",{"mediation","service"}),
T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.","green",{"health","rumor","service"}),
T("Queue-discipline coaching at relief points; volunteers trained to prevent confrontations.","green",{"order","queue","relief"}),
T("Workplace concord circle for service districts; complaint channels clarified and posted.","green",{"service","mediation","governance"}),
T("Inter-shrine courtesy visit to reduce rivalry; shared calendar commitments established.","green",{"balance","mediation","public"}),
T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.","yellow",{"audit","governance","service"}),
T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.","yellow",{"schism","containment","public"}),
T("Rumor forensics on a circulating ‘forbidden rite’ claim; witness tree compiled and forwarded.","yellow",{"rumor","investigation","public"}),
T("Crowd-stabilization drill for festival season; steward assignments and safe exits mapped.","yellow",{"festival","crowd","public"}),
T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.","red",{"riot","crowd","public"}),
}
},
 
{key="IGA", name="Ignis Aeternum",
link=orderLink("Pyros (Reformed Stripping Path)","Ignis_Aeternum","Ignis Aeternum"),
tasks={
T("Controlled fire blessing for workshops; safety rite doubled as inspection of fuel and storage.","green",{"industry","safety"}),
T("Kiln-and-forge consecration for a new artisan street; insurance seals verified.","green",{"industry","infrastructure","service"}),
T("Firebreak marking for dry-season hills; public notices issued to farmers and herders.","green",{"agri","fire","public"}),
T("Hearth-blessing round for crowded tenements; cooking hazards corrected with wardens.","green",{"public","safety","service"}),
T("Candle and incense quality check at shrine vendors; adulterants rejected and logged.","green",{"shrine","audit","service"}),
T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.","yellow",{"contam","shrine","service"}),
T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.","yellow",{"arson","investigation","security"}),
T("Explosive-storage compliance check near port or rail yards; seals renewed and routes revised.","yellow",{"rail","port","security","industry"}),
T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.","red",{"arson","crisis","security"}),
T("Post-incident purification circuit across multiple oratories; public confidence restored by nightfall.","red",{"incident","public","crowd"}),
}
},
 
{key="CHS", name="Celestial Harmony Sect",
link=orderLink("Indigo (Reformed Stripping Path)","Celestial_Harmony_Sect","Celestial Harmony Sect"),
tasks={
T("Diplomatic courtesy visit to a city council; alignments review and oath renewal with minimal spectacle.","green",{"diplomacy","governance","service"}),
T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.","green",{"education","astronomy","service"}),
T("Inter-city liaison packet prepared for corridor towns; travel permissions confirmed.","green",{"rail","service","governance"}),
T("Boundary-harmony rite at a district line; neighbor shrines exchange tokens under witness.","green",{"balance","public","mediation"}),
T("Calm briefing for merchant captains on festival timing; reduce congestion and rumor spread.","green",{"port","service","public"}),
T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.","yellow",{"balance","mediation","archive"}),
T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.","yellow",{"rumor","investigation","public"}),
T("Quiet arbitration after contract dispute between a guild and a council office.","yellow",{"service","governance","mediation"}),
T("Transit coordination for high-profile delegates; platform escorts and crowd lanes established.","yellow",{"rail","crowd","security"}),
T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.","red",{"confrontation","crowd","security"}),
}
},
 
{key="GGS", name="Guild of Golden Shadows",
link=orderLink("Chrysos (Reformed Stripping Path)","Guild_of_Golden_Shadows","Guild of Golden Shadows"),
tasks={
T("Discrete audit of offertory ledgers and voucher seals; minor variances reconciled quietly.","green",{"finance","audit","service"}),
T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.","green",{"merchant","security","service"}),
T("Port-side seal verification for sacred cargo; chain-of-custody stamped and witnessed.","green",{"port","finance","security"}),
T("Rail consignment escort paperwork checked; handover points confirmed with station stewards.","green",{"rail","service","security"}),
T("Quiet fraud-prevention briefing for shrine treasurers; common forgery patterns circulated.","green",{"finance","education","service"}),
T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.","yellow",{"forgery","investigation","finance"}),
T("Debt dispute arbitration for a sanctioned charity line; terms rewritten and sealed.","yellow",{"finance","mediation","service"}),
T("Watchlist update for suspected smugglers using festival crowds; surveillance plan filed.","yellow",{"security","crowd","port"}),
T("Recovery detail for stolen reliquary property; surveillance and lawful seizure under civic warrant.","red",{"theft","security","investigation"}),
T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.","red",{"panic","finance","service"}),
}
},
 
{key="ASS", name="Azure Sentinel Sect",
link=orderLink("Thalassa (Reformed Stripping Path)","Azure_Sentinel_Sect","Azure Sentinel Sect"),
tasks={
T("Canal and fountain sanctification circuit; water-quality blessings paired with public reassurance.","green",{"water","public","service"}),
T("Harbor hygiene briefing for fish markets; cold-chain and wash stations checked.","green",{"port","health","service"}),
T("Flood-gate inspection rite; maintenance tickets issued and signed by civic stewards.","green",{"infrastructure","water","service"}),
T("Ferry pier blessing and rope inspection; boarding lines reorganized to reduce accidents.","green",{"ferry","port","public"}),
T("Reservoir vigil for drought days; ration etiquette posted at distribution points.","green",{"water","relief","public"}),
T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.","yellow",{"port","sabotage","security"}),
T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.","yellow",{"flood","infrastructure","public"}),
T("Waterborne rumor containment bulletin; discourage panic buying and hoarding.","yellow",{"rumor","service","public"}),
T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.","red",{"outbreak","water","health"}),
T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.","red",{"infrastructure","crisis","public"}),
}
},
 
{key="REN", name="Reverie Nebulous",
link=orderLink("Nephele (Reformed Stripping Path)","Reverie_Nebulous","Reverie Nebulous"),
tasks={
T("Dream-interpretation clinic to reduce anxiety; rumor narratives redirected into sanctioned symbolism.","green",{"dream","rumor","service"}),
T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.","green",{"health","sleep","service"}),
T("Public guidance sheet on lawful visions vs. sedition pamphlets; distributed at shrines.","green",{"education","public","service"}),
T("Quiet household visits after a death or displacement; stabilize grief cycles with sanctioned imagery.","green",{"service","health","public"}),
T("Festival mood stewardship: prevent illicit revel by offering sanctioned evening rites.","green",{"festival","public","morale"}),
T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.","yellow",{"broadcast","investigation","service"}),
T("Rumor cascade mapping in service districts; identify the first carrier nodes.","yellow",{"rumor","investigation","service"}),
T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.","red",{"panic","crowd","public"}),
T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.","red",{"violence","health","service"}),
T("Emergency night curfew-support rite; safe corridors marked to reduce shadow-fear incidents.","yellow",{"security","public","service"}),
}
},
 
{key="EOF", name="Eon Fellowship",
link=orderLink("Glinos (Reformed Stripping Path)","Eon_Fellowship","Eon Fellowship"),
tasks={
T("Cold-weather relief checks and ration fairness review; storage seals verified.","green",{"relief","logistics","service"}),
T("Infrastructure endurance audit: roofs, walls, and bridges; damage triage list issued.","green",{"infrastructure","audit","industry"}),
T("Shelter staffing rotation planned for storm season; volunteer rosters sealed.","green",{"relief","service","governance"}),
T("Rail convoy support brief for frontier deliveries; pacing and rest points set.","green",{"rail","logistics","service"}),
T("Queue discipline at distribution points; conflict triggers reduced through signage.","green",{"order","queue","public"}),
T("Conflict de-escalation for resource disputes; binding queue rules posted at distribution points.","yellow",{"order","queue","governance"}),
T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.","red",{"storm","relief","public"}),
T("Frontier outpost morale visit; endurance rites and supply accountability review.","yellow",{"frontier","audit","service"}),
T("Bridge closure coordination after damage report; detours posted and escorted.","yellow",{"infrastructure","transit","public"}),
T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.","red",{"frontier","security","logistics"}),
}
},
 
{key="UOR", name="Order of the Umbral Oracle",
link=orderLink("Noctis (Reformed Stripping Path)","Order_of_the_Umbral_Oracle","Order of the Umbral Oracle"),
tasks={
T("Night watch and omen-reading in the temple quarter; guidance slips sealed for officials.","green",{"omen","security","service"}),
T("Shadow-lane safety marking near canals and alleys; lantern placement logged.","green",{"public","security","transit"}),
T("Quiet counsel for a magistrate; truth-testing rite witnessed by two scribes.","green",{"governance","inquiry","service"}),
T("Missing-persons intake desk hosted at a shrine; reports standardized and forwarded.","green",{"service","investigation","public"}),
T("Nighttime rumor monitoring in taverns and stations; early interventions logged.","green",{"rumor","rail","service"}),
T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.","yellow",{"governance","inquiry","audit"}),
T("Shadow-route mapping after disappearances; safe-path markers installed with civic approval.","yellow",{"security","transit","public"}),
T("Evidence sealing after a midnight incident; chain-of-custody verified for Temple authorities.","yellow",{"investigation","security","service"}),
T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.","red",{"demonic","panic","crowd"}),
T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.","red",{"abduction","security","service"}),
}
},
 
{key="MVE", name="Mystery of the Verdant Embrace",
link=orderLink("Opsithe (Reformed Stripping Path)","Mystery_of_the_Verdant_Embrace","Mystery of the Verdant Embrace"),
tasks={
T("Harvest blessing and storage rite; spoilage prevention guidance issued to granaries.","green",{"agriculture","harvest","agri"}),
T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.","green",{"garden","shrine","agri"}),
T("Seed and tool stipend audit for smallholders; vouchers revalidated.","green",{"audit","agri","service"}),
T("Irrigation channel blessing and silt-clearing volunteer day; water etiquette posted.","green",{"water","agri","public"}),
T("Market stall purity checks for produce; vendors instructed on safe storage.","green",{"public","health","agri"}),
T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.","yellow",{"shortage","logistics","agri"}),
T("Pest or blight containment rites; field inspections with farmers’ associations.","yellow",{"blight","agriculture","agri"}),
T("Livestock procession routing for festival markets; prevent crush and theft.","yellow",{"crowd","festival","agri"}),
T("Famine-risk surge response; emergency distribution queues sanctified and monitored.","red",{"famine","relief","agri"}),
T("Emergency containment of contaminated grain rumor; sample testing coordinated with Sanctum Vitalis.","yellow",{"rumor","health","agri"}),
}
},


    -- Alpazkigz belt
{key="CIL", name="Conclave Illuminara",
    ["Pyralis"]          = "PyralisFlag.png",
link=orderLink("Tarsica (Reformed Stripping Path)","Conclave_Illuminara","Conclave Illuminara"),
    ["Nexa"]            = "NexaFlag.png",
tasks={
    ["Saluria"]          = "SaluriaFlag.png",
T("Moonlit instruction circle; eclipse-watch protocol rehearsed with apprentice scribes.","green",{"lunar","education","service"}),
    ["Acheron"]          = "AcheronFlag.png",
T("Night-sky bulletin posted to reduce panic during overcast weeks; lawful interpretations provided.","green",{"public","rumor","service"}),
    ["Koinonía"]        = "KoinoniaFlag.png",
T("Observatory instrument check; calibration seals signed and archived.","green",{"archive","audit","service"}),
    ["Aureum"]          = "AureumFlag.png",
T("Lantern route blessing for tunnels and arcades; wayfinding clarified for late shifts.","green",{"transit","public","service"}),
    ["Aetherium"]        = "AetheriumFlag.png",
T("Quiet archive preservation: scroll humidity checks and secure storage review.","green",{"archive","service","audit"}),
    ["Erythros"]        = "ErythrosFlag.png",
T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.","yellow",{"archive","audit","security"}),
T("Public lantern rite to counter rumor cascades; controlled routes and timing set with wardens.","yellow",{"rumor","crowd","public"}),
T("Transit hub advisory after night panic; crowd lanes re-drawn and posted.","yellow",{"rail","crowd","transit"}),
T("Emergency omen bulletin after night panic; official statement drafted and delivered.","red",{"panic","public","service"}),
T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.","red",{"threat","security","service"}),
}
},


    -- Odiferia
{key="SCV", name="Sanctum Vitalis",
    ["Somniumpolis"]    = "SomniumpolisFlag.png",
link=orderLink("Plateau (Reformed Stripping Path)","Sanctum_Vitalis","Sanctum Vitalis"),
tasks={
T("Clinic day: basic health screenings and hygiene instruction; water points checked.","green",{"health","public","service"}),
T("Sanctioned healer coordination meeting; triage roles rehearsed for outbreaks.","green",{"training","health","service"}),
T("Hygiene signage deployment at markets; handwash stations verified.","green",{"public","health","service"}),
T("Nutrition counseling for ration lines; supplement vouchers distributed under Temple Bank authorization.","green",{"nutrition","relief","service"}),
T("Quiet home checks for vulnerable households; prevent panic spirals after illness rumors.","green",{"service","health","rumor"}),
T("Supplement and ration fortification distribution under Temple Bank authorization.","yellow",{"nutrition","relief","service"}),
T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.","yellow",{"outbreak","training","public"}),
T("Quarantine lane setup drill at a terminal or station; entry control rehearsed.","yellow",{"rail","security","health"}),
T("Active outbreak response: triage lanes established; quarantine support and calm messaging.","red",{"outbreak","crisis","public"}),
T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.","red",{"violence","logistics","service"}),
}
},


    -- Hafaan / Jeseri (if these exist; if not, fallback will apply)
{key="ALB", name="Temple Alabaster",
    ["Hafaan"]          = "HafaanFlag.png",
link=orderLink("Ivory (Reformed Stripping Path)","Temple_Alabaster","Temple Alabaster"),
    ["Jeseri"]          = "JeseriFlag.png",
tasks={
T("Purity inspection of ritual vessels and incense stock; minor replacements approved.","green",{"purity","shrine","service"}),
T("Doctrinal reading circle for novices; sanctioned texts distributed with margin notes.","green",{"education","service","public"}),
T("Shrine inventory check for relic storage; locks and witness rules enforced.","green",{"security","shrine","audit"}),
T("Quiet correction of unsanctioned household rites; guidance issued without spectacle.","green",{"service","public","education"}),
T("Blessing of new white-draped oratory; entry etiquette posted to reduce disorder.","green",{"shrine","public","service"}),
T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.","yellow",{"heresy","education","public"}),
T("Sanctuary security review for valuables; locking discipline and witness rules enforced.","yellow",{"security","shrine","service"}),
T("Investigation of tainted incense vendors; samples seized and chain-of-custody logged.","yellow",{"poison","investigation","service"}),
T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.","red",{"poison","crisis","security"}),
T("High-risk protection of a central temple during unrest; entry control and calm liturgy.","red",{"unrest","security","crowd"}),
}
},


    -- NSJ
{key="CIC", name="Court of the Ironclad",
    ["Skýrophos"]        = "SkyrophosFlag.png",
link=orderLink("Fenrir (Reformed Stripping Path)","Court_of_the_Ironclad","Court of the Ironclad"),
    ["Bjornopolis"]      = "BjornopolisFlag.png",
tasks={
    ["Aegirheim"]        = "AegirheimFlag.png",
T("Discipline drill for shrine wardens; non-lethal control and restraint practices.","green",{"training","security","service"}),
    ["Norsolyra"]        = "NorsolyraFlag.png",
T("Escort for a threatened courier; route cleared and handover documented.","green",{"escort","security","service"}),
    ["Thorsalon"]        = "ThorsalonFlag.png",
T("Patrol coordination with civic guards at terminals; hand signals and cordon rules standardized.","green",{"rail","security","service"}),
    ["Myrene"]          = "MyreneFlag.png",
T("Gatehouse posture review for festival season; crowd lanes chalked and posted.","green",{"festival","crowd","security"}),
    ["Ephyra"]          = "EphyraFlag.png",
T("Weapons discipline check for attached wardens; compliance logs signed.","green",{"audit","security","service"}),
    ["Thyrea"]          = "ThyreaFlag.png",
T("Guard coordination in a frontier district; watch rotations synchronized with civic militia.","yellow",{"militia","security","frontier"}),
    ["Pelagia"]          = "PelagiaFlag.png",
T("Immediate reinforcement during riot signals; cordons established and safe egress opened.","red",{"riot","crowd","security"}),
    ["Halicarn"]        = "HalicarnFlag.png",
T("Interdiction rehearsal for barricade incidents; safe corridors mapped.","yellow",{"barricade","security","public"}),
T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.","red",{"siege","security","public"}),
T("Protection detail for water or rail infrastructure after sabotage rumor; sentries posted.","yellow",{"sabotage","infrastructure","security"}),
}
},


    -- Haifan
{key="ACC", name="Accord Concordia",
    ["Keybir-Aviv"]      = "KeybirAvivFlag.png",
link=orderLink("Lukedu (Reformed Stripping Path)","Accord_Concordia","Accord Concordia"),
    ["Tel-Amin"]        = "TelAminFlag.png",
tasks={
    ["Jogi"]            = "JogiFlag.png",
T("Public harmony dialogue; disputants guided to a signed concord recorded by Temple Bank scribes.","green",{"mediation","public","service"}),
    ["Lewisburg"]        = "LewisburgFlag.png",
T("Garden pavilion mediation for family inheritance disputes; settlement sealed.","green",{"mediation","service"}),
    ["Diamandis"]        = "DiamandisFlag.png",
T("Civic cooperation workshop for rival guilds; shared calendar commitments set.","green",{"guild","coordination","service"}),
    ["Thermosalem"]      = "ThermosalemFlag.png",
T("Inter-shrine calendar alignment meeting; procession timings standardized.","green",{"balance","public","service"}),
    ["Akróstadium"]      = "AkrostadiumFlag.png",
T("Quiet counseling for newly formed neighborhood councils; rules of debate issued.","green",{"governance","education","service"}),
    ["Lykopolis"]        = "LykopolisFlag.png",
T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.","yellow",{"rite","logistics","service"}),
    ["Sufriya"]          = "SufriyaFlag.png",
T("Emergency reconciliation after faction clash; mediators embedded with escorts.","red",{"clash","crowd","security"}),
T("Transit-lane mediation after station dispute; queues reset and monitored.","yellow",{"rail","queue","public"}),
T("Harmonization response after rumor spike; joint bulletin with Harmony Sanctum circulated.","yellow",{"rumor","service","public"}),
T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.","red",{"panic","crowd","public"}),
}
},


    -- Normark + Ouriana
{key="MSV", name="Mystery of the Stygian Veil",
    ["Riddersborg"]      = "RiddersborgFlag.png",
link=orderLink("Styx (Reformed Stripping Path)","Mystery_of_the_Stygian_Veil","Mystery of the Stygian Veil"),
    ["Ardclach"]        = "ArdclachFlag.png",
tasks={
    ["Ourid"]            = "Ouridflag.png",
T("Funerary liaison and cemetery vigil support; grief rites standardized.","green",{"funeral","public","service"}),
    ["Bashkim"]          = "BashkimFlag.png",
T("Registry update for burials and memorial offerings; records sealed and witnessed.","green",{"archive","audit","service"}),
    -- Tonar flag may not exist; omit unless you have it
T("Transition counseling for displaced families; resettlement blessings recorded.","green",{"displacement","relief","service"}),
    -- ["Tonar"]        = "TonarFlag.png",
T("Cemetery perimeter inspection; prevent vandalism during festival nights.","green",{"security","public","service"}),
T("Quiet rites for recovered remains after an accident; soothe panic and rumor.","green",{"public","rumor","service"}),
T("Investigation of graveyard desecration rumor; ward placement and witness interviews.","yellow",{"desecration","investigation","security"}),
T("Relic transfer paperwork verified for a shrine move; escorts scheduled.","yellow",{"escort","security","service"}),
T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.","red",{"provocation","crowd","security"}),
T("High-risk escort of relics through hostile streets; secure route planning with wardens.","red",{"escort","security","public"}),
T("Post-violence reconciliation vigil at a canal memorial; prevent retaliation cycles.","yellow",{"violence","public","mediation"}),
}
},
 
{key="SYF", name="Sylvan Fellowship",
link=orderLink("Faun (Reformed Stripping Path)","Sylvan_Fellowship","Sylvan Fellowship"),
tasks={
T("Forest-edge shrine maintenance and trail safety check; minor repairs logged.","green",{"trail","shrine","agri"}),
T("Ecology rite with farmers: pest balance and water stewardship guidance.","green",{"ecology","agri","service"}),
T("Wildlife boundary notices posted; discourage poaching near sacred groves.","green",{"public","security","agri"}),
T("Community foraging orientation; prevent poisoning and rumor spirals.","green",{"education","health","agri"}),
T("Riverbank cleanup day; improve water flow and reduce disease risk.","green",{"water","health","public"}),
T("Containment of illegal logging or poaching rumor; community patrol coordination.","yellow",{"poaching","security","investigation"}),
T("Fire season preparedness for woodland towns; firebreak rites and evacuation path marking.","yellow",{"wildfire","public","security"}),
T("Blight liaison with Verdant Embrace; field sampling routes scheduled.","yellow",{"blight","agri","service"}),
T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.","red",{"wildfire","crisis","public"}),
T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.","red",{"threat","security","public"}),
}
},
 
{key="MRM", name="Mystery of Red Mirth",
link=orderLink("Silenus (Reformed Stripping Path)","Myster_of_Red_Mirth","Mystery of Red Mirth"),
tasks={
T("Morale rite and sanctioned celebration oversight; prevent disorder while restoring joy.","green",{"morale","public","service"}),
T("Festival logistics support; safe routes and water points verified with civic stewards.","green",{"festival","crowd","public"}),
T("Sanctioned tavern-chaplain circuit; reduce brawls and theft through presence.","green",{"public","security","service"}),
T("Reconciliation feast for rival crews; agreements sealed before witnesses.","green",{"mediation","festival","service"}),
T("Public song-rotation at market hours; discourage rumor panic with sanctioned refrains.","green",{"rumor","public","service"}),
T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.","yellow",{"illicit","order","security"}),
T("Crowd-density enforcement for a stadium day; steward posts assigned.","yellow",{"crowd","festival","security"}),
T("Emergency morale stabilization after violence; controlled gatherings and escorts.","red",{"violence","crowd","public"}),
T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.","red",{"riot","crowd","security"}),
T("Post-panic comfort rites near transit hubs; prevent stampedes and retaliatory fights.","yellow",{"panic","rail","crowd"}),
}
},
 
{key="RCA", name="Rex Catonis",
link=orderLink("Cato (Reformed Stripping Path)","Rex_Catonis","Rex Catonis"),
tasks={
T("Civic order inspection: queue rules, market discipline, and shrine traffic flow.","green",{"order","queue","public"}),
T("Courtroom courtesy briefing; witness etiquette and silence rules reinforced.","green",{"governance","public","service"}),
T("Market weights-and-measures rite; deter fraud and stabilize pricing disputes.","green",{"audit","service","public"}),
T("Transit node discipline review; platform lines repainted and stewards assigned.","green",{"rail","order","transit"}),
T("Curfew signage and quiet-hours enforcement brief; reduce night disturbances.","green",{"security","public","service"}),
T("Administrative compliance review after complaints; corrective edicts posted.","yellow",{"compliance","governance","service"}),
T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.","yellow",{"security","governance","service"}),
T("Emergency restoration of order after mob action; escorted dispersal and curfew-litany.","red",{"mob","crowd","security"}),
T("High-risk support to authorities during barricade incident; perimeter discipline enforced.","red",{"barricade","security","public"}),
T("Port and rail dispute arbitration; enforce lawful priority lanes for relief cargo.","yellow",{"port","rail","order","relief"}),
}
},
 
{key="SDL", name="Sanctum Delphica",
link=orderLink("Sisera (Reformed Stripping Path)","Sanctum_Delphica","Sanctum Delphica"),
tasks={
T("Oracle consultation clinic for officials; questions logged and sealed.","green",{"oracle","governance","service"}),
T("Archive and library integrity check; catalog drift corrected and access verified.","green",{"archive","audit","service"}),
T("Public instruction on lawful prophecy vs. pamphlet fraud; leaflets distributed.","green",{"education","public","service"}),
T("Scribe training session; improve recordkeeping for relief and stipend flows.","green",{"education","audit","service"}),
T("Observatory-liaison briefing with Conclave Illuminara; shared bulletins standardized.","green",{"service","archive","public"}),
T("Investigation of forged prophecy pamphlets; counterfeit sources traced.","yellow",{"forgery","investigation","service"}),
T("Archive sealing of a sensitive docket after unrest; access reduced and witnessed.","yellow",{"security","archive","service"}),
T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.","red",{"broadcast","crisis","public"}),
T("High-risk protection for a key temple scholar; escort and secure lodging arranged.","red",{"threat","security","service"}),
T("Transit bulletin for corridor cities after omen panic; prevent stampedes and conspiracies.","yellow",{"rail","panic","public"}),
}
},
 
{key="OAI", name="Ordo Amicitia",
link=orderLink("Erasmus (Reformed Stripping Path)","Ordo_Amicitia","Ordo Amicitia"),
tasks={
T("Charity distribution and household check-ins; kindness register updated for stipend support.","green",{"charity","relief","service"}),
T("Food-line volunteer training; prevent cutting and fights through posted rules.","green",{"relief","queue","public"}),
T("Shelter visitation for displaced workers; job referrals routed through civic offices.","green",{"service","relief","governance"}),
T("Agri stipend review for smallholders; vouchers revalidated and extensions logged.","green",{"agri","audit","service"}),
T("Compassion circuit to hospitals and clinics; morale support and supply notes recorded.","green",{"health","service","relief"}),
T("Offering logistics for a major rite; livestock and treasure accounted and sealed.","yellow",{"offering","audit","service"}),
T("Mediation for neighborhood poverty disputes; aid allocations made transparent.","yellow",{"aid","mediation","service"}),
T("Emergency relief surge after incident; soup lines sanctified and guarded.","red",{"relief","crowd","public"}),
T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.","red",{"procession","security","public"}),
T("Port-side charity intake after a ferry accident; registries updated and families notified.","yellow",{"port","relief","service"}),
}
},
 
{key="TIL", name="Temple Illuminata",
link=orderLink("Nikolaj (Reformed Stripping Path)","Temple_Illuminata","Temple Illuminata"),
tasks={
T("Lamp and beacon maintenance blessing; public wayfinding clarity restored.","green",{"beacon","transit","service"}),
T("Infrastructure illumination audit for tunnels and transit nodes; repair notices issued.","green",{"infrastructure","transit","industry"}),
T("Rail signal blessing at key junctions; reduce accidents and rumor panic.","green",{"rail","transit","public"}),
T("Workshop visit: lantern production quality review; defective stock quarantined.","green",{"industry","audit","service"}),
T("Public lantern-route map refreshed for festival season; prevent crush points.","green",{"festival","crowd","public"}),
T("Investigation of signal tampering near a transport hub; seals replaced and watchers posted.","yellow",{"tamper","security","transit"}),
T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.","red",{"sabotage","crisis","security"}),
T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.","red",{"panic","crowd","transit"}),
T("Port navigation-light inspection for fog days; harbor masters briefed.","yellow",{"port","infrastructure","service"}),
T("Tunnel crowd-discipline drill; stewards assigned and entry metering practiced.","yellow",{"crowd","infrastructure","transit"}),
}
},
}
}


local DIVISION_FLAGS = {
 
    ["Vaeringheim Division"]        = "VaeringheimDivisionFlag.png",
local orderByKey, orderByName, orderKeys, orderNames = {}, {}, {}, {}
    ["Alpazkigz Division"]           = "AlpazkigzDivisionFlag.png",
for _, o in ipairs(orders) do
    ["Hatch Ministry Division"]     = "HatchMinistryFlag.png",
orderByKey[o.key] = o
    ["New South Jangsong Division"] = "NewSouthJangsongFlag.png",
orderByName[o.name] = o
     ["Haifan Bassaridia Division"]   = "HaifaDivisionFlag.png",
orderKeys[#orderKeys+1] = o.key
    ["Bassaridian Normark Division"] = "BassaridianNormarkFlagB.png",
orderNames[#orderNames+1] = o.name
    ["Ouriana Division"]             = "OurianaFlag.png",
end
    -- Odiferia/CKD: if you have division flags, add them; otherwise fallback will apply
 
    -- ["Odiferia Division"]          = "OdiferiaDivisionFlag.png",
--------------------------------------------------------------------------------
    -- ["Council of Kings Division"] = "CouncilOfKingsDivisionFlag.png",
-- 7.1) ORDER ICONS (from Module:GeneralPortOperations addLogo mapping)
--------------------------------------------------------------------------------
local orderIconFile = {
["Order of Aurora Mystica"]        = "Auroramystica.png",
["Harmony Sanctum"]                = "Harmonysanctum.png",
["Ignis Aeternum"]                = "Ignis Aeternum.png",
["Celestial Harmony Sect"]        = "HarmonySect.png",
["Guild of Golden Shadows"]        = "GoldenShadows.png",
["Azure Sentinel Sect"]           = "AzureSentinel.png",
["Reverie Nebulous"]               = "RevNeb.png",
["Eon Fellowship"]                 = "EonFel.png",
["Order of the Umbral Oracle"]     = "Umbral.png",
["Mystery of the Verdant Embrace"] = "Verdant.png",
["Conclave Illuminara"]           = "Conclave.png",
["Sanctum Vitalis"]               = "Vitalis.png",
["Temple Alabaster"]              = "Alabaster.png",
["Court of the Ironclad"]          = "Ironclad.png",
["Accord Concordia"]              = "Concordia.png",
["Mystery of the Stygian Veil"]    = "StygianVe.png",
["Sylvan Fellowship"]              = "Sylvan.png",
["Mystery of Red Mirth"]          = "MirthFlag.png",
["Rex Catonis"]                    = "Catonis.png",
["Sanctum Delphica"]              = "SanctumDel.png",
["Ordo Amicitia"]                  = "OrdoFl.png",
["Temple Illuminata"]             = "AureumFlag.png",
}
}
local function getOrderIconMarkup(orderName)
local fn = orderIconFile[orderName]
if fn and fileExists(fn) then
return string.format("[[File:%s|20px]] ", fn)
end
return ""
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (G) DIVISIONS + CITY POOLS
-- 8) CITY PROFILES (complete)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DIVISIONS = {
local cityProfiles = {
    { name="Vaeringheim Division",       link="[[Vaeringheim Division]]",       code="VAE", weight=5,
-- =========================
      cities={"Vaeringheim","Luminaria","Serena","Symphonara","Aurelia","Somniumpolis","Nexa","Lunalis Sancta","Sylvapolis","Delphica","Catonis Atrium","Ferrum Citadel"} },
-- CORE MAJORS
    { name="Alpazkigz Division",         link="[[Alpazkigz Division]]",         code="ALP", weight=4,
-- =========================
      cities={"Pyralis","Nexa","Saluria","Acheron","Koinonía","Aureum","Aetherium","Erythros",
["Vaeringheim"] = {
              "Fanghorn","Eikbu","Galvø","Sårensby","Slevik","Sjøsborg","Storesund","Notranskja","Kaledonija"} },
tags={"capital","admin","port","templebank","canals"},
    { name="Odiferia Division",           link="[[Odiferia Division]]",           code="ODF", weight=3,
weights={GGS=4,HSA=3,RCA=3,ASS=2,CIL=2,CHS=2,OSI=1},
      cities={"Odiferia","Somniumpolis","Aetherium","Ferrum Citadel","Aurelia","Symphonara"} },
},
    { name="Hatch Ministry Division",     link="[[Hatch Ministry Division]]",     code="HAT", weight=3,
["Luminaria"] = {
      cities={"Lewisburg","Lykopolis","Jogi","Keybir-Aviv","Tel-Amin","Diamandis","Thermosalem","Akróstadium","Sufriya"} },
tags={"industry","rollingstock","innovation","dawn","infrastructure"},
    { name="New South Jangsong Division", link="[[New South Jangsong Division]]", code="NSJ", weight=3,
weights={TIL=4,OAM=3,EOF=2,IGA=2,OSI=1,GGS=1},
      cities={"Skýrophos","Bjornopolis","Aegirheim","Norsolyra","Thorsalon","Pelagia","Myrene","Thyrea","Ephyra","Halicarn"} },
},
    { name="Haifan Bassaridia Division", link="[[Haifan Bassaridia Division]]", code="HFA", weight=3,
["Serena"] = {
      cities={"Keybir-Aviv","Tel-Amin","Diamandis","Jogi","Lewisburg","Thermosalem","Akróstadium","Sufriya","Lykopolis"} },
tags={"civic","lake","hospitality","public"},
    { name="Bassaridian Normark Division",link="[[Bassaridian Normark Division]]",code="BNO",weight=2,
weights={HSA=3,ACC=2,OAI=2,ASS=1,CHS=1,OSI=1},
      cities={"Ardclach","Riddersborg"} },
},
    { name="Ouriana Division",           link="[[Ouriana Division]]",           code="OUR", weight=2,
["Pyralis"] = {
      cities={"Bashkim","Ourid","Tonar"} },
tags={"volcanic","hazard","fire","reconstruction"},
    { name="Council of Kings Division",   link="[[Council of Kings Division]]",   code="CKD", weight=1,
weights={IGA=4,ASS=2,SCV=2,EOF=1,CIC=1},
      cities={"Vaeringheim","Bashkim","Ourid","Tonar"} },
baseSev="yellow",
},
["Symphonara"] = {
tags={"civic","arts","festival","public"},
weights={MRM=3,ACC=2,OSI=2,OAM=2,HSA=1,RCA=1},
},
["Aurelia"] = {
tags={"civic","trade","public"},
weights={HSA=2,CHS=2,GGS=2,ACC=1,OSI=1},
},
["Somniumpolis"] = {
tags={"finance","dream","rumorcontrol","noctic"},
weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1},
baseSev="yellow",
},
["Nexa"] = {
tags={"market","trade","logistics"},
weights={GGS=3,HSA=2,CHS=2,RCA=1,ASS=1,ACC=1},
},
["Lunalis Sancta"] = {
tags={"lunar","archives","pilgrimage","omens"},
weights={CIL=4,REN=2,UOR=2,SDL=1,HSA=1},
},
["Sylvapolis"] = {
tags={"forest","garden","ecology","shrinework"},
weights={SYF=3,MVE=3,OAI=1,HSA=1,ASS=1},
},
 
-- =========================
-- CORE MINORS
-- =========================
["Saluria"] = {
tags={"civic","artisan","wildlife"},
weights={SYF=2,CHS=2,ACC=1,HSA=1,OAI=1},
},
["Aetherium"] = {
tags={"sky","omens","education"},
weights={CIL=2,OAM=2,CHS=2,OSI=1},
},
["Ferrum Citadel"] = {
tags={"fortress","industry","security","infrastructure"},
weights={CIC=3,RCA=2,TIL=2,EOF=1,IGA=1},
baseSev="yellow",
},
["Acheron"] = {
tags={"frontier","containment","mines","security"},
weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1,RCA=1},
baseSev="yellow",
},
["Erythros"] = {
tags={"corridor","trade","security"},
weights={GGS=2,RCA=2,TIL=2,ASS=1,IGA=1},
},
["Catonis Atrium"] = {
tags={"civic","law","order"},
weights={RCA=4,HSA=1,CHS=1,ACC=1},
},
["Delphica"] = {
tags={"oracle","archives","scholarship"},
weights={SDL=4,CIL=2,CHS=1,HSA=1},
},
["Koinonía"] = {
tags={"harmony","mediation","civic"},
weights={ACC=3,HSA=3,CHS=1,OAI=1},
},
["Aureum"] = {
tags={"wealth","craft","trade","finance"},
weights={GGS=4,HSA=1,RCA=1,ALB=1},
},
 
-- =========================
-- ALPERKIN
-- =========================
["The Alpazkigz"] = {
tags={"tribal","festival","intercession","mediation"},
weights={ACC=3,HSA=2,CHS=2,OAI=1,CIC=1},
baseSev="yellow",
},
["Odiferia"] = {
tags={"noctic","rumorcontrol","dream","folk"},
weights={REN=3,HSA=2,GGS=2,UOR=2,MSV=1},
baseSev="yellow",
},
 
-- =========================
-- NEW SOUTH JANGSONG (MAJOR)
-- =========================
["Skýrophos"] = {
tags={"integration","civic","corridor","governance"},
weights={CHS=2,HSA=2,RCA=2,TIL=2,EOF=2,ACC=1},
},
["Bjornopolis"] = {
tags={"north","fortified","industry","civic"},
weights={EOF=3,CIC=2,TIL=2,ASS=1,RCA=1},
baseSev="yellow",
},
["Aegirheim"] = {
tags={"north","coastal","harbor","watch"},
weights={ASS=3,EOF=2,CIC=1,TIL=1},
baseSev="yellow",
},
["Norsolyra"] = {
tags={"agriculture","wetlands","river","supply"},
weights={MVE=3,ASS=2,SYF=2,OAI=1,SCV=1},
},
["Thorsalon"] = {
tags={"north","vineyards","festival","supply"},
weights={MVE=2,MRM=2,EOF=2,OAI=1,ASS=1},
baseSev="yellow",
},
 
-- =========================
-- NEW SOUTH JANGSONG (MINOR)
-- =========================
["Pelagia"] = {
tags={"coastal","distribution","water","relief"},
weights={ASS=3,SCV=2,HSA=1,EOF=1},
},
["Myrene"] = {
tags={"coastal","civic","trade"},
weights={ASS=2,HSA=2,CHS=1,ACC=1},
},
["Thyrea"] = {
tags={"agriculture","market","relief"},
weights={MVE=3,OAI=2,SYF=1,HSA=1},
},
["Ephyra"] = {
tags={"forest","frontier","watch","omens"},
weights={SYF=3,UOR=2,EOF=1,CIC=1},
baseSev="yellow",
},
["Halicarn"] = {
tags={"archives","scholarship","civic"},
weights={SDL=3,CIL=2,CHS=1,HSA=1},
},
 
-- =========================
-- HAIFAN BASSARIDIA (MAJOR)
-- =========================
["Keybir-Aviv"] = {
tags={"port","trade","gateway","security"},
weights={ASS=3,GGS=2,HSA=2,RCA=1,CIC=1},
baseSev="yellow",
},
["Tel-Amin"] = {
tags={"plantation","coffee","trade","agriculture"},
weights={MVE=3,OAI=2,SYF=2,GGS=1,ASS=1},
},
["Diamandis"] = {
tags={"luxury","trade","vaulting","security"},
weights={GGS=4,RCA=2,ALB=1,HSA=1},
baseSev="yellow",
},
["Jogi"] = {
tags={"agroprocessing","market","ferry","trade"},
weights={MVE=3,OAI=2,ASS=2,SYF=1,GGS=1},
},
["Lewisburg"] = {
tags={"rail","hub","logistics","governance"},
weights={TIL=4,RCA=3,GGS=2,HSA=1,EOF=1,ASS=1},
baseSev="yellow",
},
 
-- =========================
-- HAIFAN BASSARIDIA (MINOR)
-- =========================
["Thermosalem"] = {
tags={"healing","resort","water","crowd"},
weights={SCV=3,ASS=2,HSA=2,ACC=1,RCA=1},
baseSev="yellow",
},
["Akróstadium"] = {
tags={"stadium","festival","crowd","public"},
weights={MRM=3,OSI=2,ACC=2,HSA=1},
baseSev="yellow",
},
["Sufriya"] = {
tags={"terminal","security","port","logistics"},
weights={ASS=3,GGS=2,CIC=2,RCA=1,TIL=1},
baseSev="yellow",
},
["Lykopolis"] = {
tags={"rail","corridor","security","logistics"},
weights={TIL=4,RCA=2,EOF=2,CIC=1,GGS=1},
baseSev="yellow",
},
 
-- =========================
-- NORMARK
-- =========================
["Ardclach"] = {
tags={"north","ferry","coastal","watch"},
weights={ASS=3,EOF=2,CIC=1,GGS=1},
baseSev="yellow",
},
["Riddersborg"] = {
tags={"north","rail","corridor","storm"},
weights={EOF=3,TIL=2,CIC=2,ASS=1,RCA=1},
baseSev="yellow",
},
 
-- =========================
-- OURIANA
-- =========================
["Bashkim"] = {
tags={"rail","corridor","agriculture","logistics"},
weights={MVE=2,TIL=3,RCA=2,OAI=1,EOF=1},
baseSev="yellow",
},
["Ourid"] = {
tags={"river","fisheries","supply","trade"},
weights={ASS=3,MVE=2,OAI=2,HSA=1},
},
["Tonar"] = {
tags={"quarantine","doctrine","containment","security"},
weights={ALB=3,CIC=3,RCA=2,HSA=1,SDL=1},
baseSev="red",
},
 
-- =========================
-- CALEDONIAN DEPENDENCIES
-- =========================
["Fanghorn"] = {
tags={"frontier","forests","security","watch"},
weights={EOF=3,CIC=2,SYF=2,UOR=1,RCA=1},
baseSev="yellow",
},
["Eikbu"] = {
tags={"ranching","agriculture","trail","supply"},
weights={MVE=3,SYF=2,OAI=2,EOF=1},
},
["Galvø"] = {
tags={"island","coastal","port","storm"},
weights={ASS=3,EOF=2,CIC=1},
baseSev="yellow",
},
["Sårensby"] = {
tags={"rail","corridor","industry","civic"},
weights={TIL=3,EOF=2,RCA=2,GGS=1,ASS=1},
baseSev="yellow",
},
["Slevik"] = {
tags={"north","security","civic"},
weights={EOF=3,CIC=2,ASS=1,RCA=1},
baseSev="yellow",
},
["Sjøsborg"] = {
tags={"coastal","port","trade","watch"},
weights={ASS=3,EOF=2,GGS=1},
baseSev="yellow",
},
["Storesund"] = {
tags={"coastal","trade","storm"},
weights={ASS=3,EOF=2,CIC=1},
baseSev="yellow",
},
["Notranskja"] = {
tags={"north","frontier","watch","omens"},
weights={EOF=2,CIC=2,UOR=2,ASS=1},
baseSev="yellow",
},
["Kaledonija"] = {
tags={"regionalhub","governance","trade","civic"},
weights={RCA=3,EOF=2,GGS=2,HSA=1,ASS=1},
baseSev="yellow",
},
}
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (H) OPERATION TYPES
-- 9) ECONOMIC SIGNALS: parse "Company Table" on General Port page
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local OP_GROUND = {
local econCache = nil
    { name="corridor security (rail / canal / road checkpointing)", weight=4 },
 
    { name="urban stabilization (cordon / curfew enforcement)",     weight=3 },
local function sectorOf(industryCell)
    { name="anti-smuggling interdiction / contraband seizure",       weight=3 },
local s = (industryCell or ""):lower()
    { name="infrastructure protection (port/bridge/aqueduct)",       weight=3 },
if s:find("agric", 1, true) then return "Agricultural" end
    { name="counter-insurgency sweep (cell disruption)",             weight=2 },
if s:find("service", 1, true) then return "Service" end
    { name="hazard response (evacuation / debris clearance)",       weight=2 },
if s:find("manufact", 1, true) or s:find("mining", 1, true) or s:find("energy", 1, true) then
    { name="convoy escort (supply / detainee / artifact transfer)", weight=2 },
return "Industry"
}
end
return "Other"
end
 
local function extractCompanyName(cell)
local n = cell:match("'''(.-)'''")
if n and n ~= "" then return stripWiki(n) end
n = cell:match("%*%*(.-)%*%*")
if n and n ~= "" then return stripWiki(n) end
n = stripWiki(cell)
if #n > 80 then n = n:sub(1, 80) .. "…" end
return n
end
 
local function extractHQ(cell)
local s = stripWiki(cell)
if s == "" then return nil end
if s:lower():find("fleet", 1, true) then return nil end
if s:lower():find("no fixed", 1, true) then return nil end
s = s:gsub("%s*;%s*.*$", "")
s = s:gsub("%s*%b()%s*", " ")
s = normalizeSpaces(s)
s = trim(s)
if citySet[s] then return s end
return nil
end
 
local function parseCompanyTable(wikitext)
local signals = {}
for cname, _ in pairs(citySet) do
signals[cname] = {Agricultural=0, Service=0, Industry=0, Other=0, samples={}}
end
if type(wikitext) ~= "string" then return signals end
 
local startPos = wikitext:find("====%s*Company Table%s*====")
if not startPos then startPos = wikitext:find("####%s*Company Table") end
if not startPos then return signals end
 
local tablePos = wikitext:find("{|", startPos)
if not tablePos then return signals end
local endPos = wikitext:find("|}", tablePos)
if not endPos then return signals end
 
local tableText = wikitext:sub(tablePos, endPos + 1)
 
local rows = {}
for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
rows[#rows+1] = chunk
end
local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
if lastChunk then rows[#rows+1] = lastChunk end
 
for _, row in ipairs(rows) do
if not row:match("^%s*!") then
local line = normalizeSpaces(row:gsub("\n", " "))
line = line:gsub("^%s*|%s*", "")
 
local cells = {}
for cell in line:gmatch("([^|]-)%s*||%s*") do
cells[#cells+1] = trim(cell)
end
local tail = line:match("||%s*([^|].-)%s*$")
if tail and tail ~= "" then
if #cells == 0 or cells[#cells] ~= trim(tail) then
cells[#cells+1] = trim(tail)
end
end
 
local industryCell = ""
for _, c in ipairs(cells) do
if containsAny(c, {"agric", "service", "manufact", "mining", "energy"}) then
industryCell = c
break
end
end
local sector = sectorOf(industryCell)
 
local hqCell = cells[#cells] or ""
local hq = extractHQ(hqCell)
if hq and signals[hq] then
signals[hq][sector] = (signals[hq][sector] or 0) + 1
local nameCell = cells[3] or cells[2] or cells[1] or ""
local companyName = extractCompanyName(nameCell)
if companyName ~= "" and #signals[hq].samples < 2 then
signals[hq].samples[#signals[hq].samples+1] = companyName
end
end
end
end
 
return signals
end
 
local function getEconSignals()
if econCache then return econCache end
local content = safeTitleContent("General Port of Lake Morovia")
econCache = parseCompanyTable(content)
return econCache
end
 
local function dominantSector(sig)
if not sig then return "Other" end
local best, bestN = "Other", -1
for _, k in ipairs({"Agricultural","Industry","Service","Other"}) do
local n = sig[k] or 0
if n > bestN then best, bestN = k, n end
end
return best
end
 
local function econTagFromSector(sec)
if sec == "Agricultural" then return "agri" end
if sec == "Industry" then return "industry" end
if sec == "Service" then return "service" end
return "mixed"
end
 
local function econSummary(sig)
if not sig then return "n/a" end
local a = sig.Agricultural or 0
local i = sig.Industry or 0
local s = sig.Service or 0
local o = sig.Other or 0
local dom = dominantSector(sig)
local samp = (sig.samples and #sig.samples > 0) and ("; e.g., " .. table.concat(sig.samples, ", ")) or ""
return string.format("%s (%d agri / %d ind / %d serv / %d other)%s", dom, a, i, s, o, samp)
end
 
--------------------------------------------------------------------------------
-- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
local out = {}
if type(wikitext) ~= "string" then return out end
for line in wikitext:gmatch("[^\n]+") do
if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
if city and urg then
out[stripWiki(city)] = {urg=trim(urg), event=stripWiki(event or "")}
end
end
end
return out
end
 
local function threatSev(th)
if not th or not th.urg then return nil end
local u = (th.urg or ""):upper()
if u == "N/A" or u == "LOW" then return "green" end
if u == "MEDIUM" then return "yellow" end
if u == "HIGH" or u == "CRITICAL" then return "red" end
return "yellow"
end
 
--------------------------------------------------------------------------------
-- 11) WEIGHTED ORDER PICK + TASK PICK
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
local total = 0
for _, ok in ipairs(orderKeys) do
total = total + (weights[ok] or 0)
end
if total <= 0 then
return pickFromList(nextInt, orderKeys, nil)
end
local r = (nextInt() % total) + 1
local acc = 0
for _, ok in ipairs(orderKeys) do
acc = acc + (weights[ok] or 0)
if r <= acc then return ok end
end
return orderKeys[1]
end
 
local function buildTagSet(tags)
local set = {}
for _, t in ipairs(tags or {}) do set[t] = true end
return set
end
 
local function pickTaskForCity(nextInt, taskList, tagSet)
local candidates = {}
for _, task in ipairs(taskList or {}) do
local ok = false
for _, tg in ipairs(task.tags or {}) do
if tagSet[tg] then ok = true break end
end
if ok then candidates[#candidates+1] = task end
end
if #candidates > 0 then
return pickFromList(nextInt, candidates, nil)
end
return pickFromList(nextInt, taskList, nil)
end
 
local function baseWeights()
local w = {}
for _, ok in ipairs(orderKeys) do w[ok] = 1 end
return w
end
 
local function applyWeights(w, add)
if not add then return end
for k, v in pairs(add) do
if orderByKey[k] then
w[k] = (w[k] or 0) + tonumber(v or 0)
end
end
end
 
local function biasFromEcon(w, econTag)
if econTag == "agri" then
applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1})
elseif econTag == "industry" then
applyWeights(w, {TIL=3,EOF=2,IGA=2,CIC=1,RCA=1})
elseif econTag == "service" then
applyWeights(w, {GGS=3,HSA=2,RCA=2,CIL=1,CHS=1})
else
applyWeights(w, {CHS=1,HSA=1,ASS=1})
end
end
 
local function biasFromCorridor(w, isRail, isFerry)
if isRail then applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1}) end
if isFerry then applyWeights(w, {ASS=2,GGS=1,RCA=1}) end
end


local OP_NAVAL = {
local function biasFromTier(w, tier)
    { name="maritime escort / lake patrol routing",                 weight=3 },
if tier == "major" then
    { name="VBSS / interdiction / contraband boarding",             weight=3 },
applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
    { name="blockade enforcement / straits compliance sweep",       weight=2 },
end
    { name="harbor security (lock-gates / inner basin patrol)",    weight=2 },
end
}


local OP_AIR = {
--------------------------------------------------------------------------------
    { name="ISR overwatch / route scanning",                       weight=4 },
-- 12) TEMPLE BANK NOTES
    { name="rapid extraction / medevac lift",                       weight=2 },
--------------------------------------------------------------------------------
    { name="strike deterrence package (show of force)",            weight=1 },
local bankNotes = {
"Temple Bank stipend verified; local shrine receipts reconciled; next audit window scheduled.",
"Voucher authorizations issued for ritual materials; chain-of-custody notes logged for any seized items.",
"Investor liaison note filed; payroll routing confirmed for the deployed unit; compliance clerk assigned.",
"Donation flow stabilized; offertory seals inspected; variance thresholds set for the next three days.",
"Emergency stipend line opened for crowd-control and relief logistics; disbursement witnessed by two scribes.",
}
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (I) OVERSIGHT + COMPLIANCE
-- 13) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local OVERSIGHT = {
local function inferSeverity(cityProfile, econTag, taskSev, threatLevel)
    { label="Senate of Elders liaison",                weight=3 },
local sev = taskSev or "green"
    { label="Port compliance inspector (HCE)",         weight=4 },
 
    { label="Temple Aprobelle social-harmony auditor", weight=3 },
if cityProfile and cityProfile.baseSev then
    { label="Straits Convention monitor (civilian)",   weight=3 },
sev = cityProfile.baseSev
    { label="No observer (weather delay)",             weight=1 },
end
}
 
if econTag == "industry" or econTag == "service" then
if sev == "green" then sev = "yellow" end
end
 
if threatLevel == "yellow" and sev == "green" then sev = "yellow" end
if threatLevel == "red" and sev ~= "red" then sev = "red" end
 
return sev
end
 
local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
local cityName, cityRegion, tier = c.name, c.region, c.tier
local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
local nextInt = lcg(hash31(seedStr))
 
local profile = cityProfiles[cityName] or {tags={}, weights=nil}
local tags = {}
 
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
if RAIL[cityName] then tags[#tags+1] = "rail" end
if FERRY[cityName] then tags[#tags+1] = "ferry" end
 
local sig = econSignals and econSignals[cityName] or nil
local dom = dominantSector(sig)
local econTag = econTagFromSector(dom)
tags[#tags+1] = econTag
 
local tagSet = buildTagSet(tags)
 
local th = includeThreat and threatMap and threatMap[cityName] or nil
local tSev = threatSev(th)
 
local w = baseWeights()
biasFromTier(w, tier)
biasFromEcon(w, econTag)
biasFromCorridor(w, RAIL[cityName], FERRY[cityName])
applyWeights(w, profile.weights)
 
local orderKey = weightedPickKey(nextInt, w)
local o = orderByKey[orderKey] or orders[1]
local task = pickTaskForCity(nextInt, o.tasks, tagSet)
 
local sev = inferSeverity(profile, econTag, task.sev, tSev)
 
local unitName = pickOrgUnit(nextInt, sev)
local unitMeta = org[unitName]
local unitIndex = (nextInt() % 12) + 1
local unitCode = string.format("%s-%s%d", o.key, unitMeta.code, unitIndex)
local headcount = unitMeta.size
 
local bank = pickFromList(nextInt, bankNotes, nil)


local COMPLIANCE = {
local threatText, threatUrg = "", ""
    { label="Deployment verified; corridor routing approved; ledger notes filed same-day",                 weight=6 },
if includeThreat and th then
    { label="Minor variance corrected; revised routing issued with escort buffer",                          weight=2 },
threatText = th.event or ""
    { label="Weather delay; holding pattern under local command until visibility improves",                weight=1 },
threatUrg = th.urg or ""
    { label="Mission deferred (no-go); export-control check triggered by false manifest",                  weight=1 },
end
    { label="Export request refused (constitutional prohibition); material retained under Council custody", weight=1 },
}


local function rowColor(obs, comp, uScore)
return {
    local o = (obs or ""):lower()
style = sevStyle(sev),
    local c = (comp or ""):lower()
date = di.text,
    if o:match("weather delay") then
region = cityRegion,
        return ' style="background-color:#fff3cd;"'
city = string.format("%s %s", getCityFlagMarkup(cityName), cityLink(cityName)),
    end
order = getOrderIconMarkup(o.name) .. o.link,
    if c:match("minor variance") then
unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode),
        return ' style="background-color:#fff3cd;"'
task = task.text,
    end
tags = joinTags(tags),
    if c:match("export request refused") or c:match("mission deferred") then
econ = (showEcon and sig) and econSummary(sig) or "",
        return ' style="background-color:#f8d7da;"'
bank = bank,
    end
threat = threatText,
    if uScore >= 3 then
threatUrg = threatUrg,
        return ' style="background-color:#f8d7da;"'
sev = sev,
    end
}
    return ' style="background-color:#d4edda;"'
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (J) MAIN TABLE (Flags: unknown -> ITU flag)
-- 14) MAIN TABLE
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeTable(frame)
function p.makeTable(frame)
    local di = getPSSCDate()
local args = (frame and frame.args) or {}
    local args = frame and frame.args or {}
local regionFilter = tostring(args.region or "All")
local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
local limit = tonumber(args.limit or "")


    local rows = tonumber(args.rows) or 36
local di = getDateInfo()
    local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
local econSignals = getEconSignals()
    local flagSize = tonumber(args.flagsize) or 20


    local eventMap, hasEvents = buildEventMapForToday()
local threatMap = {}
if includeThreat then
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end


    local seed = tonumber(args.seed) or (di.year * 1000 + di.dayOfYear)
local out = {}
    math.randomseed(seed)
table.insert(out, "{| class='wikitable sortable'")
local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Cult Order !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
if includeThreat then header = header .. " !! Threat Watch" end
header = header .. " !! Severity"
table.insert(out, header)


    local divFilter = tostring(args.division or ""):lower()
local count = 0
    local divPool = {}
for _, c in ipairs(cityOrder) do
    if divFilter ~= "" then
if regionPass(regionFilter, c.region) then
        for _, d in ipairs(DIVISIONS) do
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
            if d.name:lower():find(divFilter, 1, true) or d.code:lower():find(divFilter, 1, true) then
table.insert(out, "|-" .. row.style)
                table.insert(divPool, d)
            end
        end
        if #divPool == 0 then divPool = DIVISIONS end
    else
        divPool = DIVISIONS
    end


    local out = {}
local line = string.format("| %s || %s || %s || %s || %s || %s",
    table.insert(out, "{| class='wikitable sortable'")
row.date, row.city, row.region, row.order, row.unit, row.task
    table.insert(out,
)
        "! style='white-space:nowrap;' | Date" ..
if showEcon then
        " !! Division" ..
line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
        " !! City / Node" ..
end
        " !! Daily event trigger" ..
line = line .. " || " .. row.bank
        " !! Urgency" ..
if includeThreat then
        " !! Operation" ..
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
        " !! Ground Forces" ..
line = line .. " || " .. threatCell
        " !! Naval Forces" ..
end
        " !! Aerial Forces" ..
line = line .. " || " .. row.sev
        " !! Oversight" ..
        " !! Compliance / Notes"
    )


    for i = 1, rows do
table.insert(out, line)
        local div = pickWeighted(divPool)
        local cityName = pick(div.cities)


        local divFlag = flagIcon(DIVISION_FLAGS[div.name], flagSize)
count = count + 1
        local divCell = string.format("%s %s", divFlag, div.link)
if limit and count >= limit then break end
end
end


        local cityFlag = flagIcon(CITY_FLAGS[cityName], flagSize)
table.insert(out, "|}")
        local cityCell = string.format("%s %s", cityFlag, cityLink(cityName, cityName))


        local ev = eventMap[cityName]
if showLegend then
        local evDesc = ev and ev.desc or (hasEvents and "No reports" or "Events module unavailable")
table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%%;'>")
        local urg = ev and ev.urgency or "N/A"
table.insert(out, "<b>Legend:</b> ")
        local uScore = urgencyScore(urg)
table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine rites / civic support&nbsp; ")
table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Heightened oversight / rumor control / targeted intervention&nbsp; ")
table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Crisis response / unrest / outbreak / high-risk operations")
table.insert(out, "</div>")
end


        local primary = pickWeighted({
if di.calendarEvent and di.calendarEvent ~= "" then
            { name="ground", weight = 6 },
table.insert(out, string.format("\n<div style='margin-top:0.5em; font-size:90%%;'><b>Calendar note:</b> %s</div>", di.calendarEvent))
            { name="naval", weight = (div.code == "HAT" or div.code == "HFA") and 5 or 2 },
end
            { name="air",    weight = 2 + uScore },
        }).name


        local op = (primary == "naval") and pickWeighted(OP_NAVAL)
return table.concat(out, "\n")
              or (primary == "air")  and pickWeighted(OP_AIR)
end
              or pickWeighted(OP_GROUND)


        local ground, naval, air = "—","—","—"
--------------------------------------------------------------------------------
-- 15) SINGLE CITY VIEW
--------------------------------------------------------------------------------
function p.makeCity(frame)
local args = (frame and frame.args) or {}
local city = tostring(args.city or "")
if city == "" then return "''Error: city parameter required.''" end
if not citySet[city] then return "''Error: unknown city (must match canonical name).''" end


        if not (primary == "air" and uScore <= 1 and math.random() < 0.35) then
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
            local gE = pickEchelonForUrgency(GROUND_ECHELONS, uScore)
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
            ground = string.format("%s (%s-%02d)", gE, div.code, math.random(1, 24))
        end


        if primary == "naval" or (div.code == "HAT" and math.random() < 0.55) or (uScore >= 3 and math.random() < 0.35) then
local di = getDateInfo()
            local nE = pickEchelonForUrgency(NAVAL_ECHELONS, uScore)
local econSignals = getEconSignals()
            naval = string.format("%s (%s-N%02d)", nE, div.code, math.random(1, 12))
        end


        if primary == "air" or (uScore >= 2 and math.random() < 0.55) then
local threatMap = {}
            local aE = pickEchelonForUrgency(AIR_ECHELONS, uScore)
if includeThreat then
            air = string.format("%s (%s-A%02d)", aE, div.code, math.random(1, 18))
local ok, rel = pcall(require, "Module:ReligiousEvents")
        end
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end


        local obs = pickWeighted(OVERSIGHT).label
local region, tier = "Core", "minor"
        local comp = pickWeighted(COMPLIANCE).label
for _, c in ipairs(cityOrder) do
        local notes = bulletList({ "Op-ID: " .. opId(di, div.code, i) })
if c.name == city then region, tier = c.region, c.tier break end
        local style = rowColor(obs, comp, uScore)
end


        table.insert(out, "|-" .. style)
local row = buildCityRow(di, {name=city, region=region, tier=tier}, econSignals, threatMap, includeThreat, showEcon)
        table.insert(out, string.format(
            "| %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s",
            di.text, divCell, cityCell, evDesc, urg, op.name, ground, naval, air, obs, bulletList({ comp, notes })
        ))
    end


    table.insert(out, "|}")
local out = {}
table.insert(out, "{| class='wikitable'")
local header = "! Date !! City !! Region !! Cult Order !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
if includeThreat then header = header .. " !! Threat Watch" end
header = header .. " !! Severity"
table.insert(out, header)
table.insert(out, "|-" .. row.style)


    if showLegend then
local line = string.format("| %s || %s || %s || %s || %s || %s",
        table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>")
row.date, row.city, row.region, row.order, row.unit, row.task
        table.insert(out, "<b>Legend:</b> ")
)
        table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = routine/compliant&nbsp; ")
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
        table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = variance/adjustment/weather&nbsp; ")
line = line .. " || " .. row.bank
        table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = HIGH/CRITICAL or export-control/deferment")
if includeThreat then
        table.insert(out, "</div>")
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
    end
line = line .. " || " .. threatCell
end
line = line .. " || " .. row.sev
table.insert(out, line)


    return table.concat(out, "\n")
table.insert(out, "|}")
return table.concat(out, "\n")
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (K) OPTIONAL: show org ladders (for documentation / page footer)
-- 16) ORDER-ONLY VIEW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.showOrganization()
function p.makeOrder(frame)
    return table.concat({
local args = (frame and frame.args) or {}
        '{| class="wikitable" style="width:100%;"',
local wanted = tostring(args.order or "")
        '! Branch !! Smallest → Largest',
if wanted == "" then
        '|-',
return "''Error: order parameter required (use exact order name).''"
        '| Ground || Turmae → Contubernia → Ordinis → Manipuli → Centuriae → Cohortes',
end
        '|-',
local wantedOrder = orderByName[wanted]
        '| Naval || Birlik-i Gemi (Patrol Element) → Tabur-i Derya (Task Unit) → Ordu (Task Group)',
if not wantedOrder then
        '|-',
return "''Error: unknown order name (must match module order names).''"
        '| Aerial || Zeygi (Element) → Ptisis (Flight) → Moira (Squadron)',
end
        '|}',
 
    }, "\n")
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
 
local di = getDateInfo()
local econSignals = getEconSignals()
 
local threatMap = {}
if includeThreat then
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end
 
local out = {}
table.insert(out, "{| class='wikitable sortable'")
local header = "! Date !! City !! Region !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
if includeThreat then header = header .. " !! Threat Watch" end
header = header .. " !! Severity"
table.insert(out, header)
 
for _, c in ipairs(cityOrder) do
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
if row.order:find(wantedOrder.link, 1, true) then
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.unit, row.task
)
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
line = line .. " || " .. row.bank
if includeThreat then
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
line = line .. " || " .. threatCell
end
line = line .. " || " .. row.sev
table.insert(out, line)
end
end
 
table.insert(out, "|}")
return table.concat(out, "\n")
end
end


return p
return p

Revision as of 21:04, 31 December 2025

Documentation for this module may be created at Module:MissionaryDailyOpsBVR/doc

-- Module:MissionaryDailyOpsBVR (REWRITE + FLAG RESOLVER + CULT ICONS)
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic output (no math.random / no os.time seeding).
--
-- Features:
--  • City profiles (culture/spiritual tags + per-city order bias)
--  • Economic signals parsed from General Port of Lake Morovia "Company Table"
--    (counts by sector + sample HQ companies) and used to bias order + task.
--  • Optional Threat Watch integration from Module:ReligiousEvents (opt-in).
--  • City flags: existence-checked; fallback to Imperial Trade Union flag when missing.
--  • Cult/Order icons: from Module:GeneralPortOperations addLogo(...) mapping.
--
-- Usage:
--   {{#invoke:MissionaryDailyOpsBVR|makeTable}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|showEcon=yes}}
--   {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
--   {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}}

local p = {}

local cal = require('Module:BassaridianCalendar')
local DAYS_IN_YEAR = 183

--------------------------------------------------------------------------------
-- 0) SMALL UTILS
--------------------------------------------------------------------------------
local function trim(s)
	if s == nil then return "" end
	s = tostring(s)
	s = s:gsub("^%s+", ""):gsub("%s+$", "")
	return s
end

local function safeTitleContent(titleText)
	local ok, t = pcall(mw.title.new, titleText)
	if not ok or not t then return nil end
	local ok2, content = pcall(function() return t:getContent() end)
	if not ok2 then return nil end
	return content
end

local function normalizeSpaces(s)
	return (s or ""):gsub("[%s\u00A0]+", " ")
end

local function stripWiki(s)
	s = tostring(s or "")
	-- Remove files/templates quickly
	s = s:gsub("%[%[File:[^%]]+%]%]", "")
	s = s:gsub("%{%b{}%}", "")
	-- Links: [[A|B]] => B, [[A]] => A
	s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
	s = s:gsub("%[%[([^%]]+)%]%]", "%1")
	-- Bold/italics
	s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
	return trim(s)
end

local function containsAny(s, words)
	s = (s or ""):lower()
	for _, w in ipairs(words) do
		if s:find(w, 1, true) then return true end
	end
	return false
end

local function joinTags(tags)
	if not tags or #tags == 0 then return "" end
	local out = {}
	for _, t in ipairs(tags) do out[#out+1] = t end
	return table.concat(out, ", ")
end

--------------------------------------------------------------------------------
-- 1) CALENDAR PARSING
--------------------------------------------------------------------------------
local function parseCalendarString(s)
	if type(s) ~= "string" then return nil end
	local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
	if dayOfYear and year then
		return tonumber(year), tonumber(dayOfYear), month
	end
	return nil
end

local function extractEventText(s)
	if type(s) ~= "string" then return "" end
	local ev = s:match("PSSC%s+–%s+(.-)%s+–%s+Proverb:")
	return ev or ""
end

local function doyToMonthDom(doy)
	if doy <= 61 then
		return 1, doy
	elseif doy <= 122 then
		return 2, doy - 61
	else
		return 3, doy - 122
	end
end

local function getDateInfo()
	local raw = cal.getCurrentDate()
	local y, doy = parseCalendarString(raw)
	if not y or not doy then
		y, doy = 0, 1
		raw = tostring(raw or "")
	end
	local m, dom = doyToMonthDom(doy)
	return {
		year = y,
		dayOfYear = doy,
		monthNum = m,
		dom = dom,
		text = string.format("%d/%d/%d PSSC", dom, m, y),
		raw = raw,
		calendarEvent = extractEventText(raw),
	}
end

--------------------------------------------------------------------------------
-- 2) DETERMINISTIC RNG
--------------------------------------------------------------------------------
local function hash31(str)
	local h = 0
	for i = 1, #str do
		h = (h * 131 + str:byte(i)) % 2147483647
	end
	return h
end

local function lcg(seed)
	local s = seed % 2147483647
	if s <= 0 then s = s + 2147483646 end
	return function()
		s = (1103515245 * s + 12345) % 2147483647
		return s
	end
end

local function pickFromList(nextInt, list, avoidValue)
	if #list == 0 then return nil end
	if #list == 1 then return list[1] end
	local tries = 0
	while tries < 12 do
		local idx = (nextInt() % #list) + 1
		local v = list[idx]
		if v ~= avoidValue then return v end
		tries = tries + 1
	end
	for _, v in ipairs(list) do
		if v ~= avoidValue then return v end
	end
	return list[1]
end

--------------------------------------------------------------------------------
-- 3) COLOR CODING (Hatch-style)
--------------------------------------------------------------------------------
local function sevStyle(sev)
	if sev == "red" then
		return ' style="background-color:#f8d7da;"'
	elseif sev == "yellow" then
		return ' style="background-color:#fff3cd;"'
	else
		return ' style="background-color:#d4edda;"'
	end
end

--------------------------------------------------------------------------------
-- 4) CITY NETWORK (canonical list)
--------------------------------------------------------------------------------
local function normalizeForFlag(name)
	return (name or "")
		:gsub("[%s%-’]", "")
		:gsub("[Áá]", "A"):gsub("[Éé]", "E")
		:gsub("[Íí]", "I"):gsub("[Óó]", "O")
		:gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end

local function cityLink(name)
	return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", name, name)
end

-- 54 canonical cities; tier used for "major city" baseline.
local cityOrder = {
	-- Core majors
	{name="Vaeringheim",      region="Core", tier="major"},
	{name="Luminaria",        region="Core", tier="major"},
	{name="Serena",           region="Core", tier="major"},
	{name="Pyralis",          region="Core", tier="major"},
	{name="Symphonara",       region="Core", tier="major"},
	{name="Aurelia",          region="Core", tier="major"},
	{name="Somniumpolis",     region="Core", tier="major"},
	{name="Nexa",             region="Core", tier="major"},
	{name="Lunalis Sancta",   region="Core", tier="major"},
	{name="Sylvapolis",       region="Core", tier="major"},

	-- Core minors
	{name="Saluria",          region="Core", tier="minor"},
	{name="Aetherium",        region="Core", tier="minor"},
	{name="Ferrum Citadel",   region="Core", tier="minor"},
	{name="Acheron",          region="Core", tier="minor"},
	{name="Erythros",         region="Core", tier="minor"},
	{name="Catonis Atrium",   region="Core", tier="minor"},
	{name="Delphica",         region="Core", tier="minor"},
	{name="Koinonía",         region="Core", tier="minor"},
	{name="Aureum",           region="Core", tier="minor"},

	-- Alperkin
	{name="The Alpazkigz",    region="Alperkin", tier="minor"},
	{name="Odiferia",         region="Alperkin", tier="minor"},

	-- New South Jangsong
	{name="Skýrophos",        region="New South Jangsong", tier="major"},
	{name="Bjornopolis",      region="New South Jangsong", tier="major"},
	{name="Aegirheim",        region="New South Jangsong", tier="major"},
	{name="Norsolyra",        region="New South Jangsong", tier="major"},
	{name="Thorsalon",        region="New South Jangsong", tier="major"},
	{name="Pelagia",          region="New South Jangsong", tier="minor"},
	{name="Myrene",           region="New South Jangsong", tier="minor"},
	{name="Thyrea",           region="New South Jangsong", tier="minor"},
	{name="Ephyra",           region="New South Jangsong", tier="minor"},
	{name="Halicarn",         region="New South Jangsong", tier="minor"},

	-- Haifan Bassaridia
	{name="Keybir-Aviv",      region="Haifan Bassaridia", tier="major"},
	{name="Tel-Amin",         region="Haifan Bassaridia", tier="major"},
	{name="Diamandis",        region="Haifan Bassaridia", tier="major"},
	{name="Jogi",             region="Haifan Bassaridia", tier="major"},
	{name="Lewisburg",        region="Haifan Bassaridia", tier="major"},
	{name="Thermosalem",      region="Haifan Bassaridia", tier="minor"},
	{name="Akróstadium",      region="Haifan Bassaridia", tier="minor"},
	{name="Sufriya",          region="Haifan Bassaridia", tier="minor"},
	{name="Lykopolis",        region="Haifan Bassaridia", tier="minor"},

	-- Normark
	{name="Ardclach",         region="Normark", tier="major"},
	{name="Riddersborg",      region="Normark", tier="major"},

	-- Ouriana
	{name="Bashkim",          region="Ouriana", tier="major"},
	{name="Ourid",            region="Ouriana", tier="major"},
	{name="Tonar",            region="Ouriana", tier="minor"},

	-- Caledonia
	{name="Fanghorn",         region="Caledonia", tier="major"},
	{name="Eikbu",            region="Caledonia", tier="major"},
	{name="Galvø",            region="Caledonia", tier="major"},
	{name="Sårensby",         region="Caledonia", tier="minor"},
	{name="Slevik",           region="Caledonia", tier="minor"},
	{name="Sjøsborg",         region="Caledonia", tier="minor"},
	{name="Storesund",        region="Caledonia", tier="minor"},
	{name="Notranskja",       region="Caledonia", tier="minor"},
	{name="Kaledonija",       region="Caledonia", tier="minor"},
}

local function regionPass(filter, cityRegion)
	if not filter or filter == "" or filter == "All" then return true end
	if filter == "Core" then return cityRegion == "Core" end
	if filter == "NSJ" then return cityRegion == "New South Jangsong" end
	if filter == "Haifan" then return cityRegion == "Haifan Bassaridia" end
	if filter == "Caledonian" then return cityRegion == "Caledonia" end
	return cityRegion == filter
end

--------------------------------------------------------------------------------
-- 4.1) CITY FLAGS (existence-checked + fallback)
--------------------------------------------------------------------------------
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"

local function asciiNormalizeForFlag(name)
	return (name or "")
		:gsub("[%s%-’]", "")
		:gsub("[ÁáÀàÂâÄäÃãÅåĀā]", "A")
		:gsub("[ÉéÈèÊêËëĒē]", "E")
		:gsub("[ÍíÌìÎîÏïĪī]", "I")
		:gsub("[ÓóÒòÔôÖöÕõØøŌō]", "O")
		:gsub("[ÚúÙùÛûÜüŪū]", "U")
		:gsub("[ÝýŸÿ]", "Y")
		:gsub("[Ææ]", "AE")
end

local fileExistsCache = {}
local function fileExists(fileName)
	if not fileName or fileName == "" then return false end
	if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
	local ok, t = pcall(mw.title.new, "File:" .. fileName)
	local exists = (ok and t and t.exists == true) or false
	fileExistsCache[fileName] = exists
	return exists
end

local citySet = {}
for _, c in ipairs(cityOrder) do citySet[c.name] = true end

-- Explicit odd filenames you noted (plus Skyrophos).
local cityFlagOverrides = {
	["Skýrophos"] = "SkyrophosFlag.png",
	["Sårensby"]  = "SårensbyFlag.png",
	["Slevik"]    = "SlevikFlag.png.png",
	["Kaledonija"]= "KaledonijaFlag.png",
}

local cityFlagMarkupCache = {}
local function getCityFlagMarkup(cityName)
	if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end

	-- 1) explicit override
	local override = cityFlagOverrides[cityName]
	if override and fileExists(override) then
		local mk = string.format("[[File:%s|20px]]", override)
		cityFlagMarkupCache[cityName] = mk
		return mk
	end

	-- 2) try normalized candidates
	local norm = normalizeForFlag(cityName)
	local ascii = asciiNormalizeForFlag(cityName)
	local candidates = {
		norm .. "Flag.png",
		ascii .. "Flag.png",
	}

	for _, fn in ipairs(candidates) do
		if fileExists(fn) then
			local mk = string.format("[[File:%s|20px]]", fn)
			cityFlagMarkupCache[cityName] = mk
			return mk
		end
	end

	-- 3) fallback to ITU
	local mk = string.format("[[File:%s|20px]]", DEFAULT_CITY_FLAG_FILE)
	cityFlagMarkupCache[cityName] = mk
	return mk
end

--------------------------------------------------------------------------------
-- 5) CORRIDORS / ROLE HINTS (hard-coded)
--------------------------------------------------------------------------------
local RAIL = {
	["Lykopolis"]=true, ["Lewisburg"]=true, ["Bashkim"]=true, ["Somniumpolis"]=true,
	["Erythros"]=true, ["Vaeringheim"]=true, ["Symphonara"]=true, ["Skýrophos"]=true,
	["Norsolyra"]=true, ["Pelagia"]=true, ["Riddersborg"]=true, ["Sårensby"]=true,
}
local FERRY = {["Jogi"]=true, ["Ardclach"]=true}

--------------------------------------------------------------------------------
-- 6) MISSIONARY ORGANIZATION (sizes)
--------------------------------------------------------------------------------
local org = {
	Lampade     = {size=3,   code="L"},
	Hetairoi    = {size=6,   code="H"},
	Dodekade    = {size=12,  code="D"},
	Kleisthenes = {size=25,  code="K"},
	Stoa        = {size=50,  code="S"},
	Thiasos     = {size=100, code="T"},
}

local function pickOrgUnit(nextInt, sev)
	if sev == "green" then
		return pickFromList(nextInt, {"Lampade","Hetairoi"}, nil)
	elseif sev == "yellow" then
		return pickFromList(nextInt, {"Hetairoi","Dodekade","Kleisthenes"}, nil)
	else
		return pickFromList(nextInt, {"Kleisthenes","Stoa","Thiasos"}, nil)
	end
end

--------------------------------------------------------------------------------
-- 7) ORDERS + TASKS (tagged)
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
	return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
end

local function T(text, sev, tags)
	return {text=text, sev=sev, tags=tags or {}}
end

local orders = {
	{key="OSI", name="Ordo Solis Invicti",
		link=orderLink("Atos (Reformed Stripping Path)","Ordo_Solis_Invicti","Ordo Solis Invicti"),
		tasks={
			T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education","service"}),
			T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine","service"}),
			T("Solar route inspection at major junctions; traffic etiquette posted for pilgrims and commuters.","green",{"transit","rail","public"}),
			T("Sun-pledge renewal for civic clerks; oath sheets sealed and logged with Temple Bank scribes.","green",{"governance","audit","service"}),
			T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit","industry"}),
			T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit","service"}),
			T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd","public"}),
			T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
			T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security","crowd"}),
			T("Emergency morale procession after night unrest; hymn leadership and calm restoration at transit choke points.","red",{"unrest","crowd","transit"}),
		}
	},

	{key="OAM", name="Order of Aurora Mystica",
		link=orderLink("Eos (Reformed Stripping Path)","Order_of_Aurora_Mystica","Order of Aurora Mystica"),
		tasks={
			T("Sunrise vigil and reconciliation rite for competing guilds; signed harmony pledges archived by Temple Bank clerks.","green",{"mediation","guild","service"}),
			T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public","service"}),
			T("Quiet home-visits to recent migrants; orientation blessings and sanctioned neighborhood guides issued.","green",{"public","service","mediation"}),
			T("Rooftop shrine upkeep and dawn-banner replacement; civic lanes kept clear for morning processions.","green",{"shrine","infrastructure","public"}),
			T("Pilgrim flow timing review for a dawn festival; route markers placed to prevent crush points.","green",{"festival","crowd","public"}),
			T("Civic blessing for new housing blocks; routing of foot-traffic to prevent crush points and misdirection.","yellow",{"crowd","infrastructure","service"}),
			T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
			T("Dawn mediation under escort after a workplace dispute threatens walkouts; settlement notarized.","yellow",{"mediation","industry","governance"}),
			T("Omen-risk advisory for coastal weather or fog days; port stewards briefed and signage posted.","yellow",{"port","public","service"}),
			T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.","red",{"unrest","arson","crowd"}),
		}
	},

	{key="HSA", name="Harmony Sanctum",
		link=orderLink("Micras (Reformed Stripping Path)","Harmony_Sanctum","Harmony Sanctum"),
		tasks={
			T("Neighborhood mediation session for family disputes and trade-feud spillover; binding accords notarized.","green",{"mediation","service"}),
			T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.","green",{"health","rumor","service"}),
			T("Queue-discipline coaching at relief points; volunteers trained to prevent confrontations.","green",{"order","queue","relief"}),
			T("Workplace concord circle for service districts; complaint channels clarified and posted.","green",{"service","mediation","governance"}),
			T("Inter-shrine courtesy visit to reduce rivalry; shared calendar commitments established.","green",{"balance","mediation","public"}),
			T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.","yellow",{"audit","governance","service"}),
			T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.","yellow",{"schism","containment","public"}),
			T("Rumor forensics on a circulating ‘forbidden rite’ claim; witness tree compiled and forwarded.","yellow",{"rumor","investigation","public"}),
			T("Crowd-stabilization drill for festival season; steward assignments and safe exits mapped.","yellow",{"festival","crowd","public"}),
			T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.","red",{"riot","crowd","public"}),
		}
	},

	{key="IGA", name="Ignis Aeternum",
		link=orderLink("Pyros (Reformed Stripping Path)","Ignis_Aeternum","Ignis Aeternum"),
		tasks={
			T("Controlled fire blessing for workshops; safety rite doubled as inspection of fuel and storage.","green",{"industry","safety"}),
			T("Kiln-and-forge consecration for a new artisan street; insurance seals verified.","green",{"industry","infrastructure","service"}),
			T("Firebreak marking for dry-season hills; public notices issued to farmers and herders.","green",{"agri","fire","public"}),
			T("Hearth-blessing round for crowded tenements; cooking hazards corrected with wardens.","green",{"public","safety","service"}),
			T("Candle and incense quality check at shrine vendors; adulterants rejected and logged.","green",{"shrine","audit","service"}),
			T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.","yellow",{"contam","shrine","service"}),
			T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.","yellow",{"arson","investigation","security"}),
			T("Explosive-storage compliance check near port or rail yards; seals renewed and routes revised.","yellow",{"rail","port","security","industry"}),
			T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.","red",{"arson","crisis","security"}),
			T("Post-incident purification circuit across multiple oratories; public confidence restored by nightfall.","red",{"incident","public","crowd"}),
		}
	},

	{key="CHS", name="Celestial Harmony Sect",
		link=orderLink("Indigo (Reformed Stripping Path)","Celestial_Harmony_Sect","Celestial Harmony Sect"),
		tasks={
			T("Diplomatic courtesy visit to a city council; alignments review and oath renewal with minimal spectacle.","green",{"diplomacy","governance","service"}),
			T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.","green",{"education","astronomy","service"}),
			T("Inter-city liaison packet prepared for corridor towns; travel permissions confirmed.","green",{"rail","service","governance"}),
			T("Boundary-harmony rite at a district line; neighbor shrines exchange tokens under witness.","green",{"balance","public","mediation"}),
			T("Calm briefing for merchant captains on festival timing; reduce congestion and rumor spread.","green",{"port","service","public"}),
			T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.","yellow",{"balance","mediation","archive"}),
			T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.","yellow",{"rumor","investigation","public"}),
			T("Quiet arbitration after contract dispute between a guild and a council office.","yellow",{"service","governance","mediation"}),
			T("Transit coordination for high-profile delegates; platform escorts and crowd lanes established.","yellow",{"rail","crowd","security"}),
			T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.","red",{"confrontation","crowd","security"}),
		}
	},

	{key="GGS", name="Guild of Golden Shadows",
		link=orderLink("Chrysos (Reformed Stripping Path)","Guild_of_Golden_Shadows","Guild of Golden Shadows"),
		tasks={
			T("Discrete audit of offertory ledgers and voucher seals; minor variances reconciled quietly.","green",{"finance","audit","service"}),
			T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.","green",{"merchant","security","service"}),
			T("Port-side seal verification for sacred cargo; chain-of-custody stamped and witnessed.","green",{"port","finance","security"}),
			T("Rail consignment escort paperwork checked; handover points confirmed with station stewards.","green",{"rail","service","security"}),
			T("Quiet fraud-prevention briefing for shrine treasurers; common forgery patterns circulated.","green",{"finance","education","service"}),
			T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.","yellow",{"forgery","investigation","finance"}),
			T("Debt dispute arbitration for a sanctioned charity line; terms rewritten and sealed.","yellow",{"finance","mediation","service"}),
			T("Watchlist update for suspected smugglers using festival crowds; surveillance plan filed.","yellow",{"security","crowd","port"}),
			T("Recovery detail for stolen reliquary property; surveillance and lawful seizure under civic warrant.","red",{"theft","security","investigation"}),
			T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.","red",{"panic","finance","service"}),
		}
	},

	{key="ASS", name="Azure Sentinel Sect",
		link=orderLink("Thalassa (Reformed Stripping Path)","Azure_Sentinel_Sect","Azure Sentinel Sect"),
		tasks={
			T("Canal and fountain sanctification circuit; water-quality blessings paired with public reassurance.","green",{"water","public","service"}),
			T("Harbor hygiene briefing for fish markets; cold-chain and wash stations checked.","green",{"port","health","service"}),
			T("Flood-gate inspection rite; maintenance tickets issued and signed by civic stewards.","green",{"infrastructure","water","service"}),
			T("Ferry pier blessing and rope inspection; boarding lines reorganized to reduce accidents.","green",{"ferry","port","public"}),
			T("Reservoir vigil for drought days; ration etiquette posted at distribution points.","green",{"water","relief","public"}),
			T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.","yellow",{"port","sabotage","security"}),
			T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.","yellow",{"flood","infrastructure","public"}),
			T("Waterborne rumor containment bulletin; discourage panic buying and hoarding.","yellow",{"rumor","service","public"}),
			T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.","red",{"outbreak","water","health"}),
			T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.","red",{"infrastructure","crisis","public"}),
		}
	},

	{key="REN", name="Reverie Nebulous",
		link=orderLink("Nephele (Reformed Stripping Path)","Reverie_Nebulous","Reverie Nebulous"),
		tasks={
			T("Dream-interpretation clinic to reduce anxiety; rumor narratives redirected into sanctioned symbolism.","green",{"dream","rumor","service"}),
			T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.","green",{"health","sleep","service"}),
			T("Public guidance sheet on lawful visions vs. sedition pamphlets; distributed at shrines.","green",{"education","public","service"}),
			T("Quiet household visits after a death or displacement; stabilize grief cycles with sanctioned imagery.","green",{"service","health","public"}),
			T("Festival mood stewardship: prevent illicit revel by offering sanctioned evening rites.","green",{"festival","public","morale"}),
			T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.","yellow",{"broadcast","investigation","service"}),
			T("Rumor cascade mapping in service districts; identify the first carrier nodes.","yellow",{"rumor","investigation","service"}),
			T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.","red",{"panic","crowd","public"}),
			T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.","red",{"violence","health","service"}),
			T("Emergency night curfew-support rite; safe corridors marked to reduce shadow-fear incidents.","yellow",{"security","public","service"}),
		}
	},

	{key="EOF", name="Eon Fellowship",
		link=orderLink("Glinos (Reformed Stripping Path)","Eon_Fellowship","Eon Fellowship"),
		tasks={
			T("Cold-weather relief checks and ration fairness review; storage seals verified.","green",{"relief","logistics","service"}),
			T("Infrastructure endurance audit: roofs, walls, and bridges; damage triage list issued.","green",{"infrastructure","audit","industry"}),
			T("Shelter staffing rotation planned for storm season; volunteer rosters sealed.","green",{"relief","service","governance"}),
			T("Rail convoy support brief for frontier deliveries; pacing and rest points set.","green",{"rail","logistics","service"}),
			T("Queue discipline at distribution points; conflict triggers reduced through signage.","green",{"order","queue","public"}),
			T("Conflict de-escalation for resource disputes; binding queue rules posted at distribution points.","yellow",{"order","queue","governance"}),
			T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.","red",{"storm","relief","public"}),
			T("Frontier outpost morale visit; endurance rites and supply accountability review.","yellow",{"frontier","audit","service"}),
			T("Bridge closure coordination after damage report; detours posted and escorted.","yellow",{"infrastructure","transit","public"}),
			T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.","red",{"frontier","security","logistics"}),
		}
	},

	{key="UOR", name="Order of the Umbral Oracle",
		link=orderLink("Noctis (Reformed Stripping Path)","Order_of_the_Umbral_Oracle","Order of the Umbral Oracle"),
		tasks={
			T("Night watch and omen-reading in the temple quarter; guidance slips sealed for officials.","green",{"omen","security","service"}),
			T("Shadow-lane safety marking near canals and alleys; lantern placement logged.","green",{"public","security","transit"}),
			T("Quiet counsel for a magistrate; truth-testing rite witnessed by two scribes.","green",{"governance","inquiry","service"}),
			T("Missing-persons intake desk hosted at a shrine; reports standardized and forwarded.","green",{"service","investigation","public"}),
			T("Nighttime rumor monitoring in taverns and stations; early interventions logged.","green",{"rumor","rail","service"}),
			T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.","yellow",{"governance","inquiry","audit"}),
			T("Shadow-route mapping after disappearances; safe-path markers installed with civic approval.","yellow",{"security","transit","public"}),
			T("Evidence sealing after a midnight incident; chain-of-custody verified for Temple authorities.","yellow",{"investigation","security","service"}),
			T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.","red",{"demonic","panic","crowd"}),
			T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.","red",{"abduction","security","service"}),
		}
	},

	{key="MVE", name="Mystery of the Verdant Embrace",
		link=orderLink("Opsithe (Reformed Stripping Path)","Mystery_of_the_Verdant_Embrace","Mystery of the Verdant Embrace"),
		tasks={
			T("Harvest blessing and storage rite; spoilage prevention guidance issued to granaries.","green",{"agriculture","harvest","agri"}),
			T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.","green",{"garden","shrine","agri"}),
			T("Seed and tool stipend audit for smallholders; vouchers revalidated.","green",{"audit","agri","service"}),
			T("Irrigation channel blessing and silt-clearing volunteer day; water etiquette posted.","green",{"water","agri","public"}),
			T("Market stall purity checks for produce; vendors instructed on safe storage.","green",{"public","health","agri"}),
			T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.","yellow",{"shortage","logistics","agri"}),
			T("Pest or blight containment rites; field inspections with farmers’ associations.","yellow",{"blight","agriculture","agri"}),
			T("Livestock procession routing for festival markets; prevent crush and theft.","yellow",{"crowd","festival","agri"}),
			T("Famine-risk surge response; emergency distribution queues sanctified and monitored.","red",{"famine","relief","agri"}),
			T("Emergency containment of contaminated grain rumor; sample testing coordinated with Sanctum Vitalis.","yellow",{"rumor","health","agri"}),
		}
	},

	{key="CIL", name="Conclave Illuminara",
		link=orderLink("Tarsica (Reformed Stripping Path)","Conclave_Illuminara","Conclave Illuminara"),
		tasks={
			T("Moonlit instruction circle; eclipse-watch protocol rehearsed with apprentice scribes.","green",{"lunar","education","service"}),
			T("Night-sky bulletin posted to reduce panic during overcast weeks; lawful interpretations provided.","green",{"public","rumor","service"}),
			T("Observatory instrument check; calibration seals signed and archived.","green",{"archive","audit","service"}),
			T("Lantern route blessing for tunnels and arcades; wayfinding clarified for late shifts.","green",{"transit","public","service"}),
			T("Quiet archive preservation: scroll humidity checks and secure storage review.","green",{"archive","service","audit"}),
			T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.","yellow",{"archive","audit","security"}),
			T("Public lantern rite to counter rumor cascades; controlled routes and timing set with wardens.","yellow",{"rumor","crowd","public"}),
			T("Transit hub advisory after night panic; crowd lanes re-drawn and posted.","yellow",{"rail","crowd","transit"}),
			T("Emergency omen bulletin after night panic; official statement drafted and delivered.","red",{"panic","public","service"}),
			T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.","red",{"threat","security","service"}),
		}
	},

	{key="SCV", name="Sanctum Vitalis",
		link=orderLink("Plateau (Reformed Stripping Path)","Sanctum_Vitalis","Sanctum Vitalis"),
		tasks={
			T("Clinic day: basic health screenings and hygiene instruction; water points checked.","green",{"health","public","service"}),
			T("Sanctioned healer coordination meeting; triage roles rehearsed for outbreaks.","green",{"training","health","service"}),
			T("Hygiene signage deployment at markets; handwash stations verified.","green",{"public","health","service"}),
			T("Nutrition counseling for ration lines; supplement vouchers distributed under Temple Bank authorization.","green",{"nutrition","relief","service"}),
			T("Quiet home checks for vulnerable households; prevent panic spirals after illness rumors.","green",{"service","health","rumor"}),
			T("Supplement and ration fortification distribution under Temple Bank authorization.","yellow",{"nutrition","relief","service"}),
			T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.","yellow",{"outbreak","training","public"}),
			T("Quarantine lane setup drill at a terminal or station; entry control rehearsed.","yellow",{"rail","security","health"}),
			T("Active outbreak response: triage lanes established; quarantine support and calm messaging.","red",{"outbreak","crisis","public"}),
			T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.","red",{"violence","logistics","service"}),
		}
	},

	{key="ALB", name="Temple Alabaster",
		link=orderLink("Ivory (Reformed Stripping Path)","Temple_Alabaster","Temple Alabaster"),
		tasks={
			T("Purity inspection of ritual vessels and incense stock; minor replacements approved.","green",{"purity","shrine","service"}),
			T("Doctrinal reading circle for novices; sanctioned texts distributed with margin notes.","green",{"education","service","public"}),
			T("Shrine inventory check for relic storage; locks and witness rules enforced.","green",{"security","shrine","audit"}),
			T("Quiet correction of unsanctioned household rites; guidance issued without spectacle.","green",{"service","public","education"}),
			T("Blessing of new white-draped oratory; entry etiquette posted to reduce disorder.","green",{"shrine","public","service"}),
			T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.","yellow",{"heresy","education","public"}),
			T("Sanctuary security review for valuables; locking discipline and witness rules enforced.","yellow",{"security","shrine","service"}),
			T("Investigation of tainted incense vendors; samples seized and chain-of-custody logged.","yellow",{"poison","investigation","service"}),
			T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.","red",{"poison","crisis","security"}),
			T("High-risk protection of a central temple during unrest; entry control and calm liturgy.","red",{"unrest","security","crowd"}),
		}
	},

	{key="CIC", name="Court of the Ironclad",
		link=orderLink("Fenrir (Reformed Stripping Path)","Court_of_the_Ironclad","Court of the Ironclad"),
		tasks={
			T("Discipline drill for shrine wardens; non-lethal control and restraint practices.","green",{"training","security","service"}),
			T("Escort for a threatened courier; route cleared and handover documented.","green",{"escort","security","service"}),
			T("Patrol coordination with civic guards at terminals; hand signals and cordon rules standardized.","green",{"rail","security","service"}),
			T("Gatehouse posture review for festival season; crowd lanes chalked and posted.","green",{"festival","crowd","security"}),
			T("Weapons discipline check for attached wardens; compliance logs signed.","green",{"audit","security","service"}),
			T("Guard coordination in a frontier district; watch rotations synchronized with civic militia.","yellow",{"militia","security","frontier"}),
			T("Immediate reinforcement during riot signals; cordons established and safe egress opened.","red",{"riot","crowd","security"}),
			T("Interdiction rehearsal for barricade incidents; safe corridors mapped.","yellow",{"barricade","security","public"}),
			T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.","red",{"siege","security","public"}),
			T("Protection detail for water or rail infrastructure after sabotage rumor; sentries posted.","yellow",{"sabotage","infrastructure","security"}),
		}
	},

	{key="ACC", name="Accord Concordia",
		link=orderLink("Lukedu (Reformed Stripping Path)","Accord_Concordia","Accord Concordia"),
		tasks={
			T("Public harmony dialogue; disputants guided to a signed concord recorded by Temple Bank scribes.","green",{"mediation","public","service"}),
			T("Garden pavilion mediation for family inheritance disputes; settlement sealed.","green",{"mediation","service"}),
			T("Civic cooperation workshop for rival guilds; shared calendar commitments set.","green",{"guild","coordination","service"}),
			T("Inter-shrine calendar alignment meeting; procession timings standardized.","green",{"balance","public","service"}),
			T("Quiet counseling for newly formed neighborhood councils; rules of debate issued.","green",{"governance","education","service"}),
			T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.","yellow",{"rite","logistics","service"}),
			T("Emergency reconciliation after faction clash; mediators embedded with escorts.","red",{"clash","crowd","security"}),
			T("Transit-lane mediation after station dispute; queues reset and monitored.","yellow",{"rail","queue","public"}),
			T("Harmonization response after rumor spike; joint bulletin with Harmony Sanctum circulated.","yellow",{"rumor","service","public"}),
			T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.","red",{"panic","crowd","public"}),
		}
	},

	{key="MSV", name="Mystery of the Stygian Veil",
		link=orderLink("Styx (Reformed Stripping Path)","Mystery_of_the_Stygian_Veil","Mystery of the Stygian Veil"),
		tasks={
			T("Funerary liaison and cemetery vigil support; grief rites standardized.","green",{"funeral","public","service"}),
			T("Registry update for burials and memorial offerings; records sealed and witnessed.","green",{"archive","audit","service"}),
			T("Transition counseling for displaced families; resettlement blessings recorded.","green",{"displacement","relief","service"}),
			T("Cemetery perimeter inspection; prevent vandalism during festival nights.","green",{"security","public","service"}),
			T("Quiet rites for recovered remains after an accident; soothe panic and rumor.","green",{"public","rumor","service"}),
			T("Investigation of graveyard desecration rumor; ward placement and witness interviews.","yellow",{"desecration","investigation","security"}),
			T("Relic transfer paperwork verified for a shrine move; escorts scheduled.","yellow",{"escort","security","service"}),
			T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.","red",{"provocation","crowd","security"}),
			T("High-risk escort of relics through hostile streets; secure route planning with wardens.","red",{"escort","security","public"}),
			T("Post-violence reconciliation vigil at a canal memorial; prevent retaliation cycles.","yellow",{"violence","public","mediation"}),
		}
	},

	{key="SYF", name="Sylvan Fellowship",
		link=orderLink("Faun (Reformed Stripping Path)","Sylvan_Fellowship","Sylvan Fellowship"),
		tasks={
			T("Forest-edge shrine maintenance and trail safety check; minor repairs logged.","green",{"trail","shrine","agri"}),
			T("Ecology rite with farmers: pest balance and water stewardship guidance.","green",{"ecology","agri","service"}),
			T("Wildlife boundary notices posted; discourage poaching near sacred groves.","green",{"public","security","agri"}),
			T("Community foraging orientation; prevent poisoning and rumor spirals.","green",{"education","health","agri"}),
			T("Riverbank cleanup day; improve water flow and reduce disease risk.","green",{"water","health","public"}),
			T("Containment of illegal logging or poaching rumor; community patrol coordination.","yellow",{"poaching","security","investigation"}),
			T("Fire season preparedness for woodland towns; firebreak rites and evacuation path marking.","yellow",{"wildfire","public","security"}),
			T("Blight liaison with Verdant Embrace; field sampling routes scheduled.","yellow",{"blight","agri","service"}),
			T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.","red",{"wildfire","crisis","public"}),
			T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.","red",{"threat","security","public"}),
		}
	},

	{key="MRM", name="Mystery of Red Mirth",
		link=orderLink("Silenus (Reformed Stripping Path)","Myster_of_Red_Mirth","Mystery of Red Mirth"),
		tasks={
			T("Morale rite and sanctioned celebration oversight; prevent disorder while restoring joy.","green",{"morale","public","service"}),
			T("Festival logistics support; safe routes and water points verified with civic stewards.","green",{"festival","crowd","public"}),
			T("Sanctioned tavern-chaplain circuit; reduce brawls and theft through presence.","green",{"public","security","service"}),
			T("Reconciliation feast for rival crews; agreements sealed before witnesses.","green",{"mediation","festival","service"}),
			T("Public song-rotation at market hours; discourage rumor panic with sanctioned refrains.","green",{"rumor","public","service"}),
			T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.","yellow",{"illicit","order","security"}),
			T("Crowd-density enforcement for a stadium day; steward posts assigned.","yellow",{"crowd","festival","security"}),
			T("Emergency morale stabilization after violence; controlled gatherings and escorts.","red",{"violence","crowd","public"}),
			T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.","red",{"riot","crowd","security"}),
			T("Post-panic comfort rites near transit hubs; prevent stampedes and retaliatory fights.","yellow",{"panic","rail","crowd"}),
		}
	},

	{key="RCA", name="Rex Catonis",
		link=orderLink("Cato (Reformed Stripping Path)","Rex_Catonis","Rex Catonis"),
		tasks={
			T("Civic order inspection: queue rules, market discipline, and shrine traffic flow.","green",{"order","queue","public"}),
			T("Courtroom courtesy briefing; witness etiquette and silence rules reinforced.","green",{"governance","public","service"}),
			T("Market weights-and-measures rite; deter fraud and stabilize pricing disputes.","green",{"audit","service","public"}),
			T("Transit node discipline review; platform lines repainted and stewards assigned.","green",{"rail","order","transit"}),
			T("Curfew signage and quiet-hours enforcement brief; reduce night disturbances.","green",{"security","public","service"}),
			T("Administrative compliance review after complaints; corrective edicts posted.","yellow",{"compliance","governance","service"}),
			T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.","yellow",{"security","governance","service"}),
			T("Emergency restoration of order after mob action; escorted dispersal and curfew-litany.","red",{"mob","crowd","security"}),
			T("High-risk support to authorities during barricade incident; perimeter discipline enforced.","red",{"barricade","security","public"}),
			T("Port and rail dispute arbitration; enforce lawful priority lanes for relief cargo.","yellow",{"port","rail","order","relief"}),
		}
	},

	{key="SDL", name="Sanctum Delphica",
		link=orderLink("Sisera (Reformed Stripping Path)","Sanctum_Delphica","Sanctum Delphica"),
		tasks={
			T("Oracle consultation clinic for officials; questions logged and sealed.","green",{"oracle","governance","service"}),
			T("Archive and library integrity check; catalog drift corrected and access verified.","green",{"archive","audit","service"}),
			T("Public instruction on lawful prophecy vs. pamphlet fraud; leaflets distributed.","green",{"education","public","service"}),
			T("Scribe training session; improve recordkeeping for relief and stipend flows.","green",{"education","audit","service"}),
			T("Observatory-liaison briefing with Conclave Illuminara; shared bulletins standardized.","green",{"service","archive","public"}),
			T("Investigation of forged prophecy pamphlets; counterfeit sources traced.","yellow",{"forgery","investigation","service"}),
			T("Archive sealing of a sensitive docket after unrest; access reduced and witnessed.","yellow",{"security","archive","service"}),
			T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.","red",{"broadcast","crisis","public"}),
			T("High-risk protection for a key temple scholar; escort and secure lodging arranged.","red",{"threat","security","service"}),
			T("Transit bulletin for corridor cities after omen panic; prevent stampedes and conspiracies.","yellow",{"rail","panic","public"}),
		}
	},

	{key="OAI", name="Ordo Amicitia",
		link=orderLink("Erasmus (Reformed Stripping Path)","Ordo_Amicitia","Ordo Amicitia"),
		tasks={
			T("Charity distribution and household check-ins; kindness register updated for stipend support.","green",{"charity","relief","service"}),
			T("Food-line volunteer training; prevent cutting and fights through posted rules.","green",{"relief","queue","public"}),
			T("Shelter visitation for displaced workers; job referrals routed through civic offices.","green",{"service","relief","governance"}),
			T("Agri stipend review for smallholders; vouchers revalidated and extensions logged.","green",{"agri","audit","service"}),
			T("Compassion circuit to hospitals and clinics; morale support and supply notes recorded.","green",{"health","service","relief"}),
			T("Offering logistics for a major rite; livestock and treasure accounted and sealed.","yellow",{"offering","audit","service"}),
			T("Mediation for neighborhood poverty disputes; aid allocations made transparent.","yellow",{"aid","mediation","service"}),
			T("Emergency relief surge after incident; soup lines sanctified and guarded.","red",{"relief","crowd","public"}),
			T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.","red",{"procession","security","public"}),
			T("Port-side charity intake after a ferry accident; registries updated and families notified.","yellow",{"port","relief","service"}),
		}
	},

	{key="TIL", name="Temple Illuminata",
		link=orderLink("Nikolaj (Reformed Stripping Path)","Temple_Illuminata","Temple Illuminata"),
		tasks={
			T("Lamp and beacon maintenance blessing; public wayfinding clarity restored.","green",{"beacon","transit","service"}),
			T("Infrastructure illumination audit for tunnels and transit nodes; repair notices issued.","green",{"infrastructure","transit","industry"}),
			T("Rail signal blessing at key junctions; reduce accidents and rumor panic.","green",{"rail","transit","public"}),
			T("Workshop visit: lantern production quality review; defective stock quarantined.","green",{"industry","audit","service"}),
			T("Public lantern-route map refreshed for festival season; prevent crush points.","green",{"festival","crowd","public"}),
			T("Investigation of signal tampering near a transport hub; seals replaced and watchers posted.","yellow",{"tamper","security","transit"}),
			T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.","red",{"sabotage","crisis","security"}),
			T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.","red",{"panic","crowd","transit"}),
			T("Port navigation-light inspection for fog days; harbor masters briefed.","yellow",{"port","infrastructure","service"}),
			T("Tunnel crowd-discipline drill; stewards assigned and entry metering practiced.","yellow",{"crowd","infrastructure","transit"}),
		}
	},
}


local orderByKey, orderByName, orderKeys, orderNames = {}, {}, {}, {}
for _, o in ipairs(orders) do
	orderByKey[o.key] = o
	orderByName[o.name] = o
	orderKeys[#orderKeys+1] = o.key
	orderNames[#orderNames+1] = o.name
end

--------------------------------------------------------------------------------
-- 7.1) ORDER ICONS (from Module:GeneralPortOperations addLogo mapping)
--------------------------------------------------------------------------------
local orderIconFile = {
	["Order of Aurora Mystica"]         = "Auroramystica.png",
	["Harmony Sanctum"]                = "Harmonysanctum.png",
	["Ignis Aeternum"]                 = "Ignis Aeternum.png",
	["Celestial Harmony Sect"]         = "HarmonySect.png",
	["Guild of Golden Shadows"]        = "GoldenShadows.png",
	["Azure Sentinel Sect"]            = "AzureSentinel.png",
	["Reverie Nebulous"]               = "RevNeb.png",
	["Eon Fellowship"]                 = "EonFel.png",
	["Order of the Umbral Oracle"]     = "Umbral.png",
	["Mystery of the Verdant Embrace"] = "Verdant.png",
	["Conclave Illuminara"]            = "Conclave.png",
	["Sanctum Vitalis"]                = "Vitalis.png",
	["Temple Alabaster"]               = "Alabaster.png",
	["Court of the Ironclad"]          = "Ironclad.png",
	["Accord Concordia"]               = "Concordia.png",
	["Mystery of the Stygian Veil"]    = "StygianVe.png",
	["Sylvan Fellowship"]              = "Sylvan.png",
	["Mystery of Red Mirth"]           = "MirthFlag.png",
	["Rex Catonis"]                    = "Catonis.png",
	["Sanctum Delphica"]               = "SanctumDel.png",
	["Ordo Amicitia"]                  = "OrdoFl.png",
	["Temple Illuminata"]              = "AureumFlag.png",
}

local function getOrderIconMarkup(orderName)
	local fn = orderIconFile[orderName]
	if fn and fileExists(fn) then
		return string.format("[[File:%s|20px]] ", fn)
	end
	return ""
end

--------------------------------------------------------------------------------
-- 8) CITY PROFILES (complete)
--------------------------------------------------------------------------------
local cityProfiles = {
	-- =========================
	-- CORE MAJORS
	-- =========================
	["Vaeringheim"] = {
		tags={"capital","admin","port","templebank","canals"},
		weights={GGS=4,HSA=3,RCA=3,ASS=2,CIL=2,CHS=2,OSI=1},
	},
	["Luminaria"] = {
		tags={"industry","rollingstock","innovation","dawn","infrastructure"},
		weights={TIL=4,OAM=3,EOF=2,IGA=2,OSI=1,GGS=1},
	},
	["Serena"] = {
		tags={"civic","lake","hospitality","public"},
		weights={HSA=3,ACC=2,OAI=2,ASS=1,CHS=1,OSI=1},
	},
	["Pyralis"] = {
		tags={"volcanic","hazard","fire","reconstruction"},
		weights={IGA=4,ASS=2,SCV=2,EOF=1,CIC=1},
		baseSev="yellow",
	},
	["Symphonara"] = {
		tags={"civic","arts","festival","public"},
		weights={MRM=3,ACC=2,OSI=2,OAM=2,HSA=1,RCA=1},
	},
	["Aurelia"] = {
		tags={"civic","trade","public"},
		weights={HSA=2,CHS=2,GGS=2,ACC=1,OSI=1},
	},
	["Somniumpolis"] = {
		tags={"finance","dream","rumorcontrol","noctic"},
		weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1},
		baseSev="yellow",
	},
	["Nexa"] = {
		tags={"market","trade","logistics"},
		weights={GGS=3,HSA=2,CHS=2,RCA=1,ASS=1,ACC=1},
	},
	["Lunalis Sancta"] = {
		tags={"lunar","archives","pilgrimage","omens"},
		weights={CIL=4,REN=2,UOR=2,SDL=1,HSA=1},
	},
	["Sylvapolis"] = {
		tags={"forest","garden","ecology","shrinework"},
		weights={SYF=3,MVE=3,OAI=1,HSA=1,ASS=1},
	},

	-- =========================
	-- CORE MINORS
	-- =========================
	["Saluria"] = {
		tags={"civic","artisan","wildlife"},
		weights={SYF=2,CHS=2,ACC=1,HSA=1,OAI=1},
	},
	["Aetherium"] = {
		tags={"sky","omens","education"},
		weights={CIL=2,OAM=2,CHS=2,OSI=1},
	},
	["Ferrum Citadel"] = {
		tags={"fortress","industry","security","infrastructure"},
		weights={CIC=3,RCA=2,TIL=2,EOF=1,IGA=1},
		baseSev="yellow",
	},
	["Acheron"] = {
		tags={"frontier","containment","mines","security"},
		weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1,RCA=1},
		baseSev="yellow",
	},
	["Erythros"] = {
		tags={"corridor","trade","security"},
		weights={GGS=2,RCA=2,TIL=2,ASS=1,IGA=1},
	},
	["Catonis Atrium"] = {
		tags={"civic","law","order"},
		weights={RCA=4,HSA=1,CHS=1,ACC=1},
	},
	["Delphica"] = {
		tags={"oracle","archives","scholarship"},
		weights={SDL=4,CIL=2,CHS=1,HSA=1},
	},
	["Koinonía"] = {
		tags={"harmony","mediation","civic"},
		weights={ACC=3,HSA=3,CHS=1,OAI=1},
	},
	["Aureum"] = {
		tags={"wealth","craft","trade","finance"},
		weights={GGS=4,HSA=1,RCA=1,ALB=1},
	},

	-- =========================
	-- ALPERKIN
	-- =========================
	["The Alpazkigz"] = {
		tags={"tribal","festival","intercession","mediation"},
		weights={ACC=3,HSA=2,CHS=2,OAI=1,CIC=1},
		baseSev="yellow",
	},
	["Odiferia"] = {
		tags={"noctic","rumorcontrol","dream","folk"},
		weights={REN=3,HSA=2,GGS=2,UOR=2,MSV=1},
		baseSev="yellow",
	},

	-- =========================
	-- NEW SOUTH JANGSONG (MAJOR)
	-- =========================
	["Skýrophos"] = {
		tags={"integration","civic","corridor","governance"},
		weights={CHS=2,HSA=2,RCA=2,TIL=2,EOF=2,ACC=1},
	},
	["Bjornopolis"] = {
		tags={"north","fortified","industry","civic"},
		weights={EOF=3,CIC=2,TIL=2,ASS=1,RCA=1},
		baseSev="yellow",
	},
	["Aegirheim"] = {
		tags={"north","coastal","harbor","watch"},
		weights={ASS=3,EOF=2,CIC=1,TIL=1},
		baseSev="yellow",
	},
	["Norsolyra"] = {
		tags={"agriculture","wetlands","river","supply"},
		weights={MVE=3,ASS=2,SYF=2,OAI=1,SCV=1},
	},
	["Thorsalon"] = {
		tags={"north","vineyards","festival","supply"},
		weights={MVE=2,MRM=2,EOF=2,OAI=1,ASS=1},
		baseSev="yellow",
	},

	-- =========================
	-- NEW SOUTH JANGSONG (MINOR)
	-- =========================
	["Pelagia"] = {
		tags={"coastal","distribution","water","relief"},
		weights={ASS=3,SCV=2,HSA=1,EOF=1},
	},
	["Myrene"] = {
		tags={"coastal","civic","trade"},
		weights={ASS=2,HSA=2,CHS=1,ACC=1},
	},
	["Thyrea"] = {
		tags={"agriculture","market","relief"},
		weights={MVE=3,OAI=2,SYF=1,HSA=1},
	},
	["Ephyra"] = {
		tags={"forest","frontier","watch","omens"},
		weights={SYF=3,UOR=2,EOF=1,CIC=1},
		baseSev="yellow",
	},
	["Halicarn"] = {
		tags={"archives","scholarship","civic"},
		weights={SDL=3,CIL=2,CHS=1,HSA=1},
	},

	-- =========================
	-- HAIFAN BASSARIDIA (MAJOR)
	-- =========================
	["Keybir-Aviv"] = {
		tags={"port","trade","gateway","security"},
		weights={ASS=3,GGS=2,HSA=2,RCA=1,CIC=1},
		baseSev="yellow",
	},
	["Tel-Amin"] = {
		tags={"plantation","coffee","trade","agriculture"},
		weights={MVE=3,OAI=2,SYF=2,GGS=1,ASS=1},
	},
	["Diamandis"] = {
		tags={"luxury","trade","vaulting","security"},
		weights={GGS=4,RCA=2,ALB=1,HSA=1},
		baseSev="yellow",
	},
	["Jogi"] = {
		tags={"agroprocessing","market","ferry","trade"},
		weights={MVE=3,OAI=2,ASS=2,SYF=1,GGS=1},
	},
	["Lewisburg"] = {
		tags={"rail","hub","logistics","governance"},
		weights={TIL=4,RCA=3,GGS=2,HSA=1,EOF=1,ASS=1},
		baseSev="yellow",
	},

	-- =========================
	-- HAIFAN BASSARIDIA (MINOR)
	-- =========================
	["Thermosalem"] = {
		tags={"healing","resort","water","crowd"},
		weights={SCV=3,ASS=2,HSA=2,ACC=1,RCA=1},
		baseSev="yellow",
	},
	["Akróstadium"] = {
		tags={"stadium","festival","crowd","public"},
		weights={MRM=3,OSI=2,ACC=2,HSA=1},
		baseSev="yellow",
	},
	["Sufriya"] = {
		tags={"terminal","security","port","logistics"},
		weights={ASS=3,GGS=2,CIC=2,RCA=1,TIL=1},
		baseSev="yellow",
	},
	["Lykopolis"] = {
		tags={"rail","corridor","security","logistics"},
		weights={TIL=4,RCA=2,EOF=2,CIC=1,GGS=1},
		baseSev="yellow",
	},

	-- =========================
	-- NORMARK
	-- =========================
	["Ardclach"] = {
		tags={"north","ferry","coastal","watch"},
		weights={ASS=3,EOF=2,CIC=1,GGS=1},
		baseSev="yellow",
	},
	["Riddersborg"] = {
		tags={"north","rail","corridor","storm"},
		weights={EOF=3,TIL=2,CIC=2,ASS=1,RCA=1},
		baseSev="yellow",
	},

	-- =========================
	-- OURIANA
	-- =========================
	["Bashkim"] = {
		tags={"rail","corridor","agriculture","logistics"},
		weights={MVE=2,TIL=3,RCA=2,OAI=1,EOF=1},
		baseSev="yellow",
	},
	["Ourid"] = {
		tags={"river","fisheries","supply","trade"},
		weights={ASS=3,MVE=2,OAI=2,HSA=1},
	},
	["Tonar"] = {
		tags={"quarantine","doctrine","containment","security"},
		weights={ALB=3,CIC=3,RCA=2,HSA=1,SDL=1},
		baseSev="red",
	},

	-- =========================
	-- CALEDONIAN DEPENDENCIES
	-- =========================
	["Fanghorn"] = {
		tags={"frontier","forests","security","watch"},
		weights={EOF=3,CIC=2,SYF=2,UOR=1,RCA=1},
		baseSev="yellow",
	},
	["Eikbu"] = {
		tags={"ranching","agriculture","trail","supply"},
		weights={MVE=3,SYF=2,OAI=2,EOF=1},
	},
	["Galvø"] = {
		tags={"island","coastal","port","storm"},
		weights={ASS=3,EOF=2,CIC=1},
		baseSev="yellow",
	},
	["Sårensby"] = {
		tags={"rail","corridor","industry","civic"},
		weights={TIL=3,EOF=2,RCA=2,GGS=1,ASS=1},
		baseSev="yellow",
	},
	["Slevik"] = {
		tags={"north","security","civic"},
		weights={EOF=3,CIC=2,ASS=1,RCA=1},
		baseSev="yellow",
	},
	["Sjøsborg"] = {
		tags={"coastal","port","trade","watch"},
		weights={ASS=3,EOF=2,GGS=1},
		baseSev="yellow",
	},
	["Storesund"] = {
		tags={"coastal","trade","storm"},
		weights={ASS=3,EOF=2,CIC=1},
		baseSev="yellow",
	},
	["Notranskja"] = {
		tags={"north","frontier","watch","omens"},
		weights={EOF=2,CIC=2,UOR=2,ASS=1},
		baseSev="yellow",
	},
	["Kaledonija"] = {
		tags={"regionalhub","governance","trade","civic"},
		weights={RCA=3,EOF=2,GGS=2,HSA=1,ASS=1},
		baseSev="yellow",
	},
}

--------------------------------------------------------------------------------
-- 9) ECONOMIC SIGNALS: parse "Company Table" on General Port page
--------------------------------------------------------------------------------
local econCache = nil

local function sectorOf(industryCell)
	local s = (industryCell or ""):lower()
	if s:find("agric", 1, true) then return "Agricultural" end
	if s:find("service", 1, true) then return "Service" end
	if s:find("manufact", 1, true) or s:find("mining", 1, true) or s:find("energy", 1, true) then
		return "Industry"
	end
	return "Other"
end

local function extractCompanyName(cell)
	local n = cell:match("'''(.-)'''")
	if n and n ~= "" then return stripWiki(n) end
	n = cell:match("%*%*(.-)%*%*")
	if n and n ~= "" then return stripWiki(n) end
	n = stripWiki(cell)
	if #n > 80 then n = n:sub(1, 80) .. "…" end
	return n
end

local function extractHQ(cell)
	local s = stripWiki(cell)
	if s == "" then return nil end
	if s:lower():find("fleet", 1, true) then return nil end
	if s:lower():find("no fixed", 1, true) then return nil end
	s = s:gsub("%s*;%s*.*$", "")
	s = s:gsub("%s*%b()%s*", " ")
	s = normalizeSpaces(s)
	s = trim(s)
	if citySet[s] then return s end
	return nil
end

local function parseCompanyTable(wikitext)
	local signals = {}
	for cname, _ in pairs(citySet) do
		signals[cname] = {Agricultural=0, Service=0, Industry=0, Other=0, samples={}}
	end
	if type(wikitext) ~= "string" then return signals end

	local startPos = wikitext:find("====%s*Company Table%s*====")
	if not startPos then startPos = wikitext:find("####%s*Company Table") end
	if not startPos then return signals end

	local tablePos = wikitext:find("{|", startPos)
	if not tablePos then return signals end
	local endPos = wikitext:find("|}", tablePos)
	if not endPos then return signals end

	local tableText = wikitext:sub(tablePos, endPos + 1)

	local rows = {}
	for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
		rows[#rows+1] = chunk
	end
	local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
	if lastChunk then rows[#rows+1] = lastChunk end

	for _, row in ipairs(rows) do
		if not row:match("^%s*!") then
			local line = normalizeSpaces(row:gsub("\n", " "))
			line = line:gsub("^%s*|%s*", "")

			local cells = {}
			for cell in line:gmatch("([^|]-)%s*||%s*") do
				cells[#cells+1] = trim(cell)
			end
			local tail = line:match("||%s*([^|].-)%s*$")
			if tail and tail ~= "" then
				if #cells == 0 or cells[#cells] ~= trim(tail) then
					cells[#cells+1] = trim(tail)
				end
			end

			local industryCell = ""
			for _, c in ipairs(cells) do
				if containsAny(c, {"agric", "service", "manufact", "mining", "energy"}) then
					industryCell = c
					break
				end
			end
			local sector = sectorOf(industryCell)

			local hqCell = cells[#cells] or ""
			local hq = extractHQ(hqCell)
			if hq and signals[hq] then
				signals[hq][sector] = (signals[hq][sector] or 0) + 1
				local nameCell = cells[3] or cells[2] or cells[1] or ""
				local companyName = extractCompanyName(nameCell)
				if companyName ~= "" and #signals[hq].samples < 2 then
					signals[hq].samples[#signals[hq].samples+1] = companyName
				end
			end
		end
	end

	return signals
end

local function getEconSignals()
	if econCache then return econCache end
	local content = safeTitleContent("General Port of Lake Morovia")
	econCache = parseCompanyTable(content)
	return econCache
end

local function dominantSector(sig)
	if not sig then return "Other" end
	local best, bestN = "Other", -1
	for _, k in ipairs({"Agricultural","Industry","Service","Other"}) do
		local n = sig[k] or 0
		if n > bestN then best, bestN = k, n end
	end
	return best
end

local function econTagFromSector(sec)
	if sec == "Agricultural" then return "agri" end
	if sec == "Industry" then return "industry" end
	if sec == "Service" then return "service" end
	return "mixed"
end

local function econSummary(sig)
	if not sig then return "n/a" end
	local a = sig.Agricultural or 0
	local i = sig.Industry or 0
	local s = sig.Service or 0
	local o = sig.Other or 0
	local dom = dominantSector(sig)
	local samp = (sig.samples and #sig.samples > 0) and ("; e.g., " .. table.concat(sig.samples, ", ")) or ""
	return string.format("%s (%d agri / %d ind / %d serv / %d other)%s", dom, a, i, s, o, samp)
end

--------------------------------------------------------------------------------
-- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
	local out = {}
	if type(wikitext) ~= "string" then return out end
	for line in wikitext:gmatch("[^\n]+") do
		if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
			local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
			if city and urg then
				out[stripWiki(city)] = {urg=trim(urg), event=stripWiki(event or "")}
			end
		end
	end
	return out
end

local function threatSev(th)
	if not th or not th.urg then return nil end
	local u = (th.urg or ""):upper()
	if u == "N/A" or u == "LOW" then return "green" end
	if u == "MEDIUM" then return "yellow" end
	if u == "HIGH" or u == "CRITICAL" then return "red" end
	return "yellow"
end

--------------------------------------------------------------------------------
-- 11) WEIGHTED ORDER PICK + TASK PICK
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
	local total = 0
	for _, ok in ipairs(orderKeys) do
		total = total + (weights[ok] or 0)
	end
	if total <= 0 then
		return pickFromList(nextInt, orderKeys, nil)
	end
	local r = (nextInt() % total) + 1
	local acc = 0
	for _, ok in ipairs(orderKeys) do
		acc = acc + (weights[ok] or 0)
		if r <= acc then return ok end
	end
	return orderKeys[1]
end

local function buildTagSet(tags)
	local set = {}
	for _, t in ipairs(tags or {}) do set[t] = true end
	return set
end

local function pickTaskForCity(nextInt, taskList, tagSet)
	local candidates = {}
	for _, task in ipairs(taskList or {}) do
		local ok = false
		for _, tg in ipairs(task.tags or {}) do
			if tagSet[tg] then ok = true break end
		end
		if ok then candidates[#candidates+1] = task end
	end
	if #candidates > 0 then
		return pickFromList(nextInt, candidates, nil)
	end
	return pickFromList(nextInt, taskList, nil)
end

local function baseWeights()
	local w = {}
	for _, ok in ipairs(orderKeys) do w[ok] = 1 end
	return w
end

local function applyWeights(w, add)
	if not add then return end
	for k, v in pairs(add) do
		if orderByKey[k] then
			w[k] = (w[k] or 0) + tonumber(v or 0)
		end
	end
end

local function biasFromEcon(w, econTag)
	if econTag == "agri" then
		applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1})
	elseif econTag == "industry" then
		applyWeights(w, {TIL=3,EOF=2,IGA=2,CIC=1,RCA=1})
	elseif econTag == "service" then
		applyWeights(w, {GGS=3,HSA=2,RCA=2,CIL=1,CHS=1})
	else
		applyWeights(w, {CHS=1,HSA=1,ASS=1})
	end
end

local function biasFromCorridor(w, isRail, isFerry)
	if isRail then applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1}) end
	if isFerry then applyWeights(w, {ASS=2,GGS=1,RCA=1}) end
end

local function biasFromTier(w, tier)
	if tier == "major" then
		applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
	end
end

--------------------------------------------------------------------------------
-- 12) TEMPLE BANK NOTES
--------------------------------------------------------------------------------
local bankNotes = {
	"Temple Bank stipend verified; local shrine receipts reconciled; next audit window scheduled.",
	"Voucher authorizations issued for ritual materials; chain-of-custody notes logged for any seized items.",
	"Investor liaison note filed; payroll routing confirmed for the deployed unit; compliance clerk assigned.",
	"Donation flow stabilized; offertory seals inspected; variance thresholds set for the next three days.",
	"Emergency stipend line opened for crowd-control and relief logistics; disbursement witnessed by two scribes.",
}

--------------------------------------------------------------------------------
-- 13) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
local function inferSeverity(cityProfile, econTag, taskSev, threatLevel)
	local sev = taskSev or "green"

	if cityProfile and cityProfile.baseSev then
		sev = cityProfile.baseSev
	end

	if econTag == "industry" or econTag == "service" then
		if sev == "green" then sev = "yellow" end
	end

	if threatLevel == "yellow" and sev == "green" then sev = "yellow" end
	if threatLevel == "red" and sev ~= "red" then sev = "red" end

	return sev
end

local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
	local cityName, cityRegion, tier = c.name, c.region, c.tier
	local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
	local nextInt = lcg(hash31(seedStr))

	local profile = cityProfiles[cityName] or {tags={}, weights=nil}
	local tags = {}

	for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
	if RAIL[cityName] then tags[#tags+1] = "rail" end
	if FERRY[cityName] then tags[#tags+1] = "ferry" end

	local sig = econSignals and econSignals[cityName] or nil
	local dom = dominantSector(sig)
	local econTag = econTagFromSector(dom)
	tags[#tags+1] = econTag

	local tagSet = buildTagSet(tags)

	local th = includeThreat and threatMap and threatMap[cityName] or nil
	local tSev = threatSev(th)

	local w = baseWeights()
	biasFromTier(w, tier)
	biasFromEcon(w, econTag)
	biasFromCorridor(w, RAIL[cityName], FERRY[cityName])
	applyWeights(w, profile.weights)

	local orderKey = weightedPickKey(nextInt, w)
	local o = orderByKey[orderKey] or orders[1]
	local task = pickTaskForCity(nextInt, o.tasks, tagSet)

	local sev = inferSeverity(profile, econTag, task.sev, tSev)

	local unitName = pickOrgUnit(nextInt, sev)
	local unitMeta = org[unitName]
	local unitIndex = (nextInt() % 12) + 1
	local unitCode = string.format("%s-%s%d", o.key, unitMeta.code, unitIndex)
	local headcount = unitMeta.size

	local bank = pickFromList(nextInt, bankNotes, nil)

	local threatText, threatUrg = "", ""
	if includeThreat and th then
		threatText = th.event or ""
		threatUrg = th.urg or ""
	end

	return {
		style = sevStyle(sev),
		date = di.text,
		region = cityRegion,
		city = string.format("%s %s", getCityFlagMarkup(cityName), cityLink(cityName)),
		order = getOrderIconMarkup(o.name) .. o.link,
		unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode),
		task = task.text,
		tags = joinTags(tags),
		econ = (showEcon and sig) and econSummary(sig) or "",
		bank = bank,
		threat = threatText,
		threatUrg = threatUrg,
		sev = sev,
	}
end

--------------------------------------------------------------------------------
-- 14) MAIN TABLE
--------------------------------------------------------------------------------
function p.makeTable(frame)
	local args = (frame and frame.args) or {}
	local regionFilter = tostring(args.region or "All")
	local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
	local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
	local limit = tonumber(args.limit or "")

	local di = getDateInfo()
	local econSignals = getEconSignals()

	local threatMap = {}
	if includeThreat then
		local ok, rel = pcall(require, "Module:ReligiousEvents")
		if ok and rel and type(rel.makeTable) == "function" then
			threatMap = parseReligiousEventsTable(rel.makeTable())
		end
	end

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Cult Order !! Unit !! Tasking"
	if showEcon then header = header .. " !! Economic signal" end
	header = header .. " !! Temple Bank Notes"
	if includeThreat then header = header .. " !! Threat Watch" end
	header = header .. " !! Severity"
	table.insert(out, header)

	local count = 0
	for _, c in ipairs(cityOrder) do
		if regionPass(regionFilter, c.region) then
			local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
			table.insert(out, "|-" .. row.style)

			local line = string.format("| %s || %s || %s || %s || %s || %s",
				row.date, row.city, row.region, row.order, row.unit, row.task
			)
			if showEcon then
				line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
			end
			line = line .. " || " .. row.bank
			if includeThreat then
				local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
				line = line .. " || " .. threatCell
			end
			line = line .. " || " .. row.sev

			table.insert(out, line)

			count = count + 1
			if limit and count >= limit then break end
		end
	end

	table.insert(out, "|}")

	if showLegend then
		table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%%;'>")
		table.insert(out, "<b>Legend:</b> ")
		table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine rites / civic support&nbsp; ")
		table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Heightened oversight / rumor control / targeted intervention&nbsp; ")
		table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Crisis response / unrest / outbreak / high-risk operations")
		table.insert(out, "</div>")
	end

	if di.calendarEvent and di.calendarEvent ~= "" then
		table.insert(out, string.format("\n<div style='margin-top:0.5em; font-size:90%%;'><b>Calendar note:</b> %s</div>", di.calendarEvent))
	end

	return table.concat(out, "\n")
end

--------------------------------------------------------------------------------
-- 15) SINGLE CITY VIEW
--------------------------------------------------------------------------------
function p.makeCity(frame)
	local args = (frame and frame.args) or {}
	local city = tostring(args.city or "")
	if city == "" then return "''Error: city parameter required.''" end
	if not citySet[city] then return "''Error: unknown city (must match canonical name).''" end

	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
	local showEcon = tostring(args.showEcon or "no"):lower() == "yes"

	local di = getDateInfo()
	local econSignals = getEconSignals()

	local threatMap = {}
	if includeThreat then
		local ok, rel = pcall(require, "Module:ReligiousEvents")
		if ok and rel and type(rel.makeTable) == "function" then
			threatMap = parseReligiousEventsTable(rel.makeTable())
		end
	end

	local region, tier = "Core", "minor"
	for _, c in ipairs(cityOrder) do
		if c.name == city then region, tier = c.region, c.tier break end
	end

	local row = buildCityRow(di, {name=city, region=region, tier=tier}, econSignals, threatMap, includeThreat, showEcon)

	local out = {}
	table.insert(out, "{| class='wikitable'")
	local header = "! Date !! City !! Region !! Cult Order !! Unit !! Tasking"
	if showEcon then header = header .. " !! Economic signal" end
	header = header .. " !! Temple Bank Notes"
	if includeThreat then header = header .. " !! Threat Watch" end
	header = header .. " !! Severity"
	table.insert(out, header)
	table.insert(out, "|-" .. row.style)

	local line = string.format("| %s || %s || %s || %s || %s || %s",
		row.date, row.city, row.region, row.order, row.unit, row.task
	)
	if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
	line = line .. " || " .. row.bank
	if includeThreat then
		local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
		line = line .. " || " .. threatCell
	end
	line = line .. " || " .. row.sev
	table.insert(out, line)

	table.insert(out, "|}")
	return table.concat(out, "\n")
end

--------------------------------------------------------------------------------
-- 16) ORDER-ONLY VIEW
--------------------------------------------------------------------------------
function p.makeOrder(frame)
	local args = (frame and frame.args) or {}
	local wanted = tostring(args.order or "")
	if wanted == "" then
		return "''Error: order parameter required (use exact order name).''"
	end
	local wantedOrder = orderByName[wanted]
	if not wantedOrder then
		return "''Error: unknown order name (must match module order names).''"
	end

	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
	local showEcon = tostring(args.showEcon or "no"):lower() == "yes"

	local di = getDateInfo()
	local econSignals = getEconSignals()

	local threatMap = {}
	if includeThreat then
		local ok, rel = pcall(require, "Module:ReligiousEvents")
		if ok and rel and type(rel.makeTable) == "function" then
			threatMap = parseReligiousEventsTable(rel.makeTable())
		end
	end

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! Date !! City !! Region !! Unit !! Tasking"
	if showEcon then header = header .. " !! Economic signal" end
	header = header .. " !! Temple Bank Notes"
	if includeThreat then header = header .. " !! Threat Watch" end
	header = header .. " !! Severity"
	table.insert(out, header)

	for _, c in ipairs(cityOrder) do
		local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
		if row.order:find(wantedOrder.link, 1, true) then
			table.insert(out, "|-" .. row.style)
			local line = string.format("| %s || %s || %s || %s || %s",
				row.date, row.city, row.region, row.unit, row.task
			)
			if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
			line = line .. " || " .. row.bank
			if includeThreat then
				local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
				line = line .. " || " .. threatCell
			end
			line = line .. " || " .. row.sev
			table.insert(out, line)
		end
	end

	table.insert(out, "|}")
	return table.concat(out, "\n")
end

return p