feat: Card Kingdom prices, shopping cart export, and hover panel fixes (#73)

* feat: add CK prices, shopping cart export, and hover panel fixes

* fix: include commander in Buy This Deck cart export
This commit is contained in:
mwisnowski 2026-04-04 19:59:03 -07:00 committed by GitHub
parent dd996939e6
commit 69d84cc414
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 899 additions and 146 deletions

View file

@ -434,7 +434,10 @@
fetch('/api/price/' + encodeURIComponent(name))
.then(function(r){ return r.ok ? r.json() : null; })
.then(function(d){
var label = (d && d.found && d.price != null) ? ('$' + parseFloat(d.price).toFixed(2)) : 'Price unavailable';
var parts = [];
if (d && d.found && d.price != null) parts.push('TCG: $' + parseFloat(d.price).toFixed(2));
if (d && d.ck_price != null) parts.push('CK: $' + parseFloat(d.ck_price).toFixed(2));
var label = parts.length ? parts.join(' ') : 'Price unavailable';
_priceCache[name] = label;
_showTip(el, label);
})
@ -454,7 +457,8 @@
'Snow-Covered Plains','Snow-Covered Island','Snow-Covered Swamp',
'Snow-Covered Mountain','Snow-Covered Forest'
]);
var _priceNum = {}; // card name -> float|null
var _priceNum = {}; // card name -> {tcg, ck}|null
window._priceNum = _priceNum; // expose for cardHover.js
var _deckPrices = {}; // accumulated across build stages: card name -> float
var _buildToken = null;
function _fetchNum(name) {
@ -462,9 +466,12 @@
return fetch('/api/price/' + encodeURIComponent(name))
.then(function(r){ return r.ok ? r.json() : null; })
.then(function(d){
var p = (d && d.found && d.price != null) ? parseFloat(d.price) : null;
_priceNum[name] = p; return p;
}).catch(function(){ _priceNum[name] = null; return null; });
var obj = {
tcg: (d && d.found && d.price != null) ? parseFloat(d.price) : null,
ck: (d && d.ck_price != null) ? parseFloat(d.ck_price) : null,
};
_priceNum[name] = obj; return obj;
}).catch(function(){ var obj = {tcg:null,ck:null}; _priceNum[name] = obj; return obj; });
}
function _getBuildToken() {
var el = document.querySelector('[data-build-id]');
@ -507,17 +514,24 @@
var prevTotal = Object.values(_deckPrices).reduce(function(s,p){ return s + (p||0); }, 0);
var promises = [];
toFetch.forEach(function(name){ promises.push(_fetchNum(name).then(function(p){ return {name:name,price:p}; })); });
toFetch.forEach(function(name){ promises.push(_fetchNum(name).then(function(obj){ return {name:name,price:obj}; })); });
Promise.all(promises).then(function(results){
var map = {};
var prevTotal2 = Object.values(_deckPrices).reduce(function(s,p){ return s + (p||0); }, 0);
results.forEach(function(r){ map[r.name] = r.price; if (r.price !== null) _deckPrices[r.name] = r.price; });
results.forEach(function(r){ map[r.name] = r.price; if (r.price && r.price.tcg !== null) _deckPrices[r.name] = r.price.tcg; });
overlays.forEach(function(el){
var name = el.dataset.priceFor;
if (!name || BASIC_LANDS.has(name)) { el.style.display='none'; return; }
var p = map[name];
el.textContent = p !== null ? ('$' + p.toFixed(2)) : '';
if (ceiling !== null && p !== null && p > ceiling) {
var obj = map[name];
var tcg = obj ? obj.tcg : null;
var ck = obj ? obj.ck : null;
if (tcg !== null || ck !== null) {
var parts = [];
if (tcg !== null) parts.push('<span class="price-src-label">TCG</span> $' + tcg.toFixed(2));
if (ck !== null) parts.push('<span class="price-src-label">CK</span> $' + ck.toFixed(2));
el.innerHTML = parts.join('<br>');
} else { el.innerHTML = ''; }
if (ceiling !== null && tcg !== null && tcg > ceiling) {
var tile = el.closest('.card-tile,.stack-card');
if (tile) tile.classList.add('over-budget');
}
@ -525,9 +539,16 @@
inlines.forEach(function(el){
var name = el.dataset.priceFor;
if (!name || BASIC_LANDS.has(name)) { el.style.display='none'; return; }
var p = map[name];
el.textContent = p !== null ? ('$' + p.toFixed(2)) : '';
if (ceiling !== null && p !== null && p > ceiling) {
var obj = map[name];
var tcg = obj ? obj.tcg : null;
var ck = obj ? obj.ck : null;
if (tcg !== null || ck !== null) {
var parts = [];
if (tcg !== null) parts.push('<span class="price-src-label">TCG</span> $' + tcg.toFixed(2));
if (ck !== null) parts.push('<span class="price-src-label">CK</span> $' + ck.toFixed(2));
el.innerHTML = parts.join(' <span class="price-sep">·</span> ');
} else { el.innerHTML = ''; }
if (ceiling !== null && tcg !== null && tcg > ceiling) {
var row = el.closest('.list-row');
if (row) row.classList.add('over-budget');
}
@ -543,7 +564,7 @@
var n = el.dataset.priceFor;
if (n && !BASIC_LANDS.has(n) && !allNames.has(n)) {
allNames.add(n);
sumTotal += (map[n] || 0);
sumTotal += (map[n] ? (map[n].tcg || 0) : 0);
}
});
if (totalCap !== null) {

View file

@ -19,6 +19,9 @@
{{ card.name }}
</div>
{# Price overlay #}
<div class="card-price-overlay" data-price-for="{{ card.name }}" aria-hidden="true"></div>
{# Owned indicator #}
{% if card.is_owned %}
<div style="position:absolute; top:4px; right:4px; background:rgba(34,197,94,0.9); color:white; padding:2px 6px; border-radius:4px; font-size:12px; font-weight:600;">

View file

@ -84,6 +84,7 @@
cursor: pointer;
border-radius: 8px;
transition: transform 0.2s;
position: relative;
}
.similar-card-image:hover {
@ -235,6 +236,8 @@
<div class="similar-card-tile card-tile" data-card-name="{{ card.name }}">
<!-- Card Image (uses hover system for preview) -->
<div class="similar-card-image">
{# Price overlay #}
<div class="card-price-overlay" data-price-for="{{ card.name }}" aria-hidden="true"></div>
<img src="{{ card.name|card_image('normal') }}"
alt="{{ card.name }}"
loading="lazy"

View file

@ -247,7 +247,29 @@
<span class="card-stat-value">{{ card.rarity | capitalize }}</span>
</div>
{% endif %}
<div class="card-stat" id="card-detail-price-wrap" style="display:none;">
<span class="card-stat-label">Price</span>
<span class="card-stat-value" id="card-detail-price-display"></span>
</div>
</div>
<script>
(function(){
var name = {{ card.name | tojson }};
fetch('/api/price/' + encodeURIComponent(name))
.then(function(r){ return r.ok ? r.json() : null; })
.then(function(d){
if (!d) return;
var parts = [];
if (d.price != null) parts.push('<span class="price-src-label">TCG</span> $' + parseFloat(d.price).toFixed(2));
if (d.ck_price != null) parts.push('<span class="price-src-label">CK</span> $' + parseFloat(d.ck_price).toFixed(2));
if (parts.length) {
document.getElementById('card-detail-price-display').innerHTML = parts.join(' <span class="price-sep">\u00B7</span> ');
document.getElementById('card-detail-price-wrap').style.display = '';
}
}).catch(function(){});
})();
</script>
<!-- Oracle Text -->
{% if card.text %}

View file

@ -39,7 +39,7 @@
hx-target="closest .alts"
hx-swap="outerHTML"
title="Lock this alternative and unlock the current pick">
{{ it.name }}{% if it.price %} <span style="font-size:11px;opacity:.7;font-weight:normal;">${{ "%.2f"|format(it.price|float) }}</span>{% endif %}
{{ it.name }}{% if it.price or it.ck_price %} <span class="alt-prices">{% if it.price %}<span class="price-src-label">TCG</span> ${{ "%.2f"|format(it.price|float) }}{% endif %}{% if it.price and it.ck_price %} · {% endif %}{% if it.ck_price %}<span class="price-src-label">CK</span> ${{ "%.2f"|format(it.ck_price|float) }}{% endif %}</span>{% endif %}
</button>
</li>
{% endfor %}

View file

@ -55,7 +55,7 @@
{% if alt.price %}data-price="{{ alt.price }}"{% endif %}
title="{{ alt.shared_tags|join(', ') if alt.shared_tags else '' }}">
{{ alt.name }}
{% if alt.price %}<span class="alt-price">${{ '%.2f'|format(alt.price|float) }}</span>{% endif %}
{% if alt.price or alt.ck_price %}<span class="alt-price">{% if alt.price %}TCG ${{ '%.2f'|format(alt.price|float) }}{% endif %}{% if alt.price and alt.ck_price %} · {% endif %}{% if alt.ck_price %}CK ${{ '%.2f'|format(alt.ck_price|float) }}{% endif %}</span>{% endif %}
</button>
</form>
{% endfor %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block banner_subtitle %}Budget Pickups{% endblock %}
{% block banner_subtitle %}Upgrade Suggestions{% endblock %}
{% block content %}
<h2>Pickups List</h2>
<h2>Upgrade Suggestions</h2>
{% if commander %}
<div class="muted" style="margin-bottom:.5rem;">Deck: <strong>{{ commander }}</strong>{% if name %} — <span class="muted text-xs">{{ name }}</span>{% endif %}</div>
{% endif %}
@ -30,13 +30,35 @@
{% if budget_report.pickups_list %}
<p class="muted text-sm" style="margin-bottom:.5rem;">
Cards you don't own yet that fit the deck's themes and budget. Sorted by theme match priority.
Cards that fit the deck's themes and budget. Owned cards are free upgrades — just swap one in. Sorted by theme match priority.
<span class="price-legend" style="display:block; margin-top:.2rem;">TCG = TCGPlayer &middot; CK = Card Kingdom</span>
</p>
{# Cart toolbar #}
<div class="cart-toolbar" id="cart-toolbar">
<label style="display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.85rem;">
<input type="checkbox" id="cart-select-all" checked aria-label="Select all cards">
<span>Select all</span>
</label>
<span id="cart-selected-count" class="cart-label"></span>
<button id="btn-copy-tcg" class="btn btn-sm" type="button" onclick="cartCopyTCG()">Open in TCGPlayer</button>
<button id="btn-copy-ck" class="btn btn-sm" type="button" onclick="cartCopyCK()">Open in Card Kingdom</button>
</div>
{# Fallback textarea (shown when Clipboard API is unavailable) #}
<div id="cart-fallback-wrap" class="cart-fallback-wrap" style="display:none;">
<p>Clipboard access unavailable. Select all and copy the text below:</p>
<textarea id="cart-fallback-text" class="cart-fallback-textarea" readonly aria-label="Card list for manual copy"></textarea>
<button type="button" class="btn btn-sm" style="margin-top:.4rem;" onclick="document.getElementById('cart-fallback-wrap').style.display='none';">Close</button>
</div>
<table class="pickups-table" style="width:100%; border-collapse:collapse;">
<thead>
<tr>
<th class="cart-cb-th" aria-label="Select"></th>
<th style="text-align:left; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Card</th>
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Price</th>
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">TCG</th>
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">CK</th>
<th style="text-align:center; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Owned</th>
<th style="text-align:center; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Tier</th>
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Priority</th>
</tr>
@ -44,16 +66,33 @@
<tbody>
{% for card in budget_report.pickups_list %}
<tr>
<td class="cart-cb-td">
<input type="checkbox" class="cart-cb" data-card-name="{{ card.card|e }}" checked aria-label="Select {{ card.card|e }}">
</td>
<td style="padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
<span class="card-name-price-hover" data-card-name="{{ card.card|e }}">{{ card.card }}</span>
</td>
<td style="text-align:right; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
{% if card.price is not none %}
{% if owned_names is defined and card.card|lower in owned_names %}
<span class="muted text-sm">owned</span>
{% elif card.price is not none %}
${{ "%.2f"|format(card.price) }}{% if stale_prices is defined and card.card|lower in stale_prices %}<span class="stale-price-badge" title="Price may be outdated (>24h)">&#x23F1;</span>{% endif %}
{% else %}
<span class="muted"></span>
{% endif %}
</td>
<td style="text-align:right; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
{% if card.ck_price is not none %}
${{ "%.2f"|format(card.ck_price) }}
{% else %}
<span class="muted"></span>
{% endif %}
</td>
<td style="text-align:center; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
{% if owned_names is defined and card.card|lower in owned_names %}
<span class="owned-badge" title="You own this card">yes</span>
{% endif %}
</td>
<td style="text-align:center; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
<span class="tier-badge tier-badge--{{ card.tier|lower }}">{{ card.tier }}</span>
</td>
@ -64,8 +103,84 @@
{% endfor %}
</tbody>
</table>
<script>
(function () {
var allCb = document.getElementById('cart-select-all');
function getCheckedNames() {
return Array.from(document.querySelectorAll('.cart-cb:checked'))
.map(function (cb) { return cb.getAttribute('data-card-name'); });
}
function updateState() {
var all = document.querySelectorAll('.cart-cb');
var checked = document.querySelectorAll('.cart-cb:checked');
var n = checked.length, total = all.length;
var countEl = document.getElementById('cart-selected-count');
if (countEl) countEl.textContent = n + ' of ' + total + ' selected';
var btnTCG = document.getElementById('btn-copy-tcg');
var btnCK = document.getElementById('btn-copy-ck');
if (btnTCG) btnTCG.disabled = n === 0;
if (btnCK) btnCK.disabled = n === 0;
if (allCb) {
allCb.indeterminate = n > 0 && n < total;
allCb.checked = n === total;
}
}
if (allCb) {
allCb.addEventListener('change', function () {
document.querySelectorAll('.cart-cb').forEach(function (cb) { cb.checked = allCb.checked; });
updateState();
});
}
document.querySelectorAll('.cart-cb').forEach(function (cb) {
cb.addEventListener('change', updateState);
});
function stripDFC(n) { return n.split(' // ')[0].trim(); }
function showFallback(text) {
var wrap = document.getElementById('cart-fallback-wrap');
var ta = document.getElementById('cart-fallback-text');
if (!wrap || !ta) return;
ta.value = text;
wrap.style.display = 'block';
ta.focus();
ta.select();
}
function showCartToast(msg) {
var el = document.createElement('div');
el.className = 'cart-toast-top';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(function () { el.classList.add('hide'); setTimeout(function () { el.remove(); }, 300); }, 6000);
}
function openAfterCopy(url, vendorName) {
var names = getCheckedNames();
if (!names.length) return;
var text = names.map(function (n) { return '1 ' + stripDFC(n); }).join('\n');
function doOpen() {
showCartToast('List copied — paste into ' + vendorName + ' with Ctrl+V');
setTimeout(function () { window.open(url, '_blank'); }, 400);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(doOpen).catch(function () { showFallback(text); window.open(url, '_blank'); });
} else { showFallback(text); window.open(url, '_blank'); }
}
window.cartCopyTCG = function () { openAfterCopy('https://www.tcgplayer.com/massentry', 'TCGPlayer'); };
window.cartCopyCK = function () { openAfterCopy('https://www.cardkingdom.com/builder', 'Card Kingdom'); };
updateState();
})();
</script>
{% else %}
<div class="panel muted">No pickups suggestions available — your deck may already fit the budget well.</div>
<div class="panel muted">No upgrade suggestions available — your deck may already fit the budget well.</div>
{% endif %}
{% if budget_report.over_budget_cards %}
@ -102,3 +217,4 @@
<a href="/decks" class="btn" role="button">All Decks</a>
</div>
{% endblock %}

View file

@ -45,6 +45,8 @@
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}
width="320" />
{# Price overlay — ensures commander price is loaded into window._priceNum for the hover panel #}
<div class="card-price-overlay" data-price-for="{{ commander_base }}" aria-hidden="true"></div>
</div>
<div class="muted" style="margin-top:.25rem;">Commander: <span data-card-name="{{ commander }}"
data-original-name="{{ commander }}"
@ -69,12 +71,74 @@
{% endif %}
<a href="/decks/compare?A={{ name|urlencode }}" class="btn" role="button" title="Compare this deck with another">Compare…</a>
{% if budget_report %}
<a href="/decks/pickups?name={{ name|urlencode }}" class="btn" role="button" title="View cards to acquire for this budget build">Pickups List</a>
<a href="/decks/pickups?name={{ name|urlencode }}" class="btn" role="button" title="View upgrade suggestions for this deck">Upgrade Suggestions</a>
{% endif %}
<form method="get" action="/decks" style="display:inline; margin:0;">
<button type="submit">Back to Finished Decks</button>
</form>
</div>
{# Buy This Deck: collect all cards, strip DFC names, open vendor + copy to clipboard #}
{%- set _buy_cards = [] -%}
{%- if commander_base -%}
{%- set _ = _buy_cards.append({'name': commander_base, 'count': 1}) -%}
{%- endif -%}
{%- if summary and summary.type_breakdown and summary.type_breakdown.cards -%}
{%- for _btype, _bclist in summary.type_breakdown.cards.items() -%}
{%- for _bc in _bclist -%}
{%- if _bc.name -%}
{%- set _ = _buy_cards.append({'name': _bc.name, 'count': (_bc.count if _bc.count and _bc.count > 1 else 1)}) -%}
{%- endif -%}
{%- endfor -%}
{%- endfor -%}
{%- endif -%}
{% if _buy_cards %}
<div class="cart-toolbar" style="margin-top:.6rem; flex-direction:column; align-items:flex-start; gap:.35rem;" id="buy-deck-toolbar">
<span class="cart-label" style="font-weight:600; color:var(--text,#e5e7eb);">Buy this deck:</span>
<div style="display:flex; gap:.4rem; flex-wrap:wrap;">
<button class="btn btn-sm" type="button" onclick="buyViaTCG()">Open in TCGPlayer</button>
<button class="btn btn-sm" type="button" onclick="buyViaCK()">Open in Card Kingdom</button>
</div>
</div>
<div id="buy-fallback-wrap" class="cart-fallback-wrap" style="display:none;">
<p>Clipboard access unavailable. Select all and copy the text below, then paste it at the vendor site:</p>
<textarea id="buy-fallback-text" class="cart-fallback-textarea" readonly aria-label="Deck list for manual copy"></textarea>
<button type="button" class="btn btn-sm" style="margin-top:.4rem;" onclick="document.getElementById('buy-fallback-wrap').style.display='none';">Close</button>
</div>
<script>
(function () {
var _buyCards = {{ _buy_cards | tojson }};
function stripDFC(n) { return n.split(' // ')[0].trim(); }
function buildList(cards) {
return cards.map(function (c) { return c.count + ' ' + stripDFC(c.name); }).join('\n');
}
function showFallback(text) {
var wrap = document.getElementById('buy-fallback-wrap');
var ta = document.getElementById('buy-fallback-text');
if (!wrap || !ta) return;
ta.value = text; wrap.style.display = 'block'; ta.focus(); ta.select();
}
function showCartToast(msg) {
var el = document.createElement('div');
el.className = 'cart-toast-top';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(function () { el.classList.add('hide'); setTimeout(function () { el.remove(); }, 300); }, 6000);
}
function openAfterCopy(url, vendorName) {
var text = buildList(_buyCards);
function doOpen() {
showCartToast('List copied — paste into ' + vendorName + ' with Ctrl+V');
setTimeout(function () { window.open(url, '_blank'); }, 400);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(doOpen).catch(function () { showFallback(text); window.open(url, '_blank'); });
} else { showFallback(text); window.open(url, '_blank'); }
}
window.buyViaTCG = function () { openAfterCopy('https://www.tcgplayer.com/massentry', 'TCGPlayer'); };
window.buyViaCK = function () { openAfterCopy('https://www.cardkingdom.com/builder', 'Card Kingdom'); };
})();
</script>
{% endif %}
{% if budget_report %}
{% set bstatus = budget_report.budget_status %}
<div class="budget-badge budget-badge--{{ bstatus }}" style="margin-top:.6rem;">

View file

@ -8,6 +8,7 @@
<h5>Card Types</h5>
<div class="summary-view-controls">
<span class="muted">View:</span>
<span class="price-legend" style="margin-left:auto;">TCG = TCGPlayer &middot; CK = Card Kingdom</span>
<div class="seg" role="tablist" aria-label="Type view">
<button type="button" class="seg-btn" data-view="list" aria-selected="true" onclick="(function(btn){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(!list||!thumbs)return;list.classList.remove('hidden');thumbs.classList.add('hidden');btn.setAttribute('aria-selected','true');var other=btn.parentElement.querySelector('.seg-btn[data-view=thumbs]');if(other)other.setAttribute('aria-selected','false');try{localStorage.setItem('summaryTypeView','list');}catch(e){}})(this)">List</button>
<button type="button" class="seg-btn" data-view="thumbs" onclick="(function(btn){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(!list||!thumbs)return;list.classList.add('hidden');thumbs.classList.remove('hidden');btn.setAttribute('aria-selected','true');var other=btn.parentElement.querySelector('.seg-btn[data-view=list]');if(other)other.setAttribute('aria-selected','false');try{localStorage.setItem('summaryTypeView','thumbs');}catch(e){}; (function(){var tv=document.getElementById('typeview-thumbs'); if(!tv) return; tv.querySelectorAll('.stack-wrap').forEach(function(sw){var grid=sw.querySelector('.stack-grid'); if(!grid) return; var cs=getComputedStyle(sw); var cardW=parseFloat(cs.getPropertyValue('--card-w'))||160; var gap=10; var width=sw.clientWidth; if(!width||width<cardW){ sw.style.setProperty('--cols','1'); return;} var cols=Math.max(1,Math.floor((width+gap)/(cardW+gap))); sw.style.setProperty('--cols',String(cols));}); })();})(this)">Thumbnails</button>

View file

@ -184,8 +184,9 @@
{% if theme.example_cards %}
{% for c in theme.example_cards %}
{% set base_c = (c.split(' - Synergy (')[0] if ' - Synergy (' in c else c) %}
<div class="ex-card text-center" data-card-name="{{ base_c }}" data-role="example_card" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
<div class="ex-card text-center" style="position:relative" data-card-name="{{ base_c }}" data-role="example_card" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
<img class="card-thumb w-full h-auto border border-[var(--border)] rounded-[10px]" loading="lazy" decoding="async" alt="{{ c }} image" src="{{ base_c|card_image('small') }}" />
<div class="card-price-overlay" data-price-for="{{ base_c }}" aria-hidden="true"></div>
<div class="text-[11px] mt-1 whitespace-nowrap overflow-hidden text-ellipsis font-semibold card-ref" data-card-name="{{ base_c }}" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">{{ c }}</div>
</div>
{% endfor %}
@ -198,8 +199,9 @@
{% if theme.example_commanders %}
{% for c in theme.example_commanders %}
{% set base_c = (c.split(' - Synergy (')[0] if ' - Synergy (' in c else c) %}
<div class="ex-commander commander-cell text-center" data-card-name="{{ base_c }}" data-role="commander_example" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
<div class="ex-commander commander-cell text-center" style="position:relative" data-card-name="{{ base_c }}" data-role="commander_example" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
<img class="card-thumb w-full h-auto border border-[var(--border)] rounded-[10px]" loading="lazy" decoding="async" alt="{{ c }} image" src="{{ base_c|card_image('small') }}" />
<div class="card-price-overlay" data-price-for="{{ base_c }}" aria-hidden="true"></div>
<div class="text-[11px] mt-1 font-semibold whitespace-nowrap overflow-hidden text-ellipsis card-ref" data-card-name="{{ base_c }}" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">{{ c }}</div>
</div>
{% endfor %}