PageSourceSearch

https://www.w3schools.com/lib/account/dashboard-lite.js?updated=20260902

js w3schools.com collected 2026-09-24 08:23:58 UTC 85,886 bytes, 1,335 lines download raw bytes

1(function () {
2  if (window.DashboardLite) return;
3
4  var DashboardLite = {};
5
6  // ── Config ──────────────────────────────────────────────────────────────
7  DashboardLite._apiBase = 'https://api.kai.w3sapi.com/pathfinder';
8  DashboardLite._profilePicBase = 'https://www.w3profile.com';
9  DashboardLite._leagueImgBase = 'https://pathfinder.w3schools.com/images/leagues';
10  DashboardLite._profileBase = 'https://profile.w3schools.com';
11  // Logged-in league links go to the pathfinder leaderboard; anon/frozen go to
12  // the marketing page (_leagueInfoUrl) since the live board needs an account.
13  DashboardLite._leaderboardUrl = 'https://pathfinder.w3schools.com/leaderboard';
14  DashboardLite._leagueInfoUrl = '/league/';
15  DashboardLite._styleId = 'dashboard-lite-style';
16  DashboardLite._fallbackAvatar = '/images/lynx.svg';
17
18  // Named formats (ad-unit style). Each maps to a fixed box + a layout mode.
19  // No format -> 'stack' (the original 300px-wide auto-height card on default.asp).
20  DashboardLite.FORMATS = {
21    '960x300': { w: 960, h: 300, mode: 'row' },
22    '720x250': { w: 720, h: 250, mode: 'row' },
23    '300x300': { w: 300, h: 300, mode: 'grid' },
24    '160x300': { w: 160, h: 300, mode: 'col' },
25    '140x300': { w: 140, h: 300, mode: 'col' }
26  };
27
28  // Official league tier lookup (code -> display name + badge filename).
29  DashboardLite._tierMap = {
30    wd: { name: 'Wood', icon: 'wood.svg' },
31    br: { name: 'Bronze', icon: 'bronze.svg' },
32    si: { name: 'Silver', icon: 'silver.svg' },
33    go: { name: 'Gold', icon: 'gold.svg' },
34    pl: { name: 'Platinum', icon: 'platinum.svg' },
35    di: { name: 'Diamond', icon: 'diamond.svg' },
36    ll: { name: 'Logic Legend', icon: 'logic_legend.svg' },
37    ss: { name: 'Script Samurai', icon: 'script_samurai.svg' },
38    bs: { name: 'Bug Slayer', icon: 'bug_slayer.svg' },
39    sc: { name: 'Supreme Coder', icon: 'supreme_coder.svg' },
40    mc: { name: 'Master Coder', icon: 'master_coder.svg' }
41  };
42  DashboardLite._tier = function (code) {
43    return DashboardLite._tierMap[String(code || '').toLowerCase()] || DashboardLite._tierMap.wd;
44  };
45  DashboardLite._tierName = function (code) { return DashboardLite._tier(code).name; };
46  DashboardLite._tierIcon = function (code) {
47    return DashboardLite._leagueImgBase + '/' + DashboardLite._tier(code).icon;
48  };
49
50  // Mood overlay badge (avatar.layers.mood -> companion svg).
51  DashboardLite._moodBase = 'https://pathfinder.w3schools.com/images/companion/mood';
52  DashboardLite._moodMap = {
53    'mood-focused': 'direct_hit.svg', 'mood-zen': 'artist_palette.svg',
54    'mood-cool': 'smiling_face_with_sunglasses.svg', 'mood-silly': 'pile_of_poo.svg',
55    'mood-strong': 'flexed_biceps.svg', 'mood-horns': 'smiling_face_with_horns.svg',
56    'mood-unicorn': 'unicorn.svg', 'mood-zany': 'zany_face.svg'
57  };
58  DashboardLite._moodIcon = function (mood) {
59    var f = DashboardLite._moodMap[mood];
60    return f ? DashboardLite._moodBase + '/' + f : null;
61  };
62
63  // ── Social sign-in (Cognito OAuth - mirrors lib/account/auth-bar.js) ───────
64  DashboardLite._authUrl = 'https://auth.w3spaces.com';
65  DashboardLite._cognitoClientId = 'd1grqml0emh7votkb0gtrrn0';
66  DashboardLite._socialIcons = {
67    google: '<svg width="20" height="20" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>',
68    facebook: '<svg width="20" height="20" viewBox="0 0 24 24" fill="#0080ff"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
69    github: '<svg width="20" height="20" viewBox="0 0 18 18"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.00194 0C13.9726 0 18.0019 4.13093 18.0019 9.22763C18.0019 13.3037 15.4261 16.7615 11.8522 17.9828C11.3959 18.0737 11.2339 17.7856 11.2339 17.5399C11.2339 17.2357 11.2447 16.2421 11.2447 15.0073C11.2447 14.1469 10.9567 13.5854 10.6336 13.2992C12.6379 13.0706 14.7439 12.2902 14.7439 8.74599C14.7439 7.73799 14.3947 6.91543 13.8169 6.26923C13.9105 6.03613 14.2192 5.09755 13.7287 3.82675C13.7287 3.82675 12.9745 3.5795 11.2564 4.77289C10.5373 4.56859 9.76694 4.4658 9.00194 4.4622C8.23694 4.4658 7.46745 4.56859 6.74925 4.77289C5.02935 3.5795 4.27335 3.82675 4.27335 3.82675C3.78465 5.09755 4.09335 6.03613 4.18605 6.26923C3.61095 6.91543 3.25905 7.73799 3.25905 8.74599C3.25905 12.2812 5.36055 13.0735 7.35945 13.3066C7.10205 13.537 6.86895 13.9435 6.78795 14.5402C6.27495 14.776 4.97175 15.1841 4.16895 13.7738C4.16895 13.7738 3.69285 12.8871 2.78925 12.8223C2.78925 12.8223 1.91175 12.8107 2.72805 13.3831C2.72805 13.3831 3.31755 13.6666 3.72705 14.7331C3.72705 14.7331 4.25535 16.3801 6.75915 15.8221C6.76365 16.5934 6.77175 17.3203 6.77175 17.5399C6.77175 17.7838 6.60614 18.0692 6.15704 17.9837C2.58045 16.7642 0.00195312 13.3046 0.00195312 9.22763C0.00195312 4.13093 4.03215 0 9.00194 0Z" fill="#282A35"/></svg>'
70  };
71  DashboardLite._genState = function () {
72    var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', r = '';
73    for (var i = 0; i < 32; i++) r += chars.charAt(Math.floor(Math.random() * chars.length));
74    return r;
75  };
76  DashboardLite._socialUrl = function (provider, returnUrl) {
77    var idp = { google: 'Google', facebook: 'Facebook', github: 'Github' }[provider] || provider;
78    return DashboardLite._authUrl + '/oauth2/authorize' +
79      '?redirect_uri=' + encodeURIComponent(DashboardLite._profileBase + '/social-login') +
80      '&response_type=code&client_id=' + DashboardLite._cognitoClientId +
81      '&identity_provider=' + idp +
82      '&scope=openid%20aws.cognito.signin.user.admin' +
83      '&state=' + DashboardLite._genState() +
84      '&final_return=' + encodeURIComponent(returnUrl);
85  };
86  DashboardLite._openOAuthPopup = function (url, returnUrl) {
87    try {
88      document.cookie = 'socialLoginRedirectUrl=' + encodeURIComponent(returnUrl) +
89        '; path=/; domain=.w3schools.com; secure; samesite=strict';
90    } catch (e) {}
91    var w = 450, h = 450, l = (window.innerWidth - w) / 2, t = (window.innerHeight - h) / 2;
92    var popup = window.open(url, '_blank', 'width=' + w + ',height=' + h + ',left=' + l + ',top=' + t + ',popup=1');
93    if (!popup || popup.closed || typeof popup.closed === 'undefined') window.location.href = url;
94  };
95  DashboardLite._socialBtn = function (provider, returnUrl) {
96    var url = DashboardLite._socialUrl(provider, returnUrl);
97    var safeRet = String(returnUrl).replace(/'/g, "\\'");
98    var onclick = "event.preventDefault();event.stopPropagation();DashboardLite._openOAuthPopup('" +
99      url.replace(/'/g, "\\'") + "','" + safeRet + "');";
100    return '<a href="' + DashboardLite._esc(url) + '" onclick="' + onclick + '" ' +
101      'class="dlw-social ga-fp ga-dlw-' + provider + '" ' +
102      'title="Continue with ' + provider.charAt(0).toUpperCase() + provider.slice(1) + '">' +
103      DashboardLite._socialIcons[provider] + '</a>';
104  };
105
106  // Brand logos for the continue-card topic icon (product marks, not design tokens).
107  DashboardLite._langIcons = {
108    python: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 32 32"><defs><linearGradient id="dlw-py-b" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#387EB8"/><stop offset="100%" stop-color="#366994"/></linearGradient><linearGradient id="dlw-py-y" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FFE052"/><stop offset="100%" stop-color="#FFC331"/></linearGradient></defs><path d="M15.885 2.1c-7.1 0-6.651 3.07-6.651 3.07v3.19h6.752v1H6.304s-4.604.208-4.604 6.645c0 6.442 4.013 6.912 4.013 6.912H8.33v-3.361s-.13-4.013 3.9-4.013h6.762s3.772.06 3.772-3.652V5.8s.572-3.712-6.842-3.712zm-3.732 2.137a1.214 1.214 0 1 1-1.183 1.244v-.02a1.214 1.214 0 0 1 1.214-1.214z" fill="url(#dlw-py-b)"/><path d="M16.085 29.91c7.1 0 6.651-3.08 6.651-3.08v-3.18h-6.751v-1h9.47S30 23.158 30 15.995s-4.013-6.912-4.013-6.912H23.64v3.361s.13 4.013-3.9 4.013h-6.765S9.2 16.4 9.2 20.112v6.091s-.572 3.712 6.842 3.712zm3.732-2.147a1.214 1.214 0 1 1 1.214-1.214 1.214 1.214 0 0 1-1.214 1.214z" fill="url(#dlw-py-y)"/></svg>',
109    html: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#E34F26" viewBox="0 0 24 24"><path d="M1.5 0h21l-1.91 21.563L11.977 24l-8.564-2.438L1.5 0zm7.031 9.75l-.232-2.718 10.059.003.23-2.622L5.412 4.41l.698 8.01h9.126l-.326 3.426-2.91.804-2.955-.81-.188-2.11H6.248l.33 4.171L12 19.351l5.379-1.443.744-8.157H8.531z"/></svg>',
110    css: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#1572B6" viewBox="0 0 24 24"><path d="M1.5 0h21l-1.91 21.563L11.977 24l-8.565-2.438L1.5 0zm17.09 4.413L5.41 4.41l.213 2.622 10.125.002-.255 2.716h-6.64l.24 2.573h6.182l-.366 3.523-2.91.804-2.956-.81-.188-2.11h-2.61l.29 3.855L12 19.288l5.373-1.53L18.59 4.414z"/></svg>',
111    // J first, then S - the two glyphs used to be transposed and read "SJ".
112    javascript: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24"><rect width="24" height="24" fill="#F7DF1E"/><path fill="#000" d="M6.3 18.4c.3.6.7 1 1.4 1 .6 0 1-.2 1-1.2v-6.3h2v6.3c0 2-1.2 2.9-2.9 2.9-1.5 0-2.5-.8-3-1.8zM13.7 18.2c.4.7.9 1.2 1.9 1.2.8 0 1.3-.4 1.3-1 0-.7-.5-.9-1.5-1.3l-.5-.2c-1.5-.6-2.4-1.4-2.4-3 0-1.5 1.1-2.6 2.9-2.6 1.3 0 2.2.4 2.8 1.6l-1.5 1c-.3-.6-.7-.8-1.3-.8-.5 0-.9.3-.9.8 0 .6.4.8 1.3 1.2l.5.2c1.7.7 2.6 1.5 2.6 3.1 0 1.8-1.4 2.7-3.3 2.7-1.8 0-3-.9-3.6-2z"/></svg>',
113    sql: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#00758F" viewB
113ox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5c0 1.7-3.6 3-8 3S4 6.7 4 5z" opacity=".7"/><path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6c0 1.7-3.6 3-8 3s-8-1.3-8-3z" opacity=".5"/></svg>',
114    php: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#777BB4" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715l.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zM17.766 10.207h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>',
115    java: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 32 32"><path fill="#5382A1" d="M11.622 24.74s-1.264.748.9.998c2.626.308 3.968.264 6.854-.308 0 0 .76.484 1.82.9-6.49 2.802-14.674-.16-9.574-1.59zm-.826-3.924s-1.43 1.056.748 1.278c2.782.308 4.97.33 8.76-.44 0 0 .528.54 1.353.836-7.81 2.296-16.5.176-10.86-1.672z"/><path fill="#E76F00" d="M18.996 1s3.014 3.036-2.858 7.7c-4.708 3.748-1.076 5.894 0 8.338-2.744-2.478-4.774-4.664-3.414-6.692 1.996-2.97 7.53-4.422 6.272-9.346zm-1.576 14.026c1.464 1.68-.374 3.19-.374 3.19s3.702-1.92 2-4.312c-1.584-2.222-2.804-3.326 3.79-7.106 0 0-10.374 2.596-5.412 8.228z"/></svg>',
116    react: '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#61DAFB" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><g fill="none" stroke="#61DAFB" stroke-width="1"><ellipse cx="12" cy="12" rx="11" ry="4.2"/><ellipse cx="12" cy="12" rx="11" ry="4.2" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="11" ry="4.2" transform="rotate(120 12 12)"/></g></svg>'
117  };
118  DashboardLite._topicKey = function (name) {
119    var k = String(name == null ? '' : name).toLowerCase();
120    if (k.indexOf('javascript') > -1) return 'javascript';
121    if (k.indexOf('python') > -1) return 'python';
122    if (k.indexOf('html') > -1) return 'html';
123    if (k.indexOf('css') > -1) return 'css';
124    if (k.indexOf('sql') > -1) return 'sql';
125    if (k.indexOf('php') > -1) return 'php';
126    if (k.indexOf('react') > -1) return 'react';
127    if (k.indexOf('java') > -1) return 'java';
128    return '';
129  };
130  DashboardLite._topicIconHtml = function (name) {
131    var key = DashboardLite._topicKey(name);
132    if (key && DashboardLite._langIcons[key]) return DashboardLite._langIcons[key];
133    var letter = String(name || '?').trim().charAt(0).toUpperCase() || '?';
134    return '<span style="font-family:\'Source Sans Pro\',sans-serif;font-weight:700;font-size:18px;color:#04AA6D">' +
135      DashboardLite._esc(letter) + '</span>';
136  };
137
138  // ── Auth / fetch ────────────────────────────────────────────────────────
139  DashboardLite._getToken = function () {
140    if (typeof SharedAPI !== 'undefined' && typeof SharedAPI.getAccessToken === 'function') {
141      var t = SharedAPI.getAccessToken();
142      if (t) return t;
143    }
144    try {
145      var cookies = document.cookie.split(';');
146      for (var i = 0; i < cookies.length; i++) {
147        var c = cookies[i].trim();
148        if (c.indexOf('accessToken=') === 0) return c.substring('accessToken='.length);
149      }
150    } catch (e) {}
151    return null;
152  };
153
154  DashboardLite._get = function (path) {
155    var token = DashboardLite._getToken();
156    if (!token) return Promise.reject(new Error('No access token'));
157    return fetch(DashboardLite._apiBase + path, {
158      method: 'GET',
159      headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }
160    }).then(function (res) {
161      if (!res.ok) throw new Error('API ' + res.status + ' for ' + path);
162      return res.json();
163    });
164  };
165
166  DashboardLite.fetchProfile = function () {
167    return DashboardLite._get('/profile-api/profile/basic-lite?fields=personal').then(function (data) {
168      var p = (data && data.personal) || {};
169      var avatar = p.profilePicture
170        ? DashboardLite._profilePicBase + '/' + p.profilePicture : DashboardLite._fallbackAvatar;
171      return {
172        firstName: p.firstName || '', lastName: p.lastName || '',
173        fullName: ((p.firstName || '') + ' ' + (p.lastName || '')).trim(),
174        avatar: avatar
175      };
176    });
177  };
178
179  DashboardLite.fetchStreaks = function () {
180    return DashboardLite._get('/competency-api/streaks').then(function (data) {
181      data = data || {};
182      return {
183        streaks: typeof data.streaks === 'number' ? data.streaks : 0,
184        freezeUsed: data.freezeUsedThisMonth,
185        freezeRemaining: data.freezeRemainingThisMonth
186      };
187    });
188  };
189
190  DashboardLite.fetchLeagueXp = function () {
191    return DashboardLite._get('/league-api/user/xp').then(function (data) {
192      data = data || {};
193      var league = data.league || {};
194      return {
195        xp: typeof data.xp === 'number' ? data.xp : 0,
196        weeklyXp: typeof data.weeklyXp === 'number' ? data.weeklyXp : 0,
197        optedIn: !!league.optedIn, tier: league.tier || null,
198        leagueId: league.leagueId || null, cohortId: league.cohortId || null
199      };
200    });
201  };
202
203  // GET /league-api/user/profile -> rendered avatar URL + current mood.
204  DashboardLite.fetchAvatar = function () {
205    return DashboardLite._get('/league-api/user/profile').then(function (data) {
206      data = data || {};
207      var layers = (data.avatar && data.avatar.layers) || {};
208      return {
209        avatar: data.profilePictureUrl
210          ? DashboardLite._profilePicBase + '/' + data.profilePictureUrl : null,
211        mood: layers.mood || null
212      };
213    });
214  };
215
216  // GET /league-api/leaderboard/cohort -> standings for the user's own cohort.
217  DashboardLite.fetchCohort = function () {
218    return DashboardLite._get('/league-api/leaderboard/cohort').then(function (data) {
219      data = data || {};
220      return {
221        users: Array.isArray(data.users) ? data.users : [],
222        userRank: typeof data.userRank === 'number' ? data.userRank : null,
223        totalUsers: typeof data.totalUsers === 'number' ? data.totalUsers : null
224      };
225    });
226  };
227
228  // GET /competency-api/xp -> {"xp": N}. The canonical total XP - the league
229  // endpoint reports xp:0 for users who have not opted into the league.
230  DashboardLite.fetchXpTotal = function () {
231    return DashboardLite._get('/competency-api/xp').then(function (data) {
232      return (data && typeof data.xp === 'number') ? data.xp : null;
233    });
234  };
235
236  // Per-page promise memo so multiple widget instances share one request each.
237  DashboardLite._memo = {};
238  DashboardLite._once = function (key, fn) {
239    if (!DashboardLite._memo[key]) DashboardLite._memo[key] = fn();
240    return DashboardLite._memo[key];
241  };
242
243  DashboardLite.loadAll = function () {
244    var ff = function (r) { return r.status === 'fulfilled' ? r.value : null; };
245    return Promise.allSettled([
246      DashboardLite._once('profile', DashboardLite.fetchProfile),
247      DashboardLite._once('streaks', DashboardLite.fetchStreaks),
248      DashboardLite._once('league', DashboardLite.fetchLeagueXp),
249      DashboardLite._once('avatar', DashboardLite.fetchAvatar),
250      DashboardLite._once('cohort', DashboardLite.fetchCohort),
251      DashboardLite._once('xptotal', DashboardLite.fetchXpTotal)
252    ]).then(function (r) {
253      return {
254        profile: ff(r[0]), streaks: ff(r[1]), league: ff(r[2]),
255        avatarP: ff(r[3]), cohort: ff(r[4]), xpTotal: ff(r[5])
256      };
257    });
258  };
259
260  // Languages that have a weekly-practice page at /practice/practice_<slug>.php
261  DashboardLite._practiceMap = {
262    python: 'python', javascript: 'javascript', java: 'java', 'c++': 'cpp', 'c#': 'csharp',
263    c: 'c', php: 'php', kotlin: 'kotlin', r: 'r', rust: 'rust', swift: 'swift', typescript: 'typescript'
264  };
265  DashboardLite._practiceUrl = function (topicName) {
266    var slug = DashboardLite._practiceMap[String(topicName || '').toLowerCase()];
267    return slug ? '/practice/practice_' + slug + '.php' : null;
268  };
269  // A topic where the user only hit the landing page (not a real lesson).
270  DashboardLite._isLanding = function (t) {
271    return t.lessonName === 'Home' || /(?:default\.asp|index\.php)$/i.test(t.lessonRelPath || '');
272  };
273
274  // Last successful continue result, cached so frozen accounts (whose state API may
275  // refuse them) and transient failures still get their Continue row.
276  DashboardLite._contCacheKey = 'DashboardLite.lastContinue';
277  DashboardLite._readContCache = function () {
278    try {
279      var o = JSON.parse(localStorage.getItem(DashboardLite._contCacheKey));
280      return (o && o.title && o.url) ? o : null;
281    } catch (e) { return null; }
282  };
283  DashboardLite._writeContCache = function (c) {
284    try { localStorage.setItem(DashboardLite._contCacheKey, JSON.stringify(c)); } catch (e) {}
285  };
286
287  // The user's current learning language + lesson.
288  // Returns {title, topic, next, progress, url, practice} or null.
289  DashboardLite.fetchContinue = function () {
290    if (typeof MyLearning === 'undefined' || typeof MyLearning.asyncFetchUserState !== 'function') {
291      return Promise.resolve(DashboardLite._readContCache());
292    }
293    return MyLearning.asyncFetchUserState().then(function (reqRes) {
294      var d = (reqRes && reqRes.data) ? reqRes.data : {};
295      var topics = (d.activity && d.activity.topics) || d.topics || {};
296      // TODO: swap for the real "selected language" source once provided. Until then,
297      // prefer the most-recent topic where the user is actually mid-lesson (not just a
298      // landing-page visit) so e.g. Python ("Output Variables") wins over an HTML "Home" hit.
299      var best = null, bestReal = null;
300      Object.keys(topics).forEach(function (id) {
301        var t = topics[id];
302        if (!t || !t.topicName) return;
303        var uts = t.lessonInteractionUts || 0;
304        if (!best || uts > (best.lessonInteractionUts || 0)) best = t;
305        if (!DashboardLite._isLanding(t) && (!bestReal || uts > (bestReal.lessonInteractionUts || 0))) bestReal = t;
306      });
307      var chosen = bestReal || best;
308      if (!chosen) return DashboardLite._readContCache();
309      var total = Number(chosen.lessonsTotal) || 0, score = Number(chosen.lessonsScore) || 0;
310      var pct = total > 0 ? Math.max(0, Math.min(100, Math.round(score / total * 100))) : 0;
311      var rel = chosen.lessonRelPath ? String(chosen.lessonRelPath).replace(/^\//, '') : null;
312      var result = {
313        title: chosen.topicName + ' Tutorial',
314        topic: chosen.topicName,
315        next: chosen.lessonName ? 'Next up: ' + chosen.lessonName : 'Continue learning',
316        progress: pct, url: rel ? '/' + rel : '#',
317        practice: DashboardLite._practiceUrl(chosen.topicName)
318      };
319      DashboardLite._writeContCache(result);
320      return result;
321    })['catch'](function () { return DashboardLite._readContCache(); });
322  };
323
324  // ── View ────────────────────────────────────────────────────────────────
325  DashboardLite._flameSvg =
326    '<svg width="40" height="45" viewBox="0 0 43 48" fill="none" xmlns="http://www.w3.org/2000/svg">' +
327    '<path fill-rule="evenodd" clip-rule="evenodd" d="M0 28.3147C0 39.1866 8.62988 48 20.648 48C32.6662 48 42.4089 39.1866 42.4089 28.3147C42.4089 16.4391 34.0874 7.3021 26.6681 0.526583C26.3281 0.216026 25.8182 0.179203 25.4495 0.455052C23.0134 2.27727 21.0501 4.77236 19.5597 7.94034C18.0644 11.1186 17.0726 14.8936 16.5843 19.2653C16.5189 19.8511 15.971 20.2697 15.4048 20.1058C13.7124 19.6161 12.2029 18.7388 10.8762 17.4737C9.5416 16.2011 8.22419 14.4474 6.92394 12.2125C6.55348 11.5758 5.64722 11.513 5.23907 12.1263C1.82501 17.2561 0 22.9643 0 28.3147Z" fill="#FF9600"></path>' +
328    '<path d="M33.894 28.9292C33.2264 33.1276 29.8718 36.4211 25.5957 37.0766" stroke="#FF5D39" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
329
330  DashboardLite._puzzleSvg =
331    '<svg width="26" height="26" viewBox="0 0 24 24" fill="#04AA6D"><path d="M20.5 11H19V7a2 2 0 0 0-2-2h-4V3.5a2.5 2.5 0 0 0-5 0V5H4a2 2 0 0 0-2 2v3.8h1.5a2.1 2.1 0 0 1 0 4.2H2V19a2 2 0 0 0 2 2h3.8v-1.5a2.1 2.1 0 0 1 4.2 0V21H17a2 2 0 0 0 2-2v-4h1.5a2.5 2.5 0 0 0 0-5z"/></svg>';
332
333  DashboardLite._esc = function (s) {
334    return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
335      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
336    });
337  };
338
339  // Shrink a single-line title to fit its tile: drop trailing words until it fits,
340  // down to the first word (full text kept as the tooltip). e.g.
341  // "Logic Legend League" -> "Logic Legend" -> "Logic".
342  DashboardLite._fitTitle = function (el) {
343    if (!el) return;
344    var full = el.getAttribute('data-full');
345    if (full === null) { full = el.textContent; el.setAttribute('data-full', full); }
346    el.title = full;
347    el.textContent = full;
348    if (!el.clientWidth) return; // hidden / not laid out yet
349    var words = full.split(/\s+/);
350    while (words.length > 1 && el.scrollWidth > el.clientWidth) {
351      words.pop();
352      el.textContent = words.join(' ');
353    }
354  };
355  DashboardLite._fitContainer = function (container) {
356    var els = container.querySelectorAll('.dlw-title');
357    for (var i = 0; i < els.length; i++) DashboardLite._fitTitle(els[i]);
358  };
359
360  DashboardLite._injectCss = function () {
361    if (document.getElementById(DashboardLite._styleId)) return;
362    var css = [
363      // Base
364      '.dlw{position:relative;box-sizing:border-box;font-family:"Source Sans Pro","Segoe UI",sans-serif;color:#282A35;overflow:hidden;max-width:100%}',
365      '.dlw *,.dlw *::before,.dlw *::after{box-sizing:border-box}',
366      '.dlw-stage{display:flex;gap:10px;width:100%;height:100%}',
367      '.dlw-tile{position:relative;background:#fff;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.15);border:1px solid rgba(0,0,0,.06);padding:14px;display:flex;align-items:center;gap:10px;text-decoration:none;color:inherit;overflow:hidden}',
368      '.dlw-tile-ico{flex-shrink:0;display:flex;align-items:center;justify-content:center}',
369      '.dlw-tile-body{min-width:0}',
370      '.dlw-title{font-size:15px;font-weight:700;color:#282A35;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
371      '.dlw-sub{font-size:12px;color:#888;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
372      '.dlw-sub b{color:#04AA6D}',
373      '.dlw-sub:empty{display:none}',
374      '.dlw-league-rank,.dlw-league-xp{white-space:normal}',
375      '.dlw-league-rank b{white-space:nowrap}',
376      '.dlw-avatar{border-radius:50%;object-fit:cover;background:#f1f1f1;width:48px;height:48px}',
377      '.dlw-av-wrap{position:relative;display:inline-block;line-height:0}',
378      '.dlw-mood{position:absolute;top:-3px;right:-3px;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);padding:2px}',
379      '.dlw-col-mode .dlw-mood{width:16px;height:16px;top:-2px;right:-2px}',
380      '.dlw-streak-num{font-size:26px;font-weight:700;color:#FF6B35;line-height:1}',
381      '.dlw-lang-ico{width:40px;height:40px;border-radius:8px;background:#f1f1f1}',
382      '.dlw-badge{width:48px;height:48px;object-fit:contain;flex-shrink:0}',
383      '.dlw-row-mode .dlw-badge{width:76px;height:76px}',
384      '.dlw-grid-mode .dlw-badge{width:40px;height:40px}',
385      '.dlw-col-mode .dlw-badge{width:32px;height:32px}',
386      '.dlw-ring{position:relative;width:40px;height:40px;flex-shrink:0}',
387      '.dlw-ring-pct{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:9px;font-weight:700;color:#282A35}',
388      '.dlw-pickup{position:absolute;top:6px;left:50%;transform:translateX(-50%);background:#e8f5e9;color:#04AA6D;font-size:9px;font-weight:700;padding:3px 10px;border-radius:4px;text-transform:uppercase;letter-spacing:.02em;white-space:nowrap}',
389      '.dlw-action{flex-shrink:0}',
390      '.dlw-btn{display:inline-block;background:#04AA6D;color:#fff;padding:6px 18px;border-radius:5px;font-family:"Source Sans Pro",sans-serif;font-size:17px;font-weight:400;line-height:1.4;text-decoration:none;border:none;cursor:pointer;white-space:nowrap}',
391      // color repeated on :hover so page-level a:hover rules cannot turn the label green-on-green
392      'a.dlw-tile:hover .dlw-btn,.dlw-btn:hover{background:#059862;color:#fff}',
393      '.dlw-blur{filter:blur(3px);opacity:.5}',
394
395      // ── stack mode (default, ~300px) - avatar+streak top row, league & continue full rows
396      '.dlw-stack-mode{width:300px}',
397      '.dlw-stack-mode .dlw-stage{flex-direction:row;flex-wrap:wrap;gap:12px}',
398      '.dlw-stack-mode .dlw-t-avatar,.dlw-stack-mode .dlw-t-streak{flex:1 1 calc(50% - 6px);min-width:0;flex-direction:column;justify-content:center;text-align:center;gap:6px;padding:16px}',
399      '.dlw-stack-mode .dlw-t-league,.dlw-stack-mode .dlw-t-continue,.dlw-stack-mode .dlw-t-practice{flex:1 1 100%}',
400      '.dlw-stack-mode .dlw-t-league .dlw-tile-body,.dlw-stack-mode .dlw-t-continue .dlw-tile-body,.dlw-stack-mode .dlw-t-practice .dlw-tile-body{flex:1}',
401      '.dlw-grid-mode .dlw-t-practice,.dlw-col-mode .dlw-t-practice{display:none!important}',
402      '.dlw-stack-mode .dlw-t-continue{padding-top:24px}',
403      '.dlw-stack-mode .dlw-avatar{width:64px;height:64px}',
404      '.dlw-stack-mode .dlw-tile-ico svg{width:40px;height:auto}',
405
406      // ── row mode (960x300, 720x250) - tiles across, elements scaled up
407      // grid: avatar (row 1) + streak (row 2) stacked in a narrow first column; the
408      // text-heavy tiles (league/continue/practice) span both rows in the wide columns.
409      '.dlw-row-mode .dlw-stage{display:grid;grid-auto-flow:column;grid-template-columns:152px;grid-auto-columns:1fr;grid-template-rows:1fr 1fr;gap:10px}',
410      '.dlw-row-mode .dlw-tile{flex-direction:column;justify-content:center;text-align:center;gap:8px;padding:14px}',
411      '.dlw-row-mode .dlw-tile-body{display:flex;flex-direction:column;gap:5px}',
412      '.dlw-row-mode .dlw-t-avatar{grid-column:1;grid-row:1}',
413      '.dlw-row-mode .dlw-t-streak{grid-column:1;grid-row:2}',
414      '.dlw-row-mode .dlw-t-league,.dlw-row-mode .dlw-t-continue,.dlw-row-mode .dlw-t-practice{grid-row:1 / span 2}',
415      // the stacked cells are half-height - keep avatar + streak compact
416      '.dlw-row-mode .dlw-t-avatar,.dlw-row-mode .dlw-t-streak{padding:8px;gap:2px}',
417      '.dlw-row-mode .dlw-t-avatar .dlw-avatar{width:48px;height:48px}',
418      '.dlw-row-mode .dlw-t-avatar .dlw-mood{width:18px;height:18px}',
419      '.dlw-row-mode .dlw-t-avatar .dlw-title{font-size:13px}',
420      '.dlw-row-mode .dlw-t-streak .dlw-tile-ico svg{width:34px}',
421      '.dlw-row-mode .dlw-t-streak .dlw-streak-num{font-size:28px}',
422      '.dlw-row-mode .dlw-t-avatar .dlw-sub,.dlw-row-mode .dlw-t-streak .dlw-sub{font-size:11px}',
423      // full-height tiles keep the larger sizing
424      '.dlw-row-mode .dlw-tile-ico svg{width:52px;height:auto}',
425      '.dlw-row-mode .dlw-lang-ico{width:56px;height:56px}',
426      '.dlw-row-mode .dlw-title{font-size:19px}',
427      '.dlw-row-mode .dlw-sub{font-size:14px}',
428      '.dlw-row-mode .dlw-ring{width:44px;height:44px}',
429      '.dlw-row-mode .dlw-ring svg{width:44px;height:44px}',
430      '.dlw-row-mode .dlw-ring-pct{font-size:11px}',
431
432      // ── grid mode (300x300) - 2x2 vertical tiles
433      '.dlw-grid-mode .dlw-stage{flex-direction:row;flex-wrap:wrap;align-content:stretch}',
434      '.dlw-grid-mode .dlw-tile{flex:1 1 calc(50% - 5px);min-width:calc(50% - 5px);flex-direction:column;justify-content:center;text-align:center;gap:4px;padding:10px}',
435      '.dlw-grid-mode .dlw-avatar{width:44px;height:44px}',
436      '.dlw-grid-mode .dlw-tile-ico svg{width:34px;height:auto}',
437      '.dlw-grid-mode .dlw-ring{display:none}',
438      '.dlw-grid-mode .dlw-pickup{display:none}',
439
440      // ── col mode (160x300, 140x300) - 4 compact horizontal rows
441      '.dlw-col-mode .dlw-stage{flex-direction:column}',
442      '.dlw-col-mode .dlw-tile{flex:1;flex-direction:row;align-items:center;gap:10px;padding:10px}',
443      '.dlw-col-mode .dlw-tile-body{flex:1}',
444      '.dlw-col-mode .dlw-avatar{width:34px;height:34px}',
445      '.dlw-col-mode .dlw-lang-ico{width:34px;height:34px}',
446      '.dlw-col-mode .dlw-tile-ico svg{width:26px;height:auto}',
447      '.dlw-col-mode .dlw-streak-num{font-size:20px}',
448      '.dlw-col-mode .dlw-title{font-size:13px}',
449      '.dlw-col-mode .dlw-sub{font-size:11px}',
450      '.dlw-col-mode .dlw-action,.dlw-col-mode .dlw-ring,.dlw-col-mode .dlw-pickup{display:none}',
451      '.dlw-col-mode a.dlw-tile::after{content:"\\203A";color:#04AA6D;font-size:22px;font-weight:700;line-height:1;flex-shrink:0}',
452
453      // ── Anon: aspiration carousel (padlocked) + persistent sign-up bar ──
454      '.dlw-anon2{display:flex;flex-direction:column;width:100%;height:100%;background:#fff;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.15);border:1px solid rgba(0,0,0,.06);overflow:hidden}',
455      '.dlw-caro{position:relative;flex:1;min-height:150px;overflow:hidden;background:linear-gradient(180deg,#30323f,#23242f)}',
456      '.dlw-caro-lock{position:absolute;top:8px;right:8px;z-index:2;width:26px;height:26px;border-radius:50%;background:rgba(255,255,255,.16);display:flex;align-items:center;justify-content:center}',
457      '.dlw-caro-lock svg{width:14px;height:14px;fill:#fff}',
458      // slow, gentle glide - a fast snap between slides reads as motion sickness
459      '.dlw-caro-track{display:flex;height:100%;transition:transform 1.2s cubic-bezier(.45,.05,.2,1)}',
460      '.dlw-slide{flex:0 0 100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:16px 14px 26px;text-align:center;color:#fff}',
461      // combined XP + streak slide (compact - shares the box with the pinned CTA on anon/frozen)
462      '.dlw-combo{gap:9px}',
463      '.dlw-combo-head{display:flex;align-items:center;justify-content:center;gap:9px}',
464      '.dlw-combo .dlw-vis-num{font-size:34px}',
465      '.dlw-combo-head .dlw-vis-bolt svg{width:26px;height:30px}',
466      '.dlw-combo .dlw-slide-cap b{font-size:16px}',
467      '.dlw-combo .dlw-slide-cap span{font-size:12px;margin-top:2px}',
468      '.dlw-combo-stats{display:flex;align-items:stretch;justify-content:center;gap:30px;border-top:1px solid rgba(255,255,255,.14);padding-top:10px;width:78%}',
469      '.dlw-combo-stat{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;font-weight:700;color:#fff;font-size:17px}',
470      '.dlw-combo-label{font-size:12px;color:#c9cdd6;font-weight:600}',
471      '.dlw-combo-flame svg{width:18px;height:21px}',
472      '.dlw-combo-div{width:1px;background:rgba(255,255,255,.15)}',
473      // button slides share the combo's compact typography
474      // course progress bar above the Continue learning link
475      '.dlw-progressbar{display:block;width:190px;height:8px;border-radius:4px;background:rgba(255,255,255,.16);overflow:hidden}',
476      '.dlw-progressbar-fill{display:block;height:100%;background:#04AA6D;border-radius:4px}',
477      '.dlw-vis-link{text-decoration:none;color:inherit;display:block}',
478      '.dlw-cap-link{color:#fff;text-decoration:none}',
479      '.dlw-cap-link:hover{color:#fff;text-decoration:underline}',
480      '.dlw-vis-link:hover~.dlw-slide-cap .dlw-cap-link{text-decoration:underline}',
481      '.dlw-slide-vis{display:flex;align-items:center;justify-content:center;gap:10px;min-height:78px}',
482      '.dlw-slide-cap b{display:block;font-size:19px;font-weight:700;color:#fff}',
483      '.dlw-slide-cap span{display:block;font-size:13px;color:#c9cdd6;margin-top:4px}',
484      '.dlw-vis-num{font-size:46px;font-weight:700;color:#FF9600}',
485      '.dlw-vis-green{color:#04AA6D}',
486      '.dlw-vis-flame svg{width:64px;height:auto}',
487      '.dlw-vis-league img{width:58px;height:58px}',
488      '.dlw-vis-league .dlw-vis-hi{width:80px;height:80px;filter:drop-shadow(0 0 10px rgba(150,120,255,.7))}',
489      '.dlw-vis-arrow{color:#6b6f7d;font-size:24px;font-weight:700}',
490      '.dlw-vis-bolt svg{width:52px;height:58px}',
491      '.dlw-vis-noads-ico svg{width:66px;height:66px}',
492      '.dlw-vis-av{width:92px;height:92px;border-radius:50%;background:#fff}',
493      '.dlw-vis-chip{width:52px;height:52px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;margin-left:-16px;box-shadow:0 2px 6px rgba(0,0,0,.35)}',
494      '.dlw-vis-chip img{width:38px;height:38px;object-fit:contain}',
495      // Spaces preview - bare image with alpha-aware shadow, like the practice mockup
496      '.dlw-vis-spaces-img{width:250px;height:auto;filter:drop-shadow(0 10px 22px rgba(0,0,0,.5))}',
497      '.dlw-col-mode .dlw-vis-spaces-img{width:124px}',
498      // anon/frozen reels share the box with the pinned CTA - cap screenshot height
499      // so the caption stays visible above the button
500      '.dlh-anon .dlw-vis-practice-img,.dlh-frozen .dlw-vis-practice-img,' +
501      '.dlh-anon .dlw-vis-spaces-img,.dlh-frozen .dlw-vis-spaces-img{width:auto;max-width:240px;max-height:140px}',
502      // quiet per-slide action link
503      // bare image (the mockup has its own device frame) - alpha-aware shadow, no box chrome
504      '.dlw-vis-practice-img{width:250px;height:auto;filter:drop-shadow(0 10px 22px rgba(0,0,0,.5))}',
505      '.dlw-col-mode .dlw-vis-practice-img{width:124px}',
506      '.dlw-caro-dots{position:absolute;bottom:8px;left:0;right:0;display:flex;gap:6px;justify-content:center;z-index:2}',
507      '.dlw-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.35);border:none;cursor:pointer;padding:0;transition:width .2s,background .2s}',
508      '.dlw-dot.active{background:#04AA6D;width:18px;border-radius:4px}',
509      '.dlw-anon-bar{display:flex;flex-direction:column;align-items:center;gap:8px;padding:14px 18px}',
510      '.dlw-anon-signup{align-self:stretch;text-align:center;box-shadow:0 4px 14px rgba(4,170,109,.4);transition:transform .12s,background .15s}',
511      '.dlw-anon-signup:hover{transform:translateY(-1px)}',
512      '.dlw-anon-or{font-size:11px;color:#aaa}',
513      '.dlw-anon-social-row{display:flex;gap:10px;justify-content:center}',
514      '.dlw-social{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:50%;border:1px solid #ddd;background:#fff;cursor:pointer;transition:transform .15s,box-shadow .15s}',
515      '.dlw-social:hover{transform:scale(1.12);box-shadow:0 4px 12px rgba(0,0,0,.18)}',
516      '.dlw-anon-signin{font-size:12px;color:#888}',
517      '.dlw-anon-signin a{color:#04AA6D;font-weight:700;text-decoration:none}',
518      '.dlw-anon-signin a:hover{text-decoration:underline}',
519      '.dlw-cta-s{display:none}',
520      // row (960/720): carousel left, sign-up bar pinned right
521      '.dlw-row-mode .dlw-anon2{flex-direction:row}',
522      '.dlw-row-mode .dlw-anon-bar{flex:0 0 320px;justify-content:center;padding:20px 26px}',
523      '.dlw-row-mode .dlw-slide-cap b{font-size:18px}',
524      // grid (300): compact bar
525      '.dlw-grid-mode .dlw-anon-bar{padding:12px;gap:6px}',
526      '.dlw-grid-mode .dlw-anon-or{display:none}',
527      // col (160/140): tight bar, short button, small socials, no divider/caption-sub
528      '.dlw-col-mode .dlw-anon-bar{padding:10px;gap:7px}',
529      '.dlw-col-mode .dlw-anon-or{display:none}',
530      '.dlw-col-mode .dlw-cta-l{display:none}',
531      '.dlw-col-mode .dlw-cta-s{display:inline}',
532      '.dlw-col-mode .dlw-social{width:30px;height:30px}',
533      '.dlw-col-mode .dlw-slide-cap b{font-size:13px}',
534      '.dlw-col-mode .dlw-slide-cap span{display:none}',
535      '.dlw-col-mode .dlw-vis-num{font-size:26px}',
536
537      // ── Hero (full-width default.asp banner) ──
538      '.dlh,.dlh *,.dlh *::before,.dlh *::after{box-sizing:border-box}',
539      // Reserve a stable height up-front so the page does not jump when the hero
540      // resolves (empty -> content) or swaps anon/logged-in layouts.
541      '.dlh-host{display:flex;align-items:center;min-height:400px}',
542      '.dlh{display:flex;align-items:center;gap:48px;width:100%;max-width:1100px;margin:0 auto;padding:10px 20px;text-align:left;font-family:"Source Sans Pro","Segoe UI",sans-serif}',
543      '.dlh-left{flex:2;min-width:0}',
544      '.dlh-right{flex:1.2;min-width:0;max-width:520px}',
545      // one fixed size for the showcase in BOTH states so anon and logged-in heroes match
546      '.dlh-right .dlw-caro{height:340px;border-radius:16px;box-shadow:0 16px 48px rgba(0,0,0,.4)}',
547      // !important margin beats default.asp global `h1{margin:2px 0 -20px 0 !important}` which otherwise overlaps the paragraph.
548      '.dlh-title{font-size:62px!important;font-weight:700;color:#fff;margin:0 0 16px!important;line-height:1.05;font-family:"Source Sans Pro",sans-serif}',
549      '.dlh-free{font-size:21px;color:#fff;margin:0 0 20px;line-height:1.4}',
550      '.dlh-free b{color:#FFF4A3}',
551      '.dlh-support{color:#c9cdd6;font-size:16px;line-height:1.55;margin:0 0 8px;max-width:520px}',
552      '.dlh-perks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 32px;list-style:none;margin:0 0 14px;padding:0;max-width:440px}',
553      '.dlh-perks li{position:relative;padding-left:24px;color:#fff;font-size:15px;line-height:1.5;white-space:nowrap}',
554      '.dlh-perks li::before{content:"\\2713";position:absolute;left:0;color:#04AA6D;font-weight:700}',
555      // "Sign in" CTA pinned to the bottom inside the carousel (anon hero)
556      '.dlw-caro-cta{position:absolute;left:14px;right:14px;bottom:24px;z-index:2;display:block;text-align:center;background:#04AA6D;color:#fff;font-family:"Source Sans Pro",sans-serif;font-size:16px;font-weight:600;padding:10px;border-radius:8px;text-decoration:none;box-shadow:0 4px 14px rgba(0,0,0,.35);transition:background .15s}',
557      '.dlw-caro-cta:hover{background:#059862;color:#fff}',
558      // slides nudge content up just enough to clear the pinned CTA (anon Sign in /
559      // frozen Reactivate), keeping the carousel box identical across states
560      '.dlh-anon .dlw-slide,.dlh-frozen .dlw-slide{padding-bottom:58px}',
561      '.dlh-sub{font-size:14px;color:#aab;margin-top:14px}',
562      '.dlh-dash{display:inline-flex;align-items:center;gap:7px;color:#aab;font-size:14px;font-weight:600;text-decoration:none}',
563      '.dlh-dash:hover{color:#04AA6D}',
564      '.dlh-dash svg{flex-shrink:0}',
565      '.dlh-sub a{color:#04AA6D;font-weight:700;text-decoration:none}',
566      '.dlh-sub a:hover{text-decoration:underline}',
567      // badges variant: stats card on top + three badge cards below
568      '.dlh-badges{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}',
569      '.dlh-stats-card{grid-column:1/-1;display:flex;align-items:stretch;justify-content:space-between;gap:8px;padding:20px 16px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);border-radius:12px}',
570      '.dlh-stat{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:4px;text-decoration:none;min-width:0}',
571      '.dlh-stat-top{height:38px;display:flex;align-items:center;justify-content:center}',
572      '.dlh-stat b{font-size:20px;color:#fff;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}',
573      '.dlh-stat b i{font-style:normal}',
574      '.dlh-stat-unit{font-size:13px;color:#c9cdd6;font-weight:600}',
575      '.dlh-stat-label{font-size:13px;color:#c9cdd6;font-weight:600;white-space:nowrap}',
576      '.dlh-stat:hover .dlh-stat-label{color:#fff}',
577      '.dlh-stat-ico svg{width:23px;height:26px}',
578      '.dlh-stat-bolt svg{width:29px;height:32px}',
579      '.dlh-stat img{width:36px;height:36px;object-fit:contain}',
580      '.dlh-stat-locked{filter:grayscale(1);opacity:.45}',
581      '.dlh-stat-div{width:1px;background:rgba(255,255,255,.15);flex:0 0 1px}',
582      '.dlh-badge{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;padding:14px 10px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);border-radius:12px;text-decoration:none;text-align:center;transition:background .15s,transform .12s}',
583      '.dlh-badge:hover{background:rgba(255,255,255,.13);transform:translateY(-2px)}',
584      '.dlh-badge-ico{height:42px;display:flex;align-items:center;justify-content:center}',
585      '.dlh-badge-ico svg{width:34px;height:38px}',
586      '.dlh-badge-ico img{width:42px;height:42px;object-fit:contain}',
587      '.dlh-badge b{font-size:17px;color:#fff;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}',
588      '.dlh-badge b i{font-style:normal}',
589      '.dlh-badge-green{color:#04AA6D}',
590      '.dlh-badge>span:last-child{font-size:12px;color:#c9cdd6;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}',
591      // fallback when there is no continue data: "Or continue reading for free [All Tutorials]"
592      '.dlh-startrow{display:flex;align-items:center;gap:14px;margin-top:4px}',
593      '.dlh-or{color:#c9cdd6;font-size:16px}',
594      '.dlh-or-link{color:#04AA6D;font-size:16px;font-weight:700;text-decoration:none}',
595      '.dlh-or-link:hover{color:#04AA6D;text-decoration:underline}',
596      '.dlh-hname{white-space:nowrap}',
597      '.dlh-hav-wrap{position:relative;display:inline-block;vertical-align:middle;margin-left:16px;margin-top:-8px;line-height:0}',
598      '.dlh-havatar{width:56px;height:56px;border-radius:50%;background:#fff;object-fit:contain}',
599      '.dlh-face{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;pointer-events:none}',
600      // continue row: floating "pick up where you left off" tag + green-tinted border
601      '.dlh-cont2{position:relative;display:flex;width:100%;max-width:400px;box-sizing:border-box;align-items:center;gap:16px;margin-top:22px;padding:16px 18px 14px;border-radius:12px;background:rgba(255,255,255,.07);border:1px solid rgba(4,170,109,.55);text-decoration:none;transition:background .15s}',
602      '.dlh-cont2:hover{background:rgba(255,255,255,.12)}',
603      '.dlh-cont2-tag{position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:#D9EEE1;color:#04AA6D;font-size:9px;font-weight:700;padding:3px 12px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}',
604      '.dlh-cont2-ico{width:46px;height:46px;border-radius:9px;background:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0}',
605      '.dlh-cont2-txt{font-size:18px;font-weight:700;color:#fff;white-space:nowrap}',
606      '.dlh-cont2-mid{flex:1;display:flex;flex-direction:column;gap:4px;min-width:0}',
607      '.dlh-cont2 .dlw-progressbar{width:100%;max-width:170px}',
608      '@media(max-width:900px){.dlh-host{min-height:0;padding:24px 0}.dlh{flex-direction:column;text-align:center;gap:28px}.dlh-left{order:1}.dlh-right{order:2;flex:0 0 auto;width:100%;max-width:380px}.dlh-support{margin-left:auto;margin-right:auto}.dlh-perks{margin-left:auto;margin-right:auto;width:fit-content;text-align:left}.dlh-startrow{justify-content:center}.dlh-cont2{margin-left:auto;margin-right:auto}.dlh-title{font-size:48px!important}}',
609      // small screens: tighter type; the showcase (carousel / badges) stays visible
610      '@media(max-width:600px){.dlh-title{font-size:40px!important}.dlh-free{font-size:18px}}'
611    ].join('\n');
612    var el = document.createElement('style');
613    el.id = DashboardLite._styleId;
614    el.textContent = css;
615    document.head.appendChild(el);
616
617    // Re-fit titles on resize (e.g. the responsive homepage hero).
618    if (!DashboardLite._fitBound) {
619      DashboardLite._fitBound = true;
620      var t;
621      window.addEventListener('resize', function () {
622        clearTimeout(t);
623        t = setTimeout(function () {
624          var els = document.querySelectorAll('.dlw-title[data-full]');
625          for (var i = 0; i < els.length; i++) DashboardLite._fitTitle(els[i]);
626        }, 150);
627      });
628    }
629  };
630
631  DashboardLite._ringHtml = function (pct) {
632    var r = 16, circ = 2 * Math.PI * r;
633    return '<div class="dlw-ring"><svg width="40" height="40" viewBox="0 0 40 40" style="transform:rotate(-90deg)">' +
634      '<circle cx="20" cy="20" r="16" fill="none" stroke="#e8e8e8" stroke-width="4"></circle>' +
635      '<circle cx="20" cy="20" r="16" fill="none" stroke="#04AA6D" stroke-width="4" stroke-linecap="round"' +
636        ' stroke-dasharray="' + circ + '" stroke-dashoffset="' + (circ * (1 - pct / 100)) + '"></circle>' +
637      '</svg><div class="dlw-ring-pct">' + pct + '%</div></div>';
638  };
639
640  // ── Individual tile builders (composed by the widget and the hero) ──
641  DashboardLite._tileAvatar = function (xp) {
642    var fb = DashboardLite._fallbackAvatar;
643    return '<div class="dlw-tile dlw-t-avatar">' +
644      '<div class="dlw-tile-ico"><span class="dlw-av-wrap">' +
645        '<img class="dlw-avatar" src="' + fb + '" alt="Avatar" onerror="this.onerror=null;this.src=\'' + fb + '\'">' +
646        '<img class="dlw-mood" alt="" style="display:none">' +
647      '</span></div>' +
648      '<div class="dlw-tile-body">' +
649        '<div class="dlw-title dlw-name">&nbsp;</div>' +
650        '<div class="dlw-sub">XP <b class="dlw-xp">' + xp + '</b></div>' +
651      '</div>' +
652    '</div>';
653  };
654  DashboardLite._tileStreak = function (streak) {
655    return '<div class="dlw-tile dlw-t-streak">' +
656      '<div class="dlw-tile-ico">' + DashboardLite._flameSvg + '</div>' +
657      '<div class="dlw-tile-body">' +
658        '<div class="dlw-streak-num dlw-streak">' + streak + '</div>' +
659        '<div class="dlw-sub">day streak</div>' +
660        '<div class="dlw-sub dlw-freeze"></div>' +
661      '</div>' +
662    '</div>';
663  };
664  DashboardLite._tileLeague = function () {
665    var woodBadge = DashboardLite._leagueImgBase + '/wood.svg';
666    return '<a class="dlw-tile dlw-t-league ga-fp ga-widget-league" href="' + DashboardLite._leaderboardUrl + '">' +
667      '<div class="dlw-tile-ico"><img class="dlw-badge" src="' + woodBadge + '" alt="League" ' +
668        'onerror="this.onerror=null;this.src=\'' + woodBadge + '\'"></div>' +
669      '<div class="dlw-tile-body">' +
670        '<div class="dlw-title dlw-tier">Wood</div>' +
671        '<div class="dlw-sub dlw-league-rank"></div>' +
672        '<div class="dlw-sub dlw-league-xp"><b class="dlw-weekly">0</b> XP</div>' +
673      '</div>' +
674      '<span class="dlw-action"><span class="dlw-btn">View</span></span>' +
675    '</a>';
676  };
677  // Continue is filled once progress resolves; hidden until then.
678  DashboardLite._tileContinue = function () {
679    return '<a class="dlw-tile dlw-t-continue ga-fp ga-widget-continue" href="#" style="display:none"></a>';
680  };
681  // Weekly practice - shown only if the selected language has a practice page.
682  DashboardLite._tilePractice = function () {
683    return '<a class="dlw-tile dlw-t-practice ga-fp ga-widget-practice" href="#" style="display:none">
683' +
684      '<div class="dlw-tile-ico dlw-lang-ico">' + DashboardLite._puzzleSvg + '</div>' +
685      '<div class="dlw-tile-body"><div class="dlw-title dlw-practice-title">Weekly practice</div>' +
686        '<div class="dlw-sub">Solve &amp; earn bonus XP</div></div>' +
687      '<span class="dlw-action"><span class="dlw-btn">Solve</span></span>' +
688    '</a>';
689  };
690
691  DashboardLite._loggedInHtml = function (seed) {
692    seed = seed || {};
693    var xp = typeof seed._initialXp === 'number' ? seed._initialXp : 0;
694    var streak = typeof seed._initialStreak === 'number' ? seed._initialStreak : 0;
695    return '<div class="dlw-stage">' +
696      DashboardLite._tileAvatar(xp) + DashboardLite._tileStreak(streak) +
697      DashboardLite._tileLeague() + DashboardLite._tileContinue() + DashboardLite._tilePractice() +
698    '</div>';
699  };
700
701  DashboardLite._continueInner = function (c) {
702    return '<div class="dlw-pickup">Pick up where you left off</div>' +
703      '<div class="dlw-tile-ico dlw-lang-ico">' + DashboardLite._topicIconHtml(c.title) + '</div>' +
704      '<div class="dlw-tile-body">' +
705        '<div class="dlw-title">' + DashboardLite._esc(c.title) + '</div>' +
706        '<div class="dlw-sub">' + DashboardLite._esc(c.next) + '</div>' +
707      '</div>' +
708      DashboardLite._ringHtml(typeof c.progress === 'number' ? c.progress : 0) +
709      '<span class="dlw-action"><span class="dlw-btn">Continue</span></span>';
710  };
711
712  DashboardLite._lockSvg =
713    '<svg viewBox="0 0 24 24" fill="#04AA6D"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm3 8H9V6a3 3 0 0 1 6 0v3z"/></svg>';
714  // Analytics classes for tracked buttons/links: "ga-fp ga-frontpage-<name>".
715  DashboardLite._gaCls = function (name) { return 'ga-fp ga-frontpage-' + name; };
716
717  // href links only the main visual and the title - not the whole slide box.
718  DashboardLite._slide = function (visCls, vis, title, sub, href, ga) {
719    var visHtml = '<div class="dlw-slide-vis ' + visCls + '">' + vis + '</div>';
720    var titleHtml = title;
721    if (href) {
722      var h = DashboardLite._esc(href);
723      var cls = ga ? ' ' + ga : '';
724      visHtml = '<a class="dlw-vis-link' + cls + '" href="' + h + '">' + visHtml + '</a>';
725      titleHtml = '<a class="dlw-cap-link' + cls + '" href="' + h + '">' + title + '</a>';
726    }
727    return '<div class="dlw-slide">' + visHtml +
728      '<div class="dlw-slide-cap"><b>' + titleHtml + '</b><span>' + sub + '</span></div></div>';
729  };
730  DashboardLite._boltSvg = '<svg viewBox="0 0 24 24" fill="#FFD23F"><path d="M13 2L4.5 13.5H11l-1 8.5L19.5 10H12.5z"/></svg>';
731  DashboardLite._noAdsSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="#7ee0b8" stroke-width="2.2"><circle cx="12" cy="12" r="9"/><line x1="5.6" y1="5.6" x2="18.4" y2="18.4"/></svg>';
732
733  // Combined XP + streak slide: big headline, caption, divided stats row, optional button.
734  DashboardLite._comboSlide = function (head, leftLabel, leftVal, streak, btnHtml) {
735    return '<div class="dlw-slide dlw-combo">' +
736      '<div class="dlw-combo-head"><span class="dlw-vis-bolt">' + DashboardLite._boltSvg + '</span>' +
737        '<span class="dlw-vis-num dlw-vis-green">' + head + '</span></div>' +
738      '<div class="dlw-slide-cap"><b>Keep learning. Keep leveling up.</b>' +
739        '<span>Track your progress and achieve more.</span></div>' +
740      '<div class="dlw-combo-stats">' +
741        '<div class="dlw-combo-stat"><span class="dlw-combo-label">' + leftLabel + '</span><span>' + leftVal + '</span></div>' +
742        '<div class="dlw-combo-div"></div>' +
743        '<div class="dlw-combo-stat"><span class="dlw-combo-flame">' + DashboardLite._flameSvg + '</span>' +
744          '<span>' + streak + '</span><span class="dlw-combo-label">Day streak</span></div>' +
745      '</div>' +
746      (btnHtml || '') +
747    '</div>';
748  };
749  // Spaces preview screenshot (own frame in the image, shown bare like the practice mockup).
750  DashboardLite._editorMockHtml = function () {
751    return '<picture><source srcset="/images/img_spaces_preview.webp" type="image/webp">' +
752      '<img class="dlw-vis-spaces-img" src="/images/img_spaces_preview.png" alt="" loading="lazy"></picture>';
753  };
754
755  // Aspiration slides (anon) - sell the dream, padlocked.
756  DashboardLite._carouselSlides = function () {
757    var L = DashboardLite._leagueImgBase, S = DashboardLite._slide;
758    var arrow = '<span class="dlw-vis-arrow">›</span>';
759    // Same order as the logged-in reel (XP+streak combo, Spaces, practice, league),
760    // then the anon-only extras (ad-free, avatar gear). The pinned carousel CTA
761    // (Sign in / Reactivate) doubles as the combo slide's button on these reels.
762    // dummy XP numbers (no level system) - same structure as the logged-in combo
763    return DashboardLite._comboSlide('12,560', 'XP this week', '850', '7', '') +
764      S('dlw-vis-build', DashboardLite._editorMockHtml(), 'Build &amp; host sites', 'Your own Spaces, live on the web') +
765      S('dlw-vis-practice', '<picture><source srcset="/images/img_mockup.webp" type="image/webp"><img class="dlw-vis-practice-img" src="/images/img_mockup.png" alt="" loading="lazy"></picture>', 'Weekly practice', 'Fresh problems &amp; bonus XP') +
766      S('dlw-vis-league', '<img src="' + L + '/wood.svg" alt="">' + arrow + '<img src="' + L + '/gold.svg" alt="">' + arrow +
767        '<img class="dlw-vis-hi" src="' + L + '/diamond.svg" alt="">', 'Climb the leagues', 'From Wood to Master Coder',
768        DashboardLite._leagueInfoUrl, DashboardLite._gaCls('league-info')) +
769      S('dlw-vis-noads', '<span class="dlw-vis-noads-ico">' + DashboardLite._noAdsSvg + '</span>', 'Ad-free learning', 'Focus with zero distractions') +
770      S('dlw-vis-avatar', '<img class="dlw-vis-av" src="' + DashboardLite._fallbackAvatar + '" alt="">' +
771        '<span class="dlw-vis-chip">
771<img src="/lib/account/img/avatar_shirt.png" alt="" loading="lazy"></span>' +
772        '<span class="dlw-vis-chip"><img src="/lib/account/img/avatar_shorts.png" alt="" loading="lazy"></span>',
773        'Customize your avatar', 'Unlock gear, moods &amp; rare skins');
774  };
775  DashboardLite._caroSlideCount = 6;
776
777  DashboardLite._spacesUrl = 'https://spaces.w3schools.com';
778  DashboardLite._dashboardUrl = 'https://pathfinder.w3schools.com';
779
780  // Per-topic activity pages (topicName lowercase). Fallback chain for the activity
781  // badge: weekly practice -> challenges -> exercises -> quiz -> generic challenges.
782  DashboardLite._activityMaps = {
783    challenges: {
784      html: '/html/html_challenges.asp', css: '/css/css_challenges.asp',
785      python: '/python/python_challenges.asp', java: '/java/java_challenges.asp',
786      'c++': '/cpp/cpp_challenges.asp'
787    },
788    exercises: {
789      html: '/html/html_exercises.asp', css: '/css/css_exercises.asp',
790      javascript: '/js/js_exercises.asp', python: '/python/python_exercises.asp',
791      sql: '/sql/sql_exercises.asp', php: '/php/php_exercises.asp',
792      java: '/java/java_exercises.asp', c: '/c/c_exercises.php',
793      'c++': '/cpp/cpp_exercises.asp', 'c#': '/cs/cs_exercises.php',
794      jquery: '/jquery/jquery_exercises.asp', mysql: '/mysql/mysql_exercises.asp',
795      'node.js': '/nodejs/nodejs_exercises.asp', nodejs: '/nodejs/nodejs_exercises.asp',
796      r: '/r/r_exercises.asp', react: '/react/react_exercises.asp',
797      angular: '/angular/angular_exercises.asp', bootstrap: '/bootstrap5/bootstrap_exercises.php',
798      git: '/git/git_exercises.asp', kotlin: '/kotlin/kotlin_exercises.php',
799      go: '/go/go_exercises.php', swift: '/swift/swift_exercises.asp',
800      typescript: '/typescript/typescript_exercises.php', vue: '/vue/vue_exercises.php',
801      dsa: '/dsa/dsa_exercises.php', django: '/django/django_exercises.php',
802      mongodb: '/mongodb/mongodb_exercises.php', postgresql: '/postgresql/postgresql_exercises.php',
803      excel: '/excel/excel_exercises.php', aws: '/aws/aws_exercises.php',
804      bash: '/bash/bash_exercises.php', 'cyber security': '/cybersecurity/cybersecurity_exercises.php'
805    },
806    quiz: {
807      html: '/html/html_quiz.asp', css: '/css/css_quiz.asp', javascript: '/js/js_quiz.asp',
808      python: '/python/python_quiz.asp', sql: '/sql/sql_quiz.asp', php: '/php/php_quiz.asp',
809      java: '/java/java_quiz.asp', c: '/c/c_quiz.php', 'c++': '/cpp/cpp_quiz.asp',
810      'c#': '/cs/cs_quiz.php', jquery: '/jquery/jquery_quiz.asp', mysql: '/mysql/mysql_quiz.asp',
811      'node.js': '/nodejs/nodejs_quiz.asp', nodejs: '/nodejs/nodejs_quiz.asp',
812      r: '/r/r_quiz.asp', react: '/react/react_quiz.asp', angular: '/angular/angular_quiz.asp',
813      bootstrap: '/bootstrap5/bootstrap_quiz.php', git: '/git/git_quiz.asp',
814      kotlin: '/kotlin/kotlin_quiz.php', swift: '/swift/swift_quiz.asp',
815      typescript: '/typescript/typescript_quiz.php', vue: '/vue/vue_quiz.php',
816      dsa: '/dsa/dsa_quiz.php', django: '/django/django_quiz.php',
817      postgresql: '/postgresql/postgresql_quiz.php', bash: '/bash/bash_quiz.php',
818      aws: '/aws/aws_quiz.php', 'cyber security': '/cybersecurity/cybersecurity_quiz.php',
819      accessibility: '/accessibility/accessibility_quiz.php', xml: '/xml/xml_quiz.asp'
820    }
821  };
822  DashboardLite._actIcons = {
823    target: '<svg viewBox="0 0 24 24" fill="none" stroke="#04AA6D" stroke-width="2"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1" fill="#04AA6D"/></svg>',
824    pencil: '<svg viewBox="0 0 24 24" fill="#04AA6D"><path d="M3 17.25V21h3.75L17.8 9.94l-3.75-3.75L3 17.25zM20.7 7.04a1 1 0 0 0 0-1.41l-2.33-2.33a1 1 0 0 0-1.41 0l-1.83 1.82 3.75 3.75 1.82-1.83z"/></svg>',
825    question: '<svg viewBox="0 0 24 24" fill="none" stroke="#04AA6D" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.8 2.1c-.8.5-1.4 1-1.4 2"/><circle cx="12" cy="16.8" r=".5" fill="#04AA6D"/></svg>'
826  };
827  DashboardLite._activityFor = function (topic) {
828    var k = String(topic || '').toLowerCase();
829    var p = DashboardLite._practiceUrl(topic);
830    if (p) return { label: 'Practice', sub: 'Weekly problem', url: p, ico: DashboardLite._puzzleSvg };
831    var A = DashboardLite._activityMaps, I = DashboardLite._actIcons;
832    if (A.challenges[k]) return { label: 'Challenges', sub: 'Test your skills', url: A.challenges[k], ico: I.target };
833    if (A.exercises[k]) return { label: 'Exercises', sub: 'Train as you learn', url: A.exercises[k], ico: I.pencil };
834    if (A.quiz[k]) return { label: 'Quiz', sub: 'Check your knowledge', url: A.quiz[k], ico: I.question };
835    return { label: 'Challenges', sub: 'Test your skills', url: '/challenges/index.php', ico: I.target };
836  };
837
838  // Alternate logged-in showcase: stats card (total XP, weekly XP, streak, league)
839  // + two badges (Spaces and the topic's activity).
840  DashboardLite._badgesHtml = function (d, c) {
841    var L = DashboardLite._leagueImgBase;
842    d = d || {};
843    var streak = d.streaks && typeof d.streaks.streaks === 'number' ? d.streaks.streaks : 0;
844    // Total XP from competency (canonical); the league endpoint reports 0 for
845    // users who have not opted into the league.
846    var xp = typeof d.xpTotal === 'number' ? d.xpTotal
847      : (d.league && typeof d.league.xp === 'number' ? d.league.xp : 0);
848    var weekly = d.league && typeof d.league.weeklyXp === 'number' ? d.league.weeklyXp : 0;
849    var optedIn = !!(d.league && d.league.optedIn);
850    var tier = d.league && d.league.tier;
851    var rank = d.cohort && d.cohort.userRank, total = d.cohort && d.cohort.totalUsers;
852    var monitorSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="#7ee0b8" stroke-width="2" stroke-linecap="round"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg>';
853    var act = DashboardLite._activityFor(c && c.topic);
854
855    // Every stat gets the same top icon zone (empty for the XP columns) so the
856    // value and label rows line up across all four columns.
857    var stat = function (href, top, val, label, ga) {
858      return '<a class="dlh-stat ' + ga + '" href="' + DashboardLite._esc(href) + '">' +
859        '<span class="dlh-stat-top">' + (top || '') + '</span>' +
860        '<b>' + val + '</b><span class="dlh-stat-label">' + label + '</span></a>';
861    };
862    var b = function (href, ico, val, label, ga) {
863      return '<a class="dlh-badge ' + ga + '" href="' + DashboardLite._esc(href) + '">' +
864        '<span class="dlh-badge-ico">' + ico + '</span><b>' + val + '</b><span>' + label + '</span></a>';
865    };
866
867    // Three iconed columns; weekly XP lives with the league (it is the league score).
868    var tierFirst = DashboardLite._esc(DashboardLite._tierName(tier).split(' ')[0]);
869    return '<div class="dlh-badges">' +
870      '<div class="dlh-stats-card">' +
871        stat(DashboardLite._dashboardUrl, '<span class="dlh-stat-ico dlh-stat-bolt">
871' + DashboardLite._boltSvg + '</span>',
872          '<i class="dlh-badge-green">' + xp.toLocaleString() + '</i>', 'Total XP', DashboardLite._gaCls('total-xp')) +
873        '<span class="dlh-stat-div"></span>' +
874        stat(DashboardLite._dashboardUrl, '<span class="dlh-stat-ico">' + DashboardLite._flameSvg + '</span>',
875          String(streak), 'Day streak', DashboardLite._gaCls('streak')) +
876        '<span class="dlh-stat-div"></span>' +
877        (optedIn
878          ? stat(DashboardLite._leaderboardUrl,
879              '<img src="' + DashboardLite._tierIcon(tier) + '" alt="" onerror="this.onerror=null;this.src=\'' + L + '/wood.svg\'">',
880              weekly.toLocaleString() + ' <i class="dlh-stat-unit">XP</i>',
881              tierFirst + (rank ? ' &middot; #' + rank + (total ? ' of ' + total : '') : ' League'),
882              DashboardLite._gaCls('league'))
883          // Not opted into the league: the cell becomes the opt-in CTA.
884          : stat(DashboardLite._profileBase + '/league',
885              '<img class="dlh-stat-locked" src="' + L + '/wood.svg" alt="">',
886              'Join', 'Weekly league', DashboardLite._gaCls('join-league'))) +
887      '</div>' +
888      b(DashboardLite._spacesUrl, monitorSvg, 'Spaces', 'Build &amp; host', DashboardLite._gaCls('spaces')) +
889      b(act.url, act.ico, act.label, act.sub, DashboardLite._gaCls(act.label.toLowerCase())) +
890      b('https://pathfinder.w3schools.com/exams',
891        '<svg viewBox="0 0 24 24" fill="none" stroke="#7ee0b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="5"/><path d="M8.7 13.4 7 22l5-3 5 3-1.7-8.6"/></svg>',
892        'Certificates', 'Get certified', DashboardLite._gaCls('certificates')) +
893    '</div>';
894  };
895
896  DashboardLite._anonHtml = function () {
897    var ret = window.location.href;
898    var retEnc = encodeURIComponent(ret);
899    var signupHref = DashboardLite._profileBase + '/signup?redirect_url=' + retEnc;
900    var signinHref = DashboardLite._profileBase + '/log-in?redirect_url=' + retEnc;
901    var signupClick = "event.preventDefault();event.stopPropagation();" +
902      "if(typeof TopNavBar!=='undefined'&&typeof TopNavBar.showSignupDropdown==='function'){TopNavBar.showSignupDropdown();}" +
903      "else{window.open(this.href,'_blank');}";
904    var signinClick = "event.preventDefault();event.stopPropagation();" +
905      "if(typeof TopNavBar!=='undefined'&&typeof TopNavBar.showLoginDropdown==='function'){TopNavBar.showLoginDropdown();}" +
906      "else{window.open(this.href,'_blank');}";
907
908    var dots = '';
909    for (var k = 0; k < DashboardLite._caroSlideCount; k++) {
910      dots += '<button class="dlw-dot ga-fp ga-frontpage-carousel-dot' + (k === 0 ? ' active' : '') + '" aria-label="Slide ' + (k + 1) + '"></button>';
911    }
912
913    return '<div class="dlw-anon2">' +
914      '<div class="dlw-caro">' +
915        '<div class="dlw-caro-lock" aria-hidden="true">' + DashboardLite._lockSvg + '</div>' +
916        '<div class="dlw-caro-track">' + DashboardLite._carouselSlides() + '</div>' +
917        '<div class="dlw-caro-dots">' + dots + '</div>' +
918      '</div>' +
919      '<div class="dlw-anon-bar">' +
920        '<a class="dlw-btn dlw-anon-signup ga-fp ga-dlw-signup" href="' + DashboardLite._esc(signupHref) + '" ' +
921          'onclick="' + signupClick + '"><span class="dlw-cta-l">Start free trial</span>' +
922          '<span class="dlw-cta-s">Free trial</span></a>' +
923        '<div class="dlw-anon-or">or continue with</div>' +
924        '<div class="dlw-anon-social-row">' +
925          DashboardLite._socialBtn('google', ret) +
926          DashboardLite._socialBtn('facebook', ret) +
927          DashboardLite._socialBtn('github', ret) +
928        '</div>' +
929        '<div class="dlw-anon-signin">Cancel anytime · ' +
930          '<a class="ga-fp ga-dlw-signin-link" href="' + DashboardLite._esc(signinHref) + '" onclick="' + signinClick + '">Sign in</a></div>' +
931      '</div>' +
932    '</div>';
933  };
934
935  // Auto-rotating carousel, scoped to one container (multiple instances run independently).
936  DashboardLite._startCarousel = function (container) {
937    if (container._dlwCaroTimer) { clearInterval(container._dlwCaroTimer); container._dlwCaroTimer = null; }
938    var caro = container.querySelector('.dlw-caro');
939    if (!caro) return;
940    var track = caro.querySelector('.dlw-caro-track');
941    var dots = caro.querySelectorAll('.dlw-dot');
942    var count = track ? track.children.length : 0;
943    if (!track || count < 2) return;
944
945    var idx = 0, timer = null;
946    var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
947    var go = function (n) {
948      idx = (n % count + count) % count;
949      track.style.transform = 'translateX(' + (-idx * 100) + '%)';
950      for (var d = 0; d < dots.length; d++) dots[d].className = 'dlw-dot ga-fp ga-frontpage-carousel-dot' + (d === idx ? ' active' : '');
951    };
952    var stop = function () { if (timer) { clearInterval(timer); timer = null; container._dlwCaroTimer = null; } };
953    var start = function () { if (!reduce && !timer) { timer = setInterval(function () { go(idx + 1); }, 9000); container._dlwCaroTimer = timer; } };
954    for (var d = 0; d < dots.length; d++) {
955      (function (i) { dots[i].addEventListener('click', function () { go(i); stop(); start(); }); })(d);
956    }
957    caro.addEventListener('mouseenter', stop);
958    caro.addEventListener('mouseleave', start);
959    go(0);
960    start();
961  };
962
963  // ── Live data (scoped per container; one watcher feeds all instances) ─────
964  DashboardLite._instances = [];
965  DashboardLite._watching = false;
966
967  DashboardLite._readLiteState = function () {
968    if (typeof TopNavBar !== 'undefined' && TopNavBar.mylUserLiteState) return TopNavBar.mylUserLiteState;
969    return null;
970  };
971
972  DashboardLite._applyLiteStateTo = function (container, s) {
973    if (!s) return;
974    if (typeof s.xp === 'number') {
975      var x = container.querySelector('.dlw-xp');
976      if (x) x.textContent = s.xp;
977    }
978    if (typeof s.lightning === 'number') {
979      var st = container.querySelector('.dlw-streak');
980      if (st) st.textContent = s.lightning;
981    }
982  };
983
984  DashboardLite._applyLiteStateAll = function () {
985    var s = DashboardLite._readLiteState();
986    if (!s) return;
987    for (var i = 0; i < DashboardLite._instances.length; i++) {
988      DashboardLite._applyLiteStateTo(DashboardLite._instances[i], s);
989    }
990  };
991
992  DashboardLite._startLiveWatch = function () {
993    if (DashboardLite._watching) return;
994    DashboardLite._watching = true;
995    DashboardLite._applyLiteStateAll();
996    if (typeof Util !== 'undefined' && typeof Util.objFieldOnSetCallback === 'function'
997        && typeof TopNavBar !== 'undefined') {
998      try {
999        Util.objFieldOnSetCallback(TopNavBar, 'mylUserLiteState', function () {
1000          DashboardLite._applyLiteStateAll();
1001        });
1002      } catch (e) {}
1003    }
1004    var tries = 0;
1005    var timer = setInterval(function () {
1006      tries++;
1007      var s = DashboardLite._readLiteState();
1008      if (s) {
1009        DashboardLite._applyLiteStateAll();
1010        if (typeof s.xp === 'number' || typeof s.lightning === 'number') { clearInterval(timer); return; }
1011      }
1012      if (tries >= 10) clearInterval(timer);
1013    }, 1000);
1014  };
1015
1016  DashboardLite._applyData = function (container, d) {
1017    var qs = function (sel) { return container.querySelector(sel); };
1018
1019    // Name from basic-lite; avatar prefers the rendered league/user/profile picture.
1020    if (d.profile) {
1021      var n = qs('.dlw-name'); if (n) { n.removeAttribute('data-full'); n.textContent = d.profile.fullName || ''; }
1022    }
1023    var avatarUrl = (d.avatarP && d.avatarP.avatar) || (d.profile && d.profile.avatar);
1024    if (avatarUrl) { var a = qs('.dlw-avatar'); if (a) a.src = avatarUrl; }
1025
1026    // Mood badge overlay
1027    var m = qs('.dlw-mood');
1028    if (m) {
1029      var moodSrc = d.avatarP && d.avatarP.mood ? DashboardLite._moodIcon(d.avatarP.mood) : null;
1030      if (moodSrc) { m.src = moodSrc; m.style.display = ''; } else { m.style.display = 'none'; }
1031    }
1032
1033    if (d.streaks) {
1034      if (d.streaks.streaks > 0) { var s = qs('.dlw-streak'); if (s) s.textContent = d.streaks.streaks; }
1035      if (typeof d.streaks.freezeRemaining === 'number') {
1036        var f = qs('.dlw-freeze'); if (f) f.textContent = d.streaks.freezeRemaining + ' freezes left';
1037      }
1038    }
1039
1040    if (d.league) {
1041      if (d.league.xp > 0) { var x = qs('.dlw-xp'); if (x) x.textContent = d.league.xp; }
1042      var t = qs('.dlw-tier'); if (t) { t.removeAttribute('data-full'); t.textContent = DashboardLite._tierName(d.league.tier); }
1043      var b = qs('.dlw-badge'); if (b) b.src = DashboardLite._tierIcon(d.league.tier);
1044    }
1045
1046    // League standing - two stacked lines so narrow tiles don't clip:
1047    //   Rank: 2 of 20
1048    //   1015 XP
1049    var weekly = d.league ? d.league.weeklyXp : 0;
1050    var rankEl = qs('.dlw-t-league .dlw-league-rank');
1051    if (rankEl) {
1052      rankEl.innerHTML = (d.cohort && d.cohort.userRank)
1053        ? 'Rank: <b>' + d.cohort.userRank + (d.cohort.totalUsers ? ' of ' + d.cohort.totalUsers : '') + '</b>'
1054        : '';
1055    }
1056    var lxpEl = qs('.dlw-t-league .dlw-league-xp');
1057    if (lxpEl) lxpEl.innerHTML = '<b class="dlw-weekly">' + weekly + '</b> XP';
1058
1059    DashboardLite._fitContainer(container);
1060  };
1061
1062  DashboardLite._fillContinue = function (container, c) {
1063    var tile = container.querySelector('.dlw-t-continue');
1064    if (!tile) return;
1065    if (!c) { tile.style.display = 'none'; return; }
1066    tile.setAttribute('href', c.url || '#');
1067    tile.innerHTML = DashboardLite._continueInner(c);
1068    tile.style.display = '';
1069    DashboardLite._fitTitle(tile.querySelector('.dlw-title'));
1070  };
1071
1072  DashboardLite._fillPractice = function (container, c) {
1073    var tile = container.querySelector('.dlw-t-practice');
1074    if (!tile) return;
1075    if (!c || !c.practice) { tile.style.display = 'none'; return; }
1076    tile.setAttribute('href', c.practice);
1077    var ti = tile.querySelector('.dlw-practice-title');
1078    if (ti) { ti.removeAttribute('data-full'); ti.textContent = 'Practice ' + c.topic; }
1079    tile.style.display = '';
1080    DashboardLite._fitTitle(tile.querySelector('.dlw-title'));
1081  };
1082
1083  DashboardLite._isLoggedIn = function () {
1084    // Any positive signal counts. UserSession.loggedIn starts as null and only
1085    // flips to true after server/API verification (which may not happen on every
1086    // page/host), so a readable accessToken cookie is the most reliable signal.
1087    if (typeof UserSession !== 'undefined' && UserSession.loggedIn === true) return true;
1088    if (DashboardLite._getToken()) return true;
1089    if (DashboardLite._readLiteState()) return true;
1090    return false;
1091  };
1092
1093  // Logged in but the subscription/trial has ended (uic cookie plan 'closed').
1094  DashboardLite._plansUrl = 'https://order.w3schools.com/plans';
1095  DashboardLite._isFrozen = function () {
1096    try {
1097      if (typeof UserSession !== 'undefined' && typeof UserSession.getUserSubscriptionPlan === 'function') {
1098        return String(UserSession.getUserSubscriptionPlan('free')).toLowerCase() === 'closed';
1099      }
1100    } catch (e) {}
1101    return false;
1102  };
1103
1104  // Render the logged-in dashboard tiles into widgetEl and wire live data.
1105  DashboardLite._hydrateLoggedIn = function (widgetEl) {
1106    var seed = {};
1107    var lite = DashboardLite._readLiteState();
1108    if (lite) {
1109      if (typeof lite.xp === 'number') seed._initialXp = lite.xp;
1110      if (typeof lite.lightning === 'number') seed._initialStreak = lite.lightning;
1111    }
1112    widgetEl.innerHTML = DashboardLite._loggedInHtml(seed);
1113
1114    if (DashboardLite._instances.indexOf(widgetEl) === -1) DashboardLite._instances.push(widgetEl);
1115    DashboardLite._applyLiteStateTo(widgetEl, lite);
1116    DashboardLite._startLiveWatch();
1117
1118    DashboardLite.loadAll().then(function (data) { DashboardLite._applyData(widgetEl, data); });
1119    DashboardLite.fetchContinue().then(function (c) {
1120      DashboardLite._fillContinue(widgetEl, c);
1121      DashboardLite._fillPractice(widgetEl, c);
1122      // Re-fit now that all tiles are visible - showing continue/practice shrinks
1123      // every tile, so titles fit earlier (e.g. league) must re-measure.
1124      DashboardLite._fitContainer(widgetEl);
1125    });
1126  };
1127
1128  DashboardLite._paint = function (container) {
1129    if (!DashboardLite._isLoggedIn()) {
1130      container.innerHTML = DashboardLite._anonHtml();
1131      DashboardLite._startCarousel(container);
1132      return;
1133    }
1134    DashboardLite._hydrateLoggedIn(container);
1135  };
1136
1137  // Gate: wait for the user session to be verified before deciding logged-in/anon.
1138  // Shared across all instances so the session is verified only once per page.
1139  DashboardLite._sessionReadyP = null;
1140  DashboardLite.whenSessionReady = function (cb) {
1141    if (!DashboardLite._sessionReadyP) {
1142      DashboardLite._sessionReadyP = new Promise(function (resolve) {
1143        var done = false;
1144        var run = function () { if (done) return; done = true; resolve(); };
1145        var attempts = 0;
1146        var wait = function () {
1147          if (typeof MyLearning !== 'undefined' && typeof MyLearning.loadUser === 'function') {
1148            MyLearning.loadUser('dlw', run);
1149          } else if (attempts++ < 30) {
1150            setTimeout(wait, 100);
1151          } else { run(); }
1152        };
1153        wait();
1154      });
1155    }
1156    DashboardLite._sessionReadyP.then(cb);
1157  };
1158
1159  // render(containerId, format?) - format is a key of FORMATS, or omitted for the
1160  // default 300px stack layout.
1161  DashboardLite.render = function (containerId, format) {
1162    DashboardLite._injectCss();
1163    var container = document.getElementById(containerId);
1164    if (!container) return;
1165
1166    var fmt = (format && DashboardLite.FORMATS[format]) || null;
1167    container.classList.add('dlw');
1168    if (fmt) {
1169      container.classList.add('dlw-' + fmt.mode + '-mode');
1170      container.style.width = fmt.w + 'px';
1171      container.style.height = fmt.h + 'px';
1172    } else {
1173      container.classList.add('dlw-stack-mode');
1174    }
1175
1176    DashboardLite.whenSessionReady(function () { DashboardLite._paint(container); });
1177  };
1178
1179  // Carousel showcase markup. Lock badge only for anon. Custom slides/count for the live
1180  // (logged-in) reel. ctaHtml pins a button to the bottom inside the carousel.
1181  DashboardLite._caroHtml = function (showLock, slidesHtml, count, ctaHtml) {
1182    if (slidesHtml == null) slidesHtml = DashboardLite._carouselSlides();
1183    if (count == null) count = DashboardLite._caroSlideCount;
1184    var dots = '';
1185    for (var k = 0; k < count; k++) {
1186      dots += '<button class="dlw-dot ga-fp ga-frontpage-carousel-dot' + (k === 0 ? ' active' : '') + '" aria-label="Slide ' + (k + 1) + '"></button>';
1187    }
1188    return '<div class="dlw-caro">' +
1189      (showLock ? '<div class="dlw-caro-lock" aria-hidden="true">' + DashboardLite._lockSvg + '</div>' : '') +
1190      '<div class="dlw-caro-track">
1190' + slidesHtml + '</div>' +
1191      (ctaHtml || '') +
1192      '<div class="dlw-caro-dots">' + dots + '</div>' +
1193    '</div>';
1194  };
1195
1196  // ── Hero (full-width default.asp banner) — clean, marketing-style for both states ──
1197  DashboardLite._paintHero = function (container) {
1198    var ret = window.location.href, retEnc = encodeURIComponent(ret);
1199    var signupHref = DashboardLite._profileBase + '/signup?redirect_url=' + retEnc;
1200    var signinHref = DashboardLite._profileBase + '/log-in?redirect_url=' + retEnc;
1201    var signupClick = "event.preventDefault();event.stopPropagation();" +
1202      "if(typeof TopNavBar!=='undefined'&&typeof TopNavBar.showSignupDropdown==='function'){TopNavBar.showSignupDropdown();}" +
1203      "else{window.open(this.href,'_blank');}";
1204    var signinClick = "event.preventDefault();event.stopPropagation();" +
1205      "if(typeof TopNavBar!=='undefined'&&typeof TopNavBar.showLoginDropdown==='function'){TopNavBar.showLoginDropdown();}" +
1206      "else{window.open(this.href,'_blank');}";
1207
1208    if (DashboardLite._isLoggedIn()) {
1209      // Clean front-page look: personal welcome + integrated continue row + live showcase.
1210      // Frozen accounts (trial ended): learning stays free, but the live showcase becomes
1211      // the locked reel with a Reactivate CTA, and the copy reassures progress is saved.
1212      var frozen = DashboardLite._isFrozen();
1213      container.innerHTML =
1214        '<div class="dlh dlh-in' + (frozen ? ' dlh-frozen' : '') + '">' +
1215          '<div class="dlh-left">' +
1216            // Frozen accounts get the sad expression layered ON TOP of their real avatar.
1217            '<h1 class="dlh-title">Hi<span class="dlh-hname"></span>' +
1218              '<a class="dlh-hav-wrap ga-fp ga-frontpage-avatar" href="https://pathfinder.w3schools.com/profile" title="Your profile">' +
1219              '<img class="dlh-havatar" alt="" style="display:none">' +
1220              (frozen ? '<img class="dlh-face" alt="" title="Your account is paused" src="/lib/account/img/avatar_sad.svg" onerror="this.onerror=null;this.style.display=\'none\'">' : '') +
1221              '</a></h1>' +
1222            '<p class="dlh-free">' + (frozen
1223              ? 'Your trial has ended.<br><b>Reactivate to keep progress, XP and Spaces active.</b>'
1224              : 'Keep your streak alive and pick up right where you left off.') + '</p>' +
1225            (frozen ? '<p class="dlh-support">You can still read every tutorial for free. The moment you reactivate, your streaks, league spot and Spaces pick up right where they paused.</p>' : '') +
1226            '<div class="dlh-startrow dlh-continue-fallback" style="display:none">' +
1227              '<span class="dlh-or">Continue reading for free</span>' +
1228              '<a class="dlh-or-link ga-fp ga-frontpage-all-tutorials" href="/where_to_start.asp" ' +
1229                'onclick="if(typeof TopNavBar!==\'undefined\'&&typeof TopNavBar.openNavItem===\'function\'){event.preventDefault();TopNavBar.openNavItem(\'tutorials\');}">All Tutorials &rsaquo;</a>' +
1230            '</div>' +
1231            '<a class="dlh-cont2 ga-fp ga-frontpage-continue" href="#" style="display:none"></a>' +
1232            (frozen ? '' :
1233              '<div class="dlh-sub"><a class="dlh-dash ga-fp ga-frontpage-dashboard" href="https://pathfinder.w3schools.com">' +
1234                '<svg width="17" height="17" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><rect width="7" height="18" x="3" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="14" rx="1"></rect></svg>' +
1235                'Go to your dashboard</a></div>') +
1236          '</div>' +
1237          '<div class="dlh-right" id="' + container.id + '-caro">' +
1238            (frozen ? DashboardLite._caroHtml(true, null, null,
1239              '<a class="dlw-caro-cta ga-fp ga-frontpage-reactivate" href="' + DashboardLite._plansUrl + '">Reactivate</a>') : '') +
1240          '</div>' +
1241        '</div>';
1242      if (frozen) DashboardLite._startCarousel(container);
1243
1244      // Badges paint instantly from cached data (topnav lite state + the localStorage
1245      // continue cache) and re-render when the live fetches resolve - waiting for
1246      // every API to settle leaves the column empty for seconds.
1247      if (!frozen) {
1248        var caroEl0 = document.getElementById(container.id + '-caro');
1249        var lite0 = DashboardLite._readLiteState() || {};
1250        if (caroEl0) {
1251          // Seed assumes opted-in so members do not flash the "Join" cell; the
1252          // real opt-in state corrects it when loadAll resolves.
1253          caroEl0.innerHTML = DashboardLite._badgesHtml({
1254            streaks: { streaks: typeof lite0.lightning === 'number' ? lite0.lightning : 0 },
1255            league: { xp: typeof lite0.xp === 'number' ? lite0.xp : 0, optedIn: true }
1256          }, DashboardLite._readContCache());
1257        }
1258      }
1259
1260      var contP = DashboardLite.fetchContinue();
1261      contP.then(function (c) {
1262        if (c) {
1263          var row = container.querySelector('.dlh-cont2');
1264          if (row) {
1265            var pct = typeof c.progress === 'number' ? c.progress : 0;
1266            row.setAttribute('href', c.url || '#');
1267            row.innerHTML =
1268              '<span class="dlh-cont2-tag">Pick up where you left off</span>' +
1269              '<span class="dlh-cont2-ico">' + DashboardLite._topicIconHtml(c.title) + '</span>' +
1270              '<span class="dlh-cont2-mid">' +
1271                '<span class="dlh-cont2-txt">' + DashboardLite._esc(c.title) + '</span>' +
1272                '<span class="dlw-progressbar" title="' + pct + '% complete">' +
1273                  '<span class="dlw-progressbar-fill" style="width:' + pct + '%"></span></span>' +
1274              '</span>' +
1275              '<span class="dlw-btn">Continue</span>';
1276            row.style.display = '';
1277          }
1278        } else {
1279          var fb = container.querySelector('.dlh-continue-fallback');
1280          if (fb) fb.style.display = '';
1281        }
1282      });
1283      // Personalize the headline + build the showcase once real data loads.
1284      DashboardLite.loadAll().then(function (data) {
1285        var nameEl = container.querySelector('.dlh-hname');
1286        if (nameEl && data.profile && data.profile.firstName) {
1287          nameEl.textContent = ', ' + data.profile.firstName;
1288        }
1289        var avEl = container.querySelector('.dlh-havatar');
1290        var avatarUrl = (data.avatarP && data.avatarP.avatar) || (data.profile && data.profile.avatar);
1291        if (avEl && avatarUrl) { avEl.src = avatarUrl; avEl.style.display = ''; }
1292
1293        if (!frozen) {
1294          contP.then(function (c) {
1295            var caroEl = document.getElementById(container.id + '-caro');
1296            if (caroEl) caroEl.innerHTML = DashboardLite._badgesHtml(data, c);
1297          });
1298        }
1299      });
1300      return;
1301    }
1302
1303    container.innerHTML =
1304      '<div class="dlh dlh-anon">' +
1305        '<div class="dlh-left">' +
1306          '<h1 class="dlh-title">Learn to Code</h1>' +
1307          '<p class="dlh-free">Free tutorials, examples and references.<br><b>No sign-up needed, just start learning.</b></p>' +
1308          '<p class="dlh-support">Or sign up to unlock premium features:</p>' +
1309          '<ul class="dlh-perks">' +
1310            '<li>Progress tracking</li><li>Build &amp; host websites</li>' +
1311            '<li>Ad-free learning</li><li>XP &amp; Streaks</li>' +
1312            '<li>Practice problems</li><li>Leagues</li>' +
1313            '<li>Challenges</li><li>Avatar</li>' +
1314          '</ul>' +
1315          '<div class="dlh-sub">Already a member? <a class="ga-fp ga-frontpage-signin-link" href="' + DashboardLite._esc(signinHref) + '" onclick="' + signinClick + '">Sign in</a></div>' +
1316        '</div>' +
1317        '<div class="dlh-right">' +
1318          DashboardLite._caroHtml(true, null, null,
1319            '<a class="dlw-caro-cta ga-fp ga-frontpage-signin" href="' + DashboardLite._esc(signinHref) + '" onclick="' + signinClick + '">Sign in</a>') +
1320        '</div>' +
1321      '</div>';
1322    DashboardLite._startCarousel(container);
1323  };
1324
1325  // Logged-in showcase is the badge grid; anon/frozen get the aspiration carousel.
1326  DashboardLite.renderHero = function (containerId) {
1327    DashboardLite._injectCss();
1328    var container = document.getElementById(containerId);
1329    if (!container) return;
1330    container.classList.add('dlh-host'); // reserve height immediately to avoid layout jump
1331    DashboardLite.whenSessionReady(function () { DashboardLite._paintHero(container); });
1332  };
1333
1334  window.DashboardLite = DashboardLite;
1335})();

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.