-
-
- Full-size map image (pixel-accurate rendering from tileset data)
+
+
${legendItems.join('')}
+
+
+
+
Click a marker to edit. Drag to reposition.
`;
}
+// Place interactive markers on the map image once it loads
+function initInteractiveMap(map) {
+ const container = $('#imap-container');
+ const img = $('#imap-img');
+ const markersEl = $('#imap-markers');
+ if (!container || !img || !markersEl) return;
+
+ const dirName = map._dirName;
+
+ function placeMarkers() {
+ markersEl.innerHTML = '';
+ const imgW = img.naturalWidth;
+ const imgH = img.naturalHeight;
+ if (!imgW || !imgH) return;
+
+ // Compute tile size: GBA maps use 16px tiles in the source images
+ const TILE = 16;
+
+ // Collect all entities with positions
+ const entities = [];
+
+ // NPCs (non-item, non-berry)
+ (map.object_events || []).forEach((evt, i) => {
+ if ((evt.graphics_id || '').includes('ITEM_BALL')) {
+ entities.push({ type: 'item', x: evt.x, y: evt.y, idx: i, evt, label: (evt.trainer_sight_or_berry_tree_id || '').replace('ITEM_', '').replace(/_/g, ' ') });
+ } else if ((evt.graphics_id || '').includes('BERRY_TREE')) {
+ // Skip berry trees
+ } else {
+ const isTrainer = evt.trainer_type && evt.trainer_type !== 'TRAINER_TYPE_NONE';
+ let name = (evt.graphics_id || '').replace('OBJ_EVENT_GFX_', '').replace(/_/g, ' ');
+ if (evt.script && evt.script !== '0x0') {
+ const parts = evt.script.split('_EventScript_');
+ if (parts.length >= 2) name = parts[parts.length - 1].replace(/_/g, ' ');
+ }
+ entities.push({ type: isTrainer ? 'trainer' : 'npc', x: evt.x, y: evt.y, idx: i, evt, label: name });
+ }
+ });
+
+ // Hidden items
+ (map.bg_events || []).forEach((evt, i) => {
+ if (evt.type === 'hidden_item') {
+ entities.push({ type: 'hidden', x: evt.x, y: evt.y, bgIdx: i, evt, label: (evt.item || '').replace('ITEM_', '').replace(/_/g, ' ') });
+ } else if (evt.type === 'sign') {
+ entities.push({ type: 'sign', x: evt.x, y: evt.y, bgIdx: i, evt, label: 'Sign' });
+ }
+ });
+
+ // Warps
+ (map.warp_events || []).forEach((evt, i) => {
+ const dest = (evt.dest_map || '').replace('MAP_', '').replace(/_/g, ' ');
+ entities.push({ type: 'warp', x: evt.x, y: evt.y, warpIdx: i, evt, label: dest || 'Warp' });
+ });
+
+ for (const ent of entities) {
+ const marker = document.createElement('div');
+ marker.className = `imap-marker imap-marker-${ent.type}`;
+ // Position as percentage relative to image size
+ // +7 offset: maps typically have a 7-tile border on each side
+ const px = ((ent.x + 7) * TILE + TILE / 2) / imgW * 100;
+ const py = ((ent.y + 7) * TILE + TILE / 2) / imgH * 100;
+ marker.style.left = px + '%';
+ marker.style.top = py + '%';
+ marker.title = `${ent.type.toUpperCase()}: ${ent.label} (${ent.x}, ${ent.y})`;
+ marker.dataset.entityType = ent.type;
+ marker.dataset.x = ent.x;
+ marker.dataset.y = ent.y;
+
+ // Icon inside marker
+ const icons = { npc: '\u263A', trainer: '\u2694', item: '\u2666', hidden: '\u2733', warp: '\uD83D\uDEAA', sign: '\uD83D\uDCCB' };
+ marker.innerHTML = icons[ent.type] || '\u2022';
+
+ // Click to open edit menu
+ marker.addEventListener('click', (e) => {
+ e.stopPropagation();
+ openMarkerMenu(ent, map, marker);
+ });
+
+ // Drag to reposition
+ setupMarkerDrag(marker, ent, map, img, TILE);
+
+ markersEl.appendChild(marker);
+ }
+ }
+
+ if (img.complete && img.naturalWidth) {
+ placeMarkers();
+ } else {
+ img.addEventListener('load', placeMarkers);
+ }
+ // Recompute on resize
+ const observer = new ResizeObserver(() => placeMarkers());
+ observer.observe(container);
+ // Store cleanup ref
+ container._resizeObserver = observer;
+}
+
+function setupMarkerDrag(marker, entity, map, img, TILE) {
+ let isDragging = false;
+ let startX, startY;
+ const DRAG_THRESHOLD = 4;
+
+ marker.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ isDragging = false;
+ startX = e.clientX;
+ startY = e.clientY;
+ marker.classList.add('imap-marker-dragging');
+
+ const onMove = (e2) => {
+ const dx = e2.clientX - startX;
+ const dy = e2.clientY - startY;
+ if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
+ isDragging = true;
+ // Move marker visually
+ const rect = img.getBoundingClientRect();
+ const pxX = ((e2.clientX - rect.left) / rect.width) * 100;
+ const pxY = ((e2.clientY - rect.top) / rect.height) * 100;
+ marker.style.left = Math.max(0, Math.min(100, pxX)) + '%';
+ marker.style.top = Math.max(0, Math.min(100, pxY)) + '%';
+ }
+ };
+
+ const onUp = (e2) => {
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('mouseup', onUp);
+ marker.classList.remove('imap-marker-dragging');
+
+ if (isDragging) {
+ // Calculate new tile coords
+ const rect = img.getBoundingClientRect();
+ const relX = (e2.clientX - rect.left) / rect.width;
+ const relY = (e2.clientY - rect.top) / rect.height;
+ const newX = Math.round((relX * img.naturalWidth) / TILE - 7 - 0.5);
+ const newY = Math.round((relY * img.naturalHeight) / TILE - 7 - 0.5);
+
+ if (newX !== entity.x || newY !== entity.y) {
+ entity.evt.x = newX;
+ entity.evt.y = newY;
+ entity.x = newX;
+ entity.y = newY;
+ const serialized = { ...map };
+ delete serialized._dirName;
+ markChanged(`data/maps/${map._dirName}/map.json`, JSON.stringify(serialized, null, 2) + '\n');
+ toast(`Moved ${entity.type} to (${newX}, ${newY})`);
+ renderMapDetail(map._dirName);
+ }
+ }
+ };
+
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onUp);
+ e.preventDefault();
+ });
+}
+
+function openMarkerMenu(entity, map, markerEl) {
+ // Remove any existing popup
+ $$('.imap-popup').forEach(p => p.remove());
+
+ const popup = document.createElement('div');
+ popup.className = 'imap-popup';
+
+ const dirName = map._dirName;
+ let buttonsHtml = '';
+
+ if (entity.type === 'npc' || entity.type === 'trainer') {
+ buttonsHtml = `
+
Edit
+ ${entity.type === 'trainer' ? `
Party ` : ''}
+
Delete
+ `;
+ } else if (entity.type === 'item') {
+ const itemIdx = getMapItemBalls(map).indexOf(entity.evt);
+ buttonsHtml = `
+
Edit
+
Delete
+ `;
+ } else if (entity.type === 'warp') {
+ buttonsHtml = `
+
Edit
+
Delete
+ `;
+ } else if (entity.type === 'hidden') {
+ const hiddenIdx = (map.bg_events || []).filter(e => e.type === 'hidden_item').indexOf(entity.evt);
+ buttonsHtml = `
+
Edit
+
Delete
+ `;
+ } else if (entity.type === 'sign') {
+ buttonsHtml = `
Sign at (${entity.x}, ${entity.y}) `;
+ }
+
+ popup.innerHTML = `
+
+
+
+ `;
+
+ // Position near the marker
+ const container = $('#imap-container');
+ const markerRect = markerEl.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ popup.style.left = (markerRect.left - containerRect.left + markerRect.width / 2) + 'px';
+ popup.style.top = (markerRect.top - containerRect.top - 8) + 'px';
+
+ container.appendChild(popup);
+
+ // Close on click elsewhere
+ const closeHandler = (e) => {
+ if (!popup.contains(e.target) && !markerEl.contains(e.target)) {
+ popup.remove();
+ document.removeEventListener('click', closeHandler);
+ }
+ };
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
+}
+
// ── Wild Encounters Section ──
function buildEncounterSection(enc, encounterRates, map) {
const fieldTypes = ['land_mons', 'water_mons', 'rock_smash_mons', 'fishing_mons'];
@@ -4782,6 +5028,12 @@ function collectNPCs() {
}
async function renderNPCs() {
+ // If viewing a specific NPC group, render detail
+ if (state.npcDetail) {
+ await renderNPCGroupDetail(state.npcDetail);
+ return;
+ }
+
const maps = await loadMaps();
try { await loadTrainers(); } catch {}
@@ -4796,14 +5048,15 @@ async function renderNPCs() {
(n._mapId || '').toLowerCase().includes(search);
});
- // Group by graphics_id for a summary view
+ // Group by graphics_id
const groups = {};
for (const n of filtered) {
const key = n.graphics_id || 'UNKNOWN';
- if (!groups[key]) groups[key] = { graphics_id: key, count: 0, maps: new Set(), npcs: [] };
+ if (!groups[key]) groups[key] = { graphics_id: key, count: 0, maps: new Set(), npcs: [], hasTrainer: false };
groups[key].count++;
groups[key].maps.add(n._mapName);
groups[key].npcs.push(n);
+ if (n.trainer_type && n.trainer_type !== 'TRAINER_TYPE_NONE') groups[key].hasTrainer = true;
}
const sortedGroups = Object.values(groups).sort((a, b) => b.count - a.count);
@@ -4821,59 +5074,139 @@ async function renderNPCs() {
const list = $('#npc-list');
- // Show individual NPC events in a table-like view
- const maxShow = 200;
- const toShow = filtered.slice(0, maxShow);
+ // Group view: each graphics_id gets a card. Click to expand into individual entries.
+ const maxGroups = 100;
+ const groupsToShow = sortedGroups.slice(0, maxGroups);
- let rows = toShow.map((n, i) => {
- const gfx = (n.graphics_id || '').replace('OBJ_EVENT_GFX_', '').replace(/_/g, ' ');
- const isTrainer = n.trainer_type && n.trainer_type !== 'TRAINER_TYPE_NONE';
- const scriptShort = (n.script || '').replace(/_EventScript_/g, ' ').replace(/_/g, ' ');
+ let groupCards = groupsToShow.map(g => {
+ const gfx = g.graphics_id.replace('OBJ_EVENT_GFX_', '').replace(/_/g, ' ');
+ const mapList = [...g.maps].slice(0, 3).join(', ') + (g.maps.size > 3 ? ` +${g.maps.size - 3} more` : '');
+ const trainerCount = g.npcs.filter(n => n.trainer_type && n.trainer_type !== 'TRAINER_TYPE_NONE').length;
+
+ // If only 1 NPC, show inline with direct actions
+ if (g.count === 1) {
+ const n = g.npcs[0];
+ const isTrainer = n.trainer_type && n.trainer_type !== 'TRAINER_TYPE_NONE';
+ return `
+
+
+
+ `;
+ }
+
+ // Multiple NPCs: show as group card that can be clicked into
return `
-
- ${getSpriteHtml(n.graphics_id, 36)}
-
-
${escHtml(gfx)}${isTrainer ? ' TRAINER ' : ''}
-
${escHtml(n.script || 'No script')}
-
-
- ${escHtml(n._mapName)}
-
-
(${n.x}, ${n.y})
-
-
Edit
- ${isTrainer ? `
Party ` : ''}
+
+
`;
}).join('');
- if (filtered.length > maxShow) {
- rows += `
Showing ${maxShow} of ${filtered.length} NPCs. Use search to narrow down.
`;
+ if (sortedGroups.length > maxGroups) {
+ groupCards += `
Showing ${maxGroups} of ${sortedGroups.length} NPC types. Use search to narrow down.
`;
}
- list.innerHTML = `
-
-
- ${rows || '
No NPCs found matching your search.
'}
-
- `;
+ list.innerHTML = groupCards || '
No NPCs found matching your search.
';
$('#npc-search').addEventListener('input', async e => {
const pos = e.target.selectionStart;
state.search = e.target.value;
+ state.npcDetail = null;
await renderNPCs();
const el = $('#npc-search');
if (el) { el.focus(); el.selectionStart = el.selectionEnd = pos; }
});
}
+// Detail view for a specific NPC graphics_id group
+async function renderNPCGroupDetail(graphicsId) {
+ const allNPCs = collectNPCs();
+ const groupNPCs = allNPCs.filter(n => n.graphics_id === graphicsId);
+ const gfx = graphicsId.replace('OBJ_EVENT_GFX_', '').replace(/_/g, ' ');
+
+ content.innerHTML = `
+
← Back to NPCs
+
+
+ `;
+
+ const list = $('#npc-group-list');
+
+ // Group by map for better organization
+ const byMap = {};
+ for (const n of groupNPCs) {
+ if (!byMap[n._mapDirName]) byMap[n._mapDirName] = { name: n._mapName, npcs: [] };
+ byMap[n._mapDirName].npcs.push(n);
+ }
+
+ let html = '';
+ for (const [dirName, mapGroup] of Object.entries(byMap)) {
+ html += `
+ `;
+
+ for (const n of mapGroup.npcs) {
+ const isTrainer = n.trainer_type && n.trainer_type !== 'TRAINER_TYPE_NONE';
+ let npcName = gfx;
+ if (n.script && n.script !== '0x0') {
+ const parts = n.script.split('_EventScript_');
+ if (parts.length >= 2) npcName = parts[parts.length - 1].replace(/_/g, ' ');
+ }
+
+ html += `
+
+ ${getSpriteHtml(n.graphics_id, 36)}
+
+
${escHtml(npcName)}${isTrainer ? ' TRAINER ' : ''}
+
${escHtml(n.script || 'No script')}
+
+
(${n.x}, ${n.y})
+
+ Edit
+ ${isTrainer ? `Party ` : ''}
+
+
+ `;
+ }
+ html += '
';
+ }
+
+ list.innerHTML = html || '
No NPCs found.
';
+}
+
function editNPCFromList(dirName, script, x, y) {
const map = state.maps.find(m => m._dirName === dirName);
if (!map) return;
@@ -4922,9 +5255,30 @@ function parsePokemonSpecies(text) {
const growthM = body.match(/\.growthRate\s*=\s*(\w+)/);
const eggM = body.match(/\.eggGroups\s*=\s*MON_EGG_GROUPS\((\w+)(?:,\s*(\w+))?\)/);
const learnsetM = body.match(/\.levelUpLearnset\s*=\s*(\w+)/);
+ const evosM = body.match(/\.evolutions\s*=\s*EVOLUTION\((.+)\)\s*,/);
if (nameM) mon.name = nameM[1];
if (learnsetM) mon.learnsetVar = learnsetM[1];
+
+ // Parse evolution data
+ if (evosM) {
+ mon.evolutions = [];
+ const evoStr = evosM[1];
+ // Match each {EVO_METHOD, param, SPECIES_TARGET, ...} block
+ const evoBlockRegex = /\{(EVO_\w+),\s*([^,}]+),\s*(SPECIES_\w+)(?:,\s*CONDITIONS\(([^)]+)\))?\}/g;
+ let evoMatch;
+ while ((evoMatch = evoBlockRegex.exec(evoStr)) !== null) {
+ const evo = {
+ method: evoMatch[1],
+ param: evoMatch[2].trim(),
+ target: evoMatch[3],
+ };
+ if (evoMatch[4]) {
+ evo.conditions = evoMatch[4].trim();
+ }
+ mon.evolutions.push(evo);
+ }
+ }
if (hpM) mon.baseHP = parseInt(hpM[1]);
if (atkM) mon.baseAttack = parseInt(atkM[1]);
if (defM) mon.baseDefense = parseInt(defM[1]);
@@ -4978,6 +5332,32 @@ async function loadPokemonSpecies() {
return state.pokemon;
}
+function formatEvoMethod(evo) {
+ const method = (evo.method || '').replace('EVO_', '');
+ const param = (evo.param || '').replace('ITEM_', '').replace(/_/g, ' ');
+ const conditions = (evo.conditions || '').replace(/\{IF_/g, '').replace(/}/g, '').replace(/_/g, ' ').replace(/,\s*/g, ': ');
+ const labels = {
+ 'LEVEL': `Level ${param}`,
+ 'ITEM': `Use ${param}`,
+ 'TRADE': 'Trade',
+ 'TRADE_ITEM': `Trade holding ${param}`,
+ 'FRIENDSHIP': 'Friendship',
+ 'LEVEL_NIGHT': `Level ${param} (Night)`,
+ 'LEVEL_DAY': `Level ${param} (Day)`,
+ 'BEAUTY': `Beauty ${param}`,
+ 'ITEM_HOLD_DAY': `Hold ${param} (Day)`,
+ 'ITEM_HOLD_NIGHT': `Hold ${param} (Night)`,
+ 'MOVE': `Know ${param}`,
+ 'MOVE_TYPE': `Know ${param}-type move`,
+ 'SPECIFIC_MON_IN_PARTY': `${param} in party`,
+ 'LEVEL_RAIN': `Level ${param} (Rain)`,
+ 'LEVEL_DARK_TYPE_MON_IN_PARTY': `Level ${param} (Dark-type in party)`,
+ };
+ let result = labels[method] || `${method} ${param}`.trim();
+ if (conditions) result += ` [${conditions}]`;
+ return result;
+}
+
function updatePokemonInFile(mon) {
const filePath = `src/data/pokemon/species_info/${mon._file}`;
let fileContent = pendingChanges[filePath] || originalContent[filePath];
@@ -5324,6 +5704,17 @@ async function renderPokemonPage() {
const totalPages = Math.ceil(filtered.length / perPage);
const pageItems = filtered.slice(page * perPage, (page + 1) * perPage);
+ // Build evolution lookup: species -> what evolves INTO it (prevolutions)
+ const prevolutionMap = {};
+ for (const p of pokemon) {
+ if (p.evolutions) {
+ for (const evo of p.evolutions) {
+ if (!prevolutionMap[evo.target]) prevolutionMap[evo.target] = [];
+ prevolutionMap[evo.target].push({ from: p.id, fromName: p.name, method: evo.method, param: evo.param, conditions: evo.conditions });
+ }
+ }
+ }
+
content.innerHTML = `
@@ -5488,6 +5902,19 @@ function editPokemon(id) {
+ ${mon.evolutions && mon.evolutions.length > 0 ? `
+
Evolution
+
+ ${mon.evolutions.map(e => {
+ const targetMon = (state.pokemon || []).find(p => p.id === e.target);
+ const targetName = targetMon ? targetMon.name : e.target.replace('SPECIES_', '');
+ return `
+ →
+ ${escHtml(targetName)}
+ ${escHtml(formatEvoMethod(e))}
+
`;
+ }).join('')}
+
` : ''}
BST: ${mon.bst || 0} · File: ${escHtml(mon._file || '')}
diff --git a/editor/style.css b/editor/style.css
index a3f4fc7f6d..10529dade2 100644
--- a/editor/style.css
+++ b/editor/style.css
@@ -1788,3 +1788,283 @@ tbody tr:last-child td { border-bottom: none; }
display: flex;
align-items: center;
}
+
+/* ─── Interactive Map ───────────────────────── */
+.imap-container {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ overflow: hidden;
+ background: var(--bg);
+}
+.imap-container img {
+ display: block;
+ width: 100%;
+ height: auto;
+ image-rendering: pixelated;
+}
+.imap-markers {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+.imap-marker {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ transform: translate(-50%, -50%);
+ pointer-events: auto;
+ cursor: pointer;
+ border: 2px solid rgba(255,255,255,0.8);
+ box-shadow: 0 1px 4px rgba(0,0,0,0.5);
+ transition: transform 0.1s, box-shadow 0.1s;
+ z-index: 2;
+}
+.imap-marker:hover {
+ transform: translate(-50%, -50%) scale(1.3);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.7);
+ z-index: 10;
+}
+.imap-marker-dragging {
+ opacity: 0.7;
+ z-index: 20 !important;
+ cursor: grabbing;
+}
+.imap-marker-npc { background: var(--accent); color: #fff; }
+.imap-marker-trainer { background: var(--red); color: #fff; }
+.imap-marker-item { background: var(--yellow); color: #333; }
+.imap-marker-hidden { background: var(--purple); color: #fff; }
+.imap-marker-warp { background: var(--cyan); color: #fff; font-size: 9px; }
+.imap-marker-sign { background: var(--orange); color: #fff; font-size: 9px; }
+
+.imap-legend {
+ display: flex;
+ gap: 14px;
+ margin-bottom: 10px;
+ flex-wrap: wrap;
+ font-size: 12px;
+ color: var(--text-dim);
+}
+.imap-legend-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+.imap-legend-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+.imap-dot-npc { background: var(--accent); }
+.imap-dot-item { background: var(--yellow); }
+.imap-dot-hidden { background: var(--purple); }
+.imap-dot-warp { background: var(--cyan); }
+.imap-dot-sign { background: var(--orange); }
+.imap-dot-trainer { background: var(--red); }
+
+.imap-hint {
+ text-align: center;
+ font-size: 11px;
+ color: var(--text-dim);
+ margin-top: 8px;
+}
+
+/* Interactive Map Popup */
+.imap-popup {
+ position: absolute;
+ transform: translate(-50%, -100%);
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 10px 14px;
+ min-width: 180px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5);
+ z-index: 100;
+ pointer-events: auto;
+}
+.imap-popup::after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 100%;
+ transform: translateX(-50%);
+ border: 6px solid transparent;
+ border-top-color: var(--border);
+}
+.imap-popup-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 4px;
+ font-size: 13px;
+}
+.imap-popup-type {
+ font-size: 9px;
+ text-transform: uppercase;
+ font-weight: 600;
+ padding: 1px 6px;
+ border-radius: 8px;
+ color: #fff;
+}
+.imap-popup-coords {
+ font-size: 11px;
+ color: var(--text-dim);
+ font-family: monospace;
+ margin-bottom: 8px;
+}
+.imap-popup-actions {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+/* ─── NPC Group Cards ───────────────────────── */
+.npc-group-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin-bottom: 8px;
+ transition: border-color 0.15s;
+ overflow: hidden;
+}
+.npc-group-card:hover { border-color: var(--accent); }
+.npc-group-card:last-child { margin-bottom: 0; }
+.npc-group-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 12px 16px;
+}
+.npc-group-info {
+ flex: 1;
+ min-width: 0;
+}
+.npc-group-name {
+ font-size: 14px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.npc-group-meta {
+ font-size: 12px;
+ color: var(--text-dim);
+ margin-top: 2px;
+}
+.npc-group-maps {
+ font-size: 11px;
+ color: var(--text-dim);
+ margin-top: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.npc-group-badges {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+}
+.npc-group-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 28px;
+ height: 28px;
+ border-radius: 14px;
+ background: var(--bg);
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--accent);
+}
+.npc-group-arrow {
+ font-size: 12px;
+ color: var(--text-dim);
+ flex-shrink: 0;
+}
+.npc-group-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+/* NPC Group Detail — Map Section */
+.npc-group-map-section {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin-bottom: 12px;
+ overflow: hidden;
+}
+.npc-group-map-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 16px;
+ background: var(--bg-hover);
+ border-bottom: 1px solid var(--border);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--accent);
+}
+.npc-group-map-header:hover { text-decoration: underline; }
+.npc-group-map-count {
+ font-size: 11px;
+ color: var(--text-dim);
+ font-weight: 400;
+}
+
+/* ─── Evolution Display ─────────────────────── */
+.evo-pill {
+ display: inline-block;
+ padding: 1px 8px;
+ border-radius: 10px;
+ font-size: 11px;
+ font-weight: 500;
+ background: rgba(79,143,247,0.15);
+ color: var(--accent);
+ white-space: nowrap;
+ cursor: default;
+}
+.evo-pill-from {
+ background: rgba(168,85,247,0.15);
+ color: var(--purple);
+}
+.evo-chain-display {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.evo-chain-entry {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-size: 13px;
+}
+.evo-chain-arrow {
+ color: var(--accent);
+ font-size: 16px;
+ flex-shrink: 0;
+}
+.evo-chain-target {
+ font-weight: 600;
+ color: var(--text);
+}
+.evo-chain-method {
+ font-size: 12px;
+ color: var(--text-dim);
+ margin-left: auto;
+}