Module:CultRelationsDailyBVR

From MicrasWiki
Jump to navigationJump to search

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


-- Module:CultRelationsDailyBVR
-- Dynamic cult-relations index for the Reformed Stripping Path.
--
-- This module:
--   * Uses [[Module:BassaridianCalendar]] for the current PSSC date.
--   * Stores a fixed roster of cults/mysteries from Planetary Devotion.
--   * Starts from baseline alliances and rivalries.
--   * Applies deterministic daily events so relations change automatically by day.
--   * Provides table, pair, matrix, event-log, and raw-data functions.
--
-- Suggested invocations:
--   {{#invoke:CultRelationsDailyBVR|makeTable}}
--   {{#invoke:CultRelationsDailyBVR|makeTable|limit=30}}
--   {{#invoke:CultRelationsDailyBVR|makeTable|year=53|day=95}}
--   {{#invoke:CultRelationsDailyBVR|makePair|a=Harmony Sanctum|b=Ignis Aeternum}}
--   {{#invoke:CultRelationsDailyBVR|eventLog}}
--   {{#invoke:CultRelationsDailyBVR|makeMatrix}}
--   {{#invoke:CultRelationsDailyBVR|legend}}
--
-- Optional bridge from [[Module:MissionaryDailyOpsBVR]]:
--   local cultRel = require("Module:CultRelationsDailyBVR")
--   cultRel.makeTable(frame)

local p = {}

local calOk, cal = pcall(require, "Module:BassaridianCalendar")
if not calOk then
	cal = nil
end

local DAYS_IN_YEAR = 183

--------------------------------------------------------------------------------
-- 1. BASIC UTILITIES
--------------------------------------------------------------------------------

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

local function lower(s)
	return mw.ustring.lower(trim(s))
end

local function clamp(n, lo, hi)
	n = tonumber(n) or 0
	if n < lo then return lo end
	if n > hi then return hi end
	return n
end

local function sortedPair(a, b)
	a, b = trim(a), trim(b)
	if a < b then
		return a .. "||" .. b
	end
	return b .. "||" .. a
end

local function splitPairKey(k)
	local a, b = tostring(k or ""):match("^(.-)%|%|(.*)$")
	return a or "", b or ""
end

local function tableContains(t, value)
	for _, v in ipairs(t or {}) do
		if v == value then
			return true
		end
	end
	return false
end

local function shallowCopy(t)
	local out = {}
	for k, v in pairs(t or {}) do
		out[k] = v
	end
	return out
end

local function hash31(str)
	str = tostring(str or "")
	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 = tonumber(seed) or 1
	s = s % 2147483647
	if s <= 0 then
		s = s + 2147483646
	end
	return function()
		s = (1103515245 * s + 12345) % 2147483647
		return s
	end
end

local function pick(nextInt, list)
	if not list or #list == 0 then
		return nil
	end
	return list[(nextInt() % #list) + 1]
end

local function pickWeighted(nextInt, list)
	local total = 0
	for _, item in ipairs(list or {}) do
		total = total + (tonumber(item.w) or 0)
	end

	if total <= 0 then
		return list and list[1] or nil
	end

	local r = nextInt() % total
	local acc = 0
	for _, item in ipairs(list) do
		acc = acc + (tonumber(item.w) or 0)
		if r < acc then
			return item
		end
	end

	return list[#list]
end

--------------------------------------------------------------------------------
-- 2. DATE HANDLING
--------------------------------------------------------------------------------

local monthNames = {
	[1] = "Atosiel",
	[2] = "Thalassiel",
	[3] = "Opsitheiel"
}

local zodiacByDay = {
	[1] = {
		{12, "Atosien"},
		{24, "Eosena"},
		{36, "Micrasha"},
		{48, "Pyreska"},
		{61, "Indomin"}
	},
	[2] = {
		{12, "Chrysen"},
		{24, "Thalassian"},
		{36, "Nephelia"},
		{48, "Glinaeus"},
		{61, "Noctien"}
	},
	[3] = {
		{12, "Opsithia"},
		{24, "Stygian"},
		{36, "Faunian"},
		{48, "Silenian"},
		{61, "Catosien"}
	}
}

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

local function monthDomToDoy(month, dom)
	month = tonumber(month) or 1
	dom = tonumber(dom) or 1
	if month == 1 then
		return clamp(dom, 1, 61)
	elseif month == 2 then
		return 61 + clamp(dom, 1, 61)
	else
		return 122 + clamp(dom, 1, 61)
	end
end

local function zodiacFor(monthNum, dom)
	local rows = zodiacByDay[monthNum] or zodiacByDay[1]
	for _, row in ipairs(rows) do
		if dom <= row[1] then
			return row[2]
		end
	end
	return rows[#rows][2]
end

local function parseCalendarString(raw)
	raw = tostring(raw or "")

	-- Expected from getCurrentDateNoProverb:
	-- "95, Thalassiel (Noctien), 53 PSSC"
	local doy, month, zodiac, year = raw:match("^(%d+),%s*([%a]+)%s*%(([^%)]+)%),%s*(%d+)%s*PSSC")
	if doy and month and year then
		local d = tonumber(doy)
		local y = tonumber(year)
		local m = 1
		if month == "Thalassiel" then
			m = 2
		elseif month == "Opsitheiel" then
			m = 3
		end
		local mn, dom = doyToMonthDom(d)
		return {
			year = y,
			dayOfYear = d,
			monthNum = mn,
			month = monthNames[mn],
			dom = dom,
			zodiac = zodiac or zodiacFor(mn, dom),
			raw = raw,
			text = string.format("%d/%d/%d PSSC", dom, mn, y),
			longText = string.format("%d, %s (%s), %d PSSC", d, monthNames[mn], zodiac or zodiacFor(mn, dom), y)
		}
	end

	-- Accept a simpler "day/month/year PSSC" pattern.
	local dom2, month2, year2 = raw:match("^(%d+)/(%d+)/(%d+)%s*PSSC")
	if dom2 and month2 and year2 then
		local mn = clamp(tonumber(month2), 1, 3)
		local dm = clamp(tonumber(dom2), 1, 61)
		local y = tonumber(year2)
		local d = monthDomToDoy(mn, dm)
		local z = zodiacFor(mn, dm)
		return {
			year = y,
			dayOfYear = d,
			monthNum = mn,
			month = monthNames[mn],
			dom = dm,
			zodiac = z,
			raw = raw,
			text = string.format("%d/%d/%d PSSC", dm, mn, y),
			longText = string.format("%d, %s (%s), %d PSSC", d, monthNames[mn], z, y)
		}
	end

	return nil
end

local function getDateInfo(args)
	args = args or {}

	local y = tonumber(args.year or args.psscyear)
	local doy = tonumber(args.day or args.dayofyear or args.doy)

	if y and doy then
		doy = clamp(doy, 1, DAYS_IN_YEAR)
		local mn, dom = doyToMonthDom(doy)
		local z = zodiacFor(mn, dom)
		return {
			year = y,
			dayOfYear = doy,
			monthNum = mn,
			month = monthNames[mn],
			dom = dom,
			zodiac = z,
			raw = string.format("%d, %s (%s), %d PSSC", doy, monthNames[mn], z, y),
			text = string.format("%d/%d/%d PSSC", dom, mn, y),
			longText = string.format("%d, %s (%s), %d PSSC", doy, monthNames[mn], z, y)
		}
	end

	local raw = ""
	if cal and type(cal.getCurrentDateNoProverb) == "function" then
		raw = cal.getCurrentDateNoProverb()
	elseif cal and type(cal.getCurrentDate) == "function" then
		raw = cal.getCurrentDate()
	end

	local parsed = parseCalendarString(raw)
	if parsed then
		return parsed
	end

	-- Stable fallback if the calendar module is unavailable.
	local fallbackYear = 53
	local fallbackDay = 1
	local mn, dom = doyToMonthDom(fallbackDay)
	local z = zodiacFor(mn, dom)

	return {
		year = fallbackYear,
		dayOfYear = fallbackDay,
		monthNum = mn,
		month = monthNames[mn],
		dom = dom,
		zodiac = z,
		raw = "Calendar unavailable",
		text = string.format("%d/%d/%d PSSC", dom, mn, fallbackYear),
		longText = string.format("%d, %s (%s), %d PSSC", fallbackDay, monthNames[mn], z, fallbackYear)
	}
end

--------------------------------------------------------------------------------
-- 3. CULT ROSTER
-- Based on Planetary Devotion / Mystery table.
--------------------------------------------------------------------------------

local cults = {
	{
		key = "OSI",
		name = "Ordo Solis Invicti",
		divine = "Atos",
		city = "Vaeringheim",
		page = "Atos (Reformed Stripping Path)",
		anchor = "Ordo Solis Invicti",
		logo = "AtosSygil.png",
		tendency = {"order", "measure", "light", "state"}
	},
	{
		key = "AUR",
		name = "Order of Aurora Mystica",
		divine = "Eos",
		city = "Luminaria",
		page = "Eos (Reformed Stripping Path)",
		anchor = "Order of Aurora Mystica",
		logo = "Auroramystica.png",
		tendency = {"dawn", "renewal", "illumination", "mediation"}
	},
	{
		key = "HAR",
		name = "Harmony Sanctum",
		divine = "Micras",
		city = "Serena",
		page = "Micras (Reformed Stripping Path)",
		anchor = "Harmony Sanctum",
		logo = "Harmonysanctum.png",
		tendency = {"balance", "wisdom", "mediation", "order"}
	},
	{
		key = "IGN",
		name = "Ignis Aeternum",
		divine = "Pyros",
		city = "Pyralis",
		page = "Pyros (Reformed Stripping Path)",
		anchor = "Ignis Aeternum",
		logo = "Ignis Aeternum.png",
		tendency = {"fire", "renewal", "passion", "destruction"}
	},
	{
		key = "CHS",
		name = "Celestial Harmony Sect",
		divine = "Indigo and Momiji",
		city = "Symphonara",
		page = "Indigo (Reformed Stripping Path)",
		anchor = "Celestial Harmony Sect",
		logo = "HarmonySect.png",
		tendency = {"duality", "change", "mediation", "ambiguity"}
	},
	{
		key = "GGS",
		name = "Guild of Golden Shadows",
		divine = "Chrysos",
		city = "Aurelia",
		page = "Chrysos (Reformed Stripping Path)",
		anchor = "Guild of Golden Shadows",
		logo = "GoldenShadows.png",
		tendency = {"wealth", "trade", "secrecy", "espionage"}
	},
	{
		key = "AZU",
		name = "Azure Sentinel Sect",
		divine = "Thalassa",
		city = "Vaeringheim",
		page = "Thalassa (Reformed Stripping Path)",
		anchor = "Azure Sentinel Sect",
		logo = "AzureSentinel.png",
		tendency = {"sea", "war", "depth", "mystery"}
	},
	{
		key = "RNB",
		name = "Reverie Nebulous",
		divine = "Nephele",
		city = "Somniumpolis",
		page = "Nephele (Reformed Stripping Path)",
		anchor = "Reverie Nebulous",
		logo = "RevNeb.png",
		tendency = {"dreams", "clouds", "imagination", "omens"}
	},
	{
		key = "EOF",
		name = "Eon Fellowship",
		divine = "Glinos",
		city = "Nexa",
		page = "Glinos (Reformed Stripping Path)",
		anchor = "Eon Fellowship",
		logo = "EonFel.png",
		tendency = {"winter", "death", "cycle", "endurance"}
	},
	{
		key = "UMB",
		name = "Order of the Umbral Oracle",
		divine = "Noctis",
		city = "Lunalis Sancta",
		page = "Noctis (Reformed Stripping Path)",
		anchor = "Order of the Umbral Oracle",
		logo = "Umbral.png",
		tendency = {"night", "secrecy", "truth", "introspection"}
	},
	{
		key = "VDE",
		name = "Mystery of the Verdant Embrace",
		divine = "Opsithe",
		city = "Sylvapolis",
		page = "Opsithe (Reformed Stripping Path)",
		anchor = "Mystery of the Verdant Embrace",
		logo = "Verdant.png",
		tendency = {"harvest", "fertility", "nature", "stability"}
	},
	{
		key = "CON",
		name = "Conclave Illuminara",
		divine = "Tarsica",
		city = "Luminaria",
		page = "Tarsica (Reformed Stripping Path)",
		anchor = "Conclave Illuminara",
		logo = "Conclave.png",
		tendency = {"illumination", "wisdom", "moon", "eclipse"}
	},
	{
		key = "SVI",
		name = "Sanctum Vitalis",
		divine = "Plateau",
		city = "Saluria",
		page = "Plateau (Reformed Stripping Path)",
		anchor = "Sanctum Vitalis",
		logo = "Vitalis.png",
		tendency = {"health", "medicine", "relief", "stability"}
	},
	{
		key = "ALB",
		name = "Temple Alabaster",
		divine = "Ivory",
		city = "Aetherium",
		page = "Ivory (Reformed Stripping Path)",
		anchor = "Temple Alabaster",
		logo = "Alabaster.png",
		tendency = {"purity", "serenity", "neutrality", "healing"}
	},
	{
		key = "CICL",
		name = "Court of the Ironclad",
		divine = "Fenrir",
		city = "Ferrum Citadel",
		page = "Fenrir (Reformed Stripping Path)",
		anchor = "Court of the Ironclad",
		logo = "Ironclad.png",
		tendency = {"strength", "tenacity", "security", "militancy"}
	},
	{
		key = "ACC",
		name = "Accord Concordia",
		divine = "Lukedu",
		city = "Symphonara",
		page = "Lukedu (Reformed Stripping Path)",
		anchor = "Accord Concordia",
		logo = "Concordia.png",
		tendency = {"unity", "peace", "mediation", "diplomacy"}
	},
	{
		key = "STY",
		name = "Mystery of the Stygian Veil",
		divine = "Styx",
		city = "Acheron",
		page = "Styx (Reformed Stripping Path)",
		anchor = "Mystery of the Stygian Veil",
		logo = "StygianVe.png",
		tendency = {"transformation", "rebirth", "death", "threshold"}
	},
	{
		key = "SYL",
		name = "Sylvan Fellowship",
		divine = "Faun",
		city = "Sylvapolis",
		page = "Faun (Reformed Stripping Path)",
		anchor = "Sylvan Fellowship",
		logo = "Sylvan.png",
		tendency = {"nature", "fertility", "wilds", "balance"}
	},
	{
		key = "MIR",
		name = "Mystery of Red Mirth",
		divine = "Silenus",
		city = "Erythros",
		page = "Silenus (Reformed Stripping Path)",
		anchor = "Mystery of Red Mirth",
		logo = "MirthFlag.png",
		tendency = {"merriment", "festival", "wine", "release"}
	},
	{
		key = "CAT",
		name = "Rex Catonis",
		divine = "Cato",
		city = "Catonis Atrium",
		page = "Cato (Reformed Stripping Path)",
		anchor = "Rex Catonis",
		logo = "Catonis.png",
		tendency = {"discipline", "order", "law", "judgment"}
	},
	{
		key = "SDE",
		name = "Sanctum Delphica",
		divine = "Sisera",
		city = "Delphica",
		page = "Sisera (Reformed Stripping Path)",
		anchor = "Sanctum Delphica",
		logo = "SanctumDel.png",
		tendency = {"reflection", "introspection", "records", "truth"}
	},
	{
		key = "AMI",
		name = "Ordo Amicitia",
		divine = "Erasmus",
		city = "Koinonía",
		page = "Erasmus (Reformed Stripping Path)",
		anchor = "Ordo Amicitia",
		logo = "OrdoFl.png",
		tendency = {"charity", "wisdom", "relief", "mercy"}
	},
	{
		key = "ILL",
		name = "Temple Illuminata",
		divine = "Nikolaj",
		city = "Aureum",
		page = "Nikolaj (Reformed Stripping Path)",
		anchor = "Temple Illuminata",
		logo = "AureumFlag.png",
		tendency = {"epiphany", "mystery", "cosmic", "insight"}
	}
}

local cultByName = {}
local cultByKey = {}
local cultAliases = {}

for _, c in ipairs(cults) do
	cultByName[c.name] = c
	cultByKey[c.key] = c
	cultAliases[lower(c.name)] = c.name
	cultAliases[lower(c.key)] = c.name
	cultAliases[lower(c.divine)] = c.name
end

-- Common shorthand aliases.
cultAliases["aurora"] = "Order of Aurora Mystica"
cultAliases["aurora mystica"] = "Order of Aurora Mystica"
cultAliases["harmony"] = "Harmony Sanctum"
cultAliases["ignis"] = "Ignis Aeternum"
cultAliases["golden shadows"] = "Guild of Golden Shadows"
cultAliases["azure"] = "Azure Sentinel Sect"
cultAliases["reverie"] = "Reverie Nebulous"
cultAliases["eon"] = "Eon Fellowship"
cultAliases["umbral"] = "Order of the Umbral Oracle"
cultAliases["verdant"] = "Mystery of the Verdant Embrace"
cultAliases["illuminara"] = "Conclave Illuminara"
cultAliases["vitalis"] = "Sanctum Vitalis"
cultAliases["alabaster"] = "Temple Alabaster"
cultAliases["ironclad"] = "Court of the Ironclad"
cultAliases["concordia"] = "Accord Concordia"
cultAliases["stygian veil"] = "Mystery of the Stygian Veil"
cultAliases["sylvan"] = "Sylvan Fellowship"
cultAliases["red mirth"] = "Mystery of Red Mirth"
cultAliases["catonis"] = "Rex Catonis"
cultAliases["delphica"] = "Sanctum Delphica"
cultAliases["amicitia"] = "Ordo Amicitia"
cultAliases["illuminata"] = "Temple Illuminata"

local function normalizeCultName(name)
	name = trim(name)
	if cultByName[name] then
		return name
	end
	return cultAliases[lower(name)] or name
end

local function cultLink(name)
	name = normalizeCultName(name)
	local c = cultByName[name]
	if not c then
		return "[[" .. name .. "]]"
	end
	return string.format("[[%s#%s|%s]]", c.page, c.anchor, c.name)
end

local function cultMark(name)
	name = normalizeCultName(name)
	local c = cultByName[name]
	if not c then
		return cultLink(name)
	end

	local logo = ""
	if c.logo and c.logo ~= "" then
		logo = string.format("[[File:%s|18px]] ", c.logo)
	end

	return logo .. cultLink(name)
end

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

local function eventCityLink(event)
	if not event then
		return "—"
	end

	local cultName = normalizeCultName(event.a)
	local cult = cultByName[cultName]

	if not cult or not cult.city or cult.city == "" then
		return "—"
	end

	return cityLink(cult.city)
end

local function eventCityLink(event)
	if not event then
		return "—"
	end

	local cultName = normalizeCultName(event.a)
	local cult = cultByName[cultName]

	if not cult or not cult.city or cult.city == "" then
		return "—"
	end

	return cityLink(cult.city)
end

--------------------------------------------------------------------------------
-- 4. BASELINE RELATIONS
--
-- Scale:
--   +75 covenantal alliance
--   +55 strong alliance
--   +35 standing alliance
--   +15 mild affinity
--     0 neutral
--   -15 mild tension
--   -35 standing rivalry
--   -55 hostile rivalry
--   -75 sectarian enmity
--------------------------------------------------------------------------------

local baselineRows = {
	["Ordo Solis Invicti"] = {
		allies = {"Rex Catonis", "Harmony Sanctum", "Order of Aurora Mystica"},
		enemies = {},
		note = "solar order, measure, and public adjudication"
	},
	["Order of Aurora Mystica"] = {
		allies = {"Eon Fellowship", "Conclave Illuminara", "Harmony Sanctum", "Ordo Solis Invicti"},
		enemies = {"Celestial Harmony Sect", "Ignis Aeternum"},
		note = "dawn rites, renewal, and anti-destruction cooperation"
	},
	["Harmony Sanctum"] = {
		allies = {"Order of Aurora Mystica", "Mystery of the Verdant Embrace", "Guild of Golden Shadows", "Conclave Illuminara", "Accord Concordia", "Ordo Solis Invicti"},
		enemies = {"Ignis Aeternum", "Azure Sentinel Sect", "Court of the Ironclad", "Sylvan Fellowship"},
		note = "balance-oriented center of the faith"
	},
	["Ignis Aeternum"] = {
		allies = {"Azure Sentinel Sect", "Sylvan Fellowship"},
		enemies = {"Eon Fellowship", "Harmony Sanctum", "Reverie Nebulous", "Mystery of the Verdant Embrace", "Order of Aurora Mystica", "Court of the Ironclad", "Guild of Golden Shadows"},
		note = "purifying flame and destructive renewal"
	},
	["Celestial Harmony Sect"] = {
		allies = {"Order of the Umbral Oracle", "Accord Concordia", "Sanctum Delphica"},
		enemies = {"Order of Aurora Mystica", "Conclave Illuminara", "Rex Catonis"},
		note = "duality, change, and mediating ambiguity"
	},
	["Guild of Golden Shadows"] = {
		allies = {"Harmony Sanctum", "Reverie Nebulous", "Court of the Ironclad", "Sanctum Delphica"},
		enemies = {"Ignis Aeternum", "Azure Sentinel Sect"},
		note = "trade, intelligence, brokerage, and covert influence"
	},
	["Azure Sentinel Sect"] = {
		allies = {"Ignis Aeternum", "Reverie Nebulous"},
		enemies = {"Eon Fellowship", "Harmony Sanctum", "Guild of Golden Shadows"},
		note = "sea, war, depth, and temple defense"
	},
	["Reverie Nebulous"] = {
		allies = {"Guild of Golden Shadows", "Azure Sentinel Sect", "Sanctum Delphica"},
		enemies = {"Ignis Aeternum", "Eon Fellowship", "Court of the Ironclad"},
		note = "dreams, imagination, omen-craft, and social volatility"
	},
	["Eon Fellowship"] = {
		allies = {"Order of Aurora Mystica", "Court of the Ironclad", "Temple Illuminata"},
		enemies = {"Ignis Aeternum", "Azure Sentinel Sect", "Sylvan Fellowship", "Reverie Nebulous"},
		note = "winter, endurance, death, and cyclical renewal"
	},
	["Order of the Umbral Oracle"] = {
		allies = {"Harmony Sanctum", "Ordo Amicitia", "Celestial Harmony Sect", "Sanctum Delphica"},
		enemies = {},
		note = "night, hidden truth, introspection, and oracle-work"
	},
	["Mystery of the Verdant Embrace"] = {
		allies = {"Harmony Sanctum", "Ordo Amicitia", "Sanctum Vitalis", "Sylvan Fellowship"},
		enemies = {"Ignis Aeternum", "Court of the Ironclad"},
		note = "harvest, fertility, cultivated nature, and anti-destruction practice"
	},
	["Conclave Illuminara"] = {
		allies = {"Order of Aurora Mystica", "Harmony Sanctum", "Ordo Solis Invicti"},
		enemies = {"Celestial Harmony Sect"},
		note = "Tarsican illumination, eclipse interpretation, and wisdom rites"
	},
	["Sanctum Vitalis"] = {
		allies = {"Temple Alabaster", "Ordo Amicitia", "Mystery of the Verdant Embrace", "Harmony Sanctum"},
		enemies = {},
		note = "health, medicine, relief, and recovery"
	},
	["Temple Alabaster"] = {
		allies = {"Court of the Ironclad", "Ordo Amicitia", "Sanctum Vitalis", "Rex Catonis"},
		enemies = {},
		note = "purity, serenity, neutrality, and protected precincts"
	},
	["Court of the Ironclad"] = {
		allies = {"Eon Fellowship", "Guild of Golden Shadows", "Temple Alabaster", "Rex Catonis"},
		enemies = {"Ignis Aeternum", "Harmony Sanctum", "Reverie Nebulous", "Mystery of the Verdant Embrace"},
		note = "strength, tenacity, armed discipline, and crisis response"
	},
	["Accord Concordia"] = {
		allies = {"Sanctum Delphica", "Harmony Sanctum", "Celestial Harmony Sect", "Ordo Amicitia"},
		enemies = {},
		note = "unity, diplomacy, and inter-cult mediation"
	},
	["Mystery of the Stygian Veil"] = {
		allies = {"Temple Illuminata", "Order of the Umbral Oracle", "Sanctum Vitalis"},
		enemies = {"Ordo Amicitia"},
		note = "transformation, rebirth, death, and threshold rites"
	},
	["Sylvan Fellowship"] = {
		allies = {"Ignis Aeternum", "Mystery of the Verdant Embrace"},
		enemies = {"Eon Fellowship", "Harmony Sanctum"},
		note = "wild fertility, natural balance, and resistance to over-ordering"
	},
	["Mystery of Red Mirth"] = {
		allies = {"Rex Catonis", "Accord Concordia", "Ordo Amicitia"},
		enemies = {},
		note = "celebration, release, and ritual merriment"
	},
	["Rex Catonis"] = {
		allies = {"Mystery of Red Mirth", "Court of the Ironclad", "Temple Alabaster", "Ordo Solis Invicti"},
		enemies = {"Celestial Harmony Sect"},
		note = "discipline, order, law, and moral clarity"
	},
	["Sanctum Delphica"] = {
		allies = {"Accord Concordia", "Reverie Nebulous", "Order of the Umbral Oracle", "Guild of Golden Shadows", "Celestial Harmony Sect"},
		enemies = {},
		note = "reflection, records, introspection, and arbitration"
	},
	["Ordo Amicitia"] = {
		allies = {"Order of the Umbral Oracle", "Mystery of the Verdant Embrace", "Temple Alabaster", "Sanctum Vitalis", "Accord Concordia", "Mystery of Red Mirth"},
		enemies = {"Mystery of the Stygian Veil"},
		note = "charity, wisdom, aid distribution, and mercy"
	},
	["Temple Illuminata"] = {
		allies = {"Mystery of the Stygian Veil", "Eon Fellowship", "Conclave Illuminara"},
		enemies = {},
		note = "epiphanies, cosmic insight, and mystery"
	}
}

local mythicAffinities = {
	{"Order of Aurora Mystica", "Eon Fellowship", 15, "Eos–Glinos mythic affinity"},
	{"Order of Aurora Mystica", "Conclave Illuminara", 15, "Eos–Tarsica mythic affinity"},
	{"Harmony Sanctum", "Mystery of the Verdant Embrace", 15, "Micras–Opsithe mythic affinity"},
	{"Guild of Golden Shadows", "Reverie Nebulous", 15, "Chrysos–Nephele mythic affinity"},
	{"Guild of Golden Shadows", "Accord Concordia", 8, "Chrysos–Lukedu lineage affinity"},
	{"Azure Sentinel Sect", "Ignis Aeternum", 15, "Thalassa–Pyros mythic affinity"},
	{"Order of the Umbral Oracle", "Ordo Amicitia", 12, "Noctis–Erasmus affinity"},
	{"Accord Concordia", "Sanctum Delphica", 15, "Lukedu–Sisera mythic affinity"},
	{"Mystery of Red Mirth", "Rex Catonis", 15, "Silenus–Cato mythic affinity"},
	{"Mystery of the Stygian Veil", "Temple Illuminata", 15, "Styx–Nikolaj mythic affinity"},
	{"Court of the Ironclad", "Temple Alabaster", 12, "Fenrir–Ivory mythic affinity"},
	{"Sanctum Vitalis", "Mystery of the Verdant Embrace", 10, "Plateau–Opsithe lineage affinity"},
	{"Sylvan Fellowship", "Mystery of the Verdant Embrace", 10, "Faun–Opsithe natural affinity"},

	{"Ignis Aeternum", "Harmony Sanctum", -15, "Pyros–Micras mythic tension"},
	{"Ignis Aeternum", "Mystery of the Verdant Embrace", -15, "Pyros–Opsithe mythic tension"},
	{"Ignis Aeternum", "Reverie Nebulous", -12, "Pyros–Nephele mythic tension"},
	{"Azure Sentinel Sect", "Harmony Sanctum", -10, "Thalassa–Micras mythic tension"},
	{"Order of Aurora Mystica", "Celestial Harmony Sect", -10, "Eos–Indigo/Momiji mythic tension"},
	{"Conclave Illuminara", "Celestial Harmony Sect", -10, "Tarsica–Indigo/Momiji mythic tension"},
	{"Eon Fellowship", "Azure Sentinel Sect", -10, "Glinos–Thalassa mythic tension"},
	{"Eon Fellowship", "Sylvan Fellowship", -8, "winter–wild fertility tension"},
	{"Mystery of the Stygian Veil", "Ordo Amicitia", -8, "Styx–Nyssa/Erasmus secrecy tension"}
}

local function buildBaseScores()
	local scores = {}
	local notes = {}

	local function add(a, b, delta, note)
		a, b = normalizeCultName(a), normalizeCultName(b)
		if a == "" or b == "" or a == b then
			return
		end
		local k = sortedPair(a, b)
		scores[k] = clamp((scores[k] or 0) + delta, -150, 150)
		notes[k] = notes[k] or {}
		table.insert(notes[k], note)
	end

for name, row in pairs(baselineRows) do
	for _, ally in ipairs(row.allies or {}) do
		add(name, ally, 12, "standing affinity")
	end
	for _, enemy in ipairs(row.enemies or {}) do
		add(name, enemy, -12, "standing grievance")
	end
end

for _, rel in ipairs(mythicAffinities) do
	local scaled = math.floor((tonumber(rel[3]) or 0) * 0.45)
	if scaled == 0 and tonumber(rel[3]) and tonumber(rel[3]) ~= 0 then
		scaled = rel[3] > 0 and 1 or -1
	end
	add(rel[1], rel[2], scaled, rel[4])
end

	return scores, notes
end

--------------------------------------------------------------------------------
-- 5. DAILY EVENT POOL
--------------------------------------------------------------------------------

local eventPool = {
	{
		w = 9,
		type = "relief",
		severity = "green",
		delta = 8,
		title = "Joint relief distribution",
		text = "{A} and {B} jointly distribute food, medicine, or shelter stipends through Temple Bank channels.",
		cults = {"Ordo Amicitia", "Sanctum Vitalis", "Temple Alabaster", "Harmony Sanctum", "Mystery of the Verdant Embrace"}
	},
	{
		w = 8,
		type = "mediation",
		severity = "green",
		delta = 7,
		title = "Mediation circuit",
		text = "{A} and {B} conduct a mediation circuit in a tense district, improving working confidence.",
		cults = {"Harmony Sanctum", "Accord Concordia", "Order of Aurora Mystica", "Celestial Harmony Sect", "Sanctum Delphica"}
	},
	{
		w = 7,
		type = "audit",
		severity = "green",
		delta = 6,
		title = "Temple Bank audit clears both sides",
		text = "A Temple Bank audit clears {A} and {B} of misconduct allegations, reducing public suspicion.",
		cults = {"Rex Catonis", "Sanctum Delphica", "Temple Alabaster", "Guild of Golden Shadows", "Ordo Solis Invicti"}
	},
	{
		w = 6,
		type = "festival",
		severity = "green",
		delta = 9,
		title = "Shared festival stewardship",
		text = "{A} and {B} jointly steward a festival route without disorder.",
		cults = {"Mystery of Red Mirth", "Rex Catonis", "Order of Aurora Mystica", "Conclave Illuminara", "Accord Concordia", "Ordo Amicitia"}
	},
	{
		w = 6,
		type = "calendar",
		severity = "green",
		delta = 6,
		title = "Calendar synchronization",
		text = "{A} and {B} agree on calendar timing for public rites and registry deadlines.",
		cults = {"Ordo Solis Invicti", "Conclave Illuminara", "Order of Aurora Mystica", "Rex Catonis", "Sanctum Delphica"}
	},
	{
		w = 6,
		type = "omen",
		severity = "yellow",
		delta = -7,
		title = "Disputed omen",
		text = "{A} and {B} issue conflicting interpretations of a public omen.",
		cults = {"Conclave Illuminara", "Order of the Umbral Oracle", "Temple Illuminata", "Reverie Nebulous", "Celestial Harmony Sect"}
	},
	{
		w = 7,
		type = "doctrine",
		severity = "yellow",
		delta = -8,
		title = "Doctrinal protest",
		text = "{A} denounces the ritual language used by {B}.",
		cults = {"Ignis Aeternum", "Harmony Sanctum", "Eon Fellowship", "Azure Sentinel Sect", "Mystery of the Verdant Embrace", "Order of Aurora Mystica"}
	},
	{
		w = 6,
		type = "trade",
		severity = "yellow",
		delta = -6,
		title = "Trade-route accusation",
		text = "{A} accuses {B} of shielding merchants, brokers, or shrine vendors from proper review.",
		cults = {"Guild of Golden Shadows", "Azure Sentinel Sect", "Rex Catonis", "Temple Alabaster", "Sanctum Delphica", "Ordo Solis Invicti"}
	},
	{
		w = 5,
		type = "security",
		severity = "yellow",
		delta = -7,
		title = "Shrine security dispute",
		text = "{A} objects to the security posture adopted by {B} near a shrine precinct.",
		cults = {"Court of the Ironclad", "Azure Sentinel Sect", "Rex Catonis", "Harmony Sanctum", "Order of Aurora Mystica", "Temple Alabaster"}
	},
	{
		w = 5,
		type = "dream",
		severity = "yellow",
		delta = -6,
		title = "Dream-prophecy disagreement",
		text = "{A} and {B} dispute whether a recurring dream report should be treated as prophecy, warning, or rumor.",
		cults = {"Reverie Nebulous", "Order of the Umbral Oracle", "Sanctum Delphica", "Conclave Illuminara", "Temple Illuminata"}
	},
	{
		w = 4,
		type = "panic",
		severity = "red",
		delta = -12,
		title = "Rumor panic",
		text = "Panic narratives spread between communities aligned with {A} and {B}.",
		cults = {"Reverie Nebulous", "Order of the Umbral Oracle", "Temple Illuminata", "Sanctum Delphica", "Eon Fellowship"}
	},
	{
		w = 4,
		type = "violence",
		severity = "red",
		delta = -16,
		title = "Sectarian violence",
		text = "Street violence attributed to extremists worsens relations between {A} and {B}.",
		cults = {"Ignis Aeternum", "Court of the Ironclad", "Azure Sentinel Sect", "Harmony Sanctum", "Reverie Nebulous", "Mystery of the Verdant Embrace"}
	},
	{
		w = 5,
		type = "mourning",
		severity = "green",
		delta = 7,
		title = "Shared mourning rites",
		text = "{A} and {B} cooperate during mourning rites after a local death, disaster, or battlefield recovery.",
		cults = {"Mystery of the Stygian Veil", "Ordo Amicitia", "Order of the Umbral Oracle", "Sanctum Vitalis", "Temple Alabaster"}
	},
	{
		w = 5,
		type = "ecology",
		severity = "green",
		delta = 6,
		title = "Ecological repair compact",
		text = "{A} and {B} sign a temporary compact for ecological repair and pilgrimage safety.",
		cults = {"Mystery of the Verdant Embrace", "Sylvan Fellowship", "Harmony Sanctum", "Sanctum Vitalis"}
	},
	{
		w = 4,
		type = "purification",
		severity = "red",
		delta = -14,
		title = "Purification controversy",
		text = "{A} is blamed for encouraging dangerous purification language against {B}.",
		cults = {"Ignis Aeternum", "Temple Alabaster", "Harmony Sanctum", "Mystery of the Verdant Embrace", "Order of Aurora Mystica"}
	},
	{
		w = 5,
		type = "registry",
		severity = "green",
		delta = 5,
		title = "Registry reconciliation",
		text = "{A} and {B} reconcile conflicting cult ledgers through formal registry review.",
		cults = {"Rex Catonis", "Sanctum Delphica", "Guild of Golden Shadows", "Ordo Amicitia", "Accord Concordia", "Ordo Solis Invicti"}
	},
	{
		w = 5,
		type = "port",
		severity = "green",
		delta = 6,
		title = "General Port membership drive",
		text = "{A} and {B} support a General Port membership drive tied to shrine investment and community stabilization.",
		cults = {"Guild of Golden Shadows", "Ordo Amicitia", "Harmony Sanctum", "Accord Concordia", "Sanctum Delphica"}
	},
	{
		w = 4,
		type = "privateer",
		severity = "yellow",
		delta = -7,
		title = "Privateer blessing dispute",
		text = "{A} and {B} disagree over whether a privateer commission should receive ritual blessing.",
		cults = {"Azure Sentinel Sect", "Guild of Golden Shadows", "Rex Catonis", "Court of the Ironclad", "Order of the Umbral Oracle"}
	},
	{
		w = 4,
		type = "missionary",
		severity = "green",
		delta = 6,
		title = "Missionary escort compact",
		text = "{A} and {B} agree to escort missionaries through a contested road or canal route.",
		cults = {"Ordo Amicitia", "Court of the Ironclad", "Temple Alabaster", "Harmony Sanctum", "Azure Sentinel Sect"}
	},
	{
		w = 3,
		type = "noctic",
		severity = "yellow",
		delta = -7,
		title = "Noctic registration dispute",
		text = "{A} challenges {B} over Temple Bank registration standards for households affected by Noctic Vampirism.",
		cults = {"Temple Alabaster", "Sanctum Vitalis", "Order of the Umbral Oracle", "Mystery of the Stygian Veil", "Rex Catonis", "Reverie Nebulous"}
	},
	{
		w = 3,
		type = "noctic",
		severity = "green",
		delta = 7,
		title = "Noctic triage compact",
		text = "{A} and {B} cooperate to stabilize registered Noctic Vampires and separate medical cases from rumor-driven panic.",
		cults = {"Temple Alabaster", "Sanctum Vitalis", "Ordo Amicitia", "Order of the Umbral Oracle", "Sanctum Delphica"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "red",
		delta = -13,
		title = "Unauthorized Noctic induction",
		text = "An unauthorized Noctic induction rite causes {A} and {B} to clash over blame, consent, and ritual discipline.",
		cults = {"Mystery of the Stygian Veil", "Order of the Umbral Oracle", "Reverie Nebulous", "Rex Catonis", "Temple Alabaster", "Court of the Ironclad"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "red",
		delta = -14,
		title = "Counterfeit nocticine tinctures",
		text = "Counterfeit nocticine tinctures force {A} and {B} into open dispute over cargo custody, forged permissions, and shrine-market inspection.",
		cults = {"Guild of Golden Shadows", "Temple Alabaster", "Rex Catonis", "Sanctum Vitalis", "Court of the Ironclad", "Azure Sentinel Sect"}
	},
	{
		w = 3,
		type = "noctic",
		severity = "yellow",
		delta = -6,
		title = "Raw-leaf custody audit",
		text = "{A} accuses {B} of mishandling raw Noctic-Rabrev custody records during a restricted leaf audit.",
		cults = {"Guild of Golden Shadows", "Rex Catonis", "Temple Alabaster", "Sanctum Delphica", "Sanctum Vitalis", "Ordo Solis Invicti"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "green",
		delta = 6,
		title = "Blood-substitute stabilization",
		text = "{A} and {B} coordinate milk, blood-substitute, and sleep-stabilization support for affected households.",
		cults = {"Sanctum Vitalis", "Temple Alabaster", "Ordo Amicitia", "Order of the Umbral Oracle", "Mystery of the Stygian Veil"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "yellow",
		delta = -6,
		title = "Dream-contagion rumor",
		text = "A dream-contagion rumor causes tension between {A} and {B}, as both cults argue over whether visions should be treated as prophecy, panic, or illness.",
		cults = {"Reverie Nebulous", "Order of the Umbral Oracle", "Conclave Illuminara", "Sanctum Delphica", "Temple Illuminata", "Sanctum Vitalis"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "green",
		delta = 5,
		title = "Protected Noctic household escort",
		text = "{A} and {B} jointly protect registered Noctic households from retaliation during an outbreak scare.",
		cults = {"Temple Alabaster", "Court of the Ironclad", "Ordo Amicitia", "Harmony Sanctum", "Sanctum Vitalis"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "yellow",
		delta = -5,
		title = "Noctic stigma hearing",
		text = "{A} and {B} dispute whether public warnings against Noctic Vampirism have crossed into unlawful stigma against registered afflicted persons.",
		cults = {"Temple Alabaster", "Harmony Sanctum", "Rex Catonis", "Ordo Amicitia", "Court of the Ironclad", "Sanctum Vitalis"}
	},
	{
		w = 2,
		type = "noctic",
		severity = "red",
		delta = -12,
		title = "Predatory blood-rite accusation",
		text = "{A} accuses {B} of failing to prevent a predatory blood rite linked to uncontrolled Noctic hunger.",
		cults = {"Mystery of the Stygian Veil", "Order of the Umbral Oracle", "Rex Catonis", "Court of the Ironclad", "Temple Alabaster", "Reverie Nebulous"}
	}
},
	{
		w = 3,
		type = "cult-specific",
		severity = "red",
		delta = -18,
		title = "Privateer schism",
		actor = "Guild of Golden Shadows",
		targets = {"Azure Sentinel Sect", "Rex Catonis", "Harmony Sanctum", "Court of the Ironclad"},
		text = "{A} is accused of protecting rogue privateers, shrine-smugglers, and oath-brokers operating beyond General Port discipline, provoking {B}."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "red",
		delta = -20,
		title = "Azure knife mandate",
		actor = "Azure Sentinel Sect",
		targets = {"Guild of Golden Shadows", "Harmony Sanctum", "Eon Fellowship", "Rex Catonis"},
		text = "{A} is blamed for a covert assassination mandate against a protected intermediary, forcing {B} to demand inquiry and public penance."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "red",
		delta = -18,
		title = "Ironclad reprisal column",
		actor = "Court of the Ironclad",
		targets = {"Reverie Nebulous", "Harmony Sanctum", "Ignis Aeternum", "Mystery of the Verdant Embrace"},
		text = "{A} deploys a reprisal column after shrine unrest, and {B} denounces the action as punishment disguised as protection."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "red",
		delta = -19,
		title = "Fire-purification riot",
		actor = "Ignis Aeternum",
		targets = {"Harmony Sanctum", "Mystery of the Verdant Embrace", "Temple Alabaster", "Order of Aurora Mystica"},
		text = "{A} is accused of inspiring fire-purification mobs whose rites damage homes, gardens, and shrine archives protected by {B}."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "red",
		delta = -17,
		title = "Dream court panic",
		actor = "Reverie Nebulous",
		targets = {"Court of the Ironclad", "Rex Catonis", "Sanctum Delphica", "Sanctum Vitalis"},
		text = "{A} spreads dream-court testimony that {B} considers unverified, contagious, and dangerous to public order."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -12,
		title = "Balance injunction",
		actor = "Harmony Sanctum",
		targets = {"Ignis Aeternum", "Azure Sentinel Sect", "Court of the Ironclad", "Sylvan Fellowship"},
		text = "{A} issues a balance injunction limiting militant rites, and {B} answers that restraint has become a weapon."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -11,
		title = "Dawn silence breach",
		actor = "Order of Aurora Mystica",
		targets = {"Celestial Harmony Sect", "Ignis Aeternum", "Reverie Nebulous", "Order of the Umbral Oracle"},
		text = "{A} accuses {B} of breaking first-light silence during a sensitive rite of renewal."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -11,
		title = "Duality proclamation",
		actor = "Celestial Harmony Sect",
		targets = {"Order of Aurora Mystica", "Conclave Illuminara", "Rex Catonis", "Ordo Solis Invicti"},
		text = "{A} issues a duality proclamation that {B} interprets as deliberate confusion of law, calendar, and oath."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -10,
		title = "Winter moratorium",
		actor = "Eon Fellowship",
		targets = {"Ignis Aeternum", "Azure Sentinel Sect", "Sylvan Fellowship", "Reverie Nebulous"},
		text = "{A} announces a winter moratorium on risky rites, while {B} condemns it as an attempt to freeze renewal itself."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -10,
		title = "Umbral sealed testimony",
		actor = "Order of the Umbral Oracle",
		targets = {"Rex Catonis", "Sanctum Delphica", "Conclave Illuminara", "Reverie Nebulous"},
		text = "{A} withholds sealed testimony from public review, and {B} warns that hidden truth can become hidden guilt."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -10,
		title = "Verdant road closure",
		actor = "Mystery of the Verdant Embrace",
		targets = {"Ignis Aeternum", "Court of the Ironclad", "Guild of Golden Shadows", "Rex Catonis"},
		text = "{A} closes a sacred grove-road to protect harvest rites, angering {B} over trade, patrol, and emergency access."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -10,
		title = "Eclipse custody dispute",
		actor = "Conclave Illuminara",
		targets = {"Celestial Harmony Sect", "Order of the Umbral Oracle", "Temple Illuminata", "Ordo Solis Invicti"},
		text = "{A} asserts custody over eclipse readings, while {B} contests the authority to declare what the darkness revealed."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "green",
		delta = 12,
		title = "Vitalis mercy corridor",
		actor = "Sanctum Vitalis",
		targets = {"Temple Alabaster", "Ordo Amicitia", "Mystery of the Verdant Embrace", "Harmony Sanctum"},
		text = "{A} opens a mercy corridor for the sick and displaced, improving cooperation with {B}."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "green",
		delta = 10,
		title = "Alabaster sanctuary ruling",
		actor = "Temple Alabaster",
		targets = {"Sanctum Vitalis", "Ordo Amicitia", "Court of the Ironclad", "Rex Catonis"},
		text = "{A} grants sanctuary under strict purity rules, allowing {B} to accept a difficult compromise without losing face."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "green",
		delta = 10,
		title = "Concordian oath table",
		actor = "Accord Concordia",
		targets = {"Harmony Sanctum", "Sanctum Delphica", "Ordo Amicitia", "Celestial Harmony Sect"},
		text = "{A} convenes an oath table where {B} agrees to temporary restraint and shared public wording."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "red",
		delta = -16,
		title = "Stygian forbidden passage",
		actor = "Mystery of the Stygian Veil",
		targets = {"Ordo Amicitia", "Temple Alabaster", "Rex Catonis", "Sanctum Vitalis"},
		text = "{A} is accused of opening a forbidden passage between mourning, rebirth, and Noctic blood-memory rites, alarming {B}."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -11,
		title = "Sylvan beast-oath",
		actor = "Sylvan Fellowship",
		targets = {"Eon Fellowship", "Harmony Sanctum", "Rex Catonis", "Court of the Ironclad"},
		text = "{A} recognizes a wild beast-oath that {B} refuses to treat as lawful testimony."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -9,
		title = "Red Mirth excess",
		actor = "Mystery of Red Mirth",
		targets = {"Rex Catonis", "Temple Alabaster", "Ordo Solis Invicti", "Sanctum Vitalis"},
		text = "{A} is blamed for festival excess that leaves {B} to repair injuries, ledgers, and public discipline."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -10,
		title = "Catonian tribunal summons",
		actor = "Rex Catonis",
		targets = {"Celestial Harmony Sect", "Guild of Golden Shadows", "Ignis Aeternum", "Mystery of Red Mirth"},
		text = "{A} summons witnesses to a tribunal, and {B} protests that judgment has arrived before truth."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "green",
		delta = 9,
		title = "Delphic archive concord",
		actor = "Sanctum Delphica",
		targets = {"Accord Concordia", "Order of the Umbral Oracle", "Guild of Golden Shadows", "Reverie Nebulous"},
		text = "{A} reconciles contradictory archive entries, allowing {B} to withdraw an accusation without humiliation."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "green",
		delta = 11,
		title = "Amicitian ransom of mercy",
		actor = "Ordo Amicitia",
		targets = {"Temple Alabaster", "Sanctum Vitalis", "Accord Concordia", "Mystery of Red Mirth"},
		text = "{A} pays a ransom of mercy for detained families, and {B} agrees to treat the act as charity rather than interference."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "yellow",
		delta = -9,
		title = "Illuminata impossible omen",
		actor = "Temple Illuminata",
		targets = {"Conclave Illuminara", "Order of the Umbral Oracle", "Eon Fellowship", "Sanctum Delphica"},
		text = "{A} announces an impossible omen, and {B} demands proof before the claim can alter ritual calendars."
	},
	{
		w = 3,
		type = "cult-specific",
		severity = "green",
		delta = 9,
		title = "Solar measure correction",
		actor = "Ordo Solis Invicti",
		targets = {"Rex Catonis", "Conclave Illuminara", "Order of Aurora Mystica", "Harmony Sanctum"},
		text = "{A} corrects a public measure, calendar mark, or oath-window, allowing {B} to resolve a dispute without bloodshed."
	},

	{
		w = 1,
		type = "mythic",
		severity = "red",
		delta = -28,
		title = "Covenant of the broken harbor",
		actor = "Azure Sentinel Sect",
		targets = {"Guild of Golden Shadows", "Harmony Sanctum", "Rex Catonis"},
		text = "A harbor oath collapses after an assassination, and {A} is accused by {B} of turning sacred protection into a blade pointed inward."
	},
	{
		w = 1,
		type = "mythic",
		severity = "red",
		delta = -50,
		title = "Ashen procession of Pyros",
		actor = "Ignis Aeternum",
		targets = {"Mystery of the Verdant Embrace", "Harmony Sanctum", "Temple Alabaster"},
		text = "A procession of ash and flame crosses a protected district; {A} calls it renewal, while {B} calls it the opening act of war."
	},
	{
		w = 1,
		type = "mythic",
		severity = "green",
		delta = 25,
		title = "Miracle of the divided lamp",
		actor = "Order of Aurora Mystica",
		targets = {"Conclave Illuminara", "Harmony Sanctum", "Ordo Solis Invicti"},
		text = "A lamp burns in two colors at first light, and {A} accepts {B} as co-witness to a new festival of reconciliation."
	},
	{
		w = 1,
		type = "mythic",
		severity = "green",
		delta = 24,
		title = "Festival of the unbroken cup",
		actor = "Mystery of Red Mirth",
		targets = {"Rex Catonis", "Ordo Amicitia", "Accord Concordia"},
		text = "A cup dropped before the crowd does not break, and {A} invites {B} to establish a new festival of restrained joy."
	},
	{
		w = 1,
		type = "mythic",
		severity = "red",
		delta = -26,
		title = "Dream of the drowned crown",
		actor = "Reverie Nebulous",
		targets = {"Court of the Ironclad", "Azure Sentinel Sect", "Sanctum Delphica"},
		text = "A shared dream of a drowned crown spreads through the night, and {A} is accused by {B} of weaponizing prophecy."
	},
	{
		w = 1,
		type = "mythic",
		severity = "green",
		delta = 22,
		title = "Mercy under the white veil",
		actor = "Temple Alabaster",
		targets = {"Sanctum Vitalis", "Ordo Amicitia", "Mystery of the Stygian Veil"},
		text = "A dying witness survives beneath a white veil, and {A} permits {B} to declare a rare mercy rite."
	},
	{
		w = 1,
		type = "mythic",
		severity = "red",
		delta = -25,
		title = "The wolf at the sealed gate",
		actor = "Court of the Ironclad",
		targets = {"Reverie Nebulous", "Harmony Sanctum", "Mystery of the Verdant Embrace"},
		text = "A gate sealed by civic oath is broken in the name of protection, and {A} faces accusations from {B} that defense has become conquest."
	},
	{
	{
		w = 1,
		type = "mythic",
		severity = "green",
		delta = 23,
		title = "The concord of nine witnesses",
		actor = "Accord Concordia",
		targets = {"Harmony Sanctum", "Sanctum Delphica", "Ordo Amicitia"},
		text = "Nine witnesses survive a failed riot without contradiction, and {A} persuades {B} to recognize a new compact of public testimony."
	}
}

local cultEventPacks = {
	["Ordo Solis Invicti"] = {
		targets = {"Rex Catonis", "Conclave Illuminara", "Order of Aurora Mystica", "Harmony Sanctum", "Celestial Harmony Sect", "Guild of Golden Shadows"},
		events = {
			-- Positive
			{"Solar writ correction", "green", 9, "measure", "{A} corrects a disputed oath-window, allowing {B} to withdraw a challenge without public disgrace."},
			{"Nine Lamps audit", "green", 8, "audit", "{A} completes a Nine Lamps audit of shrine instruments and grants {B} renewed standing in public registries."},
			{"Daylight amnesty decree", "green", 12, "reconciliation", "{A} proclaims a daylight amnesty for lesser ritual offenses, giving {B} a path to reconciliation."},
			{"Meridian oath compact", "green", 10, "law", "{A} and {B} jointly certify vows spoken at true noon, preventing a ledger dispute from becoming a schism."},
			{"Sun-seed instrument grant", "green", 9, "craft", "{A} grants calibrated Sun-Seed instruments to artisans supervised by {B}."},
			{"Public measure concord", "green", 8, "governance", "{A} helps {B} standardize weights, volumes, bells, and ritual schedules for public use."},

			-- Negative
			{"Meridian court rebuke", "yellow", -10, "law", "{A} rebukes {B} for filing testimony outside the proper meridian hour."},
			{"Sun-seed seizure", "yellow", -12, "custody", "{A} seizes unauthorized Sun-Seeds from workshops protected by {B}."},
			{"False noon accusation", "red", -18, "doctrine", "{A} accuses {B} of falsifying a noon-vow, threatening the legitimacy of contracts sealed that day."},
			{"Broken measure scandal", "yellow", -11, "audit", "{A} discovers that measures used by {B} do not conform to solar-certified standards."},
			{"Unlicensed dawn bell", "yellow", -9, "ritual", "{A} condemns an unlicensed bell rung by {B} before the proper solar mark."},
			{"Public shadow charge", "red", -20, "crisis", "{A} declares that {B} has hidden a public matter from daylight adjudication."}
		}
	},

	["Order of Aurora Mystica"] = {
		targets = {"Conclave Illuminara", "Harmony Sanctum", "Eon Fellowship", "Celestial Harmony Sect", "Ignis Aeternum", "Reverie Nebulous"},
		events = {
			-- Positive
			{"Dawn mirror concord", "green", 10, "ritual", "{A} shares mirror-library calculations with {B}, easing conflict over first-light rites."},
			{"Aurora witness miracle", "green", 16, "miracle", "{A} reports a pale dawn radiance over the altar and names {B} as co-witness."},
			{"New beginning compact", "green", 11, "mediation", "{A} opens a compact of new beginnings for families displaced by cult unrest, with {B} providing escort or relief."},
			{"First-light reconciliation", "green", 10, "reconciliation", "{A} invites {B} to stand in silence at first light before reopening negotiations."},
			{"Mirror-library exchange", "green", 9, "archive", "{A} grants {B} access to dawn records that clarify a disputed rite."},
			{"Renewal procession accord", "green", 8, "festival", "{A} and {B} agree to share a renewal procession without competing chants."},

			-- Negative
			{"First-light silence breach", "yellow", -11, "ritual", "{A} accuses {B} of breaking sacred silence before the Dawn Key is kindled."},
			{"Renewal procession dispute", "yellow", -9, "festival", "{A} alters a renewal procession route, angering {B} over precedence and shrine access."},
			{"Broken dawn accusation", "red", -20, "doctrine", "{A} declares that {B} has turned renewal into disorder and demands public repentance."},
			{"Mirror defacement charge", "red", -18, "sacrilege", "{A} accuses devotees linked to {B} of defacing first-light mirrors."},
			{"False renewal warning", "yellow", -10, "doctrine", "{A} warns that {B} is using the language of renewal to conceal ambition."},
			{"Dawn gate closure", "yellow", -9, "security", "{A} closes a dawn gate before pilgrims aligned with {B} can enter."}
		}
	},

	["Harmony Sanctum"] = {
		targets = {"Ignis Aeternum", "Azure Sentinel Sect", "Court of the Ironclad", "Mystery of the Verdant Embrace", "Accord Concordia", "Guild of Golden Shadows"},
		events = {
			-- Positive
			{"Equilibrium hearing", "green", 10, "law", "{A} convenes an equilibrium hearing where {B} agrees to temporary restraint."},
			{"Mutual order pact", "green", 12, "governance", "{A} drafts a mutual order pact that allows {B} to preserve honor while reducing street tension."},
			{"Harmony tribunal reversal", "green", 9, "reconciliation", "{A} reverses a harsh local ruling after petitions from {B}, improving relations through mercy."},
			{"Serene bell compact", "green", 8, "mediation", "{A} and {B} agree to answer the Serene Bell before mobilizing followers."},
			{"Balanced procession route", "green", 9, "festival", "{A} redraws a procession route so {B} can participate without appearing subordinate."},
			{"Mutual grievance table", "green", 10, "diplomacy", "{A} opens a grievance table where {B} may submit complaints without public humiliation."},

			-- Negative
			{"Balance injunction", "yellow", -12, "mediation", "{A} issues a balance injunction against extreme rites, and {B} rejects it as interference."},
			{"Serene bell warning", "yellow", -8, "warning", "{A} rings the Serene Bell after rumors of violence by followers of {B}."},
			{"Broken scales riot", "red", -22, "violence", "{A} blames {B} for a riot in which balance-symbols are shattered and witnesses refuse to testify."},
			{"Equilibrium veto", "yellow", -10, "governance", "{A} vetoes a ritual mobilization proposed by {B}."},
			{"Serena mediation collapse", "red", -18, "crisis", "{A}'s mediation with {B} collapses after both sides accuse the other of bad faith."},
			{"Balance oath refusal", "yellow", -9, "doctrine", "{A} refuses to recognize an oath sworn by {B} as balanced or complete."}
		}
	},

	["Ignis Aeternum"] = {
		targets = {"Harmony Sanctum", "Mystery of the Verdant Embrace", "Temple Alabaster", "Order of Aurora Mystica", "Eon Fellowship", "Court of the Ironclad"},
		events = {
			-- Positive
			{"Ash vow renewal", "green", 8, "ritual", "{A} performs ash-vows of restraint before witnesses from {B}."},
			{"Pyric artisan compact", "green", 9, "trade", "{A} and {B} certify fire-tempered ritual tools under shared safety rules."},
			{"Controlled flame rite", "green", 10, "ritual", "{A} agrees to conduct flame rites only under safety terms accepted by {B}."},
			{"Forge blessing accord", "green", 8, "craft", "{A} blesses forge-work needed by {B} for repairs, tools, or shrine restoration."},
			{"Ashes of renewal relief", "green", 9, "relief", "{A} redirects renewal offerings toward families aided by {B}."},
			{"Festival brazier compact", "green", 8, "festival", "{A} and {B} jointly supervise public braziers during a volatile festival."},

			-- Negative
			{"Fire-purification riot", "red", -20, "violence", "{A} is accused of inspiring fire-purification mobs whose rites damage homes, gardens, and shrine archives protected by {B}."},
			{"Burned grove charge", "red", -24, "ecology", "{A} is blamed after sacred plantings under {B}'s protection are destroyed by ritual flame."},
			{"Coal-bright sermon", "yellow", -10, "doctrine", "{A} preaches that destruction is mercy, prompting {B} to warn of doctrinal danger."},
			{"Ashen procession", "red", -28, "mythic", "{A} leads an ash-covered procession through a protected district; {B} calls it the beginning of war."},
			{"Unauthorized pyre", "red", -22, "sacrilege", "{A} lights an unauthorized pyre near a precinct protected by {B}."},
			{"Flame oath ultimatum", "yellow", -12, "ultimatum", "{A} demands that {B} accept purification or be named an enemy of renewal."}
		}
	},

	["Celestial Harmony Sect"] = {
		targets = {"Order of Aurora Mystica", "Conclave Illuminara", "Rex Catonis", "Accord Concordia", "Sanctum Delphica", "Ordo Solis Invicti"},
		events = {
			-- Positive
			{"Twin-song mediation", "green", 10, "mediation", "{A} performs a twin-song mediation that allows {B} to accept ambiguity without surrendering doctrine."},
			{"Changing-face delegation", "green", 8, "diplomacy", "{A} sends a changing-face delegation to negotiate with {B} in neutral language."},
			{"Autumn mirror compact", "green", 9, "festival", "{A} and {B} jointly recognize a seasonal mirror-rite for peaceful transitions."},
			{"Dual witness accord", "green", 8, "law", "{A} provides paired witnesses whose testimony satisfies {B}'s demand for balance and clarity."},
			{"Harmony-of-opposites rite", "green", 10, "ritual", "{A} invites {B} to witness a rite reconciling opposing claims without erasing either side."},
			{"Symphonara chord settlement", "green", 9, "mediation", "{A} proposes a chord settlement in which {B} accepts layered meanings as a temporary peace."},

			-- Negative
			{"Duality proclamation", "yellow", -11, "doctrine", "{A} issues a duality proclamation that {B} reads as an attack on clear law and fixed ritual meaning."},
			{"Masked calendar objection", "yellow", -10, "calendar", "{A} questions whether a ritual date should be read as one sign or two, angering {B}."},
			{"Schism of two lamps", "red", -18, "doctrine", "{A} splits a public rite into paired observances, and {B} accuses it of dividing the faithful."},
			{"Ambiguous oath scandal", "yellow", -10, "law", "{A} defends an ambiguous oath that {B} says cannot bind anyone."},
			{"Twin-mask sedition charge", "red", -20, "public order", "{A} is accused of hiding sedition beneath ritualized ambiguity, alarming {B}."},
			{"Contradictory omen decree", "yellow", -11, "omen", "{A} declares two incompatible omens simultaneously valid, provoking {B}."}
		}
	},

	["Guild of Golden Shadows"] = {
		targets = {"Azure Sentinel Sect", "Rex Catonis", "Harmony Sanctum", "Court of the Ironclad", "Sanctum Delphica", "Ordo Amicitia"},
		events = {
			-- Positive
			{"Ransom ledger bargain", "green", 10, "trade", "{A} negotiates the release of detained merchants through a ransom ledger accepted by {B}."},
			{"Harbor wind compact", "green", 9, "commerce", "{A} negotiates safe-passage winds, docking priority, and fair-market terms with {B}."},
			{"Privateer amnesty register", "green", 11, "privateering", "{A} persuades several privateer crews to enter a lawful registry recognized by {B}."},
			{"Golden convoy insurance", "green", 8, "trade", "{A} funds convoy insurance that allows merchants aligned with {B} to resume travel."},
			{"Shadow broker exchange", "green", 9, "intelligence", "{A} trades useful harbor intelligence to {B}, preventing an ambush or riot."},
			{"General Port oath renewal", "green", 10, "commerce", "{A} renews General Port oaths in front of {B}, stabilizing shrine-market operations."},

			-- Negative
			{"Privateer schism", "red", -22, "privateering", "{A} is accused of protecting rogue privateers, shrine-smugglers, and oath-brokers beyond General Port discipline, provoking {B}."},
			{"Golden letter of marque", "yellow", -12, "privateering", "{A} issues a disputed privateering commission that {B} refuses to recognize as lawful."},
			{"False shrine cargo", "red", -20, "smuggling", "{A} is accused of moving counterfeit relics through protected cargo channels watched by {B}."},
			{"Atämiosian raider blessing", "red", -24, "piracy", "{A} blesses a raider crew before dawn, and {B} calls the act piracy disguised as devotion."},
			{"Oath-broker assassination rumor", "red", -21, "intrigue", "{A} is suspected of using oath-brokers to arrange the disappearance of a witness protected by {B}."},
			{"Harbor tithe laundering", "yellow", -11, "corruption", "{A} is accused of laundering shrine tithes through privateer prize shares, angering {B}."}
		}
	},

	["Azure Sentinel Sect"] = {
		targets = {"Guild of Golden Shadows", "Harmony Sanctum", "Eon Fellowship", "Rex Catonis", "Court of the Ironclad", "Conclave Illuminara"},
		events = {
			-- Positive
			{"Sentinel convoy escort", "green", 10, "security", "{A} escorts a vulnerable shrine convoy, allowing {B} to resume public rites."},
			{"Blue-depth amnesty", "green", 8, "reconciliation", "{A} grants amnesty to lesser harbor offenders after mediation by {B}."},
			{"Sea-wall rescue compact", "green", 10, "relief", "{A} rescues pilgrims trapped by floodwater and accepts supply support from {B}."},
			{"Harbor oath protection", "green", 9, "security", "{A} places a disputed witness under harbor-oath protection recognized by {B}."},
			{"Sentinel anti-piracy sweep", "green", 11, "security", "{A} conducts an anti-piracy sweep that protects vessels associated with {B}."},
			{"Blue Host precinct guard", "green", 8, "preservation", "{A} reinforces a protected temple precinct at the request of {B}."},

			-- Negative
			{"Azure knife mandate", "red", -24, "assassination", "{A} is blamed for a covert assassination mandate against a protected intermediary, forcing {B} to demand inquiry and penance."},
			{"Harbor silence killing", "red", -28, "assassination", "{A} is accused after a witness is found dead beneath harbor bells, and {B} alleges temple-sanctioned murder."},
			{"Drowned oath interrogation", "yellow", -12, "security", "{A} detains suspected oath-breakers in a maritime precinct, and {B} challenges the legality of the interrogation."},
			{"Sea-wall mobilization", "yellow", -10, "war", "{A} mobilizes sea-wall defenders without consulting {B}, raising fears of unilateral military action."},
			{"Black anchor reprisal", "red", -22, "reprisal", "{A} is accused of drowning a suspected traitor under anchor-rite authority, provoking {B}."},
			{"Temple fleet ultimatum", "red", -25, "war", "{A} issues a temple-fleet ultimatum that {B} treats as the first step toward open conflict."}
		}
	},

	["Reverie Nebulous"] = {
		targets = {"Court of the Ironclad", "Rex Catonis", "Sanctum Delphica", "Sanctum Vitalis", "Order of the Umbral Oracle", "Guild of Golden Shadows"},
		events = {
			-- Positive
			{"Cloud-veiled mercy vigil", "green", 9, "relief", "{A} shelters frightened families overnight, and {B} recognizes the vigil as stabilizing rather than subversive."},
			{"Morphean art compact", "green", 8, "culture", "{A} and {B} sponsor dream-painted memorial screens to calm unrest after a crisis."},
			{"Dream-healer conference", "green", 10, "medicine", "{A} hosts dream interpreters and healers from {B} to separate prophecy from illness."},
			{"Nebulous witness easing", "green", 8, "justice", "{A} helps a traumatized witness speak safely before representatives of {B}."},
			{"Somniumpolis lullaby rite", "green", 9, "ritual", "{A} performs a lullaby rite that reduces panic in districts watched by {B}."},
			{"Cloud archive donation", "green", 8, "archive", "{A} donates dream records to {B} for supervised preservation."},

			-- Negative
			{"Dream court panic", "red", -18, "prophecy", "{A} spreads dream-court testimony that {B} considers unverified, contagious, and dangerous to public order."},
			{"Nebulous confession trance", "yellow", -10, "oracle", "{A} accepts trance-confessions that {B} refuses to enter into formal record."},
			{"Somniant rumor spiral", "red", -22, "panic", "{A} is blamed for allowing Eidolan rumors to spread through dream circles, forcing {B} into emergency response."},
			{"Drowned crown vision", "red", -26, "mythic", "{A} reports a shared vision of a drowned crown, and {B} accuses it of weaponizing prophecy."},
			{"Dream contagion accusation", "yellow", -11, "noctic", "{A} is accused of spreading Noctic dream-contagion stories that {B} calls medically reckless."},
			{"False nightmare tribunal", "red", -20, "justice", "{A} holds a nightmare tribunal without evidence accepted by {B}."}
		}
	},

	["Eon Fellowship"] = {
		targets = {"Ignis Aeternum", "Azure Sentinel Sect", "Sylvan Fellowship", "Reverie Nebulous", "Order of Aurora Mystica", "Temple Illuminata"},
		events = {
			-- Positive
			{"Cold burial compact", "green", 10, "mourning", "{A} agrees to supervised burial and preservation rites with {B}."},
			{"Ice-road pilgrimage escort", "green", 8, "pilgrimage", "{A} opens a guarded winter pilgrimage road with support from {B}."},
			{"Cycle-closing reconciliation", "green", 9, "reconciliation", "{A} declares that an old grievance has reached the end of its cycle, allowing {B} to accept peace."},
			{"Winter stores release", "green", 10, "relief", "{A} releases winter stores to households assisted by {B}."},
			{"Frozen archive preservation", "green", 8, "archive", "{A} preserves fragile records for review by {B}."},
			{"Eon endurance compact", "green", 9, "security", "{A} and {B} agree to endure a tense season without mobilizing followers."},

			-- Negative
			{"Winter moratorium", "yellow", -11, "doctrine", "{A} announces a winter moratorium on dangerous rites, while {B} condemns it as an attempt to freeze renewal itself."},
			{"Glacial refusal", "red", -18, "crisis", "{A} refuses emergency ritual release until the proper cycle closes, enraging {B}."},
			{"Frozen witness dispute", "yellow", -10, "law", "{A} preserves a witness in winter custody, and {B} challenges whether the testimony remains valid."},
			{"Boreal death omen", "red", -20, "omen", "{A} declares a death-omen over a disputed precinct, alarming {B} and halting trade."},
			{"Ice-sealed gate", "yellow", -11, "security", "{A} seals a pilgrimage gate until the cycle turns, angering {B}."},
			{"Cold heresy charge", "red", -21, "doctrine", "{A} condemns {B}'s rites as impatient rebellion against the cycle of death and return."}
		}
	},

	["Order of the Umbral Oracle"] = {
		targets = {"Rex Catonis", "Sanctum Delphica", "Conclave Illuminara", "Reverie Nebulous", "Ordo Amicitia", "Temple Illuminata"},
		events = {
			-- Positive
			{"Night court confession", "green", 9, "justice", "{A} receives a night-court confession that helps {B} close a dangerous dispute."},
			{"Moonless witness escort", "green", 8, "security", "{A} escorts a threatened witness through moonless streets under protection from {B}."},
			{"Umbral truth compact", "green", 10, "oracle", "{A} agrees to reveal hidden testimony under conditions accepted by {B}."},
			{"Shadow-play reconciliation", "green", 8, "culture", "{A} stages a shadow play that allows {B} to acknowledge grief without admitting guilt."},
			{"Night ledger opening", "green", 9, "archive", "{A} opens sealed night-ledgers to auditors from {B}."},
			{"Noctis vigil protection", "green", 10, "ritual", "{A} invites {B} to guard a Noctis vigil and prevent rumor violence."},

			-- Negative
			{"Umbral sealed testimony", "yellow", -11, "secrecy", "{A} withholds sealed testimony from public review, and {B} warns that hidden truth can become hidden guilt."},
			{"Shadow play sedition", "red", -18, "public order", "{A} is accused of allowing coded sedition to spread through shadow plays, alarming {B}."},
			{"Forbidden name utterance", "yellow", -10, "ritual", "{A} permits a forbidden name during oracle work, and {B} challenges the rite's legitimacy."},
			{"Noctic veil inquiry", "red", -20, "noctic", "{A} is accused of concealing Noctic blood-memory testimony from medical officers aligned with {B}."},
			{"Moonless arrest scandal", "yellow", -11, "security", "{A} assists a moonless arrest that {B} says should have been public."},
			{"Oracle silence decree", "red", -19, "secrecy", "{A} orders silence after a vision, and {B} treats the silence as obstruction."}
		}
	},

	["Mystery of the Verdant Embrace"] = {
		targets = {"Ignis Aeternum", "Court of the Ironclad", "Guild of Golden Shadows", "Rex Catonis", "Sanctum Vitalis", "Sylvan Fellowship"},
		events = {
			-- Positive
			{"Seed tithe compact", "green", 9, "agriculture", "{A} shares seed-tithes with communities assisted by {B}."},
			{"Fertility grove arbitration", "green", 10, "mediation", "{A} invites {B} to witness grove arbitration after a harvest dispute."},
			{"Festival of first fruit", "green", 12, "festival", "{A} establishes a first-fruit festival and grants {B} a formal role in the procession."},
			{"Green canal restoration", "green", 9, "ecology", "{A} restores irrigation channels used by households protected by {B}."},
			{"Harvest sanctuary compact", "green", 10, "relief", "{A} opens harvest sanctuary for displaced families under terms accepted by {B}."},
			{"Verdant medicine exchange", "green", 8, "medicine", "{A} provides medicinal herbs to clinics or relief houses managed by {B}."},

			-- Negative
			{"Verdant road closure", "yellow", -10, "ecology", "{A} closes a sacred grove-road to protect harvest rites, angering {B} over trade, patrol, and emergency access."},
			{"Root-bound blockade", "red", -18, "blockade", "{A} is accused of using living root-barriers to block patrols and caravans linked to {B}."},
			{"Poisoned orchard accusation", "red", -21, "sabotage", "{A} accuses {B} of permitting chemical or ritual contamination of protected orchard land."},
			{"Harvest tithe refusal", "yellow", -9, "trade", "{A} refuses a harvest tithe requested by {B} for public order or relief."},
			{"Grove trespass punishment", "yellow", -11, "law", "{A} punishes trespassers in a sacred grove before {B} can review the case."},
			{"Blighted field prophecy", "red", -19, "omen", "{A} declares a blight prophecy against lands associated with {B}."}
		}
	},

	["Conclave Illuminara"] = {
		targets = {"Celestial Harmony Sect", "Order of the Umbral Oracle", "Temple Illuminata", "Ordo Solis Invicti", "Order of Aurora Mystica", "Harmony Sanctum"},
		events = {
			-- Positive
			{"Moon-bathed anointing compact", "green", 9, "ritual", "{A} shares moon-bathed anointing protocols with {B}, easing fears after an eclipse."},
			{"Tarsican archive exchange", "green", 8, "archive", "{A} opens Tarsican eclipse records to archivists from {B}."},
			{"Revealed shadow miracle", "green", 16, "miracle", "{A} identifies a shadow-sign that prevents violence, and {B} accepts the interpretation as binding."},
			{"Eclipse vigil concord", "green", 10, "festival", "{A} and {B} jointly supervise a silent eclipse vigil."},
			{"Illuminated calendar correction", "green", 9, "calendar", "{A} corrects a lunar schedule in a way that protects {B}'s ritual dignity."},
			{"Moonlit arbitration", "green", 8, "mediation", "{A} settles a dispute by reading lunar signs in the presence of {B}."},

			-- Negative
			{"Eclipse custody dispute", "yellow", -10, "calendar", "{A} asserts custody over eclipse readings, while {B} contests the authority to declare what the darkness revealed."},
			{"False eclipse charge", "red", -20, "doctrine", "{A} accuses {B} of falsifying eclipse omens to alter public decisions."},
			{"Illumination veto", "yellow", -11, "governance", "{A} vetoes a ritual calendar proposed by {B}, citing insufficient celestial proof."},
			{"Moon-shadow panic", "red", -18, "panic", "{A} is blamed for declaring a dangerous shadow-sign that panics communities aligned with {B}."},
			{"Tarsican seal dispute", "yellow", -10, "archive", "{A} refuses to unseal lunar records requested by {B}."},
			{"Eclipse procession ban", "red", -19, "festival", "{A} bans an eclipse procession planned by {B}, calling it spiritually unsafe."}
		}
	},

	["Sanctum Vitalis"] = {
		targets = {"Temple Alabaster", "Ordo Amicitia", "Mystery of the Verdant Embrace", "Harmony Sanctum", "Rex Catonis", "Order of the Umbral Oracle"},
		events = {
			-- Positive
			{"Vitalis mercy corridor", "green", 12, "relief", "{A} opens a mercy corridor for the sick and displaced, improving cooperation with {B}."},
			{"Noctic stabilization clinic", "green", 10, "noctic", "{A} establishes a Noctic stabilization clinic with supply and witness support from {B}."},
			{"Miracle of returning breath", "green", 18, "miracle", "{A} records a patient revived after ritual silence, and {B} recognizes the case as a public blessing."},
			{"Healer convoy compact", "green", 9, "relief", "{A} sends healers under escort terms accepted by {B}."},
			{"Triage registry concord", "green", 8, "medicine", "{A} and {B} agree to a shared triage registry for crisis districts."},
			{"Medicine garden dedication", "green", 9, "ecology", "{A} dedicates a medicine garden for households cared for by {B}."},

			-- Negative
			{"Triage priority dispute", "yellow", -9, "medicine", "{A} prioritizes the wounded by medical need, and {B} objects on ritual or legal grounds."},
			{"Quarantine lantern order", "yellow", -10, "quarantine", "{A} raises quarantine lanterns around a district, forcing {B} to suspend public rites."},
			{"Healer abduction alarm", "red", -20, "security", "{A} reports that healers have been abducted while serving households under {B}'s protection."},
			{"Contaminated tonic scandal", "red", -18, "medicine", "{A} accuses suppliers linked to {B} of distributing contaminated tonics."},
			{"Clinic access denial", "yellow", -10, "relief", "{A} denies clinic access to a group until {B} provides proper records."},
			{"Noctic hunger panic", "red", -22, "noctic", "{A} warns of uncontrolled Noctic hunger in households watched by {B}."}
		}
	},

	["Temple Alabaster"] = {
		targets = {"Sanctum Vitalis", "Ordo Amicitia", "Court of the Ironclad", "Rex Catonis", "Mystery of the Stygian Veil", "Ignis Aeternum"},
		events = {
			-- Positive
			{"Alabaster sanctuary ruling", "green", 10, "sanctuary", "{A} grants sanctuary under strict purity rules, allowing {B} to accept a difficult compromise without losing face."},
			{"White veil reprieve", "green", 12, "mercy", "{A} places a condemned petitioner beneath the white veil, giving {B} a ritual path to clemency."},
			{"Alabaster wash compact", "green", 9, "reconciliation", "{A} conducts a public washing rite that allows {B} to re-enter negotiations."},
			{"Pure threshold escort", "green", 8, "security", "{A} escorts vulnerable families across a purified threshold with support from {B}."},
			{"Sanctuary registry concord", "green", 9, "law", "{A} and {B} reconcile sanctuary registries after a disputed protection claim."},
			{"White-candle healing rite", "green", 10, "medicine", "{A} hosts a white-candle healing rite recognized by {B} as lawful mercy."},

			-- Negative
			{"Purity cordon dispute", "yellow", -10, "purity", "{A} establishes a purity cordon that {B} condemns as excessive."},
			{"Defiled threshold accusation", "red", -19, "sacrilege", "{A} accuses {B} of permitting a defiled person or object to cross a protected threshold."},
			{"Sanctuary breach alarm", "red", -23, "security", "{A} claims that sanctuary was breached by agents associated with {B}."},
			{"White veil exclusion", "yellow", -11, "mercy", "{A} refuses to extend the white veil to a petitioner supported by {B}."},
			{"Purification ledger purge", "yellow", -10, "audit", "{A} purges names from a purity ledger, drawing protest from {B}."},
			{"Alabaster stigma charge", "red", -18, "social order", "{A} is accused by {B} of turning purity law into public stigma."}
		}
	},

	["Court of the Ironclad"] = {
		targets = {"Reverie Nebulous", "Harmony Sanctum", "Ignis Aeternum", "Mystery of the Verdant Embrace", "Temple Alabaster", "Guild of Golden Shadows"},
		events = {
			-- Positive
			{"Shield oath escort", "green", 9, "security", "{A} escorts vulnerable pilgrims under shield oath, earning temporary trust from {B}."},
			{"Iron gate relief guard", "green", 10, "relief", "{A} guards relief convoys requested by {B}."},
			{"Ferrum restraint compact", "green", 9, "governance", "{A} agrees to keep armed devotees inside designated precincts after mediation by {B}."},
			{"Wolf-standard protection", "green", 8, "security", "{A} places a wolf-standard at a threatened shrine and grants {B} safe access."},
			{"Ironclad prisoner transfer", "green", 10, "law", "{A} transfers detainees to a supervised process accepted by {B}."},
			{"Citadel oath renewal", "green", 9, "reconciliation", "{A} renews a defensive oath in terms that {B} can publicly accept."},

			-- Negative
			{"Ironclad reprisal column", "red", -20, "reprisal", "{A} deploys a reprisal column after shrine unrest, and {B} denounces the action as punishment disguised as protection."},
			{"War-dog intimidation charge", "yellow", -10, "security", "{A} posts war-dogs at a disputed gate, and {B} calls the move intimidation."},
			{"Ferrum muster alarm", "red", -22, "war", "{A} musters armed devotees in Ferrum Citadel, convincing {B} that a sectarian campaign may be imminent."},
			{"Iron tithe enforcement", "yellow", -11, "tax", "{A} enforces an iron tithe for shrine defense, angering communities aligned with {B}."},
			{"Gate of the wolf omen", "red", -26, "mythic", "{A} claims a wolf-omen authorizes emergency rule, while {B} warns that defense has become conquest."},
			{"Unauthorized cudgel court", "red", -19, "justice", "{A} holds a rough justice hearing that {B} refuses to recognize as lawful."}
		}
	},

	["Accord Concordia"] = {
		targets = {"Harmony Sanctum", "Sanctum Delphica", "Ordo Amicitia", "Celestial Harmony Sect", "Mystery of Red Mirth", "Rex Catonis"},
		events = {
			-- Positive
			{"Concordian oath table", "green", 11, "mediation", "{A} convenes an oath table where {B} agrees to temporary restraint and shared public wording."},
			{"Three-bell ceasefire", "green", 12, "ceasefire", "{A} brokers a three-bell ceasefire that prevents retaliatory violence by {B}."},
			{"Concordia hostage exchange", "green", 14, "security", "{A} arranges a hostage or detainee exchange recognized by {B}."},
			{"Unity banner procession", "green", 9, "festival", "{A} leads a unity banner procession in which {B} accepts a shared place of honor."},
			{"Shared wording decree", "green", 8, "diplomacy", "{A} drafts shared wording that allows {B} to step back from a dangerous accusation."},
			{"Pax candle compact", "green", 10, "reconciliation", "{A} lights Pax candles with {B}, formally ending a local feud."},

			-- Negative
			{"Broken-pax accusation", "yellow", -9, "diplomacy", "{A} accuses {B} of violating a peace clause hidden in a prior compact."},
			{"Failed unity procession", "red", -18, "festival", "{A}'s unity procession collapses into accusations, and {B} abandons the route."},
			{"Pax banner desecration", "red", -20, "sacrilege", "{A} reports the desecration of a Pax banner and blames extremists tolerated by {B}."},
			{"Concordian neutrality crisis", "yellow", -10, "diplomacy", "{A} refuses to take a side, and {B} condemns neutrality as cowardice."},
			{"Broken hostage pledge", "red", -22, "security", "{A} says a detainee pledge was broken by followers of {B}."},
			{"Unity oath walkout", "yellow", -9, "mediation", "{A} watches {B} walk out before the final unity oath is sworn."}
		}
	},

	["Mystery of the Stygian Veil"] = {
		targets = {"Ordo Amicitia", "Temple Alabaster", "Rex Catonis", "Sanctum Vitalis", "Temple Illuminata", "Order of the Umbral Oracle"},
		events = {
			-- Positive
			{"Black veil mourning compact", "green", 10, "mourning", "{A} gives {B} a formal role in mourning rites for families caught between death and rebirth customs."},
			{"Phoenix ash miracle", "green", 18, "miracle", "{A} reports that ash from a funerary bowl formed a phoenix-sign, and {B} accepts it as a rare blessing."},
			{"Threshold witness accord", "green", 9, "law", "{A} permits {B} to witness a threshold rite under strict terms."},
			{"Acheron mercy vigil", "green", 10, "mourning", "{A} holds a mercy vigil for disputed dead, allowing {B} to participate without endorsing all rites."},
			{"Rebirth registry correction", "green", 8, "archive", "{A} corrects rebirth records requested by {B}."},
			{"Stygian hospice compact", "green", 11, "medicine", "{A} opens hospice passage for families under the care of {B}."},

			-- Negative
			{"Stygian forbidden passage", "red", -18, "threshold", "{A} is accused of opening a forbidden passage between mourning, rebirth, and Noctic blood-memory rites, alarming {B}."},
			{"Corpse-road secrecy dispute", "yellow", -11, "secrecy", "{A} moves the dead by a hidden road, and {B} demands public accounting."},
			{"Noctic rebirth scandal", "red", -22, "noctic", "{A} is accused of confusing Noctic induction with legitimate rebirth ritual, provoking {B}."},
			{"Acheron threshold warning", "yellow", -12, "warning", "{A} warns that {B} has mishandled a threshold rite and endangered the passage of the dead."},
			{"Unlicensed funerary descent", "red", -20, "sacrilege", "{A} conducts a descent rite without clearance accepted by {B}."},
			{"Black water accusation", "yellow", -10, "ritual", "{A} is accused by {B} of using black water from an unverified funerary source."}
		}
	},

	["Sylvan Fellowship"] = {
		targets = {"Eon Fellowship", "Harmony Sanctum", "Rex Catonis", "Court of the Ironclad", "Mystery of the Verdant Embrace", "Ignis Aeternum"},
		events = {
			-- Positive
			{"Antler moot compact", "green", 9, "mediation", "{A} convenes an antler moot where {B} accepts natural witness under strict limits."},
			{"Faunian fertility accord", "green", 12, "festival", "{A} grants {B} a place in a fertility accord after a successful birth-season rite."},
			{"Wild grove protection", "green", 9, "ecology", "{A} protects a wild grove used by pilgrims associated with {B}."},
			{"Beast-herder rescue", "green", 8, "relief", "{A} sends beast-herders to rescue stranded travelers under the care of {B}."},
			{"Sylvan seed exchange", "green", 9, "agriculture", "{A} shares wild seed knowledge with growers or healers connected to {B}."},
			{"Faunian boundary compact", "green", 10, "law", "{A} marks a wild boundary in terms that {B} agrees to enforce."},

			-- Negative
			{"Sylvan beast-oath", "yellow", -11, "law", "{A} recognizes a wild beast-oath that {B} refuses to treat as lawful testimony."},
			{"Feral shrine raid", "red", -20, "violence", "{A} is blamed after feral devotees raid a shrine storehouse associated with {B}."},
			{"Wild seed release", "yellow", -10, "ecology", "{A} releases wild seed across managed farmland, angering {B} over boundaries and crop law."},
			{"Claw-mark prophecy", "red", -18, "omen", "{A} treats claw-marks as prophecy, and {B} warns that animal sign has replaced lawful testimony."},
			{"Horned revel trespass", "yellow", -9, "festival", "{A}'s horned revelers trespass through a precinct managed by {B}."},
			{"Beast-blood accusation", "red", -21, "sacrilege", "{A} is accused of permitting beast-blood offerings that {B} considers unlawful."}
		}
	},

	["Mystery of Red Mirth"] = {
		targets = {"Rex Catonis", "Temple Alabaster", "Ordo Solis Invicti", "Sanctum Vitalis", "Accord Concordia", "Ordo Amicitia"},
		events = {
			-- Positive
			{"Unbroken cup miracle", "green", 18, "miracle", "{A} drops a festival cup before the crowd and it does not break, prompting {B} to accept a new restrained joy rite."},
			{"Mirthful amnesty feast", "green", 10, "reconciliation", "{A} hosts an amnesty feast where {B} agrees to forgive minor public offenses."},
			{"Red garland relief drive", "green", 9, "relief", "{A} turns a revel into a relief drive supervised by {B}."},
			{"Festival sobriety compact", "green", 8, "festival", "{A} agrees to keep revels within limits set by {B}."},
			{"Joyful witness procession", "green", 9, "culture", "{A} leads a joyful witness procession that calms communities associated with {B}."},
			{"Scarlet cup concord", "green", 10, "ritual", "{A} shares the scarlet cup with {B} as a sign that pleasure and order can coexist."},

			-- Negative
			{"Red Mirth excess", "yellow", -9, "festival", "{A} is blamed for festival excess that leaves {B} to repair injuries, ledgers, and public discipline."},
			{"Wine tithe dispute", "yellow", -10, "trade", "{A} withholds a wine tithe claimed by {B} for relief or discipline work."},
			{"Masked reveler riot", "red", -19, "violence", "{A} is blamed after masked revelers overturn public benches and mock the authority of {B}."},
			{"Scarlet procession ban", "yellow", -11, "public order", "{A} defies a procession limit imposed by {B}, arguing that joy cannot be licensed."},
			{"Drunken oracle scandal", "red", -18, "scandal", "{A} permits an intoxicated oracle performance that {B} declares spiritually dangerous."},
			{"Mirth tax refusal", "yellow", -9, "tax", "{A} refuses to pay a public-cleaning levy after a major revel, angering {B}."}
		}
	},

	["Rex Catonis"] = {
		targets = {"Celestial Harmony Sect", "Guild of Golden Shadows", "Ignis Aeternum", "Mystery of Red Mirth", "Court of the Ironclad", "Temple Alabaster"},
		events = {
			-- Positive
			{"Discipline edict", "green", 8, "order", "{A} issues a discipline edict that {B} accepts as necessary to prevent escalation."},
			{"Cato's iron docket", "green", 9, "law", "{A} opens an iron docket for old grievances, allowing {B} to submit claims without retaliation."},
			{"Lawful witness escort", "green", 10, "justice", "{A} escorts witnesses safely to a hearing requested by {B}."},
			{"Catonian record amnesty", "green", 8, "reconciliation", "{A} clears minor offenses from a public record after petition by {B}."},
			{"Orderly festival compact", "green", 9, "festival", "{A} accepts a festival plan from {B} after strict procession rules are adopted."},
			{"Custody chain concord", "green", 10, "audit", "{A} confirms a custody chain that allows {B} to avoid scandal."},

			-- Negative
			{"Catonian tribunal summons", "yellow", -10, "law", "{A} summons witnesses to a tribunal, and {B} protests that judgment has arrived before truth."},
			{"Chain-of-custody purge", "red", -20, "audit", "{A} purges corrupted custody records and accuses {B} of tolerating forged seals."},
			{"Public flogging dispute", "yellow", -12, "punishment", "{A} orders public punishment after unrest, while {B} argues the sentence will deepen sectarian anger."},
			{"Judgment before dawn", "red", -22, "crisis", "{A} delivers judgment before dawn, and {B} declares the procedure spiritually defective."},
			{"Iron decree overreach", "yellow", -11, "law", "{A} issues an iron decree that {B} says exceeds lawful authority."},
			{"Catonis Atrium execution alarm", "red", -24, "punishment", "{A} approves executions after a tribunal that {B} considers rushed or politically shaped."}
		}
	},

	["Sanctum Delphica"] = {
		targets = {"Accord Concordia", "Order of the Umbral Oracle", "Guild of Golden Shadows", "Reverie Nebulous", "Celestial Harmony Sect", "Conclave Illuminara"},
		events = {
			-- Positive
			{"Delphic archive concord", "green", 9, "archive", "{A} reconciles contradictory archive entries, allowing {B} to withdraw an accusation without humiliation."},
			{"Introspective fast compact", "green", 8, "ritual", "{A} and {B} agree to an introspective fast before reopening negotiations."},
			{"Delphic glass miracle", "green", 16, "miracle", "{A} reports that cracked glass rejoined during testimony, and {B} accepts the restored statement as binding."},
			{"Reflection chamber review", "green", 9, "justice", "{A} invites {B} to review testimony in a reflection chamber under neutral rules."},
			{"Archive custody settlement", "green", 10, "archive", "{A} settles a custody dispute over records needed by {B}."},
			{"Delphic quieting rite", "green", 8, "mediation", "{A} performs a quieting rite that allows {B} to step back from accusation."},

			-- Negative
			{"Mirror testimony dispute", "yellow", -9, "testimony", "{A} accepts reflected testimony that {B} considers too indirect for judgment."},
			{"Archive theft accusation", "red", -20, "theft", "{A} accuses agents linked to {B} of stealing sealed archive leaves."},
			{"False reflection scandal", "yellow", -11, "scandal", "{A} reveals that a public confession was copied from an older reflection record, embarrassing {B}."},
			{"Delphic seal refusal", "yellow", -10, "archive", "{A} refuses to break a seal requested by {B}."},
			{"Reflection heresy inquiry", "red", -18, "doctrine", "{A} opens an inquiry into reflective rites practiced by {B}."},
			{"Contradictory witness crisis", "red", -19, "justice", "{A} publishes contradictions in witness testimony, provoking outrage from {B}."}
		}
	},

	["Ordo Amicitia"] = {
		targets = {"Temple Alabaster", "Sanctum Vitalis", "Accord Concordia", "Mystery of Red Mirth", "Mystery of the Stygian Veil", "Rex Catonis"},
		events = {
			-- Positive
			{"Amicitian ransom of mercy", "green", 11, "charity", "{A} pays a ransom of mercy for detained families, and {B} agrees to treat the act as charity rather than interference."},
			{"Mercy riot shelter", "green", 10, "relief", "{A} shelters riot victims and convinces {B} to pause retaliatory measures."},
			{"Bread-and-oath procession", "green", 9, "festival", "{A} leads a bread-and-oath procession accepted by {B} as a sign of restored civic trust."},
			{"Charity convoy compact", "green", 10, "relief", "{A} sends charity convoys into a tense district under guarantees from {B}."},
			{"Mercy ledger reconciliation", "green", 8, "audit", "{A} reconciles charity ledgers with auditors from {B}."},
			{"Forgiveness table", "green", 9, "reconciliation", "{A} opens a forgiveness table where {B} may seek pardon without dishonor."},

			-- Negative
			{"Charity ledger dispute", "yellow", -9, "ledger", "{A} redirects charity ledgers toward emergency relief, and {B} objects to the lack of prior approval."},
			{"Forbidden clemency petition", "red", -18, "law", "{A} petitions for clemency in a case that {B} considers unforgivable."},
			{"Martyr of charity rumor", "yellow", -10, "rumor", "{A} spreads a martyr-charity account that {B} believes will inflame public emotion."},
			{"Mercy obstruction charge", "yellow", -9, "relief", "{A} accuses {B} of obstructing aid to families marked by scandal."},
			{"Unregistered orphan shelter", "red", -18, "custody", "{A} shelters orphans without registry papers demanded by {B}."},
			{"Charity-as-subversion warning", "red", -20, "politics", "{A} is accused by {B} of using mercy networks to undermine lawful punishment."}
		}
	},

	["Temple Illuminata"] = {
		targets = {"Conclave Illuminara", "Order of the Umbral Oracle", "Eon Fellowship", "Sanctum Delphica", "Mystery of the Stygian Veil", "Celestial Harmony Sect"},
		events = {
			-- Positive
			{"Epiphany witness compact", "green", 10, "oracle", "{A} permits {B} to witness an epiphany rite under strict silence."},
			{"Star-flame revelation", "green", 16, "miracle", "{A} records a star-flame revelation that {B} accepts as a warning rather than a threat."},
			{"Illuminated dream concord", "green", 9, "oracle", "{A} and {B} compare dream and epiphany records to calm public anxiety."},
			{"Aureum insight exchange", "green", 8, "archive", "{A} shares epiphany notes with scholars from {B}."},
			{"Nikolaj sleep blessing", "green", 10, "medicine", "{A} conducts a sleep blessing for troubled households under the care of {B}."},
			{"Cosmic witness circle", "green", 9, "ritual", "{A} allows {B} into a cosmic witness circle to verify an omen."},

			-- Negative
			{"Illuminata impossible omen", "yellow", -9, "omen", "{A} announces an impossible omen, and {B} demands proof before the claim can alter ritual calendars."},
			{"Cosmic sleep decree", "yellow", -10, "doctrine", "{A} declares a sleep-decree after troubling visions, forcing {B} to suspend routine operations."},
			{"Hypnos chamber scandal", "red", -19, "scandal", "{A} is accused of inducing visions in a sealed Hypnos chamber without consent recognized by {B}."},
			{"Aureum epiphany schism", "red", -22, "schism", "{A} proclaims a new epiphany lineage, and {B} warns that it may divide the faithful."},
			{"Star-sign forgery charge", "yellow", -11, "archive", "{A} accuses {B} of copying or altering star-sign records."},
			{"Unwaking petitioner crisis", "red", -21, "medicine", "{A} is blamed after a petitioner fails to wake from a sanctioned vision sleep, provoking {B}."}
		}
	}
}


local festivalPressures = {
	Atosien = {
		positive = {"Ordo Solis Invicti", "Conclave Illuminara", "Rex Catonis"},
		negative = {}
	},
	Eosena = {
		positive = {"Order of Aurora Mystica", "Conclave Illuminara", "Accord Concordia"},
		negative = {"Celestial Harmony Sect"}
	},
	Micrasha = {
		positive = {"Harmony Sanctum", "Mystery of the Verdant Embrace", "Accord Concordia"},
		negative = {"Ignis Aeternum"}
	},
	Pyreska = {
		positive = {"Ignis Aeternum", "Azure Sentinel Sect", "Sylvan Fellowship"},
		negative = {"Harmony Sanctum", "Mystery of the Verdant Embrace"}
	},
	Indomin = {
		positive = {"Celestial Harmony Sect", "Sanctum Delphica", "Accord Concordia"},
		negative = {"Order of Aurora Mystica", "Conclave Illuminara", "Rex Catonis"}
	},
	Chrysen = {
		positive = {"Guild of Golden Shadows", "Reverie Nebulous", "Sanctum Delphica"},
		negative = {"Azure Sentinel Sect"}
	},
	Thalassian = {
		positive = {"Azure Sentinel Sect", "Ignis Aeternum", "Reverie Nebulous"},
		negative = {"Eon Fellowship", "Harmony Sanctum"}
	},
	Nephelia = {
		positive = {"Reverie Nebulous", "Guild of Golden Shadows", "Sanctum Delphica"},
		negative = {"Ignis Aeternum", "Court of the Ironclad"}
	},
	Glinaeus = {
		positive = {"Eon Fellowship", "Court of the Ironclad", "Order of Aurora Mystica"},
		negative = {"Ignis Aeternum", "Azure Sentinel Sect"}
	},
	Noctien = {
		positive = {"Order of the Umbral Oracle", "Ordo Amicitia", "Sanctum Delphica"},
		negative = {}
	},
	Opsithia = {
		positive = {"Mystery of the Verdant Embrace", "Sanctum Vitalis", "Sylvan Fellowship"},
		negative = {"Ignis Aeternum"}
	},
	Stygian = {
		positive = {"Mystery of the Stygian Veil", "Temple Illuminata", "Order of the Umbral Oracle"},
		negative = {"Ordo Amicitia"}
	},
	Faunian = {
		positive = {"Sylvan Fellowship", "Mystery of the Verdant Embrace", "Sanctum Vitalis"},
		negative = {"Eon Fellowship"}
	},
	Silenian = {
		positive = {"Mystery of Red Mirth", "Rex Catonis", "Accord Concordia"},
		negative = {}
	},
	Catosien = {
		positive = {"Rex Catonis", "Ordo Solis Invicti", "Temple Alabaster"},
		negative = {"Celestial Harmony Sect"}
	}
}

local function pickTwoDistinct(nextInt, list)
	if not list or #list < 2 then
		return nil, nil
	end

	local a = pick(nextInt, list)
	local b = pick(nextInt, list)
	local tries = 0

	while a == b and tries < 20 do
		b = pick(nextInt, list)
		tries = tries + 1
	end

	if a == b then
		return list[1], list[2]
	end

	return a, b
end

local function makeEventText(template, a, b)
	local s = tostring(template or "")
	s = s:gsub("{A}", cultLink(a))
	s = s:gsub("{B}", cultLink(b))
	return s
end

local function generateDailyEvents(dateInfo, args)
	args = args or {}
local count = tonumber(args.events or args.eventcount or args.relationEvents) or 10
count = clamp(count, 1, 20)

	local seed = hash31("CultRelationsDailyBVR:" .. tostring(dateInfo.year) .. ":" .. tostring(dateInfo.dayOfYear))
	local nextInt = lcg(seed)
	local events = {}
	local used = {}

	local tries = 0
	while #events < count and tries < 100 do
		tries = tries + 1

local e = pickWeighted(nextInt, eventPool)

local a, b
if e.actor then
	a = normalizeCultName(e.actor)
	local targetList = e.targets or e.cults or {}
	b = normalizeCultName(pick(nextInt, targetList))
	local tries2 = 0
	while (not b or b == "" or b == a) and tries2 < 20 do
		b = normalizeCultName(pick(nextInt, targetList))
		tries2 = tries2 + 1
	end
else
	a, b = pickTwoDistinct(nextInt, e.cults)
	a, b = normalizeCultName(a), normalizeCultName(b)
end

if a and b and a ~= "" and b ~= "" and a ~= b then
	local key = sortedPair(a, b) .. "::" .. tostring(e.title)

	if not used[key] then
		used[key] = true
		table.insert(events, {
			title = e.title,
			type = e.type,
			severity = e.severity,
			a = a,
			b = b,
			delta = tonumber(e.delta) or 0,
			text = makeEventText(e.text, a, b)
		})
	end
end
	end

	-- Zodiac-season pressure. This makes the current zodiac period matter.
	local fp = festivalPressures[dateInfo.zodiac]
	if fp then
		local pa, pb = pickTwoDistinct(nextInt, fp.positive)
		if pa and pb then
			table.insert(events, {
				title = dateInfo.zodiac .. " ritual alignment",
				type = "zodiac",
				severity = "green",
				a = pa,
				b = pb,
				delta = 5,
				text = cultLink(pa) .. " and " .. cultLink(pb) .. " benefit from shared " .. dateInfo.zodiac .. " ritual emphasis."
			})
		end

		if fp.negative and #fp.negative > 0 and fp.positive and #fp.positive > 0 then
			local na = pick(nextInt, fp.negative)
			local nb = pick(nextInt, fp.positive)
			if na and nb and na ~= nb then
				table.insert(events, {
					title = dateInfo.zodiac .. " doctrinal strain",
					type = "zodiac",
					severity = "yellow",
					a = na,
					b = nb,
					delta = -4,
					text = cultLink(na) .. " experiences doctrinal strain with " .. cultLink(nb) .. " during the " .. dateInfo.zodiac .. " period."
				})
			end
		end
	end

	-- Noctic Vampirism pressure.
	-- These recurring pressures reflect raw-leaf custody, registered afflicted households,
	-- unauthorized induction, dream panic, and medical stabilization.
	if dateInfo.dayOfYear % 23 == 0 then
		table.insert(events, {
			title = "Noctic household registration review",
			type = "noctic",
			severity = "green",
			a = "Temple Alabaster",
			b = "Sanctum Vitalis",
			delta = 7,
			text = cultLink("Temple Alabaster") .. " and " .. cultLink("Sanctum Vitalis") .. " improve relations through a review of registered Noctic households and medical-stabilization records."
		})
	end

	if dateInfo.dayOfYear % 37 == 0 then
		table.insert(events, {
			title = "Raw Noctic-Rabrev interdiction",
			type = "noctic",
			severity = "yellow",
			a = "Guild of Golden Shadows",
			b = "Rex Catonis",
			delta = -6,
			text = cultLink("Guild of Golden Shadows") .. " and " .. cultLink("Rex Catonis") .. " clash over forged permissions, raw-leaf custody seals, and shrine-market inspection authority."
		})
	end

	if dateInfo.dayOfYear % 47 == 0 then
		table.insert(events, {
			title = "Noctic dream-panic containment",
			type = "noctic",
			severity = "green",
			a = "Order of the Umbral Oracle",
			b = "Reverie Nebulous",
			delta = 6,
			text = cultLink("Order of the Umbral Oracle") .. " and " .. cultLink("Reverie Nebulous") .. " coordinate dream-panic containment after reports of Noctic hallucinations and ancestral blood-memory visions."
		})
	end

	if dateInfo.dayOfYear % 59 == 0 then
		table.insert(events, {
			title = "Unauthorized Noctic induction scandal",
			type = "noctic",
			severity = "red",
			a = "Mystery of the Stygian Veil",
			b = "Temple Alabaster",
			delta = -12,
			text = cultLink("Mystery of the Stygian Veil") .. " and " .. cultLink("Temple Alabaster") .. " enter open conflict after accusations of unauthorized Noctic induction without proper medical oversight."
		})
	end

	-- Recurring calendar-cycle pressure.
	if dateInfo.dayOfYear % 17 == 0 then
		table.insert(events, {
			title = "Bacchanalia review",
			type = "governance",
			severity = "green",
			a = "Harmony Sanctum",
			b = "Accord Concordia",
			delta = 8,
			text = cultLink("Harmony Sanctum") .. " and " .. cultLink("Accord Concordia") .. " improve relations during a formal review of mediation protocols."
		})
	end

	if dateInfo.dayOfYear % 29 == 0 then
		table.insert(events, {
			title = "Fire-renewal dispute",
			type = "doctrine",
			severity = "yellow",
			a = "Ignis Aeternum",
			b = "Mystery of the Verdant Embrace",
			delta = -10,
			text = cultLink("Ignis Aeternum") .. " and " .. cultLink("Mystery of the Verdant Embrace") .. " clash over fire-renewal language near cultivated shrine land."
		})
	end

	if dateInfo.dayOfYear % 41 == 0 then
		table.insert(events, {
			title = "Lunar interpretation compact",
			type = "omen",
			severity = "green",
			a = "Conclave Illuminara",
			b = "Order of Aurora Mystica",
			delta = 9,
			text = cultLink("Conclave Illuminara") .. " and " .. cultLink("Order of Aurora Mystica") .. " publish a shared lunar interpretation compact."
		})
	end

	if dateInfo.dayOfYear % 53 == 0 then
		table.insert(events, {
			title = "Order-and-mirth procession",
			type = "festival",
			severity = "green",
			a = "Rex Catonis",
			b = "Mystery of Red Mirth",
			delta = 8,
			text = cultLink("Rex Catonis") .. " and " .. cultLink("Mystery of Red Mirth") .. " coordinate disciplined festival processions without arrests."
		})
	end

	return events
end

--------------------------------------------------------------------------------
-- 6. DAILY RELATION CALCULATION
--------------------------------------------------------------------------------

local function addDelta(scores, notes, a, b, delta, note)
	a, b = normalizeCultName(a), normalizeCultName(b)
	if a == "" or b == "" or a == b then
		return
	end

	local k = sortedPair(a, b)
	scores[k] = clamp((scores[k] or 0) + (tonumber(delta) or 0), -150, 150)
	notes[k] = notes[k] or {}
	table.insert(notes[k], note)
end

local function applyEvent(scores, notes, e)
	addDelta(scores, notes, e.a, e.b, e.delta, e.title)

	if e.type == "mythic" then
		local rowA = baselineRows[e.a]
		local rowB = baselineRows[e.b]

		for _, ally in ipairs((rowA and rowA.allies) or {}) do
			addDelta(scores, notes, e.a, ally, e.delta > 0 and 4 or -4, "mythic spillover")
		end

		for _, ally in ipairs((rowB and rowB.allies) or {}) do
			addDelta(scores, notes, e.b, ally, e.delta > 0 and 3 or -3, "mythic echo")
		end

		for _, enemy in ipairs((rowA and rowA.enemies) or {}) do
			addDelta(scores, notes, e.a, enemy, e.delta > 0 and -3 or -5, "mythic backlash")
		end
	end

	-- Severe events harden each participant's pre-existing enemies.
	if e.severity == "red" then
		for _, actor in ipairs({e.a, e.b}) do
			local row = baselineRows[actor]
			for _, enemy in ipairs((row and row.enemies) or {}) do
				addDelta(scores, notes, actor, enemy, -3, "crisis hardening")
			end
		end
	end

	-- Cooperative events improve spillover relations with standing allies.
	if e.severity == "green" then
		for _, actor in ipairs({e.a, e.b}) do
			local row = baselineRows[actor]
			for _, ally in ipairs((row and row.allies) or {}) do
				addDelta(scores, notes, actor, ally, 2, "cooperation spillover")
			end
		end
	end

	-- Yellow events add mild distrust to factions already in rivalry.
	if e.severity == "yellow" then
		for _, actor in ipairs({e.a, e.b}) do
			local row = baselineRows[actor]
			for _, enemy in ipairs((row and row.enemies) or {}) do
				addDelta(scores, notes, actor, enemy, -1, "suspicion spillover")
			end
		end
	end
end

local function buildDailyScores(dateInfo, args)
	local scores, notes = buildBaseScores()
	local events = generateDailyEvents(dateInfo, args)

	for _, e in ipairs(events) do
		applyEvent(scores, notes, e)
	end

	return scores, notes, events
end

function p.getDailyRelations(args)
	args = args or {}
	local dateInfo = getDateInfo(args)
	local scores, notes, events = buildDailyScores(dateInfo, args)

	return {
		date = dateInfo,
		scores = scores,
		notes = notes,
		events = events,
		cults = cults,
		baseline = baselineRows
	}
end

--------------------------------------------------------------------------------
-- 7. DISPLAY HELPERS
--------------------------------------------------------------------------------

local function relationLabel(score)
	score = tonumber(score) or 0

	if score >= 120 then return "Sacred union" end
	if score >= 100 then return "Miraculous compact" end
	if score >= 80 then return "Covenantal alliance" end
	if score >= 60 then return "Strong alliance" end
	if score >= 40 then return "Standing alliance" end
	if score >= 20 then return "Working cooperation" end
	if score >= 8 then return "Mild cooperation" end

	if score > -8 then return "Neutral / unstable" end
	if score > -20 then return "Minor tension" end
	if score > -40 then return "Open rivalry" end
	if score > -60 then return "Sectarian crisis" end
	if score > -80 then return "Near-war rupture" end
	if score > -100 then return "Open schism" end
	if score > -120 then return "Cult war footing" end
	if score > -140 then return "Sacred proscription" end

	return "Existential holy rupture"
end

local function scoreStyle(score)
	score = tonumber(score) or 0

	if score >= 100 then
		return "background:#b7e1cd;"
	elseif score >= 60 then
		return "background:#cfe8cf;"
	elseif score >= 20 then
		return "background:#e8f5e9;"
	elseif score > -20 then
		return "background:#f8f9fa;"
	elseif score > -60 then
		return "background:#fff3cd;"
	elseif score > -100 then
		return "background:#f8d7da;"
	else
		return "background:#e6b8b7; font-weight:bold;"
	end
end

local function severityBadge(severity)
	if severity == "red" then
		return '<span style="background:#f8d7da;border:1px solid #c99;padding:1px 5px;border-radius:8px;">Red</span>'
	elseif severity == "yellow" then
		return '<span style="background:#fff3cd;border:1px solid #cc9;padding:1px 5px;border-radius:8px;">Yellow</span>'
	end
	return '<span style="background:#d4edda;border:1px solid #9c9;padding:1px 5px;border-radius:8px;">Green</span>'
end

local function signed(n)
	n = tonumber(n) or 0
	if n > 0 then
		return "+" .. tostring(n)
	end
	return tostring(n)
end

local function getPairScore(data, a, b)
	a, b = normalizeCultName(a), normalizeCultName(b)
	return data.scores[sortedPair(a, b)] or 0
end

local function topPairs(data, limit, mode)
	limit = tonumber(limit) or 25
	mode = trim(mode or "important")

	local rows = {}

	for k, score in pairs(data.scores or {}) do
		local a, b = splitPairKey(k)
		if cultByName[a] and cultByName[b] then
			local item = {
				a = a,
				b = b,
				score = score,
				abs = math.abs(score)
			}

			if mode == "positive" then
				if score > 0 then
					table.insert(rows, item)
				end
			elseif mode == "negative" then
				if score < 0 then
					table.insert(rows, item)
				end
			else
				table.insert(rows, item)
			end
		end
	end

	table.sort(rows, function(x, y)
		if mode == "positive" then
			if x.score == y.score then return x.a .. x.b < y.a .. y.b end
			return x.score > y.score
		elseif mode == "negative" then
			if x.score == y.score then return x.a .. x.b < y.a .. y.b end
			return x.score < y.score
		else
			if x.abs == y.abs then return x.a .. x.b < y.a .. y.b end
			return x.abs > y.abs
		end
	end)

	local out = {}
	for i = 1, math.min(limit, #rows) do
		table.insert(out, rows[i])
	end

	return out
end


--------------------------------------------------------------------------------
-- CULT DAILY RANKING
-- Ranks cults by the total sum of all their pairwise relation scores.
-- Higher total = broader positive climate across the cult-relations network.
--------------------------------------------------------------------------------

local function cultRelationTotals(data)
	local totals = {}

	for _, c in ipairs(cults) do
		totals[c.name] = {
			name = c.name,
			total = 0,
			count = 0,
			positive = 0,
			negative = 0,
			neutral = 0,
			bestCult = nil,
			bestScore = nil,
			worstCult = nil,
			worstScore = nil
		}
	end

	for k, score in pairs(data.scores or {}) do
		local a, b = splitPairKey(k)
		score = tonumber(score) or 0

		if totals[a] and totals[b] then
			for _, side in ipairs({
				{name = a, other = b},
				{name = b, other = a}
			}) do
				local row = totals[side.name]
				row.total = row.total + score
				row.count = row.count + 1

				if score > 0 then
					row.positive = row.positive + 1
				elseif score < 0 then
					row.negative = row.negative + 1
				else
					row.neutral = row.neutral + 1
				end

				if not row.bestScore or score > row.bestScore then
					row.bestScore = score
					row.bestCult = side.other
				end

				if not row.worstScore or score < row.worstScore then
					row.worstScore = score
					row.worstCult = side.other
				end
			end
		end
	end

	local rows = {}
	for _, c in ipairs(cults) do
		local row = totals[c.name]
		row.average = row.count > 0 and (row.total / row.count) or 0
		table.insert(rows, row)
	end

	table.sort(rows, function(x, y)
		if x.total == y.total then
			if x.positive == y.positive then
				return x.name < y.name
			end
			return x.positive > y.positive
		end
		return x.total > y.total
	end)

	return rows
end

local function makeCultRankingTable(data, limit)
	limit = tonumber(limit) or #cults
	limit = clamp(limit, 1, #cults)

	local rows = cultRelationTotals(data)
	local out = {}

	table.insert(out, string.format("'''Daily cult-relations ranking — %s'''", data.date.longText))
	table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
	table.insert(out, '! Rank')
	table.insert(out, '! Cult')
	table.insert(out, '! Home city')
	table.insert(out, '! Total score')
	table.insert(out, '! Average relation')
	table.insert(out, '! Positive relations')
	table.insert(out, '! Negative relations')
	table.insert(out, '! Strongest relation')
	table.insert(out, '! Weakest relation')

	for i = 1, math.min(limit, #rows) do
		local r = rows[i]
		local cult = cultByName[r.name]
		local home = cult and cityLink(cult.city) or "—"

		local avgRounded = math.floor((r.average * 10) + 0.5) / 10
		local avgStatus = relationLabel(r.average)

		local best = r.bestCult and (cultLink(r.bestCult) .. " (" .. signed(r.bestScore) .. ")") or "—"
		local worst = r.worstCult and (cultLink(r.worstCult) .. " (" .. signed(r.worstScore) .. ")") or "—"

		table.insert(out, '|-')
		table.insert(out, '| ' .. i)
		table.insert(out, '| ' .. cultMark(r.name))
		table.insert(out, '| ' .. home)
		table.insert(out, string.format("| style='%s text-align:center; font-weight:bold;' | %s", scoreStyle(r.average), signed(r.total)))
		table.insert(out, string.format("| style='text-align:center;' | %s / %s", tostring(avgRounded), avgStatus))
		table.insert(out, "| style='text-align:center;' | " .. tostring(r.positive))
		table.insert(out, "| style='text-align:center;' | " .. tostring(r.negative))
		table.insert(out, '| ' .. best)
		table.insert(out, '| ' .. worst)
	end

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



local function noteText(data, a, b)
	local k = sortedPair(normalizeCultName(a), normalizeCultName(b))
	local list = data.notes[k] or {}
	if #list == 0 then
		return "neutral baseline"
	end

	local seen = {}
	local out = {}
	for _, note in ipairs(list) do
		if not seen[note] then
			seen[note] = true
			table.insert(out, note)
		end
	end

	return table.concat(out, "; ")
end

--------------------------------------------------------------------------------
-- 8. PUBLIC DISPLAY FUNCTIONS
--------------------------------------------------------------------------------

function p.eventLog(frame)
	local args = frame and frame.args or {}
	local data = p.getDailyRelations(args)

	local out = {}

	table.insert(out, string.format("'''Daily cult-relations event log — %s'''", data.date.longText))
	table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
	table.insert(out, '! Event')
	table.insert(out, '! Severity')
	table.insert(out, '! Location')
	table.insert(out, '! Cults')
	table.insert(out, '! Effect')
	table.insert(out, '! Notes')

	for _, e in ipairs(data.events or {}) do
		table.insert(out, '|-')
		table.insert(out, '| ' .. tostring(e.title or '—'))
		table.insert(out, '| ' .. severityBadge(e.severity))
		table.insert(out, '| ' .. eventCityLink(e))
		table.insert(out, '| ' .. cultMark(e.a) .. ' ↔ ' .. cultMark(e.b))
		table.insert(out, "| style='text-align:center;' | " .. signed(e.delta))
		table.insert(out, '| ' .. tostring(e.text or '—'))
	end

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

function p.makePair(frame)
	local args = frame and frame.args or {}
	local a = normalizeCultName(args.a or args[1])
	local b = normalizeCultName(args.b or args[2])

	if a == "" or b == "" then
		return "Specify two cults with <code>|a=</code> and <code>|b=</code>."
	end

	local data = p.getDailyRelations(args)
	local score = getPairScore(data, a, b)

	local out = {}

	table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"')
	table.insert(out, string.format("|+ Daily cult relation — %s", data.date.longText))
	table.insert(out, "! Cult A !! Cult B !! Score !! Status !! Basis")
	table.insert(out, "|-")
	table.insert(out, string.format(
		"| %s || %s || style='%s text-align:center; font-weight:bold;' | %d || %s || %s",
		cultMark(a),
		cultMark(b),
		scoreStyle(score),
		score,
		relationLabel(score),
		noteText(data, a, b)
	))
	table.insert(out, "|}")

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

function p.makeTable(frame)
	local args = frame and frame.args or {}
	local data = p.getDailyRelations(args)

local limit = tonumber(args.limit or args.relationsLimit) or 25
local mode = trim(args.mode or "important")
local showEvents = lower(args.showEvents or "yes") ~= "no"
local showRanking = lower(args.showRanking or args.ranking or "yes") ~= "no"
local rankingLimit = tonumber(args.rankingLimit or args.rankLimit) or #cults
local compact = lower(args.compact or "no") == "yes"

	local rows = topPairs(data, limit, mode)
	local out = {}

	table.insert(out, string.format(
		"<div style='border:1px solid #aaa;background:#f8f9fa;padding:8px;margin:6px 0;'>" ..
		"'''Reformed Stripping Path daily cult-relations index'''<br/>" ..
		"Date: '''%s'''<br/>" ..
		"Current zodiac period: '''%s'''<br/>" ..
		"Relations are recalculated from standing alliances, standing rivalries, mythic affinities, zodiac pressure, and embedded daily events. " ..
		"Positive scores indicate cooperation; negative scores indicate rivalry or hostility." ..
		"</div>",
		data.date.longText,
		data.date.zodiac
	))

	if showRanking then
		table.insert(out, makeCultRankingTable(data, rankingLimit))
		table.insert(out, "\n")
	end

	table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
	table.insert(out, "! Cult A !! Cult B !! Score !! Status")
	if not compact then
		table.insert(out, "!! Current basis")
	end

	for _, r in ipairs(rows) do
		table.insert(out, "|-")
		local row = string.format(
			"| %s || %s || style='%s text-align:center; font-weight:bold;' | %d || %s",
			cultMark(r.a),
			cultMark(r.b),
			scoreStyle(r.score),
			r.score,
			relationLabel(r.score)
		)
		if not compact then
			row = row .. " || " .. noteText(data, r.a, r.b)
		end
		table.insert(out, row)
	end

	table.insert(out, "|}")

	if showEvents then
		table.insert(out, "\n" .. p.eventLog({args = args}))
	end

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

function p.cultRanking(frame)
	local args = frame and frame.args or {}
	local data = p.getDailyRelations(args)
	local limit = tonumber(args.rankingLimit or args.rankLimit or args.limit) or #cults

	return makeCultRankingTable(data, limit)
end


function p.makeMatrix(frame)
	local args = frame and frame.args or {}
	local data = p.getDailyRelations(args)

	local out = {}

	table.insert(out, string.format("'''Cult-relations matrix — %s'''", data.date.longText))
	table.insert(out, '{| class="wikitable" style="font-size:85%; text-align:center;"')

	local header = {"! Cult"}
	for _, c in ipairs(cults) do
		table.insert(header, "!" .. c.key)
	end
	table.insert(out, table.concat(header, "\n"))

	for _, a in ipairs(cults) do
		local row = {"|-\n! " .. cultMark(a.name)}
		for _, b in ipairs(cults) do
			if a.name == b.name then
				table.insert(row, "| style='background:#eee;' | —")
			else
				local score = getPairScore(data, a.name, b.name)
				table.insert(row, string.format("| style='%s' | %d", scoreStyle(score), score))
			end
		end
		table.insert(out, table.concat(row, "\n"))
	end

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

function p.cultList(frame)
	local args = frame and frame.args or {}
	local out = {}

	table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
	table.insert(out, "! Key !! Cult / Mystery !! Divine !! Holy city !! General tendency")

	for _, c in ipairs(cults) do
		table.insert(out, "|-")
		table.insert(out, string.format(
			"| %s || %s || [[%s]] || %s || %s",
			c.key,
			cultMark(c.name),
			c.divine,
			cityLink(c.city),
			table.concat(c.tendency or {}, ", ")
		))
	end

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

function p.legend(frame)
	return table.concat({
		'{| class="wikitable" style="width:70%;"',
		'! Score range !! Meaning',
		'|-',
		'| 120 to 150 || Sacred union',
		'|-',
		'| 100 to 119 || Miraculous compact',
		'|-',
		'| 80 to 99 || Covenantal alliance',
		'|-',
		'| 60 to 79 || Strong alliance',
		'|-',
		'| 40 to 59 || Standing alliance',
		'|-',
		'| 20 to 39 || Working cooperation',
		'|-',
		'| 8 to 19 || Mild cooperation',
		'|-',
		'| -7 to 7 || Neutral / unstable',
		'|-',
		'| -19 to -8 || Minor tension',
		'|-',
		'| -39 to -20 || Open rivalry',
		'|-',
		'| -59 to -40 || Sectarian crisis',
		'|-',
		'| -79 to -60 || Near-war rupture',
		'|-',
		'| -99 to -80 || Open schism',
		'|-',
		'| -119 to -100 || Cult war footing',
		'|-',
		'| -139 to -120 || Sacred proscription',
		'|-',
		'| -150 to -140 || Existential holy rupture',
		'|}'
	}, "\n")
end

--------------------------------------------------------------------------------
-- 9. RAW / DEBUG FUNCTIONS
--------------------------------------------------------------------------------

function p.rawDate(frame)
	local args = frame and frame.args or {}
	local d = getDateInfo(args)
	return string.format(
		"dayOfYear=%s; month=%s; day=%s; zodiac=%s; year=%s; raw=%s",
		tostring(d.dayOfYear),
		tostring(d.month),
		tostring(d.dom),
		tostring(d.zodiac),
		tostring(d.year),
		tostring(d.raw)
	)
end

function p.rawScore(frame)
	local args = frame and frame.args or {}
	local a = normalizeCultName(args.a or args[1])
	local b = normalizeCultName(args.b or args[2])
	if a == "" or b == "" then
		return "0"
	end
	local data = p.getDailyRelations(args)
	return tostring(getPairScore(data, a, b))
end

function p.rawLabel(frame)
	local args = frame and frame.args or {}
	local a = normalizeCultName(args.a or args[1])
	local b = normalizeCultName(args.b or args[2])
	if a == "" or b == "" then
		return "Neutral / watchful"
	end
	local data = p.getDailyRelations(args)
	return relationLabel(getPairScore(data, a, b))
end

function p.rawEvents(frame)
	local args = frame and frame.args or {}
	local data = p.getDailyRelations(args)
	local out = {}
	for _, e in ipairs(data.events or {}) do
		table.insert(out, string.format(
			"%s: %s ↔ %s (%s)",
			e.title,
			e.a,
			e.b,
			signed(e.delta)
		))
	end
	return table.concat(out, "; ")
end

return p