PageSourceSearch

https://nbsva.web.app/

html nbsva.web.app collected 2026-10-03 12:30:54 UTC 28,857 bytes, 526 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<title>N.B.S.V.A | Official Verified Blood Strike Vendors in Nigeria</title>
7<meta name="description" content="The Nigeria Blood Strike Vendors Association — the official organization for verified Blood Strike vendors. View vendor profiles, ratings and reviews, and verify any seller before you deal.">
8<meta name="keywords" content="Blood Strike Vendor Nigeria, Verified Blood Strike Vendors, Blood Strike Vendor Association, NBSVA, Trusted Blood Strike Vendors, Blood Strike Nigeria">
9<link rel="canonical" href="https://nbsva.web.app/">
10<meta property="og:title" content="N.B.S.V.A — Official Verified Blood Strike Vendors in Nigeria">
11<meta property="og:description" content="Trade safe. Deal smart. Stay protected. Verify any seller before you deal.">
12<meta property="og:type" content="website">
13<meta property="og:url" content="https://nbsva.web.app/">
14<meta property="og:image" content="https://nbsva.web.app/assets/social-banner.png">
15<meta property="og:image:width" content="1983">
16<meta property="og:image:height" content="793">
17<meta name="twitter:card" content="summary_large_image">
18<meta name="twitter:title" content="N.B.S.V.A — Official Verified Blood Strike Vendors in Nigeria">
19<meta name="twitter:description" content="Trade safe. Deal smart. Stay protected. Verify any seller before you deal.">
20<meta name="twitter:image" content="https://nbsva.web.app/assets/social-banner.png">
21<link rel="icon" type="image/x-icon" href="/favicon.ico">
22<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png">
23<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon-512.png">
24<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
25<meta name="mobile-web-app-capable" content="yes">
26<meta name="apple-mobile-web-app-capable" content="yes">
27<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
28<meta name="apple-mobile-web-app-title" content="N.B.S.V.A">
29<link rel="manifest" href="manifest.json">
30<meta name="theme-color" content="#070707">
31<link rel="preconnect" href="https://fonts.googleapis.com">
32<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
33<link rel="preconnect" href="https://the-hunt-scrims.firebaseio.com" crossorigin>
34<link rel="preconnect" href="https://www.gstatic.com" crossorigin>
35<link rel="preload" as="script" href="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js" crossorigin>
36<link rel="preload" as="script" href="https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js" crossorigin>
37<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@700;800;900&family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
38<link rel="stylesheet" href="css/style.css">
39<link rel="stylesheet" href="css/responsive.css">
40<script type="application/ld+json">
41{
42  "@context": "https://schema.org",
43  "@type": "Organization",
44  "name": "The Nigeria Blood Strike Vendors Association",
45  "alternateName": "NBSVA",
46  "url": "https://nbsva.web.app/",
47  "logo": "https://nbsva.web.app/assets/logo.png",
48  "foundingDate": "2024",
49  "slogan": "Trade Safe. Deal Smart. Stay Protected.",
50  "description": "The official organization for verified Blood Strike vendors in Nigeria.",
51  "areaServed": "NG"
52}
53</script>
53
54</head>
55<body class="home-v2">
56<div id="nav-wrapper"></div>
57
58<!-- ============ HERO ============ -->
59<section class="hero v2-hero">
60  <div class="container">
61    <div class="v2-hero-split">
62      <div class="v2-hero-copy">
63        <span class="v2-est-pill"><span class="v2-est-dot"></span> EST. 2024</span>
64        <h1 class="v2-hero-title">
65          Trade Safe.<br>
66          Deal Smart.<br>
67          <span class="v2-red">Stay Protected.</span>
68        </h1>
69        <p class="v2-hero-lede">The #1 Verified Vendor Association for the Blood Strike community in Nigeria.</p>
70        <div class="v2-hero-actions">
71          <a href="#check" class="v2-btn v2-btn-red">Instant Vendor Check</a>
72          <a href="/contact" class="v2-btn v2-btn-outline">Report a Scammer</a>
73        </div>
74      </div>
75      <div class="v2-hero-art">
76        <div class="v2-hero-glow"></div>
77        <img src="assets/logo.png" alt="" class="v2-hero-watermark">
78        <img src="assets/hero-hitman.png" alt="N.B.S.V.A" class="v2-hero-char" onerror="this.style.display='none'">
79      </div>
80
81    </div>
82  </div>
83</section>
84
85<!-- ============ LATEST BLACKLISTED TICKER ============ -->
86<div class="enf-ticker v2-ticker" id="enfTicker" style="display:none">
87  <div class="container v2-ticker-row">
88    <a href="/enforcement" id="enfTickerLink">&#9888; Latest blacklisted: &hellip;</a>
89    <a href="/enforcement" class="v2-ticker-more">View All Enforcement &rarr;</a>
90  </div>
91</div>
92
93<!-- ============ LIVE COMMUNITY STATS ============ -->
94<section class="section tight v2-stats-section">
95  <div class="container">
96    <div class="v2-live-label"><span class="v2-live-dot grn"></span> Live community stats <span class="v2-live-sub">Real-time from our database</span></div>
97    <div class="stats-grid v2-stats-grid">
98      <div class="stat v2-stat v2-glow-grn"><span class="icon">&#9989;</span><div class="num grn" id="stVendors" data-count="0">0</div><div class="label">Verified Vendors</div><a href="/vendors" class="stat-link">View all vendors &rarr;</a></div>
99      <div class="stat v2-stat v2-glow-red"><span class="icon">&#128683;</span><div class="num rd" id="stEnforcement" data-count="0">0</div><div class="label">Enforcement Entries</div><a href="/enforcement" class="stat-link">View all entries &rarr;</a></div>
100      <div class="stat v2-stat v2-glow-gld"><span class="icon">&#11088;</span><div class="num gld" id="stReviews" data-count="0">0</div><div class="label">Reviews Posted</div><a href="/vendors" class="stat-link">View all reviews &rarr;</a></div>
101      <div class="stat v2-stat v2-glow-blu"><span class="icon">&#128737;&#65039;</span><div class="num blu" id="stResolved" data-count="0">0</div><div class="label">Reports Resolved</div><a href="/enforcement" class="stat-link">View reports &rarr;</a></div>
102    </div>
103  </div>
104</section>
105
106<!-- ============ LIVE ENFORCEMENT FEED (carousel) ============ -->
107<section class="section tight" id="feedSection" style="display:none;padding-bottom:-1px">
108  <div class="container">
109    <div class="feed-carousel-wrap v2-feed-wrap">
110      <div class="feed-carousel-head">
111        <span class="live-dot v2-live-label"><span class="v2-live-dot red"></span> Live enforcement feed <span class="v2-live-sub">Stay informed. Stay safe.</span></span>
112        <div class="feed-carousel-arrows">
113          <button type="button" onclick="feedScroll(-1)" aria-label="Previous">&larr;</button>
114          <button type="button" onclick="feedScroll(1)" aria-label="Next">&rarr;</button>
115        </div>
116      </div>
117      <div class="feed-carousel-track" id="feedTrack"></div>
118      <div class="feed-carousel-dots" id="feedDots"></div>
119    </div>
120  </div>
121</section>
122
123<!-- ============ HOW IT WORKS ============ -->
124<section class="section tight band v2-how">
125  <div class="container">
126    <div class="section-head">
127      <span class="label-caps">Three simple steps to trade safely</span>
128      <h2 class="v2-h2">How it works</h2>
129    </div>
130    <div class="how-steps-v2">
131      <div class="how-step-v2 v2-step">
132        <div class="how-step-v2-icon grn"><span class="how-step-v2-num" style="color:var(--green)">1</span>&#128269;</div>
133        <div><h3>Check the vendor</h3><p>Verify their profile, trust score and reviews before you deal.</p></div>
134      </div>
135      <div class="how-arrow">&rarr;</div>
136      <div class="how-step-v2 v2-step">
137        <div class="how-step-v2-icon blu"><span class="how-step-v2-num" style="color:var(--blue)">2</span>&#128172;</div>
138        <div><h3>Read their reviews</h3><p>See what other buyers are saying about their track record.</p></div>
139      </div>
140      <div class="how-arrow">&rarr;</div>
141      <div class="how-step-v2 v2-step">
142        <div class="how-step-v2-icon ppl"><span class="how-step-v2-num" style="color:#c084fc">3</span>&#9989;</div>
143        <div><h3>Deal safely</h3><p>Make smart, secure transactions — or use a Verified Vendor as middleman.</p></div>
144      </div>
145    </div>
146  </div>
147</section>
148
149<!-- ============ INSTANT VENDOR CHECK ============ -->
150<section class="section tight" id="check">
151  <div class="container">
152    <div class="verify-box v2-verify">
153      <h3 style="margin-bottom:6px">Instant Vendor Check</h3>
154      <p style="color:var(--muted);font-size:.86rem;line-height:1.6;margin-bottom:14px">Type a seller's name or WhatsApp number to confirm they're officially verified — before you deal.</p>
155      <div class="verify-row">
156        <input type="text" id="qvInput" placeholder="e.g. JMAROXS or 234815..." style="flex:1;background:#0b0b0b;border:1px solid var(--line);border-radius:10px;padding:13px 14px;color:var(--text);font-size:.95rem" onkeydown="if(event.key==='Enter')quickVerify()">
157        <button class="v2-btn v2-btn-red" style="padding:12px 20px" onclick="quickVerify()">Check</button>
158      </div>
159      <div class="verify-result" id="qvResult"></div>
160    </div>
161  </div>
162</section>
163
164<!-- ============ RECENT REVIEWS ============ -->
165<section class="section tight">
166  <div class="container">
167    <div id="recentReviewsSection" class="v2-duo-reviews v2-reviews-full">
168      <div class="v2-panel-head">
169        <div class="v2-panel-label">Recent reviews</div>
170        <a href="/vendors" class="v2-viewall">View All &rarr;</a>
171      </div>
172      <div class="reviews-strip" id="recentReviewsBox">
173        <div class="review-tile review-skeleton"><div class="rv-skel-stars"></div><div class="rv-skel-line l1"></div><div class="rv-skel-line l2"></div><div class="rv-skel-who"></div></div>
174        <div class="review-tile review-skeleton"><div class="rv-skel-stars"></div><div class="rv-skel-line l1"></div><div class="rv-skel-line l2"></div><div class="rv-skel-who"></div></div>
175        <div class="review-tile review-skeleton"><div class="rv-skel-stars"></div><div class="rv-skel-line l1"></div><div class="rv-skel-line l2"></div><div class="rv-skel-who"></div></div>
176      </div>
177    </div>
178  </div>
179</section>
180
181<!-- ============ TOP 5 VENDOR LEADERBOARD ============ -->
182<section class="section tight" id="leaderboardSection">
183  <div class="container">
184    <div class="v2-lb">
185      <div class="v2-panel-head">
186        <div class="v2-panel-label gold">&#127942; Top 5 vendor leaderboard</div>
187        <a href="/vendors" class="v2-viewall">Full Directory &rarr;</a>
188      </div>
189      <div id="leaderBox">
190        <div class="lb-row lb-skeleton"><span class="lb-rank">1</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div>
191        <div class="lb-row lb-skeleton"><span class="lb-rank">2</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div>
192        <div class="lb-row lb-skeleton"><span class="lb-rank">3</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div>
193        <div class="lb-row lb-skeleton"><span class="lb-rank">
1934</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div>
194        <div class="lb-row lb-skeleton"><span class="lb-rank">5</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div>
195      </div>
196    </div>
197  </div>
198</section>
199
200<div class="divider container"></div>
201
202<!-- ============ RECENT ENFORCEMENT ALERTS ============ -->
203<section class="section tight">
204  <div class="container">
205    <div class="section-head">
206      <span class="label-caps" style="color:var(--red2)">Trust &amp; Safety</span>
207      <h2 class="v2-h2">Recent enforcement alerts</h2>
208      <p>Impersonators and confirmed scammers the association has caught. Tap one to see the evidence.</p>
209    </div>
210    <div id="homeScams" class="enf-grid"></div>
211    <div style="margin-top:26px">
212      <a href="/enforcement" class="v2-btn v2-btn-outline">View Full Enforcement List</a>
213    </div>
214  </div>
215</section>
216
217<!-- ============ BROWSE BY CATEGORY ============ -->
218<section class="section tight">
219  <div class="container">
220    <div class="section-head">
221      <span class="label-caps">Browse by category</span>
222      <h2 class="v2-h2">Find vendors based on what you need</h2>
223    </div>
224    <div class="cat-tile-grid" id="homeCatTiles"></div>
225    <div style="margin-top:24px">
226      <a href="/vendors" class="v2-btn v2-btn-outline">View All Vendors &rarr;</a>
227    </div>
228  </div>
229</section>
230
231<div class="divider container"></div>
232
233<!-- ============ TOGETHER BANNER ============ -->
234<section class="section tight">
235  <div class="container">
236    <div class="together-banner v2-together">
237      <div class="v2-together-copy">
238        <img src="assets/logo.png" alt="" class="v2-together-crest" loading="lazy">
239        <h2 class="v2-together-title">Together, we build a <span class="v2-red">safer</span> community</h2>
240        <p>Your safety is our priority. Report scammers, leave honest reviews, and help protect others.</p>
241        <div class="v2-hero-actions">
242          <a href="/contact" class="v2-btn v2-btn-red">&#9888; Report a Scammer</a>
243          <a href="/become-a-vendor" class="v2-btn v2-btn-outline">&#128100; Become a Vendor</a>
244        </div>
245      </div>
246      <div class="together-banner-art v2-together-art">
247        <img src="assets/hero-hitman.png" alt="" loading="lazy" onerror="this.parentElement.style.display='none'">
248      </div>
249    </div>
250  </div>
251</section>
252
253<!-- ============ NEWSLETTER ============ -->
254<section class="section tight">
255  <div class="container">
256    <div class="newsletter-box v2-newsletter">
257      <div style="display:flex;align-items:center;gap:16px;flex:1;min-width:240px">
258        <span class="nl-icon">&#128231;</span>
259        <div>
260          <h3>Stay <span class="v2-red">Updated</span></h3>
261          <p>Subscribe to get the latest enforcement alerts, safety tips and platform updates.</p>
262        </div>
263      </div>
264      <form class="newsletter-form" onsubmit="return subscribeNewsletter(event)">
265        <input type="email" id="nlEmail" placeholder="Enter your email address" required>
266        <button type="submit" class="v2-btn v2-btn-red">Subscribe</button>
267      </form>
268      <p class="newsletter-hint">&#128274; No spam. Unsubscribe anytime.</p>
269    </div>
270  </div>
271</section>
272
273<!-- ============ FEATURE STRIP ============ -->
274<section class="section tight" style="padding-top:0">
275  <div class="container">
276    <div class="feature-strip v2-features">
277      <div class="feature-strip-item"><span class="icon">&#128101;</span><div><h4>Stronger Community</h4><p>Chat, support and trust all in one place.</p></div></div>
278      <div class="feature-strip-item"><span class="icon">&#128202;</span><div><h4>Transparency First</h4><p>Real reviews, real vendors, real results.</p></div></div>
279      <div class="feature-strip-item"><span class="icon">&#128737;&#65039;</span><div><h4>Safer Trading</h4><p>We protect you from scammers and fraud.</p></div></div>
280      <div class="feature-strip-item"><span class="icon">&#127475;&#127468;</span><div><h4>Built for Nigeria</h4><p>Proudly serving the Blood Strike community.</p></div></div>
281    </div>
282  </div>
283</section>
284
285<div id="footer-wrapper"></div>
286<script src="js/nav.js"></script>
vendor: 1 bytes, line 286
286
287<script crossorigin="anonymous" src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
vendor: 1 bytes, line 287
287
288<script crossorigin="anonymous" src="https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js"></script>
vendor: 1 bytes, line 288
288
289<script crossorigin="anonymous" src="https://www.gstatic.com/firebasejs/9.22.0/firebase-functions-compat.js"></script>
vendor: 1 bytes, line 289
289
290<script>var NBSVA_NEEDS = ['vendors', 'scams', 'reviews', 'reports'];</script>
vendor: 1 bytes, line 290
290
291<script src="js/firebase-init.js"></script>
vendor: 1 bytes, line 291
291
292<script src="js/data.js"></script>
vendor: 1 bytes, line 292
292
293<script src="js/reviews.js"></script>
vendor: 1 bytes, line 293
293
294<script src="js/catalog.js"></script>
vendor: 1 bytes, line 294
294
295<script src="js/main.js"></script>
vendor: 1 bytes, line 295
295
296<script src="js/nova-client.js"></script>
vendor: 1 bytes, line 296
296
297<script>
298function renderHomeScams() {
299  var box = document.getElementById('homeScams');
300  if (!box) return;
301  box.innerHTML = allScams().slice(0, 3).map(scamCard).join('') || '<p style="color:var(--muted);text-align:center;grid-column:1/-1">No enforcement records yet.</p>';
302}
303renderHomeScams();
304
305/* Category tiles — same regex matching as the Vendor Directory's category
306   chips (vendors.html), so a vendor counted here shows up under the same
307   category if you tap through to /vendors. */
308var HOME_CAT_TILES = [
309  { label: 'Accounts', icon: '&#128100;', color: 'grn', re: /account/i },
310  { label: 'Top-ups', icon: '&#128179;', color: 'blu', re: /top-?up|\bcp\b|gems|gold|coin/i },
311  { label: 'Passes', icon: '&#127903;', color: 'gld', re: /pass/i },
312  { label: 'Recovery/Unlinking', icon: '&#128274;', color: 'rd', re: /recover|unlink|unban|unbind/i },
313  { label: 'Escrow/MM', icon: '&#128737;&#65039;', color: 'blu', re: /escrow|middleman|\bmm\b/i }
314];
315var CAT_TILE_COLORS = { grn: { bg: 'var(--green-soft)', fg: 'var(--green)' }, blu: { bg: 'rgba(29,155,240,.12)', fg: 'var(--blue)' }, gld: { bg: 'rgba(245,179,1,.12)', fg: 'var(--gold)' }, rd: { bg: 'rgba(240,68,56,.12)', fg: 'var(--red)' } };
316function renderHomeCatTiles() {
317  var vendors = allVendors();
318  document.getElementById('homeCatTiles').innerHTML = HOME_CAT_TILES.map(function (c) {
319    var n = vendors.filter(function (v) { return c.re.test(v.sells || ''); }).length;
320    var col = CAT_TILE_COLORS[c.color];
321    return '<a href="/vendors" class="cat-tile"><div class="cat-tile-icon" style="background:' + col.bg + ';color:' + col.fg + '">' + c.icon + '</div>'
322      + '<div style="font-weight:700;font-size:.92rem">' + c.label + '</div>'
323      + '<div class="cat-tile-count">' + n + '+ Vendors</div></a>';
324  }).join('');
325}
326renderHomeCatTiles();
327
328/* Newsletter — no email backend exists yet, so this saves the address to
329   /newsletterSignups for staff to export later rather than silently
330   discarding it or pretending to send a real confirmation email. */
331function subscribeNewsletter(e) {
332  e.preventDefault();
333  var input = document.getElementById('nlEmail');
334  var email = input.value.trim();
335  if (!email) return false;
336  if (typeof fbDB === 'undefined') { showToast('Could not save — try again shortly.', 'err'); return false; }
337  fbDB.ref('newsletterSignups').push({ email: email, ts: Date.now() }).then(function () {
338    showToast('Subscribed! You\'ll get enforcement alerts and updates.', 'ok');
339    input.value = '';
340  }).catch(function () {
341    showToast('Could not subscribe right now — try again later.', 'err');
342  });
343  return false;
344}
345
346/* Stat numbers are computed from live data, never hand-typed. Setting
347   data-count before DOMContentLoaded lets the existing count-up animation
348   (animateCounters, in main.js) play with the right value on first load;
349   later live updates (a new vendor/scam added) just set the number directly. */
350function updateHomeStats(live) {
351  var vendors = allVendors();
352  var scams = allScams();
353  var reviewCount = (typeof _rvCache !== 'undefined')
354    ? Object.keys(_rvCache).reduce(function (sum, vid) { return sum + _rvCache[vid].length; }, 0) : 0;
355  // "Resolved" = reports that went through the vendor-report review flow and were
356  // approved (reportedBy.verified) — the only outcome a public visitor can read;
357  // rejected reports live in /rejectedScams, which is staff-only for privacy.
358  var resolvedCount = scams.filter(function (s) { return s.reportedBy && s.reportedBy.verified; }).length;
359  [['stVendors', vendors.length], ['stEnforcement', scams.length], ['stReviews', reviewCount], ['stResolved', resolvedCount]].forEach(function (pair) {
360    var el = document.getElementById(pair[0]);
361    if (!el) return;
362    el.dataset.count = pair[1];
363    if (live) el.textContent = pair[1];
364  });
365}
366updateHomeStats(false);
367
368/* Rotates through the 5 most recently blacklisted entries — same public
369   record set as enforcement.html (staff/vendor scam reports; vendor-status
370   blacklists don't carry a `date` precise enough for "x days ago", so the
371   ticker sticks to actual scam reports, which always have one). */
372var _tickerTimer = null, _tickerIdx = 0;
373function daysAgo(dateStr) {
374  var d = Date.parse(dateStr);
375  if (isNaN(d)) return '';
376  var days = Math.max(0, Math.floor((Date.now() - d) / 86400000));
377  return days === 0 ? 'today' : days === 1 ? '1 day ago' : days + ' days ago';
378}
379function renderTicker() {
380  var entries = allScams().filter(function (s) { return s.status === 'Blacklisted'; })
381    .sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); }).slice(0, 5);
382  var box = document.getElementById('enfTicker');
383  if (!entries.length) { box.style.display = 'none'; if (_tickerTimer) clearInterval(_tickerTimer); return; }
384  box.style.display = 'block';
385  var link = document.getElementById('enfTickerLink');
386  function show(i) {
387    var s = entries[i % entries.length];
388    link.innerHTML = '&#9888; Latest blacklisted: <strong>' + escapeHtml(s.name) + '</strong> &mdash; ' + escapeHtml(s.type || '') + ' &mdash; ' + daysAgo(s.date);
389    link.href = s.id ? '/scam-detail?id=' + encodeURIComponent(s.id) : '/enforcement';
390  }
391  show(0); _tickerIdx = 0;
392  if (_tickerTimer) clearInterval(_tickerTimer);
393  if (entries.length > 1) _tickerTimer = setInterval(function () { _tickerIdx++; show(_tickerIdx); }, 5000);
394}
395renderTicker();
396
397/* Live Enforcement Feed carousel — same entries as the ticker above (any
398   status, not just Blacklisted, so Suspended/Under Appeal show too),
399   rendered as scrollable cards instead of rotating text. */
400function feedStatusClass(status) { return status === 'Blacklisted' ? 'tag-red' : status === 'Under Appeal' ? 'tag-blue' : 'tag-gold'; }
401function renderFeedCarousel() {
402  var section = document.getElementById('feedSection');
403  var entries = allScams().sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); }).slice(0, 8);
404  if (!entries.length) { section.style.display = 'none'; return; }
405  section.style.display = 'block';
406  document.getElementById('feedTrack').innerHTML = entries.map(function (s) {
407    var href = s.id ? '/scam-detail?id=' + encodeURIComponent(s.id) : '/enforcement';
408    var initial = (s.name || '?').charAt(0).toUpperCase();
409    return '<a href="' + href + '" class="feed-card">'
410      + '<div class="feed-card-top">'
411      + (s.photo ? '<img src="' + s.photo + '" class="feed-card-avatar" alt="">' : '<div class="feed-card-avatar">' + initial + '</div>')
412      + '<div style="min-width:0"><span class="tag ' + feedStatusClass(s.status) + '" style="font-size:.64rem;padding:2px 8px">' + escapeHtml(s.status) + '</span>'
413      + '<div class="feed-card-name">' + escapeHtml(s.name) + '</div></div>'
414      + '</div>'
415      + '<div class="feed-card-meta">' + escapeHtml(s.type || '') + ' &bull; ' + daysAgo(s.date) + '</div>'
416      + '</a>';
417  }).join('');
418  var dots = document.getElementById('feedDots');
419  dots.innerHTML = entries.map(function (_, i) { return '<span' + (i === 0 ? ' class="active"' : '') + '></span>'; }).join('');
420  var track = document.getElementById('feedTrack');
421  track.onscroll = function () {
422    var cardWidth = 222; // .feed-card flex-basis (210px) + gap (12px)
423    var idx = Math.round(track.scrollLeft / cardWidth);
424    Array.prototype.forEach.call(dots.children, function (d, i) { d.classList.toggle('active', i === idx); });
425  };
426}
427function feedScroll(dir) {
428  var track = document.getElementById('feedTrack');
429  track.scrollBy({ left: dir * 222, behavior: 'smooth' });
430}
431renderFeedCarousel();
432
433/* Recent Reviews — always visible; skeleton → live tiles → empty state. */
434function renderRecentReviews() {
435  var section = document.getElementById('recentReviewsSection');
436  if (!section) return;
437  section.style.display = 'block';
438  var box = document.getElementById('recentReviewsBox');
439  if (!box) return;
440
441  if (typeof _rvCache === 'undefined') return; // keep skeleton until Firebase loads
442  var all = [];
443  Object.keys(_rvCache).forEach(function (vid) {
444    _rvCache[vid].forEach(function (r) { all.push({ r: r, vendorId: vid }); });
445  });
446  all.sort(function (a, b) { return (b.r.ts || 0) - (a.r.ts || 0); });
447  var recent = all.slice(0, 5);
448
449  if (!recent.length) {
450    box.innerHTML = '<div class="reviews-empty"><span>&#128172;</span><p>No reviews yet — they\'ll show up here as buyers leave them.</p></div>';
451    return;
452  }
453
454  box.innerHTML = recent.map(function (item) {
455    var v = allVendors(true, true).find(function (x) { return x.id === item.vendorId; });
456    var vname = v ? v.name : 'a vendor';
457    return '<div class="review-tile" onclick="location.href=\'/vendors?v=' + encodeURIComponent(item.vendorId) + '\'">'
458      + '<span class="stars">' + starsOf(item.r.stars) + '</span>'
459      + '<p>&ldquo;' + escapeHtml(item.r.text) + '&rdquo;</p>'
460      + '<div class="who">&mdash; ' + escapeHtml(item.r.name) + ' on ' + escapeHtml(vname) + '</div>'
461      + '</div>';
462  }).join('');
463}
464renderRecentReviews();
465
466var _homeRefresh = window._nbsvaRefresh;
467window._nbsvaRefresh = function () {
468  if (_homeRefresh) _homeRefresh();
469  renderHomeScams(); updateHomeStats(true); renderTicker(); renderFeedCarousel(); renderRecentReviews(); renderHomeCatTiles(); renderLeaderboard();
470};
471
472/* Ranking must never be computed from cache state that just HAPPENS to be
473   empty because Firebase hasn't answered yet — _rvCache = {} on page load
474   looks identical to "these vendors genuinely have 0 reviews" (see
475   ratingSummary()/trustScore() in js/main.js), so without an explicit
476   "did the real data arrive" signal this would render a ranking built off
477   a snapshot that's still empty by default, not by fact.
478   _reviewsLoaded/_vendorsLoaded (js/firebase-init.js) are only set true
479   inside the real on('value') callbacks, so gating on them (not on cache
480   size) is what actually distinguishes "not loaded" from "loaded, and
481   there's nothing there". */
482var _lbWaitStart = Date.now();
483var LEADERBOARD_TIMEOUT_MS = 15000;
484function renderLeaderboard() {
485  var box = document.getElementById('leaderBox');
486  if (!box || typeof allVendors === 'undefined') return;
487
488  if (!_reviewsLoaded || !_vendorsLoaded) {
489    if (Date.now() - _lbWaitStart > LEADERBOARD_TIMEOUT_MS) {
490      box.innerHTML = '<p style="color:var(--muted);text-align:center;padding:20px 0">Couldn’t load rankings — check your connection and reload the page.</p>';
491    }
492    return; // keep the skeleton (or the timeout message above) — never a guessed ranking
493  }
494
495  var ranked = allVendors().filter(function (v) {
496    return (v.status || 'verified') === 'verified';
497  }).map(function (v) {
498    var t = trustScore(v.id), r = ratingSummary(v.id);
499    return { v: v, score: t.score, avg: r.avg, count: r.count };
500  }).sort(function (a, b) {
501    return (b.score - a.score) || (b.count - a.count) || (b.avg - a.avg);
502  }).slice(0, 5);
503  if (!ranked.length) { document.getElementById('leaderboardSection').style.display = 'none'; return; }
504  box.innerHTML = ranked.map(function (row, i) {
505    var v = row.v;
506    var medal = i === 0 ? '\uD83E\uDD47' : i === 1 ? '\uD83E\uDD48' : i === 2 ? '\uD83E\uDD49' : '#' + (i + 1);
507    return '<div class="lb-row' + (i === 0 ? ' first' : '') + '" onclick="location.href=\'/vendors?v=' + encodeURIComponent(v.id) + '\'">'
508      + '<span class="lb-rank">' + medal + '</span>'
509      + vendorAvatar(v, 40)
510      + '<div class="lb-who"><span class="lb-name">' + v.name + ' <span class="vcheck">&#10003;</span></span>'
511      + '<span class="lb-sub">' + (row.count ? starsOf(row.avg) + ' (' + row.count + ')' : 'No reviews yet') + '</span></div>'
512      + '<div class="lb-trust"><span class="l">Trust</span><span class="n">' + row.score + '%</span></div>'
513      + '</div>';
514  }).join('');
515}
516renderLeaderboard();
517/* Firebase only re-triggers renderLeaderboard() (via _nbsvaRefresh above)
518   when a listener actually delivers a snapshot — if one never fires at all
519   (offline, blocked request, no network error callback), nothing would
520   ever re-check the timeout and the skeleton would spin forever instead of
521   showing the "couldn't load" message. This one-shot check fires once,
522   after the timeout window, purely to catch that case. */
523setTimeout(renderLeaderboard, LEADERBOARD_TIMEOUT_MS + 500);
524</script>
524
525</body>
526</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.