import { t, i18n } from '../../i18n.js'
import { directus } from '../../services/directus.js'
import { auth } from '../../services/auth.js'
import { favoritesService } from '../../services/favorites.js'
import { getXmrRates, formatPrice as formatCurrencyPrice } from '../../services/currency.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../chat-widget.js'
import '../location-map.js'
import '../listing-card.js'
import { categoriesService } from '../../services/categories.js'
import { reputationService } from '../../services/reputation.js'
import { generatePseudonym, generateAvatar } from '../../services/identity.js'
class PageListing extends HTMLElement {
constructor() {
super()
this.listing = null
this.sellerListings = []
this.loading = true
this.isFavorite = false
this.rates = null
this.isOwner = false
this.sellerReputation = null
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
}
connectedCallback() {
this.listingId = this.dataset.id
this._unsubs = []
this.render()
this.loadListing()
this._unsubs.push(i18n.subscribe(() => this.render()))
window.addEventListener('currency-changed', this.handleCurrencyChange)
}
disconnectedCallback() {
this._unsubs.forEach(fn => fn())
this._unsubs = []
window.removeEventListener('currency-changed', this.handleCurrencyChange)
this.resetMetaTags()
}
handleCurrencyChange() {
this.render()
}
async loadListing() {
try {
this.listing = await directus.getListing(this.listingId)
this.rates = await getXmrRates()
this.loadFavoriteState()
// Check if current user is owner
await this.checkOwnership()
// Increment view counter only if not owner
if (!this.isOwner) {
const newViews = await directus.incrementListingViews(this.listingId)
if (newViews !== null) {
this.listing.views = newViews
}
}
// Load other listings from same seller
if (this.listing?.user_created) {
await this.loadSellerListings()
}
await this.loadSellerReputation()
} catch (e) {
console.error('Failed to load listing:', e)
this.listing = null
}
this.loading = false
this.render()
this.setupEventListeners()
this.updateMetaTags()
if (this.dataset.chat && auth.isLoggedIn()) {
const chatWidget = this.querySelector('chat-widget')
if (this.dataset.chat !== '1') {
chatWidget?.setAttribute('conversation-id', this.dataset.chat)
}
const dialog = this.querySelector('#contact-dialog')
dialog?.showModal()
chatWidget?.activate()
}
}
updateMetaTags() {
if (!this.listing) return
const title = `${this.listing.title} โ kashilo.com`
const description = (this.listing.description || '').substring(0, 160)
const imageId = this.listing.images?.[0]?.directus_files_id?.id || this.listing.images?.[0]?.directus_files_id
const imageUrl = imageId ? directus.getFileUrl(imageId, { width: 1200, height: 630, fit: 'cover' }) : 'https://kashilo.com/assets/press/og-image.png'
const url = `https://kashilo.com/#/listing/${this.listing.id}`
document.title = title
this._setMeta('description', description)
this._setMeta('og:title', title, true)
this._setMeta('og:description', description, true)
this._setMeta('og:image', imageUrl, true)
this._setMeta('og:url', url, true)
this._setMeta('og:type', 'product', true)
this._setMeta('twitter:title', title)
this._setMeta('twitter:description', description)
this._setMeta('twitter:image', imageUrl)
}
resetMetaTags() {
const defaultTitle = 'kashilo.com โ Private Classifieds with Monero'
const defaultDesc = 'Buy and sell privately with Monero. No account needed, E2E encrypted chat.'
const defaultImage = 'https://kashilo.com/assets/press/og-image.png'
document.title = defaultTitle
this._setMeta('description', defaultDesc)
this._setMeta('og:title', defaultTitle, true)
this._setMeta('og:description', defaultDesc, true)
this._setMeta('og:image', defaultImage, true)
this._setMeta('og:url', 'https://kashilo.com', true)
this._setMeta('og:type', 'website', true)
this._setMeta('twitter:title', defaultTitle)
this._setMeta('twitter:description', defaultDesc)
this._setMeta('twitter:image', defaultImage)
}
_setMeta(name, content, isProperty = false) {
const attr = isProperty ? 'property' : 'name'
let el = document.querySelector(`meta[${attr}="${name}"]`)
if (el) {
el.setAttribute('content', content)
} else {
el = document.createElement('meta')
el.setAttribute(attr, name)
el.setAttribute('content', content)
document.head.appendChild(el)
}
}
async checkOwnership() {
if (!auth.isLoggedIn() || !this.listing?.user_created) {
this.isOwner = false
return
}
try {
const user = await auth.getUser()
this.isOwner = user?.id === this.listing.user_created
} catch (e) {
this.isOwner = false
}
}
async loadSellerListings() {
try {
const response = await directus.getListings({
filter: {
status: { _eq: 'published' },
user_created: { _eq: this.listing.user_created },
id: { _neq: this.listing.id }
},
limit: 4
})
this.sellerListings = response.items || []
} catch (e) {
console.error('Failed to load seller listings:', e)
this.sellerListings = []
}
}
async loadSellerReputation() {
if (!this.listing?.id || !auth.isLoggedIn()) return
try {
const convsResponse = await directus.get('/items/conversations', {
filter: { listing_id: { _eq: this.listing.id } },
fields: ['participant_hash_2'],
limit: 1
})
const conv = (convsResponse.data || [])[0]
if (conv?.participant_hash_2) {
this.sellerReputation = await reputationService.getReputation(conv.participant_hash_2)
}
} catch (e) {
// No conversations yet = no reputation to show
}
}
loadFavoriteState() {
this.isFavorite = favoritesService.isFavorite(this.listingId)
}
toggleFavorite() {
favoritesService.toggle(this.listingId)
this.isFavorite = !this.isFavorite
const btn = this.querySelector('#favorite-btn')
btn?.classList.toggle('active', this.isFavorite)
btn.innerHTML = this.getFavoriteIcon()
}
async copyShareLink() {
const url = window.location.href
try {
await navigator.clipboard.writeText(url)
const btn = this.querySelector('#share-btn')
btn?.classList.add('copied')
setTimeout(() => btn?.classList.remove('copied'), 2000)
} catch (e) {
console.error('Failed to copy:', e)
}
}
render() {
if (this.loading) {
this.innerHTML = /* html */`
`
return
}
if (!this.listing) {
this.innerHTML = /* html */`
`
return
}
if (this.listing.status !== 'published' && !this.isOwner) {
this.innerHTML = /* html */`
`
return
}
const images = (this.listing.images || []).slice(0, 5)
const hasImages = images.length > 0
const firstImage = hasImages ? this.getImageUrl(images[0]) : null
this.allImages = images
const categoryName = this.listing.category
? categoriesService.getTranslatedName(this.listing.category)
: ''
const priceInfo = this.getFormattedPrice()
const createdDate = this.listing.date_created
? new Date(this.listing.date_created).toLocaleDateString()
: ''
this.innerHTML = /* html */`
${firstImage
? `

`
: this.getPlaceholderSvg()}
${images.length > 1 ? `
${images.map((img, i) => `
`).join('')}
` : ''}
${this.formatDescription(this.listing.description)}
${this.renderVerificationSection()}
${this.listing.location ? `
` : ''}
${this.sellerListings.length > 0 ? `
${t('listing.moreFromSeller')}
${this.sellerListings.map(listing => this.renderListingCard(listing)).join('')}
` : ''}
${this.renderContactDialog()}
`
}
renderSidebar() {
// Owner view: show edit button instead of contact
if (this.isOwner) {
const paymentProcessing = this.listing?.payment_status === 'processing'
const paymentPending = this.listing?.status === 'draft' && this.listing?.payment_status !== 'paid'
return /* html */`
${paymentProcessing ? `
` : ''}
`
}
return /* html */`
`
}
renderSellerReputation() {
if (!this.sellerReputation) return ''
const rep = this.sellerReputation
const levelInfo = reputationService.getLevelInfo(rep.level)
const dealCount = rep.deals_completed
const dealText = dealCount === 1 ? t('reputation.dealsSingular') : t('reputation.deals', { count: dealCount })
let ratingHtml = ''
if (rep.avg_rating > 0) {
const stars = 'โ
'.repeat(Math.round(rep.avg_rating)) + 'โ'.repeat(5 - Math.round(rep.avg_rating))
ratingHtml = /* html */`${stars}`
}
const showWarning = rep.level === 'new'
return /* html */`
${levelInfo.badge}
${t(levelInfo.i18nKey)}
${dealCount > 0 ? `ยท ${dealText}` : ''}
${ratingHtml}
${showWarning ? /* html */`
${t('reputation.newWarning')}
` : ''}
`
}
renderVerificationSection() {
if (!this.listing?.verified || !this.listing?.verification_image) return ''
const imageId = typeof this.listing.verification_image === 'object'
? this.listing.verification_image.id
: this.listing.verification_image
const imageUrl = directus.getFileUrl(imageId)
const code = this.listing.verification_code || ''
const date = this.listing.verification_date
? new Date(this.listing.verification_date).toLocaleDateString()
: ''
return /* html */`
${t('verification.verified')}
${t('verification.proofHint')}
${t('verification.proofCode')}
${escapeHTML(code)}
${date ? `
${t('verification.verifiedDate', { date })}
` : ''}
`
}
renderListingCard(listing) {
const imageId = listing.images?.[0]?.directus_files_id?.id || listing.images?.[0]?.directus_files_id
const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 180) : ''
const locationName = listing.location?.name || ''
return /* html */`
`
}
renderContactDialog() {
return /* html */`
`
}
getFavoriteIcon() {
return this.isFavorite
? ``
: ``
}
getPlaceholderSvg() {
return /* html */`
`
}
setupEventListeners() {
// Contact dialog
const dialog = this.querySelector('#contact-dialog')
const closeBtn = this.querySelector('#dialog-close')
this.querySelectorAll('#contact-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (!auth.isLoggedIn()) {
document.querySelector('auth-modal')?.show()
return
}
dialog?.showModal()
this.querySelector('chat-widget')?.activate()
})
})
closeBtn?.addEventListener('click', () => dialog?.close())
dialog?.addEventListener('click', (e) => {
if (e.target === dialog) dialog.close()
})
// Dialog tabs
this.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab
this.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'))
btn.classList.add('active')
this.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('hidden', content.id !== `tab-${tab}`)
})
})
})
// Copy Monero address
this.querySelector('#copy-addr-btn')?.addEventListener('click', async () => {
const addr = this.querySelector('#monero-addr')?.textContent
if (addr) {
await navigator.clipboard.writeText(addr)
const btn = this.querySelector('#copy-addr-btn')
btn?.classList.add('copied')
setTimeout(() => btn?.classList.remove('copied'), 2000)
}
})
// Favorite button
this.querySelector('#favorite-btn')?.addEventListener('click', () => this.toggleFavorite())
// Share button
this.querySelector('#share-btn')?.addEventListener('click', () => this.copyShareLink())
// Thumbnail gallery
this.querySelectorAll('.thumbnail').forEach(thumb => {
thumb.addEventListener('click', () => {
const index = parseInt(thumb.dataset.index)
const mainImg = this.querySelector('#main-img')
if (mainImg && this.allImages[index]) {
mainImg.src = this.getImageUrl(this.allImages[index])
this.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'))
thumb.classList.add('active')
}
})
})
}
getImageUrl(image, size = null) {
const fileId = image?.directus_files_id?.id || image?.directus_files_id || image
if (!fileId) return null
return size
? directus.getThumbnailUrl(fileId, size)
: directus.getFileUrl(fileId)
}
formatPrice(price, currency = 'EUR') {
if (!price && price !== 0) return t('listing.priceOnRequest')
if (currency === 'XMR') {
return `${parseFloat(price).toFixed(4)} XMR`
}
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: currency
}).format(price)
}
getFormattedPrice() {
const { price, currency } = this.listing
if (!price && price !== 0) {
return { primary: t('listing.priceOnRequest'), secondary: null }
}
if (this.rates) {
const listing = {
price: parseFloat(price),
currency,
price_mode: currency === 'XMR' ? 'xmr' : 'fiat'
}
return formatCurrencyPrice(listing, this.rates)
}
return {
primary: this.formatPrice(price, currency),
secondary: null
}
}
getConditionLabel(condition) {
const labels = {
new: t('create.conditionNew'),
like_new: t('create.conditionLikeNew'),
good: t('create.conditionGood'),
fair: t('create.conditionFair'),
poor: t('create.conditionPoor')
}
return labels[condition] || condition
}
formatExpiresAt(expiresAt) {
const expires = new Date(expiresAt)
const now = new Date()
const diffMs = expires - now
if (diffMs <= 0) {
return `โฑ ${t('listing.expired')}`
}
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24))
if (diffDays === 1) {
return `โฑ ${t('listing.expiresIn1Day')}`
}
return `โฑ ${t('listing.expiresInDays', { days: diffDays })}`
}
formatViews(count) {
if (count === 1) {
return `1 ${t('listing.viewSingular')}`
}
return `${count} ${t('listing.viewPlural')}`
}
formatDescription(text) {
if (!text) return ''
return escapeHTML(text).replace(/\n/g, '
')
}
}
customElements.define('page-listing', PageListing)
const style = document.createElement('style')
style.textContent = /* css */`
page-listing .listing-detail {
padding: var(--space-lg) 0;
}
/* Gallery - Full Width */
page-listing .listing-gallery {
background: var(--color-bg-secondary);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: var(--space-xl);
}
page-listing .listing-image-main {
display: block;
width: 100%;
overflow: hidden;
border-radius: var(--radius-md);
}
page-listing .listing-image-main img {
display: block;
width: 100%;
height: auto;
border-radius: var(--radius-md);
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
page-listing .listing-image-main:hover img {
transform: scale(1.08);
}
page-listing .listing-image-main .placeholder-icon {
width: 80px;
height: 80px;
color: var(--color-border);
}
page-listing .listing-thumbnails {
display: flex;
gap: var(--space-xs);
padding: var(--space-sm);
overflow-x: auto;
}
page-listing .thumbnail {
flex-shrink: 0;
width: 80px;
height: 80px;
border: 2px solid transparent;
border-radius: var(--radius-md);
overflow: hidden;
cursor: pointer;
padding: 0;
background: var(--color-bg-tertiary);
}
page-listing .thumbnail.active {
border-color: var(--color-primary);
}
page-listing .thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Two Column Layout */
page-listing .listing-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: var(--space-xl);
align-items: start;
}
page-listing .listing-main {
min-width: 0;
}
/* Mobile: Hide desktop sidebar, show mobile elements */
page-listing .sidebar-mobile {
display: none;
}
page-listing .location-mobile {
display: none;
}
@media (max-width: 768px) {
page-listing .listing-layout {
grid-template-columns: 1fr;
}
page-listing .listing-sidebar.sidebar-desktop {
display: none;
}
page-listing .sidebar-mobile {
display: flex;
margin-bottom: var(--space-xl);
}
page-listing .location-mobile {
display: block;
}
page-listing .sidebar-card {
padding: var(--space-md);
}
}
page-listing .listing-header {
margin-bottom: var(--space-xl);
}
page-listing .listing-header h1 {
margin: var(--space-sm) 0;
font-size: var(--font-size-2xl);
}
page-listing > .listing-detail .listing-header .listing-price-wrapper {
margin: var(--space-sm) 0;
}
page-listing > .listing-detail .listing-header .listing-price-wrapper > .listing-price {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
margin: 0;
}
page-listing > .listing-detail .listing-header .listing-price-wrapper > .listing-price-secondary {
font-size: var(--font-size-base);
color: var(--color-text-muted);
margin: var(--space-xs) 0 0;
}
page-listing .listing-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
margin-top: var(--space-md);
}
page-listing .meta-item {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
background: var(--color-bg-secondary);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
}
page-listing .verified-badge {
background: var(--color-success);
color: #fff;
font-weight: var(--font-weight-medium);
}
page-listing .meta-date {
margin-left: auto;
}
page-listing .views-icon {
filter: grayscale(1);
}
page-listing .listing-description {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-lg);
margin-bottom: var(--space-xl);
}
page-listing .verification-section {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-lg);
margin-bottom: var(--space-xl);
}
page-listing .verification-hint {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0 0 var(--space-md);
}
page-listing .verification-proof {
display: flex;
gap: var(--space-lg);
align-items: flex-start;
}
page-listing .verification-proof-image {
flex-shrink: 0;
width: 200px;
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
}
page-listing .verification-proof-image img {
display: block;
width: 100%;
height: auto;
cursor: zoom-in;
}
page-listing .verification-proof-info {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
page-listing .verification-proof-label {
display: block;
font-size: var(--font-size-xs);
color: var(--color-text-muted);
margin-bottom: var(--space-xs);
}
page-listing .verification-proof-value {
font-family: monospace;
font-size: var(--font-size-2xl);
font-weight: 700;
letter-spacing: 0.3em;
color: var(--color-success);
}
page-listing .verification-proof-date {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
@media (max-width: 768px) {
page-listing .verification-proof {
flex-direction: column;
}
page-listing .verification-proof-image {
width: 100%;
}
}
page-listing .listing-location-section {
margin-bottom: var(--space-xl);
}
page-listing .listing-description h2,
page-listing .verification-section h2,
page-listing .listing-location-section h2 {
font-size: var(--font-size-lg);
margin-bottom: var(--space-md);
}
page-listing .description-text {
line-height: 1.6;
color: var(--color-text-secondary);
}
/* Sidebar */
page-listing .listing-sidebar {
display: flex;
flex-direction: column;
gap: var(--space-md);
position: sticky;
top: calc(3 * var(--space-xl));
}
page-listing .sidebar-mobile {
flex-direction: column;
gap: var(--space-md);
}
page-listing .sidebar-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-lg);
}
page-listing .sidebar-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
}
page-listing .sidebar-btn + .sidebar-btn {
margin-top: var(--space-sm);
}
page-listing .sidebar-actions {
display: flex;
flex-direction: column;
gap: var(--space-xs);
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border);
}
page-listing .action-btn {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: transparent;
border: none;
border-radius: var(--radius-md);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
font-size: var(--font-size-sm);
}
page-listing .action-btn:hover {
background: var(--color-bg-tertiary);
color: var(--color-text);
}
page-listing .action-btn.active {
color: var(--color-accent);
background: var(--color-bg-tertiary);
}
page-listing .action-btn.copied {
color: var(--color-success);
}
/* Seller Card */
page-listing .seller-header {
display: flex;
align-items: center;
gap: var(--space-md);
}
page-listing .seller-avatar {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
page-listing .seller-avatar svg {
width: 48px;
height: 48px;
display: block;
}
page-listing .seller-info {
display: flex;
flex-direction: column;
}
page-listing .seller-info span {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
page-listing .seller-auto-label {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
font-style: italic;
}
page-listing .seller-reputation {
padding: var(--space-sm) 0 0;
margin-top: var(--space-sm);
border-top: 1px solid var(--color-border);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
page-listing .seller-level {
display: flex;
align-items: center;
gap: var(--space-xs);
font-size: var(--font-size-sm);
}
page-listing .level-badge {
font-size: var(--font-size-base);
}
page-listing .level-name {
color: var(--color-text-secondary);
font-weight: var(--font-weight-medium);
}
page-listing .deal-count {
color: var(--color-text-muted);
}
page-listing .seller-rating {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
letter-spacing: 1px;
}
page-listing .seller-warning {
margin-top: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
color: var(--color-text-muted);
line-height: 1.5;
}
/* Seller Listings */
page-listing .seller-listings {
margin-top: var(--space-3xl);
padding-top: var(--space-xl);
border-top: 1px solid var(--color-border);
}
page-listing .seller-listings h2 {
margin-bottom: var(--space-lg);
}
page-listing .seller-listings-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
}
@media (max-width: 768px) {
page-listing .seller-listings-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Dialog */
page-listing .contact-dialog {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-xl);
max-width: 500px;
width: calc(100% - 2 * var(--space-md));
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
}
page-listing .contact-dialog::backdrop {
background: var(--color-overlay);
}
page-listing .dialog-tabs {
display: flex;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-sm);
}
page-listing .tab-btn {
padding: var(--space-sm) var(--space-md);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-muted);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
background: transparent;
border: none;
cursor: pointer;
}
page-listing .tab-btn:hover {
color: var(--color-text);
background: var(--color-bg-secondary);
}
page-listing .tab-btn.active {
color: var(--color-primary);
background: var(--color-primary-light);
}
page-listing .tab-content.hidden {
display: none;
}
page-listing .dialog-close {
position: absolute;
top: var(--space-md);
right: var(--space-md);
padding: var(--space-xs);
color: var(--color-text-muted);
background: transparent;
border: none;
cursor: pointer;
}
page-listing .dialog-close:hover {
color: var(--color-text);
}
page-listing .dialog-subtitle {
color: var(--color-text-secondary);
margin-bottom: var(--space-lg);
}
page-listing .monero-section {
margin-bottom: var(--space-lg);
}
page-listing .monero-section label {
display: block;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
margin-bottom: var(--space-sm);
color: var(--color-text-secondary);
}
page-listing .monero-address {
display: flex;
gap: var(--space-sm);
align-items: stretch;
}
page-listing .monero-address code {
flex: 1;
padding: var(--space-sm) var(--space-md);
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
word-break: break-all;
line-height: 1.4;
}
page-listing .btn-copy {
padding: var(--space-sm);
flex-shrink: 0;
}
page-listing .btn-copy.copied {
background: var(--color-success);
border-color: var(--color-success);
color: white;
}
page-listing .dialog-hint {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
text-align: center;
}
/* Payment Processing Badge (sidebar) */
page-listing .payment-processing-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-warning, #e6a700);
}
page-listing .processing-badge {
display: flex;
align-items: center;
gap: var(--space-sm);
color: var(--color-warning, #e6a700);
}
page-listing .processing-hint {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin-top: var(--space-sm);
line-height: 1.5;
}
/* Loading & Empty States */
page-listing .loading {
display: flex;
justify-content: center;
padding: var(--space-3xl);
}
page-listing .empty-state {
text-align: center;
padding: var(--space-3xl);
}
page-listing .empty-state-icon {
font-size: 3rem;
margin-bottom: var(--space-md);
}
`
document.head.appendChild(style)