RENDERING: "\n // Pagination state\n const PAGE_SIZE = 20;\n let visibleCount = PAGE_SIZE;\n let restoringFromURL = false;\n\n let filteredArticles = [...articles];\n\n function buildArticleCard(article) {\n const flag = LANGUAGE_FLAGS[article.lang] || '๐';\n const dirAttr = IS_RTL ? ' dir=\"ltr\"' : '';\n const langBadge = `<span class=\"language-badge\"${dirAttr} aria-label=\"${esc(article.lang)} language\"><span aria-hidden=\"true\">${flag}</span> ${esc(article.lang.toUpperCase())}</span>`;\n\n const availableLangs = article.availableLanguages || [article.lang];\n let availableDisplay = '';\n if (availableLangs.length > 1) {\n const availableBadges = availableLangs.map(l => {\n const lf = LANGUAGE_FLAGS[l] || '๐';\n return `<span class=\"lang-badge-sm\"${dirAttr}><span aria-hidden=\"true\">${lf}</span> ${esc(l.toUpperCase())}</span>`;\n }).join(' ');\n availableDisplay = `<p class=\"available-languages\"><strong>${AVAILABLE_IN_TEXT}:</strong> ${availableBadges}</p>`;\n }\n\n const primaryTopic = (article.topics && article.topics.length > 0) ? article.topics[0] : '';\n const recency = computeRecency(article.date);\n const recencyAttr = recency ? ` data-date-recent=\"${recency}\"` : '';\n const recencyBadge = recency ? `<span class=\"recency-badge\" data-recency=\"${recency}\">${esc(localizeRecency(recency))}</span>` : '';\n\n return `\n <article class=\"article-card\" data-type=\"${esc(article.type)}\" data-topic=\"${esc(primaryTopic)}\"${recencyAttr}>\n <div class=\"article-meta\">\n <time class=\"article-date\" datetime=\"${esc(article.date)}\">${formatDate(article.date)}</time>\n <span class=\"article-type\" data-type=\"${esc(article.type)}\">${typeIcon(article.type)} ${localizeType(article.type)}</span>\n ${recencyBadge}\n ${langBadge}\n </div>\n <h2 class=\"article-title\">\n <a href=\"${safeHref(article.slug)}\">${esc(article.title)}</a>\n </h2>\n <p class=\"article-excerpt\">${esc(article.excerpt)}</p>\n ${availableDisplay}\n <div class=\"article-tags\">\n ${article.tags.filter(Boolean).map(tag => `<span class=\"tag\">${esc(tag)}</span>`).join('')}\n </div>\n </article>\n `;\n }\n\n // Compute coarse recency bucket so CSS can surface a \"today\" / \"this-week\"\n // / \"this-month\" badge without re-running JS per scroll. Uses the\n // article date in the user's timezone, not UTC, so \"today\" feels right.\n function computeRecency(dateStr) {\n try {\n const d = new Date(dateStr);\n if (isNaN(d.getTime())) return null;\n const now = new Date();\n const dayMs = 24 * 60 * 60 * 1000;\n const diff = (now.getTime() - d.getTime()) / dayMs;\n if (diff < 0) return null; // future-dated, leave plain\n if (diff < 1.0) return 'today';\n if (diff < 7.0) return 'this-week';\n if (diff < 31.0) return 'this-month';\n return null;\n } catch (e) { return null; }\n }\n\n function localizeRecency(bucket) {\n return RECENCY_LABELS[bucket] || bucket;\n }\n\n // Emoji icon per article type โ keeps the visual language consistent with\n // the rest of the site (root index.html, political-intelligence.html).\n function typeIcon(type) {\n switch (type) {\n case 'prospective': return '<span aria-hidden=\"true\">๐ฎ</span>';\n case 'retrospective': return '<span aria-hidden=\"true\">๐</span>';\n case 'analysis': return '<span aria-hidden=\"true\">๐ง </span>';\n case 'breaking': return '<span aria-hidden=\"true\">โก</span>';\n default: return '<span aria-hidden=\"true\">๐ฐ</span>';\n }\n }\n\n function renderPage() {\n const grid = document.getElementById('articles-grid');\n const noArticles = document.getElementById('no-articles');\n const noResults = document.getElementById('no-results');\n const counter = document.getElementById('article-counter');\n const btn = document.getElementById('load-more-btn');\n\n if (articles.length === 0) {\n grid.innerHTML = '';\n grid.removeAttribute('aria-busy');\n if (noArticles) noArticles.hidden = false;\n noResults.hidden = true;\n if (counter) counter.textContent = '';\n if (btn) btn.hidden = true;\n updateFilterChrome();\n return;\n }\n\n if (filteredArticles.length === 0) {\n grid.innerHTML = '';\n grid.removeAttribute('aria-busy');\n noResults.hidden = false;\n if (noArticles) noArticles.hidden = true;\n if (counter) counter.textContent = '';\n if (btn) btn.hidden = true;\n updateFilterChrome();\n return;\n }\n\n if (noArticles) noArticles.hidden = true;\n noResults.hidden = true;\n\n const visible = filteredArticles.slice(0, visibleCount);\n grid.innerHTML = visible.map(buildArticleCard).join('');\n grid.removeAttribute('aria-busy');\n\n const shown = visible.length;\n const total = filteredArticles.length;\n if (counter) counter.textContent = i18nShowing(shown, total);\n\n if (btn) {\n if (total > visibleCount) {\n btn.hidden = false;\n btn.setAttribute('aria-label', i18nLoadMore);\n } else {\n btn.hidden = true;\n }\n }\n\n updateFilterChrome();\n }\n" = ...
Card building + page render + recency helpers. Includes STATE prefix.