From 6501703c8d0752632086c97cfd668827eca248d2 Mon Sep 17 00:00:00 2001 From: Pyronewbic Date: Tue, 12 May 2026 12:19:46 +0530 Subject: [PATCH] feat: condition badges, price outlier warnings, sort control, result counts - Show detectedCondition badges on raw listing cards, replace "Ungraded" - Flag price outliers via flagPriceOutliers() in API pipeline (demo + live) - Consistent shipping display with green "Free shipping" - Sort dropdown (price asc/desc) persists across source filter changes - Tab labels show result counts: "Active (6)" / "Sold (3)" - Umbreon demo data: add detectedCondition (NM/LP) based on AI grades - Detail panel: prefer detectedCondition over generic "Ungraded" --- api.js | 12 ++--- lib/data/demo.js | 10 ++-- public/app.js | 122 ++++++++++++++++++++++++++++++++++++---------- public/index.html | 4 ++ public/style.css | 116 +++++++++++++++++++++++++++++++++++-------- 5 files changed, 209 insertions(+), 55 deletions(-) diff --git a/api.js b/api.js index 9eaccc1..9b0c5a6 100644 --- a/api.js +++ b/api.js @@ -12,7 +12,7 @@ import { searchMagi } from "./lib/sources/magi.js"; import { searchYahooAuctions } from "./lib/sources/yahooauctions.js"; import { getPsaGradingSignal } from "./lib/grading/psa.js"; import { gradeImage } from "./lib/grading/grading.js"; -import { parseListingLanguagesFromInput, filterByCondition, detectCondition } from "./lib/search/filters.js"; +import { parseListingLanguagesFromInput, filterByCondition, detectCondition, flagPriceOutliers } from "./lib/search/filters.js"; import { buildEbaySearchQuery } from "./lib/search/listingQuery.js"; import { EBAY_CATEGORY_TCG_SINGLE_CARDS_US } from "./lib/search/ebayCategories.js"; import { getRedisStatus, sha256 } from "./lib/data/redis-cache.js"; @@ -224,9 +224,9 @@ app.get("/api/search", apiAuthMiddleware, (req, res, next) => { req._errorType = if (wantDemo) { const demoResult = getDemoSearchResult(q, { source: req.query.source, condition: req.query.condition }); for (const country of Object.keys(demoResult.activeByCountry || {})) { - demoResult.activeByCountry[country] = demoResult.activeByCountry[country].map(item => ({ + demoResult.activeByCountry[country] = flagPriceOutliers(demoResult.activeByCountry[country].map(item => ({ ...item, detectedCondition: item.detectedCondition || detectCondition(item), - })); + }))); } if (demoResult.sold?.length) recordSoldPrices(q, demoResult.sold, demoResult.source).catch(() => {}); return res.json(demoResult); @@ -278,12 +278,12 @@ app.get("/api/search", apiAuthMiddleware, (req, res, next) => { req._errorType = } } - // Add detected condition to each listing + // Add detected condition + flag outliers for (const country of Object.keys(result.activeByCountry || {})) { - result.activeByCountry[country] = result.activeByCountry[country].map(item => ({ + result.activeByCountry[country] = flagPriceOutliers(result.activeByCountry[country].map(item => ({ ...item, detectedCondition: item.detectedCondition || detectCondition(item), - })); + }))); } // Filter by condition if requested diff --git a/lib/data/demo.js b/lib/data/demo.js index 81f302f..f54cb27 100644 --- a/lib/data/demo.js +++ b/lib/data/demo.js @@ -110,7 +110,7 @@ const DEMO_CARDS = { { itemId: "v1|178048869261|0", itemWebUrl: "https://www.ebay.com/itm/178048869261", title: "Pokemon Indonesia 2025 Umbreon Ex sv8a 217/187 SAR Special Art Rare", - price: 379.90, priceCurrency: "USD", shippingLabel: "$19.00", totalCost: 398.90, condition: "Ungraded", + price: 379.90, priceCurrency: "USD", shippingLabel: "$19.00", totalCost: 398.90, condition: "Ungraded", detectedCondition: "LP", imageUrl: "https://i.ebayimg.com/images/g/dvIAAeSwCB9p3n5z/s-l500.jpg", additionalImages: [{ imageUrl: "https://i.ebayimg.com/images/g/WUIAAeSwmlFp3n58/s-l500.jpg" }], grade: { overall: 7.5, centering: 7.5, corners: 9.0, edges: 9.0, surface: 9.0, confidence: 0.70, mode: "llm-detailed", notes: "Grade limiter: centering — Back centering off, bottom border wider than top ~63/37.", limitations: "", subgradeDetails: { centering: { score: 7.5, confidence: 0.75, detail: "Front ~54/46 acceptable. Back noticeably off — bottom wider than top ~63/37." }, corners: { score: 9.0, confidence: 0.65, detail: "Corners appear clean. No close-up available — reduced confidence." }, edges: { score: 9.0, confidence: 0.62, detail: "Edges look clean from full shots. No close-up detail available." }, surface: { score: 9.0, confidence: 0.78, detail: "Front surface clean, no scratches or print defects visible." } } }, @@ -118,7 +118,7 @@ const DEMO_CARDS = { { itemId: "v1|397899646795|0", itemWebUrl: "https://www.ebay.com/itm/397899646795", title: "Umbreon ex SAR 217/187 SV8a TERASTAL FEST EX Japanese Pokemon TCG", - price: 400, priceCurrency: "USD", shippingLabel: "Free", totalCost: 400, condition: "Ungraded", + price: 400, priceCurrency: "USD", shippingLabel: "Free", totalCost: 400, condition: "Ungraded", detectedCondition: "NM", imageUrl: "https://i.ebayimg.com/images/g/XYkAAeSw8fBp9JS-/s-l500.jpg", additionalImages: [ { imageUrl: "https://i.ebayimg.com/images/g/PcwAAeSw5zhp9JS~/s-l500.jpg" }, @@ -130,7 +130,7 @@ const DEMO_CARDS = { { itemId: "v1|177832326093|0", itemWebUrl: "https://www.ebay.com/itm/177832326093", title: "Umbreon ex SAR 217/187 Terastal Festival sv8a 2024 Pokemon Card Japanese NM", - price: 400, priceCurrency: "USD", shippingLabel: "Free", totalCost: 400, condition: "Ungraded", + price: 400, priceCurrency: "USD", shippingLabel: "Free", totalCost: 400, condition: "Ungraded", detectedCondition: "NM", imageUrl: "https://i.ebayimg.com/images/g/FTcAAeSwiLRpgfeC/s-l500.jpg", additionalImages: [{ imageUrl: "https://i.ebayimg.com/images/g/7NIAAeSw8s9pgfeE/s-l500.jpg" }], grade: { overall: 8.5, centering: 9.0, corners: 9.0, edges: 9.5, surface: 8.5, confidence: 0.74, mode: "llm-detailed", notes: "Grade limiter: surface — Faint holo texture disruption, likely print variation.", limitations: "", subgradeDetails: { centering: { score: 9.0, confidence: 0.76, detail: "Centering solid ~53/47 front. Back appears centered from single photo." }, corners: { score: 9.0, confidence: 0.70, detail: "Corners look clean. Single back photo limits full assessment." }, edges: { score: 9.5, confidence: 0.72, detail: "Edges clean with no whitening visible from available angles." }, surface: { score: 8.5, confidence: 0.78, detail: "Faint holo texture disruption under light. Likely print variation but could cost half a point." } } }, @@ -138,7 +138,7 @@ const DEMO_CARDS = { { itemId: "v1|397643034526|0", itemWebUrl: "https://www.ebay.com/itm/397643034526", title: "With tracking Umbreon ex SAR 217/187 Terastal Festival sv8a 2024 Pokemon Card", - price: 415.31, priceCurrency: "USD", shippingLabel: "Free", totalCost: 415.31, condition: "Ungraded", + price: 415.31, priceCurrency: "USD", shippingLabel: "Free", totalCost: 415.31, condition: "Ungraded", detectedCondition: "NM", imageUrl: "https://i.ebayimg.com/images/g/8fIAAeSwny1pnSPT/s-l500.jpg", additionalImages: [ { imageUrl: "https://i.ebayimg.com/images/g/2iYAAeSwV0hpnSPT/s-l500.jpg" }, @@ -150,7 +150,7 @@ const DEMO_CARDS = { { itemId: "v1|397467499018|0", itemWebUrl: "https://www.ebay.com/itm/397467499018", title: "With tracking Umbreon ex SAR 217/187 Terastal Festival sv8a 2024 Pokemon Card", - price: 425.65, priceCurrency: "USD", shippingLabel: "Free", totalCost: 425.65, condition: "Ungraded", + price: 425.65, priceCurrency: "USD", shippingLabel: "Free", totalCost: 425.65, condition: "Ungraded", detectedCondition: "LP", imageUrl: "https://i.ebayimg.com/images/g/MK8AAeSwqEVpXH5z/s-l500.jpg", additionalImages: [ { imageUrl: "https://i.ebayimg.com/images/g/xrYAAeSwMW1pXH5z/s-l500.jpg" }, diff --git a/public/app.js b/public/app.js index fa08846..4ba4dcb 100644 --- a/public/app.js +++ b/public/app.js @@ -21,6 +21,7 @@ let allItems = []; let allActive = []; let allSold = []; let activeSourceFilter = "all"; +let currentSort = "price-asc"; let currentPsaSignal = null; document.querySelectorAll(".hint").forEach(h => { @@ -43,6 +44,21 @@ document.querySelectorAll(".list-tab").forEach(tab => { }); }); +document.getElementById("sort-select").addEventListener("change", (e) => { + currentSort = e.target.value; + applySourceFilter(); +}); + +function sortItems(items) { + const sorted = [...items]; + if (currentSort === "price-desc") { + sorted.sort((a, b) => (b.totalCost || b.price) - (a.totalCost || a.price)); + } else { + sorted.sort((a, b) => (a.totalCost || a.price) - (b.totalCost || b.price)); + } + return sorted; +} + form.addEventListener("submit", async (e) => { e.preventDefault(); const q = input.value.trim(); @@ -117,15 +133,21 @@ function render(data) { allActive = active; allSold = sold; activeSourceFilter = "all"; + currentSort = "price-asc"; + document.getElementById("sort-select").value = "price-asc"; const isMulti = data.source === "multi"; const sources = isMulti ? detectSources(active, sold) : []; renderSourceFilters(sources); - renderList(activeList, active); - renderList(soldList, sold); + renderList(activeList, sortItems(active)); + renderList(soldList, sortItems(sold)); + const activeTab = document.querySelector('.list-tab[data-tab="active"]'); const soldTab = document.querySelector('.list-tab[data-tab="sold"]'); + activeTab.textContent = `Active (${active.length})`; + soldTab.textContent = `Sold (${sold.length})`; + if (hasGrades && soldTotal === 0) { soldTab.classList.add("hidden"); } else { @@ -187,13 +209,16 @@ function applySourceFilter() { ? () => true : (item) => itemSource(item.itemWebUrl) === activeSourceFilter; - const filteredActive = allActive.filter(filterFn); - const filteredSold = allSold.filter(filterFn); + const filteredActive = sortItems(allActive.filter(filterFn)); + const filteredSold = sortItems(allSold.filter(filterFn)); allItems = [...filteredActive, ...filteredSold]; renderList(activeList, filteredActive); renderList(soldList, filteredSold); + document.querySelector('.list-tab[data-tab="active"]').textContent = `Active (${filteredActive.length})`; + document.querySelector('.list-tab[data-tab="sold"]').textContent = `Sold (${filteredSold.length})`; + detailPanel.innerHTML = '
Click a listing to inspect
'; if (filteredActive.length) selectItem(filteredActive[0].itemId); } @@ -242,7 +267,26 @@ function renderList(container, items) { const price = formatPrice(item.price, item.priceCurrency); const imgSrc = item.imageUrl && !item.imageUrl.includes("placeholder") ? item.imageUrl : ""; const imgHtml = imgSrc ? `` : `
`; - const condition = item.condition ? `${esc(item.condition)}` : ""; + + let displayCond = item.condition || ""; + if (!item.listingGradeLabel && item.detectedCondition) { + displayCond = item.detectedCondition; + } else if (displayCond === "Ungraded" || displayCond === "ungraded") { + displayCond = item.detectedCondition || ""; + } + const useBadge = !item.condition && item.detectedCondition; + const conditionHtml = displayCond + ? `${esc(displayCond)}` + : ""; + + const shippingHtml = item.shippingLabel === "Free" || item.shippingLabel === "free" + ? 'Free shipping' + : item.shippingLabel && item.shippingLabel !== "—" + ? `+ ${esc(item.shippingLabel)}` + : ""; + + const outlierHtml = item._priceOutlier ? 'Price outlier' : ""; + const gradeChip = item.grade && !item.grade.error ? `${item.grade.overall.toFixed(1)}` : item.listingGradeLabel @@ -258,9 +302,11 @@ function renderList(container, items) {
${price} ${gradeChip} + ${shippingHtml} ${srcTag}
- ${condition} + ${conditionHtml} + ${outlierHtml} `; @@ -319,10 +365,14 @@ function selectItem(itemId) { `; const fields = []; - const condVal = item.condition - ? (slabLabel ? `${item.condition} Graded` : item.condition) - : (slabLabel ? `Graded` : ""); - if (condVal) fields.push({ label: "Condition", value: condVal, raw: true }); + if (slabLabel) { + fields.push({ label: "Condition", value: `Graded`, raw: true }); + } else { + const condVal = (item.condition === "Ungraded" || item.condition === "ungraded") + ? (item.detectedCondition || "") + : (item.detectedCondition || item.condition || ""); + if (condVal) fields.push({ label: "Condition", value: condVal }); + } if (item.soldDate || item.endedDate) fields.push({ label: "Sold", value: item.soldDate || item.endedDate }); if (item.priceJPY) fields.push({ label: "Price (JPY)", value: `¥${item.priceJPY.toLocaleString()}` }); if (item.totalCost && item.totalCost !== item.price) fields.push({ label: "Item Price", value: formatPrice(item.price, item.priceCurrency) }); @@ -351,8 +401,8 @@ function selectItem(itemId) { ${summaryHtml}
${fieldsHtml} -
+
${hasGrade ? `` : ""} @@ -365,7 +415,7 @@ function selectItem(itemId) {
@@ -412,14 +462,13 @@ async function loadCardIdentity(query) { ).join(""); const setName = card.setName || ""; - container.innerHTML = ` -
- ${esc(card.cardId)} - ${card.rarity ? `${esc(card.rarity)}` : ""} - ${setName ? `${esc(setName)}` : ""} -
- ${nameHtml ? `
${nameHtml}
` : ""} - `; + const parts = [ + `${esc(card.cardId)}`, + card.rarity ? `${esc(card.rarity)}` : "", + setName ? `${esc(setName)}` : "", + nameHtml ? `${nameHtml}` : "", + ].filter(Boolean).join(""); + container.innerHTML = parts; } catch {} } @@ -437,10 +486,19 @@ async function loadArbitrage(query) { container.classList.remove("hidden"); const arb = data.arbitrage; + const sorted = names.sort((a, b) => sources[a].lowest - sources[b].lowest); + const savingsHtml = arb ? (() => { + const match = arb.summary.match(/\$[\d,.]+/); + const pctMatch = arb.summary.match(/(\d+%)\s*spread/); + const savings = match ? match[0] : ""; + const spread = pctMatch ? pctMatch[1] : ""; + return `
${savings} cheaper on ${esc(arb.cheapest.source)}${spread ? `${spread} spread` : ""}
`; + })() : ""; + container.innerHTML = `
Cross-Source Prices
- ${names.sort((a, b) => sources[a].lowest - sources[b].lowest).map(s => { + ${sorted.map(s => { const d = sources[s]; const isCheapest = arb && s === arb.cheapest.source; return `
@@ -448,10 +506,11 @@ async function loadArbitrage(query) {
${formatPrice(d.lowest, d.currency)}
${d.priceJPY ? `
¥${d.priceJPY.toLocaleString()}
` : ""}
${d.count} listing${d.count !== 1 ? "s" : ""}
+ ${isCheapest ? `Best Price` : ""}
`; }).join("")}
- ${arb ? `
${esc(arb.summary)}
` : ""} + ${savingsHtml} `; } catch {} } @@ -481,7 +540,7 @@ async function loadPriceChart(query) { if (data.stats) { statsEl.innerHTML = ` Low: ${formatPrice(data.stats.min, "USD")} - Avg: ${formatPrice(data.stats.avg, "USD")} + Avg: ${formatPrice(data.stats.avg, "USD")} High: ${formatPrice(data.stats.max, "USD")} ${data.stats.count} sales `; @@ -492,7 +551,7 @@ async function loadPriceChart(query) { function drawPriceChart(canvas, points) { const ctx = canvas.getContext("2d"); const w = canvas.parentElement.clientWidth; - const h = 120; + const h = 140; canvas.width = w; canvas.height = h; canvas.style.width = w + "px"; @@ -502,7 +561,7 @@ function drawPriceChart(canvas, points) { const max = Math.max(...prices) * 1.05; const range = max - min || 1; - const pad = { top: 10, right: 10, bottom: 20, left: 50 }; + const pad = { top: 10, right: 10, bottom: 32, left: 50 }; const cw = w - pad.left - pad.right; const ch = h - pad.top - pad.bottom; @@ -556,6 +615,19 @@ function drawPriceChart(canvas, points) { ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill(); }); + + // X-axis date labels + const fmt = (d) => { const dt = new Date(d); return `${dt.getMonth() + 1}/${dt.getDate()}`; }; + ctx.fillStyle = "rgba(138,138,154,0.6)"; + ctx.font = "10px 'JetBrains Mono', monospace"; + ctx.textAlign = "center"; + const maxLabels = Math.min(points.length, 5); + const step = maxLabels > 1 ? (points.length - 1) / (maxLabels - 1) : 0; + for (let i = 0; i < maxLabels; i++) { + const idx = Math.round(i * step); + const x = pad.left + (idx / (points.length - 1 || 1)) * cw; + ctx.fillText(fmt(points[idx].recordedAt), x, h - 6); + } } function renderPsaInline(psa) { diff --git a/public/index.html b/public/index.html index c484e6c..4580a6f 100644 --- a/public/index.html +++ b/public/index.html @@ -51,6 +51,10 @@

Research any Pokemon card
in seconds

+
diff --git a/public/style.css b/public/style.css index 9bc13f4..db02c7e 100644 --- a/public/style.css +++ b/public/style.css @@ -310,6 +310,22 @@ main { color: var(--gold); background: var(--gold-dim); } +.sort-select { + margin-left: auto; + background: var(--inset); + color: var(--muted); + border: 1px solid var(--border); + border-radius: 6px; + padding: 4px 10px; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + cursor: pointer; + outline: none; + appearance: none; + -webkit-appearance: none; +} +.sort-select:focus { border-color: rgba(217, 182, 118, 0.3); color: var(--text); } +.sort-select option { background: var(--panel); color: var(--text); } .source-filters { display: flex; @@ -651,11 +667,12 @@ main { } .gem-bar-track { flex: 1; - height: 5px; + height: 6px; background: rgba(255,255,255,0.06); border-radius: 3px; overflow: hidden; - max-width: 60px; + min-width: 40px; + max-width: 80px; } .gem-bar-fill { height: 100%; @@ -663,34 +680,59 @@ main { border-radius: 3px; } -.card-identity { display: flex; align-items: center; gap: 6px; margin-left: auto; } -.card-id-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } +.card-identity { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + background: var(--inset); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 14px; + margin-bottom: 14px; +} .card-id-badge { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--gold); background: var(--gold-dim); padding: 3px 8px; border-radius: 4px; font-weight: 600; } -.card-id-rarity { font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 11px; font-weight: 700; color: var(--text); background: var(--inset); border: 1px solid var(--border); padding: 2px 6px; border-radius: 3px; } +.card-id-rarity { font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 11px; font-weight: 700; color: var(--text); background: var(--panel); border: 1px solid var(--border); padding: 2px 6px; border-radius: 3px; } .card-id-set { font-size: 11px; color: var(--muted); } -.card-id-num { font-size: 11px; color: var(--muted); font-family: 'JetBrains Mono', monospace; } -.card-id-names { margin-top: 4px; display: flex; gap: 10px; } .card-id-name { font-size: 11px; color: var(--text); } +.card-id-sep { width: 1px; height: 14px; background: var(--border); flex-shrink: 0; } .card-id-lang { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin-right: 2px; } -.arbitrage-container { background: var(--inset); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; margin-bottom: 12px; } +.arbitrage-container { background: var(--inset); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 14px; } .arbitrage-sources { display: flex; gap: 8px; margin-top: 8px; } .arb-source { flex: 1; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 10px; text-align: center; } -.arb-source.arb-cheapest { border-color: var(--green); background: rgba(124, 224, 168, 0.05); } +.arb-source.arb-cheapest { border-color: var(--green); background: rgba(124, 224, 168, 0.08); } +.arb-best-chip { font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--green); background: rgba(124, 224, 168, 0.12); border: 1px solid rgba(124, 224, 168, 0.3); padding: 2px 7px; border-radius: 3px; margin-top: 6px; display: inline-block; } .arb-source-name { font-family: 'JetBrains Mono', monospace; font-size: 10px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; } .arb-cheapest .arb-source-name { color: var(--green); } .arb-source-price { font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 18px; font-weight: 700; color: var(--text); } .arb-cheapest .arb-source-price { color: var(--green); } .arb-source-jpy { font-size: 10px; color: var(--muted); } .arb-source-count { font-size: 10px; color: var(--muted); margin-top: 2px; } -.arb-summary { margin-top: 8px; font-size: 11px; color: var(--gold); text-align: center; } +.arb-summary { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border); + font-family: 'Space Grotesk', system-ui, sans-serif; + font-size: 13px; + font-weight: 600; + color: var(--green); + text-align: center; +} +.arb-summary-spread { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 500; + color: var(--muted); + margin-left: 6px; +} .price-chart-container { background: var(--inset); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; - margin-bottom: 12px; + margin-bottom: 14px; } .price-chart-container canvas { width: 100%; @@ -699,7 +741,9 @@ main { .price-chart-stats { display: flex; gap: 16px; - margin-top: 8px; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border); font-size: 11px; color: var(--muted); } @@ -707,24 +751,31 @@ main { color: var(--text); font-family: 'Space Grotesk', system-ui, sans-serif; } +.price-chart-stats .stat-avg b { + color: var(--gold); + font-size: 13px; +} .detail-actions { display: flex; gap: 10px; - margin-top: 4px; + margin-top: 8px; } .detail-actions a { - display: inline-block; + display: inline-flex; + align-items: center; + gap: 6px; padding: 8px 16px; - background: var(--gold); - color: var(--bg); + background: transparent; + color: var(--gold); + border: 1px solid rgba(217, 182, 118, 0.25); border-radius: 6px; font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 13px; - font-weight: 600; - transition: opacity 0.2s; + font-weight: 500; + transition: all 0.2s; } -.detail-actions a:hover { opacity: 0.85; text-decoration: none; } +.detail-actions a:hover { border-color: var(--gold); background: rgba(217, 182, 118, 0.06); text-decoration: none; } .listing-card .thumb { width: 72px; @@ -774,6 +825,33 @@ main { font-size: 12px; color: var(--muted); } +.condition-badge { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); + background: var(--inset); + border: 1px solid var(--border); + padding: 1px 6px; + border-radius: 3px; +} +.price-outlier { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--red); + background: rgba(255, 93, 93, 0.08); + border: 1px solid rgba(255, 93, 93, 0.25); + padding: 1px 6px; + border-radius: 3px; + display: inline-block; + margin-top: 4px; +} +.shipping-free { color: var(--green); } .listing-card .sold-date { font-size: 11px;