PageSourceSearch

https://web-production-0a75.up.railway.app/

html web-production-0a75.up.railway.app collected 2026-10-03 12:14:43 UTC 111,741 bytes, 2,097 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"/>
6<title>PAL Network — ICAN ICAR Dashboard</title>
7<!-- Google tag (gtag.js) -->
vendor: 64 bytes, lines 7-8
7
8<script async src="https://www.googletagmanager.com/gtag/js?id=
8G-LPRWNRKRQV
vendor: 12 bytes, line 8
8"></script>
9<script>
10  
vendor: 133 bytes, lines 10-13
10window.dataLayer = window.dataLayer || [];
11  function gtag(){dataLayer.push(arguments);}
12  gtag('js', new Date());
13  gtag('config', '
13G-LPRWNRKRQV
vendor: 4 bytes, line 13
13');
14</script>
vendor: 1 bytes, line 14
14
15<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
vendor: 1 bytes, line 15
15
16<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
16
17<style>
18*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
19body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#F0F4F8;color:#1A202C;height:100vh;display:flex;flex-direction:column;overflow:hidden}
20
21/* ── Navbar ───────────────────────────────────────────── */
22.navbar{background:#fff;color:#1A202C;padding:8px 24px;display:flex;align-items:center;gap:20px;position:sticky;top:0;z-index:100;box-shadow:0 1px 6px rgba(0,0,0,.12);border-bottom:1px solid #E2E8F0;flex-shrink:0}
23.navbar-logo{display:flex;align-items:center;gap:0;flex-shrink:0}
24.navbar-divider{width:1px;height:36px;background:#E2E8F0;margin:0 4px}
25.navbar-survey{display:flex;flex-direction:column}
26.navbar-survey-title{font-size:13px;font-weight:700;color:#014663;letter-spacing:.3px}
27.navbar-survey-sub{font-size:11px;color:#718096}
28.navbar-tag{background:#66B255;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.8px;margin-left:auto;color:#fff}
29
30/* ── Page tabs ────────────────────────────────────────── */
31.page-tabs{display:flex;background:#fff;border-bottom:2px solid #E2E8F0;padding:0 20px;flex-shrink:0}
32.page-tab{padding:10px 20px;font-size:13px;font-weight:600;color:#718096;border:none;background:none;cursor:pointer;border-bottom:3px solid transparent;margin-bottom:-2px;transition:all .15s}
33.page-tab.active{color:#014663;border-bottom-color:#014663}
34.page-tab:hover:not(.active){color:#2D3748;background:#F7FAFC}
35
36/* ── Layout ───────────────────────────────────────────── */
37.layout{display:flex;flex:1;min-height:0;overflow:hidden}
38
39/* ── Sidebar ──────────────────────────────────────────── */
40.sidebar{width:260px;min-width:260px;flex-shrink:0;background:#fff;border-right:1px solid #E2E8F0;overflow-y:auto;overflow-x:hidden;padding:12px}
41
42details{border:1px solid #E2E8F0;border-radius:8px;margin-bottom:6px;overflow:hidden}
43summary{list-style:none;padding:8px 12px;cursor:pointer;background:#F7FAFC;font-size:12px;font-weight:600;color:#4A5568;display:flex;justify-content:space-between;align-items:center;user-select:none}
44summary::-webkit-details-marker{display:none}
45summary::after{content:'▶';font-size:9px;transition:transform .2s;color:#A0AEC0}
46details[open] summary::after{transform:rotate(90deg)}
47summary:hover{background:#EDF2F7}
48.filter-body{padding:10px 12px;background:#fff}
49
50.cb-list{display:flex;flex-direction:column;gap:2px;max-height:160px;overflow-y:auto}
51.cb-item{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;padding:3px 4px;border-radius:4px}
52.cb-item:hover{background:#EDF2F7}
53.cb-item input[type=checkbox]{cursor:pointer;accent-color:#014663;flex-shrink:0}
54.cb-actions{display:flex;gap:4px;margin-top:5px}
55
56.pill-row{display:flex;gap:4px;flex-wrap:wrap}
57.pill{padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600;border:1.5px solid #CBD5E0;background:#fff;cursor:pointer;transition:all .15s;color:#4A5568}
58.pill.active{background:#014663;border-color:#014663;color:#fff}
59.pill:hover:not(.active){background:#EDF2F7}
60
61.chip{font-size:11px;padding:2px 7px;border-radius:10px;background:#EDF2F7;border:1px solid #CBD5E0;cursor:pointer;color:#4A5568;transition:all .15s}
62.chip:hover{background:#014663;color:#fff;border-color:#014663}
63
64.filter-select{width:100%;padding:6px 8px;border-radius:6px;border:1.5px solid #CBD5E0;font-size:12px;background:#fff;color:#2D3748;cursor:pointer;outline:none}
65.filter-select:focus{border-color:#014663}
66
67.btn-row{display:flex;gap:6px;margin-top:8px}
68.btn{flex:1;padding:8px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:none;transition:all .15s}
69.btn-primary{background:#014663;color:#fff}
70.btn-primary:hover{background:#012f47}
71.btn-reset{background:#EDF2F7;color:#4A5568;border:1.5px solid #CBD5E0}
72.btn-reset:hover{background:#E2E8F0}
73.sidebar-n{font-size:12px;color:#1A202C;text-align:center;margin-top:4px}
74
75/* ── Main area ────────────────────────────────────────── */
76.main{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:14px}
77.page{display:none;flex-direction:column;gap:14px}
78.page.active{display:flex}
79
80/* Controls row */
81.controls-row{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap}
82.ctrl-group{display:flex;flex-direction:column;gap:4px}
83.ctrl-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#718096}
84.mpl-tabs{display:flex;border-radius:8px;overflow:hidden;border:1.5px solid #CBD5E0}
85.mpl-tab{padding:7px 14px;font-size:12px;font-weight:600;background:#fff;color:#4A5568;border:none;cursor:pointer;border-right:1px solid #CBD5E0;transition:all .15s}
86.mpl-tab:last-child{border-right:none}
87.mpl-tab.active{background:#014663;color:#fff}
88.mpl-tab:hover:not(.active){background:#EDF2F7}
89
90/* Info note */
91.info-note{background:#EBF8FF;border:1px solid #90CDF4;border-radius:8px;padding:10px 14px;font-size:12px;color:#014663;line-height:1.6}
92.info-note details summary{background:transparent;padding:0;font-size:12px;font-weight:600;color:#014663;border:none}
93.info-note details[open] summary{margin-bottom:8px}
94.info-note details .filter-body{padding:0;background:transparent;font-size:12px;color:#2C5282;line-height:1.6}
95
96/* Chart box */
97.chart-box{background:#fff;border-radius:12px;padding:18px 20px;box-shadow:0 1px 4px rgba(0,0,0,.07)}
98.chart-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px;flex-wrap:wrap;gap:8px}
99.chart-title{font-size:13px;font-weight:700;color:#014663}
100.chart-sub{font-size:11px;color:#718096;margin-top:2px}
101.warn-note{font-size:11px;color:#C05621;background:#FFFAF0;border:1px solid #F6AD55;border-radius:6px;padding:3px 9px}
102.chart-wrap{position:relative;height:380px}
103
104/* All-MPL 2-panel grid (Math + Reading only) */
105.chart-all-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
106.chart-panel{background:#F7FAFC;border-radius:10px;padding:14px 16px;border:1px solid #E2E8F0}
107.chart-panel-title{font-size:12px;font-weight:700;text-align:center;color:#014663;margin-bottom:10px;padding-bottom:6px;border-bottom:2px solid #66B255}
108
109/* Table */
110.table-box{background:#fff;border-radius:12px;padding:16px 20px;box-shadow:0 1px 4px rgba(0,0,0,.07);overflow-x:auto}
111.table-title{font-size:13px;font-weight:700;margin-bottom:10px;color:#014663}
112table{width:100%;border-collapse:collapse;font-size:12px}
113thead th{background:#F7FAFC;padding:7px 12px;text-align:left;font-weight:700;color:#4A5568;border-bottom:2px solid #E2E8F0;font-size:11px;text-transform:uppercase;letter-spacing:.6px;white-space:nowrap}
114tbody tr:hover{background:#F7FAFC}
115tbody td{padding:7px 12px;border-bottom:1px solid #EDF2F7}
116.pct-bar-wrap{display:flex;align-items:center;gap:6px}
117.pct-mini-bar{height:6px;border-radius:3px;background:#014663;min-width:2px}
118.suppressed{color:#C05621;font-style:italic;font-size:11px}
119.n-col{color:#A0AEC0;font-size:11px}
120.warn-badge{display:inline-flex;align-items:center;gap:3px;background:#FFFAF0;border:1px solid #F6AD55;color:#C05621;font-size:10px;font-weight:600;padding:2px 6px;border-radius:10px}
121
122/* Access tab */
123.access-section{display:flex;flex-direction:column;gap:10px}
124.section-hdr{font-size:13px;font-weight:700;color:#014663;padding:6px 0;border-bottom:2px solid #66B255;margin-bottom:4px}
125.access-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
126.access-card{background:#fff;border-radius:10px;padding:14px 16px;box-shadow:0 1px 4px rgba(0,0,0,.07)}
127.access-card-title{font-size:10px;font-weight:700;color:#014663;text-transform:uppercase;letter-spacing:.7px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}
128.btn-export{padding:4px 9px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;border:1.5px solid #CBD5E0;background:#fff;color:#4A5568;display:inline-flex;align-items:center;gap:3px;transition:all .15s;white-space:nowrap;flex-shrink:0}
129.btn-export:hover{background:#014663;color:#fff;border-color:#014663}
130.export-btns{display:flex;gap:6px;align-items:center;flex-shrink:0}
131.access-note{font-size:11px;color:#718096;background:#F7FAFC;border-radius:6px;padding:8px 12px;border:1px solid #E2E8F0}
132.acc-wrap{position:relative;height:300px}
133
134/* Loading */
135.loading{display:none;position:fixed;inset:0;background:rgba(255,255,255,.8);z-index:200;align-items:center;justify-content:center;flex-direction:column;gap:10px}
136.loading.show{display:flex}
137.spinner{width:34px;height:34px;border:4px solid #E2E8F0;border-top-color:#014663;border-radius:50%;animation:spin .7s linear infinite}
138@keyframes spin{to{transform:rotate(360deg)}}
139
140/* Chatbot */
141.chat-fab{position:fixed;bottom:24px;right:24px;width:54px;height:54px;border-radius:50%;background:#014663;color:#fff;font-size:22px;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;z-index:300;transition:transform .2s}
142.chat-fab:hover{transform:scale(1.08)}
143.chat-panel{position:fixed;bottom:90px;right:24px;width:370px;height:510px;background:#fff;border-radius:14px;box-shadow:0 8px 40px rgba(0,0,0,.2);display:none;flex-direction:column;z-index:300;overflow:hidden;border:1px solid #E2E8F0}
144.chat-panel.open{display:flex}
145.chat-hdr{background:#014663;color:#fff;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}
146.chat-hdr-title{font-size:14px;font-weight:700}
147.chat-hdr-sub{font-size:11px;color:#90CDF4;margin-top:1px}
148.chat-close{background:none;border:none;color:#90CDF4;font-size:18px;cursor:pointer}
149.chat-msgs{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;background:#F7FAFC}
150.msg{max-width:86%;display:flex;flex-direction:column;gap:2px}
151.msg.user{align-self:flex-end;align-items:flex-end}
152.msg.assistant{align-self:flex-start}
153.msg-bubble{padding:8px 12px;border-radius:12px;font-size:13px;line-height:1.5}
154.msg.user .msg-bubble{background:#014663;color:#fff;border-radius:12px 12px 4px 12px}
155.msg.assistant .msg-bubble{background:#fff;border:1px solid #E2E8F0;white-space:pre-wrap;color:#1A202C;border-radius:12px 12px 12px 4px}
156.msg-time{font-size:10px;color:#A0AEC0}
157.typing-dots{display:flex;gap:4px;padding:8px 12px}
158.typing-dots span{width:7px;height:7px;border-radius:50%;background:#CBD5E0;animation:bounce .9s infinite}
159.typing-dots span:nth-child(2){animation-delay:.15s}
160.typing-dots span:nth-child(3){animation-delay:.3s}
161@keyframes bounce{0%,80%,100%{transform:scale(.8)}40%{transform:scale(1.2)}}
162.chat-examples{padding:10px 14px;background:#EDF2F7;border-top:1px solid #E2E8F0}
163.chat-examples p{font-size:11px;color:#718096;margin-bottom:5px;font-weight:600}
164.ex-chips{display:flex;flex-wrap:wrap;gap:4px}
165.chat-input-row{padding:10px;border-top:1px solid #E2E8F0;display:flex;gap:6px;background:#fff}
166.chat-input{flex:1;padding:7px 12px;border-radius:18px;border:1.5px solid #CBD5E0;font-size:13px;outline:none;resize:none}
167.chat-input:focus{border-color:#014663}
168.chat-send{width:34px;height:34px;border-radius:50%;background:#014663;color:#fff;border:none;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
169.chat-send:hover{background:#012f47}
170.chat-send:disabled{background:#CBD5E0;cursor:default}
171
172/* Language toggle */
173.lang-btn{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:700;border:1.5px solid #CBD5E0;background:#fff;color:#4A5568;cursor:pointer;transition:all .15s}
174.lang-btn.active{background:#014663;color:#fff;border-color:#014663}
175.lang-btn:hover:not(.active){background:#EDF2F7}
176
177/* ── Guide button & modal ─────────────────────────────────── */
178.guide-btn{padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;border:1.5px solid #014663;background:#fff;color:#014663;cursor:pointer;transition:all .15s;margin-left:8px;white-space:nowrap}
179.guide-btn:hover{background:#014663;color:#fff}
180.labels-toggle-btn{padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;border:1.5px solid #CBD5E0;background:#fff;color:#4A5568;cursor:pointer;transition:all .15s;margin-left:8px;white-space:nowrap}
181.labels-toggle-btn:hover{border-color:#014663;color:#014663}
182.labels-toggle-btn.active{background:#014663;border-color:#014663;color:#fff}
183.guide-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1000;align-items:center;justify-content:center}
184.guide-overlay.open{display:flex}
185.guide-modal-box{background:#fff;border-radius:12px;width:min(900px,94vw);height:90vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,.25)}
186.guide-modal-hdr{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #E2E8F0;flex-shrink:0}
187.guide-iframe{flex:1;border:none;width:100%}
188.guide-close-btn{background:none;border:none;font-size:16px;cursor:pointer;color:#718096;padding:2px 6px;border-radius:4px;line-height:1}
189.guide-close-btn:hover{background:#EDF2F7;color:#1A202C}
190.guide-download-btn{font-size:11px;font-weight:600;color:#014663;text-decoration:none;padding:4px 10px;border:1.5px solid #014663;border-radius:5px;transition:all .15s}
191.guide-download-btn:hover{background:#014663;color:#fff}
192
193@media(max-width:900px){
194  .chart-all-grid{grid-template-columns:1fr}
195  .access-grid{grid-template-columns:1fr}
196  .sidebar{width:220px;min-width:220px}
197}
198@media(max-width:680px){
199  .layout{flex-direction:column;height:auto}
200  .sidebar{width:100%;border-right:none;border-bottom:1px solid #E2E8F0;max-height:none;overflow-y:visible}
201}
202</style>
203</head>
204<body>
205
206<nav class="navbar">
207  <div class="navbar-logo">
208    <img src="/static/pal_logo.png" alt="PAL Network" style="height:50px;width:auto"/>
209  </div>
210  <div class="navbar-divider"></div>
211  <div class="navbar-survey">
212    <div class="navbar-survey-title">ICAN · ICAR Learning Outcomes Dashboard</div>
213    <div class="navbar-survey-sub">2025 Survey · 11 Countries · ~89,000 Children</div>
214  </div>
215  <span class="navbar-tag">2025</span>
216  <div style="display:flex;gap:4px;margin-left:8px">
217    <button class="lang-btn active" onclick="applyLanguage('en')">EN</button>
218    <button class="lang-btn" onclick="applyLanguage('fr')">FR</button>
219    <button class="lang-btn" onclick="applyLanguage('es')">ES</button>
220    <button class="lang-btn" onclick="applyLanguage('pt')">PT</button>
221  </div>
222<button class="guide-btn" onclick="openDemo()" title="Watch a demo">▶ Watch a Demo</button>
223<button class="guide-btn" onclick="openGuide()" title="User Guide">📖 User Guide</button>
224</nav>
225
226<!-- ── User Guide Modal ──────────────────────────────────────── -->
227<div id="guide-modal" class="guide-overlay" onclick="closeGuideOnOverlay(event)">
228  <div class="guide-modal-box">
229    <div class="guide-modal-hdr">
230      <span style="font-weight:700;font-size:14px;color:#014663">📖 Dashboard User Guide</span>
231      <div style="display:flex;gap:8px;align-items:center">
232        <a href="/static/user-guide.pdf" download="ICAN-ICAR_Dashboard_User_Guide.pdf" class="guide-download-btn">⬇ Download PDF</a>
233        <button class="guide-close-btn" onclick="closeGuide()" title="Close">✕</button>
234      </div>
235    </div>
236    <iframe id="guide-iframe" src="" class="guide-iframe" title="User Guide"></iframe>
237  </div>
238</div>
239
240<div id="demo-modal" class="guide-overlay" onclick="closeDemoOnOverlay(event)">
241  <div class="guide-modal-box" style="height:auto;max-height:90vh">
242    <div class="guide-modal-hdr">
243      <span style="font-weight:700;font-size:14px;color:#014663">▶ Dashboard Demo</span>
244      <button class="guide-close-btn" onclick="closeDemo()" title="Close">✕</button>
245    </div>
246    <div style="padding:16px;background:#000;border-radius:0 0 12px 12px">
247      <video id="demo-video" controls style="width:100%;border-radius:6px;display:block;max-height:75vh">
248        <source src="/static/dashboard_demo_final.mp4" type="video/mp4">
249      </video>
250    </div>
251  </div>
252</div>
253
254<div class="page-tabs">
255  <button class="page-tab active" onclick="switchPage('mpl',this)">📊 MPL Results</button>
256  <button class="page-tab" onclick="switchPage('access',this)">🌐 Reach &amp; Access</button>
257</div>
258
259<div class="loading" id="loading"><div class="spinner"></div><span style="font-size:13px;color:#4A5568">Loading…</span></div>
260
261<div class="layout">
262
263<!-- ── Sidebar ──────────────────────────────────────────────────────────────── -->
264<aside class="sidebar">
265
266  <details open>
267    <summary>Countries</summary>
268    <div class="filter-body">
269      <div class="cb-list" id="country-checks"></div>
270      <div class="cb-actions">
271        <span class="chip" onclick="selectAll('country-checks','country-cb')">All</span>
272        <span class="chip" onclick="clearAll('country-checks','country-cb')">None</span>
273      </div>
274    </div>
275  </details>
276
277  <details>
278    <summary>Gender</summary>
279    <div class="filter-body">
280      <div class="pill-row" id="gender-pills">
281        <button class="pill active" data-val="all" onclick="setPill(this,'gender-pills',setGender)">All</button>
282        <button class="pill" data-val="Girls" onclick="setPill(this,'gender-pills',setGender)">Girls</button>
283        <button class="pill" data-val="Boys" onclick="setPill(this,'gender-pills',setGender)">Boys</button>
284      </div>
285    </div>
286  </details>
287
288  <details>
289    <summary>Location</summary>
290    <div class="filter-body">
291      <div class="pill-row" id="location-pills">
292        <button class="pill active" data-val="all" onclick="setPill(this,'location-pills',setLocation)">All</button>
293        <button class="pill" data-val="Urban" onclick="setPill(this,'location-pills',setLocation)">Urban</button>
294        <button class="pill" data-val="Rural" onclick="setPill(this,'location-pills',setLocation)">Rural</button>
295      </div>
296    </div>
297  </details>
298
299  <details>
300    <summary>Age</summary>
301    <div class="filter-body">
302      <div class="cb-list" id="age-checks"></div>
303      <div class="cb-actions">
304        <span class="chip" onclick="selectAll('age-checks','age-cb')">All</span>
305        <span class="chip" onclick="clearAll('age-checks','age-cb')">None</span>
306      </div>
307    </div>
308  </details>
309
310  <details>
311    <summary>Grade</summary>
312    <div class="filter-body">
313      <div style="font-size:11px;color:#A0AEC0;margin-bottom:4px">Enrolled children only · Pre-Primary to Grade 8</div>
314      <div class="cb-list" id="grade-checks"></div>
315      <div class="cb-actions">
316        <span class="chip" onclick="selectAll('grade-checks','grade-cb')">All</span>
317        <span class="chip" onclick="clearAll('grade-checks','grade-cb')">None</span>
318      </div>
319    </div>
320  </details>
321
322  <details id="ctx-details">
323    <summary>Contextual Variable</summary>
324    <div class="filter-body">
325      <div style="font-size:11px;color:#C05621;margin-bottom:6px">⚠ One at a time only</div>
326      <select class="filter-select" id="ctx-var-select" onchange="onCtxVarChange()">
327        <option value="">— None —</option>
328      </select>
329      <div id="ctx-values-wrap" style="display:none;margin-top:8px">
330        <div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:#718096;margin-bottom:4px">Filter by:</div>
331        <div class="cb-list" id="ctx-value-checks"></div>
332      </div>
333    </div>
334  </details>
335
336  <div class="btn-row">
337    <button class="btn btn-primary" onclick="applyFilters()">Apply</button>
338    <button class="btn btn-reset" onclick="resetFilters()">Reset</button>
339  </div>
340  <div class="sidebar-n" id="sidebar-n"></div>
341</aside>
342
343<!-- ── Main area ─────────────────────────────────────────────────────────────── -->
344<main class="main">
345
346  <!-- MPL Page -->
347  <div class="page active" id="page-mpl">
348
349    <div class="controls-row">
350      <div class="ctrl-group">
351        <div class="ctrl-label">Break down by</div>
352        <select class="filter-select" style="width:240px" id="breakdown-select" onchange="onBreakdownChange()">
353          <option value="country">Country</option>
354          <option value="gender">Gender (Girls / Boys)</option>
355          <option value="location">Location (Urban / Rural)</option>
356          <option value="grade">Grade (learning trajectory)</option>
357          <option value="age">Age (age trajectory)</option>
358          <option value="enrollment">Enrollment Status</option>
359          <option value="home_lang">Home Language Matches Assessment</option>
360          <option value="parental_edu">Highest Parental Education</option>
361          <option value="phone">Type of Phone in Home</option>
362          <option value="computer">Access to Computer/Tablet</option>
363          <option value="school_type">School Type</option>
364        </select>
365      </div>
366      <div class="ctrl-group">
367        <div class="ctrl-label">MPL Indicator</div>
368        <div class="mpl-tabs">
369          <button class="mpl-tab active" data-mpl="MPL_math"  onclick="setMPL(this)">Math</button>
370          <button class="mpl-tab" data-mpl="MPL_lang"         onclick="setMPL(this)">Reading</button>
371          <button class="mpl-tab" data-mpl="MPL_both"         onclick="setMPL(this)">Both</button>
372          <button class="mpl-tab" data-mpl="ALL"              onclick="setMPL(this)">All</button>
373        </div>
374      </div>
375      <div class="ctrl-group">
376        <div class="ctrl-label">Enable Chart Labels</div>
377        <button id="labels-toggle" class="labels-toggle-btn active" onclick="toggleLabels()">Labels On</button>
378      </div>
379    </div>
380
381    <!-- MPL info note -->
382    <div class="info-note">
383      <details>
384        <summary>ℹ What is MPL?</summary>
385        <div class="filter-body">
386          <strong>Minimum Proficiency Level (MPL)</strong> is the global benchmark for foundational learning under SDG 4.1.1(a). It represents the minimum competencies children need for future learning.<br><br>
387          <strong>For Reading:</strong> An MPL-aligned assessment confirms a child can accurately decode and understand simple Grade-2 text. This is measured by at least 20 score points, with a minimum of 10 dedicated to reading comprehension tasks such as recognizing common words and retrieving explicit information from the text.<br><br>
388          <strong>For Mathematics:</strong> The MPL indicates a child has mastered core number sense and computation skills. Alignment requires at least 20 score points linked to Grade-2 content, with 10 points focused on the Number and Operations domain. To meet this threshold, children must demonstrate proficiency in at least three subconstructs: identifying and counting whole numbers, representing numbers in equivalent ways, solving basic operations, or solving simple real-world problems.<br><br>
389        </div>
390      </details>
391    </div>
392
393    <!-- Chart box -->
394    <div class="chart-box" id="main-chart-box">
395      <div class="chart-header">
396        <div>
397          <div class="chart-title" id="chart-title">Percentage of children achieving Minimum Proficiency Levels in Math by country</div>
398          <div class="chart-sub" id="chart-sub">Survey-weighted estimates</div>
399        </div>
400        <div style="display:flex;align-items:center;gap:8px">
401          <div class="warn-note" id="warn-note" style="display:none">⚠ Some cells suppressed (N &lt; 100)</div>
402          <div class="export-btns">
403            <button class="btn-export" onclick="exportChartPNG()" title="Download chart as PNG">↓ PNG</button>
404            <button class="btn-export" onclick="exportTableExcel()" title="Download data as Excel">↓ Excel</button>
405          </div>
406        </div>
407      </div>
408      <!-- Single MPL chart -->
409      <div id="single-chart-wrap" class="chart-wrap">
410        <canvas id="mpl-chart"></canvas>
411      </div>
412      <!-- All-MPL 2-panel (Math + Reading) -->
413      <div id="all-chart-wrap" class="chart-all-grid" style="display:none">
414        <div class="chart-panel">
415          <div class="chart-panel-title">Math</div>
416          <div style="position:relative;height:280px"><canvas id="chart-math"></canvas></div>
417        </div>
418        <div class="chart-panel">
419          <div class="chart-panel-title">Reading</div>
420          <div style="position:relative;height:280px"><canvas id="chart-lang"></canvas></div>
421        </div>
422      </div>
423    </div>
424
425    <!-- Data table -->
426    <div class="table-box">
427      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
428        <div class="table-title" style="margin-bottom:0" id="table-title">Detailed Results</div>
429        <button class="btn-export" onclick="exportTableExcel()" title="Download table as Excel">↓ Excel</button>
430      </div>
431      <table>
432        <thead>
433          <tr>
434            <th id="th-group">Country</th>
435            <th>MPL Math</th><th>MPL Reading</th><th>MPL Both</th>
436            <th>N (unweighted)</th>
437          </tr>
438        </thead>
439        <tbody id="results-tbody">
440          <tr><td colspan="5" style="text-align:center;color:#A0AEC0;padding:24px">Apply filters to see results</td></tr>
441        </tbody>
442      </table>
443    </div>
444
445  </div><!-- end page-mpl -->
446
447  <!-- Access Page -->
448  <div class="page" id="page-access">
449
450    <div class="access-note">
451      The following charts show the context of communities we visited that include: children's access to school by gender, location, school type and availability of different assets in the households.
452    </div>
453
454    <div class="controls-row" style="margin-bottom:4px">
455      <div class="ctrl-group">
456        <div class="ctrl-label">View indicator</div>
457        <select class="filter-select" style="width:340px" id="access-view-select" onchange="onAccessViewChange()">
458          <option value="all">All indicators</option>
459          <optgroup label="Children's access to school">
460            <option value="acc-enrolled">Percentage of children currently enrolled</option>
461            <option value="acc-gender">Enrolled by gender (Girls vs Boys)</option>
462            <option value="acc-location">Enrolled by location (Rural vs Urban)</option>
463            <option value="acc-school">Enrolled by school type</option>
464          </optgroup>
465          <optgroup label="Household access">
466            <option value="acc-homelang">Home language matches assessment</option>
467            <option value="acc-computer">Households with computer or tablet</option>
468            <option value="acc-phone">Households by phone availability</option>
469            <option value="acc-edu">Parental education level</option>
470          </optgroup>
471        </select>
472      </div>
473    </div>
474
475    <!-- Child Reach section -->
476    <div class="access-section">
477      <div class="section-hdr">Children's access to school</div>
478      <div class="access-grid">
479        <div class="access-card">
480          <div class="access-card-title">Percentage of children currently enrolled <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-enrolled','Enrolled')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-enrolled','Enrolled')">↓ Excel</button></div></div>
481          <div class="acc-wrap"><canvas id="acc-enrolled"></canvas></div>
482        </div>
483        <div class="access-card">
484          <div class="access-card-title">Percentage of children currently enrolled by gender (Girls vs Boys) <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-gender','Gender')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-gender','Gender')">↓ Excel</button></div></div>
485          <div class="acc-wrap"><canvas id="acc-gender"></canvas></div>
486        </div>
487        <div class="access-card">
488          <div class="access-card-title">Percentage of children currently enrolled by location (Rural vs Urban) <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-location','Location')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-location','Location')">↓ Excel</button></div></div>
489          <div class="acc-wrap">
489<canvas id="acc-location"></canvas></div>
490        </div>
491        <div class="access-card">
492          <div class="access-card-title">Percentage of children currently enrolled by school type <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-school','School_Type')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-school','School_Type')">↓ Excel</button></div></div>
493          <div class="acc-wrap"><canvas id="acc-school"></canvas></div>
494        </div>
495      </div>
496    </div>
497
498    <!-- Household Access section -->
499    <div class="access-section">
500      <div class="section-hdr">Access to various assets within households</div>
501      <div class="access-grid">
502        <div class="access-card">
503          <div class="access-card-title">Percentage of households speaking same language as the assessment <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-homelang','Home_Language')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-homelang','Home_Language')">↓ Excel</button></div></div>
504          <div class="acc-wrap"><canvas id="acc-homelang"></canvas></div>
505        </div>
506        <div class="access-card">
507          <div class="access-card-title">Percentage of households with a computer or tablet available <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-computer','Computer_Tablet')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-computer','Computer_Tablet')">↓ Excel</button></div></div>
508          <div class="acc-wrap"><canvas id="acc-computer"></canvas></div>
509        </div>
510        <div class="access-card">
511          <div class="access-card-title">Percentage of households by phone availability <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-phone','Phone_Type')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-phone','Phone_Type')">↓ Excel</button></div></div>
512          <div class="acc-wrap"><canvas id="acc-phone"></canvas></div>
513        </div>
514        <div class="access-card">
515          <div class="access-card-title">Percentage of households with educated parents by highest level of education attained <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-edu','Parental_Education')">↓ PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-edu','Parental_Education')">↓ Excel</button></div></div>
516          <div class="acc-wrap"><canvas id="acc-edu"></canvas></div>
517        </div>
518      </div>
519    </div>
520
521  </div><!-- end page-access -->
522
523</main>
524</div>
525
526<!-- Chatbot -->
527<button class="chat-fab" onclick="toggleChat()" title="Ask about the data">💬</button>
528<div class="chat-panel" id="chat-panel">
529  <div class="chat-hdr">
530    <div><div class="chat-hdr-title">📊 Data Assistant</div>
531    <div class="chat-hdr-sub">Ask about MPL outcomes or reach & access</div></div>
532    <button class="chat-close" onclick="toggleChat()">✕</button>
533  </div>
534  <div class="chat-msgs" id="chat-msgs">
535    <div class="msg assistant">
536      <div class="msg-bubble">Hi! Ask me about learning outcomes or access indicators — e.g. "What % of children in Kenya achieve MPL in reading?" or "What % of households in Mali have smartphones?" ✅</div>
537    </div>
538  </div>
539  <div class="chat-examples">
540    <p>Try asking:</p>
541    <div class="ex-chips">
542      <span class="chip" onclick="sendEx(this)">MPL reading rate in Kenya?</span>
543      <span class="chip" onclick="sendEx(this)">Girls vs boys math MPL in Tanzania</span>
544      <span class="chip" onclick="sendEx(this)">% enrolled in Mali by location</span>
545      <span class="chip" onclick="sendEx(this)">Smartphone access in Pakistan</span>
546    </div>
547  </div>
548  <div class="chat-input-row">
549    <textarea class="chat-input" id="chat-input" rows="1" placeholder="Ask a question…" onkeydown="chatKey(event)"></textarea>
550    <button class="chat-send" id="chat-send" onclick="sendChat()">➤</button>
551  </div>
552</div>
553
554<script>
555// ═══════════════════════════════════════════════════════════════════
556// PAL Color palette
557// ═══════════════════════════════════════════════════════════════════
558const PAL = {
559  teal:        '#014663',
560  green:       '#66B255',
561  darkGray:    '#61656B',
562  lightGray:   '#B4BBC1',
563  orange:      '#F37230',
564  lightOrange: '#FFB68D',
565  blue:        '#0077B6',
566  lightGreen:  '#90BE6D',
567  midGray:     '#6C757D',
568  deepOrange:  '#F9844A',
569  gold:        '#FFC300',
570};
571// Ordered palette for multi-series charts
572const COLORS = [
573  PAL.teal, PAL.green, PAL.orange, PAL.blue,
574  PAL.gold, PAL.deepOrange, PAL.lightGreen, PAL.darkGray,
575  PAL.lightOrange, PAL.midGray, PAL.lightGray,
576];
577
578// ═══════════════════════════════════════════════════════════════════
579// Translations
580// ═══════════════════════════════════════════════════════════════════
581const T = {
582  en: {
583    navTitle:       'ICAN · ICAR Learning Outcomes Dashboard',
584    navSub:         '2025 Survey · 11 Countries · ~89,000 Children',
585    tabMpl:         '📊 MPL Results',
586    tabAccess:      '🌐 Reach & Access',
587    loading:        'Loading…',
588    // Sidebar
589    sCountries:     'Countries',
590    sGender:        'Gender',
591    sLocation:      'Location',
592    sAge:           'Age',
593    sGrade:         'Grade',
594    sCtx:           'Contextual Variable',
595    sCtxWarn:       '⚠ One at a time only',
596    sCtxNone:       '— None —',
597    sCtxFilterBy:   'Filter by:',
598    sGradeNote:     'Enrolled children only · Pre-Primary to Grade 8',
599    sBtnApply:      'Apply',
600    sBtnReset:      'Reset',
601    sPillAll:       'All',
602    sPillGirls:     'Girls',
603    sPillBoys:      'Boys',
604    sPillUrban:     'Urban',
605    sPillRural:     'Rural',
606    sChipAll:       'All',
607    sChipNone:      'None',
608    // MPL controls
609    ctrlBreakdown:  'Break down by',
610    ctrlMpl:        'MPL Indicator',
611    bdCountry:      'Country',
612    bdGender:       'Gender (Girls / Boys)',
613    bdLocation:     'Location (Urban / Rural)',
614    bdGrade:        'Grade (learning trajectory)',
615    bdAge:          'Age (age trajectory)',
616    bdEnrollment:   'Enrollment Status',
617    bdHomeLang:     'Home Language Matches Assessment',
618    bdParentalEdu:  'Highest Parental Education',
619    bdPhone:        'Type of Phone in Home',
620    bdComputer:     'Access to Computer/Tablet',
621    bdSchoolType:   'School Type',
622    mplMath:        'Math',
623    mplReading:     'Reading',
624    mplBoth:        'Both',
625    mplAll:         'All',
626    // MPL info
627    mplInfoSummary: 'ℹ What is MPL?',
628    mplInfoBody: '<strong>Minimum Proficiency Level (MPL)</strong> is the global benchmark for foundational learning under SDG 4.1.1(a). It represents the minimum competencies children need for future learning.<br><br><strong>For Reading:</strong> An MPL-aligned assessment confirms a child can accurately decode and understand simple Grade-2 text. This is measured by at least 20 score points, with a minimum of 10 dedicated to reading comprehension tasks such as recognizing common words and retrieving explicit information from the text.<br><br><strong>For Mathematics:</strong> The MPL indicates a child has mastered core number sense and computation skills. Alignment requires at least 20 score points linked to Grade-2 content, with 10 points focused on the Number and Operations domain. To meet this threshold, children must demonstrate proficiency in at least three subconstructs: identifying and counting whole numbers, representing numbers in equivalent ways, solving basic operations, or solving simple real-world problems.',
629    // Chart
630    warnSuppressed: '⚠ Some cells suppressed (N < 100)',
631    chartSub:       'Survey-weighted estimates',
632    tableTitle:     'Detailed Results',
633    thMplMath:      'MPL Math',
634    thMplReading:   'MPL Reading',
635    thMplBoth:      'MPL Both',
636    thN:            'N (unweighted)',
637    noData:         'Apply filters to see results',
638    // Export buttons
639    exportPng:      '↓ PNG',
640    exportExcel:    '↓ Excel',
641    // Access tab
642    accessNote:     "The following charts show the context of communities we visited that include: children's access to school by gender, location, school type and availability of different assets in the households.",
643    accessViewLabel:'View indicator',
644    accessViewAll:  'All indicators',
645    accGrpSchool:   "Children's access to school",
646    accGrpHH:       'Access to various assets within households',
647    accOptEnrolled: 'Percentage of children currently enrolled',
648    accOptGender:   'Enrolled by gender (Girls vs Boys)',
649    accOptLocation: 'Enrolled by location (Rural vs Urban)',
650    accOptSchool:   'Enrolled by school type',
651    accOptHomelang: 'Home language matches assessment',
652    accOptComputer: 'Households with computer or tablet',
653    accOptPhone:    'Households by phone availability',
654    accOptEdu:      'Parental education level',
655    hdrSchool:      "Children's access to school",
656    hdrHH:          'Access to various assets within households',
657    cardEnrolled:   'Percentage of children currently enrolled',
658    cardGender:     'Percentage of children currently enrolled by gender (Girls vs Boys)',
659    cardLocation:   'Percentage of children currently enrolled by location (Rural vs Urban)',
660    cardSchool:     'Percentage of children currently enrolled by school type',
661    cardHomelang:   'Percentage of households speaking same language as the assessment',
662    cardComputer:   'Percentage of households with a computer or tablet available',
663    cardPhone:      'Percentage of households by phone availability',
664    cardEdu:        'Percentage of households with educated parents by highest level of education attained',
665    // Chatbot
666    chatTitle:      '📊 Data Assistant',
667    chatSub:        'Ask about MPL outcomes or reach & access',
668    chatGreeting:   'Hi! Ask me about learning outcomes or access indicators — e.g. "What % of children in Kenya achieve MPL in reading?" or "What % of households in Mali have smartphones?" ✅\n\nTip: Type your question in French, Spanish or Portuguese to get a response in that language.',
669    chatTryAsking:  'Try asking:',
670    chatPlaceholder:'Ask a question…',
671    chatEx1:        'MPL reading rate in Kenya?',
672    chatEx2:        'Girls vs boys math MPL in Tanzania',
673    chatEx3:        '% enrolled in Mali by location',
674    chatEx4:        'Smartphone access in Pakistan',
675    mplAxisLabel:   '% of children achieving MPL',
676    catGirls:'Girls', catBoys:'Boys',
677    catUrban:'Urban', catRural:'Rural',
678    catGovt:'Government', catPrivate:'Private', catOther:'Other',
679    catNoPhone:'No Phone', catBasicPhone:'Basic Phone', catSmartphone:'Smartphone',
680    catNone:'None', catPrimary:'Primary', catSecondary:'Secondary', catTertiary:'Tertiary', catUniversity:'University',
681    catEnrolled:'% Enrolled', catHomeLang:'% Home Language Match', catComputer:'% With Computer/Tablet',
682    countries: {Bangladesh:'Bangladesh',Kenya:'Kenya',Mali:'Mali',Mexico:'Mexico',Mozambique:'Mozambique',Nepal:'Nepal',Nicaragua:'Nicaragua',Pakistan:'Pakistan',Senegal:'Senegal',Tanzania:'Tanzania',Uganda:'Uganda'},
683  },
684  fr: {
685    navTitle:       'Tableau de bord des résultats d\'apprentissage ICAN · ICAR',
686    navSub:         'Enquête 2025 · 11 pays · ~89 000 enfants',
687    tabMpl:         '📊 Résultats CPM',
688    tabAccess:      '🌐 Portée & Accès',
689    loading:        'Chargement…',
690    sCountries:     'Pays',
691    sGender:        'Genre',
692    sLocation:      'Localisation',
693    sAge:           'Âge',
694    sGrade:         'Classe',
695    sCtx:           'Variable contextuelle',
696    sCtxWarn:       '⚠ Une seule à la fois',
697    sCtxNone:       '— Aucune —',
698    sCtxFilterBy:   'Filtrer par :',
699    sGradeNote:     'Enfants scolarisés uniquement · Pré-primaire à Classe 8',
700    sBtnApply:      'Appliquer',
701    sBtnReset:      'Réinitialiser',
702    sPillAll:       'Tous',
703    sPillGirls:     'Filles',
704    sPillBoys:      'Garçons',
705    sPillUrban:     'Urbain',
706    sPillRural:     'Rural',
707    sChipAll:       'Tous',
708    sChipNone:      'Aucun',
709    ctrlBreakdown:  'Ventiler par',
710    ctrlMpl:        'Indicateur CPM',
711    bdCountry:      'Pays',
712    bdGender:       'Genre (Filles / Garçons)',
713    bdLocation:     'Localisation (Urbain / Rural)',
714    bdGrade:        'Classe (trajectoire d\'apprentissage)',
715    bdAge:          'Âge (trajectoire par âge)',
716    bdEnrollment:   'Statut de scolarisation',
717    bdHomeLang:     'Langue du foyer correspond à l\'évaluation',
718    bdParentalEdu:  'Niveau d\'éducation parental le plus élevé',
719    bdPhone:        'Type de téléphone au foyer',
720    bdComputer:     'Accès à un ordinateur/tablette',
721    bdSchoolType:   'Type d\'école',
722    mplMath:        'Maths',
723    mplReading:     'Lecture',
724    mplBoth:        'Les deux',
725    mplAll:         'Tout',
726    mplInfoSummary: 'ℹ Qu\'est-ce que le CPM ?',
727    mplInfoBody: '<strong>Niveau Minimum de Compétences (NMC)</strong> est la référence mondiale pour l\'apprentissage fondamental dans le cadre de l\'ODD 4.1.1(a). Il représente les compétences minimales dont les enfants ont besoin pour leur apprentissage futur.<br><br><strong>Pour la lecture :</strong> Une évaluation alignée sur le NMC confirme qu\'un enfant peut décoder et comprendre avec précision un texte simple de niveau CE1. Cela est mesuré par au moins 20 points de score, avec un minimum de 10 consacrés à des tâches de compréhension de lecture telles que la reconnaissance de mots courants et l\'extraction d\'informations explicites du texte.<br><br><strong>Pour les mathématiques :</strong> Le NMC indique qu\'un enfant a maîtrisé les compétences fondamentales en numération et en calcul. L\'alignement requiert au moins 20 points de score liés au contenu de CE1, avec 10 points axés sur le domaine des Nombres et Opérations. Pour atteindre ce seuil, les enfants doivent démontrer leur compétence dans au moins trois sous-construits : identifier et compter des nombres entiers, représenter des nombres de manières équivalentes, résoudre des opérations de base, ou résoudre des problèmes simples du quotidien.',
728    warnSuppressed: '⚠ Certaines cellules supprimées (N < 100)',
729    chartSub:       'Estimations pondérées par sondage',
730    tableTitle:     'Résultats détaillés',
731    thMplMath:      'CPM Maths',
732    thMplReading:   'CPM Lecture',
733    thMplBoth:      'CPM Les deux',
734    thN:            'N (non pondéré)',
735    noData:         'Appliquer les filtres pour voir les résultats',
736    exportPng:      '↓ PNG',
737    exportExcel:    '↓ Excel',
738    accessNote:     'Les graphiques suivants présentent le contexte des communautés visitées : accès des enfants à l\'école par genre, localisation, type d\'école et disponibilité des ressources dans les ménages.',
739    accessViewLabel:'Voir l\'indicateur',
740    accessViewAll:  'Tous les indicateurs',
741    accGrpSchool:   'Accès des enfants à l\'école',
742    accGrpHH:       'Accès aux ressources dans les ménages',
743    accOptEnrolled: 'Pourcentage d\'enfants actuellement scolarisés',
744    accOptGender:   'Scolarisés par genre (Filles vs Garçons)',
745    accOptLocation: 'Scolarisés par localisation (Rural vs Urbain)',
746    accOptSchool:   'Scolarisés par type d\'école',
747    accOptHomelang: 'Langue du foyer correspond à l\'évaluation',
748    accOptComputer: 'Ménages avec ordinateur ou tablette',
749    accOptPhone:    'Ménages par disponibilité du téléphone',
750    accOptEdu:      'Niveau d\'éducation parental',
751    hdrSchool:      'Accès des enfants à l\'école',
752    hdrHH:          'Accès aux ressources dans les ménages',
753    cardEnrolled:   'Pourcentage d\'enfants actuellement scolarisés',
754    cardGender:     'Pourcentage d\'enfants scolarisés par genre (Filles vs Garçons)',
755    cardLocation:   'Pourcentage d\'enfants scolarisés par localisation (Rural vs Urbain)',
756    cardSchool:     'Pourcentage d\'enfants scolarisés par type d\'école',
757    cardHomelang:   'Pourcentage de ménages parlant la même langue que l\'évaluation',
758    cardComputer:   'Pourcentage de ménages disposant d\'un ordinateur ou d\'une tablette',
759    cardPhone:      'Pourcentage de ménages par disponibilité du téléphone',
760    cardEdu:        'Pourcentage de ménages avec parents éduqués par niveau d\'éducation le plus élevé',
761    chatTitle:      '📊 Assistant de données',
762    chatSub:        'Posez des questions sur les résultats CPM ou l\'accès',
763    chatGreeting:   'Bonjour ! Posez-moi des questions sur les résultats d\'apprentissage ou les indicateurs d\'accès ✅\n\nAstuce : Posez votre question dans votre langue pour obtenir une réponse dans cette langue.',
764    chatTryAsking:  'Essayez de demander :',
765    chatPlaceholder:'Posez une question…',
766    chatEx1:        'Taux CPM lecture au Kenya ?',
767    chatEx2:        'CPM maths filles vs garçons en Tanzanie',
768    chatEx3:        '% scolarisés au Mali par localisation',
769    chatEx4:        'Accès aux smartphones au Pakistan',
770    mplAxisLabel:   '% d\'enfants atteignant le CPM',
771    catGirls:'Filles', catBoys:'Garçons',
772    catUrban:'Urbain', catRural:'Rural',
773    catGovt:'Public', catPrivate:'Privé', catOther:'Autre',
774    catNoPhone:'Sans téléphone', catBasicPhone:'Téléphone basique', catSmartphone:'Smartphone',
775    catNone:'Aucun', catPrimary:'Primaire', catSecondary:'Secondaire', catTertiary:'Tertiaire', catUniversity:'Université',
776    catEnrolled:'% Scolarisés', catHomeLang:'% Langue du foyer', catComputer:'% Avec ordinateur/tablette',
777    countries: {Bangladesh:'Bangladesh',Kenya:'Kenya',Mali:'Mali',Mexico:'Mexique',Mozambique:'Mozambique',Nepal:'Népal',Nicaragua:'Nicaragua',Pakistan:'Pakistan',Senegal:'Sénégal',Tanzania:'Tanzanie',Uganda:'Ouganda'},
778  },
779  es: {
780    navTitle:       'Tablero de resultados de aprendizaje ICAN · ICAR',
781    navSub:         'Encuesta 2025 · 11 países · ~89.000 niños',
782    tabMpl:         '📊 Resultados NMP',
783    tabAccess:      '🌐 Alcance y Acceso',
784    loading:        'Cargando…',
785    sCountries:     'Países',
786    sGender:        'Género',
787    sLocation:      'Ubicación',
788    sAge:           'Edad',
789    sGrade:         'Grado',
790    sCtx:           'Variable contextual',
791    sCtxWarn:       '⚠ Solo una a la vez',
792    sCtxNone:       '— Ninguna —',
793    sCtxFilterBy:   'Filtrar por:',
794    sGradeNote:     'Solo niños matriculados · Pre-primaria a Grado 8',
795    sBtnApply:      'Aplicar',
796    sBtnReset:      'Restablecer',
797    sPillAll:       'Todos',
798    sPillGirls:     'Niñas',
799    sPillBoys:      'Niños',
800    sPillUrban:     'Urbano',
801    sPillRural:     'Rural',
802    sChipAll:       'Todos',
803    sChipNone:      'Ninguno',
804    ctrlBreakdown:  'Desglosar por',
805    ctrlMpl:        'Indicador NMP',
806    bdCountry:      'País',
807    bdGender:       'Género (Niñas / Niños)',
808    bdLocation:     'Ubicación (Urbano / Rural)',
809    bdGrade:        'Grado (trayectoria de aprendizaje)',
810    bdAge:          'Edad (trayectoria por edad)',
811    bdEnrollment:   'Estado de matriculación',
812    bdHomeLang:     'Idioma del hogar coincide con la evaluación',
813    bdParentalEdu:  'Nivel educativo parental más alto',
814    bdPhone:        'Tipo de teléfono en el hogar',
815    bdComputer:     'Acceso a computadora/tableta',
816    bdSchoolType:   'Tipo de escuela',
817    mplMath:        'Matemáticas',
818    mplReading:     'Lectura',
819    mplBoth:        'Ambos',
820    mplAll:         'Todos',
821    mplInfoSummary: 'ℹ ¿Qué es el NMP?',
822    mplInfoBody: '<strong>Nivel Mínimo de Competencias (NMC)</strong> es el referente mundial para el aprendizaje fundamental en el marco del ODS 4.1.1(a). Representa las competencias mínimas que los niños necesitan para su aprendizaje futuro.<br><br><strong>Para Lectura:</strong> Una evaluación alineada con el NMC confirma que un niño puede decodificar y comprender con precisión un texto simple de 2.° grado. Esto se mide con al menos 20 puntos de puntaje, con un mínimo de 10 dedicados a tareas de comprensión lectora como reconocer palabras comunes y recuperar información explícita del texto.<br><br><strong>Para Matemáticas:</strong> El NMC indica que un niño ha dominado las habilidades básicas de sentido numérico y cálculo. La alineación requiere al menos 20 puntos de puntaje vinculados al contenido de 2.° grado, con 10 puntos enfocados en el dominio de Números y Operaciones. Para alcanzar este umbral, los niños deben demostrar competencia en al menos tres subconstructos: identificar y contar números enteros, representar números de formas equivalentes, resolver operaciones básicas o resolver problemas simples del mundo real.',
823    warnSuppressed: '⚠ Algunas celdas suprimidas (N < 100)',
824    chartSub:       'Estimaciones ponderadas por muestreo',
825    tableTitle:     'Resultados detallados',
826    thMplMath:      'NMP Matemáticas',
827    thMplReading:   'NMP Lectura',
828    thMplBoth:      'NMP Ambos',
829    thN:            'N (no ponderado)',
830    noData:         'Aplique filtros para ver los resultados',
831    exportPng:      '↓ PNG',
832    exportExcel:    '↓ Excel',
833    accessNote:     'Los siguientes gráficos muestran el contexto de las comunidades visitadas: acceso de los niños a la escuela por género, ubicación, tipo de escuela y disponibilidad de recursos en los hogares.',
834    accessViewLabel:'Ver indicador',
835    accessViewAll:  'Todos los indicadores',
836    accGrpSchool:   'Acceso de los niños a la escuela',
837    accGrpHH:       'Acceso a recursos en los hogares',
838    accOptEnrolled: 'Porcentaje de niños actualmente matriculados',
839    accOptGender:   'Matriculados por género (Niñas vs Niños)',
840    accOptLocation: 'Matriculados por ubicación (Rural vs Urbano)',
841    accOptSchool:   'Matriculados por tipo de escuela',
842    accOptHomelang: 'Idioma del hogar coincide con la evaluación',
843    accOptComputer: 'Hogares con computadora o tableta',
844    accOptPhone:    'Hogares por disponibilidad de teléfono',
845    accOptEdu:      'Nivel educativo parental',
846    hdrSchool:      'Acceso de los niños a la escuela',
847    hdrHH:          'Acceso a recursos en los hogares',
848    cardEnrolled:   'Porcentaje de niños actualmente matriculados',
849    cardGender:     'Porcentaje de niños matriculados por género (Niñas vs Niños)',
850    cardLocation:   'Porcentaje de niños matriculados por ubicación (Rural vs Urbano)',
851    cardSchool:     'Porcentaje de niños matriculados por tipo de escuela',
852    cardHomelang:   'Porcentaje de hogares que hablan el mismo idioma que la evaluación',
853    cardComputer:   'Porcentaje de hogares con computadora o tableta disponible',
854    cardPhone:      'Porcentaje de hogares por disponibilidad de teléfono',
855    cardEdu:        'Porcentaje de hogares con padres educados por nivel más alto alcanzado',
856    chatTitle:      '📊 Asistente de datos',
857    chatSub:        'Pregunte sobre resultados NMP o acceso',
858    chatGreeting:   '¡Hola! Pregúnteme sobre resultados de aprendizaje o indicadores de acceso ✅\n\nConsejo: Escriba su pregunta en su idioma para recibir una respuesta en ese idioma.',
859    chatTryAsking:  'Intente preguntar:',
860    chatPlaceholder:'Haga una pregunta…',
861    chatEx1:        '¿Tasa NMP lectura en Kenia?',
862    chatEx2:        'NMP matemáticas niñas vs niños en Tanzania',
863    chatEx3:        '% matriculados en Malí por ubicación',
864    chatEx4:        'Acceso a smartphones en Pakistán',
865    mplAxisLabel:   '% de niños que alcanzan el NMP',
866    catGirls:'Niñas', catBoys:'Niños',
867    catUrban:'Urbano', catRural:'Rural',
868    catGovt:'Estatal', catPrivate:'Privado', catOther:'Otro',
869    catNoPhone:'Sin teléfono', catBasicPhone:'Teléfono básico', catSmartphone:'Smartphone',
870    catNone:'Ninguno', catPrimary:'Primaria', catSecondary:'Secundaria', catTertiary:'Terciaria', catUniversity:'Universidad',
871    catEnrolled:'% Matriculados', catHomeLang:'% Lengua del hogar', catComputer:'% Con computadora/tableta',
872    countries: {Bangladesh:'Bangladés',Kenya:'Kenia',Mali:'Malí',Mexico:'México',Mozambique:'Mozambique',Nepal:'Nepal',Nicaragua:'Nicaragua',Pakistan:'Pakistán',Senegal:'Senegal',Tanzania:'Tanzania',Uganda:'Uganda'},
873  },
874  pt: {
875    navTitle:       'Painel de Resultados de Aprendizagem ICAN · ICAR',
876    navSub:         'Pesquisa 2025 · 11 países · ~89.000 crianças',
877    tabMpl:         '📊 Resultados NMP',
878    tabAccess:      '🌐 Alcance e Acesso',
879    loading:        'Carregando…',
880    sCountries:     'Países',
881    sGender:        'Género',
882    sLocation:      'Localização',
883    sAge:           'Idade',
884    sGrade:         'Série',
885    sCtx:           'Variável contextual',
886    sCtxWarn:       '⚠ Apenas uma de cada vez',
887    sCtxNone:       '— Nenhuma —',
888    sCtxFilterBy:   'Filtrar por:',
889    sGradeNote:     'Apenas crianças matriculadas · Pré-primário a Série 8',
890    sBtnApply:      'Aplicar',
891    sBtnReset:      'Redefinir',
892    sPillAll:       'Todos',
893    sPillGirls:     'Meninas',
894    sPillBoys:      'Meninos',
895    sPillUrban:     'Urbano',
896    sPillRural:     'Rural',
897    sChipAll:       'Todos',
898    sChipNone:      'Nenhum',
899    ctrlBreakdown:  'Desagregar por',
900    ctrlMpl:        'Indicador NMP',
901    bdCountry:      'País',
902    bdGender:       'Género (Meninas / Meninos)',
903    bdLocation:     'Localização (Urbano / Rural)',
904    bdGrade:        'Série (trajetória de aprendizagem)',
905    bdAge:          'Idade (trajetória por idade)',
906    bdEnrollment:   'Status de matrícula',
907    bdHomeLang:     'Língua do lar corresponde à avaliação',
908    bdParentalEdu:  'Nível educacional parental mais alto',
909    bdPhone:        'Tipo de telefone no lar',
910    bdComputer:     'Acesso a computador/tablet',
911    bdSchoolType:   'Tipo de escola',
912    mplMath:        'Matemática',
913    mplReading:     'Leitura',
914    mplBoth:        'Ambos',
915    mplAll:         'Todos',
916    mplInfoSummary: 'ℹ O que é o NMP?',
917    mplInfoBody: '<strong>Nível Mínimo de Proficiência (NMP)</strong> é o referencial global para a aprendizagem fundamental no âmbito do ODS 4.1.1(a). Representa as competências mínimas de que as crianças necessitam para a sua aprendizagem futura.<br><br><strong>Para Leitura:</strong> Uma avaliação alinhada com o NMP confirma que uma criança consegue descodificar e compreender com precisão um texto simples do 2.° ano. Isto é medido por pelo menos 20 pontos de pontuação, com um mínimo de 10 dedicados a tarefas de compreensão de leitura, como reconhecer palavras comuns e recuperar informação explícita do texto.<br><br><strong>Para Matemática:</strong> O NMP indica que uma criança dominou as competências básicas de sentido numérico e cálculo. O alinhamento requer pelo menos 20 pontos de pontuação ligados ao conteúdo do 2.° ano, com 10 pontos focados no domínio de Números e Operações. Para atingir este limiar, as crianças devem demonstrar proficiência em pelo menos três subconstrutos: identificar e contar números inteiros, representar números de formas equivalentes, resolver operações básicas ou resolver problemas simples do mundo real.',
918    warnSuppressed: '⚠ Algumas células suprimidas (N < 100)',
919    chartSub:       'Estimativas ponderadas por amostragem',
920    tableTitle:     'Resultados detalhados',
921    thMplMath:      'NMP Matemática',
922    thMplReading:   'NMP Leitura',
923    thMplBoth:      'NMP Ambos',
924    thN:            'N (não ponderado)',
925    noData:         'Aplique filtros para ver os resultados',
926    exportPng:      '↓ PNG',
927    exportExcel:    '↓ Excel',
928    accessNote:     'Os gráficos a seguir mostram o contexto das comunidades visitadas: acesso das crianças à escola por género, localização, tipo de escola e disponibilidade de recursos nos lares.',
929    accessViewLabel:'Ver indicador',
930    accessViewAll:  'Todos os indicadores',
931    accGrpSchool:   'Acesso das crianças à escola',
932    accGrpHH:       'Acesso a recursos nos lares',
933    accOptEnrolled: 'Percentagem de crianças actualmente matriculadas',
934    accOptGender:   'Matriculadas por género (Meninas vs Meninos)',
935    accOptLocation: 'Matriculadas por localização (Rural vs Urbano)',
936    accOptSchool:   'Matriculadas por tipo de escola',
937    accOptHomelang: 'Língua do lar corresponde à avaliação',
938    accOptComputer: 'Lares com computador ou tablet',
939    accOptPhone:    'Lares por disponibilidade de telefone',
940    accOptEdu:      'Nível educacional parental',
941    hdrSchool:      'Acesso das crianças à escola',
942    hdrHH:          'Acesso a recursos nos lares',
943    cardEnrolled:   'Percentagem de crianças actualmente matriculadas',
944    cardGender:     'Percentagem de crianças matriculadas por género (Meninas vs Meninos)',
945    cardLocation:   'Percentagem de crianças matriculadas por localização (Rural vs Urbano)',
946    cardSchool:     'Percentagem de crianças matriculadas por tipo de escola',
947    cardHomelang:   'Percentagem de lares que falam a mesma língua da avaliação',
948    cardComputer:   'Percentagem de lares com computador ou tablet disponível',
949    cardPhone:      'Percentagem de lares por disponibilidade de telefone',
950    cardEdu:        'Percentagem de lares com pais educados pelo nível mais alto de educação atingido',
951    chatTitle:      '📊 Assistente de dados',
952    chatSub:        'Pergunte sobre resultados NMP ou acesso',
953    chatGreeting:   'Olá! Faça-me perguntas sobre resultados de aprendizagem ou indicadores de acesso ✅\n\nDica: Escreva a sua pergunta no seu idioma para receber uma resposta nesse idioma.',
954    chatTryAsking:  'Tente perguntar:',
955    chatPlaceholder:'Faça uma pergunta…',
956    chatEx1:        'Taxa NMP leitura no Quénia?',
957    chatEx2:        'NMP matemática meninas vs meninos na Tanzânia',
958    chatEx3:        '% matriculados no Mali por localização',
959    chatEx4:        'Acesso a smartphones no Paquistão',
960    mplAxisLabel:   '% de crianças que atingem o NMP',
961    catGirls:'Meninas', catBoys:'Meninos',
962    catUrban:'Urbano', catRural:'Rural',
963    catGovt:'Público', catPrivate:'Privado', catOther:'Outro',
964    catNoPhone:'Sem telefone', catBasicPhone:'Telefone básico', catSmartphone:'Smartphone',
965    catNone:'Nenhum', catPrimary:'Primário', catSecondary:'Secundário', catTertiary:'Terciário', catUniversity:'Universidade',
966    catEnrolled:'% Matriculadas', catHomeLang:'% Língua do lar', catComputer:'% Com computador/tablet',
967    countries: {Bangladesh:'Bangladesh',Kenya:'Quénia',Mali:'Mali',Mexico:'México',Mozambique:'Moçambique',Nepal:'Nepal',Nicaragua:'Nicarágua',Pakistan:'Paquistão',Senegal:'Senegal',Tanzania:'Tanzânia',Uganda:'Uganda'},
968  },
969};
970
971let currentLang = 'en';
972
973// Translate a country name (English key → localised display name)
974function tc(name) {
975  return (T[currentLang].countries || {})[name] || name;
976}
977// Translate a label/category key
978function tl(key) { return T[currentLang][key] || T.en[key] || key; }
979
980function applyLanguage(lang) {
981  currentLang = lang;
982  const t = T[lang];
983
984  // Navbar buttons
985  document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.textContent === lang.toUpperCase()));
986
987  // Navbar text
988  document.querySelector('.navbar-survey-title').textContent = t.navTitle;
989  document.querySelector('.navbar-survey-sub').textContent   = t.navSub;
990
991  // Page tabs
992  const tabs = document.querySelectorAll('.page-tab');
993  tabs[0].textContent = t.tabMpl;
994  tabs[1].innerHTML   = t.tabAccess;
995
996  // Loading
997  document.querySelector('#loading span').textContent = t.loading;
998
999  // Sidebar summaries
1000  const summaries = document.querySelectorAll('aside.sidebar details summary');
1001  summaries[0].childNodes[0].textContent = t.sCountries;
1002  summaries[1].childNodes[0].textContent = t.sGender;
1003  summaries[2].childNodes[0].textContent = t.sLocation;
1004  summaries[3].childNodes[0].textContent = t.sAge;
1005  summaries[4].childNodes[0].textContent = t.sGrade;
1006  summaries[5].childNodes[0].textContent = t.sCtx;
1007
1008  // Sidebar pills
1009  const gPills = document.querySelectorAll('#gender-pills .pill');
1010  gPills[0].textContent = t.sPillAll;
1011  gPills[1].textContent = t.sPillGirls;
1012  gPills[2].textContent = t.sPillBoys;
1013  const lPills = document.querySelectorAll('#location-pills .pill');
1014  lPills[0].textContent = t.sPillAll;
1015  lPills[1].textContent = t.sPillUrban;
1016  lPills[2].textContent = t.sPillRural;
1017
1018  // Sidebar chips
1019  document.querySelectorAll('#country-checks + .cb-actions .chip')[0].textContent = t.sChipAll;
1020  document.querySelectorAll('#country-checks + .cb-actions .chip')[1].textContent = t.sChipNone;
1021  document.querySelectorAll('#age-checks + .cb-actions .chip')[0].textContent     = t.sChipAll;
1022  document.querySelectorAll('#age-checks + .cb-actions .chip')[1].textContent     = t.sChipNone;
1023  document.querySelectorAll('#grade-checks + .cb-actions .chip')[0].textContent   = t.sChipAll;
1024  document.querySelectorAll('#grade-checks + .cb-actions .chip')[1].textContent   = t.sChipNone;
1025
1026  // Grade note
1027  document.querySelector('#grade-checks').previousElementSibling.textContent = t.sGradeNote;
1028
1029  // Ctx var warning
1030  document.querySelector('#ctx-details .filter-body div').textContent = t.sCtxWarn;
1031  document.querySelector('#ctx-var-select option[value=""]').textContent = t.sCtxNone;
1032  document.querySelector('#ctx-values-wrap > div').textContent = t.sCtxFilterBy;
1033
1034  // Apply / Reset buttons
1035  document.querySelector('.btn-primary').textContent = t.sBtnApply;
1036  document.querySelector('.btn-reset').textContent   = t.sBtnReset;
1037
1038  // MPL controls
1039  const ctrlLabels = document.querySelectorAll('#page-mpl .ctrl-label');
1040  ctrlLabels[0].textContent = t.ctrlBreakdown;
1041  ctrlLabels[1].textContent = t.ctrlMpl;
1042
1043  // Breakdown select options
1044  const bdSel = document.getElementById('breakdown-select');
1045  const bdMap = ['bdCountry','bdGender','bdLocation','bdGrade','bdAge','bdEnrollment','bdHomeLang','bdParentalEdu','bdPhone','bdComputer','bdSchoolType'];
1046  bdSel.querySelectorAll('option').forEach((o,i) => { if (bdMap[i]) o.textContent = t[bdMap[i]]; });
1047
1048  // MPL tabs
1049  const mplTabs = document.querySelectorAll('.mpl-tab');
1050  mplTabs[0].textContent = t.mplMath;
1051  mplTabs[1].textContent = t.mplReading;
1052  mplTabs[2].textContent = t.mplBoth;
1053  mplTabs[3].textContent = t.mplAll;
1054
1055  // MPL info summary + body
1056  document.querySelector('.info-note details summary').textContent = t.mplInfoSummary;
1057  document.querySelector('.info-note details .filter-body').innerHTML = t.mplInfoBody;
1058
1059  // Chart subtitle & warn
1060  document.getElementById('chart-sub').textContent = t.chartSub;
1061  const wn = document.getElementById('warn-note');
1062  if (wn) wn.textContent = t.warnSuppressed;
1063
1064  // Export buttons in chart box
1065  const exportBtns = document.querySelectorAll('#main-chart-box .btn-export');
1066  if (exportBtns[0]) exportBtns[0].textContent = t.exportPng;
1067  if (exportBtns[1]) exportBtns[1].textContent = t.exportExcel;
1068
1069  // Table
1070  document.getElementById('table-title').textContent = t.tableTitle;
1071  document.querySelector('#results-tbody tr td')?.setAttribute('data-empty', t.noData);
1072  const ths = document.querySelectorAll('thead th');
1073  if (ths[1]) ths[1].textContent = t.thMplMath;
1074  if (ths[2]) ths[2].textContent = t.thMplReading;
1075  if (ths[3]) ths[3].textContent = t.thMplBoth;
1076  if (ths[4]) ths[4].textContent = t.thN;
1077
1078  // Table Excel button
1079  const tblExcel = document.querySelector('.table-box .btn-export');
1080  if (tblExcel) tblExcel.textContent = t.exportExcel;
1081
1082  // Access tab
1083  document.querySelector('.access-note').textContent = t.accessNote;
1084  document.querySelector('#page-access .ctrl-label').textContent = t.accessViewLabel;
1085
1086  // Access view select
1087  const avSel = document.getElementById('access-view-select');
1088  avSel.options[0].textContent = t.accessViewAll;
1089  const optGroups = avSel.querySelectorAll('optgroup');
1090  optGroups[0].label = t.accGrpSchool;
1091  optGroups[1].label = t.accGrpHH;
1092  const accOpts = avSel.querySelectorAll('option:not([value="all"])');
1093  const accOptKeys = ['accOptEnrolled','accOptGender','accOptLocation','accOptSchool','accOptHomelang','accOptComputer','accOptPhone','accOptEdu'];
1094  accOpts.forEach((o,i) => { if (accOptKeys[i]) o.textContent = t[accOptKeys[i]]; });
1095
1096  // Section headers
1097  const sectionHdrs = document.querySelectorAll('#page-access .section-hdr');
1098  if (sectionHdrs[0]) sectionHdrs[0].textContent = t.hdrSchool;
1099  if (sectionHdrs[1]) sectionHdrs[1].textContent = t.hdrHH;
1100
1101  // Access card titles (first text node only, preserving export buttons)
1102  const cardKeys = ['cardEnrolled','cardGender','cardLocation','cardSchool','cardHomelang','cardComputer','cardPhone','cardEdu'];
1103  document.querySelectorAll('.access-card-title').forEach((el, i) => {
1104    if (cardKeys[i]) el.childNodes[0].textContent = t[cardKeys[i]] + ' ';
1105  });
1106
1107  // Access export buttons
1108  document.querySelectorAll('.access-card .btn-export').forEach(b => {
1109    b.textContent = b.onclick?.toString().includes('Excel') ? t.exportExcel : t.exportPng;
1110  });
1111
1112  // Chatbot
1113  document.querySelector('.chat-hdr-title').textContent = t.chatTitle;
1114  document.querySelector('.chat-hdr-sub').textContent   = t.chatSub;
1115  document.querySelector('.chat-examples p').textContent = t.chatTryAsking;
1116  document.getElementById('chat-input').placeholder     = t.chatPlaceholder;
1117  const exChips = document.querySelectorAll('.ex-chips .chip');
1118  if (exChips[0]) exChips[0].textContent = t.chatEx1;
1119  if (exChips[1]) exChips[1].textContent = t.chatEx2;
1120  if (exChips[2]) exChips[2].textContent = t.chatEx3;
1121  if (exChips[3]) exChips[3].textContent = t.chatEx4;
1122
1123  // Update country checkbox labels to translated names (keep value as English)
1124  document.querySelectorAll('#country-checks .cb-item').forEach(label => {
1125    const cb = label.querySelector('input');
1126    if (cb) label.childNodes[label.childNodes.length - 1].textContent = ' ' + tc(cb.value);
1127  });
1128
1129  // Re-render charts with translated labels if data is loaded
1130  if (currentMplData.countries) {
1131    updateChartTitle(currentMplData.breakdown_label);
1132    renderMPLCharts();
1133  }
1134  if (typeof accessData !== 'undefined' && accessData && accessData.countries) {
1135    renderAccessCharts();
1136  }
1137}
1138
1139// ═══════════════════════════════════════════════════════════════════
1140// State
1141// ═══════════════════════════════════════════════════════════════════
1142const state = {
1143  breakdown: 'country', mpl: 'MPL_math',
1144  countries: [], gender: 'all', location: 'all',
1145  ages: [], grades: [], ctx_var: '', ctx_vals: [], page: 'mpl',
1146  accessView: 'all',
1147};
1148let filterOptions = {};
1149let currentMplData = {};
1150let chartInstances = {};
1151let showLabels = true;
1152
1153function toggleLabels() {
1154  showLabels = !showLabels;
1155  const btn = document.getElementById('labels-toggle');
1156  btn.classList.toggle('active', showLabels);
1157  btn.textContent = showLabels ? 'Labels On' : 'Labels Off';
1158  if (currentMplData.countries || currentMplData.groups) renderMPLCharts();
1159  if (accessData) renderAccessCharts();
1160}
1161const chatHistory = [];
1162
1163const CTXKEYS = new Set(['enrollment','home_lang','parental_edu','phone','computer','school_type']);
1164// Line chart breakdowns (trajectory)
1165const LINE_BREAKDOWNS = new Set(['grade','age']);
1166const MPL_SHORT = {MPL_math:'Math', MPL_lang:'Reading', MPL_both:'Both'};
1167
1168// ═══════════════════════════════════════════════════════════════════
1169// Init
1170// ═══════════════════════════════════════════════════════════════════
1171async function init() {
1172  showLoading(true);
1173  try {
1174    filterOptions = await fetch('/api/filter-options').then(r => r.json());
1175    buildFilterUI();
1176    await refresh();
1177  } catch(e) {
1178    console.error(e);
1179    alert('Could not connect to server. Make sure Flask is running.');
1180  } finally { showLoading(false); }
1181}
1182
1183function buildFilterUI() {
1184  const cc = document.getElementById('country-checks');
1185  cc.innerHTML = filterOptions.countries.map(c =>
1186    `<label class="cb-item"><input type="checkbox" class="country-cb" value="${c}" checked onchange="onCbChange()"> ${tc(c)}</label>`
1187  ).join('');
1188
1189  const ac = document.getElementById('age-checks');
1190  ac.innerHTML = filterOptions.ages.map(a =>
1191    `<label class="cb-item"><input type="checkbox" class="age-cb" value="${a}" checked onchange="onCbChange()"> Age ${a}</label>`
1192  ).join('');
1193
1194  const gc = document.getElementById('grade-checks');
1195  gc.innerHTML = filterOptions.grades.map(g =>
1196    `<label class="cb-item"><input type="checkbox" class="grade-cb" value="${g}" checked onchange="onCbChange()"> ${g}</label>`
1197  ).join('');
1198
1199  const sel = document.getElementById('ctx-var-select');
1200  sel.innerHTML = `<option value="">${T[currentLang].sCtxNone}</option>` +
1201    Object.entries(filterOptions.ctx_vars).map(([k,v]) =>
1202      `<option value="${k}">${v.label}</option>`).join('');
1203}
1204
1205// ═══════════════════════════════════════════════════════════════════
1206// Filter helpers
1207// ═══════════════════════════════════════════════════════════════════
1208function selectAll(listId, cls) {
1209  document.querySelectorAll(`#${listId} .${cls}`).forEach(c => c.checked = true);
1210  onCbChange();
1211}
1212function clearAll(listId, cls) {
1213  document.querySelectorAll(`#${listId} .${cls}`).forEach(c => c.checked = false);
1214  onCbChange();
1215}
1216function onCbChange() { /* state updated on applyFilters */ }
1217
1218function setPill(btn, groupId, fn) {
1219  document.querySelectorAll(`#${groupId} .pill`).forEach(b => b.classList.remove('active'));
1220  btn.classList.add('active');
1221  fn(btn.dataset.val);
1222}
1223function setGender(val)   { state.gender   = val; }
1224function setLocation(val) { state.location = val; }
1225
1226function onCtxVarChange() {
1227  const v = document.getElementById('ctx-var-select').value;
1228  state.ctx_var  = v;
1229  state.ctx_vals = [];
1230  const wrap = document.getElementById('ctx-values-wrap');
1231  if (!v) { wrap.style.display = 'none'; return; }
1232  if (CTXKEYS.has(state.breakdown)) {
1233    alert('The current breakdown already uses a contextual variable. Switch breakdown to a standard variable first.');
1234    document.getElementById('ctx-var-select').value = '';
1235    state.ctx_var = '';
1236    wrap.style.display = 'none';
1237    return;
1238  }
1239  wrap.style.display = 'block';
1240  const opts = filterOptions.ctx_vars[v].options;
1241  document.getElementById('ctx-value-checks').innerHTML = opts.map(o =>
1242    `<label class="cb-item"><input type="checkbox" class="ctxval-cb" value="${o}" onchange="onCtxValChange()"> ${o}</label>`
1243  ).join('');
1244}
1245function onCtxValChange() {
1246  state.ctx_vals = [...document.querySelectorAll('.ctxval-cb:checked')].map(c => c.value);
1247}
1248
1249function onAccessViewChange() {
1250  state.accessView = document.getElementById('access-view-select').value;
1251  renderAccessCharts();
1252}
1253
1254function onBreakdownChange() {
1255  const bd = document.getElementById('breakdown-select').value;
1256  state.breakdown = bd;
1257  const ctxSel = document.getElementById('ctx-var-select');
1258  if (CTXKEYS.has(bd)) {
1259    ctxSel.value = ''; state.ctx_var = ''; state.ctx_vals = [];
1260    document.getElementById('ctx-values-wrap').style.display = 'none';
1261    ctxSel.disabled = true;
1262  } else {
1263    ctxSel.disabled = false;
1264  }
1265  applyFilters();
1266}
1267
1268function setMPL(btn) {
1269  document.querySelectorAll('.mpl-tab').forEach(b => b.classList.remove('active'));
1270  btn.classList.add('active');
1271  state.mpl = btn.dataset.mpl;
1272  if (currentMplData.groups || currentMplData.countries) renderMPLCharts();
1273}
1274
1275function switchPage(page, btn) {
1276  document.querySelectorAll('.page-tab').forEach(b => b.classList.remove('active'));
1277  btn.classList.add('active');
1278  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
1279  document.getElementById(`page-${page}`).classList.add('active');
1280  state.page = page;
1281  document.getElementById('ctx-details').style.display = page === 'access' ? 'none' : '';
1282  if (page === 'access') fetchAndRenderAccess();
1283}
1284
1285// ═══════════════════════════════════════════════════════════════════
1286// Payload
1287// ═══════════════════════════════════════════════════════════════════
1288function buildPayload() {
1289  const f = {};
1290  const countries = [...document.querySelectorAll('.country-cb:checked')].map(c => c.value);
1291  if (countries.length < filterOptions.countries.length) f.countries = countries;
1292  if (state.gender !== 'all')   f.genders   = [state.gender];
1293  if (state.location !== 'all') f.locations = [state.location];
1294  const ages = [...document.querySelectorAll('.age-cb:checked')].map(c => parseInt(c.value));
1295  if (ages.length < filterOptions.ages.length) f.ages = ages;
1296  const grades = [...document.querySelectorAll('.grade-cb:checked')].map(c => c.value);
1297  if (grades.length < filterOptions.grades.length) f.grades = grades;
1298  if (state.ctx_var && state.ctx_vals.length)
1299    f.ctx_filter = { var: state.ctx_var, values: state.ctx_vals };
1300  return f;
1301}
1302
1303async function applyFilters() { await refresh(); }
1304
1305function resetFilters() {
1306  state.gender = 'all'; state.location = 'all';
1307  state.ctx_var = ''; state.ctx_vals = [];
1308  document.querySelectorAll('.country-cb,.age-cb,.grade-cb').forEach(c => c.checked = true);
1309  document.querySelectorAll('#gender-pills .pill').forEach(b => b.classList.toggle('active', b.dataset.val==='all'));
1310  document.querySelectorAll('#location-pills .pill').forEach(b => b.classList.toggle('active', b.dataset.val==='all'));
1311  document.getElementById('ctx-var-select').value = '';
1312  document.getElementById('ctx-values-wrap').style.display = 'none';
1313  refresh();
1314}
1315
1316// ═══════════════════════════════════════════════════════════════════
1317// Fetch & render
1318// ═══════════════════════════════════════════════════════════════════
1319async function refresh() {
1320  showLoading(true);
1321  try {
1322    if (state.page === 'mpl') await fetchAndRenderMPL();
1323    else                       await fetchAndRenderAccess();
1324  } finally { showLoading(false); }
1325}
1326
1327async function fetchAndRenderMPL() {
1328  const filters = buildPayload();
1329  let data;
1330  if (state.breakdown === 'country') {
1331    data = await fetch('/api/stats', {
1332      method: 'POST', headers: {'Content-Type':'application/json'},
1333      body: JSON.stringify({filters, breakdown: 'country'})
1334    }).then(r => r.json());
1335    currentMplData = {
1336      countries: data.groups, categories: null,
1337      results: Object.fromEntries(data.groups.map(g => [g, {'__single__': data.results[g]}])),
1338      breakdown_label: 'Country', n_total: data.n_total, chartType: 'bar',
1339    };
1340  } else {
1341    data = await fetch('/api/stats/country-cross', {
1342      method: 'POST', headers: {'Content-Type':'application/json'},
1343      body: JSON.stringify({filters, secondary: state.breakdown})
1344    }).then(r => r.json());
1345    // grade & age → line trajectory; others → dumbbell
1346    const chartType = LINE_BREAKDOWNS.has(state.breakdown) ? 'line' : 'dumbbell';
1347    currentMplData = {...data, chartType, breakdown_label: data.secondary_label};
1348  }
1349  document.getElementById('sidebar-n').textContent =
1350    `Filtered total: ${(currentMplData.n_total||0).toLocaleString()} children`;
1351  renderMPLCharts();
1352  renderTable();
1353}
1354
1355// ═══════════════════════════════════════════════════════════════════
1356// MPL chart rendering
1357// ═══════════════════════════════════════════════════════════════════
1358function renderMPLCharts() {
1359  const isAll = state.mpl === 'ALL';
1360  document.getElementById('single-chart-wrap').style.display = isAll ? 'none' : 'block';
1361  document.getElementById('all-chart-wrap').style.display    = isAll ? 'grid' : 'none';
1362
1363  const {countries, categories, results, chartType, breakdown_label} = currentMplData;
1364  if (!countries || !countries.length) return;
1365
1366  updateChartTitle(breakdown_label);
1367
1368  let hasWarn = false;
1369  countries.forEach(c => {
1370    const cats = categories || ['__single__'];
1371    cats.forEach(cat => {
1372      ['MPL_math','MPL_lang','MPL_both'].forEach(m => {
1373        if (results[c]?.[cat]?.[m]?.warning) hasWarn = true;
1374      });
1375    });
1376  });
1377  document.getElementById('warn-note').style.display = hasWarn ? '' : 'none';
1378
1379  if (isAll) {
1380    ['MPL_math','MPL_lang'].forEach(mpl => {
1381      const canvasId = {MPL_math:'chart-math',MPL_lang:'chart-lang'}[mpl];
1382      renderSingleChart(canvasId, mpl, countries, categories, results, chartType, true);
1383    });
1384  } else {
1385    renderSingleChart('mpl-chart', state.mpl, countries, categories, results, chartType, false);
1386  }
1387}
1388
1389function renderSingleChart(canvasId, mpl, countries, categories, results, chartType, compact) {
1390  destroyChart(canvasId);
1391  const canvas = document.getElementById(canvasId);
1392  if (!canvas) return;
1393  const ctx = canvas.getContext('2d');
1394
1395  if (chartType === 'bar') {
1396    const estimates = countries.map(c => results[c]?.['__single__']?.[mpl]?.estimate ?? null);
1397    const warnings  = countries.map(c => results[c]?.['__single__']?.[mpl]?.warning ?? false);
1398    const ns        = countries.map(c => results[c]?.['__single__']?.[mpl]?.n_unweighted ?? 0);
1399    const barData   = estimates.map((v, i) => warnings[i] ? 0 : (v ?? 0));
1400    const bgColors  = estimates.map((_, i) => warnings[i] ? '#FED7AA' : PAL.teal);
1401
1402    chartInstances[canvasId] = new Chart(ctx, {
1403      type: 'bar',
1404      data: {labels: countries.map(tc), datasets: [{label: MPL_SHORT[mpl], data: barData,
1405        backgroundColor: bgColors, borderRadius: 4}]},
1406      options: {
1407        indexAxis: 'y', responsive: true, maintainAspectRatio: false,
1408        plugins: {
1409          legend: {display: false},
1410          tooltip: {callbacks: {label: (c) => {
1411            const i = c.dataIndex;
1412            return warnings[i] ? `⚠ Suppressed — N=${ns[i]} (<100)`
1413                               : `${estimates[i]}%   (N=${ns[i].toLocaleString()})`;
1414          }}}
1415        },
1416        scales: {
1417          x: {min:0, max:100, title:{display:!compact, text: tl('mplAxisLabel'), font:{size:10}},
1418              grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:11}}},
1419          y: {grid:{display:false}, ticks:{font:{size:compact?9:11}, color:PAL.teal}}
1420        }
1421      },
1422      plugins: [suppressPlugin(countries, warnings, true), barLabelPlugin(warnings)]
1423    });
1424  }
1425
1426  else if (chartType === 'dumbbell') {
1427    const cats = categories || [];
1428    const reversedCountries = [...countries].reverse();
1429    const ptRadius = compact ? 6 : 8;
1430    const datasets = cats.map((cat, ci) => ({
1431      label: cat,
1432      data: reversedCountries.map((c, yi) => {
1433        const s = results[c]?.[cat]?.[mpl];
1434        if (!s || s.warning || s.estimate === null) return null;
1435        return {x: s.estimate, y: yi};
1436      }).filter(Boolean),
1437      backgroundColor: COLORS[ci % COLORS.length],
1438      pointRadius: ptRadius, pointHoverRadius: ptRadius + 2, showLine: false,
1439    }));
1440
1441    chartInstances[canvasId] = new Chart(ctx, {
1442      type: 'scatter',
1443      data: {datasets},
1444      options: {
1445        responsive: true, maintainAspectRatio: false,
1446        plugins: {
1447          legend: {display: cats.length > 1, position:'bottom',
1448                   labels:{boxWidth:10, font:{size:compact?9:11}, color:PAL.darkGray}},
1449          tooltip: {callbacks: {label: c => `${c.dataset.label}: ${c.parsed.x}%`}}
1450        },
1451        scales: {
1452          x: {min:0, max:100,
1453              title:{display:!compact, text: tl('mplAxisLabel'), font:{size:10}},
1454              grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:10}}},
1455          y: {min: -1, max: reversedCountries.length,
1456              grid:{color:'#EDF2F7'},
1457              ticks:{stepSize:1, font:{size:compact?9:11}, color:PAL.teal,
1458                     callback: v => { const i=Math.round(v); return (i>=0&&i<reversedCountries.length)?tc(reversedCountries[i]||''):null; }}}
1459        }
1460      },
1461      plugins: [dumbbellPlugin(reversedCountries, cats, results, mpl)]
1462    });
1463  }
1464
1465  else if (chartType === 'line') {
1466    const cats = categories || [];
1467    const xLabel = state.breakdown === 'age' ? 'Age' : 'Grade';
1468    const datasets = countries.map((c, ci) => ({
1469      label: tc(c),
1470      data: cats.map(cat => {
1471        const s = results[c]?.[cat]?.[mpl];
1472        return (!s || s.warning) ? null : s.estimate;
1473      }),
1474      borderColor: COLORS[ci % COLORS.length],
1475      backgroundColor: COLORS[ci % COLORS.length] + '22',
1476      tension: 0.3, pointRadius: compact ? 4 : 5, borderWidth: compact ? 1.5 : 2,
1477      spanGaps: false,
1478    }));
1479
1480    chartInstances[canvasId] = new Chart(ctx, {
1481      type: 'line',
1482      data: {labels: cats, datasets},
1483      options: {
1484        responsive: true, maintainAspectRatio: false,
1485        plugins: {
1486          legend: {display: !compact, position:'bottom',
1487                   labels:{boxWidth:10, font:{size:compact?9:11}}},
1488          tooltip: {callbacks: {label: c => `${c.dataset.label}: ${c.parsed.y}%`}}
1489        },
1490        scales: {
1491          x: {title:{display:!compact, text:xLabel, font:{size:10}},
1492              grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:10}}},
1493          y: {min:0, max:100,
1494              title:{display:!compact, text: tl('mplAxisLabel'), font:{size:10}},
1495              grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:10}}}
1496        }
1497      },
1498      plugins: [lineLabelPlugin()]
1499    });
1500  }
1501}
1502
1503function suppressPlugin(labels, warnings, horizontal) {
1504  return {
1505    id: 'suppress_' + Math.random(),
1506    afterDatasetsDraw(chart) {
1507      const ctx2 = chart.ctx;
1508      ctx2.save();
1509      ctx2.font = 'bold 10px sans-serif';
1510      ctx2.fillStyle = '#C05621';
1511      chart.getDatasetMeta(0).data.forEach((bar, i) => {
1512        if (!warnings[i]) return;
1513        ctx2.fillText('⚠ N<100', horizontal ? bar.x + 4 : bar.x - 10, horizontal ? bar.y + 4 : bar.y - 4);
1514      });
1515      ctx2.restore();
1516    }
1517  };
1518}
1519
1520function barLabelPlugin(warnings) {
1521  return {
1522    id: 'barLabel_' + Math.random(),
1523    afterDatasetsDraw(chart) {
1524      if (!showLabels) return;
1525      const ctx2 = chart.ctx;
1526      ctx2.save();
1527      ctx2.font = 'bold 10px Arial, sans-serif';
1528      ctx2.fillStyle = '#2D3748';
1529      ctx2.textBaseline = 'middle';
1530      ctx2.textAlign = 'left';
1531      const meta = chart.getDatasetMeta(0);
1532      meta.data.forEach((bar, i) => {
1533        if (warnings && warnings[i]) return;
1534        const val = chart.data.datasets[0].data[i];
1535        if (!val) return;
1536        if (val >= 95) {
1537          ctx2.fillStyle = '#fff';
1538          ctx2.textAlign = 'right';
1539          ctx2.fillText(`${val}%`, bar.x - 4, bar.y);
1540          ctx2.fillStyle = '#2D3748';
1541          ctx2.textAlign = 'left';
1542        } else {
1543          ctx2.fillText(`${val}%`, bar.x + 4, bar.y);
1544        }
1545      });
1546      ctx2.restore();
1547    }
1548  };
1549}
1550
1551function stackedLabelPlugin() {
1552  return {
1553    id: 'stackedLabel_' + Math.random(),
1554    afterDatasetsDraw(chart) {
1555      if (!showLabels) return;
1556      const ctx2 = chart.ctx;
1557      ctx2.save();
1558      ctx2.font = 'bold 10px Arial, sans-serif';
1559      ctx2.fillStyle = '#fff';
1560      ctx2.textBaseline = 'middle';
1561      ctx2.textAlign = 'center';
1562      chart.data.datasets.forEach((dataset, dsi) => {
1563        const meta = chart.getDatasetMeta(dsi);
1564        meta.data.forEach((bar, i) => {
1565          const val = dataset.data[i];
1566          if (!val || val < 4) return;
1567          const segW = bar.x - bar.base;
1568          if (segW < 26) return;
1569          ctx2.fillText(`${Math.round(val)}%`, bar.base + segW / 2, bar.y);
1570        });
1571      });
1572      ctx2.restore();
1573    }
1574  };
1575}
1576
1577function dumbbellPlugin(countries, cats, results, mpl) {
1578  return {
1579    id: 'dumbbell_' + Math.random(),
1580    afterDatasetsDraw(chart) {
1581      const ctx2 = chart.ctx;
1582      ctx2.save();
1583      ctx2.strokeStyle = '#CBD5E0';
1584      ctx2.lineWidth = 1.5;
1585      countries.forEach((c, yi) => {
1586        const xs = [];
1587        cats.forEach((cat, ci) => {
1588          const pt = chart.getDatasetMeta(ci).data.find(p =>
1589            p.$context?.raw && Math.abs(p.$context.raw.y - yi) < 0.1
1590          );
1591          if (pt) xs.push(pt.x);
1592        });
1593        if (xs.length >= 2) {
1594          const yPx = chart.scales.y.getPixelForValue(yi);
1595          ctx2.beginPath();
1596          ctx2.moveTo(Math.min(...xs), yPx);
1597          ctx2.lineTo(Math.max(...xs), yPx);
1598          ctx2.stroke();
1599        }
1600      });
1601      ctx2.restore();
1602    },
1603    afterDraw(chart) {
1604      if (!showLabels) return;
1605      const ctx2 = chart.ctx;
1606      ctx2.save();
1607      ctx2.font = 'bold 9px Arial, sans-serif';
1608      ctx2.textBaseline = 'middle';
1609      const ptRadius = chart.data.datasets[0]?.pointRadius || 6;
1610      const gap = ptRadius + 3;
1611      countries.forEach((c, yi) => {
1612        const dots = [];
1613        cats.forEach((cat, ci) => {
1614          const meta = chart.getDatasetMeta(ci);
1615          const pt = meta.data.find(p => p.$context?.raw && Math.abs(p.$context.raw.y - yi) < 0.1);
1616          if (pt) dots.push({ci, pt, x: pt.x, y: pt.y, val: pt.$context.raw.x});
1617        });
1618        if (!dots.length) return;
1619        dots.sort((a, b) => a.x - b.x);
1620        dots.forEach((d, idx) => {
1621          ctx2.fillStyle = chart.data.datasets[d.ci]?.backgroundColor || '#333';
1622          if (dots.length === 1 || idx === dots.length - 1) {
1623            ctx2.textAlign = 'left';
1624            ctx2.fillText(`${d.val}%`, d.x + gap, d.y);
1625          } else {
1626            ctx2.textAlign = 'right';
1627            ctx2.fillText(`${d.val}%`, d.x - gap, d.y);
1628          }
1629        });
1630      });
1631      ctx2.restore();
1632    }
1633  };
1634}
1635
1636function lineLabelPlugin() {
1637  return {
1638    id: 'lineLabel_' + Math.random(),
1639    afterDatasetsDraw(chart) {
1640      if (!showLabels) return;
1641      const ctx2 = chart.ctx;
1642      ctx2.save();
1643      ctx2.font = 'bold 9px Arial, sans-serif';
1644      ctx2.textBaseline = 'bottom';
1645      ctx2.textAlign = 'center';
1646      chart.data.datasets.forEach((dataset, dsi) => {
1647        const meta = chart.getDatasetMeta(dsi);
1648        ctx2.fillStyle = dataset.borderColor || '#333';
1649        meta.data.forEach((pt, i) => {
1650          const val = dataset.data[i];
1651          if (val === null || val === undefined) return;
1652          ctx2.fillText(`${val}%`, pt.x, pt.y - 6);
1653        });
1654      });
1655      ctx2.restore();
1656    }
1657  };
1658}
1659
1660function destroyChart(id) {
1661  if (chartInstances[id]) { chartInstances[id].destroy(); delete chartInstances[id]; }
1662}
1663
1664function updateChartTitle(breakdownLabel) {
1665  const t = T[currentLang];
1666  const mplNames = {
1667    MPL_math: t.mplMath,
1668    MPL_lang: t.mplReading,
1669    MPL_both: `${t.mplBoth} (${t.mplMath} & ${t.mplReading})`,
1670    ALL:      `${t.mplMath}, ${t.mplReading} ${currentLang==='en'?'and':'/'} ${t.mplBoth}`
1671  };
1672  const chartTitlePhrases = {
1673    en: (mpl, bd) => bd ? `Percentage of children achieving Minimum Proficiency Levels in ${mpl} by country and ${bd.toLowerCase()}` : `Percentage of children achieving Minimum Proficiency Levels in ${mpl} by country`,
1674    fr: (mpl, bd) => bd ? `Pourcentage d'enfants atteignant les Niveaux de Compétence Minimum en ${mpl} par pays et ${bd.toLowerCase()}` : `Pourcentage d'enfants atteignant les Niveaux de Compétence Minimum en ${mpl} par pays`,
1675    es: (mpl, bd) => bd ? `Porcentaje de niños que alcanzan los Niveles Mínimos de Competencia en ${mpl} por país y ${bd.toLowerCase()}` : `Porcentaje de niños que alcanzan los Niveles Mínimos de Competencia en ${mpl} por país`,
1676    pt: (mpl, bd) => bd ? `Percentagem de crianças que atingem os Níveis Mínimos de Proficiência em ${mpl} por país e ${bd.toLowerCase()}` : `Percentagem de crianças que atingem os Níveis Mínimos de Proficiência em ${mpl} por país`,
1677  };
1678  const mplPart = mplNames[state.mpl] || state.mpl;
1679  const bd = (state.breakdown === 'country' || !breakdownLabel) ? null : breakdownLabel;
1680  document.getElementById('chart-title').textContent = (chartTitlePhrases[currentLang] || chartTitlePhrases.en)(mplPart, bd);
1681  document.getElementById('chart-sub').textContent   = t.chartSub;
1682  document.getElementById('th-group').textContent    = breakdownLabel || t.bdCountry;
1683  document.getElementById('table-title').textContent = `${t.tableTitle} — ${breakdownLabel || t.bdCountry}`;
1684}
1685
1686// ═══════════════════════════════════════════════════════════════════
1687// Table
1688// ═══════════════════════════════════════════════════════════════════
1689function renderTable() {
1690  const {countries, categories, results} = currentMplData;
1691  const tbody = document.getElementById('results-tbody');
1692  if (!countries || !countries.length) {
1693    tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#A0AEC0;padding:20px">No data matches current filters.</td></tr>';
1694    return;
1695  }
1696  const cats     = (categories && categories.length) ? categories : ['__single__'];
1697  const isSingle = cats[0] === '__single__';
1698
1699  tbody.innerHTML = countries.flatMap(c => cats.map(cat => {
1700    const row   = results[c]?.[cat] ?? {};
1701    const label = isSingle ? c : `${c} — ${cat}`;
1702    const cells = ['MPL_math','MPL_lang','MPL_both'].map(mpl => {
1703      const s = row[mpl];
1704      if (!s) return '<td>—</td>';
1705      if (s.warning) return `<td><span class="warn-badge">⚠ N=${s.n_unweighted}&lt;100</span></td>`;
1706      const w = Math.round((s.estimate/100)*70);
1707      return `<td><div class="pct-bar-wrap"><div class="pct-mini-bar" style="width:${w}px"></div><span>${s.estimate}%</span></div></td>`;
1708    }).join('');
1709    const n = row.MPL_math?.n_unweighted ?? '?';
1710    return `<tr><td><strong>${label}</strong></td>${cells}<td class="n-col">${typeof n==='number'?n.toLocaleString():n}</td></tr>`;
1711  })).join('');
1712}
1713
1714// ═══════════════════════════════════════════════════════════════════
1715// Access tab
1716// ═══════════════════════════════════════════════════════════════════
1717let accessData = null;
1718async function fetchAndRenderAccess() {
1719  showLoading(true);
1720  try {
1721    const filters = buildPayload();
1722    delete filters.ctx_filter;
1723    accessData = await fetch('/api/access-stats', {
1724      method: 'POST', headers: {'Content-Type':'application/json'},
1725      body: JSON.stringify({filters})
1726    }).then(r => r.json());
1727    document.getElementById('sidebar-n').textContent =
1728      `Filtered total: ${(accessData.n_total||0).toLocaleString()} children`;
1729    renderAccessCharts();
1730  } finally { showLoading(false); }
1731}
1732
1733function renderAccessCharts() {
1734  if (!accessData) return;
1735  const {countries, results} = accessData;
1736
1737  // Show/hide cards based on selected view
1738  const allCards = document.querySelectorAll('.access-card');
1739  const view = state.accessView || 'all';
1740  allCards.forEach(card => {
1741    const canvas = card.querySelector('canvas');
1742    if (!canvas) return;
1743    if (view === 'all') {
1744      card.style.display = '';
1745    } else {
1746      card.style.display = (canvas.id === view) ? '' : 'none';
1747    }
1748  });
1749
1750  // When single view, make the visible card full-width
1751  document.querySelectorAll('.access-grid').forEach(grid => {
1752    grid.style.gridTemplateColumns = (view === 'all') ? '1fr 1fr' : '1fr';
1753  });
1754
1755  // ── Girls + Boys stacked ─────────────────────────────────────────
1756  renderAccessStacked('acc-gender',
1757    [{key:'pct_girls', label:tl('catGirls'), color:PAL.orange},
1758     {key:'pct_boys',  label:tl('catBoys'),  color:PAL.teal}],
1759    countries, results);
1760
1761  // ── % Currently Enrolled ─────────────────────────────────────────
1762  renderAccessSingle('acc-enrolled', 'pct_enrolled', tl('catEnrolled'), countries, results, PAL.green);
1763
1764  // ── Urban + Rural stacked ────────────────────────────────────────
1765  renderAccessStacked('acc-location',
1766    [{key:'pct_urban', label:tl('catUrban'), color:PAL.blue},
1767     {key:'pct_rural', label:tl('catRural'), color:PAL.lightOrange}],
1768    countries, results);
1769
1770  // ── School type distribution ──────────────────────────────────────
1771  renderAccessDist('acc-school', 'school_type', countries, results,
1772    ['Government','Private','Other'], [PAL.teal, PAL.green, PAL.orange],
1773    [tl('catGovt'), tl('catPrivate'), tl('catOther')]);
1774
1775  // ── Home language ─────────────────────────────────────────────────
1776  renderAccessSingle('acc-homelang', 'pct_home_lang', tl('catHomeLang'), countries, results, PAL.blue);
1777
1778  // ── Computer/tablet ──────────────────────────────────────────────
1779  renderAccessSingle('acc-computer', 'pct_computer', tl('catComputer'), countries, results, PAL.gold);
1780
1781  // ── Phone type distribution ───────────────────────────────────────
1782  renderAccessDist('acc-phone', 'phone_type', countries, results,
1783    ['No Phone','Basic Phone','Smartphone'], ['#C53030', PAL.orange, PAL.green],
1784    [tl('catNoPhone'), tl('catBasicPhone'), tl('catSmartphone')]);
1785
1786  // ── Parental education ────────────────────────────────────────────
1787  renderAccessDist('acc-edu', 'parental_edu', countries, results,
1788    ['None','Primary','Secondary','Tertiary','University'],
1789    ['#C53030', PAL.orange, PAL.teal, PAL.blue, PAL.green],
1790    [tl('catNone'), tl('catPrimary'), tl('catSecondary'), tl('catTertiary'), tl('catUniversity')]);
1791}
1792
1793/** Single-series horizontal bar for one access indicator */
1794function renderAccessSingle(cid, key, label, countries, results, color) {
1795  destroyChart(cid);
1796  const canvas = document.getElementById(cid);
1797  if (!canvas) return;
1798  const estimates = countries.map(c => results[c]?.[key]?.estimate ?? null);
1799  const warnings  = countries.map(c => results[c]?.[key]?.warning ?? false);
1800  const ns        = countries.map(c => results[c]?.[key]?.n_unweighted ?? 0);
1801  chartInstances[cid] = new Chart(canvas.getContext('2d'), {
1802    type: 'bar',
1803    data: {labels: countries.map(tc), datasets: [{
1804      label, data: estimates.map((v,i) => warnings[i] ? 0 : (v??0)),
1805      backgroundColor: estimates.map((_,i) => warnings[i] ? '#FED7AA' : color),
1806      borderRadius: 3
1807    }]},
1808    options: {
1809      indexAxis: 'y', responsive: true, maintainAspectRatio: false,
1810      plugins: {legend:{display:false}, tooltip:{callbacks:{label: c =>
1811        warnings[c.dataIndex] ? `⚠ N<100` : `${estimates[c.dataIndex]}%  (N=${ns[c.dataIndex].toLocaleString()})`
1812      }}},
1813      scales: {
1814        x:{min:0,max:100,grid:{color:'#EDF2F7'},title:{display:true,text:'%',font:{size:10}},ticks:{font:{size:10}}},
1815        y:{grid:{display:false},ticks:{font:{size:10},color:PAL.teal}}
1816      }
1817    },
1818    plugins: [barLabelPlugin(warnings)]
1819  });
1820}
1821
1822/** Stacked horizontal bar for 2 indicators in one bar per country (e.g. Girls + Boys) */
1823function renderAccessStacked(cid, series, countries, results) {
1824  destroyChart(cid);
1825  const canvas = document.getElementById(cid);
1826  if (!canvas) return;
1827  const datasets = series.map(s => ({
1828    label: s.label,
1829    data: countries.map(c => {
1830      const v = results[c]?.[s.key];
1831      return (v && !v.warning) ? v.estimate : 0;
1832    }),
1833    backgroundColor: s.color,
1834    borderRadius: 2,
1835    maxBarThickness: 16,
1836  }));
1837  chartInstances[cid] = new Chart(canvas.getContext('2d'), {
1838    type: 'bar',
1839    data: {labels: countries.map(tc), datasets},
1840    options: {
1841      indexAxis: 'y', responsive: true, maintainAspectRatio: false,
1842      plugins: {
1843        legend:{display:true, position:'bottom', labels:{boxWidth:10,font:{size:10}}},
1844        tooltip:{callbacks:{label: c => `${c.dataset.label}: ${c.parsed.x ?? '?'}%`}}
1845      },
1846      scales: {
1847        x:{stacked:true, min:0, max:100, grid:{color:'#EDF2F7'},
1848           title:{display:true,text:'%',font:{size:10}}, ticks:{font:{size:10}}},
1849        y:{stacked:true, grid:{display:false}, ticks:{font:{size:10},color:PAL.teal}}
1850      }
1851    },
1852    plugins: [stackedLabelPlugin()]
1853  });
1854}
1855
1856function renderAccessDist(cid, key, countries, results, cats, colors, catLabels) {
1857  destroyChart(cid);
1858  const canvas = document.getElementById(cid);
1859  if (!canvas) return;
1860  const datasets = cats.map((cat, ci) => ({
1861    label: catLabels ? catLabels[ci] : cat,
1862    data: countries.map(c => results[c]?.[key]?.[cat]?.estimate ?? 0),
1863    backgroundColor: colors[ci], borderRadius: 2,
1864  }));
1865  chartInstances[cid] = new Chart(canvas.getContext('2d'), {
1866    type: 'bar',
1867    data: {labels: countries.map(tc), datasets},
1868    options: {
1869      indexAxis: 'y', responsive: true, maintainAspectRatio: false,
1870      plugins: {legend:{display:true,position:'bottom',labels:{boxWidth:10,font:{size:10}}}},
1871      scales: {
1872        x:{stacked:true,min:0,max:100,grid:{color:'#EDF2F7'},title:{display:true,text:'%',font:{size:10}},ticks:{font:{size:10}}},
1873        y:{stacked:true,grid:{display:false},ticks:{font:{size:10},color:PAL.teal}}
1874      }
1875    },
1876    plugins: [stackedLabelPlugin()]
1877  });
1878}
1879
1880// ═══════════════════════════════════════════════════════════════════
1881// Chatbot
1882// ═══════════════════════════════════════════════════════════════════
1883// ── User Guide modal ────────────────────────────────────────────────
1884function openDemo() {
1885  document.getElementById('demo-modal').classList.add('open');
1886}
1887function closeDemo() {
1888  const modal = document.getElementById('demo-modal');
1889  modal.classList.remove('open');
1890  document.getElementById('demo-video').pause();
1891}
1892function closeDemoOnOverlay(e) {
1893  if (e.target === document.getElementById('demo-modal')) closeDemo();
1894}
1895
1896function openGuide() {
1897  const modal = document.getElementById('guide-modal');
1898  const iframe = document.getElementById('guide-iframe');
1899  const guideMap = {en:'user-guide.pdf', fr:'user-guide-fr.pdf', es:'user-guide-es.pdf', pt:'user-guide-pt.pdf'};
1900  const pdf = '/static/' + (guideMap[currentLang] || guideMap.en);
1901  iframe.src = pdf;
1902  // Update download link to match language
1903  modal.querySelector('.guide-download-btn').href = pdf;
1904  modal.classList.add('open');
1905  document.body.style.overflow = 'hidden';
1906}
1907function closeGuide() {
1908  document.getElementById('guide-modal').classList.remove('open');
1909  document.body.style.overflow = '';
1910}
1911function closeGuideOnOverlay(e) {
1912  if (e.target === document.getElementById('guide-modal')) closeGuide();
1913}
1914
1915function toggleChat() { document.getElementById('chat-panel').classList.toggle('open'); }
1916function chatKey(e)   { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } }
1917function sendEx(chip) { document.getElementById('chat-input').value = chip.textContent.trim(); sendChat(); }
1918
1919async function sendChat() {
1920  const input = document.getElementById('chat-input');
1921  const text  = input.value.trim();
1922  if (!text) return;
1923  addMsg('user', text);
1924  chatHistory.push({role:'user', content:text});
1925  input.value = '';
1926  document.getElementById('chat-send').disabled = true;
1927  const typing = addTyping();
1928  try {
1929    const res  = await fetch('/api/chat', {method:'POST',
1930      headers:{'Content-Type':'application/json'},
1931      body: JSON.stringify({messages: chatHistory, lang: currentLang})});
1932    const data = await res.json();
1933    typing.remove();
1934    const reply = data.error ? `⚠️ ${data.error}` : data.response;
1935    addMsg('assistant', reply);
1936    if (!data.error) chatHistory.push({role:'assistant', content:reply});
1937  } catch(e) {
1938    typing.remove();
1939    addMsg('assistant','⚠️ Could not reach the server. Please try again.');
1940  } finally { document.getElementById('chat-send').disabled = false; }
1941}
1942
1943function addMsg(role, text) {
1944  const msgs = document.getElementById('chat-msgs');
1945  const now  = new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
1946  const div  = document.createElement('div');
1947  div.className = `msg ${role}`;
1948  div.innerHTML = `<div class="msg-bubble">${esc(text)}</div><div class="msg-time">${now}</div>`;
1949  msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div;
1950}
1951function addTyping() {
1952  const msgs = document.getElementById('chat-msgs');
1953  const div  = document.createElement('div');
1954  div.className = 'msg assistant';
1955  div.innerHTML = '<div class="msg-bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div>';
1956  msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div;
1957}
1958function esc(s) {
1959  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
1960}
1961
1962function showLoading(on) { document.getElementById('loading').classList.toggle('show',on); }
1963
1964// ═══════════════════════════════════════════════════════════════════
1965// Export helpers
1966// ═══════════════════════════════════════════════════════════════════
1967function _chartTitledDataURL(chart, title, subtitle) {
1968  const src    = chart.canvas;
1969  const titleH = subtitle ? 56 : 36;
1970  const tmp    = document.createElement('canvas');
1971  tmp.width    = src.width;
1972  tmp.height   = src.height + titleH;
1973  const ctx    = tmp.getContext('2d');
1974  ctx.fillStyle = '#ffffff';
1975  ctx.fillRect(0, 0, tmp.width, tmp.height);
1976  ctx.fillStyle = '#014663';
1977  ctx.font = 'bold 13px Calibri, Arial, sans-serif';
1978  ctx.textBaseline = 'alphabetic';
1979  ctx.fillText(title, 12, subtitle ? 22 : 22);
1980  if (subtitle) {
1981    ctx.fillStyle = '#718096';
1982    ctx.font = '11px Calibri, Arial, sans-serif';
1983    ctx.fillText(subtitle, 12, 42);
1984  }
1985  ctx.drawImage(src, 0, titleH);
1986  return tmp.toDataURL('image/png');
1987}
1988
1989function _downloadURL(dataURL, filename) {
1990  const a = document.createElement('a');
1991  a.href = dataURL;
1992  a.download = filename;
1993  a.click();
1994}
1995
1996function exportChartPNG() {
1997  const date     = new Date().toISOString().slice(0,10);
1998  const subtitle = document.getElementById('chart-sub').textContent;
1999  const isAll    = state.mpl === 'ALL';
2000  if (isAll) {
2001    const baseTitle = document.getElementById('chart-title').textContent;
2002    const items = [
2003      {id:'chart-math', label:'Math',    title: baseTitle.replace(/Math.*$/, 'Math' + baseTitle.slice(baseTitle.
2003indexOf(' by')))},
2004      {id:'chart-lang', label:'Reading', title: baseTitle.replace(/Math.*$/, 'Reading' + baseTitle.slice(baseTitle.indexOf(' by')))},
2005    ];
2006    items.forEach(({id, label, title}, i) => {
2007      const chart = chartInstances[id];
2008      if (!chart) return;
2009      setTimeout(() => {
2010        _downloadURL(_chartTitledDataURL(chart, title, subtitle), `PAL_MPL_${label}_${date}.png`);
2011      }, i * 400);
2012    });
2013  } else {
2014    const chart = chartInstances['mpl-chart'];
2015    if (!chart) return;
2016    const title    = document.getElementById('chart-title').textContent;
2017    const mplName  = {MPL_math:'Math', MPL_lang:'Reading', MPL_both:'Both'}[state.mpl] || state.mpl;
2018    _downloadURL(_chartTitledDataURL(chart, title, subtitle), `PAL_MPL_${mplName}_${date}.png`);
2019  }
2020}
2021
2022function exportAccessExcel(cid, label) {
2023  if (!accessData) { alert('No data loaded yet — apply filters first.'); return; }
2024  if (typeof XLSX === 'undefined') { alert('Excel library not loaded. Check your connection and refresh.'); return; }
2025  const {countries, results} = accessData;
2026  const cfg = {
2027    'acc-gender':   {type:'single', cols:['Country','% Girls','% Boys'],                                          keys:['pct_girls','pct_boys']},
2028    'acc-enrolled': {type:'single', cols:['Country','% Enrolled'],                                                keys:['pct_enrolled']},
2029    'acc-location': {type:'single', cols:['Country','% Urban','% Rural'],                                         keys:['pct_urban','pct_rural']},
2030    'acc-homelang': {type:'single', cols:['Country','% Home Language Match'],                                     keys:['pct_home_lang']},
2031    'acc-computer': {type:'single', cols:['Country','% Computer/Tablet'],                                         keys:['pct_computer']},
2032    'acc-school':   {type:'dist',   cols:['Country','Government (%)','Private (%)','Other (%)'],                  distKey:'school_type',   cats:['Government','Private','Other']},
2033    'acc-phone':    {type:'dist',   cols:['Country','No Phone (%)','Basic Phone (%)','Smartphone (%)'],           distKey:'phone_type',    c
2033ats:['No Phone','Basic Phone','Smartphone']},
2034    'acc-edu':      {type:'dist',   cols:['Country','None (%)','Primary (%)','Secondary (%)','Tertiary (%)','University (%)'], distKey:'parental_edu', cats:['None','Primary','Secondary','Tertiary','University']},
2035  }[cid];
2036  if (!cfg) return;
2037  const rows = [cfg.cols];
2038  countries.forEach(c => {
2039    const r = results[c] ?? {};
2040    if (cfg.type === 'single') {
2041      rows.push([c, ...cfg.keys.map(k => {
2042        const v = r[k];
2043        return !v ? '—' : v.warning ? 'Suppressed (N<100)' : (v.estimate ?? '—');
2044      })]);
2045    } else {
2046      rows.push([c, ...cfg.cats.map(cat => r[cfg.distKey]?.[cat]?.estimate ?? '—')]);
2047    }
2048  });
2049  const wb = XLSX.utils.book_new();
2050  const ws = XLSX.utils.aoa_to_sheet(rows);
2051  ws['!cols'] = cfg.cols.map(() => ({wch: 20}));
2052  XLSX.utils.book_append_sheet(wb, ws, label.replace(/_/g,' '));
2053  XLSX.writeFile(wb, `PAL_Access_${label}_${new Date().toISOString().slice(0,10)}.xlsx`);
2054}
2055
2056function exportAccessChart(cid, label) {
2057  const chart = chartInstances[cid];
2058  if (!chart) { alert('No chart loaded yet — apply filters first.'); return; }
2059  const date     = new Date().toISOString().slice(0,10);
2060  const canvas   = document.getElementById(cid);
2061  const titleEl  = canvas?.closest('.access-card')?.querySelector('.access-card-title');
2062  const title    = titleEl ? titleEl.childNodes[0].textContent.trim() : label.replace(/_/g,' ');
2063  _downloadURL(_chartTitledDataURL(chart, title, 'Survey-weighted estimates'), `PAL_Access_${label}_${date}.png`);
2064}
2065
2066function exportTableExcel() {
2067  const {countries, categories, results} = currentMplData;
2068  if (!countries || !countries.length) { alert('No data to export — apply filters first.'); return; }
2069  if (typeof XLSX === 'undefined') { alert('Excel library not loaded. Check your connection and refresh.'); return; }
2070  const cats = (categories && categories.length) ? categories : ['__single__'];
2071  const isSingle = cats[0] === '__single__';
2072  const rows = [['Group', 'MPL Math (%)', 'MPL Reading (%)', 'MPL Both (%)', 'N (unweighted)']];
2073  countries.forEach(c => {
2074    cats.forEach(cat => {
2075      const row   = results[c]?.[cat] ?? {};
2076      const label = isSingle ? c : `${c} — ${cat}`;
2077      rows.push([
2078        label,
2079        row.MPL_math?.warning ? 'Suppressed (N<100)' : (row.MPL_math?.estimate ?? '—'),
2080        row.MPL_lang?.warning ? 'Suppressed (N<100)' : (row.MPL_lang?.estimate ?? '—'),
2081        row.MPL_both?.warning ? 'Suppressed (N<100)' : (row.MPL_both?.estimate ?? '—'),
2082        row.MPL_math?.n_unweighted ?? '—',
2083      ]);
2084    });
2085  });
2086  const wb = XLSX.utils.book_new();
2087  const ws = XLSX.utils.aoa_to_sheet(rows);
2088  ws['!cols'] = [{wch:32},{wch:15},{wch:15},{wch:15},{wch:16}];
2089  XLSX.utils.book_append_sheet(wb, ws, 'MPL Results');
2090  const date = new Date().toISOString().slice(0,10);
2091  XLSX.writeFile(wb, `PAL_ICAN_ICAR_Results_${date}.xlsx`);
2092}
2093
2094window.addEventListener('DOMContentLoaded', init);
2095</script>
2095
2096</body>
2097</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.