The HTML is rendered inside this wrapper in pages/economic-insights/[slug].vue:
<div class="insight-html-content" :data-locale="locale" v-html="insight.body_html"></div>- The parent page handles: back navigation, page header (title/subtitle/eyebrow), access gate (MAX tier), SEO meta tags, and activity tracking.
- Your HTML is ONLY the page body content — no page header, no access gate, no
<html>/<head>/<body>tags.
The wrapper div has data-locale="vi" or data-locale="en" based on the user's current locale. The app injects this CSS:
.insight-html-content[data-locale="vi"] [data-lang="en"] { display: none !important; }
.insight-html-content[data-locale="en"] [data-lang="vi"] { display: none !important; }Convention: Wrap all translatable text in paired spans:
<span data-lang="vi">Vietnamese text here</span><span data-lang="en">English text here</span>Content that is identical in both languages (numbers, dollar amounts, percentages, proper names, dates, ticker symbols) should NOT be wrapped — leave as plain text.
The app sets data-theme="light" or data-theme="dark" on the <html> element. Your HTML inherits this automatically. Use the app's CSS variables for all colors:
| Variable | Light | Dark | Usage |
|---|---|---|---|
--bg-color |
#ffffff |
#1a1a1a |
Page background |
--text-color |
#1a1a1a |
#ffffff |
Primary text |
--text-secondary |
#666 |
#b3b3b3 |
Secondary text, descriptions |
--text-tertiary |
#999 |
#888 |
Tertiary text, captions, timestamps |
--border-color |
#e1e1e1 |
#333 |
Borders, dividers |
--border-light |
#e5e5e5 |
#444 |
Lighter borders |
--border-lighter |
#f5f5f5 |
#2a2a2a |
Subtlest borders |
--card-bg |
#ffffff |
#222 |
Card backgrounds |
--surface-1 |
#ffffff |
#2a2a2a |
Elevated surface |
--surface-2 |
#f8f9fa |
#333 |
Section backgrounds, callouts |
--surface-3 |
#f0f0f0 |
#3a3a3a |
Deeper surface |
--hover-bg |
#f0f1f2 |
#3a3a3a |
Hover states |
--input-bg |
#f8f9fa |
#2a2a2a |
Input fields |
--accent-color |
#10b981 |
#10b981 |
Accent/brand color (emerald) |
--positive-color |
#2d7d32 |
#4caf50 |
Positive/green values |
--positive-bg |
#e8f5e8 |
#1a3d1a |
Positive background |
--negative-color |
#c33 |
#f44336 |
Negative/red values |
--negative-bg |
#fee |
#3d1a1a |
Negative background |
--primary-color |
#374151 |
#d1d5db |
Primary buttons |
--tab-bg |
#f8f9fa |
#2a2a2a |
Tab backgrounds |
--tab-hover-bg |
#f0f1f2 |
#3a3a3a |
Tab hover |
--warning-bg |
#fff3cd |
#3d3a1a |
Warning backgrounds |
--warning-color |
#856404 |
#ffc107 |
Warning text |
Rules:
- ALWAYS use
var(--xxx)for text, backgrounds, and borders — never hardcode#fffor#000. - For brand/semantic colors (red for critical, blue for coalition, etc.) use direct hex values — these don't change with theme.
- When brand colors need a translucent background, use
rgba()with low opacity (0.08–0.15). If dark mode needs a different opacity, add an explicit override:.my-badge { background: rgba(220, 38, 38, 0.08); } [data-theme="dark"] .my-badge { background: rgba(220, 38, 38, 0.14); }
The app automatically executes <script> tags found in the HTML content after rendering. You can include vanilla JS for interactivity (tab switching, filters, accordions, sliders, etc.). Scripts run inside the page DOM — they have access to document.querySelector etc.
Important: Use an IIFE to avoid polluting the global scope. Expose interactive functions on window with a unique prefix:
<script>
(function() {
function switchTab(key) { /* ... */ }
window.__myPrefixSwitchTab = switchTab;
switchTab('default');
})();
</script><style>
/* All CSS for this page — no scoping needed */
/* Use CSS variables from the table above for theme compatibility */
/* Include responsive breakpoints at 768px and 480px */
</style>
<div class="my-root">
<!-- Page content with bilingual text -->
</div>
<script>
(function() {
// Interactivity logic (tab switching, filters, etc.)
})();
</script>The app uses these fonts (already loaded globally):
| Font | Usage |
|---|---|
'Be Vietnam Pro' |
Body text, UI elements, labels — the primary font |
'Playfair Display' |
Display headings, editorial titles |
'JetBrains Mono' / 'SF Mono' / 'Fira Code' |
Monospace numbers, data values |
Section title:
<div class="section-title" style="color: #2563EB">
<span data-lang="vi">Tiêu đề phần</span><span data-lang="en">Section Title</span>
</div>.section-title {
font-family: 'Be Vietnam Pro', sans-serif;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 12px;
}Card:
.card {
background: var(--card-bg);
border: 1px solid var(--border-color);
padding: 16px;
margin-bottom: 12px;
}Callout / assessment box:
.callout {
background: var(--surface-2);
border-left: 3px solid #2c6fbb;
padding: 16px 20px;
font-size: 13px;
line-height: 1.75;
color: var(--text-secondary);
}Metric chip (small info badge):
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
color: var(--text-tertiary);
padding: 5px 10px;
border: 1px solid var(--border-color);
border-radius: 2px;
}
.chip strong { color: var(--text-color); font-weight: 600; }Tab bar:
.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 16px; }
.tab {
background: transparent;
border: 1px solid var(--border-color);
padding: 6px 14px;
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.tab.active {
background: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}Tooltip (CSS-only hover):
<span class="ttip-anchor">
<span class="ttip-icon">?</span>
<span class="ttip-popup">Tooltip text here</span>
</span>.ttip-anchor { position: relative; display: inline-block; cursor: help; }
.ttip-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 14px; height: 14px; font-size: 9px; font-weight: 700;
border-radius: 50%; border: 1px solid var(--border-color);
color: var(--text-tertiary); background: var(--surface-2);
}
.ttip-popup {
display: none; position: absolute; bottom: calc(100% + 8px); left: 50%;
transform: translateX(-50%); background: var(--text-color); color: var(--bg-color);
padding: 8px 12px; font-size: 11px; line-height: 1.5; white-space: nowrap;
border-radius: 4px; z-index: 100; pointer-events: none;
}
.ttip-anchor:hover .ttip-popup { display: block; }Crisis badge with pulse animation:
.pulse-dot {
width: 8px; height: 8px; border-radius: 50%; background: #DC2626;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}Data table:
.data-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.data-table th {
text-align: left; padding: 8px 10px; font-weight: 600;
color: var(--text-secondary); border-bottom: 2px solid var(--border-color);
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
}
.data-table td {
padding: 8px 10px; border-bottom: 1px solid var(--border-color);
color: var(--text-color);
}
.data-table tr:hover { background: var(--hover-bg); }Filter buttons:
.filter-btn {
padding: 5px 12px; font-size: 10px; font-weight: 600;
background: transparent; border: 1px solid var(--border-color);
color: var(--text-secondary); cursor: pointer;
}
.filter-btn.active {
background: var(--text-color); color: var(--bg-color);
border-color: var(--text-color);
}Always include responsive styles:
@media (max-width: 768px) {
/* Tablet: stack grids to 1-2 columns, reduce padding */
}
@media (max-width: 480px) {
/* Mobile: single column, smaller fonts, collapse filters */
}For pages with multiple tabs:
<div class="tabs" role="tablist">
<button class="tab active" data-tab-key="overview" onclick="window.__mySwitch('overview')" role="tab" aria-selected="true">
<span data-lang="vi">Tổng Quan</span><span data-lang="en">Overview</span>
</button>
<button class="tab" data-tab-key="details" onclick="window.__mySwitch('details')" role="tab" aria-selected="false">
<span data-lang="vi">Chi Tiết</span><span data-lang="en">Details</span>
</button>
</div>
<div class="tab-content" data-tab="overview">
<!-- Overview content -->
</div>
<div class="tab-content" data-tab="details" style="display:none">
<!-- Details content -->
</div>
<script>
(function() {
function switchTab(key) {
document.querySelectorAll('.tab-content').forEach(function(el) {
el.style.display = el.getAttribute('data-tab') === key ? '' : 'none';
});
document.querySelectorAll('.tab').forEach(function(el) {
var isActive = el.getAttribute('data-tab-key') === key;
el.classList.toggle('active', isActive);
el.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
}
window.__mySwitch = switchTab;
})();
</script><div class="accordion-item">
<div class="accordion-header" onclick="this.parentElement.classList.toggle('open')">
<span>Title</span>
<span class="toggle-icon">+</span>
</div>
<div class="accordion-body">
<p>Expandable content here</p>
</div>
</div>.accordion-body { display: none; }
.accordion-item.open .accordion-body { display: block; }
.accordion-item.open .toggle-icon::after { content: '−'; }- No
<html>,<head>, or<body>tags - All CSS in a single
<style>block at the top - All JS in a
<script>block at the bottom, wrapped in IIFE - All translatable text uses
<span data-lang="vi">/<span data-lang="en">pairs - Shared content (numbers, names, dates) is NOT wrapped in data-lang
- All colors use CSS variables (
var(--text-color), etc.) — no hardcoded black/white - Dark mode overrides use
[data-theme="dark"] .class { ... }where needed - Responsive styles at 768px and 480px breakpoints
- Interactive functions use
window.__uniquePrefixnamespace - Font families:
'Be Vietnam Pro'for body,'Playfair Display'for headings -
border-radius: 0or2px(the app uses sharp/minimal corners)