<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://deadlock.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=~2026-SlotReloadHex823</id>
	<title>The Deadlock Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://deadlock.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=~2026-SlotReloadHex823"/>
	<link rel="alternate" type="text/html" href="https://deadlock.wiki/Special:Contributions/~2026-SlotReloadHex823"/>
	<updated>2026-09-15T09:02:57Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://deadlock.wiki/Module:Abilities/card?diff=104482</id>
		<title>Module:Abilities/card</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:Abilities/card?diff=104482"/>
		<updated>2026-07-31T12:04:47Z</updated>

		<summary type="html">&lt;p&gt;~2026-SlotReloadHex823: Size upgrade descriptions by measuring the rendered text against the box instead of counting bytes of the raw string, fixing punctuation and non-English localisations (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;local lang = require &amp;quot;Module:Lang&amp;quot;&lt;br /&gt;
local commonutils = require &amp;quot;Module:Utilities&amp;quot;&lt;br /&gt;
local utils = require &amp;quot;Module:Abilities/utils&amp;quot;&lt;br /&gt;
local Icon = require&amp;quot;Module:Abilities/icon&amp;quot;&lt;br /&gt;
local textfit = require &amp;quot;Module:TextFit&amp;quot;&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
local data = mw.loadJsonData(&amp;quot;Data:AbilityCards.json&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
function get_hero_key(hero_name)&lt;br /&gt;
	if hero_name == nil then return nil end&lt;br /&gt;
		&lt;br /&gt;
	-- if name starts with &amp;quot;hero_&amp;quot;, use it directly as the key&lt;br /&gt;
	if string.sub(hero_name, 1, 5) == &amp;quot;hero_&amp;quot; then return hero_name end&lt;br /&gt;
	&lt;br /&gt;
	local resource_lookup = mw.loadJsonData(&amp;quot;Data:ResourceLookup.json&amp;quot;)&lt;br /&gt;
	return resource_lookup[hero_name:lower()].key&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}--&lt;br /&gt;
-- Args:&lt;br /&gt;
--   HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under &amp;quot;Name&amp;quot;&lt;br /&gt;
--   ABILITY_NUM (required) - Selects ability at index 1 to 4&lt;br /&gt;
--   ADD_LINK - Add a hyperlink to the title to the associated page&lt;br /&gt;
--   NOTES - User note data to include in the footer of the ability card&lt;br /&gt;
p.get_ability_card = function(frame)&lt;br /&gt;
	local hero_name = frame.args[1]&lt;br /&gt;
	local ability_num = frame.args[2]&lt;br /&gt;
	local add_link = frame.args[3]&lt;br /&gt;
	local notes = frame.args[4]&lt;br /&gt;
	&lt;br /&gt;
	local hero_key = get_hero_key(hero_name)&lt;br /&gt;
	if (hero_key == nil) then return &#039;Hero with name&#039; .. hero_name .. &#039;not found&#039; end&lt;br /&gt;
	&lt;br /&gt;
	return build_ability_card(hero_key, ability_num, add_link, notes)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2&lt;br /&gt;
function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then &lt;br /&gt;
		return &#039;Ability data not found for hero &#039; ..hero_key.. &#039; and num &#039; .. ability_num&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local ability_name_localized = lang.get_string(ability.Key)&lt;br /&gt;
	local name_link = nil&lt;br /&gt;
	if add_link == &#039;true&#039; then&lt;br /&gt;
		name_link = ability_name_localized&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if notes_source_page ~= nil and notes ~= &amp;quot;&amp;quot; then&lt;br /&gt;
		-- Notes comes from a /Notes page, and the notes are not blank&lt;br /&gt;
		-- Confirm the notes source page exists, otherwise, don&#039;t display any notes&lt;br /&gt;
		local title = mw.title.new(notes_source_page)&lt;br /&gt;
		if not (title and title.exists) then&lt;br /&gt;
			notes = &amp;quot;&amp;quot;&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	&lt;br /&gt;
	local info1_desc = get_info_desc(hero_key, ability_num, 1)&lt;br /&gt;
	local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1)&lt;br /&gt;
