Module:LegendaryPokemon: Difference between revisions

From Pokemon Revolution Online Wiki
Jump to navigation Jump to search
Cae (talk | contribs)
created module
 
Cae (talk | contribs)
No edit summary
 
(6 intermediate revisions by the same user not shown)
Line 74: Line 74:
end
end


-- Same 3-column shape as Module:Pokemon's statBarRow: a nowrap label that
-- only takes the width its own text needs, a bar column pinned to width:
-- 100% that soaks up all the leftover space, and a small fixed-width value
-- column after it -- rather than forcing the label into a fixed 40%, which
-- is what was crowding the bar and making the whole row look right-heavy.
local function statBar(frame, label, value, dark)
local function statBar(frame, label, value, dark)
value = tonumber(value) or 0
value = tonumber(value) or 0
local pct = math.max(0, math.min(100, (value / STAT_MAX) * 100))
local pct = math.floor(value / STAT_MAX * 100 + 0.5)
local hue = math.floor((math.max(0, math.min(STAT_MAX, value)) / STAT_MAX) * 130) -- red (low) -> teal (high)
if pct < 2 then pct = 2 end
return '<tr><td style="width: 40%; text-align: left; padding: 2px 4px; font-size: 11px; color: #' .. dark ..
if pct > 100 then pct = 100 end
';">' .. label .. '</td><td style="padding: 2px 4px;">' ..
local hue = math.floor(math.max(0, math.min(180, value * 180 / STAT_MAX)) + 0.5) -- red (low) -> teal (high)
'<div style="background: #FFFFFF; border: 1px solid #' .. dark .. '; border-radius: 8px; overflow: hidden;">' ..
local fill = 'hsl(' .. hue .. ', 85%, 45%)'
'<div style="width: ' .. pct .. '%; background: hsl(' .. hue ..
return '<tr>' ..
', 65%, 45%); color: #FFFFFF; font-size: 10px; font-weight: normal; padding: 1px 4px; ' ..
'<td style="text-align: right; padding: 2px 6px 2px 2px; font-weight: bold; font-size: 10px; ' ..
'white-space: nowrap;">' .. value .. '</div></div></td></tr>'
'color: #' .. dark .. '; white-space: nowrap;">' .. label .. '</td>' ..
'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid ' ..
'#999999; border-radius: 8px; height: 12px; overflow: hidden;">' ..
'<div style="width: ' .. pct .. '%; height: 100%; background-color: ' .. fill .. ';"></div>' ..
'</div></td>' ..
'<td style="text-align: left; padding: 2px 2px 2px 6px; font-weight: bold; font-size: 10px; ' ..
'width: 28px;">' .. value .. '</td></tr>'
end
end


-- Cells are forced to an even 50/50 split (rather than sizing to content) so
-- a longer type name -- "Fighting" next to "Psychic", say -- can't crowd the
-- other cell into overflowing; text is still free to wrap onto a second
-- line at that width rather than being clipped. border: none resets both
-- tables below to kill a default wiki-skin table border that was otherwise
-- showing through as an unstyled brownish line around this part of the card.
local function typeBadges(frame, row)
local function typeBadges(frame, row)
local t1 = nonEmpty(row.type1)
local t1 = nonEmpty(row.type1)
Line 91: Line 108:
local c1 = typeColor(frame, t1)
local c1 = typeColor(frame, t1)
if not t2 then
if not t2 then
return '<table align="center" style="width: 80%; font-weight: bold; font-size: 12px;"><tr>' ..
return '<table align="center" style="width: 80%; border: none; border-collapse: collapse; ' ..
'font-weight: bold; font-size: 12px;"><tr>' ..
'<td style="background-color: #' .. c1.dark .. '; border-radius: 15px; padding: 2px;">[[' .. t1 ..
'<td style="background-color: #' .. c1.dark .. '; border-radius: 15px; padding: 2px;">[[' .. t1 ..
'_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>'
'_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>'
end
end
local c2 = typeColor(frame, t2)
local c2 = typeColor(frame, t2)
return '<table align="center" style="width: 80%; font-weight: bold; font-size: 12px;"><tr>' ..
return '<table align="center" style="width: 80%; table-layout: fixed; border: none; border-collapse: collapse; ' ..
'<td style="background-color: #' .. c1.dark .. '; border-radius: 15px 1px 1px 15px; padding: 2px;">[[' ..
'font-weight: bold; font-size: 12px;"><tr>' ..
'<td style="width: 50%; box-sizing: border-box; background-color: #' .. c1.dark ..
'; border-radius: 15px 1px 1px 15px; padding: 2px; overflow-wrap: break-word;">[[' ..
t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' ..
t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' ..
'<td style="background-color: #' .. c2.dark .. '; border-radius: 1px 15px 15px 1px; padding: 2px;">[[' ..
'<td style="width: 50%; box-sizing: border-box; background-color: #' .. c2.dark ..
'; border-radius: 1px 15px 15px 1px; padding: 2px; overflow-wrap: break-word;">[[' ..
t2 .. '_(type)|<span style="color: #FFFFFF;">' .. t2 .. '</span>]]</td></tr></table>'
t2 .. '_(type)|<span style="color: #FFFFFF;">' .. t2 .. '</span>]]</td></tr></table>'
end
end
Line 106: Line 127:
local row = pokemonByName()[name]
local row = pokemonByName()[name]
if not row then
if not row then
return '<div style="display: inline-block; vertical-align: top; width: 240px; margin: 4px; ' ..
return '<div style="flex: 1 1 220px; width: 100%; box-sizing: border-box; margin: 4px; color: red;">' ..
'color: red;">Data load error: No Pokémon data found for ' .. name .. '</div>', nil
'Data load error: No Pokémon data found for ' .. name .. '</div>', nil
end
end


