Armería: PvP campos de batalla, logros por categoría, bandas/mazmorras y página de hermandad

- PvP: pestaña Campos de batalla (pvpstats) con resultado, stats por partida y fecha/hora.
- Logros: rejilla por categoría con anillos de progreso + detalle por categoría
  (datos de Achievement.db2 + Achievement_Category.db2, iconos vía wowhead).
- Bandas: progreso por dificultad (10/25/heroico) con jefes reales de DungeonEncounter.db2
  y tooltip de jefes; agrupado por expansión (Clásico/TBC/WotLK).
- Mazmorras: mismo sistema, Normal + Heroico, Clásico/TBC/WotLK.
- Página de hermandad estilo armería: cabecera (emblema, puntos = unión de logros de
  miembros sin duplicar, nº miembros, fundada), pestañas Hermandad/Logros, roster con
  iconos de clase/raza, columna Función (rol por spec), filtros clase/función,
  orden por rango/puntos/nivel de objeto/nivel/nombre, paginación.
- Barra lateral: mensaje diario con login (iron-session, gated por pertenencia),
  logros recientes y desgloses de clases y funciones.
- Cabecera de personaje: enlace a la hermandad + nombre del reino.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 01:18:30 +00:00
parent 476e5dba87
commit beba7301a1
203 changed files with 31304 additions and 269 deletions
@@ -9,26 +9,42 @@ import {
getCharacterProfessions,
getItemIcons,
getAchievementPoints,
getCharacterTalents,
getCharacterTalentGroups,
getCharacterSpecs,
getSpellIcons,
getActivityPage,
getAchievementOverview,
getRaidProgress,
getDungeonProgress,
getRealmName,
getQuestCount,
getQuestPage,
getQuestStatusCounts,
getCharacterCollection,
getReputations,
getPvpRank,
getCharacterArena,
getPvpCurrency,
getBattlegroundHistory,
} from '@/lib/armory'
import { raceName, className, classColor, factionOf, classPower } from '@/lib/wow-data'
import { raceName, className, classColor, factionOf, classPower, XP_FOR_LEVEL } from '@/lib/wow-data'
import { PageShell } from '@/components/PageShell'
import { ArmoryPaperdoll, type SheetItem } from '@/components/ArmoryPaperdoll'
import { wowheadIcon } from '@/lib/wowhead'
import { ArmoryTalents } from '@/components/ArmoryTalents'
import { ArmoryActivity } from '@/components/ArmoryActivity'
import { WowheadRefresh } from '@/components/WowheadRefresh'
import { ArmoryTalentTab } from '@/components/ArmoryTalentTab'
import { ArmoryAchievements } from '@/components/ArmoryAchievements'
import { ArmoryRaids } from '@/components/ArmoryRaids'
import { ArmoryQuests } from '@/components/ArmoryQuests'
import { ArmoryTabs, type ArmoryTab } from '@/components/ArmoryTabs'
import { ArmoryReputations } from '@/components/ArmoryReputations'
import { ArmoryCollection } from '@/components/ArmoryCollection'
import { ArmoryPvpRank } from '@/components/ArmoryPvpRank'
import { ArmoryBattlegrounds } from '@/components/ArmoryBattlegrounds'
export const dynamic = 'force-dynamic'
const MAX_LEVEL = 80
export default async function CharacterPage({
params,
}: {
params: Promise<{ locale: string; guid: string }>
}) {
export default async function CharacterPage({ params }: { params: Promise<{ locale: string; guid: string }> }) {
const { locale, guid } = await params
setRequestLocale(locale)
const t = await getTranslations('Armory')
@@ -37,18 +53,36 @@ export default async function CharacterPage({
const character = await getCharacter(id)
if (!character) notFound()
const [equipment, customizations, stats, professions, achievementPoints, talents, specs, activityData] = await Promise.all([
getCharacterEquipment(id, locale),
getCharacterCustomizations(id),
getCharacterStats(id),
getCharacterProfessions(id, locale),
getAchievementPoints(id),
getCharacterTalents(id, character.class, locale),
getCharacterSpecs(id, character.class, locale),
getActivityPage(id, locale, 1, 10, ''),
const [equipment, customizations, stats, professions, achievementPoints, talentGroups, specs, activityData, questCount] =
await Promise.all([
getCharacterEquipment(id, locale),
getCharacterCustomizations(id),
getCharacterStats(id),
getCharacterProfessions(id, locale),
getAchievementPoints(id),
getCharacterTalentGroups(id, character.class, locale),
getCharacterSpecs(id, character.class, locale),
getActivityPage(id, locale, 1, 10, '', ''),
getQuestCount(id),
])
const [questData, mounts, pets, questCounts, reputations, pvpRankData, arena, pvpCurrency, bgHistory, achOverview, raidProgress, dungeonProgress, realmName] = await Promise.all([
getQuestPage(id, locale, 1, 15, '', '', 'all'),
getCharacterCollection(id, locale, 'mounts'),
getCharacterCollection(id, locale, 'pets'),
getQuestStatusCounts(id),
getReputations(id, locale),
getPvpRank(1, 30, ''),
getCharacterArena(id),
getPvpCurrency(id),
getBattlegroundHistory(id),
getAchievementOverview(id, factionOf(character.race), locale),
getRaidProgress(id, locale),
getDungeonProgress(id, locale),
getRealmName(),
])
const qGrand = questCounts.rewarded + questCounts.complete + questCounts.inprogress + questCounts.failed
const icons = await getItemIcons(equipment.map((e) => e.entry))
const spellIcons = talents ? await getSpellIcons(talents.trees.flatMap((tr) => tr.talents.map((c) => c.spell))) : {}
const spellIcons = await getSpellIcons(talentGroups.spellIds)
const items: SheetItem[] = equipment.map((e) => ({
slot: e.slot,
@@ -70,6 +104,10 @@ export default async function CharacterPage({
const factionLabel = faction === 'alliance' ? t('alliance') : faction === 'horde' ? t('horde') : ''
const power = classPower(character.class, locale)
const xpNext = character.level < MAX_LEVEL ? XP_FOR_LEVEL[character.level] ?? 0 : 0
const xpPct =
character.level >= MAX_LEVEL ? 100 : xpNext > 0 ? Math.min(100, Math.round((character.xp / xpNext) * 100)) : 0
const statRows = stats
? [
{ label: t('strength'), value: `${stats.strength}` },
@@ -89,6 +127,431 @@ export default async function CharacterPage({
]
: []
// ---- Pestaña Equipación: modelo + spec + stats/profesiones/PvP ----
const equipTab = (
<>
<ArmoryPaperdoll
race={character.race}
gender={character.gender}
customizations={customizations}
items={items}
health={character.health}
power={character.power}
powerLabel={power.label}
powerColor={power.color}
locale={locale}
labels={{
loading: t('modelLoading'),
unavailable: t('modelUnavailable'),
health: t('health'),
itemLevel: t('itemLevel'),
model: t('model3d'),
list: t('equipment'),
}}
/>
{specs.primary && (
<div className="armory-panel armory-spec-panel">
<h2 className="armory-section-title">{t('specialization')}</h2>
<div className="armory-specs">
<div className="armory-spec-box active">
<span
className="armory-spec-box-icon"
style={{ backgroundImage: `url(${wowheadIcon(specs.primary.icon, 'medium')})` }}
/>
<div className="armory-spec-box-info">
<div className="armory-spec-box-name">{specs.primary.name}</div>
<div className="armory-spec-box-dist">{specs.primary.dist.join(' / ')}</div>
</div>
</div>
<div className={`armory-spec-box ${specs.secondary ? '' : 'na'}`}>
{specs.secondary ? (
<>
<span
className="armory-spec-box-icon"
style={{ backgroundImage: `url(${wowheadIcon(specs.secondary.icon, 'medium')})` }}
/>
<div className="armory-spec-box-info">
<div className="armory-spec-box-name">{specs.secondary.name}</div>
<div className="armory-spec-box-dist">{specs.secondary.dist.join(' / ')}</div>
</div>
</>
) : (
<span className="armory-spec-box-na">N/A</span>
)}
</div>
</div>
</div>
)}
<div className="armory-panels">
<div className="armory-panel">
<h2 className="armory-section-title">{t('stats')}</h2>
{stats ? (
<div className="armory-stats-grid">
{statRows.map((r) => (
<div className="armory-stat" key={r.label}>
<span className="armory-stat-label">{r.label}</span>
<span className="armory-stat-value">{r.value}</span>
</div>
))}
</div>
) : (
<p className="armory-hint armory-stats-pending">{t('statsPending')}</p>
)}
</div>
<div className="armory-panel">
<h2 className="armory-section-title">{t('professions')}</h2>
{professions.length ? (
<div className="armory-prof-groups">
{(['primary', 'secondary'] as const).map((kind) => {
const list = professions.filter((p) => p.kind === kind)
if (!list.length) return null
return (
<div className="armory-prof-group" key={kind}>
<h3 className="armory-prof-grouptitle">
{kind === 'primary' ? t('primaryProfs') : t('secondaryProfs')}
</h3>
{list.map((p) => (
<div className="armory-prof-row" key={p.skill}>
<span
className="armory-prof-icon"
style={{ backgroundImage: `url(${wowheadIcon(p.icon, 'small')})` }}
/>
<div className="armory-prof-body">
<div className="armory-prof-head">
<span className="armory-prof-name">
{p.name}
{p.rank ? <span className="armory-prof-rank"> · {p.rank}</span> : null}
</span>
<span className="armory-prof-value">
{p.value}
<span className="armory-prof-max"> / {p.max}</span>
</span>
</div>
<div className="armory-prof-bar">
<span style={{ width: `${Math.round((p.value / Math.max(1, p.max)) * 100)}%` }} />
</div>
</div>
</div>
))}
</div>
)
})}
</div>
) : (
<p className="armory-hint">{t('noProfessions')}</p>
)}
</div>
</div>
</>
)
// ---- Pestaña PvP (sub-pestañas: Estadísticas + Rank mundo) ----
const pvpStatsSub = (
<div className="armory-panel">
<div className="armory-pvp-tiles">
<div className="armory-pvp-tile">
<span className="armory-pvp-tile-num" style={{ color: '#ff5555' }}>
{character.totalKills.toLocaleString(locale)}
</span>
<span className="armory-pvp-tile-lbl">{t('totalKills')}</span>
</div>
<div className="armory-pvp-tile">
<span className="armory-pvp-tile-num" style={{ color: '#ff8888' }}>
{character.todayKills.toLocaleString(locale)}
</span>
<span className="armory-pvp-tile-lbl">{t('todayKills')}</span>
</div>
<div className="armory-pvp-tile">
<span className="armory-pvp-tile-num" style={{ color: '#e08a3a' }}>
{character.yesterdayKills.toLocaleString(locale)}
</span>
<span className="armory-pvp-tile-lbl">{t('yesterdayKills')}</span>
</div>
<div className="armory-pvp-tile">
<span className="armory-pvp-tile-num" style={{ color: '#ffd100' }}>
{pvpCurrency.honor.toLocaleString(locale)}
</span>
<span className="armory-pvp-tile-lbl">{t('honorPoints')}</span>
</div>
<div className="armory-pvp-tile">
<span className="armory-pvp-tile-num" style={{ color: '#9b59b6' }}>
{pvpCurrency.arena.toLocaleString(locale)}
</span>
<span className="armory-pvp-tile-lbl">{t('arenaPoints')}</span>
</div>
</div>
</div>
)
const arenaSub = (
<div className="armory-panel">
<div className="armory-arena">
{arena.map((b) => {
const losses = Math.max(0, b.seasonGames - b.seasonWins)
const winRate = b.seasonGames ? Math.round((b.seasonWins / b.seasonGames) * 100) : 0
return (
<div className="armory-arena-card" key={b.slot}>
<div className="armory-arena-bracket">{b.bracket}</div>
<span className={`armory-arena-mode ${b.seasonGames > 0 ? 'rated' : 'none'}`}>
{b.seasonGames > 0 ? t('arenaRated') : t('arenaUnrated')}
</span>
<div className="armory-arena-rating">{b.personalRating}</div>
<div className="armory-arena-rating-lbl">{t('arenaRating')}</div>
{b.seasonGames > 0 ? (
<div className="armory-arena-rows">
<div className="armory-arena-row">
<span>{t('arenaSeason')}</span>
<span>
<b className="armory-arena-w">{b.seasonWins}</b> / <b className="armory-arena-l">{losses}</b> ·{' '}
{winRate}%
</span>
</div>
<div className="armory-arena-row">
<span>{t('arenaWeek')}</span>
<span>
{b.weekWins} / {Math.max(0, b.weekGames - b.weekWins)}
</span>
</div>
<div className="armory-arena-row">
<span>MMR</span>
<span>{b.mmr}</span>
</div>
{b.rank > 0 ? (
<div className="armory-arena-row">
<span>{t('arenaRank')}</span>
<span>#{b.rank}</span>
</div>
) : null}
</div>
) : (
<div className="armory-arena-nogames">{t('arenaNoGames')}</div>
)}
</div>
)
})}
</div>
</div>
)
const pvpTab = (
<ArmoryTabs
tabs={[
{ key: 'pvpstats', label: t('pvpStats'), icon: 'fas fa-skull', content: pvpStatsSub },
{ key: 'arena', label: t('arena'), icon: 'fas fa-shield-halved', content: arenaSub },
{
key: 'battlegrounds',
label: t('battlegrounds'),
icon: 'fas fa-flag-checkered',
content: (
<ArmoryBattlegrounds
locale={locale}
rows={bgHistory.rows}
matches={bgHistory.matches}
wins={bgHistory.wins}
losses={bgHistory.losses}
winRate={bgHistory.winRate}
labels={{
history: t('bgHistory'),
desc: t('bgDesc'),
matches: t('bgMatches'),
wins: t('bgWins'),
losses: t('bgLosses'),
winRate: t('bgWinRate'),
result: t('bgResult'),
win: t('bgWin'),
loss: t('bgLoss'),
kb: t('bgKb'),
hk: t('bgHk'),
deaths: t('bgDeaths'),
damage: t('bgDamage'),
healing: t('bgHealing'),
honor: t('bgHonor'),
date: t('bgDate'),
none: t('bgNone'),
battleground: t('battlegrounds'),
}}
/>
),
},
{
key: 'pvprank',
label: t('pvpRank'),
icon: 'fas fa-trophy',
content: (
<ArmoryPvpRank
locale={locale}
initial={pvpRankData.entries}
total={pvpRankData.total}
pageSize={30}
labels={{
title: t('pvpRankTitle'),
desc: t('pvpRankDesc'),
search: t('searchPlayer'),
empty: t('noPlayers'),
character: t('rankPersonaje'),
kills: t('rankKills'),
playedTime: t('playedTime'),
}}
/>
),
},
]}
/>
)
const raidsTab = (
<ArmoryRaids
locale={locale}
raids={raidProgress}
labels={{
bosses: t('raidBosses'),
empty: t('raidNoData'),
classic: t('expClassic'),
tbc: t('expTbc'),
wotlk: t('expWotlk'),
}}
/>
)
const dungeonsTab = (
<ArmoryRaids
locale={locale}
raids={dungeonProgress}
labels={{
bosses: t('raidBosses'),
empty: t('raidNoData'),
classic: t('expClassic'),
tbc: t('expTbc'),
wotlk: t('expWotlk'),
}}
/>
)
// ---- Pestaña Talentos (toggle Primaria/Secundaria + árbol + glifos) ----
const talentsTab = talentGroups.groups.length ? (
<ArmoryTalentTab
groups={talentGroups.groups}
activeIndex={talentGroups.activeIndex}
spellIcons={spellIcons}
locale={locale}
labels={{
distribution: t('distribution'),
glyphs: t('glyphs'),
majorGlyphs: t('majorGlyphs'),
minorGlyphs: t('minorGlyphs'),
primary: t('primary'),
secondary: t('secondary'),
activeSpec: t('activeSpec'),
}}
/>
) : (
<div className="armory-panel">
<p className="armory-hint">{t('noTalents')}</p>
</div>
)
// ---- Pestaña Logros ----
const logrosTab = (
<ArmoryAchievements
locale={locale}
guid={id}
categories={achOverview.categories}
earnedPoints={achOverview.earnedPoints}
totalCompleted={achOverview.totalCompleted}
labels={{
summary: t('achSummaryLine'),
back: t('achBack'),
reward: t('achReward'),
empty: t('achEmptyCat'),
of: t('achOf'),
feats: t('achFeats'),
}}
/>
)
const questsTab = (
<div className="armory-panel armory-quests-panel">
<ArmoryQuests
guid={id}
locale={locale}
initial={questData.entries}
total={questData.total}
grandTotal={qGrand}
pageSize={15}
statuses={[
{ key: 'all', label: t('allStatuses'), count: qGrand },
{ key: 'inprogress', label: t('qsInProgress'), count: questCounts.inprogress },
{ key: 'complete', label: t('qsComplete'), count: questCounts.complete },
{ key: 'failed', label: t('qsFailed'), count: questCounts.failed },
{ key: 'rewarded', label: t('qsRewarded'), count: questCounts.rewarded },
]}
badgeLabels={{
rewarded: t('bRewarded'),
complete: t('bComplete'),
inprogress: t('bInProgress'),
failed: t('bFailed'),
}}
labels={{
filterId: t('filterId'),
filterName: t('filterName'),
empty: t('noQuests'),
quest: t('quest'),
statusCol: t('statusCol'),
title: t('quests'),
pageOf: '',
}}
/>
</div>
)
const mountsTab = (
<ArmoryCollection
items={mounts}
locale={locale}
pageSize={30}
labels={{ title: t('mountsCollected', { n: mounts.length }), search: t('collSearch'), empty: t('noMounts') }}
/>
)
const petsTab = (
<ArmoryCollection
items={pets}
locale={locale}
pageSize={30}
labels={{ title: t('petsCollected', { n: pets.length }), search: t('collSearch'), empty: t('noPets') }}
/>
)
const repsTab = (
<ArmoryReputations
reputations={reputations}
pageSize={30}
labels={{ title: t('reputations'), search: t('repSearch'), empty: t('noReps') }}
/>
)
const tabs: ArmoryTab[] = [
{ key: 'equip', label: t('tabEquip'), icon: 'fas fa-shield-alt', content: equipTab },
{
key: 'talents',
label: t('talents'),
icon: 'fas fa-tree',
count: talentGroups.groups[talentGroups.activeIndex]?.total,
content: talentsTab,
},
{ key: 'achievements', label: t('achievements'), icon: 'fas fa-trophy', count: activityData.total, content: logrosTab },
{ key: 'raids', label: t('raids'), icon: 'fas fa-dragon', content: raidsTab },
{ key: 'dungeons', label: t('dungeons'), icon: 'fas fa-dungeon', content: dungeonsTab },
{ key: 'quests', label: t('quests'), icon: 'fas fa-scroll', count: questCount, content: questsTab },
{ key: 'reputations', label: t('reputations'), icon: 'fas fa-handshake', count: reputations.length, content: repsTab },
{ key: 'pvp', label: t('pvp'), icon: 'fas fa-khanda', content: pvpTab },
{ key: 'mounts', label: t('mounts'), icon: 'fas fa-horse', count: mounts.length, content: mountsTab },
{ key: 'pets', label: t('pets'), icon: 'fas fa-paw', count: pets.length, content: petsTab },
]
return (
<PageShell title={character.name}>
<div className="main-wide">
@@ -121,134 +584,26 @@ export default async function CharacterPage({
&lt;{character.guildName}&gt;
</Link>
)}
{realmName && <span className="armory-realm">{realmName}</span>}
<span className="armory-achpoints" title={t('achievementPoints')}>
<i className="fas fa-star" /> {achievementPoints.toLocaleString(locale)}
</span>
</div>
</div>
<ArmoryPaperdoll
race={character.race}
gender={character.gender}
customizations={customizations}
items={items}
health={character.health}
power={character.power}
powerLabel={power.label}
powerColor={power.color}
locale={locale}
labels={{
loading: t('modelLoading'),
unavailable: t('modelUnavailable'),
health: t('health'),
itemLevel: t('itemLevel'),
model: t('model3d'),
list: t('equipment'),
}}
/>
{specs.primary && (
<div className="armory-panel armory-spec-panel">
<h2 className="armory-section-title">{t('specialization')}</h2>
<div className="armory-specs">
<div className="armory-spec-box active">
<span
className="armory-spec-box-icon"
style={{ backgroundImage: `url(${wowheadIcon(specs.primary.icon, 'medium')})` }}
/>
<div className="armory-spec-box-info">
<div className="armory-spec-box-name">{specs.primary.name}</div>
<div className="armory-spec-box-dist">{specs.primary.dist.join(' / ')}</div>
</div>
</div>
<div className={`armory-spec-box ${specs.secondary ? '' : 'na'}`}>
{specs.secondary ? (
<>
<span
className="armory-spec-box-icon"
style={{ backgroundImage: `url(${wowheadIcon(specs.secondary.icon, 'medium')})` }}
/>
<div className="armory-spec-box-info">
<div className="armory-spec-box-name">{specs.secondary.name}</div>
<div className="armory-spec-box-dist">{specs.secondary.dist.join(' / ')}</div>
</div>
</>
) : (
<span className="armory-spec-box-na">N/A</span>
)}
</div>
</div>
<div className="armory-xpbar">
<div className="armory-xpbar-head">
<span className="armory-xpbar-label">{t('level')}</span>
<span className="armory-xpbar-value">
{character.level >= MAX_LEVEL ? `${character.level} (MAX)` : character.level}
</span>
</div>
)}
{talents && talents.total > 0 && (
<div className="armory-panel armory-talents-panel">
<h2 className="armory-section-title">{t('talents')}</h2>
<WowheadRefresh dep={`talents-${id}`} />
<ArmoryTalents
trees={talents.trees}
spec={talents.spec}
total={talents.total}
maxPoints={Math.max(0, character.level - 9)}
locale={locale}
spellIcons={spellIcons}
/>
</div>
)}
{activityData.entries.length > 0 && (
<div className="armory-panel armory-activity-panel">
<h2 className="armory-section-title">{t('recentActivity')}</h2>
<ArmoryActivity
guid={id}
locale={locale}
initial={activityData.entries}
total={activityData.total}
pageSize={10}
labels={{ earned: t('earned'), search: t('achSearch'), empty: t('noAchievements') }}
/>
</div>
)}
<div className="armory-panels">
<div className="armory-panel">
<h2 className="armory-section-title">{t('stats')}</h2>
{stats ? (
<div className="armory-stats-grid">
{statRows.map((r) => (
<div className="armory-stat" key={r.label}>
<span className="armory-stat-label">{r.label}</span>
<span className="armory-stat-value">{r.value}</span>
</div>
))}
</div>
) : (
<p className="armory-hint armory-stats-pending">{t('statsPending')}</p>
)}
</div>
<div className="armory-panel">
<h2 className="armory-section-title">{t('professions')}</h2>
{professions.length ? (
<div className="armory-prof-list">
{professions.map((p) => (
<div className="armory-prof" key={p.skill}>
<span className="armory-prof-nameblock">
<span className="armory-prof-name">{p.name}</span>
{p.rank ? <span className="armory-prof-rank">{p.rank}</span> : null}
</span>
<span className="armory-prof-value">
{p.value}
<span className="armory-prof-max"> / {p.max}</span>
</span>
</div>
))}
</div>
) : (
<p className="armory-hint">{t('noProfessions')}</p>
)}
<div className="armory-xpbar-track">
<span style={{ width: `${xpPct}%` }} />
</div>
</div>
<ArmoryTabs tabs={tabs} />
</div>
</div>
</PageShell>
@@ -1,54 +1,248 @@
import { notFound } from 'next/navigation'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/navigation'
import { getGuild } from '@/lib/armory'
import { raceName, className, classColor } from '@/lib/wow-data'
import {
getGuildInfo,
getGuildRoster,
getGuildRecentAchievements,
getRealmName,
getGuildAchievementOverview,
getGuildRoleBreakdown,
} from '@/lib/armory'
import { getSession } from '@/lib/session'
import { isGuildMemberByAccount } from '@/lib/armory'
import { className, classColor } from '@/lib/wow-data'
import { wowheadIcon } from '@/lib/wowhead'
import { PageShell } from '@/components/PageShell'
import { ArmoryGuildRoster } from '@/components/ArmoryGuildRoster'
import { ArmoryAchievements } from '@/components/ArmoryAchievements'
export const dynamic = 'force-dynamic'
const PAGE_SIZE = 25
export default async function GuildPage({
params,
searchParams,
}: {
params: Promise<{ locale: string; guid: string }>
searchParams: Promise<{ tab?: string }>
}) {
const { locale, guid } = await params
const { tab } = await searchParams
setRequestLocale(locale)
const t = await getTranslations('Armory')
const guild = await getGuild(Number(guid))
if (!guild) notFound()
const guildId = Number(guid)
const info = await getGuildInfo(guildId)
if (!info) notFound()
const isLogros = tab === 'logros'
const emblemHue = (info.emblem.bg * 7) % 360
return (
<PageShell title={guild.name}>
<div className="main-wide">
<PageShell title={info.name}>
<div className="main-wide armory-guild-page">
<p className="forum-path">
<Link href="/armory"> {t('backToArmory')}</Link>
</p>
<div className="armory-char-sub">
{guild.members.length} {t('members')}
{guild.leaderName && (
<>
{' · '}
{t('leader')}: {guild.leaderName}
</>
)}
{/* Cabecera de hermandad */}
<div className="armory-guild-head">
<div className="armory-guild-emblem" style={{ background: `hsl(${emblemHue} 45% 22%)` }}>
<i className="fas fa-dragon" style={{ color: `hsl(${(emblemHue + 40) % 360} 70% 65%)` }} />
</div>
<div className="armory-guild-headinfo">
<h1 className="armory-guild-title">{info.name}</h1>
<div className="armory-guild-realm">{/* reino */}</div>
<div className="armory-guild-stats">
<span className="armory-guild-stat">
<i className="fas fa-shield" /> {info.guildPoints.toLocaleString(locale)}
</span>
<span className="armory-guild-stat">
<i className="fas fa-users" /> {info.memberCount} {t('members')}
</span>
{info.createdYear > 0 && (
<span className="armory-guild-stat muted">
{t('guildFounded')} {info.createdYear}
</span>
)}
</div>
</div>
</div>
<div className="armory-list" style={{ marginTop: 16 }}>
{guild.members.map((m) => (
<Link key={m.guid} href={`/armory/character/${m.guid}`} className="armory-row">
<span className="armory-name" style={{ color: classColor(m.class) }}>
{m.name}
</span>
<span className="armory-meta">
{t('level')} {m.level} · {raceName(m.race, locale)} · {className(m.class, locale)}
</span>
</Link>
))}
<div className="armory-guild-tabbar">
<Link href={`/armory/guild/${guildId}`} className={`armory-guild-tab${!isLogros ? ' active' : ''}`}>
{t('guild')}
</Link>
<Link href={`/armory/guild/${guildId}?tab=logros`} className={`armory-guild-tab${isLogros ? ' active' : ''}`}>
{t('guildAchievements')}
</Link>
</div>
{isLogros ? (
<GuildLogros guildId={guildId} locale={locale} t={t} />
) : (
<GuildRoster guildId={guildId} locale={locale} info={info} t={t} />
)}
</div>
</PageShell>
)
}
/* ---- Vista Hermandad (roster + barra lateral) ---- */
async function GuildRoster({
guildId,
locale,
info,
t,
}: {
guildId: number
locale: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info: any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
t: any
}) {
const session = await getSession()
const accountId = session.accountId ?? 0
const [roster, recent, realm, roleBreak, isMember] = await Promise.all([
getGuildRoster(guildId, { page: 1, pageSize: PAGE_SIZE, classes: [], roles: [], sort: 'rank' }),
getGuildRecentAchievements(guildId, locale, 5),
getRealmName(),
getGuildRoleBreakdown(guildId),
isGuildMemberByAccount(accountId, guildId),
])
const motdState: 'guest' | 'member' | 'nonmember' = !accountId ? 'guest' : isMember ? 'member' : 'nonmember'
const roleRows: { key: 'tank' | 'healer' | 'dps'; label: string; color: string; count: number }[] = [
{ key: 'tank', label: t('roleTank'), color: '#4a90d9', count: roleBreak.tank },
{ key: 'healer', label: t('roleHealer'), color: '#4ade5f', count: roleBreak.healer },
{ key: 'dps', label: t('roleDps'), color: '#d9534f', count: roleBreak.dps },
]
const maxRole = Math.max(1, roleBreak.tank, roleBreak.healer, roleBreak.dps)
const maxCount = Math.max(1, ...info.classBreakdown.map((c: { count: number }) => c.count))
const fmtDate = (unix: number) => {
if (!unix) return ''
const d = new Date(unix * 1000)
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
}
return (
<div className="armory-guild-layout">
<div className="armory-guild-main">
<h2 className="armory-guild-section">
{t('guildMembers')} <span className="armory-guild-realm-inline">· {realm}</span>
</h2>
<ArmoryGuildRoster
guildid={guildId}
locale={locale}
initial={roster.members}
total={roster.total}
pageSize={PAGE_SIZE}
classes={info.classBreakdown.map((c: { class: number }) => c.class)}
labels={{
view: t('guildView'), sortRank: t('guildSortRank'), sortPoints: t('guildSortPoints'),
sortLevel: t('guildSortLevel'), sortName: t('guildSortName'), filterClass: t('guildFilterClass'),
allClasses: t('guildAllClasses'), filterRole: t('guildFilterRole'), allRoles: t('guildAllRoles'),
roleTank: t('roleTank'), roleHealer: t('roleHealer'), roleDps: t('roleDps'),
sortIlvl: t('guildSortIlvl'), ilvl: t('colIlvl'),
name: t('colName'), race: t('colRace'), class: t('colClass'), role: t('colFunction'),
level: t('colLevel'), rank: t('colRank'), points: t('guildSortPoints'),
}}
/>
</div>
<aside className="armory-guild-side">
<div className="armory-guild-card">
<h3 className="armory-guild-card-title">{t('guildMotd')}</h3>
{motdState === 'member' ? (
<div className="armory-guild-motd">{info.motd?.trim() ? info.motd : t('guildMotdEmpty')}</div>
) : motdState === 'guest' ? (
<div className="armory-guild-motd-locked">
<i className="fas fa-lock" />{' '}
{t('guildMotdGuest')} <Link href="/my-account" className="armory-guild-login">{t('login')}</Link>
</div>
) : (
<div className="armory-guild-motd-locked">
<i className="fas fa-lock" /> {t('guildMotdLocked')}
</div>
)}
</div>
{recent.length > 0 && (
<div className="armory-guild-card">
<h3 className="armory-guild-card-title">{t('guildRecent')}</h3>
<ul className="armory-guild-recent">
{recent.map((a) => (
<li key={a.id} className="armory-guild-recent-item">
<img src={wowheadIcon(a.icon, 'medium')} alt="" className="armory-guild-recent-ico" loading="lazy" />
<span className="armory-guild-recent-name">{a.name}</span>
<span className="armory-guild-recent-date">{fmtDate(a.date)}</span>
</li>
))}
</ul>
</div>
)}
{info.classBreakdown.length > 0 && (
<div className="armory-guild-card">
<h3 className="armory-guild-card-title">{t('guildOverview')}</h3>
<div className="armory-guild-chart-lbl">{t('guildClassBreak')}</div>
<div className="armory-guild-chart">
{info.classBreakdown.map((c: { class: number; count: number }) => (
<div className="armory-guild-bar-col" key={c.class} title={`${className(c.class, locale)}: ${c.count}`}>
<div className="armory-guild-bar-wrap">
<div
className="armory-guild-bar"
style={{ height: `${Math.round((c.count / maxCount) * 100)}%`, background: classColor(c.class) }}
/>
</div>
</div>
))}
</div>
<div className="armory-guild-chart-lbl" style={{ marginTop: 14 }}>{t('guildRoleBreak')}</div>
<div className="armory-guild-roles">
{roleRows.map((r) => (
<div className="armory-guild-role-row" key={r.key}>
<span className="armory-guild-role-lbl" style={{ color: r.color }}>{r.label}</span>
<div className="armory-guild-role-track">
<div className="armory-guild-role-fill" style={{ width: `${Math.round((r.count / maxRole) * 100)}%`, background: r.color }} />
</div>
<span className="armory-guild-role-n">{r.count}</span>
</div>
))}
</div>
</div>
)}
</aside>
</div>
)
}
/* ---- Vista Logros de hermandad (anillos por categoría) ---- */
async function GuildLogros({
guildId,
locale,
t,
}: {
guildId: number
locale: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
t: any
}) {
const overview = await getGuildAchievementOverview(guildId, null, locale)
return (
<ArmoryAchievements
locale={locale}
guid={guildId}
categories={overview.categories}
earnedPoints={overview.earnedPoints}
totalCompleted={overview.totalCompleted}
apiPath="/api/armory/guild-achievements"
idKey="guild"
labels={{
summary: t('achSummaryLine'), back: t('achBack'), reward: t('achReward'),
empty: t('achEmptyCat'), of: t('achOf'), feats: t('achFeats'),
}}
/>
)
}
@@ -0,0 +1,19 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getAchievementCategory } from '@/lib/armory'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const PAGE_SIZE = 20
/** Logros conseguidos de un personaje dentro de una categoría (paginado). */
export async function GET(req: NextRequest) {
const sp = req.nextUrl.searchParams
const guid = Number(sp.get('guid')) || 0
const cat = Number(sp.get('cat')) || 0
const page = Math.max(1, Number(sp.get('page')) || 1)
const locale = sp.get('locale') === 'en' ? 'en' : 'es'
if (!guid || !cat) return NextResponse.json({ entries: [], total: 0, pageSize: PAGE_SIZE })
const { entries, total } = await getAchievementCategory(guid, cat, locale, page, PAGE_SIZE)
return NextResponse.json({ entries, total, pageSize: PAGE_SIZE })
}
+4 -3
View File
@@ -6,14 +6,15 @@ export const dynamic = 'force-dynamic'
const PAGE_SIZE = 10
/** Actividad reciente paginada + búsqueda (por nombre o ID) para la armería. */
/** Logros paginados + filtros (por ID y por nombre) para la armería. */
export async function GET(req: NextRequest) {
const sp = req.nextUrl.searchParams
const guid = Number(sp.get('guid'))
const locale = sp.get('locale') === 'en' ? 'en' : 'es'
const page = Math.max(1, Number(sp.get('page')) || 1)
const q = sp.get('q') || ''
const filterId = sp.get('id') || ''
const filterName = sp.get('name') || ''
if (!guid) return NextResponse.json({ entries: [], total: 0, pageSize: PAGE_SIZE })
const { entries, total } = await getActivityPage(guid, locale, page, PAGE_SIZE, q)
const { entries, total } = await getActivityPage(guid, locale, page, PAGE_SIZE, filterId, filterName)
return NextResponse.json({ entries, total, pageSize: PAGE_SIZE })
}
@@ -0,0 +1,19 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getGuildAchievementCategory } from '@/lib/armory'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const PAGE_SIZE = 20
/** Logros de una categoría conseguidos por la hermandad (paginado). */
export async function GET(req: NextRequest) {
const sp = req.nextUrl.searchParams
const guild = Number(sp.get('guild')) || 0
const cat = Number(sp.get('cat')) || 0
const page = Math.max(1, Number(sp.get('page')) || 1)
const locale = sp.get('locale') === 'en' ? 'en' : 'es'
if (!guild || !cat) return NextResponse.json({ entries: [], total: 0, pageSize: PAGE_SIZE })
const { entries, total } = await getGuildAchievementCategory(guild, cat, locale, page, PAGE_SIZE)
return NextResponse.json({ entries, total, pageSize: PAGE_SIZE })
}
@@ -0,0 +1,25 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getGuildRoster } from '@/lib/armory'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
/** Roster de hermandad paginado con filtro por clase y orden. */
export async function GET(req: NextRequest) {
const sp = req.nextUrl.searchParams
const guildid = Number(sp.get('guild')) || 0
const page = Math.max(1, Number(sp.get('page')) || 1)
const pageSize = Math.min(50, Math.max(10, Number(sp.get('pageSize')) || 25))
const sort = sp.get('sort') || 'rank'
const classes = (sp.get('classes') || '')
.split(',')
.map((n) => Number(n))
.filter((n) => Number.isInteger(n) && n > 0)
const roles = (sp.get('roles') || '')
.split(',')
.map((s) => s.trim())
.filter((s) => s === 'tank' || s === 'healer' || s === 'dps')
if (!guildid) return NextResponse.json({ members: [], total: 0, pageSize })
const { members, total } = await getGuildRoster(guildid, { page, pageSize, classes, roles, sort })
return NextResponse.json({ members, total, pageSize })
}
+16
View File
@@ -0,0 +1,16 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getPvpRank } from '@/lib/armory'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const PAGE_SIZE = 30
/** Ranking mundial de bajas (Total Kills) con búsqueda + paginación. */
export async function GET(req: NextRequest) {
const sp = req.nextUrl.searchParams
const page = Math.max(1, Number(sp.get('page')) || 1)
const q = sp.get('q') || ''
const { entries, total } = await getPvpRank(page, PAGE_SIZE, q)
return NextResponse.json({ entries, total, pageSize: PAGE_SIZE })
}
+21
View File
@@ -0,0 +1,21 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getQuestPage } from '@/lib/armory'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const PAGE_SIZE = 15
/** Misiones paginadas + filtros (ID, nombre, estado). */
export async function GET(req: NextRequest) {
const sp = req.nextUrl.searchParams
const guid = Number(sp.get('guid'))
const locale = sp.get('locale') === 'en' ? 'en' : 'es'
const page = Math.max(1, Number(sp.get('page')) || 1)
const filterId = sp.get('id') || ''
const filterName = sp.get('name') || ''
const status = sp.get('status') || 'all'
if (!guid) return NextResponse.json({ entries: [], total: 0, pageSize: PAGE_SIZE })
const { entries, total } = await getQuestPage(guid, locale, page, PAGE_SIZE, filterId, filterName, status)
return NextResponse.json({ entries, total, pageSize: PAGE_SIZE })
}
+522
View File
@@ -327,3 +327,525 @@ a.armory-activity-link { background-image: none !important; }
.armory-pager button.active { background: #d79602; color: #14110d; border-color: #d79602; }
.armory-pager button:disabled { opacity: .4; cursor: default; }
.armory-pager-dots { color: #6d6a5e; padding: 6px 2px; align-self: center; }
/* ===== Profesiones con barras (Primarias / Secundarias) ===== */
.armory-prof-groups { display: flex; flex-direction: column; gap: 16px; }
.armory-prof-grouptitle { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: #6d6a5e; margin: 0 0 10px; font-weight: bold; }
.armory-prof-row { display: flex; align-items: center; gap: 11px; margin-bottom: 11px; }
.armory-prof-row:last-child { margin-bottom: 0; }
.armory-prof-icon { width: 36px; height: 36px; flex: 0 0 36px; border-radius: 4px; background: #0d0b08 center/cover no-repeat; box-shadow: inset 0 0 0 1px rgba(255,255,255,.09); }
.armory-prof-body { flex: 1; min-width: 0; }
.armory-prof-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 5px; }
.armory-prof-name { font-size: 13px; font-weight: bold; color: #d4cdbb; }
.armory-prof-rank { font-size: 11px; color: #6d6a5e; font-weight: normal; }
.armory-prof-value { font-size: 13px; font-weight: bold; color: #d79602; white-space: nowrap; }
.armory-prof-max { color: #6d6a5e; font-weight: normal; }
.armory-prof-bar { height: 7px; background: rgba(255,255,255,.06); border-radius: 4px; overflow: hidden; }
.armory-prof-bar > span { display: block; height: 100%; background: linear-gradient(90deg, #a8791a, #e0b030); border-radius: 4px; }
/* ===== Barra de XP/Nivel ===== */
.armory-xpbar { margin: 8px 0 18px; }
.armory-xpbar-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.armory-xpbar-label { font-size: 13px; color: #8a8578; }
.armory-xpbar-value { font-size: 18px; font-weight: bold; color: #d79602; }
.armory-xpbar-track { height: 10px; background: rgba(255,255,255,.05); border-radius: 5px; overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255,255,255,.04); }
.armory-xpbar-track > span { display: block; height: 100%; border-radius: 5px; background: linear-gradient(90deg, #7a5cff, #b452d6 45%, #e0813a 80%, #f0b429); }
/* ===== Pestañas de la ficha ===== */
.armory-ctabbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.armory-ctab {
display: inline-flex; align-items: center; gap: 8px; padding: 11px 16px; border-radius: 6px; cursor: pointer;
background: rgba(0,0,0,.25); border: 1px solid #2a2723; color: #d4cdbb; font-weight: bold; font-size: 14px; transition: all .12s;
}
.armory-ctab i { color: #8a8578; }
.armory-ctab:hover { border-color: rgba(215,150,2,.4); }
.armory-ctab.active { background: #d79602; color: #14110d; border-color: #d79602; }
.armory-ctab.active i { color: #14110d; }
.armory-ctab-count { font-size: 12px; padding: 1px 8px; border-radius: 10px; background: rgba(0,0,0,.3); color: #d79602; }
.armory-ctab.active .armory-ctab-count { background: rgba(0,0,0,.25); color: #14110d; }
.armory-ctab-panel { display: flex; flex-direction: column; gap: 16px; }
/* ===== PvP ===== */
.armory-pvp { display: flex; flex-direction: column; gap: 8px; }
.armory-pvp-stat { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,.03); }
.armory-pvp-label { font-size: 12px; color: #8a8578; }
.armory-pvp-value { font-size: 13px; font-weight: bold; color: #d4cdbb; }
/* ===== Glifos ===== */
.armory-glyphs { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 18px; }
.armory-glyph { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
.armory-glyph:last-child { margin-bottom: 0; }
.armory-glyph-icon { width: 30px; height: 30px; flex: 0 0 30px; border-radius: 4px; background: #0d0b08 center/cover no-repeat; box-shadow: inset 0 0 0 1px rgba(163,53,238,.5); }
.armory-glyph-name { font-size: 13px; color: #d4cdbb; }
/* ===== Pestaña Misiones ===== */
.armory-quest-filters { display: grid; grid-template-columns: 1fr 1fr auto; gap: 10px; margin-bottom: 10px; }
.armory-quest-status { padding: 9px 12px; border-radius: 4px; background: #14110d; border: 1px solid #2a2723; color: #d4cdbb; font-size: 13px; cursor: pointer; }
.armory-quest-status:focus { outline: none; border-color: #d79602; }
.armory-quest-count { font-size: 12px; color: #8a8578; margin: 0 0 12px; }
.armory-quest-table { width: 100%; border-collapse: collapse; }
.armory-quest-table.loading { opacity: .5; }
.armory-quest-table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: #6d6a5e; padding: 8px 6px; border-bottom: 1px solid #2a2723; }
.armory-quest-th-status, .armory-quest-td-status { text-align: right; white-space: nowrap; }
.armory-quest-table td { padding: 9px 6px; border-bottom: 1px solid rgba(255,255,255,.03); font-size: 13px; }
.armory-quest-link { color: #ffd100; text-decoration: none; font-weight: bold; }
.armory-quest-link:hover { text-decoration: underline; }
a.armory-quest-link { background-image: none !important; }
.armory-quest-badge { font-size: 11px; font-weight: bold; padding: 2px 9px; border-radius: 10px; }
.armory-quest-badge.completed { color: #1eff00; background: rgba(30,255,0,.1); }
.armory-quest-badge.inprogress { color: #d79602; background: rgba(215,150,2,.1); }
@media (max-width: 600px) { .armory-quest-filters { grid-template-columns: 1fr; } }
/* ===== Colecciones (Monturas / Mascotas) ===== */
.armory-collection-head { font-size: 14px; color: #d4cdbb; padding: 11px 14px; margin-bottom: 14px; background: rgba(0,0,0,.2); border-radius: 4px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.03); }
.armory-collection { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px; }
.armory-coll-card { display: flex; align-items: center; gap: 11px; padding: 9px 12px; border-radius: 4px; text-decoration: none; background: rgba(0,0,0,.2); box-shadow: inset 0 0 0 1px rgba(255,255,255,.04); transition: background .12s; }
.armory-coll-card:hover { background: rgba(255,255,255,.05); }
.armory-coll-icon { width: 36px; height: 36px; flex: 0 0 36px; border-radius: 4px; background: #0d0b08 center/cover no-repeat; box-shadow: inset 0 0 0 1px rgba(255,255,255,.08); }
.armory-coll-name { font-size: 13px; font-weight: bold; color: #d4cdbb; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.armory-coll-card { background-image: none !important; }
/* ===== Toggle Primaria/Secundaria + distribución + 4 columnas (árboles + glifos) ===== */
.armory-spec-toggle { display: flex; gap: 8px; justify-content: center; margin-bottom: 16px; }
.armory-spec-toggle button { padding: 9px 20px; border-radius: 6px; background: rgba(0,0,0,.25); border: 1px solid #2a2723; color: #d4cdbb; font-weight: bold; font-size: 14px; cursor: pointer; transition: all .12s; }
.armory-spec-toggle button:hover { border-color: rgba(215,150,2,.4); }
.armory-spec-toggle button.active { background: #d79602; color: #14110d; border-color: #d79602; }
.armory-spec-toggle-dot { font-weight: normal; font-size: 12px; opacity: .85; }
.armory-tdist { text-align: center; padding: 16px; margin-bottom: 16px; background: rgba(0,0,0,.2); border-radius: 6px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.03); }
.armory-tdist-label { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #8a8578; margin-bottom: 6px; }
.armory-tdist-value { font-size: 26px; font-weight: bold; color: #d79602; letter-spacing: .05em; }
.armory-talent-cols { display: grid; grid-template-columns: repeat(3, auto) minmax(200px, 1fr); gap: 14px; justify-content: center; align-items: start; }
.armory-glyph-col .armory-glyph-group { margin-bottom: 14px; }
@media (max-width: 980px) { .armory-talent-cols { grid-template-columns: 1fr; } }
/* ===== Misiones: título, desplegable de estado, filas con ID ===== */
.armory-quest-title { font-size: 20px; font-weight: bold; color: #d4cdbb; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }
.armory-quest-title i { color: #d79602; }
.armory-quest-title-count { font-size: 15px; font-weight: normal; color: #8a8578; }
.armory-quest-id { font-size: 11px; color: #6d6a5e; font-weight: bold; }
a.armory-quest-link { font-weight: bold; }
/* Desplegable custom de estado */
.armory-dropdown { position: relative; }
.armory-dropdown-btn { display: flex; align-items: center; gap: 8px; width: 100%; padding: 9px 12px; border-radius: 4px; background: #14110d; border: 1px solid #2a2723; color: #d4cdbb; font-size: 13px; cursor: pointer; }
.armory-dropdown-btn:hover { border-color: #d79602; }
.armory-dropdown-caret { margin-left: auto; color: #6d6a5e; }
.armory-dropdown-list { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 60; background: #14110d; border: 1px solid #2a2723; border-radius: 4px; box-shadow: 0 8px 24px rgba(0,0,0,.6); overflow: hidden; }
.armory-dropdown-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 12px; background: none; border: 0; color: #d4cdbb; font-size: 13px; cursor: pointer; text-align: left; }
.armory-dropdown-item:hover { background: rgba(255,255,255,.06); }
.armory-dropdown-item.active { background: rgba(215,150,2,.12); }
.armory-dropdown-noicon { width: 14px; display: inline-block; }
/* Logros: enlace amarillo + fecha */
.armory-ach-link { color: #ffd100 !important; }
.armory-ach-date { color: #d4cdbb; font-variant-numeric: tabular-nums; }
.armory-ach-filters { grid-template-columns: 1fr 1fr; }
/* ===== Reputaciones ===== */
.armory-reps { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px 24px; margin-top: 14px; }
.armory-rep-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 5px; }
.armory-rep-name { font-size: 13px; font-weight: bold; color: #d4cdbb; }
.armory-rep-rank { font-size: 12px; font-weight: bold; }
.armory-rep-nums { font-size: 11px; color: #6d6a5e; }
.armory-rep-bar { height: 8px; background: rgba(255,255,255,.06); border-radius: 4px; overflow: hidden; }
.armory-rep-bar > span { display: block; height: 100%; border-radius: 4px; }
/* ===== PvP: tiles de estadísticas ===== */
.armory-pvp-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; }
.armory-pvp-tile { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 22px 14px; border-radius: 8px; background: rgba(0,0,0,.22); box-shadow: inset 0 0 0 1px rgba(255,255,255,.04); }
.armory-pvp-tile-num { font-size: 30px; font-weight: bold; line-height: 1; }
.armory-pvp-tile-lbl { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: #8a8578; }
/* ===== PvP: ranking mundial ===== */
.armory-pvprank-header { text-align: center; margin-bottom: 16px; }
.armory-pvprank-header .armory-quest-title { justify-content: center; font-size: 24px; }
.armory-pvprank-desc { color: #8a8578; font-size: 14px; margin: 4px 0 0; }
.armory-rank-table th { color: #d79602; }
.armory-rank-th-pos { width: 60px; }
.armory-rank-th-kills, .armory-rank-th-time { text-align: right; }
.armory-rank-pos { font-weight: bold; color: #d4cdbb; white-space: nowrap; }
.armory-rank-medal { font-size: 15px; }
.armory-rank-name { color: #ffd100; text-decoration: none; font-weight: bold; }
.armory-rank-name:hover { text-decoration: underline; }
.armory-rank-kills { text-align: right; color: #ff5555; font-weight: bold; font-variant-numeric: tabular-nums; }
.armory-rank-time { text-align: right; color: #8a8578; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Fix alineación de la tabla de ranking (cabeceras derecha + anchos de columna) */
.armory-rank-table th.armory-rank-th-kills, .armory-rank-table td.armory-rank-kills,
.armory-rank-table th.armory-rank-th-time, .armory-rank-table td.armory-rank-time { text-align: right; }
.armory-rank-th-pos, .armory-rank-pos { width: 70px; }
.armory-rank-th-kills, .armory-rank-kills { width: 120px; }
.armory-rank-th-time, .armory-rank-time { width: 150px; }
/* ===== Arena (brackets 2v2/3v3/5v5) ===== */
.armory-arena { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.armory-arena-card { text-align: center; padding: 20px 16px; border-radius: 8px; background: rgba(0,0,0,.22); box-shadow: inset 0 0 0 1px rgba(255,255,255,.04); }
.armory-arena-bracket { font-size: 15px; font-weight: bold; color: #d79602; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 10px; }
.armory-arena-rating { font-size: 34px; font-weight: bold; color: #d4cdbb; line-height: 1; }
.armory-arena-rating-lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: #6d6a5e; margin: 4px 0 14px; }
.armory-arena-rows { display: flex; flex-direction: column; gap: 6px; text-align: left; }
.armory-arena-row { display: flex; justify-content: space-between; font-size: 13px; color: #8a8578; padding: 3px 0; border-top: 1px solid rgba(255,255,255,.04); }
.armory-arena-row span:last-child { color: #d4cdbb; font-weight: bold; }
.armory-arena-w { color: #1eff00; }
.armory-arena-l { color: #ff5555; }
.armory-arena-nogames { font-size: 13px; color: #6d6a5e; padding: 10px 0; }
/* Badge de modo de arena (puntuado/sin partidas) */
.armory-arena-mode { display: inline-block; font-size: 10px; font-weight: bold; text-transform: uppercase; letter-spacing: .05em; padding: 2px 10px; border-radius: 10px; margin-bottom: 12px; }
.armory-arena-mode.rated { color: #d79602; background: rgba(215,150,2,.14); }
.armory-arena-mode.none { color: #6d6a5e; background: rgba(255,255,255,.04); }
/* ===== Campos de batalla (PvP) ===== */
.armory-bg-panel { padding: 18px; }
.armory-bg-header { margin-bottom: 14px; }
.armory-bg-desc { margin: 4px 0 0; color: #9a9488; font-size: 13px; }
.armory-bg-summary {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 16px;
}
.armory-bg-stat {
display: flex; flex-direction: column; align-items: center; gap: 4px;
background: #17140f; border: 1px solid #2b2620; border-radius: 8px; padding: 12px 8px;
}
.armory-bg-stat-num { font-size: 22px; font-weight: 700; }
.armory-bg-stat-lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: #8a847a; }
.armory-bg-table-wrap { overflow-x: auto; }
.armory-bg-table { width: 100%; }
.armory-bg-table th.armory-bg-c, .armory-bg-table td.armory-bg-c { text-align: center; white-space: nowrap; }
.armory-bg-name { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; white-space: nowrap; }
.armory-bg-ico {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px; border-radius: 5px; color: #fff; font-size: 12px;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.15);
}
.armory-bg-res {
display: inline-block; min-width: 68px; padding: 3px 8px; border-radius: 5px;
font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px;
}
.armory-bg-res.win { background: rgba(46,204,113,.15); color: #4ee08a; border: 1px solid rgba(46,204,113,.4); }
.armory-bg-res.loss { background: rgba(255,85,85,.13); color: #ff7a7a; border: 1px solid rgba(255,85,85,.4); }
.armory-bg-kb { color: #ffd100; font-weight: 600; }
.armory-bg-honor { color: #e0a13a; font-weight: 600; }
.armory-bg-date { color: #8a847a; font-size: 12px; }
@media (max-width: 640px) {
.armory-bg-summary { grid-template-columns: repeat(2, 1fr); }
}
/* Fecha + hora del BG (apilado) */
.armory-bg-dt { display: inline-flex; flex-direction: column; align-items: flex-end; line-height: 1.25; }
.armory-bg-dt-d { color: #cfc8bb; font-size: 12px; }
.armory-bg-dt-t { color: #8a847a; font-size: 11px; font-variant-numeric: tabular-nums; }
/* ===== Logros: rejilla de categorías (anillos) ===== */
.armory-ach2 { padding: 16px; }
.armory-ach2-summary { margin: 0 0 16px; color: #b8b0a2; font-size: 14px; }
.armory-ach-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px;
}
.armory-ach-card {
display: flex; flex-direction: column; align-items: center; gap: 10px;
background: #17140f; border: 1px solid #2b2620; border-radius: 10px;
padding: 18px 10px 14px; cursor: pointer; transition: border-color .15s, background .15s, transform .1s;
font-family: inherit; text-align: center;
}
.armory-ach-card:hover { border-color: #c8a24c; background: #1e1a13; transform: translateY(-2px); }
.armory-ach-ring {
position: relative; width: 86px; height: 86px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
.armory-ach-ring::before {
content: ''; position: absolute; inset: 11px; border-radius: 50%; background: #17140f;
}
.armory-ach-card:hover .armory-ach-ring::before { background: #1e1a13; }
.armory-ach-ring-pct {
position: relative; z-index: 1; font-size: 18px; font-weight: 700; color: #fff;
}
.armory-ach-ring.feats {
background: #241f19; color: #7d7568; font-size: 30px;
}
.armory-ach-ring.feats::before { inset: 4px; }
.armory-ach-ring.feats > i { position: relative; z-index: 1; }
.armory-ach-cat-name { color: #e6b53c; font-size: 14px; font-weight: 600; line-height: 1.2; }
.armory-ach-cat-pts { color: #d9c9a0; font-size: 13px; display: inline-flex; align-items: center; gap: 5px; }
.armory-ach-cat-pts > i { color: #c8a24c; }
/* ===== Logros: detalle de una categoría ===== */
.armory-ach2-head { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.armory-ach2-back {
display: inline-flex; align-items: center; gap: 7px; background: #17140f; border: 1px solid #2b2620;
color: #d9c9a0; border-radius: 8px; padding: 8px 14px; cursor: pointer; font-family: inherit; font-size: 13px;
transition: border-color .15s, color .15s;
}
.armory-ach2-back:hover { border-color: #c8a24c; color: #fff; }
.armory-ach2-title { font-size: 20px; font-weight: 700; color: #e6b53c; display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.armory-ach2-sub { font-size: 13px; font-weight: 400; color: #8a847a; }
.armory-ach2-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; transition: opacity .15s; }
.armory-ach2-list.loading { opacity: .5; }
.armory-ach2-item {
display: flex; align-items: flex-start; gap: 14px;
background: #17140f; border: 1px solid #2b2620; border-radius: 8px; padding: 12px 14px;
}
.armory-ach2-item:hover { border-color: #3a342b; }
.armory-ach2-icon, .armory-ach2-name { background-image: none !important; }
.armory-ach2-icon {
flex: 0 0 auto; width: 48px; height: 48px; border-radius: 6px; overflow: hidden;
border: 1px solid #3a342b; display: block;
}
.armory-ach2-icon img { width: 100%; height: 100%; display: block; }
.armory-ach2-body { flex: 1 1 auto; min-width: 0; }
.armory-ach2-name { color: #4a9eff; font-weight: 600; font-size: 15px; text-decoration: none; }
.armory-ach2-name:hover { text-decoration: underline; }
.armory-ach2-desc { margin: 3px 0 0; color: #b8b0a2; font-size: 13px; line-height: 1.35; }
.armory-ach2-reward { margin: 5px 0 0; color: #7fd18a; font-size: 12px; }
.armory-ach2-reward > i { margin-right: 4px; }
.armory-ach2-meta { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.armory-ach2-pts {
display: inline-flex; align-items: center; gap: 5px; color: #ffd100; font-weight: 700; font-size: 15px;
}
.armory-ach2-pts > i { color: #c8a24c; }
.armory-ach2-date { color: #7d7568; font-size: 12px; font-variant-numeric: tabular-nums; }
@media (max-width: 560px) {
.armory-ach-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
.armory-ach2-meta { flex-direction: row; gap: 12px; }
}
/* Fix: los anillos se aplastaban (flex-shrink en columna) -> forzar tamaño fijo */
.armory-ach-ring { flex: 0 0 86px; box-sizing: border-box; }
.armory-ach-ring.feats { flex: 0 0 86px; }
.armory-ach-card { min-height: 168px; justify-content: flex-start; }
.armory-ach-cat-name { margin-top: 2px; }
/* Fix hueco iconos: wowhead iconiza los <a> (ins/del + padding). Neutralizar. */
.armory-ach2-item { align-items: center; }
.armory-ach2-icon {
padding: 0 !important; width: 48px !important; height: 48px !important;
margin: 0 !important;
}
.armory-ach2-icon img { width: 48px !important; height: 48px !important; }
.armory-ach2-icon ins, .armory-ach2-icon del,
.armory-ach2-name ins, .armory-ach2-name del { display: none !important; }
.armory-ach2-name { padding-left: 0 !important; background-image: none !important; }
/* Fix descuadre: nombres de 2 líneas (JcJ, Mazmorras) empujaban el escudo.
Reservar altura fija para el nombre y anclar los puntos abajo. */
.armory-ach-card { justify-content: flex-start; }
.armory-ach-cat-name {
min-height: 2.5em; display: flex; align-items: center; justify-content: center;
margin-top: 4px;
}
.armory-ach-cat-pts { margin-top: auto; padding-top: 4px; }
/* Override: el margin-top:auto sacaba los escudos fuera de la tarjeta.
Quitarlo y usar solo altura reservada del nombre para alinear los puntos. */
.armory-ach-card { min-height: 0 !important; justify-content: flex-start; gap: 8px; padding: 18px 10px 16px; }
.armory-ach-cat-name { min-height: 2.6em; margin-top: 0; }
.armory-ach-cat-pts { margin-top: 0 !important; padding-top: 0; }
/* Iconos del detalle: asegurar caja limpia sin restos de wowhead */
.armory-ach2-item { align-items: center; }
.armory-ach2-icon {
width: 48px !important; height: 48px !important; padding: 0 !important; margin: 0 !important;
overflow: hidden; border-radius: 6px;
}
.armory-ach2-icon img { width: 48px !important; height: 48px !important; display: block !important; }
.armory-ach2-icon ins, .armory-ach2-icon del,
.armory-ach2-name ins, .armory-ach2-name del { display: none !important; }
.armory-ach2-name { padding-left: 0 !important; }
/* ==== Reset definitivo de la tarjeta de categoría (deshace fixes previos) ==== */
.armory-ach-card {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 10px !important;
min-height: 196px !important;
padding: 18px 10px 16px !important;
overflow: visible !important;
}
.armory-ach-ring {
flex: 0 0 86px !important;
width: 86px !important;
height: 86px !important;
box-sizing: border-box !important;
}
.armory-ach-ring.feats { flex: 0 0 86px !important; width: 86px !important; height: 86px !important; }
.armory-ach-cat-name {
min-height: 2.6em !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.armory-ach-cat-pts {
margin: 0 !important;
padding-top: 2px !important;
}
/* ===== Pestaña Bandas (progreso de raids) ===== */
.armory-raids { padding: 16px; }
.armory-raid-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 14px;
}
.armory-raid-card {
background: #17140f; border: 1px solid #2b2620; border-radius: 10px; overflow: visible;
display: flex; flex-direction: column;
}
.armory-raid-banner {
border-radius: 10px 10px 0 0; padding: 14px 14px 12px; min-height: 62px;
display: flex; flex-direction: column; justify-content: center; gap: 3px;
box-shadow: inset 0 -40px 40px -30px rgba(0,0,0,.6);
}
.armory-raid-name { color: #fff; font-weight: 700; font-size: 15px; line-height: 1.2; text-shadow: 0 1px 3px rgba(0,0,0,.7); }
.armory-raid-count { color: rgba(255,255,255,.75); font-size: 12px; text-shadow: 0 1px 2px rgba(0,0,0,.7); }
.armory-raid-diffs { padding: 10px 12px 12px; display: flex; flex-direction: column; gap: 7px; }
.armory-raid-row { position: relative; display: flex; align-items: center; gap: 10px; }
.armory-raid-diff-lbl {
flex: 0 0 68px; color: #d9a441; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .3px;
}
.armory-raid-bar {
position: relative; flex: 1 1 auto; height: 22px; border-radius: 5px; overflow: hidden;
background: #241f19; border: 1px solid #322c24; cursor: default;
}
.armory-raid-bar-fill { position: absolute; inset: 0 auto 0 0; height: 100%; border-radius: 4px 0 0 4px; transition: width .3s; }
.armory-raid-bar.full .armory-raid-bar-fill { background: linear-gradient(90deg, #2f9e4f, #37c15f); width: 100% !important; }
.armory-raid-bar.partial .armory-raid-bar-fill { background: linear-gradient(90deg, #c8781f, #e0902f); }
.armory-raid-bar.empty .armory-raid-bar-fill { background: transparent; }
.armory-raid-bar-txt {
position: relative; z-index: 1; display: block; text-align: center; line-height: 22px;
font-size: 12px; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.6);
}
/* Tooltip de jefes */
.armory-raid-tip {
position: absolute; left: 78px; bottom: calc(100% + 8px); z-index: 30;
min-width: 220px; max-width: 280px;
background: #0d0b08; border: 1px solid #c8a24c; border-radius: 8px; padding: 10px 12px;
box-shadow: 0 8px 24px rgba(0,0,0,.6);
opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity .12s, transform .12s;
pointer-events: none;
}
.armory-raid-row:hover .armory-raid-tip { opacity: 1; visibility: visible; transform: translateY(0); }
.armory-raid-tip-head { color: #e6b53c; font-weight: 700; font-size: 13px; margin-bottom: 6px; border-bottom: 1px solid #2b2620; padding-bottom: 5px; }
.armory-raid-tip ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.armory-raid-tip li { font-size: 12px; color: #9a948a; display: flex; gap: 6px; }
.armory-raid-tip li.killed { color: #7fd18a; }
.armory-raid-tip li.alive { color: #8a847a; }
.armory-raid-tip-x { color: inherit; font-weight: 700; opacity: .85; min-width: 24px; }
@media (max-width: 560px) {
.armory-raid-grid { grid-template-columns: 1fr; }
}
/* Cabeceras de expansión en Bandas */
.armory-raid-exp { margin-bottom: 22px; }
.armory-raid-exp:last-child { margin-bottom: 0; }
.armory-raid-exp-title {
margin: 0 0 12px; font-size: 15px; font-weight: 700; color: #e6b53c;
text-transform: uppercase; letter-spacing: 1px;
padding-bottom: 8px; border-bottom: 1px solid #2b2620;
}
/* ===== Página de hermandad ===== */
.armory-guild-page { max-width: 1200px; margin: 0 auto; }
.armory-guild-head { display: flex; align-items: center; gap: 18px; margin: 8px 0 4px; }
.armory-guild-emblem {
flex: 0 0 auto; width: 76px; height: 76px; border-radius: 50%;
display: flex; align-items: center; justify-content: center; font-size: 34px;
border: 2px solid #c8a24c; box-shadow: inset 0 0 20px rgba(0,0,0,.5);
}
.armory-guild-title { margin: 0; font-size: 28px; font-weight: 800; color: #fff; line-height: 1.1; }
.armory-guild-realm { color: #9a948a; font-size: 13px; margin-top: 2px; }
.armory-guild-stats { display: flex; gap: 18px; margin-top: 8px; flex-wrap: wrap; }
.armory-guild-stat { display: inline-flex; align-items: center; gap: 6px; color: #d9c9a0; font-size: 14px; }
.armory-guild-stat > i { color: #c8a24c; }
.armory-guild-stat.muted { color: #8a847a; }
.armory-guild-tabbar { display: flex; gap: 8px; border-bottom: 1px solid #2b2620; margin: 16px 0 18px; }
.armory-guild-tab { padding: 8px 16px; border-radius: 6px 6px 0 0; font-weight: 700; font-size: 14px; color: #9a948a; }
.armory-guild-tab.active { background: #c8a24c; color: #1a1710; }
.armory-guild-layout { display: grid; grid-template-columns: 1fr 340px; gap: 22px; align-items: start; }
.armory-guild-section { font-size: 14px; text-transform: uppercase; letter-spacing: .5px; color: #b8b0a2; margin: 0 0 12px; font-weight: 700; }
/* Filtros */
.armory-guild-filters { display: flex; gap: 16px; margin-bottom: 14px; flex-wrap: wrap; }
.armory-guild-filter { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: #8a847a; }
.armory-guild-filter select {
background: #17140f; border: 1px solid #3a342b; color: #e6ddc9; border-radius: 6px;
padding: 8px 10px; font-family: inherit; font-size: 13px; min-width: 190px; cursor: pointer;
}
.armory-guild-filter select:focus { outline: none; border-color: #c8a24c; }
/* Tabla roster */
.armory-guild-table { width: 100%; border-collapse: collapse; transition: opacity .15s; }
.armory-guild-table.loading { opacity: .5; }
.armory-guild-table th {
text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: #c8a24c;
padding: 8px 10px; border-bottom: 1px solid #2b2620; font-weight: 700;
}
.armory-guild-table th.armory-gt-c, .armory-guild-table td.armory-gt-c { text-align: center; }
.armory-guild-table th.armory-gt-pts, .armory-guild-table td.armory-gt-pts { text-align: right; }
.armory-guild-table td { padding: 7px 10px; border-bottom: 1px solid #201c16; }
.armory-guild-table tbody tr:hover { background: #17140f; }
.armory-gt-name { display: inline-flex; align-items: center; gap: 9px; font-weight: 600; text-decoration: none; }
.armory-gt-name:hover { text-decoration: underline; }
.armory-gt-avatar { width: 26px; height: 26px; border-radius: 50%; border: 1px solid #3a342b; object-fit: cover; }
.armory-gt-ico { width: 24px; height: 24px; border-radius: 5px; vertical-align: middle; }
.armory-gt-level { color: #cfc8bb; font-weight: 600; }
.armory-gt-rank { color: #b8b0a2; font-size: 13px; }
.armory-gt-points { color: #ffd100; font-weight: 700; display: inline-flex; align-items: center; gap: 5px; }
.armory-gt-points > i { color: #c8a24c; }
/* Barra lateral */
.armory-guild-side { display: flex; flex-direction: column; gap: 16px; }
.armory-guild-card { background: #17140f; border: 1px solid #2b2620; border-radius: 10px; padding: 14px 16px; }
.armory-guild-card-title { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: .5px; color: #c8a24c; font-weight: 700; }
.armory-guild-motd-locked { color: #8a847a; font-size: 13px; line-height: 1.4; }
.armory-guild-motd-locked > i { margin-right: 6px; color: #6a6459; }
.armory-guild-recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.armory-guild-recent-item { display: flex; align-items: center; gap: 10px; }
.armory-guild-recent-ico { width: 34px; height: 34px; border-radius: 6px; border: 1px solid #3a342b; }
.armory-guild-recent-name { flex: 1 1 auto; color: #e6b53c; font-size: 13px; font-weight: 600; }
.armory-guild-recent-date { color: #7d7568; font-size: 11px; }
.armory-guild-chart-lbl { font-size: 12px; color: #9a948a; margin-bottom: 8px; }
.armory-guild-chart { display: flex; align-items: flex-end; gap: 5px; height: 96px; }
.armory-guild-bar-col { flex: 1 1 0; height: 100%; display: flex; align-items: flex-end; }
.armory-guild-bar-wrap { width: 100%; height: 100%; display: flex; align-items: flex-end; }
.armory-guild-bar { width: 100%; min-height: 3px; border-radius: 3px 3px 0 0; }
@media (max-width: 900px) {
.armory-guild-layout { grid-template-columns: 1fr; }
}
/* Tag de reino en la cabecera del personaje */
.armory-realm {
display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 12px;
background: #17140f; border: 1px solid #2b2620; color: #9a948a; font-size: 12px;
}
/* Logros de hermandad + realm inline */
.armory-guild-realm-inline { color: #7d7568; font-weight: 400; text-transform: none; letter-spacing: 0; }
.armory-guild-tab { text-decoration: none; }
.armory-guild-page .armory-ach2 { margin-top: 4px; }
/* Columna Función + desglose de funciones */
.armory-gt-role {
display: inline-flex; align-items: center; justify-content: center;
width: 26px; height: 26px; border-radius: 50%; background: #17140f;
border: 1px solid currentColor; font-size: 12px;
}
.armory-guild-roles { display: flex; flex-direction: column; gap: 8px; }
.armory-guild-role-row { display: grid; grid-template-columns: 62px 1fr 24px; align-items: center; gap: 8px; }
.armory-guild-role-lbl { font-size: 12px; font-weight: 700; }
.armory-guild-role-track { height: 16px; background: #241f19; border: 1px solid #322c24; border-radius: 4px; overflow: hidden; }
.armory-guild-role-fill { height: 100%; min-width: 2px; border-radius: 3px; transition: width .3s; }
.armory-guild-role-n { font-size: 12px; color: #cfc8bb; text-align: right; font-variant-numeric: tabular-nums; }
/* Fix botón azul de pestaña + mensaje diario + ilvl */
.armory-guild-tab { color: #c9c2b5 !important; }
.armory-guild-tab:hover { color: #fff !important; }
.armory-guild-tab.active { color: #1a1710 !important; }
.armory-guild-motd { color: #cfc8bb; font-size: 13px; line-height: 1.45; white-space: pre-wrap; }
.armory-guild-login { color: #e6b53c !important; text-decoration: underline; font-weight: 600; }
.armory-gt-ilvl { color: #7fb4ff; font-weight: 700; }