&lt;br /&gt;
	local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1)&lt;br /&gt;
	&lt;br /&gt;
	if #info1_alt_boxes &amp;gt; 7 then&lt;br /&gt;
		error(#info1_alt_boxes .. &#039; alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2&#039;)	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local info2_desc = get_info_desc(hero_key, ability_num, 2)&lt;br /&gt;
	local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2)&lt;br /&gt;
	local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2)&lt;br /&gt;
	&lt;br /&gt;
	if #info2_alt_boxes &amp;gt; 7 then&lt;br /&gt;
		error(#info2_alt_boxes .. &#039; alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2&#039;)	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local info3_desc = get_info_desc(hero_key, ability_num, 3)&lt;br /&gt;
	local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3)&lt;br /&gt;
	local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3)&lt;br /&gt;
	&lt;br /&gt;
	if #info3_alt_boxes &amp;gt; 7 then&lt;br /&gt;
		error(#info3_alt_boxes .. &#039; alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2&#039;)	&lt;br /&gt;
	end&lt;br /&gt;
			&lt;br /&gt;
	local upgrades = get_upgrade_boxes(hero_key, ability_num)&lt;br /&gt;
	&lt;br /&gt;
	return frame:expandTemplate{&lt;br /&gt;
		title = &amp;quot;Ability_card_v2/Card&amp;quot;,&lt;br /&gt;
		args = {&lt;br /&gt;
			id = hero_key .. &#039;:&#039; .. ability.Key,&lt;br /&gt;
			hero_key = hero_key,&lt;br /&gt;
			ability_num = ability_num,&lt;br /&gt;
			&lt;br /&gt;
			-- Header info defined in various attributes	&lt;br /&gt;
			name = ability_name_localized,&lt;br /&gt;
			name_link = name_link,&lt;br /&gt;
			icon = lang.get_string(ability.Key, &#039;en&#039;) .. &#039;.png&#039;,&lt;br /&gt;
			&lt;br /&gt;
			channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,&lt;br /&gt;
			channel_time_scale = get_attr_scale(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),&lt;br /&gt;
			radius = ability.Radius and ability.Radius.Value,&lt;br /&gt;
			radius_scale = get_attr_scale(ability.Radius),&lt;br /&gt;
			radius_scale_type = get_attr_scale_type(ability.Radius),&lt;br /&gt;
			radius_scale_multiply = get_attr_scale_multiply(ability.Radius),&lt;br /&gt;
			range = ability.AbilityCastRange and ability.AbilityCastRange.Value,&lt;br /&gt;
			range_scale = get_attr_scale(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_type = get_attr_scale_type(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),&lt;br /&gt;
			duration = ability.AbilityDuration and ability.AbilityDuration.Value,&lt;br /&gt;
			duration_scale = get_attr_scale(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_type = get_attr_scale_type(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),&lt;br /&gt;
			-- ability_width = format_value_with_prepost(width_key, ability[width_key]),&lt;br /&gt;
			cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,&lt;br /&gt;
			cooldown_scale = get_attr_scale(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),&lt;br /&gt;
			charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,&lt;br /&gt;
			charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0,&lt;br /&gt;
			&lt;br /&gt;
			-- Info section #1 defined in &amp;quot;Info1&amp;quot; attribute&lt;br /&gt;
			hide_info1 = infobox_is_hidden(ability.Info1) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
			info1_desc = info1_desc,&lt;br /&gt;
			info1_mainbox1 = info1_main_boxes[1],&lt;br /&gt;
			info1_mainbox2 = info1_main_boxes[2],&lt;br /&gt;
			info1_mainbox3 = info1_main_boxes[3],&lt;br /&gt;
			info1_mainbox4 = info1_main_boxes[4],&lt;br /&gt;
			info1_mainbox5 = info1_main_boxes[5],&lt;br /&gt;
			info1_altbox1 =  info1_alt_boxes[1],&lt;br /&gt;
			info1_altbox2 =  info1_alt_boxes[2],&lt;br /&gt;
			info1_altbox3 =  info1_alt_boxes[3],&lt;br /&gt;
			info1_altbox4 =  info1_alt_boxes[4],&lt;br /&gt;
			info1_altbox5 =  info1_alt_boxes[5],&lt;br /&gt;
			info1_altbox6 =  info1_alt_boxes[6],&lt;br /&gt;
			info1_altbox7 =  info1_alt_boxes[7],&lt;br /&gt;
&lt;br /&gt;
			-- Info section #2 defined in &amp;quot;Info2&amp;quot; attribute&lt;br /&gt;
			hide_info2 = infobox_is_hidden(ability.Info2) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
			info2_desc = info2_desc,&lt;br /&gt;
			info2_mainbox1 = info2_main_boxes[1],&lt;br /&gt;
			info2_mainbox2 = info2_main_boxes[2],&lt;br /&gt;
			info2_mainbox3 = info2_main_boxes[3],&lt;br /&gt;
			info2_mainbox4 = info2_main_boxes[4],&lt;br /&gt;
			info2_mainbox5 = info2_main_boxes[5],&lt;br /&gt;
			info2_altbox1 =  info2_alt_boxes[1],&lt;br /&gt;
			info2_altbox2 =  info2_alt_boxes[2],&lt;br /&gt;
			info2_altbox3 =  info2_alt_boxes[3],&lt;br /&gt;
			info2_altbox4 =  info2_alt_boxes[4],&lt;br /&gt;
			info2_altbox5 =  info2_alt_boxes[5],&lt;br /&gt;
			info2_altbox6 =  info2_alt_boxes[6],&lt;br /&gt;
			info2_altbox7 =  info2_alt_boxes[7],&lt;br /&gt;
&lt;br /&gt;
			-- Info section #3 defined in &amp;quot;Info3&amp;quot; attribute&lt;br /&gt;
			hide_info3 = infobox_is_hidden(ability.Info3) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
			info3_desc = info3_desc,&lt;br /&gt;
			info3_mainbox1 = info3_main_boxes[1],&lt;br /&gt;
			info3_mainbox2 = info3_main_boxes[2],&lt;br /&gt;
			info3_mainbox3 = info3_main_boxes[3],&lt;br /&gt;
			info3_mainbox4 = info3_main_boxes[4],&lt;br /&gt;
			info3_mainbox5 = info3_main_boxes[5],&lt;br /&gt;
			info3_altbox1 =  info3_alt_boxes[1],&lt;br /&gt;
			info3_altbox2 =  info3_alt_boxes[2],&lt;br /&gt;
			info3_altbox3 =  info3_alt_boxes[3],&lt;br /&gt;
			info3_altbox4 =  info3_alt_boxes[4],&lt;br /&gt;
			info3_altbox5 =  info3_alt_boxes[5],&lt;br /&gt;
			info3_altbox6 =  info3_alt_boxes[6],&lt;br /&gt;
			info3_altbox7 =  info3_alt_boxes[7],&lt;br /&gt;
&lt;br /&gt;
			-- Ability upgrades defined in &amp;quot;Upgrades&amp;quot; attribute&lt;br /&gt;
			upgrade1 = upgrades[1],&lt;br /&gt;
			upgrade2 = upgrades[2],&lt;br /&gt;
			upgrade3 = upgrades[3],&lt;br /&gt;
			&lt;br /&gt;
			-- User-created ability notes&lt;br /&gt;
			notes = notes,&lt;br /&gt;
			notes_source_page = notes_source_page&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function infobox_is_hidden(infobox)&lt;br /&gt;
	if infobox == nil then &lt;br /&gt;
		return false&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return infobox.RequiresUpgradeIndex ~= nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- An attribute may carry either a single scaling or a list of them.&lt;br /&gt;
-- Extra entries are stat-category markers (duration, cooldown, range, radius,&lt;br /&gt;
-- healing, charges) that always hold a value of 1 and are never displayed, so&lt;br /&gt;
-- return the first entry that actually renders a badge.&lt;br /&gt;
-- A zero-valued scaling is still returned when nothing else is displayable. The&lt;br /&gt;
-- card renders it as an x0 placeholder, and the ability upgrade gadget fills&lt;br /&gt;
-- that element in when an upgrade grants real scaling (Doorman&#039;s Doorway).&lt;br /&gt;
local function pick_scale(attr)&lt;br /&gt;
	if not attr then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local scale = attr.Scale&lt;br /&gt;
	if type(scale) ~= &#039;table&#039; then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	-- A single scaling is stored directly as an object&lt;br /&gt;
	if scale[1] == nil then&lt;br /&gt;
		return scale&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local placeholder = nil&lt;br /&gt;
	for _, entry in ipairs(scale) do&lt;br /&gt;
		if type(entry) == &#039;table&#039; and Icon.isDisplayedScale(entry.Type) then&lt;br /&gt;
			if entry.Value ~= 0 then&lt;br /&gt;
				return entry&lt;br /&gt;
			end&lt;br /&gt;
			placeholder = placeholder or entry&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return placeholder&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Get scaling of attribute, return nil if none is found&lt;br /&gt;
function get_attr_scale(attr)&lt;br /&gt;
	local scale = pick_scale(attr)&lt;br /&gt;
	if not scale then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if scale.Value == 0 then&lt;br /&gt;
		return nil	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return commonutils.round_to_sig_fig(scale.Value, 3)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Get scaling type of attribute, return nil if none is found&lt;br /&gt;
function get_attr_scale_type(attr)&lt;br /&gt;
	local scale = pick_scale(attr)&lt;br /&gt;
	if not scale then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if scale.Value == 0 then&lt;br /&gt;
		return nil	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return scale.Type&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Check if scaling should be multiplied instead of added&lt;br /&gt;
function get_attr_scale_multiply(attr)&lt;br /&gt;
	local scale = pick_scale(attr)&lt;br /&gt;
	if not scale then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return scale.Multiply&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Get the description for an ability&#039;s info section&lt;br /&gt;
--{{#invoke:AbilityData|get_info_desc|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}--&lt;br /&gt;
p.get_info_desc = function(frame)&lt;br /&gt;
	local hero_key = frame.args[1]&lt;br /&gt;
	local ability_num = frame.args[2]&lt;br /&gt;
	local info_section_num = frame.args[3]&lt;br /&gt;
	&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return {} end -- return empty table, not string&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	if info_section.DescKey == nil then&lt;br /&gt;
		return &#039;&#039;	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return frame:preprocess(lang.get_string(info_section.DescKey))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_info_desc(hero_key, ability_num, info_section_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	if info_section.DescKey == nil then&lt;br /&gt;
		return &#039;&#039;	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	return frame:preprocess(lang.get_string(info_section.DescKey))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_main_boxes(hero_key, ability_num, info_section_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	local main = info_section.Main&lt;br /&gt;
	if main == nil then&lt;br /&gt;
		return {}&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	&lt;br /&gt;
	local props = {}&lt;br /&gt;
	&lt;br /&gt;
	-- Start with the standard Props array&lt;br /&gt;
	if main.Props then&lt;br /&gt;
		for _, prop in ipairs(main.Props) do&lt;br /&gt;
			table.insert(props, prop)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	&lt;br /&gt;
	local main_boxes = {}&lt;br /&gt;
	for k, prop in ipairs(props) do  -- use ipairs for ordered iteration&lt;br /&gt;
		local value = prop.Value&lt;br /&gt;
		local scale = pick_scale(prop)&lt;br /&gt;
		-- If it scales with melee damage, set the value using the hero&#039;s base melee to match in game visuals&lt;br /&gt;
		if scale and scale.Type == &#039;melee&#039; then&lt;br /&gt;
			local hero = get_hero_data(hero_key)&lt;br /&gt;
			value = prop.Value + hero.LightMeleeDamage * scale.Value&lt;br /&gt;
			elseif scale and scale.Type == &#039;heavy_melee&#039; then&lt;br /&gt;
			local hero = get_hero_data(hero_key)&lt;br /&gt;
			value = prop.Value + hero.HeavyMeleeDamage * scale.Value&lt;br /&gt;
			elseif scale and scale.Type == &#039;weapon_power&#039; then&lt;br /&gt;
			local hero = get_hero_data(hero_key)&lt;br /&gt;
			value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100&lt;br /&gt;
		end&lt;br /&gt;
			&lt;br /&gt;
		local icon = Icon.getAttrIcon(prop.Type)&lt;br /&gt;
		&lt;br /&gt;
		local main_box&lt;br /&gt;
		if prop.StatusEffect then&lt;br /&gt;
			main_box = frame:expandTemplate{&lt;br /&gt;
				title = &amp;quot;Ability_card_v2/Card/StatusBox&amp;quot;,&lt;br /&gt;
				args = {&lt;br /&gt;
					title = prop.Title,&lt;br /&gt;
					key = prop.Key,&lt;br /&gt;
					value = value,&lt;br /&gt;
					status_effect = prop.StatusEffect,&lt;br /&gt;
					icon = icon and icon.img,&lt;br /&gt;
					icon_link = icon and icon.link,&lt;br /&gt;
					icon_color = icon and icon.color,&lt;br /&gt;
					icon_size = icon and icon.size,&lt;br /&gt;
					scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),&lt;br /&gt;
					scale_type =  scale and scale.Type,&lt;br /&gt;
					-- allow nil value here as some status effects don&#039;t have a value but should not be hidden&lt;br /&gt;
					hide = value == 0 and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		else &lt;br /&gt;
			main_box = frame:expandTemplate{&lt;br /&gt;
				title = &amp;quot;Ability_card_v2/Card/MainBox&amp;quot;,&lt;br /&gt;
				args = {&lt;br /&gt;
					title = prop.Title,&lt;br /&gt;
					key = prop.Key,&lt;br /&gt;
					value = value,&lt;br /&gt;
					icon = icon and icon.img,&lt;br /&gt;
					icon_link = icon and icon.link,&lt;br /&gt;
					icon_color = icon and icon.color,&lt;br /&gt;
					icon_size = icon and icon.size,&lt;br /&gt;
					scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),&lt;br /&gt;
					scale_type =  scale and scale.Type,&lt;br /&gt;
					hide = (value == nil or value == 0) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		end&lt;br /&gt;
		table.insert(main_boxes, main_box)&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return main_boxes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_hero_data(hero_key)&lt;br /&gt;
	local hero_data = mw.loadJsonData(&amp;quot;Data:HeroData.json&amp;quot;)&lt;br /&gt;
	local hero = hero_data[hero_key]&lt;br /&gt;
	if (hero == nil) then return &amp;quot;Hero Not Found&amp;quot; end&lt;br /&gt;
	return hero&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_alt_boxes(hero_key, ability_num, info_section_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	local props = info_section.Alt&lt;br /&gt;
	if props == nil then&lt;br /&gt;
		return &#039;&#039;&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	&lt;br /&gt;
	local alt_boxes = {}&lt;br /&gt;
	&lt;br /&gt;
    for k, prop in ipairs(props) do&lt;br /&gt;
    	-- Some props don&#039;t have values, as those come from upgrades&lt;br /&gt;
    	-- For now, we will ignore these and only show data for the base ability&lt;br /&gt;
    	if prop.Value then&lt;br /&gt;
    		local icon = Icon.getAttrIcon(prop.Type)&lt;br /&gt;
&lt;br /&gt;
    		local value = prop.Value&lt;br /&gt;
    		local scale = pick_scale(prop)&lt;br /&gt;
&lt;br /&gt;
    		if scale and scale.Type == &#039;weapon_power&#039; then&lt;br /&gt;
    			local hero = get_hero_data(hero_key)&lt;br /&gt;
    			value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100&lt;br /&gt;
    		end&lt;br /&gt;
&lt;br /&gt;
    		local alt_box = frame:expandTemplate{&lt;br /&gt;
    			title = &amp;quot;Ability_card_v2/Card/AltBox&amp;quot;,&lt;br /&gt;
    			args = {&lt;br /&gt;
    				key = prop.Key,&lt;br /&gt;
    				value = value,&lt;br /&gt;
    				icon = icon and icon.img,&lt;br /&gt;
    				icon_link = icon and icon.link,&lt;br /&gt;
    				icon_color = icon and icon.color,&lt;br /&gt;
    				icon_size = icon and icon.size,&lt;br /&gt;
    				scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),&lt;br /&gt;
    				scale_type = scale and scale.Type,&lt;br /&gt;
    				hide = value == 0 and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
    			}&lt;br /&gt;
    		}&lt;br /&gt;
    		table.insert(alt_boxes, alt_box)&lt;br /&gt;
    	end&lt;br /&gt;
    end&lt;br /&gt;
	&lt;br /&gt;
	return alt_boxes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local UPGRADE_COST_MAP = {1, 2, 5}&lt;br /&gt;
&lt;br /&gt;
-- Upgrade description sizing. These are design proportions, not copies of any&lt;br /&gt;
-- CSS measurement: the browser supplies the box width through the container&lt;br /&gt;
-- query on .ac-upgrade-descwrap, so nothing here needs updating if the card&#039;s&lt;br /&gt;
-- widths, margins or padding change.&lt;br /&gt;
local UG_DESC_ASPECT = 0.357  -- max height of the text block, as a multiple of its width&lt;br /&gt;
local UG_LINE_HEIGHT = 1.4    -- emitted alongside the size so the two cannot drift&lt;br /&gt;
local UG_MAX_CQW     = 11.9   -- ceiling, as % of the text width (~16px on a full-width card)&lt;br /&gt;
local UG_MIN_CQW     = 6.7    -- floor (~9px)&lt;br /&gt;
&lt;br /&gt;
function get_upgrade_boxes(hero_key, ability_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
&lt;br /&gt;
	local upgrades = ability.Upgrades&lt;br /&gt;
	&lt;br /&gt;
	local upgrade_boxes = {}&lt;br /&gt;
	for k, upgrade in ipairs(upgrades) do&lt;br /&gt;
		local description = lang.get_string(upgrade.DescKey)&lt;br /&gt;
&lt;br /&gt;
		-- bypass some keys if they are mistakenly left in game files&lt;br /&gt;
		local IGNORE_DESC_KEYS = {&#039;citadel_ability_hornet_snipe_t3_desc&#039;, &#039;ability_fencer_ultimate_t3_desc&#039;}&lt;br /&gt;
		&lt;br /&gt;
		-- Generate a description if there is no localized string&lt;br /&gt;
		if (description == nil or description == &#039;&#039; or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then&lt;br /&gt;
			description = create_description(upgrade, frame)&lt;br /&gt;
		else&lt;br /&gt;
			-- Replace embedded value placeholders with just the base value&lt;br /&gt;
			-- Pattern: {&#039;Value&#039;: X, &#039;Scale&#039;: {...}} -&amp;gt; X&lt;br /&gt;
			description = description:gsub(&amp;quot;{&#039;Value&#039;:%s*([%d%.%-]+),%s*&#039;Scale&#039;:%s*{[^}]+}}&amp;quot;, function(value)&lt;br /&gt;
				return commonutils.round_to_sig_fig(tonumber(value), 3)&lt;br /&gt;
			end)&lt;br /&gt;
			-- Pattern: {&#039;Value&#039;: X} -&amp;gt; X&lt;br /&gt;
			description = description:gsub(&amp;quot;{&#039;Value&#039;:%s*([%d%.%-]+)}&amp;quot;, function(value)&lt;br /&gt;
				return commonutils.round_to_sig_fig(tonumber(value), 3)&lt;br /&gt;
			end)&lt;br /&gt;
		end&lt;br /&gt;
		&lt;br /&gt;
		-- Expand once, then size from the text the reader actually sees, rather&lt;br /&gt;
		-- than from a character count of the unexpanded string.&lt;br /&gt;
		local rendered = frame:preprocess(description)&lt;br /&gt;
		local cqw = textfit.fit(rendered, {&lt;br /&gt;
			aspect = UG_DESC_ASPECT,&lt;br /&gt;
			lineHeight = UG_LINE_HEIGHT,&lt;br /&gt;
			max = UG_MAX_CQW,&lt;br /&gt;
			min = UG_MIN_CQW,&lt;br /&gt;
		})&lt;br /&gt;
		&lt;br /&gt;
		local upgrade_scale = pick_scale(upgrade)&lt;br /&gt;
		local upgrade_box = frame:expandTemplate{&lt;br /&gt;
			title = &amp;quot;Ability_card_v2/Card/UpgradeBox&amp;quot;,&lt;br /&gt;
			args = {&lt;br /&gt;
				index = k,&lt;br /&gt;
				cost = UPGRADE_COST_MAP[k],	&lt;br /&gt;
				description = rendered,&lt;br /&gt;
				scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),&lt;br /&gt;
				scale_type =  upgrade_scale and upgrade_scale.Type,&lt;br /&gt;
				-- Measured against the box&#039;s own width, so the size stays correct&lt;br /&gt;
				-- when the card renders below its 500px max-width.&lt;br /&gt;
				fontsize = string.format(&#039;%.2fcqw&#039;, cqw),&lt;br /&gt;
				lineheight = UG_LINE_HEIGHT&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
		table.insert(upgrade_boxes, upgrade_box)&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return upgrade_boxes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
function create_description(prop, frame)&lt;br /&gt;
    local value_lines = {}   -- normal stat changes&lt;br /&gt;
    local scale_lines = {}   -- &amp;quot;Increased X scaling&amp;quot; notes&lt;br /&gt;
    local seen = {}&lt;br /&gt;
&lt;br /&gt;
    for k, v in pairs(prop) do&lt;br /&gt;
        if k == &#039;DescKey&#039; then&lt;br /&gt;
            -- skip&lt;br /&gt;
        elseif type(v) == &#039;table&#039; then&lt;br /&gt;
            local has_value = v.Value ~= nil&lt;br /&gt;
            local has_scale = v.Scale and v.Scale.Value and v.Scale.Value ~= 0&lt;br /&gt;
&lt;br /&gt;
            -- Value is 0 and we have a scaling change → scaling note&lt;br /&gt;
            if has_value and has_scale and v.Value == 0 then&lt;br /&gt;
                if not seen[&amp;quot;scale_&amp;quot; .. k] then&lt;br /&gt;
                    local scale_type = v.Scale.Type&lt;br /&gt;
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)&lt;br /&gt;
                    table.insert(scale_lines, &amp;quot;Increased &amp;quot; .. capitalized .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
                    seen[&amp;quot;scale_&amp;quot; .. k] = true&lt;br /&gt;
                end&lt;br /&gt;
            -- Only a scaling change, no value → scaling note (future bot correction)&lt;br /&gt;
            elseif not has_value and has_scale then&lt;br /&gt;
                if not seen[&amp;quot;scale_&amp;quot; .. k] then&lt;br /&gt;
                    local scale_type = v.Scale.Type&lt;br /&gt;
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)&lt;br /&gt;
                    table.insert(scale_lines, &amp;quot;Increased &amp;quot; .. capitalized .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
                    seen[&amp;quot;scale_&amp;quot; .. k] = true&lt;br /&gt;
                end&lt;br /&gt;
            -- Normal value (0 without scale, or non‑zero with/without scale)&lt;br /&gt;
            elseif has_value then&lt;br /&gt;
                local formatted_value = utils.format_value_with_prepost(k, v.Value, frame)&lt;br /&gt;
                local attr_name = lang.get_string(k .. &#039;_label&#039;)&lt;br /&gt;
                local key = formatted_value .. &#039;|&#039; .. attr_name&lt;br /&gt;
                if not seen[key] then&lt;br /&gt;
                    table.insert(value_lines, string.format(&#039;%s %s&#039;, formatted_value, attr_name))&lt;br /&gt;
                    seen[key] = true&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        else&lt;br /&gt;
            -- Plain number/string (e.g. OutgoingDamagePenaltyPercent = -15)&lt;br /&gt;
            local formatted_value = utils.format_value_with_prepost(k, v, frame)&lt;br /&gt;
            local attr_name = lang.get_string(k .. &#039;_label&#039;)&lt;br /&gt;
            local key = formatted_value .. &#039;|&#039; .. attr_name&lt;br /&gt;
            if not seen[key] then&lt;br /&gt;
                table.insert(value_lines, string.format(&#039;%s %s&#039;, formatted_value, attr_name))&lt;br /&gt;
                seen[key] = true&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Combine: value lines first, then scaling lines&lt;br /&gt;
    local all_lines = {}&lt;br /&gt;
    for _, line in ipairs(value_lines) do&lt;br /&gt;
        table.insert(all_lines, line)&lt;br /&gt;
    end&lt;br /&gt;
    for _, line in ipairs(scale_lines) do&lt;br /&gt;
        table.insert(all_lines, line)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return table.concat(all_lines, &#039;&amp;lt;br&amp;gt;&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function find_width_key(ability)&lt;br /&gt;
	for key, value in pairs(ability) do&lt;br /&gt;
		if type(key) == &amp;quot;string&amp;quot; and key:sub(-5) == &amp;quot;Width&amp;quot; then&lt;br /&gt;
			return key&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--for use from an ability page (ability pages are WIP), not hero pages&lt;br /&gt;
function p.write_ability_card_from_ability_key(frame)&lt;br /&gt;
	local ability_key = frame.args[1]&lt;br /&gt;
	if ability_key == nil then return &amp;quot;ability_key &#039;&amp;quot; .. ability_key &amp;quot;&#039; not provided&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	-- Determine the hero and ability number&lt;br /&gt;
	local found_hero_key&lt;br /&gt;
	local found_ability_num&lt;br /&gt;
	for hero_key, card_data in pairs(data) do&lt;br /&gt;
		if found_hero_key == nil or found_ability_num == nil then&lt;br /&gt;
			for ability_num, ability_data in pairs(card_data) do&lt;br /&gt;
				if ability_num ~= &amp;quot;Name&amp;quot; then&lt;br /&gt;
					if ability_data[&#039;Key&#039;] == ability_key then &lt;br /&gt;
						found_hero_key = hero_key&lt;br /&gt;
						found_ability_num = ability_num&lt;br /&gt;
						break &lt;br /&gt;
					end -- hero_key and ability_num found&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if found_ability_num == nil or found_hero_key == nil then return &amp;quot;ability_key &amp;quot; .. ability_key .. &amp;quot; is not used by any heroes&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	-- Get notes for this ability&lt;br /&gt;
	local notes_source_page_name = utils.get_notes_source_page_name(ability_key)&lt;br /&gt;
	local notes_str = frame:preprocess(&amp;quot;{{&amp;quot;..notes_source_page_name..&amp;quot;}}&amp;quot;)&lt;br /&gt;
	&lt;br /&gt;
	-- Create the ability card&lt;br /&gt;
	return p.get_ability_card_from_key(found_hero_key, found_ability_num, true, notes_str, notes_source_page_name)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--from hero key*&lt;br /&gt;
--notes parameter will eventually be removed, as notes_source_page would be sufficient or could even be determined from within this function&lt;br /&gt;
function p.get_ability_card_from_key(hero_key, ability_num, add_link, notes, notes_source_page)&lt;br /&gt;
	if type(hero_key) == &#039;table&#039; and hero_key.args then&lt;br /&gt;
		local frame = hero_key&lt;br /&gt;
		hero_key = frame.args[1]&lt;br /&gt;
		ability_num = frame.args[2]&lt;br /&gt;
		add_link = frame.args[3]&lt;br /&gt;
		notes = frame.args[4]&lt;br /&gt;
		notes_source_page = frame.args[5]&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then &lt;br /&gt;
		return &#039;Ability data not found for hero &#039; ..hero_key.. &#039; and num &#039; .. ability_num&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local ability_name_localized = lang.get_string(ability.Key)&lt;br /&gt;
	local name_link = nil&lt;br /&gt;
	if add_link == &#039;true&#039; then&lt;br /&gt;
		name_link = ability_name_localized&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if notes_source_page ~= nil and notes ~= &amp;quot;&amp;quot; then&lt;br /&gt;
		--Notes comes from a /Notes page, and the notes are not blank&lt;br /&gt;
		-- Confirm the notes source page exists, otherwise, don&#039;t display any notes&lt;br /&gt;
		local title = mw.title.new(notes_source_page)&lt;br /&gt;
		if not (title and title.exists) then&lt;br /&gt;
			notes = &amp;quot;&amp;quot;&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return mw.getCurrentFrame():expandTemplate{&lt;br /&gt;
		title = &amp;quot;Ability card v2/Card&amp;quot;,&lt;br /&gt;
		args = {&lt;br /&gt;
			hero_key = hero_key,&lt;br /&gt;
			ability_num = ability_num,&lt;br /&gt;
			name = ability_name_localized,&lt;br /&gt;
			name_link = name_link,&lt;br /&gt;
			icon = lang.get_string(ability.Key, &#039;en&#039;) .. &#039;.png&#039;,&lt;br /&gt;
			description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)),&lt;br /&gt;
			channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,&lt;br /&gt;
			channel_time_scale = get_attr_scale(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),&lt;br /&gt;
			radius = ability.Radius and ability.Radius.Value,&lt;br /&gt;
			radius_scale = get_attr_scale(ability.Radius),&lt;br /&gt;
			radius_scale_type = get_attr_scale_type(ability.Radius),&lt;br /&gt;
			radius_scale_multiply = get_attr_scale_multiply(ability.Radius),&lt;br /&gt;
			range = ability.AbilityCastRange and ability.AbilityCastRange.Value,&lt;br /&gt;
			range_scale = get_attr_scale(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_type = get_attr_scale_type(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),&lt;br /&gt;
			duration = ability.AbilityDuration and ability.AbilityDuration.Value,&lt;br /&gt;
			duration_scale = get_attr_scale(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_type = get_attr_scale_type(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),&lt;br /&gt;
			-- ability_width = format_value_with_prepost(width_key, ability[width_key]),&lt;br /&gt;
			cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,&lt;br /&gt;
			cooldown_scale = get_attr_scale(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),&lt;br /&gt;
			charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,&lt;br /&gt;
			charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0,&lt;br /&gt;
		}&lt;br /&gt;
	}	&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-SlotReloadHex823</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Template:Ability_card_v2/Card/UpgradeBox?diff=104481</id>
		<title>Template:Ability card v2/Card/UpgradeBox</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Template:Ability_card_v2/Card/UpgradeBox?diff=104481"/>
		<updated>2026-07-31T12:00:16Z</updated>

		<summary type="html">&lt;p&gt;~2026-SlotReloadHex823: Wrap the upgrade description in a container-query box and take the font size from the module instead of a viewport clamp (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;includeonly&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;ability-upgrade-{{{cost}}}&amp;quot; data-index=&amp;quot;{{{index}}}&amp;quot; tabindex=0 class=&amp;quot;cdx-button cdx-button--action-progressive ability-upgrade&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;ability-upgrade-header&amp;quot;&amp;gt;{{Ap|{{{cost}}}|icon_size=15px}}&amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;ac-upgrade-descwrap&amp;quot; style=&amp;quot;container-type: inline-size;&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ac-upgrade-desc&amp;quot; style=&amp;quot;font-size: {{{fontsize|0.9rem}}}; line-height: {{{lineheight|1.4}}};&amp;quot;&amp;gt;{{{description}}}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div&amp;gt;{{#if: {{{scale_type|}}}|{{Ability card v2/Card/Scaling|{{{scale_value}}}|scale_type={{{scale_type}}}}}}}&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&amp;lt;/includeonly&amp;gt;&lt;br /&gt;
&amp;lt;noinclude&amp;gt;&lt;br /&gt;
{{#invoke:Abilities/card|get_ability_card|Wraith|2|false}}&lt;br /&gt;
&lt;br /&gt;
Button functionality is handled by [[MediaWiki:Gadget-ability-upgrades.js]].&lt;br /&gt;
&amp;lt;/noinclude&amp;gt;&lt;/div&gt;</summary>
		<author><name>~2026-SlotReloadHex823</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Template:Ability_card_v2/Card/styles.css?diff=104480</id>
		<title>Template:Ability card v2/Card/styles.css</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Template:Ability_card_v2/Card/styles.css?diff=104480"/>
		<updated>2026-07-31T11:59:49Z</updated>

		<summary type="html">&lt;p&gt;~2026-SlotReloadHex823: Move the upgrade description into a wrapper that owns its padding, so the box width can drive the font size (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* TemplateStyles for Template:Ability card v2/Card&lt;br /&gt;
   All classes here correspond 1-to-1 with inline styles previously in the template.&lt;br /&gt;
   Overriding any of these classes will affect the rendered card layout/appearance. */&lt;br /&gt;
&lt;br /&gt;
/* Root card container */&lt;br /&gt;
.ability-card {&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
    line-height: 1.4;&lt;br /&gt;
    font-family: &#039;Retail Demo&#039;, &#039;Open Sans&#039;, &#039;PT Serif&#039;, serif;&lt;br /&gt;
    color: #FFEFD7;&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    background: #121013;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 500px;&lt;br /&gt;
    border-radius: 4px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Header full-width wrapper */&lt;br /&gt;
.ac-header-wrapper {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Header inner row: left + right columns */&lt;br /&gt;
.ac-header {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    justify-content: space-between;&lt;br /&gt;
    background: #121013;&lt;br /&gt;
    border-radius: 13px 13px 0 0;&lt;br /&gt;
    margin: 0 20px;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Left column of header */&lt;br /&gt;
.ac-header-left {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Title row: icon + name */&lt;br /&gt;
.ac-title {&lt;br /&gt;
    font-size: 2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Icon span (coloured filter to match game palette) */&lt;br /&gt;
.ac-icon {&lt;br /&gt;
    filter: brightness(0) saturate(100%) invert(98%) sepia(19%) saturate(1458%) hue-rotate(301deg) brightness(102%) contrast(109%);&lt;br /&gt;
    padding-right: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Ability name span */&lt;br /&gt;
.ac-name {&lt;br /&gt;
    font-family: &#039;Valve Occult&#039;, &#039;Retail Demo&#039;, &#039;Open Sans&#039;;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Row of header attribute icons (channel time, range, etc.) */&lt;br /&gt;
.ac-header-attrs {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Module can insert invisible p tags that make margins in arbitrary places&lt;br /&gt;
Hide them and ensable only when they are actually necessary */&lt;br /&gt;
.ability-card p {&lt;br /&gt;
	display: none;&lt;br /&gt;
}&lt;br /&gt;
.ability-card .ac-title p {&lt;br /&gt;
	display: block;&lt;br /&gt;
}&lt;br /&gt;
.ability-card .ac-mainbox-value p {&lt;br /&gt;
	display: block;&lt;br /&gt;
}&lt;br /&gt;
.ability-card .ac-info-alt p {&lt;br /&gt;
	display: block;&lt;br /&gt;
}&lt;br /&gt;
.ability-card .ac-headerattr-ss p {&lt;br /&gt;
	display: block;&lt;br /&gt;
}&lt;br /&gt;
.ability-card .ac-mainbox-scale p {&lt;br /&gt;
	display: block;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/* Right column of header (charges + cooldown) */&lt;br /&gt;
.ac-header-right {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    align-items: flex-end;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Charges + charge-cooldown row */&lt;br /&gt;
.ac-charge-row {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin-bottom: 5px;&lt;br /&gt;
    gap: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Info sections outer wrapper */&lt;br /&gt;
.ac-info-outer {&lt;br /&gt;
    margin: 15px 10px 0 10px;&lt;br /&gt;
    padding: 3px 0 5px 0;&lt;br /&gt;
    width: calc(100% - 18px);&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Individual info section (shared base) */&lt;br /&gt;
.ac-info-section {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Description text inside an info section */&lt;br /&gt;
.ac-info-desc {&lt;br /&gt;
    padding: 0 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Main stat boxes row */&lt;br /&gt;
.ac-info-main {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    justify-content: center;&lt;br /&gt;
    width: calc(97% - 5px);&lt;br /&gt;
    min-width: 280px;&lt;br /&gt;
    align-items: flex-end;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Alt (secondary) stat boxes row */&lt;br /&gt;
.ac-info-alt {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    justify-content: center;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    background-color: #2A2A2A;&lt;br /&gt;
    width: calc(97% - 19px);&lt;br /&gt;
    min-width: 267px;&lt;br /&gt;
    padding: 0 4px;&lt;br /&gt;
    column-gap: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Upgrades row */&lt;br /&gt;
.ac-upgrades {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    margin-bottom: 5px;&lt;br /&gt;
    justify-content: center;&lt;br /&gt;
    width: calc(97% - 16px);&lt;br /&gt;
    min-width: 280px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Notes outer container */&lt;br /&gt;
.ac-notes-wrapper {&lt;br /&gt;
    align-self: flex-start;&lt;br /&gt;
    margin: 10px 0 10px 20px;&lt;br /&gt;
    font-size: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Notes heading span */&lt;br /&gt;
.ac-notes-title {&lt;br /&gt;
    font-size: 1.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Notes edit link span */&lt;br /&gt;
.ac-notes-edit {&lt;br /&gt;
    font-size: 0.7rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Collapsible notes body */&lt;br /&gt;
.ac-notes-body {&lt;br /&gt;
    align-self: flex-start;&lt;br /&gt;
    margin: 10px 0 10px 20px;&lt;br /&gt;
    font-size: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* -----------------------------------------------------------------------&lt;br /&gt;
   HeaderAttr sub-template styles&lt;br /&gt;
   ----------------------------------------------------------------------- */&lt;br /&gt;
&lt;br /&gt;
/* Header attribute pill (cast range, cooldown, charges, etc.) */&lt;br /&gt;
.ability-headerattr {&lt;br /&gt;
    background-color: #2C2C2C;&lt;br /&gt;
    padding: 6px;&lt;br /&gt;
    white-space: nowrap;&lt;br /&gt;
    font-size: calc(1em - 2px);&lt;br /&gt;
    border-radius: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-headerattr &amp;gt; span {&lt;br /&gt;
	vertical-align: middle;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Scaling badge: outer relative anchor */&lt;br /&gt;
.ac-headerattr-ss {&lt;br /&gt;
    position: relative;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Scaling badge: absolute positioned badge */&lt;br /&gt;
.ac-headerattr-ss-badge {&lt;br /&gt;
    position: absolute;&lt;br /&gt;
    top: -22px;&lt;br /&gt;
    right: -18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* -----------------------------------------------------------------------&lt;br /&gt;
   MainBox sub-template styles&lt;br /&gt;
   ----------------------------------------------------------------------- */&lt;br /&gt;
&lt;br /&gt;
/* Outer mainbox wrapper */&lt;br /&gt;
.ability-mainbox {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    flex-grow: 1;&lt;br /&gt;
    flex-basis: 0;&lt;br /&gt;
    max-height: 100%;&lt;br /&gt;
    padding: 8px 0 2px 0;&lt;br /&gt;
    margin: 2px;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Optional title above the stat (e.g. &amp;quot;On Impact&amp;quot;, &amp;quot;Explosion&amp;quot;) */&lt;br /&gt;
.ac-mainbox-title {&lt;br /&gt;
    padding-bottom: 2px;&lt;br /&gt;
    font-size: 0.8rem;&lt;br /&gt;
    color: #9C9C9C;&lt;br /&gt;
    font-variant: small-caps;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Relative anchor for the scale badge */&lt;br /&gt;
.ac-mainbox-scale {&lt;br /&gt;
    position: relative;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Absolute positioned scale badge */&lt;br /&gt;
.ac-mainbox-scale-badge {&lt;br /&gt;
    position: absolute;&lt;br /&gt;
    top: -16px;&lt;br /&gt;
    right: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Inner stat display box — base styles shared by all variants */&lt;br /&gt;
.ac-mainbox-inner {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    justify-content: center;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    border-radius: 1px;&lt;br /&gt;
    height: 60px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Default (no scale type) */&lt;br /&gt;
.ac-mainbox-inner--default {&lt;br /&gt;
    background-color: #2A2A2A;&lt;br /&gt;
    border: 3px solid #2A2A2A;&lt;br /&gt;
    box-shadow: 0 0 10px #2A2A2A;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Spirit power scaling */&lt;br /&gt;
.ac-mainbox-inner--spirit {&lt;br /&gt;
    background: radial-gradient(circle, rgba(42,41,43,1) 0%, rgba(59,49,69,1) 100%);&lt;br /&gt;
    border: 3px solid #583D6F;&lt;br /&gt;
    box-shadow: 0 0 10px #3d2c4d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Power increase (boon) scaling */&lt;br /&gt;
.ac-mainbox-inner--boon {&lt;br /&gt;
    background: radial-gradient(circle, rgba(42,41,43,1) 0%, rgba(61,78,70,1) 100%);&lt;br /&gt;
    border: 3px solid #48675a;&lt;br /&gt;
    box-shadow: 0 0 10px #3d2c4d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Weapon damage scaling */&lt;br /&gt;
.ac-mainbox-inner--weapon {&lt;br /&gt;
    background: radial-gradient(circle, rgba(36,37,36,1) 0%, rgba(66,55,47,1) 100%);&lt;br /&gt;
    border: 3px solid #5b412b;&lt;br /&gt;
    box-shadow: 0 0 10px #3d2c4d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Melee damage scaling */&lt;br /&gt;
.ac-mainbox-inner--melee {&lt;br /&gt;
    background: radial-gradient(circle, rgba(36,37,36,1) 0%, rgba(66,55,47,1) 100%);&lt;br /&gt;
    border: 3px solid #5b412b;&lt;br /&gt;
    box-shadow: 0 0 10px #3d2c4d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Value row (bold icon + number) */&lt;br /&gt;
.ac-mainbox-value {&lt;br /&gt;
	font-family: &#039;Valve Occult&#039;;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    margin-top: 4px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ac-mainbox-value p {&lt;br /&gt;
	margin: auto 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Bottom label */&lt;br /&gt;
.ac-mainbox-label {&lt;br /&gt;
    font-size: 0.75em;&lt;br /&gt;
    padding-bottom: 3px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* -----------------------------------------------------------------------&lt;br /&gt;
   UpgradeBox sub-template styles&lt;br /&gt;
   ----------------------------------------------------------------------- */&lt;br /&gt;
&lt;br /&gt;
/* Outer upgrade card */&lt;br /&gt;
.ability-upgrade {&lt;br /&gt;
    white-space: normal;&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    flex-grow: 1;&lt;br /&gt;
    flex-basis: 0;&lt;br /&gt;
    justify-content: flex-start;&lt;br /&gt;
    max-height: 120px;&lt;br /&gt;
    background-color: #131211;&lt;br /&gt;
    padding: 0 0 0 0;&lt;br /&gt;
    margin: 6px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 0 10px #2A2A2A;&lt;br /&gt;
    border-radius: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Upgrade cost header bar */&lt;br /&gt;
.ability-upgrade-header {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    padding: 2px 0 0 2px;&lt;br /&gt;
    font-family: &#039;Retail Demo bold&#039;;&lt;br /&gt;
    background-color: #402f4c;&lt;br /&gt;
    border-radius: 5px 5px 0 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Wrapper around the upgrade description. It owns the padding so that its&lt;br /&gt;
   content box equals the usable text width exactly: Module:TextFit emits the&lt;br /&gt;
   font size in cqw measured against this box, which is why no pixel geometry&lt;br /&gt;
   has to be duplicated in Lua. Do not add padding or borders to&lt;br /&gt;
   .ac-upgrade-desc, or the emitted sizes will stop matching the real space.&lt;br /&gt;
   container-type: inline-size is set inline on the element in&lt;br /&gt;
   Template:Ability card v2/Card/UpgradeBox, because TemplateStyles does not&lt;br /&gt;
   allow that property here. */&lt;br /&gt;
.ac-upgrade-descwrap {&lt;br /&gt;
    padding: 4px;&lt;br /&gt;
    margin: auto 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Upgrade description text */&lt;br /&gt;
.ac-upgrade-desc {&lt;br /&gt;
    overflow-wrap: anywhere;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* -----------------------------------------------------------------------&lt;br /&gt;
   StatusBox sub-template styles&lt;br /&gt;
   ----------------------------------------------------------------------- */&lt;br /&gt;
&lt;br /&gt;
/* Inner stat display box — centered column layout (shared base, all variants) */&lt;br /&gt;
.ac-statusbox-inner {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    flex-grow: 1;&lt;br /&gt;
    justify-content: center;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    border-radius: 1px;&lt;br /&gt;
    height: 60px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Status effect name row (icon + localized name) */&lt;br /&gt;
.ac-statusbox-effect {&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    margin-top: 4px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Row containing value + label */&lt;br /&gt;
.ac-statusbox-value-row {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 5px;&lt;br /&gt;
    font-size: 0.75rem;&lt;br /&gt;
    padding-bottom: 3px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Value div */&lt;br /&gt;
.ac-statusbox-value {&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Fallback text when no value is present */&lt;br /&gt;
.ac-statusbox-no-value {&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    font-size: 0.75rem;&lt;br /&gt;
    color: #B2B2B2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* -----------------------------------------------------------------------&lt;br /&gt;
   AltBox sub-template styles&lt;br /&gt;
   ----------------------------------------------------------------------- */&lt;br /&gt;
&lt;br /&gt;
/* Outer altbox container */&lt;br /&gt;
.ability-altbox {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-direction: row;&lt;br /&gt;
    flex-grow: 1;&lt;br /&gt;
    flex-basis: 0;&lt;br /&gt;
    justify-content: center;&lt;br /&gt;
    background-color: #2A2A2A;&lt;br /&gt;
    font-size: 0.85em;&lt;br /&gt;
    margin: 3px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 0 10px #2A2A2A;&lt;br /&gt;
    border-radius: 1px;&lt;br /&gt;
    white-space: nowrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Inner span wrapping icon + value + label */&lt;br /&gt;
.ac-altbox-inner {&lt;br /&gt;
    white-space: nowrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Bottom label span */&lt;br /&gt;
.ac-altbox-label {&lt;br /&gt;
    font-size: 0.75rem;&lt;br /&gt;
    white-space: nowrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Unit-of-measure suffix (e.g. &amp;quot;m&amp;quot;, &amp;quot;%&amp;quot;) */&lt;br /&gt;
.ac-altbox-uom {&lt;br /&gt;
    font-size: 10px;&lt;br /&gt;
    color: #9C9C9C;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* -----------------------------------------------------------------------&lt;br /&gt;
   Scaling sub-template styles&lt;br /&gt;
   ----------------------------------------------------------------------- */&lt;br /&gt;
&lt;br /&gt;
.ability-scaling {&lt;br /&gt;
    font-size: 0.8em;&lt;br /&gt;
    white-space: nowrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-scaling-attr {&lt;br /&gt;
    font-family: &#039;Valve Pulp&#039;, &#039;Retail Demo&#039;, &#039;PT Serif&#039;, &#039;Palatino&#039;;&lt;br /&gt;
    width: auto;&lt;br /&gt;
    border-radius: 2px;&lt;br /&gt;
    padding: 0 2px;&lt;br /&gt;
    vertical-align: middle;&lt;br /&gt;
}&lt;br /&gt;
   &lt;br /&gt;
/* Spirit scaling */&lt;br /&gt;
.ac-scaling--spirit .ability-scaling-attr {&lt;br /&gt;
    color: #ce91ff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Weapon scaling */&lt;br /&gt;
.ac-scaling--weapon .ability-scaling-attr {&lt;br /&gt;
    color: #ec981a;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Melee scaling */&lt;br /&gt;
.ac-scaling--melee .ability-scaling-attr {&lt;br /&gt;
    color: #ec981a;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Boon scaling */&lt;br /&gt;
.ac-scaling--boon .ability-scaling-attr {&lt;br /&gt;
    color: #54e1c5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* ================================================== */&lt;br /&gt;
/* ===   Gadget Ability Upgrades            === */&lt;br /&gt;
/* ================================================== */&lt;br /&gt;
&lt;br /&gt;
.hide {&lt;br /&gt;
    display: none !important;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-attr {&lt;br /&gt;
    transition: text-shadow 0.6s ease;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-attr.highlight {&lt;br /&gt;
    text-shadow: &lt;br /&gt;
        0 0 6px rgba(255, 250, 0, 0.6),&lt;br /&gt;
        0 0 12px rgba(255, 250, 0, 0.5),&lt;br /&gt;
        0 0 24px rgba(255, 250, 0, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade {&lt;br /&gt;
    transition: all 0.1s ease;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade {&lt;br /&gt;
    transition: all 0.1s ease;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    box-shadow: unset !important;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade.hovered {&lt;br /&gt;
    background-color: #3d2d52a3 !important;&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade.selected {&lt;br /&gt;
    background-color: #503667 !important;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade.selected.hovered {&lt;br /&gt;
    background-color: #3d2d52a3 !important;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade-header {&lt;br /&gt;
    transition: background-color 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ability-upgrade.selected .ability-upgrade-header {&lt;br /&gt;
    background-color: #11140E !important;&lt;br /&gt;
    transition: background-color 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.cdx-button {&lt;br /&gt;
	border:1px solid #2a2a2a&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>~2026-SlotReloadHex823</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Module:TextFit?diff=104476</id>
		<title>Module:TextFit</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:TextFit?diff=104476"/>
		<updated>2026-07-31T11:11:08Z</updated>

		<summary type="html">&lt;p&gt;~2026-SlotReloadHex823: Add shared text-fitting helper: per-glyph advance widths for Retail Demo and Open Sans, with a scale-invariant fitter for sizing text into a box (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;--[==[&lt;br /&gt;
Shared text-fitting helper.&lt;br /&gt;
&lt;br /&gt;
Estimates the largest font size at which a string will wrap into a box of a&lt;br /&gt;
given width and height, using real per-glyph advance widths rather than a&lt;br /&gt;
character count. Used by [[Module:Abilities/card]] to size ability upgrade&lt;br /&gt;
descriptions; [[Module:ItemBox]] can migrate onto it.&lt;br /&gt;
&lt;br /&gt;
Widths are in em. Sources, all extracted with fonttools:&lt;br /&gt;
  Retail Demo regular  [[:File:Retaildemo-regular.woff2]]  unitsPerEm 1000&lt;br /&gt;
  Retail Demo bold     [[:File:Retaildemo-bold.woff2]]     unitsPerEm 1000&lt;br /&gt;
  Open Sans            [[:File:Open_Sans.woff2]]           unitsPerEm 2048&lt;br /&gt;
&lt;br /&gt;
Retail Demo is subset to 65 codepoints (space, comma, period, 0-9, A-Z, a-z).&lt;br /&gt;
MediaWiki:Common.css declares the card font stack as&lt;br /&gt;
&#039;Retail Demo&#039;, &#039;Open Sans&#039;, &#039;PT Serif&#039;, serif, so the browser falls back per&lt;br /&gt;
character; FALLBACK_WIDTHS holds the Open Sans widths actually used for&lt;br /&gt;
punctuation, Cyrillic and accented Latin.&lt;br /&gt;
]==]&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
local ustring = mw.ustring&lt;br /&gt;
&lt;br /&gt;
-- Retail Demo, normal weight&lt;br /&gt;
local RD_REGULAR = {&lt;br /&gt;
	[&amp;quot; &amp;quot;]=0.240, [&amp;quot;,&amp;quot;]=0.221, [&amp;quot;.&amp;quot;]=0.221, [&amp;quot;0&amp;quot;]=0.645, [&amp;quot;1&amp;quot;]=0.268, [&amp;quot;2&amp;quot;]=0.549, [&amp;quot;3&amp;quot;]=0.557, [&amp;quot;4&amp;quot;]=0.601,&lt;br /&gt;
	[&amp;quot;5&amp;quot;]=0.538, [&amp;quot;6&amp;quot;]=0.583, [&amp;quot;7&amp;quot;]=0.487, [&amp;quot;8&amp;quot;]=0.555, [&amp;quot;9&amp;quot;]=0.580, A=0.624, B=0.585, C=0.641,&lt;br /&gt;
	D=0.696, E=0.554, F=0.525, G=0.695, H=0.703, I=0.237, J=0.268, K=0.626,&lt;br /&gt;
	L=0.494, M=0.927, N=0.700, O=0.730, P=0.558, Q=0.734, R=0.614, S=0.527,&lt;br /&gt;
	T=0.550, U=0.679, V=0.624, W=0.939, X=0.665, Y=0.603, Z=0.647, a=0.553,&lt;br /&gt;
	b=0.587, c=0.530, d=0.588, e=0.551, f=0.394, g=0.544, h=0.600, i=0.238,&lt;br /&gt;
	j=0.235, k=0.552, l=0.238, m=0.923, n=0.600, o=0.603, p=0.583, q=0.587,&lt;br /&gt;
	r=0.369, s=0.477, t=0.413, u=0.600, v=0.509, w=0.810, x=0.540, y=0.513,&lt;br /&gt;
	z=0.554,&lt;br /&gt;
	[&amp;quot; &amp;quot;]=0.240, -- U+00A0 no-break space&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- Retail Demo, bold weight&lt;br /&gt;
local RD_BOLD = {&lt;br /&gt;
	[&amp;quot; &amp;quot;]=0.240, [&amp;quot;,&amp;quot;]=0.281, [&amp;quot;.&amp;quot;]=0.281, [&amp;quot;0&amp;quot;]=0.662, [&amp;quot;1&amp;quot;]=0.312, [&amp;quot;2&amp;quot;]=0.560, [&amp;quot;3&amp;quot;]=0.566, [&amp;quot;4&amp;quot;]=0.612,&lt;br /&gt;
	[&amp;quot;5&amp;quot;]=0.562, [&amp;quot;6&amp;quot;]=0.601, [&amp;quot;7&amp;quot;]=0.520, [&amp;quot;8&amp;quot;]=0.583, [&amp;quot;9&amp;quot;]=0.598, A=0.677, B=0.590, C=0.621,&lt;br /&gt;
	D=0.693, E=0.552, F=0.530, G=0.686, H=0.702, I=0.276, J=0.301, K=0.637,&lt;br /&gt;
	L=0.491, M=0.904, N=0.695, O=0.741, P=0.585, Q=0.740, R=0.628, S=0.555,&lt;br /&gt;
	T=0.555, U=0.694, V=0.668, W=0.913, X=0.667, Y=0.635, Z=0.666, a=0.573,&lt;br /&gt;
	b=0.608, c=0.473, d=0.604, e=0.570, f=0.382, g=0.558, h=0.608, i=0.269,&lt;br /&gt;
	j=0.266, k=0.584, l=0.268, m=0.907, n=0.602, o=0.611, p=0.604, q=0.605,&lt;br /&gt;
	r=0.392, s=0.496, t=0.404, u=0.602, v=0.549, w=0.776, x=0.571, y=0.550,&lt;br /&gt;
	z=0.561,&lt;br /&gt;
	[&amp;quot; &amp;quot;]=0.240, -- U+00A0 no-break space&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- Open Sans, for the characters Retail Demo does not contain&lt;br /&gt;
local FALLBACK_WIDTHS = {&lt;br /&gt;
	[&amp;quot;!&amp;quot;]=0.264, [&amp;quot;\&amp;quot;&amp;quot;]=0.398, [&amp;quot;#&amp;quot;]=0.646, [&amp;quot;$&amp;quot;]=0.572, [&amp;quot;%&amp;quot;]=0.827, [&amp;quot;&amp;amp;&amp;quot;]=0.729, [&amp;quot;&#039;&amp;quot;]=0.219, [&amp;quot;(&amp;quot;]=0.295,&lt;br /&gt;
	[&amp;quot;)&amp;quot;]=0.295, [&amp;quot;*&amp;quot;]=0.551, [&amp;quot;+&amp;quot;]=0.572, [&amp;quot;-&amp;quot;]=0.322, [&amp;quot;/&amp;quot;]=0.367, [&amp;quot;:&amp;quot;]=0.263, [&amp;quot;;&amp;quot;]=0.263, [&amp;quot;&amp;lt;&amp;quot;]=0.572,&lt;br /&gt;
	[&amp;quot;=&amp;quot;]=0.572, [&amp;quot;&amp;gt;&amp;quot;]=0.572, [&amp;quot;?&amp;quot;]=0.432, [&amp;quot;@&amp;quot;]=0.896, [&amp;quot;[&amp;quot;]=0.327, [&amp;quot;\\&amp;quot;]=0.367, [&amp;quot;]&amp;quot;]=0.327, [&amp;quot;^&amp;quot;]=0.572,&lt;br /&gt;
	[&amp;quot;_&amp;quot;]=0.438, [&amp;quot;`&amp;quot;]=0.277, [&amp;quot;{&amp;quot;]=0.375, [&amp;quot;|&amp;quot;]=0.549, [&amp;quot;}&amp;quot;]=0.375, [&amp;quot;~&amp;quot;]=0.572, [&amp;quot;¡&amp;quot;]=0.264, [&amp;quot;¢&amp;quot;]=0.572,&lt;br /&gt;
	[&amp;quot;£&amp;quot;]=0.572, [&amp;quot;¤&amp;quot;]=0.572, [&amp;quot;¥&amp;quot;]=0.572, [&amp;quot;¦&amp;quot;]=0.549, [&amp;quot;§&amp;quot;]=0.514, [&amp;quot;¨&amp;quot;]=0.580, [&amp;quot;©&amp;quot;]=0.832, [&amp;quot;ª&amp;quot;]=0.353,&lt;br /&gt;
	[&amp;quot;«&amp;quot;]=0.496, [&amp;quot;¬&amp;quot;]=0.572, [&amp;quot;­&amp;quot;]=0.322, [&amp;quot;®&amp;quot;]=0.832, [&amp;quot;¯&amp;quot;]=0.500, [&amp;quot;°&amp;quot;]=0.428, [&amp;quot;±&amp;quot;]=0.572, [&amp;quot;²&amp;quot;]=0.348,&lt;br /&gt;
	[&amp;quot;³&amp;quot;]=0.348, [&amp;quot;´&amp;quot;]=0.277, [&amp;quot;µ&amp;quot;]=0.618, [&amp;quot;¶&amp;quot;]=0.655, [&amp;quot;·&amp;quot;]=0.263, [&amp;quot;¸&amp;quot;]=0.222, [&amp;quot;¹&amp;quot;]=0.348, [&amp;quot;º&amp;quot;]=0.374,&lt;br /&gt;
	[&amp;quot;»&amp;quot;]=0.496, [&amp;quot;¼&amp;quot;]=0.740, [&amp;quot;½&amp;quot;]=0.768, [&amp;quot;¾&amp;quot;]=0.778, [&amp;quot;¿&amp;quot;]=0.432, [&amp;quot;À&amp;quot;]=0.632, [&amp;quot;Á&amp;quot;]=0.632, [&amp;quot;Â&amp;quot;]=0.632,&lt;br /&gt;
	[&amp;quot;Ã&amp;quot;]=0.632, [&amp;quot;Ä&amp;quot;]=0.632, [&amp;quot;Å&amp;quot;]=0.632, [&amp;quot;Æ&amp;quot;]=0.868, [&amp;quot;Ç&amp;quot;]=0.630, [&amp;quot;È&amp;quot;]=0.556, [&amp;quot;É&amp;quot;]=0.556, [&amp;quot;Ê&amp;quot;]=0.556,&lt;br /&gt;
	[&amp;quot;Ë&amp;quot;]=0.556, [&amp;quot;Ì&amp;quot;]=0.279, [&amp;quot;Í&amp;quot;]=0.279, [&amp;quot;Î&amp;quot;]=0.279, [&amp;quot;Ï&amp;quot;]=0.279, [&amp;quot;Ð&amp;quot;]=0.726, [&amp;quot;Ñ&amp;quot;]=0.753, [&amp;quot;Ò&amp;quot;]=0.778,&lt;br /&gt;
	[&amp;quot;Ó&amp;quot;]=0.778, [&amp;quot;Ô&amp;quot;]=0.778, [&amp;quot;Õ&amp;quot;]=0.778, [&amp;quot;Ö&amp;quot;]=0.778, [&amp;quot;×&amp;quot;]=0.572, [&amp;quot;Ø&amp;quot;]=0.778, [&amp;quot;Ù&amp;quot;]=0.729, [&amp;quot;Ú&amp;quot;]=0.729,&lt;br /&gt;
	[&amp;quot;Û&amp;quot;]=0.729, [&amp;quot;Ü&amp;quot;]=0.729, [&amp;quot;Ý&amp;quot;]=0.559, [&amp;quot;Þ&amp;quot;]=0.602, [&amp;quot;ß&amp;quot;]=0.623, [&amp;quot;à&amp;quot;]=0.556, [&amp;quot;á&amp;quot;]=0.556, [&amp;quot;â&amp;quot;]=0.556,&lt;br /&gt;
	[&amp;quot;ã&amp;quot;]=0.556, [&amp;quot;ä&amp;quot;]=0.556, [&amp;quot;å&amp;quot;]=0.556, [&amp;quot;æ&amp;quot;]=0.862, [&amp;quot;ç&amp;quot;]=0.479, [&amp;quot;è&amp;quot;]=0.562, [&amp;quot;é&amp;quot;]=0.562, [&amp;quot;ê&amp;quot;]=0.562,&lt;br /&gt;
	[&amp;quot;ë&amp;quot;]=0.562, [&amp;quot;ì&amp;quot;]=0.252, [&amp;quot;í&amp;quot;]=0.252, [&amp;quot;î&amp;quot;]=0.252, [&amp;quot;ï&amp;quot;]=0.252, [&amp;quot;ð&amp;quot;]=0.600, [&amp;quot;ñ&amp;quot;]=0.613, [&amp;quot;ò&amp;quot;]=0.602,&lt;br /&gt;
	[&amp;quot;ó&amp;quot;]=0.602, [&amp;quot;ô&amp;quot;]=0.602, [&amp;quot;õ&amp;quot;]=0.602, [&amp;quot;ö&amp;quot;]=0.602, [&amp;quot;÷&amp;quot;]=0.572, [&amp;quot;ø&amp;quot;]=0.602, [&amp;quot;ù&amp;quot;]=0.613, [&amp;quot;ú&amp;quot;]=0.613,&lt;br /&gt;
	[&amp;quot;û&amp;quot;]=0.613, [&amp;quot;ü&amp;quot;]=0.613, [&amp;quot;ý&amp;quot;]=0.501, [&amp;quot;þ&amp;quot;]=0.612, [&amp;quot;ÿ&amp;quot;]=0.501, [&amp;quot;Ā&amp;quot;]=0.632, [&amp;quot;ā&amp;quot;]=0.556, [&amp;quot;Ă&amp;quot;]=0.632,&lt;br /&gt;
	[&amp;quot;ă&amp;quot;]=0.556, [&amp;quot;Ą&amp;quot;]=0.632, [&amp;quot;ą&amp;quot;]=0.556, [&amp;quot;Ć&amp;quot;]=0.630, [&amp;quot;ć&amp;quot;]=0.479, [&amp;quot;Ĉ&amp;quot;]=0.630, [&amp;quot;ĉ&amp;quot;]=0.479, [&amp;quot;Ċ&amp;quot;]=0.630,&lt;br /&gt;
	[&amp;quot;ċ&amp;quot;]=0.479, [&amp;quot;Č&amp;quot;]=0.630, [&amp;quot;č&amp;quot;]=0.479, [&amp;quot;Ď&amp;quot;]=0.726, [&amp;quot;ď&amp;quot;]=0.612, [&amp;quot;Đ&amp;quot;]=0.726, [&amp;quot;đ&amp;quot;]=0.613, [&amp;quot;Ē&amp;quot;]=0.556,&lt;br /&gt;
	[&amp;quot;ē&amp;quot;]=0.562, [&amp;quot;Ĕ&amp;quot;]=0.556, [&amp;quot;ĕ&amp;quot;]=0.562, [&amp;quot;Ė&amp;quot;]=0.556, [&amp;quot;ė&amp;quot;]=0.562, [&amp;quot;Ę&amp;quot;]=0.556, [&amp;quot;ę&amp;quot;]=0.562, [&amp;quot;Ě&amp;quot;]=0.556,&lt;br /&gt;
	[&amp;quot;ě&amp;quot;]=0.562, [&amp;quot;Ĝ&amp;quot;]=0.727, [&amp;quot;ĝ&amp;quot;]=0.543, [&amp;quot;Ğ&amp;quot;]=0.727, [&amp;quot;ğ&amp;quot;]=0.543, [&amp;quot;Ġ&amp;quot;]=0.727, [&amp;quot;ġ&amp;quot;]=0.543, [&amp;quot;Ģ&amp;quot;]=0.727,&lt;br /&gt;
	[&amp;quot;ģ&amp;quot;]=0.543, [&amp;quot;Ĥ&amp;quot;]=0.737, [&amp;quot;ĥ&amp;quot;]=0.613, [&amp;quot;Ħ&amp;quot;]=0.737, [&amp;quot;ħ&amp;quot;]=0.613, [&amp;quot;Ĩ&amp;quot;]=0.279, [&amp;quot;ĩ&amp;quot;]=0.252, [&amp;quot;Ī&amp;quot;]=0.279,&lt;br /&gt;
	[&amp;quot;ī&amp;quot;]=0.252, [&amp;quot;Ĭ&amp;quot;]=0.279, [&amp;quot;ĭ&amp;quot;]=0.252, [&amp;quot;Į&amp;quot;]=0.279, [&amp;quot;į&amp;quot;]=0.252, [&amp;quot;İ&amp;quot;]=0.279, [&amp;quot;ı&amp;quot;]=0.252, [&amp;quot;Ĳ&amp;quot;]=0.548,&lt;br /&gt;
	[&amp;quot;ĳ&amp;quot;]=0.505, [&amp;quot;Ĵ&amp;quot;]=0.269, [&amp;quot;ĵ&amp;quot;]=0.252, [&amp;quot;Ķ&amp;quot;]=0.612, [&amp;quot;ķ&amp;quot;]=0.525, [&amp;quot;ĸ&amp;quot;]=0.525, [&amp;quot;Ĺ&amp;quot;]=0.522, [&amp;quot;ĺ&amp;quot;]=0.252,&lt;br /&gt;
	[&amp;quot;Ļ&amp;quot;]=0.522, [&amp;quot;ļ&amp;quot;]=0.252, [&amp;quot;Ľ&amp;quot;]=0.522, [&amp;quot;ľ&amp;quot;]=0.252, [&amp;quot;Ŀ&amp;quot;]=0.522, [&amp;quot;ŀ&amp;quot;]=0.261, [&amp;quot;Ł&amp;quot;]=0.522, [&amp;quot;ł&amp;quot;]=0.252,&lt;br /&gt;
	[&amp;quot;Ń&amp;quot;]=0.753, [&amp;quot;ń&amp;quot;]=0.613, [&amp;quot;Ņ&amp;quot;]=0.753, [&amp;quot;ņ&amp;quot;]=0.613, [&amp;quot;Ň&amp;quot;]=0.753, [&amp;quot;ň&amp;quot;]=0.613, [&amp;quot;ŉ&amp;quot;]=0.676, [&amp;quot;Ŋ&amp;quot;]=0.753,&lt;br /&gt;
	[&amp;quot;ŋ&amp;quot;]=0.613, [&amp;quot;Ō&amp;quot;]=0.778, [&amp;quot;ō&amp;quot;]=0.602, [&amp;quot;Ŏ&amp;quot;]=0.778, [&amp;quot;ŏ&amp;quot;]=0.602, [&amp;quot;Ő&amp;quot;]=0.778, [&amp;quot;ő&amp;quot;]=0.602, [&amp;quot;Œ&amp;quot;]=0.925,&lt;br /&gt;
	[&amp;quot;œ&amp;quot;]=0.948, [&amp;quot;Ŕ&amp;quot;]=0.617, [&amp;quot;ŕ&amp;quot;]=0.409, [&amp;quot;Ŗ&amp;quot;]=0.617, [&amp;quot;ŗ&amp;quot;]=0.409, [&amp;quot;Ř&amp;quot;]=0.617, [&amp;quot;ř&amp;quot;]=0.409, [&amp;quot;Ś&amp;quot;]=0.548,&lt;br /&gt;
	[&amp;quot;ś&amp;quot;]=0.477, [&amp;quot;Ŝ&amp;quot;]=0.548, [&amp;quot;ŝ&amp;quot;]=0.477, [&amp;quot;Ş&amp;quot;]=0.548, [&amp;quot;ş&amp;quot;]=0.477, [&amp;quot;Š&amp;quot;]=0.548, [&amp;quot;š&amp;quot;]=0.477, [&amp;quot;Ţ&amp;quot;]=0.551,&lt;br /&gt;
	[&amp;quot;ţ&amp;quot;]=0.356, [&amp;quot;Ť&amp;quot;]=0.551, [&amp;quot;ť&amp;quot;]=0.356, [&amp;quot;Ŧ&amp;quot;]=0.551, [&amp;quot;ŧ&amp;quot;]=0.356, [&amp;quot;Ũ&amp;quot;]=0.729, [&amp;quot;ũ&amp;quot;]=0.613, [&amp;quot;Ū&amp;quot;]=0.729,&lt;br /&gt;
	[&amp;quot;ū&amp;quot;]=0.613, [&amp;quot;Ŭ&amp;quot;]=0.729, [&amp;quot;ŭ&amp;quot;]=0.613, [&amp;quot;Ů&amp;quot;]=0.729, [&amp;quot;ů&amp;quot;]=0.613, [&amp;quot;Ű&amp;quot;]=0.729, [&amp;quot;ű&amp;quot;]=0.613, [&amp;quot;Ų&amp;quot;]=0.729,&lt;br /&gt;
	[&amp;quot;ų&amp;quot;]=0.613, [&amp;quot;Ŵ&amp;quot;]=0.923, [&amp;quot;ŵ&amp;quot;]=0.775, [&amp;quot;Ŷ&amp;quot;]=0.559, [&amp;quot;ŷ&amp;quot;]=0.501, [&amp;quot;Ÿ&amp;quot;]=0.559, [&amp;quot;Ź&amp;quot;]=0.572, [&amp;quot;ź&amp;quot;]=0.469,&lt;br /&gt;
	[&amp;quot;Ż&amp;quot;]=0.572, [&amp;quot;ż&amp;quot;]=0.469, [&amp;quot;Ž&amp;quot;]=0.572, [&amp;quot;ž&amp;quot;]=0.469, [&amp;quot;ſ&amp;quot;]=0.323, [&amp;quot;Ѐ&amp;quot;]=0.556, [&amp;quot;Ё&amp;quot;]=0.556, [&amp;quot;Ђ&amp;quot;]=0.733,&lt;br /&gt;
	[&amp;quot;Ѓ&amp;quot;]=0.520, [&amp;quot;Є&amp;quot;]=0.639, [&amp;quot;Ѕ&amp;quot;]=0.548, [&amp;quot;І&amp;quot;]=0.279, [&amp;quot;Ї&amp;quot;]=0.279, [&amp;quot;Ј&amp;quot;]=0.269, [&amp;quot;Љ&amp;quot;]=0.935, [&amp;quot;Њ&amp;quot;]=0.958,&lt;br /&gt;
	[&amp;quot;Ћ&amp;quot;]=0.733, [&amp;quot;Ќ&amp;quot;]=0.610, [&amp;quot;Ѝ&amp;quot;]=0.760, [&amp;quot;Ў&amp;quot;]=0.618, [&amp;quot;Џ&amp;quot;]=0.728, [&amp;quot;А&amp;quot;]=0.632, [&amp;quot;Б&amp;quot;]=0.612, [&amp;quot;В&amp;quot;]=0.646,&lt;br /&gt;
	[&amp;quot;Г&amp;quot;]=0.520, [&amp;quot;Д&amp;quot;]=0.684, [&amp;quot;Е&amp;quot;]=0.556, [&amp;quot;Ж&amp;quot;]=0.841, [&amp;quot;З&amp;quot;]=0.583, [&amp;quot;И&amp;quot;]=0.760, [&amp;quot;Й&amp;quot;]=0.760, [&amp;quot;К&amp;quot;]=0.610,&lt;br /&gt;
	[&amp;quot;Л&amp;quot;]=0.703, [&amp;quot;М&amp;quot;]=0.899, [&amp;quot;Н&amp;quot;]=0.737, [&amp;quot;О&amp;quot;]=0.778, [&amp;quot;П&amp;quot;]=0.728, [&amp;quot;Р&amp;quot;]=0.602, [&amp;quot;С&amp;quot;]=0.630, [&amp;quot;Т&amp;quot;]=0.551,&lt;br /&gt;
	[&amp;quot;У&amp;quot;]=0.618, [&amp;quot;Ф&amp;quot;]=0.797, [&amp;quot;Х&amp;quot;]=0.578, [&amp;quot;Ц&amp;quot;]=0.737, [&amp;quot;Ч&amp;quot;]=0.693, [&amp;quot;Ш&amp;quot;]=1.032, [&amp;quot;Щ&amp;quot;]=1.037, [&amp;quot;Ъ&amp;quot;]=0.686,&lt;br /&gt;
	[&amp;quot;Ы&amp;quot;]=0.848, [&amp;quot;Ь&amp;quot;]=0.634, [&amp;quot;Э&amp;quot;]=0.630, [&amp;quot;Ю&amp;quot;]=1.048, [&amp;quot;Я&amp;quot;]=0.632, [&amp;quot;а&amp;quot;]=0.556, [&amp;quot;б&amp;quot;]=0.595, [&amp;quot;в&amp;quot;]=0.574,&lt;br /&gt;
	[&amp;quot;г&amp;quot;]=0.431, [&amp;quot;д&amp;quot;]=0.575, [&amp;quot;е&amp;quot;]=0.562, [&amp;quot;ж&amp;quot;]=0.735, [&amp;quot;з&amp;quot;]=0.483, [&amp;quot;и&amp;quot;]=0.636, [&amp;quot;й&amp;quot;]=0.636, [&amp;quot;к&amp;quot;]=0.517,&lt;br /&gt;
	[&amp;quot;л&amp;quot;]=0.572, [&amp;quot;м&amp;quot;]=0.733, [&amp;quot;н&amp;quot;]=0.633, [&amp;quot;о&amp;quot;]=0.602, [&amp;quot;п&amp;quot;]=0.620, [&amp;quot;р&amp;quot;]=0.612, [&amp;quot;с&amp;quot;]=0.479, [&amp;quot;т&amp;quot;]=0.471,&lt;br /&gt;
	[&amp;quot;у&amp;quot;]=0.501, [&amp;quot;ф&amp;quot;]=0.715, [&amp;quot;х&amp;quot;]=0.523, [&amp;quot;ц&amp;quot;]=0.626, [&amp;quot;ч&amp;quot;]=0.607, [&amp;quot;ш&amp;quot;]=0.890, [&amp;quot;щ&amp;quot;]=0.897, [&amp;quot;ъ&amp;quot;]=0.687,&lt;br /&gt;
	[&amp;quot;ы&amp;quot;]=0.768, [&amp;quot;ь&amp;quot;]=0.590, [&amp;quot;э&amp;quot;]=0.494, [&amp;quot;ю&amp;quot;]=0.829, [&amp;quot;я&amp;quot;]=0.554, [&amp;quot;ѐ&amp;quot;]=0.562, [&amp;quot;ё&amp;quot;]=0.562, [&amp;quot;ђ&amp;quot;]=0.613,&lt;br /&gt;
	[&amp;quot;ѓ&amp;quot;]=0.431, [&amp;quot;є&amp;quot;]=0.493, [&amp;quot;ѕ&amp;quot;]=0.477, [&amp;quot;і&amp;quot;]=0.252, [&amp;quot;ї&amp;quot;]=0.252, [&amp;quot;ј&amp;quot;]=0.252, [&amp;quot;љ&amp;quot;]=0.839, [&amp;quot;њ&amp;quot;]=0.889,&lt;br /&gt;
	[&amp;quot;ћ&amp;quot;]=0.613, [&amp;quot;ќ&amp;quot;]=0.517, [&amp;quot;ѝ&amp;quot;]=0.636, [&amp;quot;ў&amp;quot;]=0.501, [&amp;quot;џ&amp;quot;]=0.622, [&amp;quot;–&amp;quot;]=0.500, [&amp;quot;—&amp;quot;]=1.000, [&amp;quot;―&amp;quot;]=1.000,&lt;br /&gt;
	[&amp;quot;‘&amp;quot;]=0.169, [&amp;quot;’&amp;quot;]=0.169, [&amp;quot;‚&amp;quot;]=0.245, [&amp;quot;‛&amp;quot;]=0.169, [&amp;quot;“&amp;quot;]=0.349, [&amp;quot;”&amp;quot;]=0.349, [&amp;quot;…&amp;quot;]=0.778, [&amp;quot;€&amp;quot;]=0.572,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- No bold Open Sans is loaded, so browsers synthesise it, which widens&lt;br /&gt;
-- advances slightly. Retail Demo&#039;s own measured bold/regular mean is 1.037.&lt;br /&gt;
local SYNTH_BOLD = 1.03&lt;br /&gt;
&lt;br /&gt;
-- Characters in neither face (CJK, rare symbols).&lt;br /&gt;
local CJK_WIDTH = 2.0&lt;br /&gt;
local FALLBACK  = 0.55&lt;br /&gt;
&lt;br /&gt;
local VOID_TAGS = { br = true, img = true, hr = true, input = true,&lt;br /&gt;
                    meta = true, link = true, wbr = true }&lt;br /&gt;
local BOLD_TAGS = { b = true, strong = true }&lt;br /&gt;
&lt;br /&gt;
local function charWidth(ch, bold)&lt;br /&gt;
	local w = (bold and RD_BOLD or RD_REGULAR)[ch]&lt;br /&gt;
	if w then return w end&lt;br /&gt;
&lt;br /&gt;
	w = FALLBACK_WIDTHS[ch]&lt;br /&gt;
	if w then&lt;br /&gt;
		return bold and w * SYNTH_BOLD or w&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local cp = ustring.codepoint(ch) or 0&lt;br /&gt;
	if (cp &amp;gt;= 0x1100 and cp &amp;lt;= 0x115F) or (cp &amp;gt;= 0x2E80 and cp &amp;lt;= 0xA4CF)&lt;br /&gt;
		or (cp &amp;gt;= 0xAC00 and cp &amp;lt;= 0xD7A3) or (cp &amp;gt;= 0xF900 and cp &amp;lt;= 0xFAFF)&lt;br /&gt;
		or (cp &amp;gt;= 0xFF00 and cp &amp;lt;= 0xFF60) or (cp &amp;gt;= 0xFFE0 and cp &amp;lt;= 0xFFE6) then&lt;br /&gt;
		return CJK_WIDTH&lt;br /&gt;
	end&lt;br /&gt;
	return FALLBACK&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function wordWidth(word)&lt;br /&gt;
	local total = 0&lt;br /&gt;
	for _, part in ipairs(word) do&lt;br /&gt;
		for ch in ustring.gmatch(part.text, &amp;quot;.&amp;quot;) do&lt;br /&gt;
			total = total + charWidth(ch, part.bold)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return total&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Reduce expanded wikitext/HTML to the text a reader actually sees, keeping&lt;br /&gt;
-- track of which runs are bold. Returns a list of lines; each line is a list&lt;br /&gt;
-- of words; each word is a list of {text=, bold=} parts.&lt;br /&gt;
-- Words break only on real whitespace: NBSP and tag boundaries do not split&lt;br /&gt;
-- them, matching how the browser wraps.&lt;br /&gt;
function p.parse(src)&lt;br /&gt;
	src = mw.text.killMarkers(src or &amp;quot;&amp;quot;)&lt;br /&gt;
	src = src:gsub(&amp;quot;%[%[[Ff]ile:[^%[%]]*%]%]&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
	src = src:gsub(&amp;quot;%[%[[^%[%]|]*|([^%[%]]*)%]%]&amp;quot;, &amp;quot;%1&amp;quot;)&lt;br /&gt;
	src = src:gsub(&amp;quot;%[%[([^%[%]]*)%]%]&amp;quot;, &amp;quot;%1&amp;quot;)&lt;br /&gt;
	-- Quote markup survives frame:preprocess; strip it so it is not measured.&lt;br /&gt;
	src = src:gsub(&amp;quot;&#039;&#039;&#039;&#039;&#039;&amp;quot;, &amp;quot;&amp;quot;):gsub(&amp;quot;&#039;&#039;&#039;&amp;quot;, &amp;quot;&amp;quot;):gsub(&amp;quot;&#039;&#039;&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
	local lines, line, word, part = {}, {}, {}, nil&lt;br /&gt;
	local stack = {}&lt;br /&gt;
&lt;br /&gt;
	local function boldNow()&lt;br /&gt;
		for k = #stack, 1, -1 do&lt;br /&gt;
			if stack[k] then return true end&lt;br /&gt;
		end&lt;br /&gt;
		return false&lt;br /&gt;
	end&lt;br /&gt;
	local function endWord()&lt;br /&gt;
		if part then table.insert(word, part); part = nil end&lt;br /&gt;
		if #word &amp;gt; 0 then table.insert(line, word); word = {} end&lt;br /&gt;
	end&lt;br /&gt;
	local function endLine()&lt;br /&gt;
		endWord()&lt;br /&gt;
		table.insert(lines, line); line = {}&lt;br /&gt;
	end&lt;br /&gt;
	local function push(ch, bold)&lt;br /&gt;
		if part == nil or part.bold ~= bold then&lt;br /&gt;
			if part then table.insert(word, part) end&lt;br /&gt;
			part = { text = ch, bold = bold }&lt;br /&gt;
		else&lt;br /&gt;
			part.text = part.text .. ch&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local i, n = 1, #src&lt;br /&gt;
	while i &amp;lt;= n do&lt;br /&gt;
		if src:sub(i, i) == &amp;quot;&amp;lt;&amp;quot; then&lt;br /&gt;
			local j = src:find(&amp;quot;&amp;gt;&amp;quot;, i, true)&lt;br /&gt;
			if not j then break end&lt;br /&gt;
			local tag = src:sub(i + 1, j - 1)&lt;br /&gt;
			local closing, name = tag:match(&amp;quot;^%s*(/?)%s*(%a+)&amp;quot;)&lt;br /&gt;
			if name then&lt;br /&gt;
				name = name:lower()&lt;br /&gt;
				if name == &amp;quot;br&amp;quot; then&lt;br /&gt;
					endLine()&lt;br /&gt;
				elseif VOID_TAGS[name] then -- no effect on weight&lt;br /&gt;
				elseif closing == &amp;quot;/&amp;quot; then&lt;br /&gt;
					table.remove(stack)&lt;br /&gt;
				elseif not tag:match(&amp;quot;/%s*$&amp;quot;) then&lt;br /&gt;
					table.insert(stack, BOLD_TAGS[name] == true&lt;br /&gt;
						or tag:lower():match(&amp;quot;font%-weight%s*:%s*bold&amp;quot;) ~= nil&lt;br /&gt;
						or tag:lower():match(&amp;quot;font%-weight%s*:%s*[6-9]00&amp;quot;) ~= nil)&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			i = j + 1&lt;br /&gt;
		else&lt;br /&gt;
			local k = src:find(&amp;quot;&amp;lt;&amp;quot;, i, true) or (n + 1)&lt;br /&gt;
			local chunk = mw.text.decode(src:sub(i, k - 1), true)&lt;br /&gt;
			local bold = boldNow()&lt;br /&gt;
			for ch in ustring.gmatch(chunk, &amp;quot;.&amp;quot;) do&lt;br /&gt;
				if ch == &amp;quot;\n&amp;quot; then&lt;br /&gt;
					endLine()&lt;br /&gt;
				elseif ch == &amp;quot; &amp;quot; or ch == &amp;quot;\t&amp;quot; or ch == &amp;quot;\r&amp;quot; then&lt;br /&gt;
					endWord()&lt;br /&gt;
				else&lt;br /&gt;
					push(ch, bold)      -- NBSP falls here: joins, never breaks&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			i = k&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	endLine()&lt;br /&gt;
	return lines&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Greedy wrap with the usable width normalised to 1, so `r` is the font size&lt;br /&gt;
-- expressed as a fraction of that width. Returns the number of rendered lines.&lt;br /&gt;
local function lineCount(lines, r)&lt;br /&gt;
	local space = charWidth(&amp;quot; &amp;quot;, false) * r&lt;br /&gt;
	local total = 0&lt;br /&gt;
	for _, words in ipairs(lines) do&lt;br /&gt;
		local cur = 0&lt;br /&gt;
		total = total + 1&lt;br /&gt;
		for _, word in ipairs(words) do&lt;br /&gt;
			local ww = wordWidth(word) * r&lt;br /&gt;
			if cur == 0 then&lt;br /&gt;
				cur = ww&lt;br /&gt;
			elseif cur + space + ww &amp;lt;= 1 then&lt;br /&gt;
				cur = cur + space + ww&lt;br /&gt;
			else&lt;br /&gt;
				total = total + 1&lt;br /&gt;
				cur = ww&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return total&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Largest font size at which `text` fits its box, returned as a PERCENTAGE of&lt;br /&gt;
-- the box&#039;s usable text width. Emit it as a cqw value against a query&lt;br /&gt;
-- container whose content box is exactly that width; no pixel geometry is&lt;br /&gt;
-- needed here or in the caller.&lt;br /&gt;
--&lt;br /&gt;
-- Line counting is scale-invariant in width -- every term scales with the font&lt;br /&gt;
-- size -- so the ratio is all this needs. Only the height constraint carries a&lt;br /&gt;
-- shape, and that is given as an aspect ratio.&lt;br /&gt;
--&lt;br /&gt;
-- opts:&lt;br /&gt;
--   aspect      max height of the text block, as a multiple of its width&lt;br /&gt;
--   lineHeight  line-height multiplier used when rendering&lt;br /&gt;
--   max, min    ceiling and floor, also as a percentage of the width&lt;br /&gt;
--   step        search granularity&lt;br /&gt;
function p.fit(text, opts)&lt;br /&gt;
	local aspect = opts.aspect&lt;br /&gt;
	local lineH  = opts.lineHeight or 1.2&lt;br /&gt;
	local maxPct = opts.max or 100&lt;br /&gt;
	local minPct = opts.min or 1&lt;br /&gt;
	local step   = opts.step or 0.1&lt;br /&gt;
&lt;br /&gt;
	local lines = p.parse(text)&lt;br /&gt;
&lt;br /&gt;
	local widest = 0&lt;br /&gt;
	for _, words in ipairs(lines) do&lt;br /&gt;
		for _, word in ipairs(words) do&lt;br /&gt;
			local ww = wordWidth(word)&lt;br /&gt;
			if ww &amp;gt; widest then widest = ww end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	if widest == 0 then return maxPct end&lt;br /&gt;
&lt;br /&gt;
	-- The longest single word must not overflow horizontally&lt;br /&gt;
	local pct = math.min(100 / widest, maxPct)&lt;br /&gt;
&lt;br /&gt;
	-- Then shrink until the wrapped block fits vertically&lt;br /&gt;
	while pct &amp;gt; minPct and lineCount(lines, pct / 100) * lineH * (pct / 100) &amp;gt; aspect do&lt;br /&gt;
		pct = pct - step&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return math.max(pct, minPct)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-SlotReloadHex823</name></author>
	</entry>
</feed>