Line 121: Line 142:
end
end


-- flex: grows/shrinks within a group's flex row (220px basis); width:
-- 100% takes over instead when this card is the sole content of a
-- block-level parent (a single-Pokémon entry) -- flex properties are
-- simply inert outside of a flex container, so both rules can coexist.
local buf = {}
local buf = {}
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; width: 240px; margin: 4px; ' ..
buf[#buf + 1] = '<div style="flex: 1 1 220px; width: 100%; min-width: 0; box-sizing: border-box; margin: 4px; ' ..
'background-color: #' .. c.base .. '; border: 3px solid #' .. c.dark ..
'background-color: #' .. c.base .. '; border: 3px solid #' .. c.dark ..
'; border-radius: 20px; padding: 6px; text-align: center; color: #' .. c.dark .. '; font-weight: bold;">'
'; border-radius: 20px; padding: 6px; text-align: center; color: #' .. c.dark .. '; font-weight: bold;">'
Line 130: Line 155:
'|<span style="color: #' .. c.dark .. ';">' .. name .. '</span>]]</span>' .. genLine .. '</div>'
'|<span style="color: #' .. c.dark .. ';">' .. name .. '</span>]]</span>' .. genLine .. '</div>'


buf[#buf + 1] = '<div style="background-color: #FFFFFF; border-radius: 15px; height: 150px; display: flex; ' ..
-- 150x150px (not just 150px) fits the sprite *within* a 150x150 box on
'align-items: center; justify-content: center; margin-bottom: 6px;">[[File:' .. (row.number or '') ..
-- whichever dimension is the constraint, instead of only fixing width
name .. '.png|150px]]</div>'
-- and leaving height to the image's own aspect ratio -- a tall/narrow
-- sprite was overflowing the fixed-height box under the old |150px form.
buf[#buf + 1] = '<div style="background-color: #FFFFFF; border-radius: 15px; height: 150px; overflow: hidden; ' ..
'display: flex; align-items: center; justify-content: center; margin-bottom: 6px;">[[File:' ..
(row.number or '') .. name .. '.png|150x150px]]</div>'


buf[#buf + 1] = typeBadges(frame, row)
buf[#buf + 1] = typeBadges(frame, row)


buf[#buf + 1] = '<table style="width: 100%; margin-top: 6px;">' ..
buf[#buf + 1] = '<table style="width: 100%; border: none; border-collapse: collapse; margin-top: 6px;">' ..
statBar(frame, 'HP', row.hp, c.dark) ..
statBar(frame, 'HP', row.hp, c.dark) ..
statBar(frame, 'Attack', row.atk, c.dark) ..
statBar(frame, 'Attack', row.atk, c.dark) ..
Line 143: Line 172:
statBar(frame, 'Sp. Def', row.spdef, c.dark) ..
statBar(frame, 'Sp. Def', row.spdef, c.dark) ..
statBar(frame, 'Speed', row.spd, c.dark) ..
statBar(frame, 'Speed', row.spd, c.dark) ..
'<tr><td style="text-align: left; padding: 3px 4px; font-size: 11px;">Total</td>' ..
'</table>'
'<td style="padding: 3px 4px; font-size: 11px;">' .. bst .. '</td></tr></table>'
 
buf[#buf + 1] = '<div style="text-align: center; margin-top: 4px;">' ..
'<div style="display: inline-block; background-color: #FFFFFF; border: 1px solid #' .. c.dark ..
'; padding: 2px 10px; border-radius: 10px; font-size: 11px;">Total: ' .. bst .. '</div></div>'


buf[#buf + 1] = '</div>'
buf[#buf + 1] = '</div>'
Line 150: Line 182:
end
end


-- width: 100% + border-box is the actual fix here: without a constrained
-- width, a long Requirements sentence has no wrap point to size against, so
-- the inline-block ancestor's shrink-to-fit layout stretches the *whole
-- card* to fit that one line instead of wrapping text inside a fixed box.
local function requirementsBox(dark, requirements)
local function requirementsBox(dark, requirements)
return '<div style="background-color: #FFFFFF; color: #000000; border-radius: 15px; font-weight: normal; ' ..
return '<div style="width: 100%; box-sizing: border-box; background-color: #FFFFFF; color: #000000; ' ..
'text-align: left; border: 1px solid #' .. dark .. '; padding: 8px; margin-top: 4px;">' ..
'border-radius: 15px; font-weight: normal; text-align: left; border: 1px solid #' .. dark ..
'; padding: 8px; margin-top: 4px; overflow-wrap: break-word;">' ..
(requirements or '') .. '</div>'
(requirements or '') .. '</div>'
end
end
Line 187: Line 224:
end
end
local c = typeColor(frame, row.type1)
local c = typeColor(frame, row.type1)
return '<div style="display: inline-block; vertical-align: top; background-color: #' .. c.base ..
-- 50% width, matching the original single-Pokémon template's own
'; border: 1px solid #' .. c.dark .. '; border-radius: 22px; padding: 4px; margin: 4px;">' ..
-- width: 50% -- two of these sit side by side and together span
-- the full row. calc() leaves room for this wrapper's own margin
-- (and the small whitespace gap browsers render between adjacent
-- inline-block elements from separate template calls) so two
-- actually fit per row at true 50/50 instead of the second one
-- wrapping early. Still immune to the Requirements-text sizing bug:
-- width is a definite percentage of the page column, not
-- shrink-to-fit, so a long sentence just wraps instead of stretching it.
return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' ..
'width: calc(50% - 10px); background-color: #' .. c.base .. '; border: 1px solid #' .. c.dark ..
'; border-radius: 22px; padding: 4px; margin: 5px;">' ..
cardHtml .. requirementsBox(c.dark, requirements) .. '</div>'
cardHtml .. requirementsBox(c.dark, requirements) .. '</div>'
end
end
Line 203: Line 250:
end
end


-- Full width, block-level -- matches the original group template's own
-- width: 100%, so a group always takes the whole page width and stacks
-- one per row, same as before. width: 100% is a definite size relative
-- to the page column rather than shrink-to-fit, so it's still immune to
-- the long-Requirements-text sizing bug; member cards flex to fill it.
local buf = {}
local buf = {}
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; background-color: #' .. rc.base ..
buf[#buf + 1] = '<div style="box-sizing: border-box; width: 100%; background-color: #' .. rc.base ..
'; border: 3px solid #' .. rc.dark .. '; border-radius: 25px; padding: 6px; margin: 4px; ' ..
'; border: 3px solid #' .. rc.dark .. '; border-radius: 25px; padding: 6px; margin: 8px 0; ' ..
'text-align: center; max-width: 100%;">'
'text-align: center;">'


buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 8px; ' ..
buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 8px; ' ..

Latest revision as of 18:45, 4 August 2026

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

local p = {}

local DATA_FORMAT = 'CSV with header'
local STAT_MAX = 255
local ROMAN = { 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX' }

local function nonEmpty(s)
	if s == nil then return nil end
	s = mw.text.trim(tostring(s))
	if s == '' then return nil end
	return s
end

local pokemonRowsCache = nil
local function pokemonByName()
	if pokemonRowsCache then return pokemonRowsCache end
	local csvData = mw.ext.externalData.getExternalData{
		url = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/PokemonRawList',
		format = DATA_FORMAT,
		data = {
			number = 'Pokedex Number', name = 'Name', type1 = 'Type1', type2 = 'Type2', generation = 'Generation',
			hp = 'HP', atk = 'Attack', def = 'Defense', spatk = 'Special Attack', spdef = 'Special Defense',
			spd = 'Speed',
		},
	}
	pokemonRowsCache = {}
	if csvData then
		for _, row in ipairs(csvData) do
			if nonEmpty(row.name) then
				pokemonRowsCache[row.name] = row
			end
		end
	end
	return pokemonRowsCache
end

local typeColorCache = {}
local function typeColor(frame, pokeType)
	pokeType = nonEmpty(pokeType) or 'Normal'
	if not typeColorCache[pokeType] then
		typeColorCache[pokeType] = {
			light = frame:expandTemplate{ title = pokeType .. ' color light' },
			base = frame:expandTemplate{ title = pokeType .. ' color' },
			dark = frame:expandTemplate{ title = pokeType .. ' color dark' },
		}
	end
	return typeColorCache[pokeType]
end

local regionNameCache = {}
local function regionForGen(frame, genRoman)
	if not regionNameCache[genRoman] then
		regionNameCache[genRoman] = frame:expandTemplate{ title = 'LegendaryRegionFromGen', args = { genRoman } }
	end
	return regionNameCache[genRoman]
end

local regionColorCache = {}
local function regionColor(frame, regionName)
	if not regionColorCache[regionName] then
		regionColorCache[regionName] = {
			light = frame:expandTemplate{ title = regionName .. ' color light' },
			base = frame:expandTemplate{ title = regionName .. ' color' },
			dark = frame:expandTemplate{ title = regionName .. ' color dark' },
		}
	end
	return regionColorCache[regionName]
end

local function genToRoman(genValue)
	local digits = mw.ustring.match(tostring(genValue or ''), '^%d+')
	local n = digits and tonumber(digits)
	return (n and ROMAN[n]) or tostring(genValue or '')
end

-- Same 3-column shape as Module:Pokemon's statBarRow: a nowrap label that
-- only takes the width its own text needs, a bar column pinned to width:
-- 100% that soaks up all the leftover space, and a small fixed-width value
-- column after it -- rather than forcing the label into a fixed 40%, which
-- is what was crowding the bar and making the whole row look right-heavy.
local function statBar(frame, label, value, dark)
	value = tonumber(value) or 0
	local pct = math.floor(value / STAT_MAX * 100 + 0.5)
	if pct < 2 then pct = 2 end
	if pct > 100 then pct = 100 end
	local hue = math.floor(math.max(0, math.min(180, value * 180 / STAT_MAX)) + 0.5) -- red (low) -> teal (high)
	local fill = 'hsl(' .. hue .. ', 85%, 45%)'
	return '<tr>' ..
		'<td style="text-align: right; padding: 2px 6px 2px 2px; font-weight: bold; font-size: 10px; ' ..
		'color: #' .. dark .. '; white-space: nowrap;">' .. label .. '</td>' ..
		'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid ' ..
		'#999999; border-radius: 8px; height: 12px; overflow: hidden;">' ..
		'<div style="width: ' .. pct .. '%; height: 100%; background-color: ' .. fill .. ';"></div>' ..
		'</div></td>' ..
		'<td style="text-align: left; padding: 2px 2px 2px 6px; font-weight: bold; font-size: 10px; ' ..
		'width: 28px;">' .. value .. '</td></tr>'
end

-- Cells are forced to an even 50/50 split (rather than sizing to content) so
-- a longer type name -- "Fighting" next to "Psychic", say -- can't crowd the
-- other cell into overflowing; text is still free to wrap onto a second
-- line at that width rather than being clipped. border: none resets both
-- tables below to kill a default wiki-skin table border that was otherwise
-- showing through as an unstyled brownish line around this part of the card.
local function typeBadges(frame, row)
	local t1 = nonEmpty(row.type1)
	local t2 = nonEmpty(row.type2)
	local c1 = typeColor(frame, t1)
	if not t2 then
		return '<table align="center" style="width: 80%; border: none; border-collapse: collapse; ' ..
			'font-weight: bold; font-size: 12px;"><tr>' ..
			'<td style="background-color: #' .. c1.dark .. '; border-radius: 15px; padding: 2px;">[[' .. t1 ..
			'_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>'
	end
	local c2 = typeColor(frame, t2)
	return '<table align="center" style="width: 80%; table-layout: fixed; border: none; border-collapse: collapse; ' ..
		'font-weight: bold; font-size: 12px;"><tr>' ..
		'<td style="width: 50%; box-sizing: border-box; background-color: #' .. c1.dark ..
		'; border-radius: 15px 1px 1px 15px; padding: 2px; overflow-wrap: break-word;">[[' ..
		t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' ..
		'<td style="width: 50%; box-sizing: border-box; background-color: #' .. c2.dark ..
		'; border-radius: 1px 15px 15px 1px; padding: 2px; overflow-wrap: break-word;">[[' ..
		t2 .. '_(type)|<span style="color: #FFFFFF;">' .. t2 .. '</span>]]</td></tr></table>'
end

local function memberCard(frame, name, showGeneration)
	local row = pokemonByName()[name]
	if not row then
		return '<div style="flex: 1 1 220px; width: 100%; box-sizing: border-box; margin: 4px; color: red;">' ..
			'Data load error: No Pokémon data found for ' .. name .. '</div>', nil
	end

	local c = typeColor(frame, row.type1)
	local bst = (tonumber(row.hp) or 0) + (tonumber(row.atk) or 0) + (tonumber(row.def) or 0) +
		(tonumber(row.spatk) or 0) + (tonumber(row.spdef) or 0) + (tonumber(row.spd) or 0)

	local genLine = ''
	if showGeneration then
		local genRoman = genToRoman(row.generation)
		genLine = '<br><span style="font-size: 11px;">[[:Category:Generation ' .. genRoman ..
			' Pokémon|<span style="color: #' .. c.dark .. ';">Generation ' .. genRoman .. '</span>]]</span>'
	end

	-- flex: grows/shrinks within a group's flex row (220px basis); width:
	-- 100% takes over instead when this card is the sole content of a
	-- block-level parent (a single-Pokémon entry) -- flex properties are
	-- simply inert outside of a flex container, so both rules can coexist.
	local buf = {}
	buf[#buf + 1] = '<div style="flex: 1 1 220px; width: 100%; min-width: 0; box-sizing: border-box; margin: 4px; ' ..
		'background-color: #' .. c.base .. '; border: 3px solid #' .. c.dark ..
		'; border-radius: 20px; padding: 6px; text-align: center; color: #' .. c.dark .. '; font-weight: bold;">'

	buf[#buf + 1] = '<div style="background-color: #' .. c.light .. '; border-radius: 15px; padding: 5px; ' ..
		'margin-bottom: 6px;">#' .. (row.number or '') .. '&nbsp;<span style="font-size: 16px;">[[' .. name ..
		'|<span style="color: #' .. c.dark .. ';">' .. name .. '</span>]]</span>' .. genLine .. '</div>'

	-- 150x150px (not just 150px) fits the sprite *within* a 150x150 box on
	-- whichever dimension is the constraint, instead of only fixing width
	-- and leaving height to the image's own aspect ratio -- a tall/narrow
	-- sprite was overflowing the fixed-height box under the old |150px form.
	buf[#buf + 1] = '<div style="background-color: #FFFFFF; border-radius: 15px; height: 150px; overflow: hidden; ' ..
		'display: flex; align-items: center; justify-content: center; margin-bottom: 6px;">[[File:' ..
		(row.number or '') .. name .. '.png|150x150px]]</div>'

	buf[#buf + 1] = typeBadges(frame, row)

	buf[#buf + 1] = '<table style="width: 100%; border: none; border-collapse: collapse; margin-top: 6px;">' ..
		statBar(frame, 'HP', row.hp, c.dark) ..
		statBar(frame, 'Attack', row.atk, c.dark) ..
		statBar(frame, 'Defense', row.def, c.dark) ..
		statBar(frame, 'Sp. Atk', row.spatk, c.dark) ..
		statBar(frame, 'Sp. Def', row.spdef, c.dark) ..
		statBar(frame, 'Speed', row.spd, c.dark) ..
		'</table>'

	buf[#buf + 1] = '<div style="text-align: center; margin-top: 4px;">' ..
		'<div style="display: inline-block; background-color: #FFFFFF; border: 1px solid #' .. c.dark ..
		'; padding: 2px 10px; border-radius: 10px; font-size: 11px;">Total: ' .. bst .. '</div></div>'

	buf[#buf + 1] = '</div>'
	return table.concat(buf), row
end

-- width: 100% + border-box is the actual fix here: without a constrained
-- width, a long Requirements sentence has no wrap point to size against, so
-- the inline-block ancestor's shrink-to-fit layout stretches the *whole
-- card* to fit that one line instead of wrapping text inside a fixed box.
local function requirementsBox(dark, requirements)
	return '<div style="width: 100%; box-sizing: border-box; background-color: #FFFFFF; color: #000000; ' ..
		'border-radius: 15px; font-weight: normal; text-align: left; border: 1px solid #' .. dark ..
		'; padding: 8px; margin-top: 4px; overflow-wrap: break-word;">' ..
		(requirements or '') .. '</div>'
end

function p.card(frame)
	local parent = frame:getParent()
	local args = (parent and parent.args) or frame.args

	local name = nonEmpty(args.Name)
	if not name then
		return "<span style='color:red'>Usage: {{LegendaryPokemon|Name=...|Requirements=...|" ..
			"Pokemon=Name1\\Name2 (omit Pokemon= for a single-species entry)}}</span>"
	end

	local pokemonArg = nonEmpty(args.Pokemon)
	local members = {}
	if pokemonArg then
		for _, part in ipairs(mw.text.split(pokemonArg, '\\', true)) do
			local trimmed = mw.text.trim(part)
			if trimmed ~= '' then
				members[#members + 1] = trimmed
			end
		end
	else
		members = { name }
	end

	local requirements = args.Requirements or ''

	if #members == 1 then
		local cardHtml, row = memberCard(frame, members[1], true)
		if not row then
			return cardHtml
		end
		local c = typeColor(frame, row.type1)
		-- 50% width, matching the original single-Pokémon template's own
		-- width: 50% -- two of these sit side by side and together span
		-- the full row. calc() leaves room for this wrapper's own margin
		-- (and the small whitespace gap browsers render between adjacent
		-- inline-block elements from separate template calls) so two
		-- actually fit per row at true 50/50 instead of the second one
		-- wrapping early. Still immune to the Requirements-text sizing bug:
		-- width is a definite percentage of the page column, not
		-- shrink-to-fit, so a long sentence just wraps instead of stretching it.
		return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' ..
			'width: calc(50% - 10px); background-color: #' .. c.base .. '; border: 1px solid #' .. c.dark ..
			'; border-radius: 22px; padding: 4px; margin: 5px;">' ..
			cardHtml .. requirementsBox(c.dark, requirements) .. '</div>'
	end

	local firstRow = pokemonByName()[members[1]]
	local genRoman = nonEmpty(args.Gen) or (firstRow and genToRoman(firstRow.generation)) or '?'
	local regionName = regionForGen(frame, genRoman)
	local rc = regionColor(frame, regionName)

	local cardsHtml = {}
	for _, member in ipairs(members) do
		local cardHtml = memberCard(frame, member, false)
		cardsHtml[#cardsHtml + 1] = cardHtml
	end

	-- Full width, block-level -- matches the original group template's own
	-- width: 100%, so a group always takes the whole page width and stacks
	-- one per row, same as before. width: 100% is a definite size relative
	-- to the page column rather than shrink-to-fit, so it's still immune to
	-- the long-Requirements-text sizing bug; member cards flex to fill it.
	local buf = {}
	buf[#buf + 1] = '<div style="box-sizing: border-box; width: 100%; background-color: #' .. rc.base ..
		'; border: 3px solid #' .. rc.dark .. '; border-radius: 25px; padding: 6px; margin: 8px 0; ' ..
		'text-align: center;">'

	buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 8px; ' ..
		'color: #' .. rc.dark .. '; font-weight: bold;"><span style="font-size: 18px;">' .. name ..
		'</span><br><span style="font-size: 12px;">[[:Category:Generation ' .. genRoman ..
		' Pokémon|<span style="color: #' .. rc.dark .. ';">Generation ' .. genRoman .. '</span>]]</span></div>'

	buf[#buf + 1] = '<div style="display: flex; flex-wrap: wrap; justify-content: center; margin-top: 6px;">' ..
		table.concat(cardsHtml) .. '</div>'

	buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 6px; ' ..
		'margin-top: 6px; color: #' .. rc.dark .. '; font-weight: bold; text-align: left;">' ..
		'<div style="text-align: center;">Availability</div>' .. requirementsBox(rc.dark, requirements) .. '</div>'

	buf[#buf + 1] = '</div>'
	return table.concat(buf)
end

return p