/** * — Public shareable group buy page. * * Attributes: space, buy-id * Fetches GET /api/group-buys/:id, polls every 10s. * Shows product hero, tier progress, pledge panel. * Demo mode when buyId starts with 'demo-'. */ class FolkGroupBuyPage extends HTMLElement { private shadow: ShadowRoot; private space = 'default'; private buyId = ''; private data: any = null; private loading = true; private error = ''; private pollTimer: ReturnType | null = null; private pledgeQty = 1; private pledgeName = ''; private pledging = false; private pledged = false; private simQty = 0; // "What If" simulator quantity (0 = off) private commonsSharePct = 20; // % of revenue to commons constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); } connectedCallback() { this.space = this.getAttribute('space') || 'default'; this.buyId = this.getAttribute('buy-id') || ''; this.loadData(); this.startPolling(); } disconnectedCallback() { this.stopPolling(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^(\/[^/]+)?\/rcart/); return match ? match[0] : '/rcart'; } private async loadData() { this.loading = true; this.render(); if (this.buyId.startsWith('demo-')) { this.data = this.getDemoData(); this.loading = false; this.render(); return; } try { const res = await fetch(`${this.getApiBase()}/api/group-buys/${this.buyId}`); if (!res.ok) throw new Error('Group buy not found'); this.data = await res.json(); } catch (e) { this.error = e instanceof Error ? e.message : 'Failed to load group buy'; } this.loading = false; this.render(); } private startPolling() { this.pollTimer = setInterval(async () => { if (!this.buyId || this.buyId.startsWith('demo-')) return; try { const res = await fetch(`${this.getApiBase()}/api/group-buys/${this.buyId}`); if (res.ok) { const updated = await res.json(); if (updated.totalPledged !== this.data?.totalPledged) { this.data = updated; this.render(); } } } catch { /* silent poll fail */ } }, 10000); } private stopPolling() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } } private getDemoData() { return { id: this.buyId, title: 'Cosmolocal Network Tee', productType: 'tee', imageUrl: '/images/catalog/catalog-cosmolocal-tee.jpg', description: 'Bella+Canvas 3001 tee with the Cosmolocal Network radial design in teal and coral.', tiers: [ { min_qty: 1, per_unit: 25, currency: 'USD' }, { min_qty: 10, per_unit: 21.25, currency: 'USD' }, { min_qty: 25, per_unit: 18, currency: 'USD' }, { min_qty: 50, per_unit: 15, currency: 'USD' }, ], status: 'OPEN', totalPledged: 17, currentTier: { min_qty: 10, per_unit: 21.25, currency: 'USD' }, pledges: [ { id: 'p1', displayName: 'Alice', quantity: 5, pledgedAt: new Date(Date.now() - 86400000 * 3).toISOString() }, { id: 'p2', displayName: 'Bob', quantity: 3, pledgedAt: new Date(Date.now() - 86400000 * 2).toISOString() }, { id: 'p3', displayName: 'Cooperative Hub', quantity: 6, pledgedAt: new Date(Date.now() - 86400000).toISOString() }, { id: 'p4', displayName: 'Dana', quantity: 3, pledgedAt: new Date(Date.now() - 3600000).toISOString() }, ], closesAt: new Date(Date.now() + 86400000 * 14).toISOString(), createdAt: new Date(Date.now() - 86400000 * 5).toISOString(), }; } private async submitPledge() { if (this.pledging || this.pledgeQty < 1) return; if (this.buyId.startsWith('demo-')) { this.data.totalPledged += this.pledgeQty; this.data.pledges.push({ id: `p-${Date.now()}`, displayName: this.pledgeName || 'Anonymous', quantity: this.pledgeQty, pledgedAt: new Date().toISOString(), }); // Recalculate current tier const tiers = this.data.tiers; this.data.currentTier = [...tiers].reverse().find((t: any) => this.data.totalPledged >= t.min_qty) || tiers[0]; this.pledged = true; this.render(); return; } this.pledging = true; this.render(); try { const res = await fetch(`${this.getApiBase()}/api/group-buys/${this.buyId}/pledge`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(localStorage.getItem('encryptid-token') ? { 'Authorization': `Bearer ${localStorage.getItem('encryptid-token')}` } : {}), }, body: JSON.stringify({ quantity: this.pledgeQty, displayName: this.pledgeName || 'Anonymous' }), }); if (res.ok) { this.pledged = true; await this.loadData(); } } catch (e) { console.error('Failed to pledge:', e); } this.pledging = false; this.render(); } private render() { const styles = this.getStyles(); if (this.loading) { this.shadow.innerHTML = `
Loading group buy...
`; return; } if (this.error) { this.shadow.innerHTML = `
${this.esc(this.error)}
`; return; } const d = this.data; if (!d) return; const nextTier = d.tiers.find((t: any) => t.min_qty > d.totalPledged); const nextTierQty = nextTier ? nextTier.min_qty : d.tiers[d.tiers.length - 1]?.min_qty || 0; const progressPct = nextTier ? Math.min(100, Math.round((d.totalPledged / nextTier.min_qty) * 100)) : 100; const remaining = nextTier ? nextTier.min_qty - d.totalPledged : 0; const closesDate = new Date(d.closesAt); const daysLeft = Math.max(0, Math.ceil((closesDate.getTime() - Date.now()) / 86400000)); // "What If" simulator calculations const simTotal = this.simQty > 0 ? d.totalPledged + this.simQty : d.totalPledged; const simTier = [...d.tiers].reverse().find((t: any) => simTotal >= t.min_qty) || d.tiers[0]; const simNextTier = d.tiers.find((t: any) => t.min_qty > simTotal); const simProgressPct = simNextTier ? Math.min(100, Math.round((simTotal / simNextTier.min_qty) * 100)) : 100; const maxTierQty = d.tiers[d.tiers.length - 1]?.min_qty || 50; // Revenue & commons calculations const currentRevenue = d.totalPledged * (d.currentTier?.per_unit || d.tiers[0]?.per_unit || 0); const simRevenue = simTotal * simTier.per_unit; const commonsRevenue = simRevenue * (this.commonsSharePct / 100); const currentCommons = currentRevenue * (this.commonsSharePct / 100); // Overall progress toward max tier (for the fill-up visual) const overallPct = Math.min(100, Math.round((d.totalPledged / maxTierQty) * 100)); // People SVG icons for social proof const peopleCount = d.pledges?.length || 0; this.shadow.innerHTML = `
${d.imageUrl ? `${this.esc(d.title)}` : ''}

${this.esc(d.title)}

${d.productType ? `${this.esc(d.productType)}` : ''} ${d.status}
${d.description ? `

${this.esc(d.description)}

` : ''}
${d.totalPledged}
pledged
${peopleCount}
backers
${daysLeft}
days left
$${(d.currentTier?.per_unit || d.tiers[0]?.per_unit || 0).toFixed(2)}
per unit
${d.totalPledged} / ${maxTierQty}
${d.tiers.map((t: any) => { const tierPct = Math.round((t.min_qty / maxTierQty) * 100); return `
${t.min_qty}+
`; }).join('')}
${Array.from({length: Math.min(peopleCount, 8)}, (_, i) => `` ).join('')} ${peopleCount > 8 ? `+${peopleCount - 8}` : ''}

Volume Pricing Tiers

${d.tiers.map((t: any, i: number) => { const active = d.currentTier && t.min_qty === d.currentTier.min_qty; const reached = d.totalPledged >= t.min_qty; const simReached = simTotal >= t.min_qty; const simActive = this.simQty > 0 && simTier && t.min_qty === simTier.min_qty; const savings = i > 0 ? Math.round((1 - t.per_unit / d.tiers[0].per_unit) * 100) : 0; const tierRevenue = t.min_qty * t.per_unit; const tierCommons = tierRevenue * (this.commonsSharePct / 100); return `
${t.min_qty}+ $${t.per_unit.toFixed(2)}/ea ${savings > 0 ? `-${savings}%` : `base`} $${tierCommons.toFixed(0)} commons ${reached ? `` : this.simQty > 0 && simReached ? `` : ``}
`; }).join('')}
${nextTier ? `
${remaining} more to unlock $${nextTier.per_unit.toFixed(2)}/ea ${d.totalPledged} / ${nextTierQty}
` : `
🎉 Best tier unlocked!
`}

What If...?

Explore how more pledges unlock better pricing and grow the commons.

${this.simQty > 0 ? `
Total pledged ${d.totalPledged} + ${this.simQty} = ${simTotal}
Tier unlocked $${simTier.per_unit.toFixed(2)}/ea${simTier.per_unit < (d.currentTier?.per_unit || d.tiers[0].per_unit) ? ` (better!)` : ''}
Total revenue $${simRevenue.toFixed(2)}
${simNextTier ? `
Next unlock ${simNextTier.min_qty - simTotal} more for $${simNextTier.per_unit.toFixed(2)}/ea
` : `
🎉 Best tier unlocked!
`}
` : ''}

Commons Revenue

${this.commonsSharePct}%
$${currentCommons.toFixed(2)}
Current
${this.simQty > 0 ? '→' : ''}
${this.simQty > 0 ? `
$${commonsRevenue.toFixed(2)}
Projected
+$${(commonsRevenue - currentCommons).toFixed(2)}
Growth
` : ''}

Pledges (${d.pledges.length})

${d.pledges.map((p: any) => `
${this.esc(p.displayName.charAt(0).toUpperCase())} ${this.esc(p.displayName)} ${p.quantity} unit${p.quantity > 1 ? 's' : ''}
`).join('')}

Join this Group Buy

${this.pledged ? `

Pledge submitted! Share this link to bring more people in.

` : `
${d.currentTier ? `
$${(d.currentTier.per_unit * this.pledgeQty).toFixed(2)}
at current tier ($${d.currentTier.per_unit.toFixed(2)}/ea)
` : ''}

No payment collected now. You'll be notified when the group buy closes.

`}
`; this.bindEvents(); } private bindEvents() { this.shadow.querySelector("[data-action='qty-dec']")?.addEventListener("click", () => { if (this.pledgeQty > 1) { this.pledgeQty--; this.render(); } }); this.shadow.querySelector("[data-action='qty-inc']")?.addEventListener("click", () => { this.pledgeQty++; this.render(); }); this.shadow.querySelector("[data-field='pledge-qty']")?.addEventListener("change", (e) => { this.pledgeQty = Math.max(1, parseInt((e.target as HTMLInputElement).value) || 1); this.render(); }); this.shadow.querySelector("[data-field='pledge-name']")?.addEventListener("input", (e) => { this.pledgeName = (e.target as HTMLInputElement).value; }); this.shadow.querySelector("[data-action='submit-pledge']")?.addEventListener("click", () => { this.submitPledge(); }); this.shadow.querySelector("[data-action='copy-link']")?.addEventListener("click", () => { navigator.clipboard.writeText(window.location.href); const btn = this.shadow.querySelector("[data-action='copy-link']") as HTMLElement; if (btn) { btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = 'Copy Share Link'; }, 2000); } }); // "What If" simulator controls const simSlider = this.shadow.querySelector("[data-field='sim-qty']") as HTMLInputElement; const simNum = this.shadow.querySelector("[data-field='sim-qty-num']") as HTMLInputElement; simSlider?.addEventListener("input", (e) => { this.simQty = parseInt((e.target as HTMLInputElement).value) || 0; this.render(); }); simNum?.addEventListener("change", (e) => { this.simQty = Math.max(0, parseInt((e.target as HTMLInputElement).value) || 0); this.render(); }); // Commons share slider this.shadow.querySelector("[data-field='commons-pct']")?.addEventListener("input", (e) => { this.commonsSharePct = parseInt((e.target as HTMLInputElement).value) || 0; this.render(); }); } private getStyles(): string { return ` :host { display: block; padding: 2rem 1.5rem; width: 100%; max-width: 960px; } * { box-sizing: border-box; } .loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); } .error { background: rgba(239,68,68,0.1); border: 1px solid var(--rs-error); border-radius: 8px; padding: 1.5rem; color: #fca5a5; text-align: center; } .hero { display: flex; gap: 2rem; margin-bottom: 2.5rem; padding: 1.5rem; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 16px; } .hero-img { width: 220px; height: 220px; border-radius: 12px; object-fit: cover; flex-shrink: 0; } .hero-title { color: var(--rs-text-primary); font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; line-height: 1.2; } .hero-tags { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; } .hero-desc { color: var(--rs-text-secondary); font-size: 0.9375rem; line-height: 1.6; margin: 0.5rem 0 1rem; } .hero-stats { display: flex; gap: 1.5rem; padding-top: 0.75rem; border-top: 1px solid var(--rs-border-subtle); } .hero-stat { text-align: center; } .hero-stat-value { color: var(--rs-text-primary); font-size: 1.25rem; font-weight: 700; } .hero-stat-label { color: var(--rs-text-muted); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; } .tag { display: inline-block; padding: 0.1875rem 0.625rem; border-radius: 6px; font-size: 0.6875rem; font-weight: 500; } .tag-type { background: rgba(99,102,241,0.1); color: var(--rs-primary-hover); } .status { padding: 0.1875rem 0.625rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 600; } .status-open { background: rgba(34,197,94,0.15); color: #4ade80; } .status-locked { background: rgba(251,191,36,0.15); color: #fbbf24; } .status-ordered { background: rgba(99,102,241,0.15); color: #a5b4fc; } .status-cancelled { background: rgba(239,68,68,0.15); color: #f87171; } .main-grid { display: grid; grid-template-columns: 1fr 340px; gap: 2rem; align-items: start; } h3 { color: var(--rs-text-primary); font-size: 1.0625rem; font-weight: 600; margin: 0 0 0.75rem; } /* Fill-up progress visual */ .fill-visual { display: flex; gap: 1.5rem; align-items: flex-end; margin-bottom: 2rem; } .fill-visual__container { position: relative; width: 80px; height: 180px; border: 2px solid var(--rs-border); border-radius: 12px; overflow: hidden; background: var(--rs-bg-surface); flex-shrink: 0; } .fill-visual__liquid { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(180deg, #f59e0b 0%, #22c55e 100%); transition: height 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); border-radius: 0 0 10px 10px; } .fill-visual__wave { position: absolute; top: -4px; left: -10%; right: -10%; height: 10px; background: radial-gradient(ellipse at 50% 100%, transparent 60%, currentColor 61%); opacity: 0.2; animation: wave 3s ease-in-out infinite; } @keyframes wave { 0%,100% { transform: translateX(-5%); } 50% { transform: translateX(5%); } } .fill-visual__label { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1; text-shadow: 0 1px 3px rgba(0,0,0,0.3); } .fill-visual__qty { color: #fff; font-size: 1.5rem; font-weight: 800; } .fill-visual__of { color: rgba(255,255,255,0.7); font-size: 0.6875rem; } .fill-visual__marker { position: absolute; left: 0; right: 0; display: flex; align-items: center; z-index: 2; } .fill-visual__marker-line { flex: 1; height: 1px; background: rgba(255,255,255,0.3); border-top: 1px dashed var(--rs-border); } .fill-visual__marker-label { position: absolute; right: -3rem; font-size: 0.625rem; color: var(--rs-text-muted); white-space: nowrap; } .fill-visual__people { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: flex-end; } .fill-visual__person { width: 24px; height: 24px; color: var(--rs-text-muted); opacity: 0.6; animation: personPop 0.3s ease-out both; } .fill-visual__more { font-size: 0.6875rem; color: var(--rs-text-muted); font-weight: 600; padding: 0.125rem 0.375rem; background: var(--rs-bg-surface-raised); border-radius: 999px; } @keyframes personPop { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 0.6; } } .tier-table { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; overflow: hidden; margin-bottom: 1.25rem; } .tier-row { display: flex; align-items: center; padding: 0.75rem 1rem; border-bottom: 1px solid var(--rs-border-subtle); gap: 1rem; transition: background 0.15s; } .tier-row:last-child { border-bottom: none; } .tier-current { background: rgba(99,102,241,0.1); border-left: 3px solid var(--rs-primary-hover); } .tier-reached { } .tier-qty { color: var(--rs-text-primary); font-weight: 700; min-width: 3rem; font-size: 0.9375rem; } .tier-price { color: var(--rs-text-primary); flex: 1; font-size: 0.9375rem; } .tier-savings { color: #4ade80; font-size: 0.8125rem; font-weight: 600; min-width: 3.5rem; } .tier-savings--base { color: var(--rs-text-muted); font-weight: 400; } .tier-check { color: #4ade80; font-size: 0.9375rem; min-width: 1.25rem; text-align: center; } .progress-section { margin-bottom: 1.5rem; } .progress-label { color: var(--rs-text-primary); font-size: 0.875rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; } .progress-meta { color: var(--rs-text-muted); font-size: 0.75rem; } .progress-bar { background: var(--rs-bg-surface-raised); border-radius: 999px; height: 12px; overflow: hidden; } .progress-fill { background: linear-gradient(90deg, #f59e0b, #22c55e); height: 100%; border-radius: 999px; transition: width 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } .text-green { color: #4ade80; } .pledges-list { margin-bottom: 1.5rem; } .pledge-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--rs-border-subtle); } .pledge-row:last-child { border-bottom: none; } .pledge-avatar { width: 28px; height: 28px; border-radius: 999px; flex-shrink: 0; background: linear-gradient(135deg, #6366f1, #a855f7); color: #fff; font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center; } .pledge-name { color: var(--rs-text-primary); font-size: 0.875rem; flex: 1; } .pledge-qty { color: var(--rs-text-secondary); font-size: 0.8125rem; font-weight: 600; white-space: nowrap; } .pledge-panel { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 16px; padding: 1.5rem; height: fit-content; position: sticky; top: 1.5rem; box-shadow: 0 4px 24px rgba(0,0,0,0.1); } .pledge-panel__header { margin-bottom: 1rem; } .pledge-panel__header h3 { margin-bottom: 0.25rem; } .pledge-panel__social { color: var(--rs-text-muted); font-size: 0.75rem; } .pledge-form { display: flex; flex-direction: column; gap: 0.875rem; } .pledge-form label { color: var(--rs-text-secondary); font-size: 0.8125rem; font-weight: 500; } .input { padding: 0.5rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; } .input:focus { outline: none; border-color: var(--rs-primary); box-shadow: 0 0 0 2px rgba(99,102,241,0.15); } .qty-controls { display: flex; align-items: center; gap: 0.5rem; } .qty-input { width: 60px; text-align: center; padding: 0.375rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; } .pledge-price-box { background: rgba(99,102,241,0.05); border: 1px solid rgba(99,102,241,0.15); border-radius: 10px; padding: 0.75rem; text-align: center; } .pledge-price-amount { color: var(--rs-text-primary); font-size: 1.375rem; font-weight: 700; } .pledge-price-label { color: var(--rs-text-muted); font-size: 0.75rem; margin-top: 0.125rem; } .btn { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 0.875rem; transition: all 0.15s; } .btn:hover { border-color: var(--rs-border-strong); } .btn-primary { background: var(--rs-primary-hover); border-color: var(--rs-primary); color: #fff; } .btn-primary:hover { background: #4338ca; } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .btn-pledge { background: linear-gradient(135deg, #22c55e, #16a34a); border: none; color: #fff; font-weight: 700; font-size: 1.0625rem; padding: 0.875rem; border-radius: 12px; box-shadow: 0 2px 8px rgba(34,197,94,0.3); } .btn-pledge:hover { background: linear-gradient(135deg, #16a34a, #15803d); box-shadow: 0 4px 12px rgba(34,197,94,0.4); } .btn-pledge:disabled { opacity: 0.5; cursor: not-allowed; } .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; } .btn-lg { padding: 0.875rem; font-size: 1rem; width: 100%; } .pledge-disclaimer { color: var(--rs-text-muted); font-size: 0.6875rem; text-align: center; margin: 0; line-height: 1.4; } .pledge-success { text-align: center; padding: 0.5rem 0; } .pledge-success-icon { font-size: 2.5rem; color: #4ade80; margin-bottom: 0.75rem; } .pledge-success p { color: var(--rs-text-secondary); font-size: 0.875rem; margin: 0.5rem 0 1.25rem; line-height: 1.5; } /* Tier commons column */ .tier-commons { color: var(--rs-text-muted); font-size: 0.6875rem; min-width: 5rem; text-align: right; } /* Simulated tier states */ .tier-sim-active { background: rgba(168,85,247,0.1); border-left: 3px solid #a855f7; } .tier-sim-reached { } .tier-check-sim { color: #a855f7; opacity: 0.7; } /* "What If" simulator */ .sim-section { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 16px; padding: 1.5rem; margin: 1.5rem 0; } .sim-desc { color: var(--rs-text-secondary); font-size: 0.8125rem; margin: 0 0 1rem; line-height: 1.4; } .sim-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; } .sim-label { color: var(--rs-text-secondary); font-size: 0.8125rem; font-weight: 500; white-space: nowrap; } .sim-slider { flex: 1; accent-color: #a855f7; height: 6px; cursor: pointer; } .sim-qty-input { width: 60px; } .sim-results { background: rgba(168,85,247,0.05); border: 1px solid rgba(168,85,247,0.2); border-radius: 10px; padding: 1rem; margin-bottom: 1rem; } .sim-row { display: flex; justify-content: space-between; padding: 0.3125rem 0; color: var(--rs-text-primary); font-size: 0.875rem; } .sim-value { text-align: right; } .sim-better { color: #4ade80; font-weight: 600; font-size: 0.8125rem; } .sim-fill { background: linear-gradient(90deg, #a855f7, #d946ef); } /* Commons revenue */ .commons-section { border-top: 1px solid var(--rs-border-subtle); padding-top: 1rem; margin-top: 0.75rem; } .commons-section h4 { color: var(--rs-text-primary); font-size: 0.9375rem; font-weight: 600; margin: 0 0 0.75rem; } .commons-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; } .commons-slider { flex: 1; accent-color: #4ade80; height: 6px; cursor: pointer; } .commons-pct-label { color: #4ade80; font-weight: 600; font-size: 0.875rem; min-width: 2.5rem; text-align: right; } .commons-stats { display: flex; align-items: center; gap: 1rem; } .commons-stat { text-align: center; } .commons-stat-value { color: var(--rs-text-primary); font-weight: 700; font-size: 1.125rem; } .commons-stat-label { color: var(--rs-text-muted); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.125rem; } .commons-stat-projected .commons-stat-value { color: #a855f7; } .commons-stat-arrow { color: var(--rs-text-muted); font-size: 1.25rem; } @media (max-width: 768px) { :host { padding: 1.25rem 1rem; } .hero { flex-direction: column; padding: 1.25rem; } .hero-img { width: 100%; height: auto; aspect-ratio: 1; } .hero-stats { flex-wrap: wrap; gap: 1rem; } .main-grid { grid-template-columns: 1fr; } .pledge-panel { position: static; } .commons-stats { flex-wrap: wrap; gap: 0.5rem; } .fill-visual { flex-direction: column; align-items: center; } .fill-visual__marker-label { display: none; } } @media (max-width: 480px) { :host { padding: 1rem 0.75rem; } .hero { padding: 1rem; gap: 1rem; } .hero-title { font-size: 1.375rem; } .hero-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; } .hero-stat-value { font-size: 1.125rem; } .tier-commons { display: none; } .tier-row { padding: 0.625rem 0.75rem; gap: 0.5rem; } .tier-qty { font-size: 0.8125rem; min-width: 2.5rem; } .tier-price { font-size: 0.8125rem; } .sim-controls { flex-direction: column; align-items: stretch; gap: 0.5rem; } .sim-label { white-space: normal; } .commons-controls { flex-direction: column; align-items: stretch; gap: 0.5rem; } .pledge-panel { padding: 1rem; } h3 { font-size: 0.9375rem; } } `; } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } } customElements.define("folk-group-buy-page", FolkGroupBuyPage);