PageSourceSearch

https://akhirahprojects.com/script.js

js akhirahprojects.com collected 2026-10-02 01:21:49 UTC 276,857 bytes, 6,531 lines download raw bytes

1/* Akhirah Projects — landing runtime */
2
3(function(){
4  "use strict";
5
6  // -------------------------
7  // Config
8  // -------------------------
9  const LIBRARY_INDEX = "/data/library.json";
10  const BOOKS_INDEX = "/data/books.json";
11  const AUDIO_INDEX = "/data/audio.json";
12  const VIDEOS_INDEX = "/data/videos.json";
13  const APPS_REGISTRY = "/data/apps.json";
14  const PRAYER_CITIES_INDEX = "/data/prayer-cities.json";
15  const UPDATES_FEED = "/data/updates.json";
16  const APPS_REGISTRY_TIMEOUT_MS = 5000;
17  const FEATURE_FLAGS = {
18    hub: false,
19  };
20
21const fetchWithTimeout = async (url, opts = {}, timeoutMs = 8000) => {
22  const controller = new AbortController();
23  const t = setTimeout(() => controller.abort(), timeoutMs);
24  try{
25    const res = await fetch(url, { ...opts, signal: controller.signal });
26    return res;
27  } finally {
28    clearTimeout(t);
29  }
30};
31
32
33  // -------------------------
34  // Small helpers
35  // -------------------------
36  const $ = (sel, root=document) => root.querySelector(sel);
37  const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
38
39  function safeText(v){
40    if (v == null) return "";
41    if (typeof v === "string") return v;
42    try { return String(v); } catch { return ""; }
43  }
44
45  function currentLocale(){
46    try{ return window.AP_I18N?.current?.() || "en"; }
47    catch{ return "en"; }
48  }
49
50  function libraryBrowseHref(){
51    return "/library/";
52  }
53
54  function escapeHtml(str){
55    return safeText(str)
56      .replace(/&/g,"&")
57      .replace(/</g,"&lt;")
58      .replace(/>/g,"&gt;")
59      .replace(/"/g,"&quot;")
60      .replace(/'/g,"&#39;");
61  }
62
63  function applyFeatureFlags(){
64    Object.entries(FEATURE_FLAGS).forEach(([key, enabled]) => {
65      document.querySelectorAll(`[data-feature="${key}"]`).forEach((el) => {
66        el.hidden = !enabled;
67        if (enabled) el.removeAttribute("aria-hidden");
68        else el.setAttribute("aria-hidden", "true");
69      });
70    });
71  }
72
73
74  function normalizeAppPath(path){
75    const s = safeText(path).trim();
76    return s.startsWith("/") ? s : `/${s}`;
77  }
78
79  function registryStatus(message){
80    const el = document.getElementById("appRegistryStatus");
81    if (el) el.textContent = message;
82  }
83
84  function i18n(){
85    return window.AP_I18N || null;
86  }
87
88  function getRegistryApps(payload, includeHomepageCards = false){
89    const rawApps = Array.isArray(payload?.apps) ? payload.apps : [];
90    return rawApps
91      .filter((app) => app && (app.navigation === true || (includeHomepageCards && app.homepageCard === true)))
92      .filter((app) => app.status === "active" || app.status === "public-linked")
93      .map((app) => ({
94        id: safeText(app.id).trim(),
95        name: safeText(app.name || app.shortName).trim(),
96        shortName: safeText(app.shortName || app.name).trim(),
97        category: safeText(app.category).trim(),
98        canonicalPath: normalizeAppPath(app.canonicalPath || app.canonicalUrl || "#"),
99        showcasePath: normalizeAppPath(app.showcasePath || app.showcaseUrl || app.canonicalPath || app.canonicalUrl || "#"),
100        description: safeText(app.description || app?.seo?.description).trim(),
101        homepageBadge: safeText(app.homepageBadge).trim(),
102        homepageDescription: safeText(app.homepageDescription || app.description || app?.seo?.description).trim(),
103        homepageCta: safeText(app.homepageCta).trim(),
104        status: safeText(app.status).trim(),
105      }))
106      .filter((app) => app.id && app.name && app.canonicalPath && app.canonicalPath !== "/#");
107  }
108
109  function getPublicRegistryApps(payload){
110    return getRegistryApps(payload);
111  }
112
113  function getHomepageRegistryApps(payload){
114    return getRegistryApps(payload, true);
115  }
116
117  function labelFromCategory(category){
118    return safeText(category)
119      .replace(/[-_]+/g, " ")
120      .replace(/\b\w/g, (m) => m.toUpperCase())
121      .trim() || "App";
122  }
123
124  const HOMEPAGE_CARD_ORDER = ["mindmap", "designer", "prayer", "vidveil", "focus"];
125
126  function appVisualMarkup(id){
127    if(id === "prayer") return '<div class="product-visual product-visual--app-icon product-visual--prayer" aria-hidden="true"><img class="product-icon product-icon--prayer" src="/assets/apps/prayer-app-icon.webp" alt="" width="512" height="512" loading="lazy" decoding="async"></div>';
128    if(id === "mindmap") return '<div class="product-visual product-visual--app-icon product-visual--mindmap" aria-hidden="true"><img class="product-icon" src="/assets/apps/kawn-mindmap-icon.webp" alt="" width="512" height="512" loading="lazy" decoding="async"></div>';
129    if(id === "store") return '<div class="product-visual product-visual--store" aria-hidden="true"><span class="store-sheet store-sheet--back"></span><span class="store-sheet store-sheet--front"><i></i><i></i><i></i></span><span class="map-label">BENEFICIAL GOODS</span></div>';
130    if(id === "designer") return '<div class="product-visual product-visual--app-icon product-visual--designer" aria-hidden="true"><img class="product-icon" src="/assets/apps/akhirah-designer-icon.webp" alt="" width="512" height="512" loading="lazy" decoding="async"></div>';
131    if(id === "vidveil") return '<div class="product-visual product-visual--app-icon product-visual--v
131idveil" aria-hidden="true"><img class="product-icon product-icon--vidveil" src="/vidveil/assets/vidveil-icon-128.png" alt="" width="128" height="128" loading="lazy" decoding="async"></div>';
132    if(id === "focus") return '<div class="product-visual product-visual--app-icon product-visual--focus" aria-hidden="true"><img class="product-icon product-icon--focus" src="/assets/apps/chatgpt-token-counter-focus-tools-icon.webp" alt="" width="512" height="512" loading="lazy" decoding="async"></div>';
133    return '<div class="product-visual" aria-hidden="true"><span class="product-orb"></span></div>';
134  }
135
136  function renderRegistryAppCards(apps){
137    const grid = document.getElementById("appRegistryGrid");
138    const cardApps = apps
139      .filter((app) => HOMEPAGE_CARD_ORDER.includes(app.id))
140      .sort((a, b) => HOMEPAGE_CARD_ORDER.indexOf(a.id) - HOMEPAGE_CARD_ORDER.indexOf(b.id));
141    if (!grid || !cardApps.length) return;
142
143    grid.innerHTML = cardApps.map((app) => {
144      const label = i18n()?.appBadge(app.id, app.homepageBadge || labelFromCategory(app.category)) || app.homepageBadge || labelFromCategory(app.category);
145      const name = i18n()?.appName(app.id, app.name) || app.name;
146      const description = i18n()?.appDescription(app.id, app.homepageDescription || app.description) || app.homepageDescription || app.description;
147      const cta = i18n()?.appCta(app.id, app.homepageCta || `Open ${app.shortName || app.name} →`) || app.homepageCta || `Open ${app.shortName || app.name} →`;
148      const openLabel = i18n()?.appAria(app.id, `Open ${app.name}`) || `Open ${app.name}`;
149      const cardVariant = app.id === "mindmap" ? "product-card--featured" : "product-card--supporting";
150      const linkAttribute = ["vidveil", "focus"].includes(app.id) ? `data-homepage-tool-id="${escapeHtml(app.id)}"` : `data-app-link-id="${escapeHtml(app.id)}"`;
151      return `
152        <article class="tile app-registry-card product-card ${cardVariant}" data-app-card="${escapeHtml(app.id)}" data-reveal="up">
153          ${appVisualMarkup(app.id)}
154          <div class="tile-top">
155            <div class="badge">${escapeHtml(label)}</div>
156            <h3>${escapeHtml(name)}</h3>
157            <p>${escapeHtml(description)}</p>
158          </div>
159          <div class="tile-actions app-registry-actions">
160            <a class="link" href="${escapeHtml(app.canonicalPath)}" ${linkAttribute} aria-label="${escapeHtml(openLabel)}">${escapeHtml(cta)}</a>
161          </div>
162        </article>`;
163    }).join("");
164
165    grid.removeAttribute("data-registry-fallback");
166  }
167
168  function renderRegistryFooterLinks(apps){
169    const host = document.getElementById("footerExploreApps");
170    if (!host || !apps.length) return;
171
172    const footerPathFor = (app) => app.id === "mindmap" && app.showcasePath ? app.showcasePath : app.canonicalPath;
173
174    host.innerHTML = apps.map((app) => (
175      `<a href="${escapeHtml(footerPathFor(app))}">${escapeHtml(i18n()?.appShortName(app.id, app.shortName || app.name) || app.shortName || app.name)}</a>`
176    )).join("");
177    host.removeAttribute("data-registry-fallback");
178  }
179
180  function escapeAttrSelector(value){
181    return safeText(value).replace(/[\"\]]/g, "\\$&");
182  }
183
184  function syncRegistryLinks(apps){
185    apps.forEach((app) => {
186      document.querySelectorAll(`[data-app-link-id="${escapeAttrSelector(app.id)}"]`).forEach((link) => {
187        if (link.tagName === "A") {
188          const targetPath = link.getAttribute("data-app-link-target") === "showcase"
189            ? app.showcasePath
190            : app.canonicalPath;
191          link.setAttribute("href", targetPath);
192        }
193        const textEl = link.querySelector(".sb-text, .bn-txt");
194        if (textEl) textEl.textContent = i18n()?.appShortName(app.id, app.shortName || app.name) || app.shortName || app.name;
195      });
196    });
197  }
198
199  async function initAppRegistry(){
200    try{
201      const res = await fetchWithTimeout(APPS_REGISTRY, { cache: "no-store" }, APPS_REGISTRY_TIMEOUT_MS);
202      if(!res.ok) throw new Error(`apps registry http ${res.status}`);
203      const payload = await res.json();
204      const apps = getPublicRegistryApps(payload);
205      const homepageApps = getHomepageRegistryApps(payload);
206      if(!apps.length) throw new Error("apps registry empty");
207      window.__AP_ACTIVE_APP_COUNT = apps.length;
208
209      renderRegistryAppCards(homepageApps);
210      renderRegistryFooterLinks(apps);
211      syncRegistryLinks(apps);
212      const localizer = i18n();
213      if (localizer) localizer.apply(localizer.current());
214      initReveal();
215      const loaded = i18n()?.t("apps.statusLoaded");
216      registryStatus(typeof loaded === "function" ? loaded(apps.length) : `Loaded ${apps.length} active app links from the platform registry.`);
217    }catch(_err){
218      registryStatus(i18n()?.t("apps.statusBuiltIn", "Using built-in app link fallback.") || "Using built-in app link fallback.");
219    }
220  }
221
222  function renderLibraryShowcaseCard(item, index){
223    const type = safeText(item?.libraryType || "article");
224    const href = type === "book"
225      ? `/library/books/${encodeURIComponent(safeText(item?.slug || "").trim())}/`
226      : type === "audio"
227        ? `/library/audio/${encodeURIComponent(safeText(item?.slug || "").trim())}/`
228        : type === "video"
229          ? `/library/videos/${encodeURIComponent(safeText(item?.slug || "").trim())}/`
230        : `/library/${encodeURIComponent(safeText(item?.slug || "").trim())}`;
231    const title = escapeHtml(safeText(item?.title || `Untitled ${type}`));
232    const description = escapeHtml(safeText(item?.description || ""));
233    const readLabel = escapeHtml(i18n()?.t("library.read", "Open in the Library →") || "Open in the Library →");
234    const browseLabel = escapeHtml(i18n()?.t("library.browse", "Browse the library →") || "Browse the library →");
235    const tileClass = index === 0 ? "tile lib-showcase-card lib-tile-featured" : "tile lib-showcase-card";
236    const category = escapeHtml(i18n()?.t(`library.formats.${type}`, type.replace(/\b\w/g, (letter) => letter.toUpperCase())) || type);
237    const published = escapeHtml(safeText(item?.datePublished || item?.dateModified || "").slice(0, 10));
238
239    return `
240      <article class="${tileClass}" lang="en">
241        <div class="lib-showcase-meta"><span class="lib-showcase-index">0${index + 1}</span><span>${category}</span>${published ? `<time datetime="${published}">${published}</time>` : ""}</div>
242        <div class="tile-top">
243          <h3><a class="link" href="${escapeHtml(href)}">${title}</a></h3>
244          <p>${description}</p>
245        </div>
246        <div class="tile-actions">
247          <a class="link" href="${escapeHtml(href)}">${readLabel}</a>
248          ${index === 0 ? `<a class="link subtle" href="${escapeHtml(libraryBrowseHref())}">${browseLabel}</a>` : ""}
249        </div>
250      </article>
251    `;
252  }
253
254  function renderAudioShowcaseEmpty(){
255    const copy = i18n()?.COPY?.[i18n()?.current?.()]?.library || {};
256    return `
257      <article class="tile lib-showcase-card">
258        <div class="lib-showcase-meta"><span class="lib-showcase-index">03</span><span>${escapeHtml(copy.formats?.audio || "Audio")}</span></div>
259        <div class="tile-top">
260          <h3>${escapeHtml(copy.audioEmptyTitle || "Audio collection in preparation")}</h3>
261          <p>${escapeHtml(copy.audioEmptyBody || "Approved lectures, recitations, and other beneficial recordings will appear here.")}</p>
262        </div>
263        <div class="tile-actions"><a class="link" href="/library/?type=audio">${escapeHtml(copy.viewAudio || "View Audio →")}</a></div>
264      </article>`;
265  }
266
267  function renderVideoShowcaseEmpty(){
268    const copy = i18n()?.COPY?.[i18n()?.current?.()]?.library || {};
269    return `
270      <article class="tile lib-showcase-card">
271        <div class="lib-showcase-meta"><span class="lib-showcase-index">04</span><span>${escapeHtml(copy.formats?.videos || "Videos")}</span></div>
272        <div class="tile-top">
273          <h3>${escapeHtml(copy.videoEmptyTitle || "Video collection in preparation")}</h3>
274          <p>${escapeHtml(copy.videoEmptyBody || "Reviewed lectures and lessons will appear here for intentional watching.")}</p>
275        </div>
276        <div class="tile-actions"><a class="link" href="/library/?type=videos">${escapeHtml(copy.viewVideos || "View Videos →")}</a></div>
277      </article>`;
278  }
279
280  function renderLibraryShowcaseFallback(grid){
281    const browseLabel = escapeHtml(i18n()?.t("library.browse", "Browse the library →") || "Browse the library →");
282    grid.innerHTML = `
283      <div class="tile lib-tile-featured" lang="en">
284        <div class="tile-top">
285          <h3><a class="link" href="${escapeHtml(libraryBrowseHref())}">Library archive</a></h3>
286          <p>Open the full Library for books, articles, beneficial audio, and curated videos.</p>
287        </div>
288        <div class="tile-actions">
289          <a class="link" href="${escapeHtml(libraryBrowseHref())}">${browseLabel}</a>
290        </div>
291      </div>
292    `;
293  }
294
295  async function renderLibraryShowcase(){
296    const grid = $("#libraryFeatureGrid");
297    if(!grid) return;
298
299    try{
300      const settled = await Promise.allSettled([
301        fetchWithTimeout(LIBRARY_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : Promise.reject(new Error("articles unavailable"))),
302        fetchWithTimeout(BOOKS_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : Promise.reject(new Error("books unavailable"))),
303        fetchWithTimeout(AUDIO_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : null),
304        fetchWithTimeout(VIDEOS_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : null),
305      ]);
306      if(settled[0].status !== "fulfilled" || settled[1].status !== "fulfilled") throw new Error("required Library registries unavailable");
307      const articlePayload = settled[0].value;
308      const bookPayload = settled[1].value;
309      const audioPayload = settled[2].status === "fulfilled" ? settled[2].value : null;
310      const videoPayload = settled[3].status === "fulfilled" ? settled[3].value : null;
311      const newest = (records, libraryType) => records
312        .filter((item) => safeText(item?.status).toLowerCase() === "published" && safeText(item?.slug).trim())
313        .sort((a, b) => {
314          const da = Date.parse(safeText(a?.datePublished || a?.dateModified || ""));
315          const db = Date.parse(safeText(b?.datePublished || b?.dateModified || ""));
316          return (Number.isFinite(db) ? db : 0) - (Number.isFinite(da) ? da : 0);
317        })
318        .map((item) => ({ ...item, libraryType }))[0] || null;
319      const balanced = [
320        newest(Array.isArray(bookPayload?.books) ? bookPayload.books : [], "book"),
321        newest(Array.isArray(articlePayload?.articles) ? articlePayload.articles : [], "article"),
322        newest(Array.isArray(audioPayload?.audio) ? audioPayload.audio : [], "audio"),
323        newest(Array.isArray(videoPayload?.videos) ? videoPayload.videos : [], "video"),
324      ];
325      if(!balanced[0] || !balanced[1]) throw new Error("required Library previews unavailable");
326      grid.innerHTML = balanced.slice(0, 2).map(renderLibraryShowcaseCard).join("")
327        + (balanced[2] ? renderLibraryShowcaseCard(balanced[2], 2) : renderAudioShowcaseEmpty())
328        + (balanced[3] ? renderLibraryShowcaseCard(balanced[3], 3) : renderVideoShowcaseEmpty());
329      initReveal();
330    }catch(err){
331      console.warn("library showcase skipped:", err?.message || err);
332      renderLibraryShowcaseFallback(grid);
333    }
334  }
335
336  let __apUpdatesItems = [];
337  let __apUpdatesSpeed = 1.5;
338  let __apUpdatesIndex = 0;
339  let __apUpdatesOffset = 0;
340  let __apUpdatesBaseDuration = 42;
341
342  function updatesCopy(path, fallback){
343    return window.AP_I18N?.t(`updates.${path}`, fallback) || fallback || "";
344  }
345
346  function localizedValue(value){
347    if(value == null) return "";
348    if(typeof value === "string" || typeof value === "number") return safeText(value);
349    if(typeof value === "object"){
350      const locale = currentLocale();
351      return safeText(value[locale] || value.en || value[Object.keys(value)[0]] || "");
352    }
353    return safeText(value);
354  }
355
356  function isPublicUpdateItem(item, now){
357    if(!item || typeof item !== "object") return false;
358    if(item.visible === false || item.draft === true) return false;
359    const title = localizedValue(item.title).trim();
360    if(!title) return false;
361    if(item.expiresAt){
362      const expires = Date.parse(item.expiresAt);
363      if(Number.isFinite(expires) && expires < now) return false;
364    }
365    return true;
366  }
367
368  function normalizeUpdateItem(item){
369    const type = ["release", "upcoming", "fix"].includes(item.type) ? item.type : "upcoming";
370    const href = safeText(item.href || "").trim();
371    return {
372      id: safeText(item.id || `${type}-${item.date || item.target || item.title}`).trim(),
373      type,
374      status: safeText(item.status || "").trim(),
375      version: safeText(item.version || "").trim(),
376      date: safeText(item.date || "").trim(),
377      target: safeText(item.target || "").trim(),
378      title: localizedValue(item.title).trim(),
379      summary: localizedValue(item.summary).trim(),
380      product: localizedValue(item.product).trim(),
381      impact: localizedValue(item.impact).trim(),
382      cta: localizedValue(item.cta).trim(),
383      href: href && (href.startsWith("/") || /^https?:\/\//i.test(href)) ? href : "",
384    };
385  }
386
387  function updateTypeLabel(type){
388    return updatesCopy(`types.${type}`, type);
389  }
390
391  function stripCategoryMarkers(value, category){
392    const text = String(value || "");
393    if(category === "color") return text.replace(/\{\{color-(?:gold|emerald|ivory|rose)\|([^{}]+)\}\}/g, "$1").replace(/__([^_]+)__/g, "$1");
394    if(category === "font") return text.replace(/\{\{font-(?:sans|serif|mono)\|([^{}]+)\}\}/g, "$1");
395    if(category === "weight") return text.replace(/\{\{weight-thin\|([^{}]+)\}\}/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1");
396    return text;
397  }
398
399  function formatUpdateInlineText(value){
400    let html = escapeHtml(value || "");
401    let previous = "";
402    while(html !== previous){
403      previous = html;
404      html = html
405        .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
406        .replace(/__([^_]+)__/g, '<span class="updates-ticker-accent">$1</span>')
407        .replace(/==([^=]+)==/g, '<span class="updates-ticker-mark">$1</span>')
408        .replace(/\*([^*]+)\*/g, "<em>$1</em>")
409        .replace(/\{\{(font-(?:sans|serif|mono)|color-(?:gold|emerald|ivory|rose)|weight-thin)\|([^{}]+)\}\}/g, '<span class="$1">$2</span>');
410    }
411    return html;
412  }
413
414  function stripUpdateInlineText(value){
415    let text = String(value || "");
416    let previous = "";
417    while(text !== previous){
418      previous = text;
419      text = text
420        .replace(/\{\{(?:font-(?:sans|serif|mono)|color-(?:gold|emerald|ivory|rose)|weight-thin)\|([^{}]+)\}\}/g, "$1")
421        .replace(/\*\*([^*]+)\*\*/g, "$1")
422        .replace(/__([^_]+)__/g, "$1")
423        .replace(/==([^=]+)==/g, "$1")
424        .replace(/\*([^*]+)\*/g, "$1");
425    }
426    return text;
427  }
428
429  function updateTickerText(item){
430    return `${updateTypeLabel(item.type)}: ${stripUpdateInlineText(item.title)}`;
431  }
432
433  function updateTickerHtml(item){
434    return `<span class="updates-ticker__type">${escapeHtml(updateTypeLabel(item.type))}:</span> <span class="updates-ticker__text">${formatUpdateInlineText(item.title)}</span>`;
435  }
436
437  function applyUpdatesControlLabels(section){
438    const next = section.querySelector("[data-updates-next]");
439    const speedLabel = section.querySelector(".updates-ticker__speed-label");
440    const nextLabel = updatesCopy("next", "Next update");
441    const speedText = updatesCopy("speed", "Speed");
442    if(next){
443      next.setAttribute("aria-label", nextLabel);
444      next.setAttribute("title", nextLabel);
445    }
446    if(speedLabel){
447      speedLabel.textContent = speedText;
448      speedLabel.setAttribute("aria-hidden", "true");
449    }
450    section.querySelectorAll("[data-updates-speed]").forEach((button) => {
451      const speed = Number(button.dataset.updatesSpeed);
452      const name = speed === 1 ? updatesCopy("slow", "Slow") : speed === 1.5 ? updatesCopy("normal", "Normal") : updatesCopy("fast", "Fast");
453      button.textContent = name;
454      button.setAttribute("aria-label", `${name} (${speed}×)`);
455      button.title = `${name} (${speed}×)`;
456    });
457  }
458
459  function applyTickerMetrics(section, offset = __apUpdatesOffset){
460    const viewport = section.querySelector(".updates-ticker__viewport");
461    const track = section.querySelector(".updates-ticker__track");
462    const run = section.querySelector(".updates-ticker__run");
463    if(!viewport || !track || !run) return;
464    const compactViewport = window.matchMedia?.("(max-width:560px)").matches;
465    const startGap = compactViewport ? 0 : viewport.clientWidth;
466    run.style.paddingLeft = `${startGap}px`;
467    const runWidth = run.scrollWidth;
468    __apUpdatesBaseDuration = Math.max(24, runWidth / 36);
469    const duration = __apUpdatesBaseDuration / __apUpdatesSpeed;
470    track.style.setProperty("--ticker-run-width", `${runWidth}px`);
471    track.style.setProperty("--ticker-duration", `${duration}s`);
472    track.style.animationDelay = runWidth > 0 ? `${-(Math.max(0, offset) * duration / runWidth)}s` : "0s";
473  }
474
475  function bindUpdatesControls(section){
476    if(section.dataset.updatesControlsReady === "true"){
477      applyUpdatesControlLabels(section);
478      return;
479    }
480    section.dataset.updatesControlsReady = "true";
481    const next = section.querySelector("[data-updates-next]");
482    const speedButtons = Array.from(section.querySelectorAll("[data-updates-speed]"));
483    const status = section.querySelector("[data-updates-status]");
484    const getItems = () => {
485      const run = section.querySelector(".updates-ticker__run");
486      return run ? Array.from(run.children).filter((item) => item.matches(".updates-ticker__item, .updates-ticker__link")) : [];
487    };
488    const setSpeed = (speed) => {
489      __apUpdatesSpeed = Number(speed) || 1;
490      speedButtons.forEach((button) => {
491        const active = Number(button.dataset.updatesSpeed) === __apUpdatesSpeed;
492        button.classList.toggle("is-active", active);
493        button.setAttribute("aria-pressed", active ? "true" : "false");
494      });
495      applyTickerMetrics(section);
496    };
497    next?.addEventListener("click", () => {
498      const items = getItems();
499      if(items.length < 2) return;
500      __apUpdatesIndex = (__apUpdatesIndex + 1) % items.length;
501      const item = items[__apUpdatesIndex];
502      const run = item.parentElement;
503      const isRtl = section.classList.contains("updates-ticker--rtl");
504      __apUpdatesOffset = isRtl
505        ? Math.max(0, run.scrollWidth - item.offsetLeft - item.offsetWidth)
506        : Math.max(0, item.offsetLeft);
507      applyTickerMetrics(section, __apUpdatesOffset);
508      if(status) status.textContent = updateTickerText(__apUpdatesItems[__apUpdatesIndex]);
509    });
510    speedButtons.forEach((button) => button.addEventListener("click", () => setSpeed(button.dataset.updatesSpeed)));
511    window.addEventListener("ap:languagechange", () => {
512      const rtl = ["ar", "ur"].includes(currentLocale());
513      section.dir = rtl ? "rtl" : "ltr";
514      section.classList.toggle("updates-ticker--rtl", rtl);
515      section.classList.toggle("updates-ticker--ltr", !rtl);
516      applyUpdatesControlLabels(section);
517      applyTickerMetrics(section);
518    });
519    let resizeTimer = 0;
520    window.addEventListener("resize", () => {
521      window.clearTimeout(resizeTimer);
522      resizeTimer = window.setTimeout(() => applyTickerMetrics(section), 120);
523    }, { passive: true });
524    applyUpdatesControlLabels(section);
525    setSpeed(__apUpdatesSpeed);
526  }
527
528  function renderUpdatesTicker(){
529    const section = $("#updates");
530    const track = $("#updatesTickerTrack");
531    if(!section || !track) return;
532
533    if(!__apUpdatesItems.length){
534      section.hidden = true;
535      return;
536    }
537
538    section.hidden = false;
539    const rtl = ["ar", "ur"].includes(currentLocale());
540    section.dir = rtl ? "rtl" : "ltr";
541    section.classList.toggle("updates-ticker--rtl", rtl);
542    section.classList.toggle("updates-ticker--ltr", !rtl);
543    __apUpdatesIndex = 0;
544    __apUpdatesOffset = 0;
545    const content = __apUpdatesItems.map((item) => {
546      const body = `<span class="updates-ticker__item updates-ticker__item--${escapeHtml(item.type)}">${updateTickerHtml(item)}</span>`;
547      return item.href
548        ? `<a class="updates-ticker__link" href="${escapeHtml(item.href)}">${body}<span class="updates-ticker__cta">${escapeHtml(item.cta || "Open")}</span></a>`
549        : body;
550    }).join('<span class="updates-ticker__sep" aria-hidden="true">◆</span>');
551    track.innerHTML = `<div class="updates-ticker__run">${content}</div><div class="updates-ticker__run" aria-hidden="true">${content}</div>`;
552    bindUpdatesControls(section);
553    applyTickerMetrics(section);
554  }
555
556  async function initUpdatesSection(){
557    const section = $("#updates");
558    if(!section) return;
559    section.hidden = true;
560    try{
561      const res = await fetchWithTimeout(UPDATES_FEED, { cache: "no-store" }, 5000);
562      if(!res.ok) throw new Error(`updates http ${res.status}`);
563      const payload = await res.json();
564      const now = Date.now();
565      __apUpdatesItems = (Array.isArray(payload?.items) ? payload.items : [])
566        .filter((item) => isPublicUpdateItem(item, now))
567        .map(normalizeUpdateItem);
568      renderUpdatesTicker();
569    }catch(err){
570      __apUpdatesItems = [];
571      section.hidden = true;
572      console.warn("updates section skipped:", err?.message || err);
573    }
574  }
575
576
577  function scheduleIdle(task, timeout = 900){
578    if ("requestIdleCallback" in window){
579      window.requestIdleCallback(() => task(), { timeout });
580      return;
581    }
582    window.setTimeout(task, 180);
583  }
584
585  function getPrimaryScrollHost(){
586    const scrollRoot = document.querySelector(".app-main");
587    const isDesktopShell = !!(scrollRoot && window.matchMedia && window.matchMedia("(min-width: 1024px)").matches);
588    return isDesktopShell ? scrollRoot : window;
589  }
590
591  function releaseBootState(){
592    const root = document.documentElement;
593    let done = false;
594    const finish = () => {
595      if (done) return;
596      done = true;
597      root.classList.remove("is-booting");
598    };
599    const releaseSoon = () => {
600      if ("requestAnimationFrame" in window){
601        requestAnimationFrame(() => requestAnimationFrame(finish));
602        return;
603      }
604      window.setTimeout(finish, 60);
605    };
606    const releaseAfterFonts = () => {
607      const fontsReady = document.fonts && document.fonts.ready;
608      if (!fontsReady || typeof fontsReady.then !== "function"){
609        releaseSoon();
610        return;
611      }
612      fontsReady.then(releaseSoon).catch(releaseSoon);
613    };
614
615    if (document.readyState === "complete"){
616      releaseAfterFonts();
617      return;
618    }
619
620    window.addEventListener("load", releaseAfterFonts, { once: true });
621    window.setTimeout(releaseSoon, 420);
622  }
623
624  function initScrollExperience(){
625    const root = document.documentElement;
626    if(root.dataset.scrollExperienceReady === "true") return;
627    root.dataset.scrollExperienceReady = "true";
628
629    const appMain = document.querySelector(".app-main");
630    const reducedMotionQuery = window.matchMedia?.("(prefers-reduced-motion: reduce)");
631    const desktopGlideQuery = window.matchMedia?.("(min-width: 1024px) and (pointer: fine)");
632    const reduce = reducedMotionQuery?.matches;
633    const supportsViewTimeline = window.CSS?.supports?.("animation-timeline", "view()") === true;
634    const progress = document.createElement("div");
635    const progressLine = document.createElement("span");
636    progress.className = "ap-scroll-progress";
637    progress.setAttribute("aria-hidden", "true");
638    progress.append(progressLine);
639    document.body.append(progress);
640
641    let host = null;
642    let raf = 0;
643    let headerScrolled = false;
644    let lenis = null;
645
646    if(!reduce && supportsViewTimeline) root.classList.add("has-scroll-scenes");
647
648    const readScroll = () => host === window
649      ? window.scrollY || document.documentElement.scrollTop || 0
650      : host?.scrollTop || 0;
651
652    const maxScroll = () => host === window
653      ? document.documentElement.scrollHeight - window.innerHeight
654      : (host?.scrollHeight || 0) - (host?.clientHeight || 0);
655
656    const paint = () => {
657      raf = 0;
658      const current = readScroll();
659      const maximum = maxScroll();
660      const ratio = Math.max(0, Math.min(1, current / Math.max(1, maximum)));
661      progressLine.style.transform = `scaleX(${ratio.toFixed(4)})`;
662      if(!headerScrolled && current > 32){
663        headerScrolled = true;
664        appMain?.classList.add("is-scrolled");
665      }else if(headerScrolled && current < 5){
666        headerScrolled = false;
667        appMain?.classList.remove("is-scrolled");
668      }
669    };
670
671    const schedule = () => {
672      if(raf) return;
673      raf = requestAnimationFrame(paint);
674    };
675
676    const bind = () => {
677      const nextHost = getPrimaryScrollHost();
678      if(nextHost === host){
679        schedule();
680        return;
681      }
682      host?.removeEventListener("scroll", schedule);
683      host = nextHost;
684      host.addEventListener("scroll", schedule, { passive: true });
685      schedule();
686    };
687
688    const ensureLenisContent = () => {
689      if(!appMain) return null;
690      const existing = appMain.querySelector(":scope > .ap-lenis-content");
691      if(existing) return existing;
692      const content = document.createElement("div");
693      content.className = "ap-lenis-content";
694      while(appMain.firstChild) content.append(appMain.firstChild);
695      appMain.append(content);
696      return content;
697    };
698
699    const stopLenis = () => {
700      if(!lenis) return;
701      lenis.destroy();
702      lenis = null;
703      window.APLenis = null;
704      root.classList.remove("has-lenis-glide");
705    };
706
707    const syncLenis = () => {
708      const shouldGlide = !!(
709        appMain &&
710        desktopGlideQuery?.matches &&
711        !reducedMotionQuery?.matches &&
712        typeof window.Lenis === "function"
713      );
714      if(!shouldGlide){
715        stopLenis();
716        return;
717      }
718      if(lenis){
719        lenis.resize();
720        return;
721      }
722
723      const content = ensureLenisContent();
724      if(!content) return;
725      lenis = new window.Lenis({
726        wrapper: appMain,
727        content,
728        eventsTarget: appMain,
729        autoRaf: true,
730        smoothWheel: true,
731        syncTouch: false,
732        lerp: 0.09,
733        wheelMultiplier: 0.92,
734        overscroll: false,
735        anchors: false,
736        stopInertiaOnNavigate: true,
737        prevent: (node) => node.matches?.(
738          ".sidebar-inner, .ph-settings-panel, textarea, select, [data-lenis-prevent]"
739        ) === true,
740      });
741      lenis.on("scroll", schedule);
742      window.APLenis = lenis;
743      root.classList.add("has-lenis-glide");
744      schedule();
745    };
746
747    const syncEnvironment = () => {
748      bind();
749      syncLenis();
750      if(!reducedMotionQuery?.matches && supportsViewTimeline) root.classList.add("has-scroll-scenes");
751      else root.classList.remove("has-scroll-scenes");
752    };
753
754    const desktopQuery = window.matchMedia?.("(min-width: 1024px)");
755    desktopQuery?.addEventListener?.("change", syncEnvironment);
756    desktopGlideQuery?.addEventListener?.("change", syncEnvironment);
757    reducedMotionQuery?.addEventListener?.("change", syncEnvironment);
758    window.addEventListener("resize", schedule, { passive: true });
759    syncEnvironment();
760  }
761
762  function initCommandPalette(){
763    const palette = document.getElementById("commandPalette");
764    const panel = palette?.querySelector(".command-palette__panel");
765    const backdrop = palette?.querySelector(".command-palette__backdrop");
766    const input = document.getElementById("commandPaletteInput");
767    const resultsHost = document.getElementById("commandPaletteResults");
768    const hint = document.getElementById("commandPaletteHint");
769    const status = document.getElementById("commandPaletteStatus");
770    const triggers = Array.from(document.querySelectorAll("[data-command-trigger]"));
771    const closeButtons = Array.from(palette?.querySelectorAll("[data-command-close]") || []);
772    if(!palette || !panel || !backdrop || !input || !resultsHost || !triggers.length) return;
773    if(palette.dataset.commandReady === "true") return;
774    palette.dataset.commandReady = "true";
775
776    const RECENTS_KEY = "ap_command_recents_v1";
777    const MAX_RESULTS = 10;
778    const localeCopy = {
779      en: {
780        trigger: "Search everything",
781        dialog: "Search Akhirah Projects",
782        close: "Close search",
783        footer: { navigate: "Navigate", open: "Open", close: "Close", privacy: "Recent choices stay on this device" },
784        placeholder: "Search apps, books, articles, audio, videos, cities and tools...",
785        hint: "Type a name, topic, city, or action",
786        ready: "Everything is ready to search",
787        loading: "Adding Library items and prayer cities…",
788        count: (count) => `${count} ${count === 1 ? "result" : "results"}`,
789        emptyTitle: "Nothing found yet",
790        emptyBody: "Try an app, an article topic, a city, or a tool such as Qibla.",
791        groups: { recent: "Recent", app: "Apps", tool: "Tools", book: "Books", article: "Articles", audio: "Audio", city: "Prayer cities" },
792        types: { app: "Akhirah Projects app", tool: "Utility", book: "Library book", article: "Library article", audio: "Library audio", city: "Prayer times" },
793      },
794      ar: {
795        trigger: "البحث في كل شيء",
796        dialog: "البحث في Akhirah Projects",
797        close: "إغلاق البحث",
798        footer: { navigate: "تنقّل", open: "افتح", close: "أغلق", privacy: "تبقى اختياراتك الأخيرة على هذا الجهاز" },
799        placeholder: "ابحث عن التطبيقات والكتب والمقالات والصوتيات والمدن والأدوات…",
800        hint: "اكتب اسمًا أو موضوعًا أو مدينة أو أداة",
801        ready: "كل المحتوى جاهز للبحث",
802        loading: "جارٍ إضافة محتوى المكتبة ومدن مواقيت الصلاة…",
803        count: (count) => `${count} نتيجة`,
804        emptyTitle: "لم نعثر على نتيجة",
805        emptyBody: "جرّب اسم تطبيق أو موضوع مقال أو مدينة أو أداة مثل القبلة.",
806        groups: { recent: "الأخيرة", app: "التطبيقات", tool: "الأدوات", book: "الكتب", article: "المقالات", audio: "الصوتيات", city: "مدن مواقيت الصلاة" },
807        types: { app: "تطبيق من Akhirah Projects", tool: "أداة", book: "كتاب في المكتبة", article: "مقال في المكتبة", audio: "مادة صوتية", city: "مواقيت الصلاة" },
808      },
809      ur: {
810        trigger: "سب کچھ تلاش کریں",
811        dialog: "Akhirah Projects میں تلاش",
812        close: "تلاش بند کریں",
813        footer: { navigate: "منتقل ہوں", open: "کھولیں", close: "بند کریں", privacy: "حالیہ انتخاب اØ
813³ÛŒ ڈیوائس پر رہتے ہیں" },
814        placeholder: "ایپس، کتابوں، مضامین، آڈیو، شہروں اور ٹولز میں تلاش کریں…",
815        hint: "نام، موضوع، شہر یا ٹول لکھیں",
816        ready: "تمام مواد تلاش کے لیے تیار ہے",
817        loading: "لائبریری مواد اور نماز کے شہر شامل کیے جا رہے ہیں…",
818        count: (count) => `${count} نتائج`,
819        emptyTitle: "کوئی نتیجہ نہیں ملا",
820        emptyBody: "ایپ، مضمون کا موضوع، شہر یا قبلہ جیسے کسی ٹول کو تلاش کریں۔",
821        groups: { recent: "حالیہ", app: "ایپس", tool: "ٹولز", book: "کتابیں", article: "مضامین", audio: "آڈیو", city: "نماز کے شہر" },
822        types: { app: "Akhirah Projects ایپ", tool: "ٹول", book: "لائبریری کتاب", article: "لائبریری مضمون", audio: "لائبریری آڈیو", city: "نماز کے اوقات" },
823      },
824    };
825    const utilityDefinitions = [
826      { id: "home", url: "/", featured: true, title: { en: "Akhirah Projects home", ar: "الرئيسية", ur: "آخرت پروجیکٹس مرکز" }, description: { en: "Return to the digital home for the Ummah", ar: "العودة إلى البيت الرقمي للأمة", ur: "امت کے ڈیجیٹل گھر پر واپس جائیں" }, keywords: "home start homepage main الرئيسية مرکز گھر" },
827      { id: "qibla", url: "/qibla/", localized: true, featured: true, title: { en: "Qibla finder", ar: "محدد القبلة", ur: "قبلہ تلاش کریں" }, description: { en: "Find the Qibla direction from your location", ar: "اعرف اتجاه القبلة من موقعك", ur: "اپنے مقام سے قبلہ کی سمت معلوم کریں" }, keywords: "qibla compass direction kaaba القبلة اتجاه قبلہ سمت" },
828      { id: "tasbih", url: "/tasbih", localized: true, featured: true, title: { en: "Digital Tasbih", ar: "المسبحة الرقمية", ur: "ڈیجیٹل تسبیح" }, description: { en: "A calm counter for dhikr", ar: "عداد هادئ للذكر", ur: "ذکر کے لیے سادہ کاؤنٹر" }, keywords: "tasbih dhikr counter سبح ذکر تسبیح کاؤنٹر" },
829      { id: "dua", url: "/dua/", featured: true, title: { en: "Dua collection", ar: "مجموعة الأدعية", ur: "دعاؤں کا مجموعہ" }, description: { en: "Open beneficial supplications", ar: "افتح مجموعة من الأدعية النافعة", ur: "مفید دعائیں کھولیں" }, keywords: "dua duas supplication prayer أدعية دعاء دعا دعائیں" },
830      { id: "asma", url: "/asma-ul-husna/", title: { en: "Asma ul Husna", ar: "أسماء الله الحسنى", ur: "اسماء الحسنیٰ" }, description: { en: "Explore the beautiful names of Allah", ar: "تعرّف على أسماء الله الحسنى", ur: "اللہ تعالیٰ کے خوبصورت نام" }, keywords: "asma ul husna names Allah 99 أسماء الله الحسنى اسماء الحسنی" },
831      { id: "zakat", url: "/zakat-calculator/", title: { en: "Zakat calculator", ar: "حاسبة الزكاة", ur: "زکوٰۃ کیلکولیٹر" }, description: { en: "Calculate a practical Zakat estimate", ar: "احسب تقديرًا عمليًا للزكاة", ur: "زکوٰۃ کا عملی تخمینہ لگائیں" }, keywords: "zakat calculator nisab gold silver زكاة حاسبة زکوٰۃ کیلکولیٹر نصاب" },
832      { id: "hijri", url: "/hijri/converter/", title: { en: "Hijri date converter", ar: "محول التاريخ الهجري", ur: "ہجری تاریخ کنورٹر" }, description: { en: "Convert between Hijri and Gregorian dates", ar: "حوّل بين التاريخين الهجري والميلادي", ur: "ہجری اور عیسوی تاریخ تبدیل کریں" }, keywords: "hijri date converter calendar islamic gregorian هجري تاريخ ہجری تاریخ" },
833      { id: "install", url: "/install/", title: { en: "Install Akhirah Projects", ar: "تثبيت Akhirah Projects", ur: "Akhirah Projects انسٹال کریں" }, description: { en: "Add the site to iOS, Android, or desktop", ar: "أضف الموقع إلى iOS أو Android أو سطح المكتب", ur: "سائٹ کو iOS، Android یا ڈیسک ٹاپ پر شامل کریں" }, keywords: "install pwa app ios android desktop تثبيت تطبيق انسٹال ایپ" },
834      { id: "kawn", url: "/kawn/", featured: true, title: { en: "Explore Kawn Mindmap", ar: "استكشف Kawn Mindmap", ur: "Kawn Mindmap دریافت کریں" }, description: { en: "Visual research, references, notes, and knowledge maps", ar: "بحث بصري ومراجع وملاحظات وخرائط معرفية", ur: "بصری تحقیق، حوالہ جات، نوٹس اور علمی نقشے" }, keywords: "kawn mindmap mind map research nodes references notes خريطة ذهنية بحث مائنڈ میپ تحقیق" },
835      { id: "library", url: "/library/", featured: true, title: { en: "Knowledge library", ar: "المكتبة المعرفية", ur: "علمی لائبریری" }, description: { en: "Read books and articles, and listen to beneficial audio", ar: "اقرأ الكتب والمقالات واستمع إلى الصوتيات النافعة", ur: "کتابیں اور مضامین پڑھیں اور مفید آڈیو سنیں" }, keywords: "library books articles audio listen read knowledge مكتبة كتب مقالات صوتيات لائبریری کتابیں مضامین آڈیو" },
836      { id: "focus", url: "/focus/", title: { en: "Focus Tools for ChatGPT", ar: "أدوات التركيز لـ ChatGPT", ur: "ChatGPT کے لیے فوکس ٹولز" }, description: { en: "Tools for focused, more reliable long conversations", ar: "أدوات لمحادثات طويلة أكثر هدوءًا وموثوقية", ur: "طویل گفتگو کے لیے پرسکون اور قابل اعتماد ٹولز" }, keywords: "focus chatgpt productivity notification composer risk manager تركيز فوکس" },
837    ];
838
839    let isOpen = false;
840    let activeIndex = -1;
841    let visibleEntries = [];
842    let allEntries = [];
843    let appsPayload = null;
844    let libraryPayload = null;
845    let booksPayload = null;
846    let audioPayload = null;
847    let citiesPayload = null;
848    let loadPromise = null;
849    let restoreFocus = null;
850    let motionRevision = 0;
851    const backgroundVisibility = new Map();
852
853    const copy = () => localeCopy[currentLocale()] || localeCopy.en;
854    const localized = (value) => safeText(value?.[currentLocale()] || value?.en || value);
855    const normalizeSearch = (value) => safeText(value)
856      .normalize("NFKD")
857      .replace(/[\u0300-\u036f]/g, "")
858      .replace(/[أإآٱ]/g, "ا")
859      .replace(/ى/g, "ي")
860      .replace(/ة/g, "ه")
861      .toLocaleLowerCase()
862      .replace(/[^\p{L}\p{N}]+/gu, " ")
863      .trim();
864
865    function typeIcon(type){
866      if(type === "app") return '<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="4" width="6" height="6" rx="1.4" stroke="currentColor" stroke-width="1.5"/><rect x="14" y="4" width="6" height="6" rx="1.4" stroke="currentColor" stroke-width="1.5"/><rect x="4" y="14" width="6" height="6" rx="1.4" stroke="currentColor" stroke-width="1.5"/><rect x="14" y="14" width="6" height="6" rx="1.4" stroke="currentColor" stroke-width="1.5"/></svg>';
867      if(type === "article") return '<svg viewBox="0 0 24 24" fill="none"><path d="M6 4.5h9l3 3V20H6V4.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M15 4.5V8h3M9 11h6M9 14h6M9 17h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
868      if(type === "book") return '<svg viewBox="0 0 24 24" fill="none"><path d="M5 4.5h6.5V20H6.5A1.5 1.5 0 0 1 5 18.5v-14ZM11.5 4.5H18A1.5 1.5 0 0 1 19.5 6v12.5A1.5 1.5 0 0 1 18 20h-6.5" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>';
869      if(type === "audio") return '<svg viewBox="0 0 24 24" fill="none"><path d="M9 18V6l9-2v12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="6.5" cy="18" r="2.5" stroke="currentColor" stroke-width="1.5"/><circle cx="15.5" cy="16" r="2.5" stroke="currentColor" stroke-width="1.5"/></svg>';
870      if(type === "city") return '<svg viewBox="0 0 24 24" fill="none"><path d="M12 20s6-5.2 6-10a6 6 0 1 0-12 0c0 4.8 6 10 6 10Z" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="10" r="2" stroke="currentColor" stroke-width="1.4"/></svg>';
871      return '<svg viewBox="0 0 24 24" fill="none"><path d="M12 3.5 14 10l6.5 2-6.5 2-2 6.5-2-6.5-6.5-2 6.5-2 2-6.5Z" stroke="currentColor" stroke-width="1.45" stroke-linejoin="round"/></svg>';
872    }
873
874    function buildEntries(){
875      const locale = currentLocale();
876      const localePrefix = locale === "en" ? "" : `/${locale}`;
877      const entries = utilityDefinitions.map((item) => ({
878        id: `tool:${item.id}`,
879        type: "tool",
880        title: localized(item.title),
881        description: localized(item.description),
882        url: item.localized && locale !== "en" ? `${localePrefix}${item.url}` : item.url,
883        keywords: item.keywords,
884        featured: item.featured === true,
885      }));
886
887      let apps = getPublicRegistryApps(appsPayload || {});
888      if(!apps.length){
889        apps = [
890          { id: "prayer", name: "PrayerApp", shortName: "Prayer", canonicalPath: "/prayer", category: "islamic utility", description: "Prayer times, the next prayer, and daily Islamic rhythm." },
891          { id: "mindmap", name: "Kawn Mindmap", shortName: "MindMap", canonicalPath: "/mindmap", category: "knowledge workspace", description: "Privacy-first visual research and knowledge mapping." },
892          { id: "store", name: "Store", shortName: "Store", canonicalPath: "/store/", category: "beneficial goods", description: "Beneficial digital goods from Akhirah Projects." },
893        ];
894      }
895      apps.forEach((app) => entries.push({
896        id: `app:${app.id}`,
897        type: "app",
898        title: i18n()?.appName(app.id, app.name) || app.name,
899        description: i18n()?.appDescription(app.id, app.description) || app.description,
900        url: app.canonicalPath,
901        keywords: `${app.id} ${app.shortName} ${app.category} ${app.name}`,
902        featured: true,
903      }));
904
905      const articles = Array.isArray(libraryPayload?.articles) ? libraryPayload.articles : [];
906      articles.filter((article) => article?.status === "published" && article?.slug).forEach((article) => entries.push({
907        id: `article:${article.slug}`,
908        type: "article",
909        title: safeText(article.title),
910        description: safeText(article.description),
911        url: `/library/${encodeURIComponent(article.slug)}`,
912        keywords: `${(article.keywords || []).join(" ")} ${(article.categories || []).join(" ")}`,
913      }));
914
915      const books = Array.isArray(booksPayload?.books) ? booksPayload.books : [];
916      books.filter((book) => book?.status === "published" && book?.slug).forEach((book) => entries.push({
917        id: `book:${book.slug}`,
918        type: "book",
919        title: safeText(book.title),
920        description: safeText(book.description),
921        url: `/library/books/${encodeURIComponent(book.slug)}/`,
922        keywords: `${book.author || ""} ${book.nativeTitle || ""} ${book.language || ""} ${book.category || ""}`,
923      }));
924
925      const audio = Array.isArray(audioPayload?.audio) ? audioPayload.audio : [];
926      audio.filter((item) => item?.status === "published" && item?.slug).forEach((item) => entries.push({
927        id: `audio:${item.slug}`,
928        type: "audio",
929        title: safeText(item.title),
930        description: safeText(item.description),
931        url: `/library/audio/${encodeURIComponent(item.slug)}/`,
932        keywords: `${item.creator || ""} ${item.language || ""} ${item.kind || ""} ${item.category || ""}`,
933      }));
934
935      const cities = Array.isArray(citiesPayload) ? citiesPayload : [];
936      cities.forEach((city) => {
937        const cityName = locale === "ar" ? city.city_ar || city.city : locale === "ur" ? city.city_ur || city.city : city.city;
938        entries.push({
939          id: `city:${city.country_slug}:${city.city_slug}:${locale}`,
940          type: "city",
941          title: safeText(cityName),
942          description: safeText(city.country),
943          url: `${localePrefix}/prayer-times/${city.country_slug}/${city.city_slug}/`,
944          keywords: `${city.city} ${city.city_ar || ""} ${city.city_ur || ""} ${city.country} ${city.country_slug} prayer salah salat نماز صلاة`,
945        });
946      });
947
948      allEntries = entries.map((entry) => ({
949        ...entry,
950        searchText: normalizeSearch(`${entry.title} ${entry.description} ${entry.keywords} ${entry.url}`),
951      }));
952    }
953
954    function readRecents(){
955      try{
956        const parsed = JSON.parse(localStorage.getItem(RECENTS_KEY) || "[]");
957        return Array.isArray(parsed) ? parsed.slice(0, 5).map(String) : [];
958      }catch(_){ return []; }
959    }
960
961    function remember(entry){
962      try{
963        const next = [entry.id, ...readRecents().filter((id) => id !== entry.id)].slice(0, 5);
964        localStorage.setItem(RECENTS_KEY, JSON.stringify(next));
965      }catch(_){}
966    }
967
968    function scoreEntry(entry, query){
969      if(!query) return entry.featured ? 20 : 0;
970      const title = normalizeSearch(entry.title);
971      const description = normalizeSearch(entry.description);
972      if(title === query) return 120;
973      if(title.startsWith(query)) return 95;
974      if(title.split(" ").some((word) => word.startsWith(query))) return 82;
975      if(title.includes(query)) return 70;
976      const terms = query.split(" ").filter(Boolean);
977      if(terms.length > 1 && terms.every((term) => entry.searchText.includes(term))) return 62 + terms.length;
978      if(entry.searchText.includes(query)) return description.includes(query) ? 48 : 38;
979      return -1;
980    }
981
982    function setActive(index, scroll = true){
983      const options = Array.from(resultsHost.querySelectorAll("[role='option']"));
984      if(!options.length){
985        activeIndex = -1;
986        input.removeAttribute("aria-activedescendant");
987        return;
988      }
989      activeIndex = (index + options.length) % options.length;
990      options.forEach((option, optionIndex) => option.setAttribute("aria-selected", String(optionIndex === activeIndex)));
991      const active = options[activeIndex];
992      input.setAttribute("aria-activedescendant", active.id);
993      if(scroll) active.scrollIntoView({ block: "nearest" });
994    }
995
996    function render(){
997      const query = normalizeSearch(input.value);
998      let ranked;
999      let recentIds = [];
1000      if(query){
1001        ranked = allEntries
1002          .map((entry) => ({ entry, score: scoreEntry(entry, query) }))
1003          .filter((item) => item.score >= 0)
1004          .sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title))
1005          .slice(0, MAX_RESULTS)
1006          .map((item) => item.entry);
1007        const resultTypeOrder = [...new Set(ranked.map((entry) => entry.type))];
1008        ranked = resultTypeOrder.flatMap((type) => ranked.filter((entry) => entry.type === type));
1009      }else{
1010        recentIds = readRecents();
1011        const recentEntries = recentIds.map((id) => allEntries.find((entry) => entry.id === id)).filter(Boolean);
1012        const featured = allEntries.filter((entry) => entry.featured && !recentIds.includes(entry.id));
1013        ranked = [...recentEntries, ...featured].slice(0, MAX_RESULTS);
1014      }
1015      visibleEntries = ranked;
1016
1017      if(!ranked.length){
1018        resultsHost.innerHTML = `<div class="command-palette__empty"><span class="command-palette__empty-mark" aria-hidden="true">Ø£</span><strong>${escapeHtml(copy().emptyTitle)}</strong><span>${escapeHtml(copy().emptyBody)}</span></div>`;
1019        status.textContent = copy().count(0);
1020        setActive(-1);
1021        return;
1022      }
1023
1024      let previousGroup = "";
1025      resultsHost.innerHTML = ranked.map((entry, index) => {
1026        const isRecent = !query && recentIds.includes(entry.id);
1027        const group = isRecent ? "recent" : entry.type;
1028        const groupMarkup = group !== previousGroup
1029          ? `<div class="command-palette__group-label" role="presentation">${escapeHtml(copy().groups[group] || group)}</div>`
1030          : "";
1031        previousGroup = group;
1032        return `${groupMarkup}<button class="command-palette__option" id="command-option-${index}" type="button" role="option" aria-selected="false" data-command-index="${index}"><span class="command-palette__type-icon" aria-hidden="true">${typeIcon(entry.type)}</span><span class="command-palette__copy"><span class="command-palette__title">${escapeHtml(entry.title)}</span><span class="command-palette__description">${escapeHtml(entry.description || copy().types[entry.type])}</span></span><span class="command-palette__route">${escapeHtml(entry.url)}</span></button>`;
1033      }).join("");
1034      status.textContent = copy().count(ranked.length);
1035      setActive(0, false);
1036    }
1037
1038    function applyLocale(){
1039      const currentCopy = copy();
1040      const shortcut = /Mac|iPhone|iPad/i.test(navigator.platform || "") ? "⌘K" : "Ctrl K";
1041      triggers.forEach((trigger) => {
1042        trigger.setAttribute("aria-label", currentCopy.dialog);
1043        const label = trigger.querySelector(".sb-command__text");
1044        const key = trigger.querySelector(".sb-command__key");
1045        if(label) label.textContent = currentCopy.trigger;
1046        if(key) key.textContent = shortcut;
1047      });
1048      document.getElementById("commandPaletteTitle").textContent = currentCopy.dialog;
1049      input.placeholder = currentCopy.placeholder;
1050      hint.textContent = currentCopy.hint;
1051      resultsHost.setAttribute("aria-label", currentCopy.dialog);
1052      closeButtons.forEach((button) => button.setAttribute("aria-label", currentCopy.close));
1053      palette.querySelectorAll("[data-command-footer]").forEach((element) => {
1054        const key = element.getAttribute("data-command-footer");
1055        if(currentCopy.footer[key]) element.textContent = currentCopy.footer[key];
1056      });
1057      buildEntries();
1058      render();
1059    }
1060
1061    async function loadIndex(){
1062      if(loadPromise) return loadPromise;
1063      status.textContent = copy().loading;
1064      loadPromise = Promise.allSettled([
1065        fetchWithTimeout(APPS_REGISTRY, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : null),
1066        fetchWithTimeout(LIBRARY_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : null),
1067        fetchWithTimeout(BOOKS_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : null),
1068        fetchWithTimeout(AUDIO_INDEX, { cache: "no-store" }, 5000).then((response) => response.ok ? response.json() : null),
1069        fetchWithTimeout(PRAYER_CITIES_INDEX, { cache: "no-store" }, 6000).then((response) => response.ok ? response.json() : null),
1070      ]).then((settled) => {
1071        appsPayload = settled[0].status === "fulfilled" ? settled[0].value : null;
1072        libraryPayload = settled[1].status === "fulfilled" ? settled[1].value : null;
1073        booksPayload = settled[2].status === "fulfilled" ? settled[2].value : null;
1074        audioPayload = settled[3].status === "fulfilled" ? settled[3].value : null;
1075        citiesPayload = settled[4].status === "fulfilled" ? settled[4].value : null;
1076        buildEntries();
1077        if(isOpen) render();
1078        status.textContent = copy().ready;
1079      }).catch(() => {
1080        status.textContent = "";
1081      });
1082      return loadPromise;
1083    }
1084
1085    function cleanMotionStyles(){
1086      [palette, backdrop, panel].forEach((element) => {
1087        element.style.removeProperty("opacity");
1088        element.style.removeProperty("transform");
1089      });
1090    }
1091
1092    function openPalette(trigger){
1093      if(isOpen) return;
1094      isOpen = true;
1095      const revision = ++motionRevision;
1096      restoreFocus = trigger instanceof HTMLElement ? trigger : document.activeElement;
1097      palette.hidden = false;
1098      palette.setAttribute("aria-hidden", "false");
1099      document.documentElement.classList.add("command-palette-open");
1100      [document.querySelector(".app-shell"), document.querySelector(".mobile-bar"), document.querySelector(".skip")]
1101        .filter(Boolean)
1102        .forEach((element) => {
1103          backgroundVisibility.set(element, element.getAttribute("aria-hidden"));
1104          element.setAttribute("aria-hidden", "true");
1105          element.inert = true;
1106        });
1107      window.APLenis?.stop?.();
1108      document.getElementById("sidebar")?.classList.remove("is-open");
1109      document.getElementById("scrim")?.classList.remove("is-open");
1110      document.body.classList.remove("mobile-nav-open");
1111      input.value = "";
1112      render();
1113      input.focus({ preventScroll: true });
1114      loadIndex();
1115
1116      const animate = window.APMotion?.animate;
1117      const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
1118      if(typeof animate !== "function" || reduce){
1119        cleanMotionStyles();
1120        return;
1121      }
1122      palette.style.opacity = "1";
1123      backdrop.style.opacity = "0";
1124      panel.style.opacity = "0";
1125      panel.style.transform = "translateY(-18px) scale(.972)";
1126      animate(backdrop, { opacity: 1 }, { duration: .24, ease: "easeOut" });
1127      const entrance = animate(panel, { opacity: 1, transform: "translateY(0px) scale(1)" }, { duration: .42, ease: [.16, 1, .3, 1] });
1128      Promise.resolve(entrance).then(() => { if(isOpen && revision === motionRevision) cleanMotionStyles(); });
1129    }
1130
1131    function finishClose(revision){
1132      if(isOpen || revision !== motionRevision) return;
1133      palette.hidden = true;
1134      palette.setAttribute("aria-hidden", "true");
1135      document.documentElement.classList.remove("command-palette-open");
1136      backgroundVisibility.forEach((previousAria, element) => {
1137        if(previousAria == null) element.removeAttribute("aria-hidden");
1138        else element.setAttribute("aria-hidden", previousAria);
1139        element.inert = false;
1140      });
1141      backgroundVisibility.clear();
1142      window.APLenis?.start?.();
1143      cleanMotionStyles();
1144      const target = restoreFocus;
1145      restoreFocus = null;
1146      if(target instanceof HTMLElement && target.isConnected) target.focus({ preventScroll: true });
1147    }
1148
1149    function closePalette(){
1150      if(!isOpen) return;
1151      isOpen = false;
1152      const revision = ++motionRevision;
1153      const animate = window.APMotion?.animate;
1154      const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
1155      if(typeof animate !== "function" || reduce){
1156        finishClose(revision);
1157        return;
1158      }
1159      animate(backdrop, { opacity: 0 }, { duration: .18, ease: "easeIn" });
1160      const exitAnimation = animate(panel, { opacity: 0, transform: "translateY(-9px) scale(.985)" }, { duration: .2, ease: [.4, 0, 1, 1] });
1161      Promise.resolve(exitAnimation).then(() => finishClose(revision));
1162      window.setTimeout(() => finishClose(revision), 280);
1163    }
1164
1165    function openEntry(entry){
1166      if(!entry) return;
1167      remember(entry);
1168      const destination = entry.url;
1169      closePalette();
1170      window.setTimeout(() => window.location.assign(destination), 40);
1171    }
1172
1173    triggers.forEach((trigger) => trigger.addEventListener("click", () => openPalette(trigger)));
1174    closeButtons.forEach((button) => button.addEventListener("click", closePalette));
1175    input.addEventListener("input", render);
1176    input.addEventListener("keydown", (event) => {
1177      if(event.key === "ArrowDown"){
1178        event.preventDefault();
1179        setActive(activeIndex + 1);
1180      }else if(event.key === "ArrowUp"){
1181        event.preventDefault();
1182        setActive(activeIndex - 1);
1183      }else if(event.key === "Home"){
1184        event.preventDefault();
1185        setActive(0);
1186      }else if(event.key === "End"){
1187        event.preventDefault();
1188        setActive(visibleEntries.length - 1);
1189      }else if(event.key === "Enter"){
1190        event.preventDefault();
1191        openEntry(visibleEntries[activeIndex]);
1192      }
1193    });
1194    resultsHost.addEventListener("click", (event) => {
1195      const option = event.target.closest("[data-command-index]");
1196      if(!option) return;
1197      openEntry(visibleEntries[Number(option.dataset.commandIndex)]);
1198    });
1199    resultsHost.addEventListener("pointermove", (event) => {
1200      const option = event.target.closest("[data-command-index]");
1201      if(option) setActive(Number(option.dataset.commandIndex), false);
1202    }, { passive: true });
1203    window.addEventListener("keydown", (event) => {
1204      const target = event.target;
1205      const editable = target instanceof HTMLElement && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName));
1206      const commandKey = (event.metaKey || event.ctrlKey) && !event.altKey && (event.code === "KeyK" || event.key.toLowerCase() === "k");
1207      const slashKey = event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey && !editable;
1208      if(commandKey || slashKey){
1209        event.preventDefault();
1210        event.stopPropagation();
1211        if(isOpen) input.focus();
1212        else openPalette(target instanceof HTMLElement ? target : null);
1213        return;
1214      }
1215      if(!isOpen) return;
1216      if(event.key === "Escape"){
1217        event.preventDefault();
1218        closePalette();
1219      }else if(event.key === "Tab"){
1220        const focusables = [input, ...closeButtons.filter((button) => button.tabIndex !== -1)];
1221        const current = focusables.indexOf(document.activeElement);
1222        const next = event.shiftKey ? (current - 1 + focusables.length) % focusables.length : (current + 1) % focusables.length;
1223        event.preventDefault();
1224        focusables[next].focus();
1225      }
1226    }, { capture: true });
1227    window.addEventListener("ap:languagechange", applyLocale);
1228    window.APCommandPalette = { open: () => openPalette(null), close: closePalette, rebuild: applyLocale };
1229
1230    buildEntries();
1231    applyLocale();
1232    scheduleIdle(loadIndex, 1400);
1233  }
1234
1235  function boot(){
1236    try{
1237      // Reveal and boot-state release are the fail-open shell. Keep them ahead
1238      // of optional widgets so one enhancement cannot leave the page blank.
1239      initReveal();
1240      releaseBootState();
1241      applyFeatureFlags();
1242      initAppRegistry().catch(() => {});
1243      renderLibraryShowcase().catch(() => {});
1244      initUpdatesSection().catch(() => {});
1245      initHeroGlow();
1246      initHeroLauncher();
1247      initFaqMotion();
1248      initCommandPalette();
1249      initCardFx();
1250      initScrollExperience();
1251      window.addEventListener("ap:languagechange", () => {
1252        renderLibraryShowcase().catch(() => {});
1253        renderUpdatesTicker();
1254      });
1255    }catch(err){
1256      document.documentElement.classList.remove("is-booting", "reveal-ready");
1257      document.querySelectorAll("[data-reveal]").forEach((element) => element.classList.add("is-visible"));
1258      console.error("[landing] boot failed", err);
1259    }
1260  }
1261
1262
1263  function initHeroLauncher(){
1264    const launcher = document.querySelector("[data-hero-launcher]");
1265    if(!launcher || launcher.dataset.launcherReady === "true") return;
1266    launcher.dataset.launcherReady = "true";
1267
1268    const trigger = launcher.querySelector(".hero-launcher__trigger");
1269    const menu = launcher.querySelector(".hero-launcher__menu");
1270    const plus = launcher.querySelector(".hero-launcher__plus");
1271    const caption = launcher.querySelector(".hero-launcher__caption");
1272    const items = Array.from(launcher.querySelectorAll(".hero-launcher__item"));
1273    if(!trigger || !menu || !plus || !items.length) return;
1274
1275    const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1276    const motionAnimate = window.APMotion && window.APMotion.animate;
1277    const supportsMotionPath = !window.CSS || !CSS.supports || CSS.supports("offset-path", 'path("M 0 0 L 1 1")');
1278    const canAnimate = typeof motionAnimate === "function" && !reduceMotion && supportsMotionPath;
1279    let isOpen = false;
1280    let focusTimer = 0;
1281
1282    if(canAnimate){
1283      launcher.classList.add("has-motion");
1284      plus.style.transform = "rotate(0deg)";
1285      items.forEach((item) => {
1286        item.style.offsetDistance = "0%";
1287        item.style.opacity = "0";
1288        item.style.transform = "scale(.56)";
1289      });
1290    }
1291
1292    const updateTriggerLabel = () => {
1293      const label = isOpen
1294        ? (trigger.dataset.closeLabel || "Close quick access")
1295        : (trigger.dataset.openLabel || "Open quick access");
1296      trigger.setAttribute("aria-label", label);
1297      trigger.setAttribute("aria-expanded", String(isOpen));
1298      if(caption){
1299        caption.textContent = isOpen
1300          ? (trigger.dataset.closeCaption || "Close")
1301          : (trigger.dataset.openCaption || "Explore");
1302      }
1303    };
1304
1305    const setMenuInteractive = (interactive) => {
1306      menu.setAttribute("aria-hidden", String(!interactive));
1307      items.forEach((item, index) => {
1308        item.tabIndex = interactive && index === 0 ? 0 : -1;
1309      });
1310    };
1311
1312    const focusItem = (index) => {
1313      items.forEach((item, itemIndex) => {
1314        item.tabIndex = itemIndex === index ? 0 : -1;
1315      });
1316      items[index].focus();
1317    };
1318
1319    const animateMenu = (nextOpen) => {
1320      if(!canAnimate) return;
1321      motionAnimate(plus, {
1322        transform: nextOpen ? "rotate(45deg)" : "rotate(0deg)"
1323      }, {
1324        duration: .36,
1325        ease: [.22, 1, .36, 1]
1326      });
1327
1328      items.forEach((item, index) => {
1329        const order = nextOpen ? index : items.length - index - 1;
1330        motionAnimate(item, {
1331          offsetDistance: nextOpen ? "100%" : "0%",
1332          opacity: nextOpen ? 1 : 0,
1333          transform: nextOpen ? "scale(1)" : "scale(.56)"
1334        }, {
1335          duration: nextOpen ? .52 : .38,
1336          delay: order * .045,
1337          ease: [.16, 1, .3, 1]
1338        });
1339      });
1340    };
1341
1342    const setOpen = (nextOpen, { focusFirst = false, restoreFocus = false } = {}) => {
1343      if(isOpen === nextOpen) return;
1344      isOpen = nextOpen;
1345      window.clearTimeout(focusTimer);
1346      launcher.classList.toggle("is-open", isOpen);
1347      setMenuInteractive(isOpen);
1348      updateTriggerLabel();
1349      animateMenu(isOpen);
1350
1351      if(isOpen && focusFirst){
1352        focusTimer = window.setTimeout(() => focusItem(0), reduceMotion ? 0 : 150);
1353      }else if(!isOpen && restoreFocus){
1354        trigger.focus();
1355      }
1356    };
1357
1358    trigger.addEventListener("click", (event) => {
1359      setOpen(!isOpen, { focusFirst: !isOpen && event.detail === 0 });
1360    });
1361
1362    items.forEach((item) => {
1363      item.addEventListener("click", () => setOpen(false));
1364    });
1365
1366    document.addEventListener("pointerdown", (event) => {
1367      if(isOpen && !launcher.contains(event.target)) setOpen(false);
1368    }, { passive: true });
1369
1370    document.addEventListener("keydown", (event) => {
1371      if(!isOpen) return;
1372      if(event.key === "Escape"){
1373        event.preventDefault();
1374        setOpen(false, { restoreFocus: true });
1375        return;
1376      }
1377
1378      const currentIndex = items.indexOf(document.activeElement);
1379      if(currentIndex < 0) return;
1380      let nextIndex = currentIndex;
1381      if(event.key === "ArrowRight" || event.key === "ArrowDown") nextIndex = (currentIndex + 1) % items.length;
1382      else if(event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = (currentIndex - 1 + items.length) % items.length;
1383      else if(event.key === "Home") nextIndex = 0;
1384      else if(event.key === "End") nextIndex = items.length - 1;
1385      else return;
1386      event.preventDefault();
1387      focusItem(nextIndex);
1388    });
1389
1390    window.addEventListener("ap:languagechange", updateTriggerLabel);
1391    setMenuInteractive(false);
1392    updateTriggerLabel();
1393  }
1394
1395
1396  function initFaqMotion(){
1397    const accordion = document.querySelector("[data-motion-accordion]");
1398    if(!accordion || accordion.dataset.motionReady === "true") return;
1399    accordion.dataset.motionReady = "true";
1400
1401    const detailsItems = Array.from(accordion.querySelectorAll("details"));
1402    const meterCurrent = document.querySelector("[data-faq-meter-current]");
1403    const meterProgress = document.querySelector("[data-faq-meter-progress]");
1404    const motionAnimate = window.APMotion && window.APMotion.animate;
1405    const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1406    if(typeof motionAnimate !== "function" || reduceMotion || !detailsItems.length) return;
1407
1408    const states = new WeakMap();
1409    const animations = new WeakMap();
1410    const contentAnimations = new WeakMap();
1411    let meterRevision = 0;
1412    accordion.classList.add("has-motion");
1413
1414    const stopAnimation = (details) => {
1415      const current = animations.get(details);
1416      if(current && typeof current.stop === "function") current.stop();
1417      animations.delete(details);
1418    };
1419
1420    const stopContentAnimations = (answer) => {
1421      Array.from(answer.children).forEach((child) => {
1422        const current = contentAnimations.get(child);
1423        if(current && typeof current.stop === "function") current.stop();
1424        contentAnimations.delete(child);
1425      });
1426    };
1427
1428    const clearContentStyles = (answer) => {
1429      stopContentAnimations(answer);
1430      Array.from(answer.children).forEach((child) => {
1431        child.style.removeProperty("opacity");
1432        child.style.removeProperty("transform");
1433        child.style.removeProperty("will-change");
1434      });
1435    };
1436
1437    const animateContent = (answer, opening, fromHidden = false) => {
1438      stopContentAnimations(answer);
1439      Array.from(answer.children).forEach((child, index) => {
1440        child.style.willChange = "transform, opacity";
1441        if(opening && fromHidden){
1442          child.style.opacity = "0";
1443          child.style.transform = "translateY(7px)";
1444        }
1445        const animation = motionAnimate(child, {
1446          opacity: opening ? 1 : 0,
1447          transform: opening ? "translateY(0px)" : "translateY(-5px)"
1448        }, {
1449          duration: opening ? .28 : .18,
1450          delay: opening ? .07 + index * .035 : 0,
1451          ease: opening ? [.16, 1, .3, 1] : [.4, 0, .2, 1]
1452        });
1453        contentAnimations.set(child, animation);
1454      });
1455    };
1456
1457    const updateMeter = (details, immediate = false) => {
1458      const index = Math.max(0, detailsItems.indexOf(details));
1459      const label = String(index + 1).padStart(2, "0");
1460      const progress = (index + 1) / detailsItems.length;
1461      const revision = ++meterRevision;
1462
1463      if(meterProgress){
1464        if(immediate) meterProgress.style.transform = `scaleX(${progress})`;
1465        else motionAnimate(meterProgress, { transform: `scaleX(${progress})` }, { duration: .48, ease: [.16, 1, .3, 1] });
1466      }
1467      if(!meterCurrent) return;
1468      if(immediate){
1469        meterCurrent.textContent = label;
1470        return;
1471      }
1472
1473      const exitAnimation = motionAnimate(meterCurrent, {
1474        opacity: 0,
1475        transform: "translateY(-5px)"
1476      }, {
1477        duration: .12,
1478        ease: "easeOut"
1479      });
1480      Promise.resolve(exitAnimation).then(() => {
1481        if(revision !== meterRevision) return;
1482        meterCurrent.textContent = label;
1483        meterCurrent.style.transform = "translateY(6px)";
1484        motionAnimate(meterCurrent, {
1485          opacity: 1,
1486          transform: "translateY(0px)"
1487        }, {
1488          duration: .24,
1489          ease: [.16, 1, .3, 1]
1490        });
1491      });
1492    };
1493
1494    const setExpanded = (details, expanded) => {
1495      const summary = details.querySelector("summary");
1496      if(summary) summary.setAttribute("aria-expanded", String(expanded));
1497    };
1498
1499    const clearAnswerStyles = (answer) => {
1500      answer.style.removeProperty("height");
1501      answer.style.removeProperty("opacity");
1502      answer.style.removeProperty("overflow");
1503      answer.style.removeProperty("will-change");
1504      clearContentStyles(answer);
1505    };
1506
1507    const closeItem = (details) => {
1508      const answer = details.querySelector(".faq-answer");
1509      if(!answer || states.get(details) === "closed") return;
1510
1511      stopAnimation(details);
1512      states.set(details, "closing");
1513      details.classList.add("is-closing");
1514      setExpanded(details, false);
1515      answer.style.overflow = "hidden";
1516      answer.style.willChange = "height, opacity";
1517      answer.style.height = `${answer.getBoundingClientRect().height}px`;
1518      animateContent(answer, false);
1519
1520      const animation = motionAnimate(answer, {
1521        height: "0px",
1522        opacity: 0
1523      }, {
1524        duration: .3,
1525        ease: [.4, 0, .2, 1]
1526      });
1527      animations.set(details, animation);
1528
1529      Promise.resolve(animation).then(() => {
1530        if(states.get(details) !== "closing") return;
1531        details.open = false;
1532        details.classList.remove("is-closing");
1533        clearAnswerStyles(answer);
1534        animations.delete(details);
1535        states.set(details, "closed");
1536      });
1537    };
1538
1539    const openItem = (details) => {
1540      const answer = details.querySelector(".faq-answer");
1541      if(!answer || states.get(details) === "open") return;
1542
1543      detailsItems.forEach((other) => {
1544        if(other !== details && (other.open || states.get(other) === "opening")) closeItem(other);
1545      });
1546
1547      const wasClosed = !details.open;
1548      stopAnimation(details);
1549      details.open = true;
1550      details.classList.remove("is-closing");
1551      states.set(details, "opening");
1552      setExpanded(details, true);
1553      updateMeter(details);
1554      answer.style.overflow = "hidden";
1555      answer.style.willChange = "height, opacity";
1556      if(wasClosed){
1557        answer.style.height = "0px";
1558        answer.style.opacity = "0";
1559      }
1560      const targetHeight = answer.scrollHeight;
1561      animateContent(answer, true, wasClosed);
1562
1563      const animation = motionAnimate(answer, {
1564        height: `${targetHeight}px`,
1565        opacity: 1
1566      }, {
1567        duration: .42,
1568        ease: [.16, 1, .3, 1]
1569      });
1570      animations.set(details, animation);
1571
1572      Promise.resolve(animation).then(() => {
1573        if(states.get(details) !== "opening") return;
1574        clearAnswerStyles(answer);
1575        animations.delete(details);
1576        states.set(details, "open");
1577      });
1578    };
1579
1580    detailsItems.forEach((details) => {
1581      const summary = details.querySelector("summary");
1582      states.set(details, details.open ? "open" : "closed");
1583      setExpanded(details, details.open);
1584      if(!summary) return;
1585      summary.addEventListener("click", (event) => {
1586        event.preventDefault();
1587        const state = states.get(details);
1588        if(state === "open" || state === "opening") closeItem(details);
1589        else openItem(details);
1590      });
1591    });
1592    updateMeter(detailsItems.find((details) => details.open) || detailsItems[0], true);
1593  }
1594
1595
1596  function initHeroGlow(){
1597    const hero = document.querySelector(".hero");
1598    if(!hero) return;
1599
1600    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1601    const isMobile = window.matchMedia && window.matchMedia("(max-width: 920px)").matches;
1602    if(reduce) return;
1603
1604    // pointer-driven glow (premium but lightweight)
1605    let raf = 0;
1606    let lastX = 0.5, lastY = 0.22;
1607
1608    function apply(){
1609      raf = 0;
1610      const x = Math.max(0, Math.min(1, lastX));
1611      const y = Math.max(0, Math.min(1, lastY));
1612      hero.style.setProperty("--glow-x", (x * 100).toFixed(2) + "%");
1613      hero.style.setProperty("--glow-y", (y * 100).toFixed(2) + "%");
1614      // secondary glow counter-moves slightly for depth
1615      hero.style.setProperty("--glow2-x", ((1 - x) * 100).toFixed(2) + "%");
1616      hero.style.setProperty("--glow2-y", (Math.max(0.18, Math.min(0.42, y + 0.10)) * 100).toFixed(2) + "%");
1617    }
1618
1619    function schedule(){
1620      if(raf) return;
1621      raf = requestAnimationFrame(apply);
1622    }
1623
1624    hero.addEventListener("pointermove", (e) => {
1625      const r = hero.getBoundingClientRect();
1626      if(!r.width || !r.height) return;
1627      lastX = (e.clientX - r.left) / r.width;
1628      lastY = (e.clientY - r.top) / r.height;
1629      schedule();
1630    }, { passive: true });
1631
1632    hero.addEventListener("pointerleave", () => {
1633      // gently return towards center
1634      lastX = 0.5;
1635      lastY = 0.22;
1636      schedule();
1637    }, { passive: true });
1638  }
1639
1640  function initCardFx(){
1641    const cards = Array.from(document.querySelectorAll(".ap-prayer-card"));
1642    if(!cards.length) return;
1643
1644    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1645    const coarse = window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
1646    if(reduce || coarse) return;
1647
1648    cards.forEach((card) => {
1649      let raf = 0;
1650      let tiltX = 0;
1651      let tiltY = 0;
1652      let spotX = 50;
1653      let spotY = 42;
1654
1655      const apply = () => {
1656        raf = 0;
1657        card.style.setProperty("--tilt-x", `${tiltX.toFixed(2)}deg`);
1658        card.style.setProperty("--tilt-y", `${tiltY.toFixed(2)}deg`);
1659        card.style.setProperty("--spot-x", `${spotX.toFixed(2)}%`);
1660        card.style.setProperty("--spot-y", `${spotY.toFixed(2)}%`);
1661      };
1662
1663      const schedule = () => {
1664        if(raf) return;
1665        raf = requestAnimationFrame(apply);
1666      };
1667
1668      card.addEventListener("pointermove", (e) => {
1669        const rect = card.getBoundingClientRect();
1670        if(!rect.width || !rect.height) return;
1671
1672        const nx = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
1673        const ny = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
1674
1675        tiltX = (0.5 - ny) * 5.6;
1676        tiltY = (nx - 0.5) * 6.8;
1677        spotX = nx * 100;
1678        spotY = ny * 100;
1679        card.style.setProperty("--fx-o", "1");
1680        schedule();
1681      }, { passive: true });
1682
1683      card.addEventListener("pointerenter", () => {
1684        card.style.setProperty("--fx-o", "1");
1685      }, { passive: true });
1686
1687      card.addEventListener("pointerleave", () => {
1688        tiltX = 0;
1689        tiltY = 0;
1690        spotX = 50;
1691        spotY = 42;
1692        card.style.setProperty("--fx-o", ".62");
1693        schedule();
1694      }, { passive: true });
1695    });
1696  }
1697
1698  function initReveal(){
1699    const els = Array.from(document.querySelectorAll("[data-reveal]:not(.reveal)"));
1700    if(!els.length) return;
1701
1702    const root = document.documentElement;
1703    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1704    const motionAnimate = window.APMotion?.animate;
1705    const canUseMotion = typeof motionAnimate === "function" && !reduce;
1706    const groupPositions = new Map();
1707
1708    els.forEach((el) => {
1709      const group = el.closest(".hero-copy, .trust-grid, .app-registry-grid, .library-feature-grid, .contact-shell, .footer-grid") || el.parentElement;
1710      const position = groupPositions.get(group) || 0;
1711      groupPositions.set(group, position + 1);
1712      const delay = Math.min(position * (el.closest(".hero") ? 54 : 46), 184);
1713
1714      el.classList.add("reveal");
1715      el.style.setProperty("--reveal-delay", `${delay}ms`);
1716
1717      const usesScrollScrub = root.classList.contains("has-scroll-scenes") && !el.closest("#top");
1718      if(usesScrollScrub){
1719        el.classList.add("is-visible", "scroll-scrub-reveal");
1720      }
1721
1722      if(canUseMotion && !usesScrollScrub && !el.matches("[data-letter-cascade]")){
1723        const direction = el.getAttribute("data-reveal");
1724        const initialTransform = direction === "left"
1725          ? "translate3d(-18px,0,0)"
1726          : direction === "right"
1727            ? "translate3d(18px,0,0)"
1728            : direction === "scale"
1729              ? "translate3d(0,8px,0) scale(.982)"
1730              : "translate3d(0,18px,0) scale(.992)";
1731        el.classList.add("motion-reveal");
1732        el.style.opacity = "0";
1733        el.style.transform = initialTransform;
1734      }
1735    });
1736
1737    // Only opt into pre-reveal hiding after the runtime has prepared every
1738    // target. With JavaScript blocked or interrupted, content stays visible.
1739    root.classList.add("reveal-ready");
1740
1741    const show = (el) => {
1742      if(el.classList.contains("is-visible")) return;
1743      el.classList.add("is-visible");
1744      if(!el.classList.contains("motion-reveal")) return;
1745
1746      const delay = (parseFloat(el.style.getPropertyValue("--reveal-delay")) || 0) / 1000;
1747      const animation = motionAnimate(el, {
1748        opacity: 1,
1749        transform: "translate3d(0,0,0) scale(1)"
1750      }, {
1751        duration: 0.72,
1752        delay,
1753        ease: [0.16, 1, 0.3, 1]
1754      });
1755
1756      Promise.resolve(animation).then(() => {
1757        el.style.removeProperty("opacity");
1758        el.style.removeProperty("transform");
1759        el.classList.remove("motion-reveal");
1760      }).catch(() => {});
1761    };
1762
1763    if(reduce){
1764      els.forEach(show);
1765      return;
1766    }
1767
1768    const host = getPrimaryScrollHost();
1769    const rootRect = host === window
1770      ? { top: 0, bottom: window.innerHeight }
1771      : host.getBoundingClientRect();
1772    const initialVisible = [];
1773
1774    const io = new IntersectionObserver((entries, obs) => {
1775      for(const entry of entries){
1776        if(entry.isIntersecting){
1777          show(entry.target);
1778          obs.unobserve(entry.target);
1779        }
1780      }
1781    }, {
1782      root: host === window ? null : host,
1783      threshold: 0.12,
1784      rootMargin: "0px 0px -6% 0px"
1785    });
1786
1787    els.forEach((el) => {
1788      if(el.classList.contains("scroll-scrub-reveal")) return;
1789      const rect = el.getBoundingClientRect();
1790      const isInitiallyVisible = rect.bottom > rootRect.top + 28 && rect.top < rootRect.bottom - 28;
1791      if(isInitiallyVisible){
1792        initialVisible.push(el);
1793        return;
1794      }
1795      io.observe(el);
1796    });
1797
1798    requestAnimationFrame(() => initialVisible.forEach(show));
1799  }
1800
1801document.addEventListener("DOMContentLoaded", boot);
1802})();
1803
1804/* ===== Spiritual Atmosphere Layer (v1.5.9) =====
1805   Calm Islamic minimal: subtle ambient light drift behind the hero & page.
1806   - Uses CSS variables on :root: --atm-x, --atm-y, --atm-s, --atm-o
1807   - Respects prefers-reduced-motion
1808*/
1809(() => {
1810  try {
1811    function phCurrentLocale(){
1812      return (window.AP_I18N && typeof window.AP_I18N.current === "function" ? window.AP_I18N.current() : "en") || "en";
1813    }
1814    const root = document.documentElement;
1815    const ambientField = document.querySelector(".ambient-field");
1816    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1817    const isMobile = window.matchMedia && window.matchMedia("(max-width: 920px)").matches;
1818    const isCoarse = window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
1819    const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
1820
1821    // If user prefers reduced motion, keep it calm and static.
1822    if (reduce) {
1823      root.style.setProperty("--atm-o", "0.7");
1824      return;
1825    }
1826
1827    // On mobile and coarse pointers, keep the background calm and static.
1828    if (isMobile || isCoarse) {
1829      root.style.setProperty("--atm-o", "0.75");
1830      root.style.setProperty("--atm-x", "0px");
1831      root.style.setProperty("--atm-y", "0px");
1832      root.style.setProperty("--atm-s", "0.18");
1833      root.style.setProperty("--atm-scale", "1.006");
1834      return;
1835    }
1836
1837    // Motion Mini powers the long-form ambient drift with native Web
1838    // Animations. The page remains fully usable and visually complete if the
1839    // optional vendor bundle is unavailable.
1840    const motionAnimate = window.APMotion && window.APMotion.animate;
1841    const ambientAnimations = [];
1842    if (ambientField && typeof motionAnimate === "function") {
1843      const plans = [
1844        [".ambient-field__orb--gold", [
1845          "translate3d(-5%,-3%,0) scale(.94)",
1846          "translate3d(7%,7%,0) scale(1.08)",
1847          "translate3d(-1%,11%,0) scale(1.01)",
1848        ], 26],
1849        [".ambient-field__orb--emerald", [
1850          "translate3d(-4%,6%,0) scale(1.02)",
1851          "translate3d(10%,-5%,0) scale(.93)",
1852          "translate3d(15%,8%,0) scale(1.07)",
1853        ], 32],
1854        [".ambient-field__orb--sky", [
1855          "translate3d(5%,9%,0) scale(.96)",
1856          "translate3d(-10%,-4%,0) scale(1.08)",
1857          "translate3d(-4%,7%,0) scale(1)",
1858        ], 29],
1859      ];
1860
1861      plans.forEach(([selector, transforms, duration]) => {
1862        const orb = ambientField.querySelector(selector);
1863        if (!orb) return;
1864        ambientAnimations.push(motionAnimate(
1865          orb,
1866          { transform: transforms },
1867          {
1868            duration,
1869            ease: "easeInOut",
1870            times: [0, 0.54, 1],
1871            repeat: Infinity,
1872            repeatType: "reverse",
1873          }
1874        ));
1875      });
1876
1877      if (ambientAnimations.length) ambientField.classList.add("is-motion-ready");
1878
1879      const syncAmbientPlayback = () => {
1880        ambientField.classList.toggle("is-paused", document.hidden);
1881        ambientAnimations.forEach((animation) => {
1882          if (document.hidden) animation.pause();
1883          else animation.play();
1884        });
1885      };
1886      document.addEventListener("visibilitychange", syncAmbientPlayback, { passive: true });
1887      syncAmbientPlayback();
1888    }
1889
1890    let targetX = 0, targetY = 0;
1891    let curX = 0, curY = 0;
1892    let raf = 0;
1893
1894    const applyAtmosphere = () => {
1895      root.style.setProperty("--atm-x", `${curX.toFixed(2)}px`);
1896      root.style.setProperty("--atm-y", `${curY.toFixed(2)}px`);
1897    };
1898
1899    const settle = () => {
1900      raf = 0;
1901      curX += (targetX - curX) * 0.14;
1902      curY += (targetY - curY) * 0.14;
1903      applyAtmosphere();
1904
1905      if (Math.abs(targetX - curX) > 0.18 || Math.abs(targetY - curY) > 0.18){
1906        raf = requestAnimationFrame(settle);
1907      }
1908    };
1909
1910    const scheduleAtmosphere = () => {
1911      if (raf) return;
1912      raf = requestAnimationFrame(settle);
1913    };
1914
1915    // Cursor-driven micro drift (very small, non-parallax)
1916    window.addEventListener("mousemove", (e) => {
1917      const w = Math.max(1, window.innerWidth);
1918      const h = Math.max(1, window.innerHeight);
1919      const nx = (e.clientX / w) * 2 - 1; // -1..1
1920      const ny = (e.clientY / h) * 2 - 1;
1921      targetX = clamp(nx * 42, -42, 42);
1922      targetY = clamp(ny * 36, -36, 36);
1923      scheduleAtmosphere();
1924    }, { passive: true });
1925
1926    // Keep atmosphere independent from scroll position. Updating root-level
1927    // visual variables on every scroll event invalidated too much of the page.
1928    root.style.setProperty("--atm-s", "0.18");
1929    root.style.setProperty("--atm-o", "0.94");
1930    root.style.setProperty("--atm-scale", "1.006");
1931    applyAtmosphere();
1932  } catch (_) {
1933    // If anything goes wrong, fail silent (never break the landing).
1934  }
1935})();
1936
1937
1938/* -------------------------------------------------
1939   YouTube-style shell behavior
1940   - Mobile: hamburger toggles top menu
1941   - Desktop/Mobile: highlight active section in sidebar/bottom-nav
1942-------------------------------------------------- */
1943(function(){
1944  "use strict";
1945  try {
1946    const nav = document.querySelector(".nav");
1947    const toggle = document.querySelector(".nav-toggle");
1948    const scrollRoot = document.querySelector(".app-main");
1949    const desktopMedia = window.matchMedia ? window.matchMedia("(min-width: 1024px)") : null;
1950    const prefersReducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1951    const smoothBehavior = prefersReducedMotion ? "auto" : "smooth";
1952
1953    const getScrollHost = () => {
1954      if (!scrollRoot) return window;
1955      if (!desktopMedia || !desktopMedia.matches) return window;
1956      return scrollRoot;
1957    };
1958
1959    const scrollSectionIntoView = (target) => {
1960      if (!target) return;
1961      const host = getScrollHost();
1962      if (host !== window && window.APLenis){
1963        window.APLenis.scrollTo(target, {
1964          offset: -84,
1965          duration: 1.05,
1966          easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
1967        });
1968        return;
1969      }
1970      if (host === window){
1971        target.scrollIntoView({ behavior: smoothBehavior, block: "start" });
1972        return;
1973      }
1974      target.scrollIntoView({ behavior: smoothBehavior, block: "start", inline: "nearest" });
1975    };
1976
1977    if (nav && toggle){
1978      const close = () => {
1979        nav.classList.remove("open");
1980        toggle.setAttribute("aria-expanded","false");
1981      };
1982      toggle.addEventListener("click", () => {
1983        const isOpen = nav.classList.toggle("open");
1984        toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
1985      });
1986      document.addEventListener("click", (e) => {
1987        if (!nav.classList.contains("open")) return;
1988        const t = e.target;
1989        if (t === toggle || toggle.contains(t) || t === nav || nav.contains(t)) return;
1990        close();
1991      });
1992      nav.addEventListener("click", (e) => {
1993        const a = e.target && e.target.closest ? e.target.closest("a") : null;
1994        if (a) close();
1995      });
1996      window.addEventListener("keydown", (e) => {
1997        if (e.key === "Escape") close();
1998      });
1999    }
2000
2001    document.addEventListener("click", (e) => {
2002      const placeholder = e.target && e.target.closest ? e.target.closest("[data-placeholder='true']") : null;
2003      if (placeholder){
2004        e.preventDefault();
2005        return;
2006      }
2007      const anchor = e.target && e.target.closest ? e.target.closest('a[href^="#"]') : null;
2008      if (!anchor) return;
2009      const href = anchor.getAttribute("href") || "";
2010      if (!href || href === "#") return;
2011      const target = document.querySelector(href);
2012      if (!target) return;
2013      e.preventDefault();
2014      scrollSectionIntoView(target);
2015      try{
2016        history.replaceState(null, "", href);
2017      }catch(_){}
2018    });
2019
2020    const links = Array.from(document.querySelectorAll("[data-nav]")).filter((el) => !el.hidden);
2021    const setActive = (key) => {
2022      links.forEach((el) => {
2023        const k = el.getAttribute("data-nav");
2024        if (!k) return;
2025        if (k === key) el.classList.add("active");
2026        else el.classList.remove("active");
2027      });
2028    };
2029
2030    const sections = [
2031      { id: "top", key: "top" },
2032      { id: "hub", key: "hub" },
2033      { id: "contact", key: "contact" },
2034    ].map((s) => ({ ...s, el: document.getElementById(s.id) }))
2035     .filter((s) => !!s.el && !s.el.hidden);
2036
2037    if (sections.length){
2038      let current = "top";
2039      setActive(current);
2040
2041      if ("IntersectionObserver" in window){
2042        const observerRoot = getScrollHost() === window ? null : scrollRoot;
2043        const io = new IntersectionObserver((entries) => {
2044          // Pick the most visible section
2045          let best = null;
2046          for (const e of entries){
2047            if (!e.isIntersecting) continue;
2048            if (!best || e.intersectionRatio > best.intersectionRatio){
2049              best = e;
2050            }
2051          }
2052          if (!best) return;
2053          const hit = sections.find((s) => s.el === best.target);
2054          if (hit && hit.key !== current){
2055            current = hit.key;
2056            setActive(current);
2057          }
2058        }, { root: observerRoot, threshold: [0.35, 0.5, 0.65] });
2059        sections.forEach((s) => io.observe(s.el));
2060      } else {
2061        // Fallback: scroll-based
2062        const onScroll = () => {
2063          const host = getScrollHost();
2064          const y = host === window ? (window.scrollY || 0) : host.scrollTop;
2065          let hit = sections[0];
2066          for (const s of sections){
2067            const top = s.el.getBoundingClientRect().top + y;
2068            if (y + 120 >= top) hit = s;
2069          }
2070          if (hit && hit.key !== current){
2071            current = hit.key;
2072            setActive(current);
2073          }
2074        };
2075        getScrollHost().addEventListener("scroll", onScroll, { passive: true });
2076        onScroll();
2077      }
2078    }
2079  } catch (_) {
2080    // Fail silent: never break the landing.
2081  }
2082})();
2083
2084
2085/* -------------------------------------------------
2086   Contact section interactions
2087   - Copy email with graceful fallback
2088   - Lightweight status messaging
2089-------------------------------------------------- */
2090(function(){
2091  "use strict";
2092  try {
2093    const emailNode = document.getElementById("emailLink");
2094    const copyBtn = document.getElementById("copyEmail");
2095    const status = document.getElementById("contactStatus");
2096    if (!emailNode || !copyBtn || !status) return;
2097
2098    const email = (emailNode.textContent || "").trim();
2099    if (!email) return;
2100
2101    const setStatus = (message, tone) => {
2102      status.textContent = message;
2103      status.classList.remove("is-success", "is-error");
2104      if (tone) status.classList.add(tone);
2105    };
2106
2107    const fallbackCopy = async () => {
2108      const area = document.createElement("textarea");
2109      area.value = email;
2110      area.setAttribute("readonly", "true");
2111      area.style.position = "fixed";
2112      area.style.opacity = "0";
2113      area.style.pointerEvents = "none";
2114      document.body.appendChild(area);
2115      area.focus();
2116      area.select();
2117      const ok = document.execCommand("copy");
2118      area.remove();
2119      if (!ok) throw new Error("fallback-copy-failed");
2120    };
2121
2122    copyBtn.addEventListener("click", async () => {
2123      const original = copyBtn.textContent;
2124      try {
2125        if (navigator.clipboard && window.isSecureContext){
2126          await navigator.clipboard.writeText(email);
2127        } else {
2128          await fallbackCopy();
2129        }
2130        copyBtn.textContent = window.AP_I18N?.t("contact.copied", "Copied") || "Copied";
2131        setStatus(window.AP_I18N?.t("contact.copied", "Email copied.") || "Email copied.", "is-success");
2132        window.setTimeout(() => {
2133          copyBtn.textContent = original;
2134        }, 1600);
2135      } catch (_) {
2136        setStatus(window.AP_I18N?.t("contact.status", "Copy was blocked by the browser. Please use the email button instead.") || "Copy was blocked by the browser. Please use the email button instead.", "is-error");
2137      }
2138    });
2139    window.addEventListener("ap:languagechange", () => {
2140      copyBtn.textContent = window.AP_I18N?.t("contact.copyEmail", "Copy email") || "Copy email";
2141      setStatus(window.AP_I18N?.t("contact.status", "Click “Copy email” to copy the address instantly.") || "Click “Copy email” to copy the address instantly.");
2142    });
2143  } catch (_) {
2144    // Fail silent: never break the landing.
2145  }
2146})();
2147
2148/* =================================================================
2149   PRAYER HERO — Live countdown, sun arc, progress bar
2150   ================================================================= */
2151(function(){
2152  "use strict";
2153  try {
2154    var PH_DATA_READY = false;
2155    var PH_PRAYERS = [
2156      { key:"fajr",    name:"Fajr",    ar:"الفجر",   time:"" },
2157      { key:"dhuhr",   name:"Dhuhr",   ar:"الظهر",   time:"" },
2158      { key:"asr",     name:"Asr",     ar:"العصر",   time:"" },
2159      { key:"maghrib", name:"Maghrib", ar:"المغرب",  time:"" },
2160      { key:"isha",    name:"Isha",    ar:"العشاء",  time:"" },
2161    ];
2162    var PH_PRAYER_NAMES = {
2163      en: { fajr:"Fajr", dhuhr:"Dhuhr", asr:"Asr", maghrib:"Maghrib", isha:"Isha", sunrise:"Sunrise" },
2164      ar: { fajr:"الفجر", dhuhr:"الظهر", asr:"العصر", maghrib:"المغرب", isha:"العشاء", sunrise:"الشروق" },
2165      ur: { fajr:"فجر", dhuhr:"ظہر", asr:"عصر", maghrib:"مغرب", isha:"عشاء", sunrise:"طلوعِ آفتاب" }
2166    };
2167
2168    // Never render guessed or seasonal prayer times. The dashboard remains
2169    // unavailable until a validated city/date API response arrives.
2170    var PH_SUNRISE = "";
2171    var PH_SUNSET  = "";
2172
2173    // Zawal = ~10-15 min before Dhuhr (sun at zenith)
2174    var PH_ZAWAL_START = null;
2175    var PH_ZAWAL_END   = null;
2176
2177    // Five forbidden windows for nawafil (Hanafi fiqh):
2178    //
2179    // IMPORTANT: The makruh periods start after the FARD prayer is
2180    // completed (i.e. after the congregation), NOT at adhan time.
2181    // Before the fard you may still pray sunnah (2 sunnah of Fajr,
2182    // 4 sunnah of Asr).
2183    //
2184    // Fard completion buffers are derived only after live timings arrive.
2185    //
2186    // Three strictly forbidden (haram) times — no prayer at all:
2187    //   1. Sunrise: from sunrise until sun is a spear-length high (~15-20 min)
2188    //   2. Zawal (zenith): when sun is at its peak (~10-12 min before Dhuhr)
2189    //   3. Sunset: from when sun begins to pale/set until it disappears (~20 min)
2190    //
2191    // Two makruh tahrimi periods — no voluntary prayers:
2192    //   4. After Fajr fard until sunrise
2193    //   5. After Asr fard until sunset begins
2194    //
2195    var PH_FAJR_FARD_DONE  = "";
2196    var PH_ASR_FARD_DONE   = "";
2197    var PH_SUNSET_BEGINS   = "";
2198
2199    var PH_FORBIDDEN = [
2200      { key:"afterFajr",  label:"After Fajr fard",  ar:"بعد فرض الفجر",  from:PH_FAJR_FARD_DONE, to:PH_SUNRISE, strict:false, msg:"No voluntary prayers after Fajr fard until sunrise" },
2201      { key:"sunrise",    label:"Sunrise",           ar:"الشروق",         from:PH_SUNRISE, to:PH_ISHRAQ, strict:true, msg:"Forbidden to pray while the sun is rising" },
2202      { key:"zawal",      label:"Zawal",             ar:"الزوال",         fromMin:PH_ZAWAL_START, toMin:PH_ZAWAL_END, strict:true, msg:"Forbidden to pray at zenith (zawal)" },
2203      { key:"afterAsr",   label:"After Asr fard",    ar:"بعد فرض العصر",  from:PH_ASR_FARD_DONE, to:PH_SUNSET_BEGINS, strict:false, msg:"No voluntary prayers after Asr fard until sunset" },
2204      { key:"sunset",     label:"Sunset",            ar:"الغروب",         from:PH_SUNSET_BEGINS, to:PH_SUNSET, strict:true, msg:"Forbidden to pray while the sun is setting" },
2205    ];
2206
2207    var ph$ = function(s){ return document.querySelector(s); };
2208    var phPad = function(n){ return String(n).padStart(2,"0"); };
2209    function phIsHHMM(value){
2210      var match = /^([01]\d|2[0-3]):([0-5]\d)$/.test(String(value || ""));
2211      return match;
2212    }
2213    var phToMin = function(t){
2214      var p=String(t || "").split(":"), h=Number(p[0]), m=Number(p[1]);
2215      return Number.isFinite(h)&&Number.isFinite(m)&&h>=0&&h<24&&m>=0&&m<60 ? h*60+m : NaN;
2216    };
2217    var PH_FORMAT_KEY = "ap_prayer_time_format_v1";
2218    var PH_PREFS_KEY = "ap_prayer_prefs_v1";
2219    var phTimeFormat = "12";
2220    var phMethodBaseNote = "Select a city to load verified prayer times.";
2221    // Stores real data from the last API load so phUpdateMethodNote can rebuild
2222    // with the current locale and school at any time.
2223    var phMethodDisplayData = window.phMethodDisplayData = (window.phMethodDisplayData || { isCustom: false, name: "", fajrBuf: 15, asrBuf: 15 });
2224    var phLocaleCopy = {
2225      en: { nextPrayer: "Next prayer", at: "at", endsIn: "Ends in", inPrefix: "in", now: "now", format12: "12-hour", format24: "24-hour", method: "Select a city to load verified prayer times.", unavailable: "Select a city to load verified prayer times.", fullSchedule: "See full schedule",
2226            strict: "Strictly forbidden", makruh: "Not recommended for nawafil", makruhMaliki: "Disliked (makruh)" },
2227      ar: { nextPrayer: "الصلاة التالية", at: "عند", endsIn: "ينتهي خلال", inPrefix: "خلال", now: "الآن", minuteSuffix: "د", format12: "12 ساعة", format24: "24 ساعة", method: "اختر مدينة لتحميل أوقات الصلاة الموثقة.", unavailable: "اختر مدينة لتحميل أوقات الصلاة الموثقة.", fullSchedule: "عرض الجدول الكامل",
2228            strict: "ممنوع تماماً", makruh: "مكروه تحريمي للنوافل", makruhMaliki: "مكروه" },
2229      ur: { nextPrayer: "اگلی نماز", at: "بوقت", endsIn: "اختتام", inPrefix: "میں", now: "اب", minuteSuffix: "منٹ", format12: "12 گھنٹے", format24: "24 گھنٹے", method: "تصدیق شدہ نماز کے اوقات لوڈ کرنے کے لیے شہر منتخب کریں۔", unavailable: "تصدیق شدہ نماز کے اوقات لوڈ کرنے کے لیے شہر منتخب کریں۔", fullSchedule: "مکمل شیڈول دیکھیں",
2230            strict: "سخت ممانعت", makruh: "نوافل کے لیے مکروہ تحریمی", makruhMaliki: "مکروہ" }
2231    };
2232    function phCurrentLocale(){
2233      return (window.AP_I18N && typeof window.AP_I18N.current === "function" ? window.AP_I18N.current() : "en") || "en";
2234    }
2235    function phCopy(){
2236      return phLocaleCopy[phCurrentLocale()] || phLocaleCopy.en;
2237    }
2238    try {
2239      var savedPrefs = JSON.parse(localStorage.getItem(PH_PREFS_KEY) || "null");
2240      var _savedFmt = (savedPrefs && savedPrefs.timeFormat) || localStorage.getItem(PH_FORMAT_KEY);
2241      phTimeFormat = _savedFmt === "24" ? "24" : "12";
2242    } catch(_){}
2243    function phFormatTime(hhmm){
2244      if(phTimeFormat !== "12") return hhmm;
2245      var p = hhmm.split(":");
2246      var h = Number(p[0]), m = Number(p[1]);
2247      var suffix = h >= 12 ? "PM" : "AM";
2248      var h12 = h % 12;
2249      if(h12 === 0) h12 = 12;
2250      return h12 + ":" + phPad(m) + " " + suffix;
2251    }
2252    function phRenderCellTime(hhmm){
2253      if(phTimeFormat !== "12"){
2254        return '<span class="ph-tcell-clock">' + hhmm + '</span>';
2255      }
2256      var p = hhmm.split(":");
2257      var h = Number(p[0]), m = Number(p[1]);
2258      var suffix = h >= 12 ? "PM" : "AM";
2259      var h12 = h % 12;
2260      if(h12 === 0) h12 = 12;
2261      return '<span class="ph-tcell-clock">' + h12 + ':' + phPad(m) + '</span><span class="ph-tcell-suffix">' + suffix + '</span>';
2262    }
2263    function phFormatRange(fromHH, toHH){
2264      if(phTimeFormat === "12"){
2265        var fromParts = phFormatTime(fromHH).split(" ");
2266        var toParts = phFormatTime(toHH).split(" ");
2267        if(fromParts[1] && fromParts[1] === toParts[1]){
2268          return fromParts[0] + "-" + toParts[0] + " " + toParts[1];
2269        }
2270      }
2271      return phFormatTime(fromHH) + "-" + phFormatTime(toHH);
2272    }
2273    function phUpdateFormatButtons(){
2274      var buttons = document.querySelectorAll("[data-time-format]");
2275      for(var i=0;i<buttons.length;i++){
2276        buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-time-format") === phTimeFormat ? "true" : "false");
2277      }
2278    }
2279    window.phUpdateMethodNote = phUpdateMethodNote;
2280    function phUpdateMethodNote(){
2281      var mn = ph$("#phMethodNote");
2282      if(!mn) return;
2283      var c = phCopy();
2284      if(!PH_DATA_READY){
2285        mn.textContent = c.unavailable || c.method;
2286        return;
2287      }
2288      var locale = phCurrentLocale();
2289      var parts = [];
2290      if(phMethodDisplayData.name){
2291        // Locale-aware "Calculated" prefix
2292        var prefixMap = {
2293          en: phMethodDisplayData.isCustom ? "Custom"     : "Calculated",
2294          ar: phMethodDisplayData.isCustom ? "مخصص"       : "محسوب",
2295          ur: phMethodDisplayData.isCustom ? "کسٹم"       : "حساب"
2296        };
2297        parts.push(prefixMap[locale] || prefixMap.en);
2298        // Method name (proper institution name — kept in English)
2299        parts.push(phMethodDisplayData.name);
2300        // Locale-aware school/madhab label
2301        var schoolMap = {
2302          hanafi:  { en:"Hanafi",  ar:"حنفي",  ur:"حنفی"  },
2303          shafii:  { en:"Shafiʼi", ar:"شافعي", ur:"شافعی" },
2304          hanbali: { en:"Hanbali", ar:"حنبلي", ur:"حنبلی" },
2305          maliki:  { en:"Maliki",  ar:"مالكي", ur:"مالکی" }
2306        };
2307        var sm = schoolMap[typeof selectedSchool === "string" ? selectedSchool : "hanafi"] || schoolMap.hanafi;
2308        parts.push(sm[locale] || sm.en);
2309      } else {
2310        // No city loaded yet — fall back to phLocaleCopy default
2311        parts.push(c.method);
2312      }
2313      // Time format suffix
2314      parts.push(phTimeFormat === "12" ? c.format12 : c.format24);
2315      mn.textContent = parts.join(" · ");
2316    }
2317    var PH_TIMEZONE = (function(){
2318      try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } catch(_) { return "UTC"; }
2319    })();
2320    function phZonedParts(date){
2321      var out = {};
2322      try {
2323        var parts = new Intl.DateTimeFormat("en-CA", {timeZone:PH_TIMEZONE,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(date || new Date());
2324        for(var i=0;i<parts.length;i++) if(parts[i].type !== "literal") out[parts[i].type] = Number(parts[i].value);
2325      } catch(_){
2326        var fallback = date || new Date();
2327        out = {year:fallback.getFullYear(),month:fallback.getMonth()+1,day:fallback.getDate(),hour:fallback.getHours(),minute:fallback.getMinutes(),second:fallback.getSeconds()};
2328      }
2329      return out;
2330    }
2331    function phNowDate(){
2332      var p = phZonedParts(new Date()), d = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second, 0));
2333      return d;
2334    }
2335    var phAt = function(hhmm){
2336      var p=hhmm.split(":"), d=phNowDate();
2337      d.setUTCHours(Number(p[0]),Number(p[1]),0,0); return d;
2338    };
2339    var PH_ARC_EDGE_PAD = 5;
2340    var phArcX = function(mins){
2341      var start=phToMin(PH_PRAYERS[0].time);
2342      var end=phToMin(PH_PRAYERS[PH_PRAYERS.length-1].time);
2343      if(mins<=start) return PH_ARC_EDGE_PAD;
2344      if(mins>=end) return 100 - PH_ARC_EDGE_PAD;
2345      return PH_ARC_EDGE_PAD + ((mins-start)/(end-start))*(100 - PH_ARC_EDGE_PAD*2);
2346    };
2347    function phIsRTL(){
2348      var l = phCurrentLocale();
2349      return l === "ar" || l === "ur";
2350    }
2351    var phNowIndicatorPrimedLTR = false;
2352    var phNowIndicatorPrimedRTL = false;
2353    function phArcPos(mins){
2354      var x = phArcX(mins);
2355      return phIsRTL() ? (100 - x) : x;
2356    }
2357    function phSetNowIndicatorPos(pct){
2358      if(!phEls.nowInd) return;
2359      // Two isolated systems to avoid cross-locale behavior leaks.
2360      if(phIsRTL()){
2361        // RTL system
2362        if(!phNowIndicatorPrimedRTL){
2363          phEls.nowInd.style.transition = "none";
2364          phEls.nowInd.style.left = "100%";
2365          phEls.nowInd.style.right = "";
2366          void phEls.nowInd.offsetWidth;
2367          phEls.nowInd.style.transition = "left 1s linear";
2368          phNowIndicatorPrimedRTL = true;
2369        }
2370        phEls.nowInd.style.right = "";
2371        phEls.nowInd.style.left = pct + "%";
2372        return;
2373      }
2374
2375      // LTR system (English) — explicitly independent from RTL state.
2376      if(!phNowIndicatorPrimedLTR){
2377        phEls.nowInd.style.transition = "none";
2378        phEls.nowInd.style.left = "0%";
2379        phEls.nowInd.style.right = "";
2380        void phEls.nowInd.offsetWidth;
2381        phEls.nowInd.style.transition = "left 1s linear";
2382        phNowIndicatorPrimedLTR = true;
2383      }
2384      phEls.nowInd.style.right = "";
2385      phEls.nowInd.style.left = pct + "%";
2386    }
2387
2388    var phGregEl=ph$("#phGregDate");
2389    if(phGregEl){
2390      phGregEl.textContent=new Intl.DateTimeFormat("en-GB",{weekday:"short",day:"2-digit",month:"short",year:"numeric"}).format(new Date());
2391    }
2392
2393    // Ishraq = ~20 min after sunrise (first permissible nawafil time)
2394    var PH_ISHRAQ = "";
2395
2396    var PH_SUNRISE_COPY = {
2397      en: { begins:"Sunrise begins", resumes:"Prayer resumes", restriction:"Sunrise restriction" },
2398      ar: { begins:"يبدأ الشروق", resumes:"يبدأ وقت الصلاة", restriction:"فترة حظر الصلاة عند الشروق" },
2399      ur: { begins:"طلوعِ آفتاب کا آغاز", resumes:"نماز کا وقت دوبارہ شروع", restriction:"طلوعِ آفتاب کا ممنوع وقت" }
2400    };
2401    function phSunriseCopy(locale){
2402      return PH_SUNRISE_COPY[locale] || PH_SUNRISE_COPY.en;
2403    }
2404    function phGetSunriseTimes(){
2405      var sunrise = String(PH_SUNRISE || "").split(" ")[0].slice(0,5);
2406      var resumes = String(PH_ISHRAQ || "").split(" ")[0].slice(0,5);
2407      if(!phIsHHMM(sunrise)) sunrise = "";
2408      if(!phIsHHMM(resumes)) resumes = "";
2409      if(!sunrise) return { sunrise:"", resumes:"" };
2410      var sunriseMin = phToMin(sunrise);
2411      var resumesMin = resumes ? phToMin(resumes) : NaN;
2412      // Keep the two reader-facing values distinct even if a stale or partial
2413      // timing response briefly supplies the same value for both.
2414      if(!resumes || !Number.isFinite(resumesMin) || resumesMin === sunriseMin){
2415        var resumeMin = (sunriseMin + 20) % 1440;
2416        resumes = phPad(Math.floor(resumeMin / 60)) + ":" + phPad(resumeMin % 60);
2417      }
2418      return { sunrise:sunrise, resumes:resumes };
2419    }
2420
2421    function phSyncSunriseCard(){
2422      var cell = ph$(".ph-tcell--sunrise");
2423      if(!cell) return;
2424      var times = phGetSunriseTimes();
2425      var copy = phSunriseCopy(phCurrentLocale());
2426      if(!times.sunrise || !times.resumes){
2427        var unavailable = "Prayer times unavailable until a verified city is selected.";
2428        var unavailableRows = cell.querySelectorAll("[data-sunrise-event] time");
2429        for(var u=0;u<unavailableRows.length;u++){
2430          unavailableRows[u].removeAttribute("datetime");
2431          unavailableRows[u].textContent = "—";
2432        }
2433        var unavailableLabel = cell.querySelector(".ph-tcell-sunrise-times");
2434        if(unavailableLabel) unavailableLabel.setAttribute("aria-label", unavailable);
2435        return;
2436      }
2437      var rows = cell.querySelectorAll("[data-sunrise-event]");
2438      for(var i=0;i<rows.length;i++){
2439        var row = rows[i];
2440        var event = row.getAttribute("data-sunrise-event");
2441        var value = event === "resumes" ? times.resumes : times.sunrise;
2442        var time = row.querySelector("time");
2443        if(time){
2444          time.setAttribute("datetime", value);
2445          time.innerHTML = phRenderCellTime(value);
2446        }
2447      }
2448      var timesEl = cell.querySelector(".ph-tcell-sunrise-times");
2449      if(timesEl) timesEl.setAttribute("aria-label", copy.begins+" "+phFormatTime(times.sunrise)+", "+copy.resumes+" "+phFormatTime(times.resumes));
2450    }
2451
2452    // ── Prayer Windows: per-madhab data ──────────────────────────────────────
2453    var PH_WINDOWS_MADHAB_KEY = "ap_windows_madhab_v1";
2454    var phWindowsMadhab = "hanafi";
2455    try {
2456      var _wm = localStorage.getItem(PH_WINDOWS_MADHAB_KEY);
2457      if(_wm && ["hanafi","shafii","hanbali","maliki"].indexOf(_wm) !== -1) phWindowsMadhab = _wm;
2458    } catch(_){}
2459    function syncWindowsMadhab(){ window.phWindowsMadhab = phWindowsMadhab; }
2460    syncWindowsMadhab();
2461
2462    var PH_MADHAB_LABELS = {
2463      hanafi:  { name:"Hanafi",  ar:"حنفي",  ur:"حنفی"  },
2464      shafii:  { name:"Shafi'i", ar:"شافعي", ur:"شافعی" },
2465      hanbali: { name:"Hanbali", ar:"حنبلي", ur:"حنبلی" },
2466      maliki:  { name:"Maliki",  ar:"مالكي", ur:"مالکی" }
2467    };
2468
2469    function phRenderStatic(){
2470      var locale = phCurrentLocale();
2471      var prayerNames = PH_PRAYER_NAMES[locale] || PH_PRAYER_NAMES.en;
2472      if(!PH_DATA_READY){
2473        var unavailable = (phCopy().unavailable || "Select a city to load verified prayer times.");
2474        var nextName = ph$("#phNextName");
2475        var nextAt = ph$("#phNextAt");
2476        if(nextName) nextName.textContent = "—";
2477        if(nextAt) nextAt.textContent = "—";
2478        var unavailableRow = ph$("#phMarkerRow");
2479        var unavailableZones = ph$("#phForbiddenZones");
2480        var unavailableSpecial = ph$("#phSpecialMarkers");
2481        if(unavailableRow) unavailableRow.innerHTML = "";
2482        if(unavailableZones) unavailableZones.innerHTML = "";
2483        if(unavailableSpecial) unavailableSpecial.innerHTML = "";
2484        var unavailableStrip = ph$("#phTimesStrip");
2485        if(unavailableStrip){
2486          var unavailableCells = PH_PRAYERS.map(function(p){
2487            var localizedName = prayerNames[p.key] || p.name;
2488            return '<div class="ph-tcell" data-key="'+p.key+'"><div class="ph-tcell-name">'+localizedName+'</div><div class="ph-tcell-time">—</div></div>';
2489          });
2490          unavailableCells.splice(1, 0, '<div class="ph-tcell ph-tcell--sunrise" data-key="sunrise"><div class="ph-tcell-name">
2490'+(prayerNames.sunrise || "Sunrise")+'</div><div class="ph-tcell-sunrise-times" aria-label="'+unavailable+'"><div class="ph-tcell-sunrise-row is-start" data-sunrise-event="begins"><span class="ph-tcell-sunrise-label">'+phSunriseCopy(locale).begins+'</span><time class="ph-tcell-time ph-tcell-sunrise-value">—</time></div><div class="ph-tcell-sunrise-row is-resume" data-sunrise-event="resumes"><span class="ph-tcell-sunrise-label">'+phSunriseCopy(locale).resumes+'</span><time class="ph-tcell-time ph-tcell-sunrise-value">—</time></div></div></div>');
2491          unavailableStrip.innerHTML = unavailableCells.join("");
2492        }
2493        phUpdateFormatButtons();
2494        phUpdateMethodNote();
2495        phApplyLocaleText();
2496        phRenderWindowsPanel();
2497        return;
2498      }
2499      var markerZoneText = {
2500        en: { sunrise: "Sunrise", sunset: "Sunset", zawal: "Zawal", makruh: "Makruh", ishraqShort: "Ishraq" },
2501        ar: { sunrise: "الشروق", sunset: "الغروب", zawal: "الزوال", makruh: "مكروه", ishraqShort: "الإشراق" },
2502        ur: { sunrise: "طلوعِ آفتاب", sunset: "غروبِ آفتاب", zawal: "زوال", makruh: "مکروہ", ishraqShort: "اشراق" }
2503      }[locale] || { sunrise: "Sunrise", sunset: "Sunset", zawal: "Zawal", makruh: "Makruh", ishraqShort: "Ishraq" };
2504      var phRow=ph$("#phMarkerRow");
2505      if(phRow){
2506        phRow.innerHTML=PH_PRAYERS.map(function(p){
2507          var x=phArcPos(phToMin(p.time));
2508          var localizedName = prayerNames[p.key] || p.name;
2509          return '<div class="ph-marker" data-key="'+p.key+'" style="left:'+x+'%; bottom:6px;"><div class="ph-marker-dot"></div><div class="ph-marker-time">'+phFormatTime(p.time)+'</div><div class="ph-marker-label">'+localizedName+'</div></div>';
2510        }).join("");
2511      }
2512      var phZonesEl=ph$("#phForbiddenZones");
2513      if(phZonesEl){
2514        phZonesEl.innerHTML=PH_FORBIDDEN.map(function(z){
2515          var fromM=z.fromMin!==undefined?z.fromMin:phToMin(z.from);
2516          var toM=z.toMin!==undefined?z.toMin:phToMin(z.to);
2517          var leftPct=Math.min(phArcPos(fromM), phArcPos(toM));
2518          var rightPct=Math.max(phArcPos(fromM), phArcPos(toM));
2519          var w=rightPct-leftPct;
2520          if(w<=0) return "";
2521          var localizedStrict = z.key === "sunrise" ? markerZoneText.sunrise : (z.key === "sunset" ? markerZoneText.sunset : (z.key === "zawal" ? markerZoneText.zawal : z.label));
2522          var shortLabel=z.strict ? localizedStrict : markerZoneText.makruh;
2523          var fromHH=phPad(Math.floor(fromM/60))+":"+phPad(fromM%60);
2524          var toHH=phPad(Math.floor(toM/60))+":"+phPad(toM%60);
2525          var timeRange=phFormatRange(fromHH,toHH);
2526          return '<div class="ph-forbidden-zone'+(z.strict?" is-strict":" is-makruh")+'" data-key="'+z.key+'" style="left:'+leftPct+'%;width:'+w+'%" title="'+z.msg+'"><span class="ph-forbidden-zone-label">'+shortLabel+'</span><span class="ph-forbidden-zone-time">'+timeRange+'</span></div>';
2527        }).join("");
2528      }
2529      var phSpecialEl=ph$("#phSpecialMarkers");
2530      if(phSpecialEl){
2531        var ishraqX=phArcPos(phToMin(PH_ISHRAQ));
2532        phSpecialEl.innerHTML=
2533          '<div class="ph-arc-special is-ishraq" style="left:'+ishraqX+'%; bottom:6px;"><div class="ph-arc-special-dot"></div><div class="ph-arc-special-label">'+markerZoneText.ishraqShort+'</div></div>';
2534      }
2535      var phStrip=ph$("#phTimesStrip");
2536      if(phStrip){
2537        var sunriseLocName = prayerNames.sunrise || "Sunrise";
2538        var sunriseCopy = phSunriseCopy(locale);
2539        var sunriseTimes = phGetSunriseTimes();
2540        var stripCells = [];
2541        PH_PRAYERS.forEach(function(p, i){
2542          var localizedName = prayerNames[p.key] || p.name;
2543          stripCells.push('<div class="ph-tcell" data-key="'+p.key+'"><div class="ph-tcell-name">'+localizedName+'</div><div class="ph-tcell-time">'+phRenderCellTime(p.time)+'</div><div class="ph-tcell-countdown" data-countdown-for="'+p.key+'" aria-live="polite"><span class="ph-tcell-countdown-kicker">Next prayer</span><time class="ph-tcell-countdown-value">—</time></div></div>');
2544          if(i === 0){
2545            stripCells.push(
2546              '<div class="ph-tcell ph-tcell--sunrise" data-key="sunrise">'+
2547                '<div class="ph-tcell-name">'+sunriseLocName+'</div>'+
2548                '<div class="ph-tcell-sunrise-times" aria-label="'+sunriseCopy.begins+' '+phFormatTime(sunriseTimes.sunrise)+', '+sunriseCopy.resumes+' '+phFormatTime(sunriseTimes.resumes)+'">'+
2549                  '<div class="ph-tcell-sunrise-row is-start" data-sunrise-event="begins"><span class="ph-tcell-sunrise-label">'+sunriseCopy.begins+'</span><time class="ph-tcell-time ph-tcell-sunrise-value" datetime="'+sunriseTimes.sunrise+'">'+phRenderCellTime(sunriseTimes.sunrise)+'</time></div>'+
2550                  '<div class="ph-tcell-sunrise-row is-resume" data-sunrise-event="resumes"><span class="ph-tcell-sunrise-label">'+sunriseCopy.resumes+'</span><time class="ph-tcell-time ph-tcell-sunrise-value" datetime="'+sunriseTimes.resumes+'">'+phRenderCellTime(sunriseTimes.resumes)+'</time></div>'+
2551                '</div>'+
2552                '<div class="ph-tcell-countdown ph-sunrise-cd" id="phSunriseCountdown" data-countdown-for="sunrise" aria-live="polite"><span class="ph-tcell-countdown-kicker">Next prayer</span><time class="ph-tcell-countdown-value">—</time></div>'+
2553              '</div>'
2554            );
2555          }
2556        });
2557        phStrip.innerHTML = stripCells.join("");
2558      }
2559      phUpdateFormatButtons();
2560      phUpdateMethodNote();
2561      phApplyLocaleText();
2562      phRenderWindowsPanel();
2563    }
2564
2565    var PH_WINDOWS_BY_MADHAB = {
2566
2567      hanafi: [
2568        {
2569          key:"afterFajr", label:"After Fajr fard", ar:"بعد فرض الفجر",
2570          from:PH_FAJR_FARD_DONE, to:PH_SUNRISE, strict:false,
2571          note:"Makruh tahrimi (prohibitively disliked) — no voluntary (nafl) prayers after completing the Fajr fard until sunrise. The two emphasized Fajr sunnah must be prayed before the fard, not after.",
2572          exception:"Qada (missed obligatory) prayers are permitted during this window."
2573        },
2574        {
2575          key:"sunrise", label:"Sunrise", ar:"وقت الشروق",
2576          from:PH_SUNRISE, to:PH_ISHRAQ, strict:true,
2577          note:"Haram — all prayer is forbidden from the moment the sun begins to appear on the horizon until it rises to a spear's height (~15–20 minutes). Any prayer offered in this window is invalid in the Hanafi school.",
2578          exception:"Sajda Tilawah (Quranic prostration) is technically permissible but slightly disliked (makruh tanzihan)."
2579        },
2580        {
2581          key:"ishraq", label:"Ishraq (permissible)", ar:"صلاة الإشراق",
2582          from:PH_ISHRAQ, to:null, strict:null,
2583          note:"The first permissible voluntary prayer window of the day, beginning ~20 minutes after sunrise. Praying two rak'ahs here after remaining seated in dhikr from Fajr until this time carries the reward of a complete Hajj and Umrah (Tirmidhi).",
2584          exception:null
2585        },
2586        {
2587          key:"zawal", label:"Zawal (zenith)", ar:"وقت الزوال",
2588          fromMin:PH_ZAWAL_START, toMin:PH_ZAWAL_END, strict:true,
2589          note:"Haram — all prayer is forbidden while the sun is at its absolute peak in the sky, for a brief window of approximately 5–15 minutes immediately before Dhuhr begins. Any prayer offered at this moment is invalid.",
2590          exception:"Sajda Tilawah (Quranic prostration) is technically permissible but slightly disliked (makruh tanzihan)."
2591        },
2592        {
2593          key:"afterAsr", label:"After Asr fard", ar:"بعد فرض العصر",
2594          from:PH_ASR_FARD_DONE, to:PH_SUNSET_BEGINS, strict:false,
2595          note:"Makruh tahrimi (prohibitively disliked) — no voluntary (nafl) prayers after completing the Asr fard until the sun begins to visibly pale before sunset. The four emphasized Asr sunnah must be prayed before the fard, not after.",
2596          exception:"Qada (missed obligatory) prayers are permitted during this window."
2597        },
2598        {
2599          key:"sunset", label:"Sunset", ar:"وقت الغروب",
2600          from:PH_SUNSET_BEGINS, to:PH_SUNSET, strict:true,
2601          note:"Haram — all prayer is forbidden once the sun's glare diminishes enough to look at it directly (~20 minutes before Maghrib) until it fully disappears below the horizon. Any prayer offered in this window is invali
2601d.",
2602          exception:"Exception: if Asr fard has not yet been prayed that day, it may still be offered during this window and remains valid."
2603        }
2604      ],
2605
2606      shafii: [
2607        {
2608          key:"afterFajr", label:"After Fajr prayer", ar:"بعد صلاة الفجر",
2609          from:PH_FAJR_FARD_DONE, to:PH_SUNRISE, strict:false,
2610          note:"Makruh tahrimi for nafl prayers without a preceding cause. In the Shafi'i school all prayers during forbidden times remain valid — none are rendered invalid. The restriction begins after performing the Fajr obligatory prayer, not at the adhan.",
2611          exception:"Dhul-sabab exception: prayers with a preceding cause — greeting the mosque (tahiyyatul masjid), post-wudu prayer, eclipse prayer — are permitted even during this window. Qada (makeup) prayers are also permitted."
2612        },
2613        {
2614          key:"sunrise", label:"Sunrise", ar:"وقت الشروق",
2615          from:PH_SUNRISE, to:PH_ISHRAQ, strict:false,
2616          note:"Makruh tahrimi for nafl without a preceding cause, from the moment the sun begins to rise until it reaches a spear's height (~15–20 minutes). Unlike the Hanafi school, prayers offered during this time are valid though sinful — they are not invalidated.",
2617          exception:"Dhul-sabab exception: cause-based nafl (tahiyyatul masjid, post-wudu, etc.) are permitted. Qada prayers are permitted. Prayers in Makkah are never restricted."
2618        },
2619        {
2620          key:"ishraq", label:"Ishraq (permissible)", ar:"صلاة الإشراق",
2621          from:PH_ISHRAQ, to:null, strict:null,
2622          note:"The first permissible voluntary prayer window of the day, beginning ~20 minutes after sunrise. Praying two rak'ahs here after remaining seated in dhikr from Fajr until this time carries the reward of a complete Hajj and Umrah (Tirmidhi).",
2623          exception:null
2624        },
2625        {
2626          key:"zawal", label:"Zawal (zenith)", ar:"وقت الزوال",
2627          fromMin:PH_ZAWAL_START, toMin:PH_ZAWAL_END, strict:false,
2628          note:"Makruh tahrimi for nafl without a cause, while the sun is at its exact peak immediately before Dhuhr (~5–15 minutes). Prayers remain valid in the Shafi'i school — they are disliked but not invalidated.",
2629          exception:"Friday exception: zawal is permissible on Jumu'ah. Dhul-sabab prayers (cause-based nafl) are permitted. Prayers in Makkah are unrestr
2629icted at all times."
2630        },
2631        {
2632          key:"afterAsr", label:"After Asr prayer", ar:"بعد صلاة العصر",
2633          from:PH_ASR_FARD_DONE, to:PH_SUNSET_BEGINS, strict:false,
2634          note:"Makruh tahrimi for nafl without a preceding cause, from after completing the Asr obligatory prayer until the sun begins to turn noticeably yellow. All prayers during this window remain valid.",
2635          exception:"Dhul-sabab exception: cause-based prayers are permitted. Qada prayers are permitted. Prayers in Makkah are unrestricted."
2636        },
2637        {
2638          key:"sunset", label:"Sunset", ar:"وقت الغروب",
2639          from:PH_SUNSET_BEGINS, to:PH_SUNSET, strict:false,
2640          note:"Makruh tahrimi for nafl without a cause, from when the sun's light turns noticeably yellow until it fully sets (~20 minutes). Prayers remain valid — not invalidated in the Shafi'i school.",
2641          exception:"Dhul-sabab exception: cause-based nafl (tahiyyatul masjid, post-wudu, etc.) are permitted. Qada prayers are permitted. Prayers in Makkah are unrestr
2641icted."
2642        }
2643      ],
2644
2645      hanbali: [
2646        {
2647          key:"afterFajrAdhan", label:"After Fajr adhan", ar:"بعد أذان الفجر",
2648          from:PH_PRAYERS[0].time, to:PH_SUNRISE, strict:true,
2649          note:"In the Hanbali school, the restriction begins at the Fajr adhan — earlier than other schools. No nafl prayers from the Fajr call to prayer until the sun rises to a spear's height. This is a stricter starting point than Hanafi (which starts after completing the fard).",
2650          exception:"The two emphasized Fajr sunnah may still be prayed before the fard. Qada (makeup) prayers, oath-obligated prayers, and tawaf prayers are permitted during this window."
2651        },
2652        {
2653          key:"sunrise", label:"Sunrise", ar:"وقت الشروق",
2654          from:PH_SUNRISE, to:PH_ISHRAQ, strict:true,
2655          note:"Forbidden for nafl from when the sun appears on the horizon until it reaches a spear's height (~15–20 minutes). This is a continuation of the post-Fajr adhan window and carries the same ruling.",
2656          exception:"Qada prayers, oath-obligated prayers, funeral prayers (if there is fear for the deceased), and tawaf prayers are all permitted during this window."
2657        },
2658        {
2659          key:"ishraq", label:"Ishraq (permissible)", ar:"صلاة الإشراق",
2660          from:PH_ISHRAQ, to:null, strict:null,
2661          note:"The first permissible voluntary prayer window of the day, beginning ~20 minutes after sunrise. Praying two rak'ahs here after remaining seated in dhikr from Fajr until this time carries the reward of a complete Hajj and Umrah (Tirmidhi).",
2662          exception:null
2663        },
2664        {
2665          key:"zawal", label:"Zawal (zenith)", ar:"وقت الزوال",
2666          fromMin:PH_ZAWAL_START, toMin:PH_ZAWAL_END, strict:true,
2667          note:"Forbidden for nafl while the sun is at its exact zenith, for a brief window of approximately 5–15 minutes before Dhuhr. This applies equally in the Hanbali school.",
2668          exception:"Qada prayers, oath-obligated prayers, tawaf prayers, and repeating a prayer in congregation are permitted."
2669        },
2670        {
2671          key:"afterAsr", label:"After Asr fard", ar:"بعد فرض العصر",
2672          from:PH_ASR_FARD_DONE, to:PH_SUNSET, strict:true,
2673          note:"Forbidden for nafl from after completing the Asr fard until complete sunset. Note: this window extends all the way to full sunset in the Hanbali school — there is no separate 'sunset begins' sub-window.",
2674          exception:"Qada prayers, oath-obligated prayers, funeral prayers (if fear for the deceased), tawaf prayers, and repeating a prayer in congregation are all permitted."
2675        }
2676      ],
2677
2678      maliki: [
2679        {
2680          key:"sunrise", label:"Sunrise", ar:"وقت الشروق",
2681          from:PH_SUNRISE, to:PH_ISHRAQ, strict:false,
2682          note:"Makruh (disliked) — voluntary prayers are discouraged while the sun is physically rising, from when it first appears on the horizon until it fully clears it (~15–20 minutes). Prayers during this time are valid but disliked.",
2683          exception:"Qada (makeup) prayers are permitted. Obligatory prayers are unaffected."
2684        },
2685        {
2686          key:"ishraq", label:"Ishraq (permissible)", ar:"صلاة الإشراق",
2687          from:PH_ISHRAQ, to:null, strict:null,
2688          note:"The first permissible voluntary prayer window of the day, beginning ~20 minutes after sunrise. Praying two rak'ahs here after remaining seated in dhikr from Fajr until this time carries the reward of a complete Hajj and Umrah (Tirmidhi).",
2689          exception:null
2690        },
2691        {
2692          key:"yellowSun", label:"Sun turns yellow (before sunset)", ar:"وقت الاصفرار",
2693          from:PH_SUNSET_BEGINS, to:PH_SUNSET, strict:false,
2694          note:"Makruh (disliked) — voluntary prayers are discouraged once the sun's light turns noticeably yellow and dims before sunset (~20 minutes before Maghrib). This is one of only two disliked windows in the Maliki school. Prayers remain valid.",
2695          exception:"Qada prayers are permitted. Obligatory prayers are unaffected."
2696        },
2697        {
2698          key:"malikirule", label:"Note: Zawal is not restricted", ar:"ملاحظة: الزوال",
2699          from:null, to:null, strict:"note",
2700          note:"Unlike the other three schools, the Maliki school does not restrict prayer at zawal (solar zenith). Praying at midday before Dhuhr enters is permissible in the Maliki school — there is no makruh at this time.",
2701          exception:null
2702        },
2703        {
2704          key:"malikirule2", label:"Unique Maliki rule: Qada takes priority", ar:"حكم خاص: القضاء أولاً",
2705          from:null, to:null, strict:"note",
2706          note:"A distinctive Maliki ruling: if a person owes any missed obligatory (qada) prayers, they should not pray voluntary (nafl) prayers until those qada are made up. This applies outside of forbidden times as a general principle, not just during these windows.",
2707          exception:null
2708        }
2709      ]
2710    };
2711
2712    // ── Prayer Windows: Arabic & Urdu translations ───────────────────────────
2713    var PH_WINDOWS_I18N = {
2714      ar: {
2715        _ui: {
2716          toggleLabel: "أوقات الكراهة والحظر",
2717          schoolOfLaw: "المذهب الفقهي",
2718          badgeHaram:    "حرام",
2719          badgeMakruhT:  "مكروه تحريمي",
2720          badgeMakruh:   "مكروه",
2721          badgeOk:       "مباح",
2722          badgeInfo:     "تنبيه"
2723        },
2724        hanafi: {
2725          afterFajr: {
2726            label: "بعد فريضة الفجر",
2727            note:  "مكروه تحريمي — لا يجوز أداء النوافل بعد الانتهاء من فريضة الفجر حتى طلوع الشمس. وسنة الفجر المؤكدة (ركعتا السنة) تُؤدَّى قبل الفرض لا بعده.",
2728            exception: "يجوز قضاء الفرائض الفائتة في هذا الوقت."
2729          },
2730          sunrise: {
2731            label: "وقت الشروق",
2732            note:  "حرام — يحرم أداء أي صلاة من لحظة بدء طلوع الشمس حتى ارتفاعها قدر رمح (نحو ١٥–٢٠ دقيقة). وأي صلاة تُؤدَّى في هذا الوقت تكون باطلة عند الحنفية.",
2733            exception: "سجدة التلاوة جائزة تقنياً مع كراهة تنزيهية خفيفة."
2734          },
2735          ishraq: {
2736            label: "صلاة الإشراق (وقت مباح)",
2737            note:  "أول وقت مباح لصلاة النوافل، يبدأ بعد نحو ٢٠ دقيقة من الشروق. ومن جلس في ذكر الله من صلاة الفجر حتى هذا الوقت ثم صلى ركعتين، نال ثواب حجة وعمرة تامتين كاملتين. (رواه الترمذي)",
2738            exception: null
2739          },
2740          zawal: {
2741            label: "وقت الزوال (الاستواء)",
2742            note:  "حرام — يحرم أداء أي صلاة والشمس في كبد السماء، في نافذة قصيرة نحو ٥–١٥ دقيقة قُبيل دخول وقت الظهر. وأي صلاة تُؤدَّى في هذه اللحظة تكون باطلة.",
2743            exception: "سجدة التلاوة جائزة مع كراهة تنزيهية."
2744          },
2745          afterAsr: {
2746            label: "بعد فريضة العصر",
2747            note:  "مكروه تحريمي — لا يجوز التنفل بعد أداء فريضة العصر حتى تبدأ الشمس في الاصفرار. وسنة العصر الأربع المؤكدة تُؤدَّى قبل الفرض لا بعده.",
2748            exception: "يجوز قضاء الفرائض الفائتة في هذا الوقت."
2749          },
2750          sunset: {
2751            label: "وقت الغروب",
2752            note:  "حرام — من حين اصفرار ضوء الشمس وضعف شعاعها (نحو ٢٠ دقيقة قبل المغرب) حتى غروبها الكامل، يحرم الصلاة وتبطل إن أُدِّيت.",
2753            exception: "استثناء: إن لم يُؤدَّ فرض العصر في ذلك اليوم بعد، فلا يزال يجوز أداؤه في هذا الوقت ويُعدُّ صحيحاً."
2754          }
2755        },
2756        shafii: {
2757          afterFajr: {
2758            label: "بعد صلاة الفجر",
2759            note:  "مكروه تحريمي للنوافل المطلقة (بلا سبب سابق). عند الشافعية تبقى جميع الصلوات المؤداة في أوقات الكراهة صحيحة ومجزئة — لا تبطل. والتقييد يبدأ بعد أداء الفريضة لا من وقت الأذان.",
2760            exception: "استثناء ذو السبب: الصلوات ذات الأسباب السابقة — تحية المسجد، صلاة الوضوء، صلاة الكسوÙ
2760 — جائزة في هذا الوقت. وكذلك قضاء الفوائت."
2761          },
2762          sunrise: {
2763            label: "وقت الشروق",
2764            note:  "مكروه تحريمي للنوافل المطلقة، من لحظة بدء طلوع الشمس حتى ارتفاعها قدر رمح (١٥–٢٠ دقيقة). وخلافاً للحنفية، الصلاة في هذا الوقت عند الشافعية صحيحة وإن كانت مذمومة.",
2765            exception: "استثناء ذو السبب: النوافل المرتبطة بسبب (تحية المسجد، صلاة الوضوء...) مباحة. والقضاء جائز. والصلاة في مكة المكرمة غير مقيدة في أي وقت."
2766          },
2767          ishraq: {
2768            label: "صلاة الإشراق (وقت مباح)",
2769            note:  "أول وقت مباح لصلاة النوافل، يبدأ بعد نحو ٢٠ دقيقة من الشروق. ومن جلس في ذكر الله من صلاة الفجر حتى هذا الوقت ثم صلى ركعتين، نال ثواب حجة وعمرة تامتين. (رواه الترمذي)",
2770            exception: null
2771          },
2772          zawal: {
2773            label: "وقت الزوال (الاستواء)",
2774            note:  "مكروه تحريمي للنوافل المطلقة أثناء استواء الشمس قُبيل دخول الظهر (٥–١٥ دقيقة). الصلاة صحيحة عند الشافعية وإن كانت مكروهة.",
2775            exception: "استثناء الجمعة: يُباح التنفل عند الزوال يوم الجمعة. وذو السبب جائز. والصلاة في مكة المكرمة مباحة على الإطلاق."
2776          },
2777          afterAsr: {
2778            label: "بعد صلاة العصر",
2779            note:  "مكروه تحريمي للنوافل المطلقة من بعد أداء فريضة العصر حتى بدء اصفرار الشمس. جميع الصلوات في هذا الوقت صحيحة عند الشافعية.",
2780            exception: "استثناء ذو السبب: الصلوات ذات الأسباب مباحة. والقضاء جائز. والصلاة في مكة مباحة."
2781          },
2782          sunset: {
2783            label: "وقت الغروب",
2784            note:  "مكروه تحريمي للنوافل المطلقة من بداية اصفرار الشمس حتى الغروب الكامل (نحو ٢٠ دقيقة). الصلاة صحيحة ولا تبطل عند الشافعية.",
2785            exception: "استثناء ذو السبب: النوافل ذات السبب مباحة. والقضاء جائز. والصلاة في مكة مباحة."
2786          }
2787        },
2788        hanbali: {
2789          afterFajrAdhan: {
2790            label: "بعد أذان الفجر",
2791            note:  "يبدأ التقييد في المذهب الحنبلي من أذان الفجر — وهو أسبق من سائر المذاهب. لا يجوز أداء أي نافلة من الأذان حتى ارتفاع الشمس قدر رمح.",
2792            exception: "يجوز أداء سنة الفجر المؤكدة قبل الفريضة. والقضاء وصلاة النذر وصلاة الطواف مباحة في هذا الوقت."
2793          },
2794          sunrise: {
2795            label: "وقت الشروق",
2796            note:  "ممنوع للنوافل من بزوغ الشمس حتى ارتفاعها قدر رمح (١٥–٢٠ دقيقة)، وهو امتداد لوقت ما بعد أذان الفجر ويحمل حكمه.",
2797            exception: "يجوز القضاء وصلاة النذر وصلاة الجنازة (عند الخوف على الميت) وصلاة الطواف في هذا الوقت."
2798          },
2799          ishraq: {
2800            label: "صلاة الإشراق (وقت مباح)",
2801            note:  "أول وقت مباح لصلاة النوافل، يبدأ بعد نحو ٢٠ دقيقة من الشروق. ومن جلس في ذكر الله من صلاة الفجر حتى هذا الوقت ثم صلى ركعتين، نال ثواب حجة وعمرة تامتين. (رواه الترمذي)",
2802            exception: null
2803          },
2804          zawal: {
2805            label: "وقت الزوال (الاستواء)",
2806            note:  "ممنوع للنوافل عند استواء الشمس، قُبيل دخول الظهر بنحو ٥–١٥ دقيقة. وينطبق هذا الحكم عند الحنابلة كما عند سائر المذاهب.",
2807            exception: "يجوز القضاء والمنذور وصلاة الطواف وإعادة الصلاة في جماعة في هذا الوقت."
2808          },
2809          afterAsr: {
2810            label: "بعد فريضة العصر",
2811            note:  "ممنوع للنوافل من بعد أداء فريضة العصر حتى الغروب التام. تنبيه: يمتد هذا الوقت عند الحنابلة حتى الغروب الكامل دون تفريق بين بداية الاصفرار والغروب.",
2812            exception: "يجوز القضاء والمنذور وصلاة الجنازة (عند الخوف على الميت) وصلاة الطواف وإعادة الصلاة في جماعة."
2813          }
2814        },
2815        maliki: {
2816          sunrise: {
2817            label: "وقت الشروق",
2818            note:  "مكروه — تُكره النوافل أثناء بزوغ الشمس، من بدء ظهورها حتى ارتفاعها الكامل (١٥–٢٠ دقيقة). الصلاة صحيحة لكنها مكروهة.",
2819            exception: "قضاء الفرائض مباح. وأداء الفرائض لا يتأثر."
2820          },
2821          ishraq: {
2822            label: "صلاة الإشراق (وقت مباح)",
2823            note:  "أول وقت مباح لصلاة النوافل، يبدأ بعد نحو ٢٠ دقيقة من الشروق. ومن جلس في ذكر الله من صلاة الفجر حتى هذا الوقت ثم صلى ركعتين، نال ثواب حجة وعمرة تامتين. (رواه الترمذي)",
2824            exception: null
2825          },
2826          yellowSun: {
2827            label: "وقت الاصفرار (قُبيل المغرب)",
2828            note:  "مكروه — تُكره النوافل حين يصفار ضوء الشمس ويخفت قُبيل المغرب (نحو ٢٠ دقيقة). وهذا أحد الوقتين المكروهين فقط في المذهب المالكي. الصلاة صحيحة.",
2829            exception: "قضاء الفرائض مباح. وأداء الفرائض لا يتأثر."
2830          },
2831          malikirule: {
2832            label: "تنبيه: الزوال غير مقيد",
2833            note:  "خلافاً للمذاهب الثلاثة الأخرى، لا تُكره الصلاة عند الزوال في المذهب المالكي. فالتنفل قُبيل دخول الظهر مباح تماماً، ولا كراهة في ذلك.",
2834            exception: null
2835          },
2836          malikirule2: {
2837            label: "حكم مالكي خاص: القضاء أولاً",
2838            note:  "حكم مميز في المذهب المالكي: من عليه قضاء فرائض فاتته، فلا ينبغي له أن يتطوع بالنوافل حتى يقضيها. وهذا مبدأ عام لا يختص بأوقات الكراهة وحدها.",
2839            exception: null
2840          }
2841        }
2842      },
2843
2844      ur: {
2845        _ui: {
2846          toggleLabel: "اوقاتِ کراہت و ممانعت",
2847          schoolOfLaw: "فقہی مذہب",
2848          badgeHaram:   "حرام",
2849          badgeMakruhT: "مکروہ تحریمی",
2850          badgeMakruh:  "مکروہ",
2851          badgeOk:      "جائز",
2852          badgeInfo:    "نوٹ"
2853        },
2854        hanafi: {
2855          afterFajr: {
2856            label: "فجر فرض کے بعد",
2857            note:  "مکروہ تحریمی — فجر کا فرض ادا کرنے کے بعد طلوعِ آفتاب تک نفل نماز پڑھنا مکروہ تحریمی ہے۔ فجر کی دونوں مؤکد سنتیں فرض سے پہلے پڑھی جائیں، بعد میں نہیں۔",
2858            exception: "اس وقت میں فائتہ فرض نمازوں کی قضا جائز ہے۔"
2859          },
2860          sunrise: {
2861            label: "وقتِ طلوعِ آفتاب",
2862            note:  "حرام — جب سے سورج افق پر نمودار ہو، نیزے کی اونچائی (تقریباً ١٥–٢٠ منٹ) تک ہر نماز ممنوع ہے۔ حنفی مذہب میں اس وقت پڑھی گئی نماز باطل ہوتی ہے۔",
2863            exception: "سجدۂ تلاوت تکنیکی طور پر جائز ہے البتہ مکروہ تنزیہی ہے۔"
2864          },
2865          ishraq: {
2866            label: "نمازِ اشراق (جائز وقت)",
2867            note:  "دن کا پہلا جائز نفل وقت، طلوعِ آفتاب کے تقریباً ٢٠ منٹ بعد شروع ہوتا ہÛ
2867’Û” جو شخص فجر کے بعد ذکرِ الہٰی میں بیٹھا رہے یہاں تک کہ اشراق کی نماز پڑھے، اسے ایک مکمل حج اور عمرے کا ثواب ملتا ہے۔ (ترمذی)",
2868            exception: null
2869          },
2870          zawal: {
2871            label: "وقتِ زوال (سورج کا عین میزان)",
2872            note:  "حرام — ظہر سے چند منٹ پہلے جب سورج بالکل عین میزان پر ہو (٥–١٥ منٹ) تو نماز ممنوع ہے۔ اس لمحے پڑھی گئی نماز حنفی مذہب کے مطابق باطل ہوتی ہے۔",
2873            exception: "سجدۂ تلاوت جائز ہے البتہ مکروہ تنزیہی ہے۔"
2874          },
2875          afterAsr: {
2876            label: "عصر فرض کے بعد",
2877            note:  "مکروہ تحریمی — عصر کا فرض ادا کرنے کے بعد سورج کی زردی شروع ہونے تک نفل نماز ناجائز ہے۔ عصر کی چار مؤکد سنتیں فرض سے پہلے پڑھی جائیں، بعد میں نہیں۔",
2878            exception: "فائتہ فرض نمازوں کی قضا اس وقت میں جائز ہے۔"
2879          },
2880          sunset: {
2881            label: "وقتِ غروبِ آفتاب",
2882            note:  "حرام — جب سورج کی تپش کم ہو جائے اور سیدھا دیکھا جا سکے (مغرب سے تقریباً ٢٠ منٹ پہلے) اس وقت سے مکمل غروب تک نماز حرام ہے اور باطل ہوتی ہے۔",
2883            exception: "استثناء: اگر اس دن عصر کا فرض ابھی تک نہیں پڑھا تو اسے اس وقت میں ادا کیا جا سکتا ہے اور وہ معتبر ہوگی۔"
2884          }
2885        },
2886        shafii: {
2887          afterFajr: {
2888            label: "فجر نماز کے بعد",
2889            note:  "نفلِ مطلق کے لیے مکروہ تحریمی (بلا پیشگی سبب)۔ شافعی مذہب میں ممنوع اوقات کی نمازیں باطل نہیں ہوتیں — معتبر رہتی ہیں۔ یہ پابندی فرض ادا کرنے کے بعد سے شروع ہوتی ہے، اذان سے نہیں۔",
2890            exception: "ذو السبب استثناء: جن نمازوں کا پیشگی سبب ہو — تحیۃ المسجد، وضو کے بعد کی نماز، نمازِ کسوف — یہ جائز ہیں۔ فائتہ نمازوں کی قضا بھی جائز ہے۔"
2891          },
2892          sunrise: {
2893            label: "وقتِ طلوعِ آفتاب",
2894            note:  "نفلِ مطلق کے لیے مکروہ تحریمی، طلوع سے ارتفاعِ آفتاب تک (١٥–٢٠ منٹ)۔ لیکن حنفی مذہب کے برعکس شافعی مذہب میں یہ نمازیں باطل نہیں ہوتیں — گناہ ہے لیکن نماز ادا ہو جاتی ہے۔",
2895            exception: "ذو السبب استثناء: سببی نفل نمازیں (تحیۃ المسجد، وضو کی نماز) جائز ہیں۔ قضا بھی جائز۔ مکہ مکرمہ میں کوئی پابندی نہیں۔"
2896          },
2897          ishraq: {
2898            label: "نمازِ اشراق (جائز وقت)",
2899            note:  "دن کا پہلا جائز نفل وقت، طلوعِ آفتاب کے تقریباً ٢٠ منٹ بعد شروع ہوتا ہے۔ جو شخص فجر کے بعد ذکرِ الہٰی میں بیٹھا رہے یہاں تک کہ اشراق کی نماز پڑھے، اسے ایک مکمل حج اور عمرے کا ثواب ملتا ہے۔ (ترمذی)",
2900            exception: null
2901          },
2902          zawal: {
2903            label: "وقتِ زوال (سورج کا عین میزان)",
2904            note:  "نفلِ مطلق کے لیے مکروہ تحریمی، ظہر سے پہلے سورج کے عین میزان پر ہونے کے وقت (٥–١٥ منٹ)۔ شافعی مذہب میں یہ نماز گو ناپسندیدہ ہے لیکن صحیح ہے۔",
2905            exception: "جمعے کا استثناء: جمعے کے دن زوال کے وقت نفل جائز ہے۔ ذو السبب نمازیں جائز ہیں۔ مکہ مکرمہ میں کوئی پابندی نہیں۔"
2906          },
2907          afterAsr: {
2908            label: "عصر نماز کے بعد",
2909            note:  "نفلِ مطلق کے لیے مکروہ تحریمی، عصر کے فرض کے بعد سورج کی زردی شروع ہونے تک۔ اس وقت کی تمام نمازیں شافعی مذہب میں معتبر ہیں۔",
2910            exception: "ذو السبب نمازیں جائز ہیں۔ قضا بھی جائز۔ مکہ میں کوئی پابندی نہیں۔"
2911          },
2912          sunset: {
2913            label: "وقتِ غروبِ آفتاب",
2914            note:  "نفلِ مطلق کے لیے مکروہ تحریمی، سورج کی زردی سے مکمل غروب تک (تقریباً ٢٠ منٹ)۔ شافعی مذہب میں یہ نمازیں باطل نہیں ہوتیں۔",
2915            exception: "ذو السبب نمازیں جائز ہیں۔ قضا بھی جائز۔ مکہ میں کوئی پابندی نہیں۔"
2916          }
2917        },
2918        hanbali: {
2919          afterFajrAdhan: {
2920            label: "اذانِ فجر کے بعد",
2921            note:  "حنبلی مذہب میں پابندی اذانِ فجر سے شروع ہوتی ہے — جو دیگر تمام مذاہب سے پہلے ہے۔ اذانِ فجر سے لے کر طلوعِ آفتاب تک کوئی بھی نفل نماز جائز نہیں۔",
2922            exception: "فجر کی مؤکد سنت فرض سے پہلے پڑھی جا سکتی ہے۔ قضا، منت کی نماز اور نمازِ طواف جائز ہیں۔"
2923          },
2924          sunrise: {
2925            label: "وقتِ طلوعِ آفتاب",
2926            note:  "طلوعِ آفتاب سے ارتفاعِ آفتاب (١٥–٢٠ منٹ) تک نفل ممنوع ہے — یہ اذانِ فجر کے بعد کے وقت کا تسلسل ہے اور اسی کا حکم رکھتا ہے۔",
2927            exception: "قضا، منت کی نماز، نمازِ جنازہ (میت خراب ہونے کا خدشہ ہو) اور نمازِ طواف جائز ہیں۔"
2928          },
2929          ishraq: {
2930            label: "نمازِ اشراق (جائز وقت)",
2931            note:  "دن کا پہلا جائز نفل وقت، طلوعِ آفتاب کے تقریباً ٢٠ منٹ بعد شروع ہوتا ہے۔ جو شخص فجر کے بعد ذکرِ الہٰی میں بیٹھا رہے یہاں تک کہ اشراق کی نماز پڑھے، اسے ایک مکمل حج اور عمرے کا ثواب ملتا ہے۔ (ترمذی)",
2932            exception: null
2933          },
2934          zawal: {
2935            label: "وقتِ زوال (سورج کا عین میزان)",
2936            note:  "ظہر سے پہلے سورج کے عین میزان پر ہونے کے وقت (٥–١٥ منٹ) نفل ممنوع ہے۔ حنبلی مذہب میں بھی یہی حکم لاگو ہوتا ہے۔",
2937            exception: "قضا، منت کی نماز، نمازِ طواف، اور جماعت میں اعادۂ نماز جائز ہے۔"
2938          },
2939          afterAsr: {
2940            label: "عصر فرض کے بعد",
2941            note:  "عصر کا فرض ادا کرنے کے بعد مکمل غروبِ آفتاب تک نفل ممنوع ہے۔ اہم بات: حنبلی مذہب میں یہ وقت مکمل غروب تک ہے — اصفرار اور غروب کی الگ الگ تقسیم نہیں کی جاتی۔",
2942            exception: "قضا، منت کی نماز، نمازِ جنازہ (میت خراب ہونے کا خدشہ)، نمازِ طواف، اور جماعت میں اعادۂ نماز جائز ہے۔"
2943          }
2944        },
2945        maliki: {
2946          sunrise: {
2947            label: "وقتِ طلوعِ آفتاب",
2948            note:  "مکروہ — سورج کے طلوع کے دوران نفل نماز مکروہ ہے، افق پر نمودار ہونے سے مکمل ارتفاع تک (١٥–٢٠ منٹ)۔ نماز معتبر رہتی ہے لیکن ناپسندیدہ ہے۔",
2949            exception: "فائتہ نمازوں کی قضا جائز ہے۔ فرائض متاثر نہیں ہوتے۔"
2950          },
2951          ishraq: {
2952            label: "نمازِ اشراق (جائز وقت)",
2953            note:  "دن کا پہلا جائز نفل وقت، طلوعِ آفتاب کے تقریباً ٢٠ منٹ بعد شروع ہوتا ہÛ
2953’Û” جو شخص فجر کے بعد ذکرِ الہٰی میں بیٹھا رہے یہاں تک کہ اشراق کی نماز پڑھے، اسے ایک مکمل حج اور عمرے کا ثواب ملتا ہے۔ (ترمذی)",
2954            exception: null
2955          },
2956          yellowSun: {
2957            label: "اصفرارِ شمس (مغرب سے پہلے)",
2958            note:  "مکروہ — مغرب سے پہلے جب سورج کی روشنی زرد پڑ جائے (تقریباً ٢٠ منٹ) تو نفل نماز مکروہ ہے۔ مالکی مذہب میں صرف دو ناپسندیدہ اوقات ہیں اور یہ ان میں سے ایک ہے۔ نماز معتبر رہتی ہے۔",
2959            exception: "فائتہ نمازوں کی قضا جائز ہے۔ فرائض متاثر نہیں ہوتے۔"
2960          },
2961          malikirule: {
2962            label: "نوٹ: زوال کا وقت ممنوع نہیں",
2963            note:  "تینوں دیگر مذاہب کے برعکس مالکی مذہب میں زوال کے وقت نماز مکروہ نہیں ہے۔ ظہر سے پہلے زوال کے وقت نماز پڑھنا مالکی مذہب میں بالکل جائز ہے — کوئی کراہت نہیں۔",
2964            exception: null
2965          },
2966          malikirule2: {
2967            label: "خاص مالکی حکم: پہلے قضا",
2968            note:  "مالکی مذہب کا ایک امتیازی حکم: جس شخص کے ذمے فائتہ فرض نمازیں ہوں، اسے نفل پڑھنے سے پہلے ان کی قضا ادا کرنی چاہیے۔ یہ ایک عمومی اصول ہے جو صرف ممنوع اوقات تک محدود نہیں۔",
2969            exception: null
2970          }
2971        }
2972      }
2973    };
2974
2975    function phGetWindowsData(){
2976      return PH_WINDOWS_BY_MADHAB[phWindowsMadhab] || PH_WINDOWS_BY_MADHAB.hanafi;
2977    }
2978
2979    function phWindowTiming(w){
2980      var sunriseTimes = phGetSunriseTimes();
2981      var timing = { from:w.from, to:w.to, fromMin:w.fromMin, toMin:w.toMin };
2982      switch(w.key){
2983        case "afterFajr": timing.from = PH_FAJR_FARD_DONE; timing.to = sunriseTimes.sunrise; break;
2984        case "afterFajrAdhan": timing.from = PH_PRAYERS[0].time; timing.to = sunriseTimes.sunrise; break;
2985        case "sunrise": timing.from = sunriseTimes.sunrise; timing.to = sunriseTimes.resumes; break;
2986        case "ishraq": timing.from = sunriseTimes.resumes; timing.to = null; break;
2987        case "zawal": timing.from = null; timing.to = null; timing.fromMin = PH_ZAWAL_START; timing.toMin = PH_ZAWAL_END; break;
2988        case "afterAsr": timing.from = PH_ASR_FARD_DONE; timing.to = PH_SUNSET_BEGINS; break;
2989        case "sunset": timing.from = PH_SUNSET_BEGINS; timing.to = PH_SUNSET; break;
2990        case "yellowSun": timing.from = PH_SUNSET_BEGINS; timing.to = PH_SUNSET; break;
2991      }
2992      return timing;
2993    }
2994
2995    function phRenderWindowsMadhab(){
2996      var sel = ph$("#phWindowsMadhab");
2997      if(!sel) return;
2998      var loc = phCurrentLocale();
2999      sel.innerHTML = ["hanafi","shafii","hanbali","maliki"].map(function(m){
3000        var d = PH_MADHAB_LABELS[m];
3001        var lbl = loc === "ar" ? d.ar : (loc === "ur" ? d.ur : d.name);
3002        var active = m === phWindowsMadhab;
3003        return '<button type="button" class="ph-wm-btn'+(active?" is-active":"")+'" data-wm="'+m+'">'+lbl+'</button>';
3004      }).join("");
3005    }
3006
3007    window.phRenderWindowsPanel = phRenderWindowsPanel;
3008    function phRenderWindowsPanel(){
3009      var el = ph$("#phWindowsPanelInner");
3010      if(!el) return;
3011      if(!PH_DATA_READY){
3012        el.innerHTML = '<p class="ph-windows-empty">'+(phCopy().unavailable || "Select a city to load verified prayer times.")+'</p>';
3013        return;
3014      }
3015      var panel = ph$("#phWindowsPanel");
3016      var wasOpen = panel && panel.classList.contains("is-open");
3017      // Single source of truth: prefs.school / selectedSchool always wins.
3018      // selectedSchool is var-hoisted; it's a string once the school init block runs.
3019      var _valid = ["hanafi","shafii","hanbali","maliki"];
3020      if(typeof selectedSchool === "string" && _valid.indexOf(selectedSchool) !== -1){
3021        phWindowsMadhab = selectedSchool;
3022        syncWindowsMadhab();
3023        try { localStorage.setItem(PH_WINDOWS_MADHAB_KEY, phWindowsMadhab); } catch(_){}
3024      }
3025      phRenderWindowsMadhab();
3026      var data = phGetWindowsData();
3027      var loc = phCurrentLocale();
3028      var isRTL = loc === "ar" || loc === "ur";
3029      var i18nLoc = PH_WINDOWS_I18N[loc] || {};
3030      var i18nMd  = i18nLoc[phWindowsMadhab] || {};
3031      var i18nUi  = i18nLoc._ui || {};
3032      var sunriseCopy = phSunriseCopy(loc);
3033      var sunriseTimes = phGetSunriseTimes();
3034
3035      // Badge text helpers
3036      var badgeHaram   = i18nUi.badgeHaram   || "Haram";
3037      var badgeMakruhT = i18nUi.badgeMakruhT || "Makruh tahrimi";
3038      var badgeMakruh  = i18nUi.badgeMakruh  || "Makruh";
3039      var badgeOk      = i18nUi.badgeOk      || "Permissible";
3040      var badgeInfo    = i18nUi.badgeInfo     || "Info";
3041
3042      var rows = data.map(function(w){
3043        var timing = phWindowTiming(w);
3044        // Pick locale strings
3045        var t = i18nMd[w.key] || {};
3046        var label     = t.label     || w.label;
3047        if(w.key === "sunrise") label = sunriseCopy.restriction;
3048        var note      = t.note      || w.note;
3049        var exception = (t.exception !== undefined ? t.exception : w.exception);
3050        // Secondary label: show Arabic script in en/ur; show English in ar
3051        var secLabel = loc === "ar" ? w.label : w.ar;
3052
3053        // Time string
3054        var timeStr = "";
3055        if(w.strict === "note" || (timing.from === null && timing.fromMin === undefined)){
3056          timeStr = "";
3057        } else if(w.strict === null){
3058          var fromM = timing.fromMin !== undefined ? timing.fromMin : phToMin(timing.from);
3059          var fromHH = phPad(Math.floor(fromM/60))+":"+phPad(fromM%60);
3060          timeStr = phFormatTime(fromHH) + "+";
3061        } else {
3062          var fromM = timing.fromMin !== undefined ? timing.fromMin : phToMin(timing.from);
3063          var toM   = timing.toMin   !== undefined ? timing.toMin   : phToMin(timing.to);
3064          var fromHH = phPad(Math.floor(fromM/60))+":"+phPad(fromM%60);
3065          var toHH   = phPad(Math.floor(toM/60))+":"+phPad(toM%60);
3066          timeStr = phFormatRange(fromHH, toHH);
3067        }
3068
3069        var timeHtml = timeStr ? '<div class="ph-windows-row-time">'+timeStr+'</div>' : '<div></div>';
3070        if(w.key === "sunrise"){
3071          timeHtml = '<div class="ph-windows-row-time ph-windows-row-time--sunrise" aria-label="'+sunriseCopy.begins+' '+phFormatTime(sunriseTimes.sunrise)+', '+sunriseCopy.resumes+' '+phFormatTime(sunriseTimes.resumes)+'">'
3072            +'<span class="ph-windows-time-point"><span class="ph-windows-time-label">'+sunriseCopy.begins+'</span><time>'+phFormatTime(sunriseTimes.sunrise)+'</time></span>'
3073            +'<span class="ph-windows-time-point"><span class="ph-windows-time-label">'+sunriseCopy.resumes+'</span><time>'+phFormatTime(sunriseTimes.resumes)+'</time></span>'
3074            +'</div>';
3075        }
3076
3077        // Badge
3078        var badgeCls, badgeText;
3079        if(w.strict === null){
3080          badgeCls = "is-ok"; badgeText = badgeOk;
3081        } else if(w.strict === true){
3082          badgeCls = "is-strict"; badgeText = badgeHaram;
3083        } else if(w.strict === false){
3084          badgeCls = "is-makruh"; badgeText = phWindowsMadhab === "maliki" ? badgeMakruh : badgeMakruhT;
3085        } else {
3086          badgeCls = "is-note"; badgeText = badgeInfo;
3087        }
3088
3089        var exceptionHtml = exception
3090          ? '<div class="ph-windows-row-exception"><svg viewBox="0 0 16 16" fill="none" width="11" height="11" aria-hidden="true"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.2"/><path d="M8 7v4M8 5.5v.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg><span>'+exception+'</span></div>'
3091          : '';
3092        return '<div class="ph-windows-row'+(w.strict==="note"?" is-note-row":"")+(isRTL?" is-rtl":"")+'" data-key="'+w.key+'">'
3093          +'<div class="ph-windows-row-name">'+label+'</div>'
3094          +timeHtml
3095          +'<div class="ph-windows-row-ar">'+secLabel+'</div>'
3096          +'<div class="ph-windows-row-note">'+note+'</div>'
3097          +'<div style="padding-top:3px; grid-column:1/-1"><span class="ph-windows-badge '+badgeCls+'">'+badgeText+'</span></div>'
3098          + exceptionHtml
3099          +'</div>';
3100      });
3101      el.innerHTML = rows.join("");
3102      if(wasOpen && panel){ panel.style.maxHeight = panel.scrollHeight + "px"; }
3103    }
3104
3105    (function phWireWindowsToggle(){
3106      var btn = ph$("#phWindowsToggle");
3107      var panel = ph$("#phWindowsPanel");
3108      if(!btn || !panel) return;
3109      btn.addEventListener("click", function(){
3110        var open = btn.getAttribute("aria-expanded") === "true";
3111        if(!open){
3112          phRenderWindowsPanel(); // always re-render with current school before showing
3113          panel.style.maxHeight = panel.scrollHeight + "px";
3114          panel.classList.add("is-open");
3115          btn.setAttribute("aria-expanded","true");
3116          panel.setAttribute("aria-hidden","false");
3117        } else {
3118          panel.style.maxHeight = "0";
3119          panel.classList.remove("is-open");
3120          btn.setAttribute("aria-expanded","false");
3121          panel.setAttribute("aria-hidden","true");
3122        }
3123      });
3124      document.addEventListener("click", function(e){
3125        var wmBtn = e.target && e.target.closest ? e.target.closest("[data-wm]") : null;
3126        if(!wmBtn) return;
3127        var next = wmBtn.getAttribute("data-wm");
3128        if(!next || next === phWindowsMadhab || !PH_WINDOWS_BY_MADHAB[next]) return;
3129        phWindowsMadhab = next;
3130        syncWindowsMadhab();
3131        try { localStorage.setItem(PH_WINDOWS_MADHAB_KEY, phWindowsMadhab); } catch(_){}
3132        // Sync Asr school selector
3133        if(typeof window.selectedSchool !== "undefined" && window.selectedSchool !== phWindowsMadhab){
3134          selectedSchool = phWindowsMadhab;
3135          window.selectedSchool = selectedSchool;
3136          if(typeof prefs !== "undefined"){ prefs.school = selectedSchool; if(typeof savePrefs==="function") savePrefs(); }
3137          if(typeof updateSchoolButtons === "function") updateSchoolButtons();
3138          if(typeof phUpdateMethodNote === "function") phUpdateMethodNote();
3139        }
3140        phRenderWindowsPanel();
3141        if(panel.classList.contains("is-open")){
3142          panel.style.maxHeight = panel.scrollHeight + "px";
3143        }
3144      });
3145    })();
3146
3147    function phApplyLocaleText(){
3148      var locale = phCurrentLocale();
3149      var isRTL = locale === "ar" || locale === "ur";
3150      var map = {
3151        en: {
3152          nextPrayer: "Next prayer",
3153          countdown: "Time remaining",
3154          units: ["Hours", "Minutes", "Seconds"],
3155          since: "Since",
3156          interval: "of interval elapsed",
3157          arc: "The day's arc",
3158          strict: "Strictly forbidden",
3159          makruh: "Not recommended for nawafil",
3160          ishraq: "Ishraq / permissible",
3161          at: "at",
3162          ends: "Ends in",
3163          m: "m",
3164          makruhShort: "Makruh",
3165          fullSchedule: "See full schedule",
3166          explore: "Explore",
3167          windowsToggle: "Prayer Windows",
3168          schoolOfLaw: "School of law",
3169          swipeHint: "Swipe to explore the day"
3170        },
3171        ar: {
3172          nextPrayer: "الصلاة التالية",
3173          countdown: "الوقت المتبقي",
3174          units: ["ساعات", "دقائق", "ثوانٍ"],
3175          since: "منذ",
3176          interval: "من الفترة المنقضية",
3177          arc: "مسار اليوم",
3178          strict: "ممنوع تماماً",
3179          makruh: "مكروه للنوافل",
3180          ishraq: "الإشراق / مباح",
3181          at: "عند",
3182          ends: "ينتهي خلال",
3183          m: "د",
3184          makruhShort: "مكروه",
3185          fullSchedule: "عرض الجدول الكامل",
3186          explore: "استكشف",
3187          windowsToggle: "أوقات الكراهة والحظر",
3188          schoolOfLaw: "المذهب الفقهي",
3189          swipeHint: "اسحب لاستكشاف اليوم"
3190        },
3191        ur: {
3192          nextPrayer: "اگلی نماز",
3193          countdown: "باقی وقت",
3194          units: ["گھنٹے", "منٹ", "سیکنڈ"],
3195          since: "سے",
3196          interval: "وقفہ گزر چکا",
3197          arc: "دن کا مدار",
3198          strict: "سخت ممانعت",
3199          makruh: "نوافل کے لیے مناسب نہیں",
3200          ishraq: "اشراق / جائز",
3201          at: "بوقت",
3202          ends: "اختتام",
3203          m: "منٹ",
3204          makruhShort: "مکروہ",
3205          fullSchedule: "مکمل شیڈول دیکھیں",
3206          explore: "دریافت کریں",
3207          windowsToggle: "اوقاتِ کراہت و ممانعت",
3208          schoolOfLaw: "فقہی مذہب",
3209          swipeHint: "دن دیکھنے کے لیے سوائپ کریں"
3210        }
3211      };
3212      var c = map[locale] || map.en;
3213      var set = function(sel, text){ var el = document.querySelector(sel); if(el) el.textContent = text; };
3214      set(".ph-next-label", c.nextPrayer);
3215      set(".ph-cd-label", c.countdown);
3216      set("#phMobileNextLabel", c.nextPrayer);
3217      set("#phMobileNextTimerLabel", c.countdown);
3218      set("#phMobileNextAtLabel", c.at);
3219      set("#phMobileNextSinceLabel", c.since);
3220      set("#phMobileNextElapsed", c.interval);
3221      document.querySelectorAll(".ph-tcell-countdown-kicker").forEach(function(el){ el.textContent = c.nextPrayer; });
3222      document.querySelectorAll(".ph-cd-unit").forEach(function(el, idx){ if(c.units[idx]) el.textContent = c.units[idx]; });
3223      set(".ph-arc-title", c.arc);
3224      var legend = document.querySelectorAll(".ph-arc-legend-item");
3225      if(legend[0]) legend[0].lastChild.textContent = c.strict;
3226      if(legend[1]) legend[1].lastChild.textContent = c.makruh;
3227      if(legend[2]) legend[2].lastChild.textContent = c.ishraq;
3228      var atEl = document.querySelector(".ph-at-time");
3229      if(atEl){
3230        atEl.firstChild.textContent = c.at + " ";
3231      }
3232      var sinceWrap = document.querySelector(".ph-cd-foot > span:first-child");
3233      if(sinceWrap && sinceWrap.firstChild){
3234        sinceWrap.firstChild.textContent = c.since + " ";
3235      }
3236      var intervalWrap = document.querySelector(".ph-cd-foot > span:last-child");
3237      if(intervalWrap){
3238        var nodes = intervalWrap.childNodes;
3239        if(nodes.length >= 2) nodes[nodes.length - 1].textContent = " " + c.interval;
3240      }
3241      document.querySelectorAll(".ph-forbidden-zone.is-makruh .ph-forbidden-zone-label").forEach(function(el){ el.textContent = c.makruhShort; });
3242      var special = document.querySelector(".ph-arc-special-label");
3243      if(special && isRTL){
3244        special.textContent = locale === "ar" ? "الإشراق" : "اشراق";
3245      } else if(special){
3246        special.textContent = "Ishraq";
3247      }
3248      var fullScheduleLink = document.querySelector(".ph-panel-foot a.link[href='/prayer']");
3249      if(fullScheduleLink){
3250        fullScheduleLink.childNodes[0].nodeValue = c.fullSchedule + " ";
3251      }
3252      // Prayer Windows panel UI strings
3253      var wtLbl = document.querySelector(".ph-windows-toggle-label");
3254      if(wtLbl) wtLbl.textContent = c.windowsToggle;
3255      var wtKicker = document.querySelector(".ph-windows-toggle-kicker");
3256      if(wtKicker) wtKicker.textContent = c.explore;
3257      var wsMdLbl = document.querySelector(".ph-windows-madhab-label");
3258      if(wsMdLbl) wsMdLbl.textContent = c.schoolOfLaw;
3259      var swipeHint = document.querySelector("#phArcSwipeHint span");
3260      if(swipeHint) swipeHint.textContent = c.swipeHint;
3261      // Method note uses locale-aware components — rebuild on language change
3262      phUpdateMethodNote();
3263    }
3264
3265    phRenderStatic();
3266    document.addEventListener("click", function(e){
3267      var btn = e.target && e.target.closest ? e.target.closest("[data-time-format]") : null;
3268      if(!btn) return;
3269      var nextFormat = btn.getAttribute("data-time-format") === "12" ? "12" : "24";
3270      if(nextFormat === phTimeFormat) return;
3271      phTimeFormat = nextFormat;
3272      try {
3273        localStorage.setItem(PH_FORMAT_KEY, phTimeFormat);
3274        var prefs = JSON.parse(localStorage.getItem(PH_PREFS_KEY) || "{}");
3275        prefs.timeFormat = phTimeFormat;
3276        localStorage.setItem(PH_PREFS_KEY, JSON.stringify(prefs));
3277      } catch(_){}
3278      phRenderStatic();
3279      phTick();
3280    });
3281
3282    var phTicksEl=ph$("#phCdProgressTicks");
3283    if(phTicksEl) phTicksEl.innerHTML=[20,40,60,80].map(function(p){return '<span style="left:'+p+'%"></span>';}).join("");
3284
3285    function phFindNext(now){
3286      var nowM=now.getUTCHours()*60+now.getUTCMinutes()+now.getUTCSeconds()/60;
3287      for(var i=0;i<PH_PRAYERS.length;i++){
3288        if(phToMin(PH_PRAYERS[i].time)>nowM){
3289          var prev;
3290          if(i===0){
3291            var d=phAt(PH_PRAYERS[PH_PRAYERS.length-1].time);
3292            d.setUTCDate(d.getUTCDate()-1);
3293            prev={name:PH_PRAYERS[PH_PRAYERS.length-1].name,ar:PH_PRAYERS[PH_PRAYERS.length-1].ar,key:PH_PRAYERS[PH_PRAYERS.length-1].key,time:PH_PRAYERS[PH_PRAYERS.length-1].time,date:d};
3294          } else {
3295            prev={name:PH_PRAYERS[i-1].name,ar:PH_PRAYERS[i-1].ar,key:PH_PRAYERS[i-1].key,time:PH_PRAYERS[i-1].time,date:phAt(PH_PRAYERS[i-1].time)};
3296          }
3297          return {next:PH_PRAYERS[i],nextDate:phAt(PH_PRAYERS[i].time),prev:prev};
3298        }
3299      }
3300      var tf=phAt(PH_PRAYERS[0].time);
3301      tf.setUTCDate(tf.getUTCDate()+1);
3302      return {next:PH_PRAYERS[0],nextDate:tf,prev:{name:PH_PRAYERS[PH_PRAYERS.length-1].name,ar:PH_PRAYERS[PH_PRAYERS.length-1].ar,key:PH_PRAYERS[PH_PRAYERS.length-1].key,time:PH_PRAYERS[PH_PRAYERS.length-1].time,date:phAt(PH_PRAYERS[PH_PRAYERS.length-1].time)}};
3303    }
3304
3305    function phFindNextCardEvent(now){
3306      var events = PH_PRAYERS.map(function(p){
3307        return { key:p.key, name:p.name, ar:p.ar, time:p.time };
3308      });
3309      events.splice(1, 0, { key:"sunrise", name:"Sunrise", ar:"الشروق", time:PH_SUNRISE });
3310      events.sort(function(a,b){ return phToMin(a.time)-phToMin(b.time); });
3311      var nowM=now.getUTCHours()*60+now.getUTCMinutes()+now.getUTCSeconds()/60;
3312      for(var i=0;i<events.length;i++){
3313        if(phToMin(events[i].time)>nowM){
3314          return {event:events[i], date:phAt(events[i].time)};
3315        }
3316      }
3317      var tomorrow=phAt(events[0].time);
3318      tomorrow.setUTCDate(tomorrow.getUTCDate()+1);
3319      return {event:events[0], date:tomorrow};
3320    }
3321
3322    function phCardCountdownText(seconds){
3323      var c=phCopy();
3324      if(seconds<=0) return c.now || "now";
3325      var total=Math.floor(seconds);
3326      var hours=Math.floor(total/3600);
3327      var minutes=Math.floor((total%3600)/60);
3328      var secs=total%60;
3329      var prefix=c.inPrefix || "in";
3330      if(hours>0) return prefix+" "+phPad(hours)+":"+phPad(minutes)+":"+phPad(secs);
3331      return prefix+" "+phPad(minutes)+":"+phPad(secs);
3332    }
3333
3334    function phCountdownDigits(seconds){
3335      var total=Math.max(0,Math.floor(seconds));
3336      var hours=Math.floor(total/3600);
3337      var minutes=Math.floor((total%3600)/60);
3338      var secs=total%60;
3339      return phPad(hours)+":"+phPad(minutes)+":"+phPad(secs);
3340    }
3341
3342    var phEls={
3343      h:ph$("#phCdH"),m:ph$("#phCdM"),s:ph$("#phCdS"),
3344      name:ph$("#phNextName"),ar:ph$("#phNextAr"),at:ph$("#phNextAt"),
3345      since:ph$("#phSincePrayer"),pct:ph$("#phPctElapsed"),bar:ph$("#phCdProgress"),
3346      clock:ph$("#phNowClock"),nowInd:ph$("#phNowIndicator"),
3347      mobile:ph$("#phMobileNext"),mobileLabel:ph$("#phMobileNextLabel"),
3348      mobileName:ph$("#phMobileNextName"),mobileAr:ph$("#phMobileNextAr"),
3349      mobileAt:ph$("#phMobileNextAt"),mobileCountdown:ph$("#phMobileNextCountdown"),
3350      mobileSince:ph$("#phMobileNextSince"),mobilePct:ph$("#phMobileNextPct"),
3351      mobileProgress:ph$("#phMobileNextProgress")
3352    };
3353
3354    var phLast={h:null,m:null,s:null,key:null};
3355    var phFlash=function(el){el.classList.remove("ph-tick");void el.offsetWidth;el.classList.add("ph-tick");};
3356
3357    function phTick(){
3358      if(!PH_DATA_READY){
3359        var unavailable = phCopy().unavailable || "Select a city to load verified prayer times.";
3360        if(phEls.name) phEls.name.textContent = "—";
3361        if(phEls.ar) phEls.ar.textContent = "";
3362        if(phEls.at) phEls.at.textContent = "—";
3363        if(phEls.since) phEls.since.textContent = "—";
3364        if(phEls.pct) phEls.pct.textContent = "—";
3365        if(phEls.bar) phEls.bar.style.width = "0%";
3366        if(phEls.clock) phEls.clock.textContent = "—";
3367        if(phEls.mobileName) phEls.mobileName.textContent = "—";
3368        if(phEls.mobileAr) phEls.mobileAr.textContent = "";
3369        if(phEls.mobileAt) phEls.mobileAt.textContent = "—";
3370        if(phEls.mobileSince) phEls.mobileSince.textContent = "—";
3371        if(phEls.mobilePct) phEls.mobilePct.textContent = "—";
3372        if(phEls.mobileProgress) phEls.mobileProgress.style.width = "0%";
3373        if(phEls.mobile) phEls.mobile.setAttribute("aria-label", unavailable);
3374        return;
3375      }
3376      var now=phNowDate();
3377      var info=phFindNext(now);
3378      var next=info.next,nextDate=info.nextDate,prev=info.prev;
3379
3380      if(phEls.name) phEls.name.textContent=next.name;
3381      if(phEls.ar) phEls.ar.textContent=next.ar;
3382      if(phEls.at) phEls.at.textContent=phFormatTime(next.time);
3383      if(phEls.since) phEls.since.textContent=prev.name;
3384
3385      if(phLast.key!==next.key) phLast.key=next.key;
3386
3387      var diff=Math.max(0,nextDate-now);
3388      var totalSec=Math.floor(diff/1000);
3389      var H=Math.floor(totalSec/3600),M=Math.floor((totalSec%3600)/60),S=totalSec%60;
3390      var hS=phPad(H),mS=phPad(M),sS=phPad(S);
3391
3392      if(phLast.h!==hS&&phEls.h){phEls.h.textContent=hS;if(phLast.h!==null)phFlash(phEls.h);phLast.h=hS;}
3393      if(phLast.m!==mS&&phEls.m){phEls.m.textContent=mS;if(phLast.m!==null)phFlash(phEls.m);phLast.m=mS;}
3394      if(phLast.s!==sS&&phEls.s){phEls.s.textContent=sS;if(phLast.s!==null)phFlash(phEls.s);phLast.s=sS;}
3395
3396      var intervalMs=nextDate-prev.date;
3397      var elapsedMs=now-prev.date;
3398      var pct=Math.max(0,Math.min(100,(elapsedMs/intervalMs)*100));
3399      if(phEls.pct) phEls.pct.textContent=pct.toFixed(0)+"%";
3400      if(phEls.bar) phEls.bar.style.width=pct+"%";
3401
3402      if(phEls.mobile){
3403        var mobileCopy=phCopy();
3404        if(phEls.mobileLabel) phEls.mobileLabel.textContent=mobileCopy.nextPrayer || "Next prayer";
3405        if(phEls.mobileName) phEls.mobileName.textContent=next.name;
3406        if(phEls.mobileAr) phEls.mobileAr.textContent=next.ar;
3407        if(phEls.mobileAt) phEls.mobileAt.textContent=phFormatTime(next.time);
3408        if(phEls.mobileCountdown){
3409          phEls.mobileCountdown.textContent=phCountdownDigits(totalSec);
3410          phEls.mobileCountdown.setAttribute("aria-label", (mobileCopy.nextPrayer || "Next prayer")+" "+next.name+" "+phCardCountdownText(totalSec));
3411        }
3412        if(phEls.mobileSince) phEls.mobileSince.textContent=prev.name;
3413        if(phEls.mobilePct) phEls.mobilePct.textContent=pct.toFixed(0)+"%";
3414        if(phEls.mobileProgress) phEls.mobileProgress.style.width=pct+"%";
3415        phEls.mobile.setAttribute("aria-label", (mobileCopy.nextPrayer || "Next prayer")+": "+next.name+" "+phCardCountdownText(totalSec));
3416      }
3417
3418      if(phEls.clock) phEls.clock.textContent=phFormatTime(phPad(now.getUTCHours())+":"+phPad(now.getUTCMinutes()));
3419
3420      var nowM=now.getUTCHours()*60+now.getUTCMinutes()+now.getUTCSeconds()/60;
3421      var markers=document.querySelectorAll(".ph-marker");
3422      for(var j=0;j<markers.length;j++){
3423        var el=markers[j],key=el.getAttribute("data-key");
3424        var p; for(var k=0;k<PH_PRAYERS.length;k++){if(PH_PRAYERS[k].key===key){p=PH_PRAYERS[k];break;}}
3425        el.classList.remove("is-past","is-next");
3426        if(phToMin(p.time)<=nowM) el.classList.add("is-past");
3427        if(key===next.key) el.classList.add("is-next");
3428      }
3429
3430      var cardNext=phFindNextCardEvent(now);
3431      var cardNextKey=cardNext.event.key;
3432      var cells=document.querySelectorAll(".ph-tcell");
3433      for(var j2=0;j2<cells.length;j2++){
3434        var el2=cells[j2],key2=el2.getAttribute("data-key");
3435        el2.classList.remove("is-past","is-next");
3436        var cardTime=key2==="sunrise" ? PH_SUNRISE : null;
3437        for(var k2=0;k2<PH_PRAYERS.length;k2++){
3438          if(PH_PRAYERS[k2].key===key2){cardTime=PH_PRAYERS[k2].time;break;}
3439        }
3440        if(!cardTime) continue;
3441        var cardMin=phToMin(cardTime);
3442        var isPast=cardMin<=nowM;
3443        if(isPast) el2.classList.add("is-past");
3444        if(key2===cardNextKey){
3445          el2.classList.add("is-next");
3446          el2.setAttribute("aria-current","time");
3447        } else {
3448          el2.removeAttribute("aria-current");
3449        }
3450        var countdown=el2.querySelector(".ph-tcell-countdown");
3451        if(countdown){
3452          var countdownValue=countdown.querySelector(".ph-tcell-countdown-value");
3453          if(key2===cardNextKey){
3454            var cardDiff=Math.max(0,Math.floor((cardNext.date-now)/1000));
3455            if(countdownValue) countdownValue.textContent=phCountdownDigits(cardDiff);
3456            else countdown.textContent=phCountdownDigits(cardDiff);
3457            countdown.setAttribute("aria-label", (phCopy().nextPrayer || "Next prayer")+" "+phCardCountdownText(cardDiff));
3458          } else {
3459            if(countdownValue) countdownValue.textContent="";
3460            else countdown.textContent="";
3461            countdown.removeAttribute("aria-label");
3462          }
3463        }
3464      }
3465      phSyncSunriseCard();
3466
3467      phSetNowIndicatorPos(phArcPos(nowM));
3468
3469      // Check if currently in a restricted prayer window (madhab-aware)
3470      var warnEl=ph$("#phNawafilWarn");
3471      var msgEl=ph$("#phNawafilMsg");
3472      var cdEl=ph$("#phWarnCountdown");
3473      var nowIndEl=phEls.nowInd;
3474      // Determine which madhab's windows to use
3475      var _warnMadhab = (typeof selectedSchool==="string" && PH_WINDOWS_BY_MADHAB[selectedSchool]) ? selectedSchool : "hanafi";
3476      var _warnWindows = PH_WINDOWS_BY_MADHAB[_warnMadhab];
3477      var activeWin=null, activeWinTo=0;
3478      for(var wi=0;wi<_warnWindows.length;wi++){
3479        var mw=_warnWindows[wi];
3480        var mwTiming=phWindowTiming(mw);
3481        if(mw.strict==="note"||mw.strict===null||mw.from===null) continue;
3482        var mwFrom=mwTiming.fromMin!==undefined?mwTiming.fromMin:phToMin(mwTiming.from);
3483        var mwTo=mwTiming.toMin!==undefined?mwTiming.toMin:(mwTiming.to?phToMin(mwTiming.to):0);
3484        if(!mwTo) continue;
3485        if(nowM>=mwFrom&&nowM<mwTo){activeWin=mw;activeWinTo=mwTo;break;}
3486      }
3487      if(warnEl&&msgEl){
3488        if(activeWin){
3489          warnEl.style.display="flex";
3490          var lc=phCopy();
3491          var _locale=phCurrentLocale();
3492          // Pick type label: strict=true → forbidden; strict=false + maliki → makruh; else makruh tahrimi
3493          var typeLabel;
3494          if(activeWin.strict===true){
3495            typeLabel=lc.strict||"Strictly forbidden";
3496          } else if(_warnMadhab==="maliki"){
3497            typeLabel=lc.makruhMaliki||"Disliked (makruh)";
3498          } else {
3499            typeLabel=lc.makruh||"Not recommended for nawafil";
3500          }
3501          // Pick localized window label
3502          var _i18nWin=(PH_WINDOWS_I18N[_locale]&&PH_WINDOWS_I18N[_locale][_warnMadhab]&&PH_WINDOWS_I18N[_locale][_warnMadhab][activeWin.key])||{};
3503          var windowLabel=_i18nWin.label||activeWin.label;
3504          // Secondary script label: show Arabic in en/ur, show English label in ar
3505          var secLabel=_locale==="ar"?activeWin.label:activeWin.ar;
3506          msgEl.innerHTML=typeLabel+' — '+windowLabel+(secLabel?'<span class="ph-warn-ar"> '+secLabel+'</span>':'');
3507          if(cdEl){
3508            var minsLeft=Math.ceil(activeWinTo-nowM);
3509            cdEl.textContent=lc.endsIn+" "+minsLeft+lc.minuteSuffix;
3510          }
3511        } else {
3512          warnEl.style.display="none";
3513        }
3514      }
3515      // Sun indicator turns red in any active restricted window
3516      if(nowIndEl){
3517        if(activeWin) nowIndEl.classList.add("is-forbidden");
3518        else nowIndEl.classList.remove("is-forbidden");
3519      }
3520      // Toggle sun/moon icon based on whether it's after Maghrib
3521      var sunEl=ph$("#phSkySun");
3522      var moonEl=ph$("#phSkyMoon");
3523      if(sunEl&&moonEl){
3524        var maghribMin=phToMin(PH_SUNSET);
3525        var fajrMin=phToMin(PH_PRAYERS[0].time);
3526        var isNight=nowM>=maghribMin||nowM<fajrMin;
3527        if(isNight){
3528          sunEl.classList.add("is-hidden");
3529          moonEl.classList.remove("is-hidden");
3530        } else {
3531          sunEl.classList.remove("is-hidden");
3532          moonEl.classList.add("is-hidden");
3533        }
3534      }
3535    }
3536
3537    phTick();
3538    window.addEventListener("ap:languagechange", function(){
3539      // Re-prime both systems on locale switch.
3540      phNowIndicatorPrimedLTR = false;
3541      phNowIndicatorPrimedRTL = false;
3542      phRenderStatic();
3543      phTick();
3544    });
3545    var phTickTimer = null;
3546    function startPhTick(){
3547      if(phTickTimer) return;
3548      phTickTimer = setInterval(phTick, 1000);
3549    }
3550    function stopPhTick(){
3551      if(!phTickTimer) return;
3552      clearInterval(phTickTimer);
3553      phTickTimer = null;
3554    }
3555    startPhTick();
3556    document.addEventListener("visibilitychange", function(){
3557      if(document.hidden){
3558        stopPhTick();
3559      } else {
3560        phTick();
3561        startPhTick();
3562      }
3563    });
3564
3565    // Auto-scroll arc to current time on mobile, then offer a one-time cue that
3566    // the full day is available by swiping horizontally.
3567    var phScrollEl=ph$("#phArcScroll");
3568    var PH_ARC_SWIPE_HINT_KEY = "ap_prayer_arc_swipe_hint_seen_v1";
3569    function phDismissArcSwipeHint(){
3570      var hint = ph$("#phArcSwipeHint");
3571      if(!hint) return;
3572      hint.classList.remove("is-visible");
3573      hint.hidden = true;
3574      try { localStorage.setItem(PH_ARC_SWIPE_HINT_KEY, "1"); } catch(_){}
3575    }
3576    function phInitArcSwipeHint(){
3577      var hint = ph$("#phArcSwipeHint");
3578      if(!phScrollEl || !hint || window.innerWidth > 640) return;
3579      var hasOverflow = phScrollEl.scrollWidth > phScrollEl.clientWidth + 8;
3580      var wasSeen = false;
3581      try { wasSeen = localStorage.getItem(PH_ARC_SWIPE_HINT_KEY) === "1"; } catch(_){}
3582      if(!hasOverflow || wasSeen) return;
3583      hint.hidden = false;
3584      hint.classList.add("is-visible");
3585      phScrollEl.addEventListener("pointerdown", phDismissArcSwipeHint, { once:true, passive:true });
3586      phScrollEl.addEventListener("keydown", function(event){
3587        if(event.key === "ArrowLeft" || event.key === "ArrowRight") phDismissArcSwipeHint();
3588      }, { once:true });
3589    }
3590    if(phScrollEl && window.innerWidth<=640){
3591      var nowInd=ph$("#phNowIndicator");
3592      requestAnimationFrame(function(){
3593        if(nowInd){
3594          var indLeft=nowInd.offsetLeft;
3595          var scrollW=phScrollEl.clientWidth;
3596          phScrollEl.scrollLeft=Math.max(0, indLeft - scrollW/2);
3597        }
3598        requestAnimationFrame(phInitArcSwipeHint);
3599      });
3600    }
3601
3602    function phClearTimings(){
3603      PH_DATA_READY = false;
3604      for(var i=0;i<PH_PRAYERS.length;i++) PH_PRAYERS[i].time = "";
3605      PH_SUNRISE = "";
3606      PH_SUNSET = "";
3607      PH_ISHRAQ = "";
3608      PH_FAJR_FARD_DONE = "";
3609      PH_ASR_FARD_DONE = "";
3610      PH_SUNSET_BEGINS = "";
3611      PH_ZAWAL_START = null;
3612      PH_ZAWAL_END = null;
3613      for(var j=0;j<PH_FORBIDDEN.length;j++){
3614        PH_FORBIDDEN[j].from = null;
3615        PH_FORBIDDEN[j].to = null;
3616        PH_FORBIDDEN[j].fromMin = null;
3617        PH_FORBIDDEN[j].toMin = null;
3618      }
3619      phRenderStatic();
3620      phTick();
3621    }
3622
3623    // Expose an API so the location/city wiring can update times in place.
3624    window.AP_PRAYER_HERO = {
3625      clearTimings: phClearTimings,
3626      setTimezone: function(timezone){
3627        if(typeof timezone === "string" && timezone){
3628          PH_TIMEZONE = timezone;
3629          phLast.h=phLast.m=phLast.s=phLast.key=null;
3630          phTick();
3631        }
3632      },
3633      applyTimings: function(timings, opts){
3634        opts = opts || {};
3635        if(opts.timezone) PH_TIMEZONE = String(opts.timezone);
3636        var keys = ["fajr","dhuhr","asr","maghrib","isha"];
3637        var capKey = {fajr:"Fajr",dhuhr:"Dhuhr",asr:"Asr",maghrib:"Maghrib",isha:"Isha"};
3638        for(var i=0;i<keys.length;i++){
3639          var k=keys[i], apiK=capKey[k];
3640          if(timings && timings[apiK]){
3641            var t = String(timings[apiK]).split(" ")[0].slice(0,5);
3642            for(var j=0;j<PH_PRAYERS.length;j++){
3643              if(PH_PRAYERS[j].key===k){ PH_PRAYERS[j].time=t; break; }
3644            }
3645          }
3646        }
3647        if(timings && timings.Sunrise){
3648          PH_SUNRISE = String(timings.Sunrise).split(" ")[0].slice(0,5);
3649        }
3650        if(timings && timings.Sunset){
3651          PH_SUNSET = String(timings.Sunset).split(" ")[0].slice(0,5);
3652        }
3653        PH_DATA_READY = true;
3654        // Recompute derived zones
3655        var dhuhrT = (function(){
3656          for(var j=0;j<PH_PRAYERS.length;j++) if(PH_PRAYERS[j].key==="dhuhr") return PH_PRAYERS[j].time;
3657          return "";
3658        })();
3659        var sunsetT = PH_SUNSET;
3660        var sunriseT = PH_SUNRISE;
3661        var dhuhrMin = phToMin(dhuhrT);
3662        PH_ZAWAL_START = dhuhrMin - 12;
3663        PH_ZAWAL_END   = dhuhrMin;
3664        // After-Fajr/After-Asr windows begin after the user's own fard is done.
3665        // City/madhab can calculate adhan and Asr entry, but mosque jamaat is local
3666        // to each masjid, so use explicit conservative completion buffers.
3667        var fardBuffers = opts.fardBuffers || {};
3668        var fajrDoneOffset = typeof fardBuffers.fajr === "number" ? fardBuffers.fajr : 15;
3669        var asrDoneOffset = typeof fardBuffers.asr === "number" ? fardBuffers.asr : 15;
3670        function addMin(hhmm, addM){
3671          var m = (phToMin(hhmm) + addM + 1440) % 1440;
3672          return phPad(Math.floor(m/60)) + ":" + phPad(m%60);
3673        }
3674        var fajrT = PH_PRAYERS[0].time;
3675        var asrT  = (function(){ for(var j=0;j<PH_PRAYERS.length;j++) if(PH_PRAYERS[j].key==="asr") return PH_PRAYERS[j].time; return ""; })();
3676        PH_FAJR_FARD_DONE = addMin(fajrT, fajrDoneOffset);
3677        PH_ASR_FARD_DONE  = addMin(asrT, asrDoneOffset);
3678        // Sunset begins 20 min before sunset/maghrib
3679        PH_SUNSET_BEGINS  = addMin(sunsetT, -20);
3680        // Ishraq ~20 min after sunrise
3681        PH_ISHRAQ         = addMin(sunriseT, 20);
3682        // Rebuild forbidden zones with new times
3683        PH_FORBIDDEN[0].from = PH_FAJR_FARD_DONE; PH_FORBIDDEN[0].to = PH_SUNRISE;
3684        PH_FORBIDDEN[1].from = PH_SUNRISE;        PH_FORBIDDEN[1].to = PH_ISHRAQ;
3685        PH_FORBIDDEN[2].fromMin = PH_ZAWAL_START; PH_FORBIDDEN[2].toMin = PH_ZAWAL_END;
3686        PH_FORBIDDEN[3].from = PH_ASR_FARD_DONE;  PH_FORBIDDEN[3].to = PH_SUNSET_BEGINS;
3687        PH_FORBIDDEN[4].from = PH_SUNSET_BEGINS;  PH_FORBIDDEN[4].to = PH_SUNSET;
3688        phLast.h=phLast.m=phLast.s=phLast.key=null;
3689        phRenderStatic();
3690        if(opts.methodNote){
3691          phMethodBaseNote = opts.methodNote;
3692          phUpdateMethodNote();
3693        }
3694        phTick();
3695      }
3696    };
3697  } catch(e) {
3698    // Fail silent: never break the landing.
3699  }
3700})();
3701
3702/* =================================================================
3703   LOCATION / CITY WIRING — geolocation + city picker + Aladhan API
3704   ================================================================= */
3705(function(){
3706  "use strict";
3707  try{
3708    function currentLocale(){
3709      return (window.AP_I18N && typeof window.AP_I18N.current === "function" ? window.AP_I18N.current() : "en") || "en";
3710    }
3711    var PRAYER_UI = {
3712      en: {
3713        loading: "Loading prayer times…",
3714        loadFail: "Could not load verified prayer times. Please try again.",
3715        geoUnavailable: "Geolocation is not available in this browser",
3716        detecting: "Detecting your location…",
3717        geoDenied: "Location permission denied — choose a city instead",
3718        geoTimeout: "Location request timed out — choose a city instead",
3719        geoFail: "Could not get your location",
3720        choosePrompt: "Detect your location or choose a city",
3721        chooseCity: "Choose city",
3722        chooseCityShort: "City",
3723        changeCity: "Change city",
3724        noCity: "No cities match",
3725        chooseApplyMethod: "Choose a city or use location to apply the saved method",
3726        chooseApplyBuffers: "Choose a city or use location to apply fard completion settings",
3727        savedReset: "Saved prayer settings reset",
3728        settings: "Settings",
3729        prayerSettings: "Prayer settings",
3730        settingsHelp: "Saved on this device until you reset them.",
3731        calculationMethod: "Calculation method",
3732        asrMadhab: "Asr madhab",
3733        savedAsrMadhab: "Saved Asr madhab",
3734        timeFormat: "Time format",
3735        savedTimeFormat: "Saved time format",
3736        fajrCompletion: "Fajr fard completion",
3737        asrCompletion: "Asr fard completion",
3738        resetSaved: "Reset saved settings",
3739        autoByCountry: "Auto by country",
3740        hanafi: "Hanafi",
3741        standardSchool: "Shafi · Maliki · Hanbali"
3742      },
3743      ar: {
3744        loading: "جارٍ تحميل مواقيت الصلاة…",
3745        loadFail: "تعذّر تحميل أوقات الصلاة الموثقة. يُرجى المحاولة مرة أخرى.",
3746        geoUnavailable: "خدمة تحديد الموقع غير متاحة في هذا المتصفح",
3747        detecting: "جارٍ تحديد موقعك…",
3748        geoDenied: "تم رفض إذن الموقع — اختر مدينة بدلاً من ذلك",
3749        geoTimeout: "انتهت مهلة طلب الموقع — اختر مدينة بدلاً من ذلك",
3750        geoFail: "تعذّر الحصول على موقعك",
3751        choosePrompt: "حدّد موقعك أو اختر مدينة",
3752        chooseCity: "اختر مدينة",
3753        chooseCityShort: "مدينة",
3754        changeCity: "تغيير المدينة",
3755        noCity: "لا توجد مدن تطابق",
3756        chooseApplyMethod: "اختر مدينة أو استخدم الموقع لتطبيق الطريقة المحفوظة",
3757        chooseApplyBuffers: "اختر مدينة أو استخدم الموقع لتطبيق إعدادات إتمام الفرض",
3758        savedReset: "تمت إعادة ضبط إعدادات الصلاة المحفوظة",
3759        settings: "الإعدادات",
3760        prayerSettings: "إعدادات الصلاة",
3761        settingsHelp: "تُحفظ على هذا الجهاز حتى تقوم بإعادة ضبطها.",
3762        calculationMethod: "طريقة الحساب",
3763        asrMadhab: "مذهب العصر",
3764        savedAsrMadhab: "مذهب العصر المحفوظ",
3765        timeFormat: "تنسيق الوقت",
3766        savedTimeFormat: "تنسيق الوقت المحفوظ",
3767        fajrCompletion: "إتمام فرض الفجر",
3768        asrCompletion: "إتمام فرض العصر",
3769        resetSaved: "إعادة ضبط الإعدادات المحفوظة",
3770        autoByCountry: "تلقائي حسب الدولة",
3771        hanafi: "حنفي",
3772        standardSchool: "شافعي · مالكي · حنبلي"
3773      },
3774      ur: {
3775        loading: "نماز کے اوقات لوڈ ہو رہے ہیں…",
3776        loadFail: "تصدیق شدہ نماز کے اوقات لوڈ نہیں ہو سکے۔ دوبارہ کوشش کریں۔",
3777        geoUnavailable: "اس براؤزر میں لوکیشن دستیاب نہیں",
3778        detecting: "آپ کا مقام معلوم کیا جا رہا ہے…",
3779        geoDenied: "لوکیشن کی اجازت مسترد ہوئی — شہر منتخب کریں",
3780        geoTimeout: "لوکیشن کی درخواست کا وقت ختم ہو گیا — شہر منتخب کریں",
3781        geoFail: "آپ کا مقام حاصل نہیں ہو سکا",
3782        choosePrompt: "اپنا مقام استعمال کریں یا شہر منتخب کریں",
3783        chooseCity: "شہر منتخب کریں",
3784        chooseCityShort: "شہر",
3785        changeCity: "شہر تبدیل کریں",
3786        noCity: "کوئی شہر مطابقت نہیں رکھتا",
3787        chooseApplyMethod: "محفوظ طریقہ لاگو کرنے کے لیے شہر منتخب کریں یا لوکیشن استعمال کریں",
3788        chooseApplyBuffers: "فرض مکمل ہونے کی ترتیبات لاگو کرنے کے لیے شہر منتخب کریں یا لوکیشن استعمال کریں",
3789        savedReset: "محفوظ نماز ترتیبات ری سیٹ کر دی گئیں",
3790        settings: "ترتیبات",
3791        prayerSettings: "نماز ترتیبات",
3792        settingsHelp: "یہ ترتیبات اسی ڈیوائس پر محفوظ رہیں گی جب تک آپ ری سیٹ نہ کریں۔",
3793        calculationMethod: "حساب کا طریقہ",
3794        asrMadhab: "عصر کا مسلک",
3795        savedAsrMadhab: "محفوظ عصر مسلک",
3796        timeFormat: "وقت فارمیٹ",
3797        savedTimeFormat: "محفوظ وقت فارمیٹ",
3798        fajrCompletion: "فجر فرض تکمیل",
3799        asrCompletion: "عصر فرض تکمیل",
3800        resetSaved: "محفوظ ترتیبات ری سیٹ کریں",
3801        autoByCountry: "ملک کے مطابق خودکار",
3802        hanafi: "حنفی",
3803        standardSchool: "شافعی · مالکی · حنبلی"
3804      }
3805    };
3806    function L(key){
3807      var locale = currentLocale();
3808      var copy = PRAYER_UI[locale] || PRAYER_UI.en;
3809      return copy[key] || PRAYER_UI.en[key] || "";
3810    }
3811    var LS_KEY = "ap_prayer_loc_v1";
3812    var PREFS_KEY = "ap_prayer_prefs_v1";
3813    var FORMAT_KEY = "ap_prayer_time_format_v1";
3814    var locText = document.getElementById("phLocText");
3815    var hijriEn = document.getElementById("phHijriEn");
3816    var hijriAr = document.getElementById("phHijriAr");
3817    var gregEl  = document.getElementById("phGregDate");
3818    var btnLoc  = document.getElementById("phBtnLocate");
3819    var btnCity = document.getElementById("phBtnCity");
3820    var btnSettings = document.getElementById("phBtnSettings");
3821    var modal   = document.getElementById("apCityModal");
3822    var settingsModal = document.getElementById("apPrayerSettingsModal");
3823    var search  = document.getElementById("apCitySearch");
3824    var listEl  = document.getElementById("apCityList");
3825    var clearBtn= document.getElementById("apCityClear");
3826    var methodSelect = document.getElementById("phMethodSelect");
3827    var fajrBufferInput = document.getElementById("phFajrBuffer");
3828    var asrBufferInput = document.getElementById("phAsrBuffer");
3829    var resetPrefsBtn = document.getElementById("phResetPrefs");
3830    var schoolBtns = Array.prototype.slice.call(document.querySelectorAll("[data-school]"));
3831    var activeModal = null;
3832    var modalRestoreFocus = null;
3833
3834    function modalFocusableElements(target){
3835      if(!target) return [];
3836      return Array.prototype.slice.call(target.querySelectorAll("button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not
3836([tabindex='-1'])"));
3837    }
3838    function openManagedModal(target, firstFocus){
3839      if(!target) return;
3840      if(activeModal && activeModal !== target) closeManagedModal(activeModal, false);
3841      activeModal = target;
3842      modalRestoreFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3843      document.documentElement.classList.add("ap-modal-open");
3844      document.body.classList.add("ap-modal-open");
3845      target.setAttribute("aria-hidden", "false");
3846      target.classList.add("is-open");
3847      window.setTimeout(function(){
3848        var focusTarget = firstFocus || modalFocusableElements(target)[0];
3849        if(focusTarget && typeof focusTarget.focus === "function"){
3850          try{ focusTarget.focus({preventScroll:true}); }catch(_){ focusTarget.focus(); }
3851        }
3852      }, 50);
3853    }
3854    function closeManagedModal(target, restoreFocus){
3855      if(!target) return;
3856      target.setAttribute("aria-hidden", "true");
3857      target.classList.remove("is-open");
3858      if(activeModal !== target) return;
3859      activeModal = null;
3860      document.documentElement.classList.remove("ap-modal-open");
3861      document.body.classList.remove("ap-modal-open");
3862      var restore = modalRestoreFocus;
3863      modalRestoreFocus = null;
3864      if(restoreFocus !== false && restore && restore.isConnected && typeof restore.focus === "function"){
3865        try{ restore.focus({preventScroll:true}); }catch(_){ restore.focus(); }
3866      }
3867    }
3868
3869    var METHOD_BY_COUNTRY = {
3870      "Pakistan": {id:1, name:"University of Islamic Sciences, Karachi"},
3871      "India": {id:1, name:"University of Islamic Sciences, Karachi"},
3872      "Bangladesh": {id:1, name:"University of Islamic Sciences, Karachi"},
3873      "Afghanistan": {id:1, name:"University of Islamic Sciences, Karachi"},
3874      "USA": {id:2, name:"Islamic Society of North America"},
3875      "United States": {id:2, name:"Islamic Society of North America"},
3876      "Canada": {id:2, name:"Islamic Society of North America"},
3877      "United Kingdom": {id:3, name:"Muslim World League"},
3878      "France": {id:12, name:"Union Organization Islamic de France"},
3879      "Germany": {id:3, name:"Muslim World League"},
3880      "Spain": {id:3, name:"Muslim World League"},
3881      "Italy": {id:3, name:"Muslim World League"},
3882      "Netherlands": {id:3, name:"Muslim World League"},
3883      "Belgium": {id:3, name:"Muslim World League"},
3884      "Sweden": {id:3, name:"Muslim World League"},
3885      "Norway": {id:3, name:"Muslim World League"},
3886      "Saudi Arabia": {id:4, name:"Umm Al-Qura University, Makkah"},
3887      "Egypt": {id:5, name:"Egyptian General Authority of Survey"},
3888      "Iran": {id:7, name:"Institute of Geophysics, University of Tehran"},
3889      "UAE": {id:16, name:"Dubai / UAE regional convention"},
3890      "United Arab Emirates": {id:16, name:"Dubai / UAE regional convention"},
3891      "Qatar": {id:10, name:"Qatar"},
3892      "Kuwait": {id:9, name:"Kuwait"},
3893      "Bahrain": {id:8, name:"Gulf Region"},
3894      "Oman": {id:8, name:"Gulf Region"},
3895      "Jordan": {id:3, name:"Muslim World League"},
3896      "Lebanon": {id:3, name:"Muslim World League"},
3897      "Syria": {id:3, name:"Muslim World League"},
3898      "Iraq": {id:3, name:"Muslim World League"},
3899      "Turkey": {id:13, name:"Diyanet Isleri Baskanligi, Turkey"},
3900      "Indonesia": {id:20, name:"Kementerian Agama Republik Indonesia"},
3901      "Malaysia": {id:17, name:"JAKIM, Malaysia"},
3902      "Singapore": {id:11, name:"Majlis Ugama Islam Singapura"},
3903      "Morocco": {id:21, name:"Morocco"},
3904      "Algeria": {id:19, name:"Algeria"},
3905      "Tunisia": {id:18, name:"Tunisia"},
3906      "Nigeria": {id:3, name:"Muslim World League"},
3907      "Sudan": {id:3, name:"Muslim World League"},
3908      "Australia": {id:3, name:"Muslim World League"}
3909    };
3910    var DEFAULT_METHOD = {id:3, name:"Muslim World League"};
3911    var METHOD_BY_ID = {};
3912    Object.keys(METHOD_BY_COUNTRY).forEach(function(country){
3913      METHOD_BY_ID[METHOD_BY_COUNTRY[country].id] = METHOD_BY_COUNTRY[country].name;
3914    });
3915    var SCHOOL_LABELS = {
3916      hanafi:  "Hanafi Asr",
3917      shafii:  "Shafi'i Asr",
3918      hanbali: "Hanbali Asr",
3919      maliki:  "Maliki Asr",
3920      standard: "Shafi/Maliki/Hanbali Asr"
3921    };
3922    var FARD_BUFFER_BY_REGION = {
3923      default: {fajr:15, asr:15},
3924      southAsia: {fajr:27, asr:20},
3925      gulf: {fajr:25, asr:15}
3926    };
3927    var SOUTH_ASIA = {"Pakistan":1,"India":1,"Bangladesh":1,"Afghanistan":1};
3928    var GULF = {"Saudi Arabia":1,"UAE":1,"United Arab Emirates":1,"Qatar":1,"Kuwait":1,"Bahrain":1,"Oman":1};
3929    var selectedSchool = "hanafi";
3930    var prayerRequestSerial = 0;
3931    function syncSelectedSchool(){ window.selectedSchool = selectedSchool; }
3932    var lastLoc = null;
3933    var prefs = readPrefs();
3934    (function(){
3935      var _s = prefs.school;
3936      if(_s === "hanafi" || _s === "shafii" || _s === "hanbali" || _s === "maliki") selectedSchool = _s;
3937      else if(_s === "standard") selectedSchool = "shafii"; // backward compat
3938    })();
3939    syncSelectedSchool();
3940
3941    var CITIES = [
3942      {
3943            "name": "Herat",
3944            "country": "Afghanistan",
3945            "lat": 34.3482,
3946            "lon": 62.1997,
3947            "tz": "Asia/Kabul"
3948      },
3949      {
3950            "name": "Kabul",
3951            "country": "Afghanistan",
3952            "lat": 34.5281,
3953            "lon": 69.1723,
3954            "tz": "Asia/Kabul"
3955      },
3956      {
3957            "name": "Kandahar",
3958            "country": "Afghanistan",
3959            "lat": 31.6133,
3960            "lon": 65.7101,
3961            "tz": "Asia/Kabul"
3962      },
3963      {
3964            "name": "Mazar-e Sharif",
3965            "country": "Afghanistan",
3966            "lat": 36.709,
3967            "lon": 67.1109,
3968            "tz": "Asia/Kabul"
3969      },
3970      {
3971            "name": "Shkoder",
3972            "country": "Albania",
3973            "lat": 42.0683,
3974            "lon": 19.5126,
3975            "tz": "Europe/Tirane"
3976      },
3977      {
3978            "name": "Tirana",
3979            "country": "Albania",
3980            "lat": 41.3274,
3981            "lon": 19.8187,
3982            "tz": "Europe/Tirane"
3983      },
3984      {
3985            "name": "Algiers",
3986            "country": "Algeria",
3987            "lat": 36.7323,
3988            "lon": 3.0875,
3989            "tz": "Africa/Algiers"
3990      },
3991      {
3992            "name": "Annaba",
3993            "country": "Algeria",
3994            "lat": 36.9,
3995            "lon": 7.7667,
3996            "tz": "Africa/Algiers"
3997      },
3998      {
3999            "name": "Constantine",
4000            "country": "Algeria",
4001            "lat": 36.365,
4002            "lon": 6.6147,
4003            "tz": "Africa/Algiers"
4004      },
4005      {
4006            "name": "Oran",
4007            "country": "Algeria",
4008            "lat": 35.6991,
4009            "lon": -0.6359,
4010            "tz": "Africa/Algiers"
4011      },
4012      {
4013            "name": "Buenos Aires",
4014            "country": "Argentina",
4015            "lat": -34.6131,
4016            "lon": -58.3772,
4017            "tz": "America/Argentina/Buenos_Aires"
4018      },
4019      {
4020            "name": "Adelaide",
4021            "country": "Australia",
4022            "lat": -34.9287,
4023            "lon": 138.5986,
4024            "tz": "Australia/Adelaide"
4025      },
4026      {
4027            "name": "Brisbane",
4028            "country": "Australia",
4029            "lat": -27.4679,
4030            "lon": 153.0281,
4031            "tz": "Australia/Brisbane"
4032      },
4033      {
4034            "name": "Canberra",
4035            "country": "Australia",
4036            "lat": -35.2835,
4037            "lon": 149.1281,
4038            "tz": "Australia/Sydney"
4039      },
4040      {
4041            "name": "Melbourne",
4042            "country": "Australia",
4043            "lat": -37.814,
4044            "lon": 144.9633,
4045            "tz": "Australia/Melbourne"
4046      },
4047      {
4048            "name": "Perth",
4049            "country": "Australia",
4050            "lat": -31.9522,
4051            "lon": 115.8614,
4052            "tz": "Australia/Perth"
4053      },
4054      {
4055            "name": "Sydney",
4056            "country": "Australia",
4057            "lat": -33.8678,
4058            "lon": 151.2073,
4059            "tz": "Australia/Sydney"
4060      },
4061      {
4062            "name": "Vienna",
4063            "country": "Austria",
4064            "lat": 48.2085,
4065            "lon": 16.3721,
4066            "tz": "Europe/Vienna"
4067      },
4068      {
4069            "name": "Baku",
4070            "country": "Azerbaijan",
4071            "lat": 40.3777,
4072            "lon": 49.892,
4073            "tz": "Asia/Baku"
4074      },
4075      {
4076            "name": "Ganja",
4077            "country": "Azerbaijan",
4078            "lat": 40.6816,
4079            "lon": 46.3613,
4080            "tz": "Asia/Baku"
4081      },
4082      {
4083            "name": "Manama",
4084            "country": "Bahrain",
4085            "lat": 26.2279,
4086            "lon": 50.5857,
4087            "tz": "Asia/Bahrain"
4088      },
4089      {
4090            "name": "Chattogram",
4091            "country": "Bangladesh",
4092            "lat": 22.3384,
4093            "lon": 91.8317,
4094            "tz": "Asia/Dhaka"
4095      },
4096      {
4097            "name": "Dhaka",
4098            "country": "Bangladesh",
4099            "lat": 23.7104,
4100            "lon": 90.4074,
4101            "tz": "Asia/Dhaka"
4102      },
4103      {
4104            "name": "Khulna",
4105            "country": "Bangladesh",
4106            "lat": 22.8098,
4107            "lon": 89.5644,
4108            "tz": "Asia/Dhaka"
4109      },
4110      {
4111            "name": "Rajshahi",
4112            "country": "Bangladesh",
4113            "lat": 24.374,
4114            "lon": 88.6011,
4115            "tz": "Asia/Dhaka"
4116      },
4117      {
4118            "name": "Sylhet",
4119            "country": "Bangladesh",
4120            "lat": 24.899,
4121            "lon": 91.872,
4122            "tz": "Asia/Dhaka"
4123      },
4124      {
4125            "name": "Antwerpen",
4126            "country": "Belgium",
4127            "lat": 51.2205,
4128            "lon": 4.4003,
4129            "tz": "Europe/Brussels"
4130      },
4131      {
4132            "name": "Brussels",
4133            "country": "Belgium",
4134            "lat": 50.8505,
4135            "lon": 4.3488,
4136            "tz": "Europe/Brussels"
4137      },
4138      {
4139            "name": "Cotonou",
4140            "country": "Benin",
4141            "lat": 6.3654,
4142            "lon": 2.4183,
4143            "tz": "Africa/Porto-Novo"
4144      },
4145      {
4146            "name": "Sarajevo",
4147            "country": "Bosnia and Herzegovina",
4148            "lat": 43.8486,
4149            "lon": 18.3564,
4150            "tz": "Europe/Sarajevo"
4151      },
4152      {
4153            "name": "Tuzla",
4154            "country": "Bosnia and Herzegovina",
4155            "lat": 44.5384,
4156            "lon": 18.6671,
4157            "tz": "Europe/Sarajevo"
4158      },
4159      {
4160            "name": "Rio de Janeiro",
4161            "country": "Brazil",
4162            "lat": -22.9064,
4163            "lon": -43.1822,
4164            "tz": "America/Sao_Paulo"
4165      },
4166      {
4167            "name": "Sao Paulo",
4168            "country": "Brazil",
4169            "lat": -23.5475,
4170            "lon": -46.6361,
4171            "tz": "America/Sao_Paulo"
4172      },
4173      {
4174            "name": "Bandar Seri Begawan",
4175            "country": "Brunei",
4176            "lat": 4.8903,
4177            "lon": 114.9401,
4178            "tz": "Asia/Brunei"
4179      },
4180      {
4181            "name": "Calgary",
4182            "country": "Canada",
4183            "lat": 51.0501,
4184            "lon": -114.0853,
4185            "tz": "America/Edmonton"
4186      },
4187      {
4188            "name": "Edmonton",
4189            "country": "Canada",
4190            "lat": 53.5501,
4191            "lon": -113.4687,
4192            "tz": "America/Edmonton"
4193      },
4194      {
4195            "name": "Montreal",
4196            "country": "Canada",
4197            "lat": 45.5088,
4198            "lon": -73.5878,
4199            "tz": "America/Toronto"
4200      },
4201      {
4202            "name": "Ottawa",
4203            "country": "Canada",
4204            "lat": 45.4112,
4205            "lon": -75.6981,
4206            "tz": "America/Toronto"
4207      },
4208      {
4209            "name": "Toronto",
4210            "country": "Canada",
4211            "lat": 43.7064,
4212            "lon": -79.3986,
4213            "tz": "America/Toronto"
4214      },
4215      {
4216            "name": "Vancouver",
4217            "country": "Canada",
4218            "lat": 49.2497,
4219            "lon": -123.1193,
4220            "tz": "America/Vancouver"
4221      },
4222      {
4223            "name": "N'Djamena",
4224            "country": "Chad",
4225            "lat": 12.1067,
4226            "lon": 15.0444,
4227            "tz": "Africa/Ndjamena"
4228      },
4229      {
4230            "name": "Beijing",
4231            "country": "China",
4232            "lat": 39.9075,
4233            "lon": 116.3972,
4234            "tz": "Asia/Shanghai"
4235      },
4236      {
4237            "name": "Guangzhou",
4238            "country": "China",
4239            "lat": 23.1167,
4240            "lon": 113.25,
4241            "tz": "Asia/Shanghai"
4242      },
4243      {
4244            "name": "Lanzhou",
4245            "country": "China",
4246            "lat": 36.057,
4247            "lon": 103.8399,
4248            "tz": "Asia/Shanghai"
4249      },
4250      {
4251            "name": "Shanghai",
4252            "country": "China",
4253            "lat": 31.2222,
4254            "lon": 121.4581,
4255            "tz": "Asia/Shanghai"
4256      },
4257      {
4258            "name": "Urumqi",
4259            "country": "China",
4260            "lat": 43.801,
4261            "lon": 87.6005,
4262            "tz": "Asia/Urumqi"
4263      },
4264      {
4265            "name": "Xi'an",
4266            "country": "China",
4267            "lat": 34.2583,
4268            "lon": 108.9286,
4269            "tz": "Asia/Shanghai"
4270      },
4271      {
4272            "name": "Copenhagen",
4273            "country": "Denmark",
4274            "lat": 55.6759,
4275            "lon": 12.5655,
4276            "tz": "Europe/Copenhagen"
4277      },
4278      {
4279            "name": "Alexandria",
4280            "country": "Egypt",
4281            "lat": 31.2018,
4282            "lon": 29.9158,
4283            "tz": "Africa/Cairo"
4284      },
4285      {
4286            "name": "Asyut",
4287            "country": "Egypt",
4288            "lat": 27.181,
4289            "lon": 31.1837,
4290            "tz": "Africa/Cairo"
4291      },
4292      {
4293            "name": "Cairo",
4294            "country": "Egypt",
4295            "lat": 30.0626,
4296            "lon": 31.2497,
4297            "tz": "Africa/Cairo"
4298      },
4299      {
4300            "name": "Giza",
4301            "country": "Egypt",
4302            "lat": 30.0094,
4303            "lon": 31.2086,
4304            "tz": "Africa/Cairo"
4305      },
4306      {
4307            "name": "Luxor",
4308            "country": "Egypt",
4309            "lat": 25.6989,
4310            "lon": 32.6421,
4311            "tz": "Africa/Cairo"
4312      },
4313      {
4314            "name": "Port Said",
4315            "country": "Egypt",
4316            "lat": 31.2653,
4317            "lon": 32.3019,
4318            "tz": "Africa/Cairo"
4319      },
4320      {
4321            "name": "Addis Ababa",
4322            "country": "Ethiopia",
4323            "lat": 9.025,
4324            "lon": 38.7469,
4325            "tz": "Africa/Addis_Ababa"
4326      },
4327      {
4328            "name": "Dire Dawa",
4329            "country": "Ethiopia",
4330            "lat": 9.5931,
4331            "lon": 41.8661,
4332            "tz": "Africa/Addis_Ababa"
4333      },
4334      {
4335            "name": "Harar",
4336            "country": "Ethiopia",
4337            "lat": 9.3139,
4338            "lon": 42.1181,
4339            "tz": "Africa/Addis_Ababa"
4340      },
4341      {
4342            "name": "Lyon",
4343            "country": "France",
4344            "lat": 45.7491,
4345            "lon": 4.8479,
4346            "tz": "Europe/Paris"
4347      },
4348      {
4349            "name": "Marseille",
4350            "country": "France",
4351            "lat": 43.297,
4352            "lon": 5.3811,
4353            "tz": "Europe/Paris"
4354      },
4355      {
4356            "name": "Paris",
4357            "country": "France",
4358            "lat": 48.8534,
4359            "lon": 2.3488,
4360            "tz": "Europe/Paris"
4361      },
4362      {
4363            "name": "Toulouse",
4364            "country": "France",
4365            "lat": 43.6043,
4366            "lon": 1.4437,
4367            "tz": "Europe/Paris"
4368      },
4369      {
4370            "name": "Banjul",
4371            "country": "Gambia",
4372            "lat": 13.4527,
4373            "lon": -16.578,
4374            "tz": "Africa/Banjul"
4375      },
4376      {
4377            "name": "Berlin",
4378            "country": "Germany",
4379            "lat": 52.5244,
4380            "lon": 13.4105,
4381            "tz": "Europe/Berlin"
4382      },
4383      {
4384            "name": "Frankfurt am Main",
4385            "country": "Germany",
4386            "lat": 50.1155,
4387            "lon": 8.6842,
4388            "tz": "Europe/Berlin"
4389      },
4390      {
4391            "name": "Hamburg",
4392            "country": "Germany",
4393            "lat": 53.5507,
4394            "lon": 9.993,
4395            "tz": "Europe/Berlin"
4396      },
4397      {
4398            "name": "Koln",
4399            "country": "Germany",
4400            "lat": 50.9333,
4401            "lon": 6.95,
4402            "tz": "Europe/Berlin"
4403      },
4404      {
4405            "name": "Munich",
4406            "country": "Germany",
4407            "lat": 48.1374,
4408            "lon": 11.5755,
4409            "tz": "Europe/Berlin"
4410      },
4411      {
4412            "name": "Accra",
4413            "country": "Ghana",
4414            "lat": 5.556,
4415            "lon": -0.1969,
4416            "tz": "Africa/Accra"
4417      },
4418      {
4419            "name": "Kumasi",
4420            "country": "Ghana",
4421            "lat": 6.6885,
4422            "lon": -1.6244,
4423            "tz": "Africa/Accra"
4424      },
4425      {
4426            "name": "Conakry",
4427            "country": "Guinea",
4428            "lat": 9.538,
4429            "lon": -13.6773,
4430            "tz": "Africa/Conakry"
4431      },
4432      {
4433            "name": "Georgetown",
4434            "country": "Guyana",
4435            "lat": 6.8045,
4436            "lon": -58.1553,
4437            "tz": "America/Guyana"
4438      },
4439      {
4440            "name": "Hong Kong",
4441            "country": "Hong Kong",
4442            "lat": 22.2783,
4443            "lon": 114.1747,
4444            "tz": "Asia/Hong_Kong"
4445      },
4446      {
4447            "name": "Ahmedabad",
4448            "country": "India",
4449            "lat": 23.0258,
4450            "lon": 72.5873,
4451            "tz": "Asia/Kolkata"
4452      },
4453      {
4454            "name": "Aligarh",
4455            "country": "India",
4456            "lat": 27.8815,
4457            "lon": 78.0746,
4458            "tz": "Asia/Kolkata"
4459      },
4460      {
4461            "name": "Bengaluru",
4462            "country": "India",
4463            "lat": 12.9719,
4464            "lon": 77.5937,
4465            "tz": "Asia/Kolkata"
4466      },
4467      {
4468            "name": "Chennai",
4469            "country": "India",
4470            "lat": 13.0878,
4471            "lon": 80.2785,
4472            "tz": "Asia/Kolkata"
4473      },
4474      {
4475            "name": "Delhi",
4476            "country": "India",
4477            "lat": 28.6519,
4478            "lon": 77.2315,
4479            "tz": "Asia/Kolkata"
4480      },
4481      {
4482            "name": "Hyderabad",
4483            "country": "India",
4484            "lat": 17.384,
4485            "lon": 78.4564,
4486            "tz": "Asia/Kolkata"
4487      },
4488      {
4489            "name": "Kolkata",
4490            "country": "India",
4491            "lat": 22.5626,
4492            "lon": 88.363,
4493            "tz": "Asia/Kolkata"
4494      },
4495      {
4496            "name": "Kozhikode",
4497            "country": "India",
4498            "lat": 11.248,
4499            "lon": 75.7804,
4500            "tz": "Asia/Kolkata"
4501      },
4502      {
4503            "name": "Lucknow",
4504            "country": "India",
4505            "lat": 26.8393,
4506            "lon": 80.9231,
4507            "tz": "Asia/Kolkata"
4508      },
4509      {
4510            "name": "Mumbai",
4511            "country": "India",
4512            "lat": 19.0728,
4513            "lon": 72.8826,
4514            "tz": "Asia/Kolkata"
4515      },
4516      {
4517            "name": "Pune",
4518            "country": "India",
4519            "lat": 18.5196,
4520            "lon": 73.8554,
4521            "tz": "Asia/Kolkata"
4522      },
4523      {
4524            "name": "Srinagar",
4525            "country": "India",
4526            "lat": 34.0857,
4527            "lon": 74.8055,
4528            "tz": "Asia/Kolkata"
4529      },
4530      {
4531            "name": "Bandung",
4532            "country": "Indonesia",
4533            "lat": -6.9222,
4534            "lon": 107.6069,
4535            "tz": "Asia/Jakarta"
4536      },
4537      {
4538            "name": "Bekasi",
4539            "country": "Indonesia",
4540            "lat": -6.2349,
4541            "lon": 106.9896,
4542            "tz": "Asia/Jakarta"
4543      },
4544      {
4545            "name": "Depok",
4546            "country": "Indonesia",
4547            "lat": -6.4,
4548            "lon": 106.8186,
4549            "tz": "Asia/Jakarta"
4550      },
4551      {
4552            "name": "Jakarta",
4553            "country": "Indonesia",
4554            "lat": -6.2146,
4555            "lon": 106.8451,
4556            "tz": "Asia/Jakarta"
4557      },
4558      {
4559            "name": "Makassar",
4560            "country": "Indonesia",
4561            "lat": -5.1486,
4562            "lon": 119.4319,
4563            "tz": "Asia/Makassar"
4564      },
4565      {
4566            "name": "Medan",
4567            "country": "Indonesia",
4568            "lat": 3.5833,
4569            "lon": 98.6667,
4570            "tz": "Asia/Jakarta"
4571      },
4572      {
4573            "name": "Palembang",
4574            "country": "Indonesia",
4575            "lat": -2.9167,
4576            "lon": 104.7458,
4577            "tz": "Asia/Jakarta"
4578      },
4579      {
4580            "name": "Semarang",
4581            "country": "Indonesia",
4582            "lat": -6.9931,
4583            "lon": 110.4208,
4584            "tz": "Asia/Jakarta"
4585      },
4586      {
4587            "name": "Surabaya",
4588            "country": "Indonesia",
4589            "lat": -7.2492,
4590            "lon": 112.7508,
4591            "tz": "Asia/Jakarta"
4592      },
4593      {
4594            "name": "Yogyakarta",
4595            "country": "Indonesia",
4596            "lat": -7.8014,
4597            "lon": 110.3647,
4598            "tz": "Asia/Jakarta"
4599      },
4600      {
4601            "name": "Ahvaz",
4602            "country": "Iran",
4603            "lat": 31.319,
4604            "lon": 48.6842,
4605            "tz": "Asia/Tehran"
4606      },
4607      {
4608            "name": "Isfahan",
4609            "country": "Iran",
4610            "lat": 32.6525,
4611            "lon": 51.6746,
4612            "tz": "Asia/Tehran"
4613      },
4614      {
4615            "name": "Karaj",
4616            "country": "Iran",
4617            "lat": 35.8327,
4618            "lon": 50.9915,
4619            "tz": "Asia/Tehran"
4620      },
4621      {
4622            "name": "Mashhad",
4623            "country": "Iran",
4624            "lat": 36.2981,
4625            "lon": 59.6057,
4626            "tz": "Asia/Tehran"
4627      },
4628      {
4629            "name": "Qom",
4630            "country": "Iran",
4631            "lat": 34.6401,
4632            "lon": 50.8764,
4633            "tz": "Asia/Tehran"
4634      },
4635      {
4636            "name": "Shiraz",
4637            "country": "Iran",
4638            "lat": 29.6103,
4639            "lon": 52.5311,
4640            "tz": "Asia/Tehran"
4641      },
4642      {
4643            "name": "Tabriz",
4644            "country": "Iran",
4645            "lat": 38.08,
4646            "lon": 46.2919,
4647            "tz": "Asia/Tehran"
4648      },
4649      {
4650            "name": "Tehran",
4651            "country": "Iran",
4652            "lat": 35.6944,
4653            "lon": 51.4215,
4654            "tz": "Asia/Tehran"
4655      },
4656      {
4657            "name": "Baghdad",
4658            "country": "Iraq",
4659            "lat": 33.3406,
4660            "lon": 44.4009,
4661            "tz": "Asia/Baghdad"
4662      },
4663      {
4664            "name": "Basrah",
4665            "country": "Iraq",
4666            "lat": 30.5085,
4667            "lon": 47.7804,
4668            "tz": "Asia/Baghdad"
4669      },
4670      {
4671            "name": "Erbil",
4672            "country": "Iraq",
4673            "lat": 36.1912,
4674            "lon": 44.0094,
4675            "tz": "Asia/Baghdad"
4676      },
4677      {
4678            "name": "Mosul",
4679            "country": "Iraq",
4680            "lat": 36.335,
4681            "lon": 43.1189,
4682            "tz": "Asia/Baghdad"
4683      },
4684      {
4685            "name": "Milan",
4686            "country": "Italy",
4687            "lat": 45.4643,
4688            "lon": 9.1895,
4689            "tz": "Europe/Rome"
4690      },
4691      {
4692            "name": "Naples",
4693            "country": "Italy",
4694            "lat": 40.8522,
4695            "lon": 14.2681,
4696            "tz": "Europe/Rome"
4697      },
4698      {
4699            "name": "Rome",
4700            "country": "Italy",
4701            "lat": 41.8919,
4702            "lon": 12.5113,
4703            "tz": "Europe/Rome"
4704      },
4705      {
4706            "name": "Abidjan",
4707            "country": "Ivory Coast",
4708            "lat": 5.3544,
4709            "lon": -4.0017,
4710            "tz": "Africa/Abidjan"
4711      },
4712      {
4713            "name": "Nagoya",
4714            "country": "Japan",
4715            "lat": 35.1815,
4716            "lon": 136.9064,
4717            "tz": "Asia/Tokyo"
4718      },
4719      {
4720            "name": "Osaka",
4721            "country": "Japan",
4722            "lat": 34.6938,
4723            "lon": 135.5011,
4724            "tz": "Asia/Tokyo"
4725      },
4726      {
4727            "name": "Tokyo",
4728            "country": "Japan",
4729            "lat": 35.6895,
4730            "lon": 139.6917,
4731            "tz": "Asia/Tokyo"
4732      },
4733      {
4734            "name": "Amman",
4735            "country": "Jordan",
4736            "lat": 31.9552,
4737            "lon": 35.945,
4738            "tz": "Asia/Amman"
4739      },
4740      {
4741            "name": "Zarqa",
4742            "country": "Jordan",
4743            "lat": 32.0727,
4744            "lon": 36.088,
4745            "tz": "Asia/Amman"
4746      },
4747      {
4748            "name": "Almaty",
4749            "country": "Kazakhstan",
4750            "lat": 43.2525,
4751            "lon": 76.9115,
4752            "tz": "Asia/Almaty"
4753      },
4754      {
4755            "name": "Astana",
4756            "country": "Kazakhstan",
4757            "lat": 51.1801,
4758            "lon": 71.446,
4759            "tz": "Asia/Almaty"
4760      },
4761      {
4762            "name": "Shymkent",
4763            "country": "Kazakhstan",
4764            "lat": 42.3099,
4765            "lon": 69.6004,
4766            "tz": "Asia/Almaty"
4767      },
4768      {
4769            "name": "Kisumu",
4770            "country": "Kenya",
4771            "lat": -0.1022,
4772            "lon": 34.7617,
4773            "tz": "Africa/Nairobi"
4774      },
4775      {
4776            "name": "Mombasa",
4777            "country": "Kenya",
4778            "lat": -4.0547,
4779            "lon": 39.6636,
4780            "tz": "Africa/Nairobi"
4781      },
4782      {
4783            "name": "Nairobi",
4784            "country": "Kenya",
4785            "lat": -1.2833,
4786            "lon": 36.8167,
4787            "tz": "Africa/Nairobi"
4788      },
4789      {
4790            "name": "Pristina",
4791            "country": "Kosovo",
4792            "lat": 42.6727,
4793            "lon": 21.1669,
4794            "tz": "Europe/Belgrade"
4795      },
4796      {
4797            "name": "Prizren",
4798            "country": "Kosovo",
4799            "lat": 42.2139,
4800            "lon": 20.7397,
4801            "tz": "Europe/Belgrade"
4802      },
4803      {
4804            "name": "Kuwait City",
4805            "country": "Kuwait",
4806            "lat": 29.367,
4807            "lon": 47.9743,
4808            "tz": "Asia/Kuwait"
4809      },
4810      {
4811            "name": "Bishkek",
4812            "country": "Kyrgyzstan",
4813            "lat": 42.87,
4814            "lon": 74.59,
4815            "tz": "Asia/Bishkek"
4816      },
4817      {
4818            "name": "Osh",
4819            "country": "Kyrgyzstan",
4820            "lat": 40.5283,
4821            "lon": 72.7985,
4822            "tz": "Asia/Bishkek"
4823      },
4824      {
4825            "name": "Beirut",
4826            "country": "Lebanon",
4827            "lat": 33.8933,
4828            "lon": 35.5016,
4829            "tz": "Asia/Beirut"
4830      },
4831      {
4832            "name": "Tripoli",
4833            "country": "Lebanon",
4834            "lat": 34.4335,
4835            "lon": 35.8441,
4836            "tz": "Asia/Beirut"
4837      },
4838      {
4839            "name": "Benghazi",
4840            "country": "Libya",
4841            "lat": 32.1149,
4842            "lon": 20.0686,
4843            "tz": "Africa/Tripoli"
4844      },
4845      {
4846            "name": "Tripoli",
4847            "country": "Libya",
4848            "lat": 32.8874,
4849            "lon": 13.1873,
4850            "tz": "Africa/Tripoli"
4851      },
4852      {
4853            "name": "George Town",
4854            "country": "Malaysia",
4855            "lat": 5.4112,
4856            "lon": 100.3354,
4857            "tz": "Asia/Kuala_Lumpur"
4858      },
4859      {
4860            "name": "Johor Bahru",
4861            "country": "Malaysia",
4862            "lat": 1.4655,
4863            "lon": 103.7578,
4864            "tz": "Asia/Kuala_Lumpur"
4865      },
4866      {
4867            "name": "Kota Bharu",
4868            "country": "Malaysia",
4869            "lat": 6.1236,
4870            "lon": 102.2433,
4871            "tz": "Asia/Kuala_Lumpur"
4872      },
4873      {
4874            "name": "Kuala Lumpur",
4875            "country": "Malaysia",
4876            "lat": 3.1412,
4877            "lon": 101.6865,
4878            "tz": "Asia/Kuala_Lumpur"
4879      },
4880      {
4881            "name": "Shah Alam",
4882            "country": "Malaysia",
4883            "lat": 3.0851,
4884            "lon": 101.5328,
4885            "tz": "Asia/Kuala_Lumpur"
4886      },
4887      {
4888            "name": "Bamako",
4889            "country": "Mali",
4890            "lat": 12.6091,
4891            "lon": -7.9752,
4892            "tz": "Africa/Bamako"
4893      },
4894      {
4895            "name": "Nouakchott",
4896            "country": "Mauritania",
4897            "lat": 18.0858,
4898            "lon": -15.9785,
4899            "tz": "Africa/Nouakchott"
4900      },
4901      {
4902            "name": "Mexico City",
4903            "country": "Mexico",
4904            "lat": 19.4285,
4905            "lon": -99.1277,
4906            "tz": "America/Mexico_City"
4907      },
4908      {
4909            "name": "Tijuana",
4910            "country": "Mexico",
4911            "lat": 32.5027,
4912            "lon": -117.0037,
4913            "tz": "America/Tijuana"
4914      },
4915      {
4916            "name": "Agadir",
4917            "country": "Morocco",
4918            "lat": 30.4202,
4919            "lon": -9.5982,
4920            "tz": "Africa/Casablanca"
4921      },
4922      {
4923            "name": "Casablanca",
4924            "country": "Morocco",
4925            "lat": 33.5883,
4926            "lon": -7.6114,
4927            "tz": "Africa/Casablanca"
4928      },
4929      {
4930            "name": "Fes",
4931            "country": "Morocco",
4932            "lat": 34.0331,
4933            "lon": -5.0003,
4934            "tz": "Africa/Casablanca"
4935      },
4936      {
4937            "name": "Marrakesh",
4938            "country": "Morocco",
4939            "lat": 31.6342,
4940            "lon": -7.9999,
4941            "tz": "Africa/Casablanca"
4942      },
4943      {
4944            "name": "Oujda",
4945            "country": "Morocco",
4946            "lat": 34.6814,
4947            "lon": -1.9086,
4948            "tz": "Africa/Casablanca"
4949      },
4950      {
4951            "name": "Rabat",
4952            "country": "Morocco",
4953            "lat": 34.0132,
4954            "lon": -6.8326,
4955            "tz": "Africa/Casablanca"
4956      },
4957      {
4958            "name": "Tangier",
4959            "country": "Morocco",
4960            "lat": 35.7673,
4961            "lon": -5.7998,
4962            "tz": "Africa/Casablanca"
4963      },
4964      {
4965            "name": "Amsterdam",
4966            "country": "Netherlands",
4967            "lat": 52.374,
4968            "lon": 4.8897,
4969            "tz": "Europe/Amsterdam"
4970      },
4971      {
4972            "name": "Rotterdam",
4973            "country": "Netherlands",
4974            "lat": 51.9225,
4975            "lon": 4.4792,
4976            "tz": "Europe/Amsterdam"
4977      },
4978      {
4979            "name": "Auckland",
4980            "country": "New Zealand",
4981            "lat": -36.8485,
4982            "lon": 174.7635,
4983            "tz": "Pacific/Auckland"
4984      },
4985      {
4986            "name": "Wellington",
4987            "country": "New Zealand",
4988            "lat": -41.2866,
4989            "lon": 174.7756,
4990            "tz": "Pacific/Auckland"
4991      },
4992      {
4993            "name": "Niamey",
4994            "country": "Niger",
4995            "lat": 13.5137,
4996            "lon": 2.1098,
4997            "tz": "Africa/Niamey"
4998      },
4999      {
5000            "name": "Zinder",
5001            "country": "Niger",
5002            "lat": 13.8072,
5003            "lon": 8.9881,
5004            "tz": "Africa/Niamey"
5005      },
5006      {
5007            "name": "Abuja",
5008            "country": "Nigeria",
5009            "lat": 9.0579,
5010            "lon": 7.4951,
5011            "tz": "Africa/Lagos"
5012      },
5013      {
5014            "name": "Ibadan",
5015            "country": "Nigeria",
5016            "lat": 7.3776,
5017            "lon": 3.9059,
5018            "tz": "Africa/Lagos"
5019      },
5020      {
5021            "name": "Ilorin",
5022            "country": "Nigeria",
5023            "lat": 8.4966,
5024            "lon": 4.5421,
5025            "tz": "Africa/Lagos"
5026      },
5027      {
5028            "name": "Kaduna",
5029            "country": "Nigeria",
5030            "lat": 10.5264,
5031            "lon": 7.4388,
5032            "tz": "Africa/Lagos"
5033      },
5034      {
5035            "name": "Kano",
5036            "country": "Nigeria",
5037            "lat": 12.0001,
5038            "lon": 8.5167,
5039            "tz": "Africa/Lagos"
5040      },
5041      {
5042            "name": "Lagos",
5043            "country": "Nigeria",
5044            "lat": 6.4541,
5045            "lon": 3.3947,
5046            "tz": "Africa/Lagos"
5047      },
5048      {
5049            "name": "Maiduguri",
5050            "country": "Nigeria",
5051            "lat": 11.8469,
5052            "lon": 13.1571,
5053            "tz": "Africa/Lagos"
5054      },
5055      {
5056            "name": "Sokoto",
5057            "country": "Nigeria",
5058            "lat": 13.0627,
5059            "lon": 5.2432,
5060            "tz": "Africa/Lagos"
5061      },
5062      {
5063            "name": "Skopje",
5064            "country": "North Macedonia",
5065            "lat": 41.9965,
5066            "lon": 21.4314,
5067            "tz": "Europe/Skopje"
5068      },
5069      {
5070            "name": "Tetovo",
5071            "country": "North Macedonia",
5072            "lat": 42.0099,
5073            "lon": 20.9714,
5074            "tz": "Europe/Skopje"
5075      },
5076      {
5077            "name": "Oslo",
5078            "country": "Norway",
5079            "lat": 59.9127,
5080            "lon": 10.7461,
5081            "tz": "Europe/Oslo"
5082      },
5083      {
5084            "name": "Muscat",
5085            "country": "Oman",
5086            "lat": 23.5841,
5087            "lon": 58.4078,
5088            "tz": "Asia/Muscat"
5089      },
5090      {
5091            "name": "Salalah",
5092            "country": "Oman",
5093            "lat": 17.015,
5094            "lon": 54.0924,
5095            "tz": "Asia/Muscat"
5096      },
5097      {
5098            "name": "Faisalabad",
5099            "country": "Pakistan",
5100            "lat": 31.4155,
5101            "lon": 73.0897,
5102            "tz": "Asia/Karachi"
5103      },
5104      {
5105            "name": "Hyderabad",
5106            "country": "Pakistan",
5107            "lat": 25.3969,
5108            "lon": 68.3772,
5109            "tz": "Asia/Karachi"
5110      },
5111      {
5112            "name": "Islamabad",
5113            "country": "Pakistan",
5114            "lat": 33.7215,
5115            "lon": 73.0433,
5116            "tz": "Asia/Karachi"
5117      },
5118      {
5119            "name": "Karachi",
5120            "country": "Pakistan",
5121            "lat": 24.8608,
5122            "lon": 67.0104,
5123            "tz": "Asia/Karachi"
5124      },
5125      {
5126            "name": "Lahore",
5127            "country": "Pakistan",
5128            "lat": 31.558,
5129            "lon": 74.3507,
5130            "tz": "Asia/Karachi"
5131      },
5132      {
5133            "name": "Multan",
5134            "country": "Pakistan",
5135            "lat": 30.1968,
5136            "lon": 71.4782,
5137            "tz": "Asia/Karachi"
5138      },
5139      {
5140            "name": "Peshawar",
5141            "country": "Pakistan",
5142            "lat": 34.008,
5143            "lon": 71.5785,
5144            "tz": "Asia/Karachi"
5145      },
5146      {
5147            "name": "Quetta",
5148            "country": "Pakistan",
5149            "lat": 30.1841,
5150            "lon": 67.0014,
5151            "tz": "Asia/Karachi"
5152      },
5153      {
5154            "name": "Rawalpindi",
5155            "country": "Pakistan",
5156            "lat": 33.5973,
5157            "lon": 73.0479,
5158            "tz": "Asia/Karachi"
5159      },
5160      {
5161            "name": "Sialkot",
5162            "country": "Pakistan",
5163            "lat": 32.4927,
5164            "lon": 74.5313,
5165            "tz": "Asia/Karachi"
5166      },
5167      {
5168            "name": "Davao",
5169            "country": "Philippines",
5170            "lat": 7.0731,
5171            "lon": 125.6128,
5172            "tz": "Asia/Manila"
5173      },
5174      {
5175            "name": "Manila",
5176            "country": "Philippines",
5177            "lat": 14.6042,
5178            "lon": 120.9822,
5179            "tz": "Asia/Manila"
5180      },
5181      {
5182            "name": "Zamboanga",
5183            "country": "Philippines",
5184            "lat": 6.9103,
5185            "lon": 122.0739,
5186            "tz": "Asia/Manila"
5187      },
5188      {
5189            "name": "Doha",
5190            "country": "Qatar",
5191            "lat": 25.2855,
5192            "lon": 51.531,
5193            "tz": "Asia/Qatar"
5194      },
5195      {
5196            "name": "Grozny",
5197            "country": "Russia",
5198            "lat": 43.312,
5199            "lon": 45.6889,
5200            "tz": "Europe/Moscow"
5201      },
5202      {
5203            "name": "Kazan",
5204            "country": "Russia",
5205            "lat": 55.7887,
5206            "lon": 49.1221,
5207            "tz": "Europe/Moscow"
5208      },
5209      {
5210            "name": "Makhachkala",
5211            "country": "Russia",
5212            "lat": 42.9778,
5213            "lon": 47.5003,
5214            "tz": "Europe/Moscow"
5215      },
5216      {
5217            "name": "Moscow",
5218            "country": "Russia",
5219            "lat": 55.752,
5220            "lon": 37.6178,
5221            "tz": "Europe/Moscow"
5222      },
5223      {
5224            "name": "Saint Petersburg",
5225            "country": "Russia",
5226            "lat": 59.9386,
5227            "lon": 30.3141,
5228            "tz": "Europe/Moscow"
5229      },
5230      {
5231            "name": "Ufa",
5232            "country": "Russia",
5233            "lat": 54.7431,
5234            "lon": 55.9678,
5235            "tz": "Asia/Yekaterinburg"
5236      },
5237      {
5238            "name": "Dammam",
5239            "country": "Saudi Arabia",
5240            "lat": 26.4344,
5241            "lon": 50.1033,
5242            "tz": "Asia/Riyadh"
5243      },
5244      {
5245            "name": "Jeddah",
5246            "country": "Saudi Arabia",
5247            "lat": 21.4901,
5248            "lon": 39.1862,
5249            "tz": "Asia/Riyadh"
5250      },
5251      {
5252            "name": "Madinah",
5253            "country": "Saudi Arabia",
5254            "lat": 24.4686,
5255            "lon": 39.6142,
5256            "tz": "Asia/Riyadh"
5257      },
5258      {
5259            "name": "Makkah",
5260            "country": "Saudi Arabia",
5261            "lat": 21.4266,
5262            "lon": 39.8256,
5263            "tz": "Asia/Riyadh"
5264      },
5265      {
5266            "name": "Riyadh",
5267            "country": "Saudi Arabia",
5268            "lat": 24.6877,
5269            "lon": 46.7219,
5270            "tz": "Asia/Riyadh"
5271      },
5272      {
5273            "name": "Taif",
5274            "country": "Saudi Arabia",
5275            "lat": 21.2703,
5276            "lon": 40.4158,
5277            "tz": "Asia/Riyadh"
5278      },
5279      {
5280            "name": "Dakar",
5281            "country": "Senegal",
5282            "lat": 14.6937,
5283            "lon": -17.4441,
5284            "tz": "Africa/Dakar"
5285      },
5286      {
5287            "name": "Touba",
5288            "country": "Senegal",
5289            "lat": 14.8623,
5290            "lon": -15.8753,
5291            "tz": "Africa/Dakar"
5292      },
5293      {
5294            "name": "Freetown",
5295            "country": "Sierra Leone",
5296            "lat": 8.4871,
5297            "lon": -13.2356,
5298            "tz": "Africa/Freetown"
5299      },
5300      {
5301            "name": "Singapore",
5302            "country": "Singapore",
5303            "lat": 1.2897,
5304            "lon": 103.8501,
5305            "tz": "Asia/Singapore"
5306      },
5307      {
5308            "name": "Bosaso",
5309            "country": "Somalia",
5310            "lat": 11.2842,
5311            "lon": 49.1816,
5312            "tz": "Africa/Mogadishu"
5313      },
5314      {
5315            "name": "Hargeysa",
5316            "country": "Somalia",
5317            "lat": 9.56,
5318            "lon": 44.065,
5319            "tz": "Africa/Mogadishu"
5320      },
5321      {
5322            "name": "Mogadishu",
5323            "country": "Somalia",
5324            "lat": 2.0371,
5325            "lon": 45.3438,
5326            "tz": "Africa/Mogadishu"
5327      },
5328      {
5329            "name": "Cape Town",
5330            "country": "South Africa",
5331            "lat": -33.9258,
5332            "lon": 18.4232,
5333            "tz": "Africa/Johannesburg"
5334      },
5335      {
5336            "name": "Durban",
5337            "country": "South Africa",
5338            "lat": -29.8579,
5339            "lon": 31.0292,
5340            "tz": "Africa/Johannesburg"
5341      },
5342      {
5343            "name": "Johannesburg",
5344            "country": "South Africa",
5345            "lat": -26.2023,
5346            "lon": 28.0436,
5347            "tz": "Africa/Johannesburg"
5348      },
5349      {
5350            "name": "Busan",
5351            "country": "South Korea",
5352            "lat": 35.1017,
5353            "lon": 129.03,
5354            "tz": "Asia/Seoul"
5355      },
5356      {
5357            "name": "Seoul",
5358            "country": "South Korea",
5359            "lat": 37.566,
5360            "lon": 126.9784,
5361            "tz": "Asia/Seoul"
5362      },
5363      {
5364            "name": "Barcelona",
5365            "country": "Spain",
5366            "lat": 41.3888,
5367            "lon": 2.159,
5368            "tz": "Europe/Madrid"
5369      },
5370      {
5371            "name": "Granada",
5372            "country": "Spain",
5373            "lat": 37.1882,
5374            "lon": -3.6067,
5375            "tz": "Europe/Madrid"
5376      },
5377      {
5378            "name": "Madrid",
5379            "country": "Spain",
5380            "lat": 40.4165,
5381            "lon": -3.7026,
5382            "tz": "Europe/Madrid"
5383      },
5384      {
5385            "name": "Khartoum",
5386            "country": "Sudan",
5387            "lat": 15.5518,
5388            "lon": 32.5324,
5389            "tz": "Africa/Khartoum"
5390      },
5391      {
5392            "name": "Omdurman",
5393            "country": "Sudan",
5394            "lat": 15.6445,
5395            "lon": 32.4777,
5396            "tz": "Africa/Khartoum"
5397      },
5398      {
5399            "name": "Port Sudan",
5400            "country": "Sudan",
5401            "lat": 19.6175,
5402            "lon": 37.2164,
5403            "tz": "Africa/Khartoum"
5404      },
5405      {
5406            "name": "Paramaribo",
5407            "country": "Suriname",
5408            "lat": 5.8664,
5409            "lon": -55.1668,
5410            "tz": "America/Paramaribo"
5411      },
5412      {
5413            "name": "Malmo",
5414            "country": "Sweden",
5415            "lat": 55.6059,
5416            "lon": 13.0007,
5417            "tz": "Europe/Stockholm"
5418      },
5419      {
5420            "name": "Stockholm",
5421            "country": "Sweden",
5422            "lat": 59.3294,
5423            "lon": 18.0687,
5424            "tz": "Europe/Stockholm"
5425      },
5426      {
5427            "name": "Zurich",
5428            "country": "Switzerland",
5429            "lat": 47.3667,
5430            "lon": 8.55,
5431            "tz": "Europe/Zurich"
5432      },
5433      {
5434            "name": "Aleppo",
5435            "country": "Syria",
5436            "lat": 36.2012,
5437            "lon": 37.1612,
5438            "tz": "Asia/Damascus"
5439      },
5440      {
5441            "name": "Damascus",
5442            "country": "Syria",
5443            "lat": 33.5102,
5444            "lon": 36.2913,
5445            "tz": "Asia/Damascus"
5446      },
5447      {
5448            "name": "Homs",
5449            "country": "Syria",
5450            "lat": 34.724,
5451            "lon": 36.7256,
5452            "tz": "Asia/Damascus"
5453      },
5454      {
5455            "name": "Taipei",
5456            "country": "Taiwan",
5457            "lat": 25.0531,
5458            "lon": 121.5264,
5459            "tz": "Asia/Taipei"
5460      },
5461      {
5462            "name": "Dushanbe",
5463            "country": "Tajikistan",
5464            "lat": 38.5358,
5465            "lon": 68.779,
5466            "tz": "Asia/Dushanbe"
5467      },
5468      {
5469            "name": "Khujand",
5470            "country": "Tajikistan",
5471            "lat": 40.2826,
5472            "lon": 69.6222,
5473            "tz": "Asia/Dushanbe"
5474      },
5475      {
5476            "name": "Arusha",
5477            "country": "Tanzania",
5478            "lat": -3.3667,
5479            "lon": 36.6833,
5480            "tz": "Africa/Dar_es_Salaam"
5481      },
5482      {
5483            "name": "Dar es Salaam",
5484            "country": "Tanzania",
5485            "lat": -6.8235,
5486            "lon": 39.2695,
5487            "tz": "Africa/Dar_es_Salaam"
5488      },
5489      {
5490            "name": "Zanzibar",
5491            "country": "Tanzania",
5492            "lat": -6.1639,
5493            "lon": 39.1979,
5494            "tz": "Africa/Dar_es_Salaam"
5495      },
5496      {
5497            "name": "Bangkok",
5498            "country": "Thailand",
5499            "lat": 13.754,
5500            "lon": 100.5014,
5501            "tz": "Asia/Bangkok"
5502      },
5503      {
5504            "name": "Hat Yai",
5505            "country": "Thailand",
5506            "lat": 7.0084,
5507            "lon": 100.4767,
5508            "tz": "Asia/Bangkok"
5509      },
5510      {
5511            "name": "Port of Spain",
5512            "country": "Trinidad and Tobago",
5513            "lat": 10.6667,
5514            "lon": -61.5189,
5515            "tz": "America/Port_of_Spain"
5516      },
5517      {
5518            "name": "Sfax",
5519            "country": "Tunisia",
5520            "lat": 34.7406,
5521            "lon": 10.7603,
5522            "tz": "Africa/Tunis"
5523      },
5524      {
5525            "name": "Sousse",
5526            "country": "Tunisia",
5527            "lat": 35.8254,
5528            "lon": 10.637,
5529            "tz": "Africa/Tunis"
5530      },
5531      {
5532            "name": "Tunis",
5533            "country": "Tunisia",
5534            "lat": 36.819,
5535            "lon": 10.1658,
5536            "tz": "Africa/Tunis"
5537      },
5538      {
5539            "name": "Adana",
5540            "country": "Turkey",
5541            "lat": 36.9862,
5542            "lon": 35.3253,
5543            "tz": "Europe/Istanbul"
5544      },
5545      {
5546            "name": "Ankara",
5547            "country": "Turkey",
5548            "lat": 39.9199,
5549            "lon": 32.8543,
5550            "tz": "Europe/Istanbul"
5551      },
5552      {
5553            "name": "Antalya",
5554            "country": "Turkey",
5555            "lat": 36.9081,
5556            "lon": 30.6956,
5557            "tz": "Europe/Istanbul"
5558      },
5559      {
5560            "name": "Bursa",
5561            "country": "Turkey",
5562            "lat": 40.1956,
5563            "lon": 29.0601,
5564            "tz": "Europe/Istanbul"
5565      },
5566      {
5567            "name": "Gaziantep",
5568            "country": "Turkey",
5569            "lat": 37.0594,
5570            "lon": 37.3825,
5571            "tz": "Europe/Istanbul"
5572      },
5573      {
5574            "name": "Istanbul",
5575            "country": "Turkey",
5576            "lat": 41.0138,
5577            "lon": 28.9497,
5578            "tz": "Europe/Istanbul"
5579      },
5580      {
5581            "name": "Izmir",
5582            "country": "Turkey",
5583            "lat": 38.4127,
5584            "lon": 27.1384,
5585            "tz": "Europe/Istanbul"
5586      },
5587      {
5588            "name": "Konya",
5589            "country": "Turkey",
5590            "lat": 37.8713,
5591            "lon": 32.4846,
5592            "tz": "Europe/Istanbul"
5593      },
5594      {
5595            "name": "Jinja",
5596            "country": "Uganda",
5597            "lat": 0.439,
5598            "lon": 33.2032,
5599            "tz": "Africa/Kampala"
5600      },
5601      {
5602            "name": "Kampala",
5603            "country": "Uganda",
5604            "lat": 0.3163,
5605            "lon": 32.5822,
5606            "tz": "Africa/Kampala"
5607      },
5608      {
5609            "name": "Abu Dhabi",
5610            "country": "United Arab Emirates",
5611            "lat": 24.4512,
5612            "lon": 54.397,
5613            "tz": "Asia/Dubai"
5614      },
5615      {
5616            "name": "Ajman",
5617            "country": "United Arab Emirates",
5618            "lat": 25.4018,
5619            "lon": 55.4788,
5620            "tz": "Asia/Dubai"
5621      },
5622      {
5623            "name": "Dubai",
5624            "country": "United Arab Emirates",
5625            "lat": 25.0772,
5626            "lon": 55.3093,
5627            "tz": "Asia/Dubai"
5628      },
5629      {
5630            "name": "Sharjah",
5631            "country": "United Arab Emirates",
5632            "lat": 25.3342,
5633            "lon": 55.4122,
5634            "tz": "Asia/Dubai"
5635      },
5636      {
5637            "name": "Birmingham",
5638            "country": "United Kingdom",
5639            "lat": 52.4814,
5640            "lon": -1.8998,
5641            "tz": "Europe/London"
5642      },
5643      {
5644            "name": "Bradford",
5645            "country": "United Kingdom",
5646            "lat": 53.7939,
5647            "lon": -1.7521,
5648            "tz": "Europe/London"
5649      },
5650      {
5651            "name": "Glasgow",
5652            "country": "United Kingdom",
5653            "lat": 55.8651,
5654            "lon": -4.2576,
5655            "tz": "Europe/London"
5656      },
5657      {
5658            "name": "Leicester",
5659            "country": "United Kingdom",
5660            "lat": 52.6386,
5661            "lon": -1.1317,
5662            "tz": "Europe/London"
5663      },
5664      {
5665            "name": "London",
5666            "country": "United Kingdom",
5667            "lat": 51.5085,
5668            "lon": -0.1257,
5669            "tz": "Europe/London"
5670      },
5671      {
5672            "name": "Luton",
5673            "country": "United Kingdom",
5674            "lat": 51.8797,
5675            "lon": -0.4175,
5676            "tz": "Europe/London"
5677      },
5678      {
5679            "name": "Manchester",
5680            "country": "United Kingdom",
5681            "lat": 53.4809,
5682            "lon": -2.2374,
5683            "tz": "Europe/London"
5684      },
5685      {
5686            "name": "Atlanta",
5687            "country": "United States",
5688            "lat": 33.749,
5689            "lon": -84.388,
5690            "tz": "America/New_York"
5691      },
5692      {
5693            "name": "Chicago",
5694            "country": "United States",
5695            "lat": 41.85,
5696            "lon": -87.65,
5697            "tz": "America/Chicago"
5698      },
5699      {
5700            "name": "Dallas",
5701            "country": "United States",
5702            "lat": 32.7831,
5703            "lon": -96.8067,
5704            "tz": "America/Chicago"
5705      },
5706      {
5707            "name": "Detroit",
5708            "country": "United States",
5709            "lat": 42.3314,
5710            "lon": -83.0457,
5711            "tz": "America/Detroit"
5712      },
5713      {
5714            "name": "Houston",
5715            "country": "United States",
5716            "lat": 29.7633,
5717            "lon": -95.3633,
5718            "tz": "America/Chicago"
5719      },
5720      {
5721            "name": "Los Angeles",
5722            "country": "United States",
5723            "lat": 34.0522,
5724            "lon": -118.2437,
5725            "tz": "America/Los_Angeles"
5726      },
5727      {
5728            "name": "Miami",
5729            "country": "United States",
5730            "lat": 25.7743,
5731            "lon": -80.1937,
5732            "tz": "America/New_York"
5733      },
5734      {
5735            "name": "New York",
5736            "country": "United States",
5737            "lat": 40.7143,
5738            "lon": -74.006,
5739            "tz": "America/New_York"
5740      },
5741      {
5742            "name": "Philadelphia",
5743            "country": "United States",
5744            "lat": 39.9524,
5745            "lon": -75.1636,
5746            "tz": "America/New_York"
5747      },
5748      {
5749            "name": "Phoenix",
5750            "country": "United States",
5751            "lat": 33.4484,
5752            "lon": -112.074,
5753            "tz": "America/Phoenix"
5754      },
5755      {
5756            "name": "San Francisco",
5757            "country": "United States",
5758            "lat": 37.7749,
5759            "lon": -122.4194,
5760            "tz": "America/Los_Angeles"
5761      },
5762      {
5763            "name": "Washington DC",
5764            "country": "United States",
5765            "lat": 38.8951,
5766            "lon": -77.0364,
5767            "tz": "America/New_York"
5768      },
5769      {
5770            "name": "Bukhara",
5771            "country": "Uzbekistan",
5772            "lat": 39.7703,
5773            "lon": 64.4307,
5774            "tz": "Asia/Samarkand"
5775      },
5776      {
5777            "name": "Samarkand",
5778            "country": "Uzbekistan",
5779            "lat": 39.6546,
5780            "lon": 66.9644,
5781            "tz": "Asia/Samarkand"
5782      },
5783      {
5784            "name": "Tashkent",
5785            "country": "Uzbekistan",
5786            "lat": 41.2647,
5787            "lon": 69.2163,
5788            "tz": "Asia/Tashkent"
5789      }
5790];
5791
5792    function pad2(n){ return String(n).padStart(2,"0"); }
5793    function todayDDMMYYYY(d, timezone){
5794      d = d || new Date();
5795      if(timezone){
5796        try {
5797          var parts = new Intl.DateTimeFormat("en-CA", {timeZone:timezone,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(d);
5798          var values = {};
5799          for(var i=0;i<parts.length;i++) if(parts[i].type !== "literal") values[parts[i].type] = parts[i].value;
5800          return values.day+"-"+values.month+"-"+values.year;
5801        } catch(_){ }
5802      }
5803      return pad2(d.getDate())+"-"+pad2(d.getMonth()+1)+"-"+d.getFullYear();
5804    }
5805    function setStatus(msg){
5806      if(locText) locText.textContent = msg;
5807    }
5808    function cityNameFromLocation(loc){
5809      var label = loc && typeof loc.label === "string" ? loc.label.trim() : "";
5810      if(!label) return "";
5811      return label.split(",")[0].trim();
5812    }
5813    function setCityButtonLabel(loc){
5814      if(!btnCity) return;
5815      var labelEl = btnCity.querySelector(".ph-btn-label");
5816      if(!labelEl) return;
5817      var cityName = cityNameFromLocation(loc);
5818      labelEl.textContent = cityName || L("chooseCity");
5819      labelEl.setAttribute("data-short-label", cityName || L("chooseCityShort"));
5820      btnCity.setAttribute("aria-label", cityName ? L("changeCity") + ": " + (loc.label || cityName) : L("chooseCity"));
5821      btnCity.classList.toggle("has-selected-city", !!cityName);
5822    }
5823    function applyPrayerModalLocale(){
5824      var setText = function(sel, text){ var el = document.querySelector(sel); if(el) el.textContent = text; };
5825      setText("#phBtnSettings .ph-btn-label", L("settings"));
5826      setText("#apPrayerSettingsModal .ph-settings-title, #apPrayerSettingsModal h3", L("prayerSettings"));
5827      setText("#apPrayerSettingsModal .ph-settings-sub, #apPrayerSettingsModal .modal-body > p", L("settingsHelp"));
5828      setText("#apCityModal .ph-school-control .ph-school-label", L("asrMadhab"));
5829      document.querySelectorAll("#apPrayerSettingsModal [data-l10n-key]").forEach(function(el){
5830        var key = el.getAttribute("data-l10n-key");
5831        if(key) el.textContent = L(key);
5832      });
5833      setText("#phResetPrefs", L("resetSaved"));
5834      setText("#phMethodSelect option[value='auto']", L("autoByCountry"));
5835      document.querySelectorAll("[data-school='hanafi']").forEach(function(el){ el.textContent = L("hanafi"); });
5836      document.querySelectorAll("[data-school='standard']").forEach(function(el){ el.textContent = L("standardSchool"); });
5837      setCityButtonLabel(lastLoc);
5838    }
5839    function readStoredValue(key){
5840      try {
5841        var localValue = localStorage.getItem(key);
5842        if(localValue !== null) return localValue;
5843      } catch(_){
5844        // Private/restricted browser contexts may deny localStorage. The
5845        // session store keeps the choice available for that browser session.
5846      }
5847      try { return sessionStorage.getItem(key); } catch(_){ return null; }
5848    }
5849    function writeStoredValue(key, value){
5850      try {
5851        localStorage.setItem(key, value);
5852        return;
5853      } catch(_){
5854        // Fall through to the session store when localStorage is unavailable.
5855      }
5856      try { sessionStorage.setItem(key, value); } catch(_){ }
5857    }
5858    function removeStoredValue(key){
5859      try { localStorage.removeItem(key); } catch(_){ }
5860      try { sessionStorage.removeItem(key); } catch(_){ }
5861    }
5862    function saveLoc(loc){
5863      try { writeStoredValue(LS_KEY, JSON.stringify(loc)); } catch(_){ }
5864    }
5865    function readPrefs(){
5866      try {
5867        var raw = localStorage.getItem(PREFS_KEY);
5868        if(!raw) return {};
5869        var v = JSON.parse(raw);
5870        return v && typeof v === "object" ? v : {};
5871      } catch(_){ return {}; }
5872    }
5873    function savePrefs(){
5874      try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch(_){}
5875    }
5876    function normalizeBuffer(v, fallback){
5877      var n = Number(v);
5878      if(!Number.isFinite(n)) return fallback;
5879      return Math.max(0, Math.min(90, Math.round(n)));
5880    }
5881    function readLoc(){
5882      try {
5883        var raw = readStoredValue(LS_KEY);
5884        if(!raw) return null;
5885        var v = JSON.parse(raw);
5886        if(v && typeof v.lat==="number" && typeof v.lon==="number") return v;
5887        return null;
5888      } catch(_){ return null; }
5889    }
5890
5891    function getCountryFromLabel(label){
5892      var parts = String(label || "").split(",");
5893      return parts.length > 1 ? parts[parts.length - 1].trim() : "";
5894    }
5895    function getCountryFromLoc(loc, label){
5896      return (loc && typeof loc.country === "string" && loc.country.trim()) || getCountryFromLabel(label);
5897    }
5898    function methodForCountry(country){
5899      if(prefs.methodId && METHOD_BY_ID[prefs.methodId]){
5900        return {id:prefs.methodId, name:METHOD_BY_ID[prefs.methodId]};
5901      }
5902      return METHOD_BY_COUNTRY[country] || DEFAULT_METHOD;
5903    }
5904    function fardBuffersForCountry(country){
5905      var base = FARD_BUFFER_BY_REGION.default;
5906      if(SOUTH_ASIA[country]) base = FARD_BUFFER_BY_REGION.southAsia;
5907      else if(GULF[country]) base = FARD_BUFFER_BY_REGION.gulf;
5908      return {
5909        fajr: normalizeBuffer(prefs.fajrBuffer, base.fajr),
5910        asr: normalizeBuffer(prefs.asrBuffer, base.asr)
5911      };
5912    }
5913    function schoolParam(){
5914      return selectedSchool === "hanafi" ? 1 : 0;
5915    }
5916    function validTimezone(timezone){
5917      if(typeof timezone !== "string" || !timezone) return false;
5918      try { new Intl.DateTimeFormat("en-CA", {timeZone:timezone}).format(); return true; } catch(_){ return false; }
5919    }
5920    function validCoordinates(lat, lon){
5921      return Number.isFinite(Number(lat)) && Number.isFinite(Number(lon)) && Number(lat)>=-90 && Number(lat)<=90 && Number(lon)>=-180 && Number(lon)<=180;
5922    }
5923    function validTimingPayload(timings){
5924      if(!timings || typeof timings !== "object") return false;
5925      var required = ["Fajr","Sunrise","Dhuhr","Asr","Sunset","Maghrib","Isha"];
5926      var values = {};
5927      for(var i=0;i<required.length;i++){
5928        var raw = String(timings[required[i]] || "").split(" ")[0].slice(0,5);
5929        if(!/^([01]\d|2[0-3]):([0-5]\d)$/.test(raw)) return false;
5930        var rawParts = raw.split(":");
5931        values[required[i]] = Number(rawParts[0]) * 60 + Number(rawParts[1]);
5932      }
5933      if(values.Fajr >= values.Sunrise || values.Sunrise >= values.Dhuhr || values.Dhuhr >= values.Sunset || values.Asr <= values.Dhuhr || values.Asr >= values.Sunset || values.Maghrib < values.Sunset - 3 || values.Maghrib > values.Sunset + 3) return false;
5934      return true;
5935    }
5936    function responseDateMatches(json, requestedDate){
5937      var responseDate = json && json.data && json.data.date && json.data.date.gregorian && json.data.date.gregorian.date;
5938      return !responseDate || responseDate === requestedDate;
5939    }
5940    function updateSchoolButtons(){
5941      for(var i=0;i<schoolBtns.length;i++){
5942        var btn = schoolBtns[i];
5943        btn.setAttribute("aria-pressed", btn.getAttribute("data-school") === selectedSchool ? "true" : "false");
5944      }
5945    }
5946    function syncSettingsFields(){
5947      if(methodSelect) methodSelect.value = prefs.methodId && METHOD_BY_ID[prefs.methodId] ? String(prefs.methodId) : "auto";
5948      var country = lastLoc ? getCountryFromLoc(lastLoc, lastLoc.label || "") : "";
5949      var base = FARD_BUFFER_BY_REGION.default;
5950      if(SOUTH_ASIA[country]) base = FARD_BUFFER_BY_REGION.southAsia;
5951      else if(GULF[country]) base = FARD_BUFFER_BY_REGION.gulf;
5952      if(fajrBufferInput) fajrBufferInput.value = normalizeBuffer(prefs.fajrBuffer, base.fajr);
5953      if(asrBufferInput) asrBufferInput.value = normalizeBuffer(prefs.asrBuffer, base.asr);
5954      updateSchoolButtons();
5955    }
5956    function refreshCurrentTimings(statusWhenMissing){
5957      syncSettingsFields();
5958      if(lastLoc && typeof lastLoc.lat === "number" && typeof lastLoc.lon === "number"){
5959        return fetchForCoords(lastLoc.lat, lastLoc.lon, lastLoc.label || "", lastLoc);
5960      }
5961      if(statusWhenMissing) setStatus(statusWhenMissing);
5962      return null;
5963    }
5964    function timingContext(label, loc){
5965      var country = getCountryFromLoc(loc, label);
5966      var method = methodForCountry(country);
5967      return {
5968        country: country,
5969        method: method,
5970        school: schoolParam(),
5971        schoolLabel: SCHOOL_LABELS[selectedSchool] || SCHOOL_LABELS.hanafi,
5972        fardBuffers: fardBuffersForCountry(country)
5973      };
5974    }
5975    function methodNoteFor(ctx){
5976      // Persist real data so phUpdateMethodNote can rebuild locale-aware str
5976ing anytime
5977      var _mdd = window.phMethodDisplayData = window.phMethodDisplayData || { isCustom:false, name:"", fajrBuf:15, asrBuf:15 };
5978      _mdd.isCustom = !!prefs.methodId;
5979      _mdd.name     = ctx.method.name;
5980      _mdd.fajrBuf  = ctx.fardBuffers.fajr;
5981      _mdd.asrBuf   = ctx.fardBuffers.asr;
5982      var bufferNote = "fard completion +" + ctx.fardBuffers.fajr + "m Fajr, +" + ctx.fardBuffers.asr + "m Asr";
5983      var methodPrefix = prefs.methodId ? "Custom method · " : "Calculated · ";
5984      return methodPrefix + ctx.method.name + " · " + ctx.schoolLabel + " · " + bufferNote;
5985    }
5986
5987    var HIJRI_MONTHS = {
5988      ar: ["محرّم", "صفر", "ربيع الأول", "ربيع الآخر", "جمادى الأولى", "جمادى الآخرة", "رجب", "شعبان", "رمضان", "شوّال", "ذو القعدة", "ذو الحجة"],
5989      ur: ["محرم", "صفر", "ربیع الاول", "ربیع الآخر", "جمادی الاول", "جمادی الآخر", "رجب", "شعبان", "رمضان", "شوال", "ذوالقعدہ", "ذوالحجہ"],
5990    };
5991
5992    function hijriMonthFor(locale, month){
5993      var target = locale === "ur" ? "ur" : "ar";
5994      var number = Number(month && month.number);
5995      if(Number.isInteger(number) && number >= 1 && number <= 12) return HIJRI_MONTHS[target][number - 1];
5996      if(month && month[target]) return month[target];
5997      var name = String(month && (month.en || month.name) || "").toLowerCase().replace(/[’']/g, "");
5998      var english = ["muharram", "safar", "rabi al-awwal", "rabi al-thani", "jumada al-awwal", "jumada al-thani", "rajab", "shaban", "ramadan", "shawwal", "dhu al-qadah", "dhu al-hijjah"];
5999      var index = english.indexOf(name);
6000      return index >= 0 ? HIJRI_MONTHS[target][index] : String(month && (month.ar || month.en || month.name) || "");
6001    }
6002
6003    function setGregDate(timezone){
6004      if(!gregEl) return;
6005      if(timezone) window.phDateTimezone = timezone;
6006      timezone = timezone || window.phDateTimezone || "";
6007      var options = {weekday:"short",day:"2-digit",month:"short",year:"numeric"};
6008      if(timezone) options.timeZone = timezone;
6009      var locale = currentLocale();
6010      var dateLocale = locale === "ar" ? "ar-u-nu-latn" : locale === "ur" ? "ur-PK-u-nu-latn" : "en-GB";
6011      gregEl.textContent = new Intl.DateTimeFormat(dateLocale,options).format(new Date());
6012    }
6013
6014    function setHijriLocal(){
6015      try {
6016        var locale = currentLocale();
6017        if(hijriEn){
6018          var en = new Intl.DateTimeFormat("en-u-ca-islamic-umalqura",{day:"numeric",month:"long",year:"numeric"}).format(new Date());
6019          hijriEn.textContent = en.replace(/\s*AH$/,"") + " AH";
6020        }
6021        if(hijriAr){
6022          var displayLocale = locale === "ur" ? "ur-PK-u-ca-islamic-umalqura-nu-latn" : "ar-u-ca-islamic-umalqura-nu-latn";
6023          var parts = new Intl.DateTimeFormat(displayLocale,{day:"numeric",month:"long",year:"numeric"}).formatToParts(new Date());
6024          var day = parts.find(function(part){ return part.type === "day"; })?.value || "";
6025          var month = parts.find(function(part){ return part.type === "month"; })?.value || "";
6026          var year = parts.find(function(part){ return part.type === "year"; })?.value || "";
6027          if(locale === "ur") month = month.replace("ر بیع", "ربیع").replace("ج مادی", "جمادی").replace("ذو ال", "ذوال");
6028          hijriAr.textContent = day + " " + month + " " + year + (locale === "ur" ? " ہجری" : " هـ");
6029        }
6030      } catch(_){
6031        if(hijriEn) hijriEn.textContent = "Today";
6032      }
6033    }
6034
6035    function setHijriFromApi(h){
6036      if(!h) return;
6037      window.phHijriData = h;
6038      try{
6039        if(hijriEn){
6040          var monthEn = (h.month && (h.month.en || h.month.name)) || "";
6041          hijriEn.textContent = (h.day||"") + " " + monthEn + " " + (h.year||"") + " AH";
6042        }
6043        if(hijriAr){
6044          var locale = currentLocale();
6045          var monthLabel = hijriMonthFor(locale, h.month);
6046          hijriAr.textContent = (h.day||"") + " " + monthLabel + " " + (h.year||"");
6047        }
6048      } catch(_){}
6049    }
6050
6051    function applyTimings(json, locLabel, ctx, timezone){
6052      if(!json || !json.data) return;
6053      var t = json.data.timings;
6054      if(!validTimingPayload(t)) throw new Error("Invalid prayer timing payload");
6055      timezone = timezone || (json.data.meta && json.data.meta.timezone) || "";
6056      ctx = ctx || timingContext(locLabel || "", null);
6057      if(!window.AP_PRAYER_HERO || typeof window.AP_PRAYER_HERO.applyTimings !== "function") throw new Error("Prayer hero runtime unavailable");
6058      if(window.AP_PRAYER_HERO && timezone && typeof window.AP_PRAYER_HERO.setTimezone === "function") window.AP_PRAYER_HERO.setTimezone(timezone);
6059      if(window.AP_PRAYER_HERO && t){
6060        window.AP_PRAYER_HERO.applyTimings(t, {
6061          methodNote: methodNoteFor(ctx),
6062          fardBuffers: ctx.fardBuffers,
6063          timezone: timezone
6064        });
6065      }
6066      setGregDate(timezone);
6067      setHijriFromApi(json.data.date && json.data.date.hijri);
6068      if(locLabel){ setStatus(locLabel); }
6069    }
6070
6071    function fetchForCoords(lat, lon, label, locMeta){
6072      if(!validCoordinates(lat, lon)) return Promise.reject(new Error("Invalid prayer coordinates"));
6073      var ctx = timingContext(label || "", locMeta || null);
6074      var timezone = (locMeta && locMeta.tz) || "";
6075      if(!validTimezone(timezone)) timezone = "";
6076      var requestedDate = todayDDMMYYYY(null, timezone);
6077      var requestId = ++prayerRequestSerial;
6078      if(window.AP_PRAYER_HERO && typeof window.AP_PRAYER_HERO.clearTimings === "function") window.AP_PRAYER_HERO.clearTimings();
6079      if(window.AP_PRAYER_HERO && timezone && typeof window.AP_PRAYER_HERO.setTimezone === "function") window.AP_PRAYER_HERO.setTimezone(timezone);
6080      var pendingLoc = {
6081        lat:lat,
6082        lon:lon,
6083        label:label || "",
6084        country: locMeta && locMeta.country,
6085        tz: locMeta && locMeta.tz,
6086        school:selectedSchool,
6087        ts:Date.now()
6088      };
6089      // Save the selected city before the network request so a refresh or a
6090      // private-session reload does not turn a valid choice back into a blank
6091      // picker while prayer data is being fetched.
6092      lastLoc = pendingLoc;
6093      saveLoc(lastLoc);
6094      setCityButtonLabel(lastLoc);
6095      var url = "https://api.aladhan.com/v1/timings/"+requestedDate
6096        +"?latitude="+encodeURIComponent(lat)
6097        +"&longitude="+encodeURIComponent(lon)
6098        +"&method="+encodeURIComponent(ctx.method.id)
6099        +"&school="+encodeURIComponent(ctx.school);
6100      setStatus(L("loading"));
6101      return fetch(url, {cache:"no-cache"})
6102        .then(function(r){ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
6103        .then(function(json){
6104          if(requestId !== prayerRequestSerial) return;
6105          if(!responseDateMatches(json, requestedDate)) throw new Error("Stale prayer timing date");
6106          var responseTimezone = (json.data && json.data.meta && json.data.meta.timezone) || timezone;
6107          if(!validTimezone(responseTimezone)) throw new Error("Invalid prayer timezone");
6108          applyTimings(json, label, ctx, responseTimezone);
6109          lastLoc = {
6110            lat:lat,
6111            lon:lon,
6112            label:label,
6113            country: locMeta && locMeta.country,
6114            tz: locMeta && locMeta.tz,
6115            school:selectedSchool,
6116            ts:Date.now()
6117          };
6118          saveLoc(lastLoc);
6119          setCityButtonLabel(lastLoc);
6120        })
6121        .catch(function(err){
6122          if(requestId === prayerRequestSerial){
6123            if(window.AP_PRAYER_HERO && typeof window.AP_PRAYER_HERO.clearTimings === "function") window.AP_PRAYER_HERO.clearTimings();
6124            setStatus(L("loadFail"));
6125            console.warn("[prayer] fetch failed:", err);
6126          }
6127        });
6128    }
6129
6130    function reverseGeocode(lat, lon){
6131      var url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat="+encodeURIComponent(lat)+"&lon="+encodeURIComponent(lon)+"&zoom=10";
6132      return fetch(url, {headers:{"Accept":"application/json"}})
6133        .then(function(r){ return r.ok ? r.json() : null; })
6134        .then(function(j){
6135          if(!j || !j.address) return null;
6136          var a = j.address;
6137          var city = a.city || a.town || a.village || a.municipality || a.county || a.state;
6138          var country = a.country;
6139          if(city && country) return city + ", " + country;
6140          if(city) return city;
6141          if(country) return country;
6142          return null;
6143        })
6144        .catch(function(){ return null; });
6145    }
6146
6147    if(btnLoc){
6148      btnLoc.addEventListener("click", function(){
6149        if(!navigator.geolocation){
6150          setStatus(L("geoUnavailable"));
6151          return;
6152        }
6153        setStatus(L("detecting"));
6154        navigator.geolocation.getCurrentPosition(function(pos){
6155          var lat = pos.coords.latitude, lon = pos.coords.longitude;
6156          reverseGeocode(lat, lon).then(function(label){
6157            var displayLabel = label || ("Lat " + lat.toFixed(2) + ", Lon " + lon.toFixed(2));
6158            var browserTz = "";
6159            try{ browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone || ""; }catch(_){}
6160            fetchForCoords(lat, lon, displayLabel, {country: getCountryFromLabel(displayLabel), tz: browserTz});
6161          });
6162        }, function(err){
6163          var msg = L("geoFail");
6164          if(err && err.code===1) msg = L("geoDenied");
6165          else if(err && err.code===3) msg = L("geoTimeout");
6166          setStatus(msg);
6167        }, {enableHighAccuracy:false, timeout:10000, maximumAge:5*60*1000});
6168      });
6169    }
6170
6171    function renderCityList(filter){
6172      if(!listEl) return;
6173      var q = (filter||"").trim().toLowerCase();
6174      var matches = CITIES.filter(function(c){
6175        if(!q) return true;
6176        return c.name.toLowerCase().indexOf(q)>=0 || c.country.toLowerCase().indexOf(q)>=0;
6177      }).slice(0, 60);
6178      if(!matches.length){
6179        listEl.innerHTML = '<div class="muted" style="padding:14px; text-align:center">
6179'+escapeHtml(L("noCity"))+' “'+escapeHtml(filter)+'”.</div>';
6180        return;
6181      }
6182      listEl.innerHTML = matches.map(function(c){
6183        return '<button type="button" class="city-pick" data-lat="'+c.lat+'" data-lon="'+c.lon+'" data-country="'+escapeAttr(c.country)+'" data-tz="'+escapeAttr(c.tz || "")+'" data-label="'+escapeAttr(c.name+", "+c.country)+'" style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid var(--line, rgba(16,35,28,.10));border-radius:10px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer">'
6184          + '<span style="font-weight:600">'+escapeHtml(c.name)+'</span>'
6185          + '<span class="muted" style="opacity:.65;font-size:.88em">'+escapeHtml(c.country)+'</span>'
6186          + '</button>';
6187      }).join("");
6188    }
6189    function escapeHtml(s){
6190      return String(s).replace(/[&<>"']/g, function(ch){
6191        return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[ch];
6192      });
6193    }
6194    function escapeAttr(s){ return escapeHtml(s); }
6195
6196    function openModal(){
6197      if(!modal) return;
6198      if(modal.parentNode !== document.body){
6199        document.body.appendChild(modal);
6200      }
6201      renderCityList("");
6202      if(search) search.value="";
6203      openManagedModal(modal, search);
6204    }
6205    function closeModal(){
6206      closeManagedModal(modal);
6207    }
6208    function openSettingsModal(){
6209      if(!settingsModal) return;
6210      // Portal: ensure modal is a direct child of <body> so it escapes
6211      // any parent stacking context (e.g. <main> with z-index) and renders
6212      // above the sticky mobile bar and bottom nav.
6213      if(settingsModal.parentNode !== document.body){
6214        document.body.appendChild(settingsModal);
6215      }
6216      syncSettingsFields();
6217      openManagedModal(settingsModal, methodSelect);
6218    }
6219    function closeSettingsModal(){
6220      closeManagedModal(settingsModal);
6221    }
6222
6223    if(btnCity){
6224      btnCity.addEventListener("click", openModal);
6225    }
6226    if(btnSettings){
6227      btnSettings.addEventListener("click", openSettingsModal);
6228    }
6229    if(modal){
6230      modal.addEventListener("click", function(e){
6231        var t = e.target;
6232        if(t && t.matches && t.matches("[data-close]")) { closeModal(); return; }
6233        var pick = t && t.closest ? t.closest(".city-pick") : null;
6234        if(pick){
6235          var lat = parseFloat(pick.getAttribute("data-lat"));
6236          var lon = parseFloat(pick.getAttribute("data-lon"));
6237          var lbl = pick.getAttribute("data-label") || "";
6238          closeModal();
6239          var country = pick.getAttribute("data-country") || getCountryFromLabel(lbl);
6240          var tz = pick.getAttribute("data-tz") || "";
6241          fetchForCoords(lat, lon, lbl, {country: country, tz: tz});
6242        }
6243      });
6244    }
6245    if(search){
6246      search.addEventListener("input", function(){ renderCityList(search.value); });
6247    }
6248    if(clearBtn){
6249      clearBtn.addEventListener("click", function(){
6250        if(search){ search.value=""; search.focus(); }
6251        renderCityList("");
6252      });
6253    }
6254    if(settingsModal){
6255      settingsModal.addEventListener("click", function(e){
6256        var t = e.target;
6257        if(t && t.matches && t.matches("[data-settings-close]")) { closeSettingsModal(); }
6258      });
6259    }
6260    if(methodSelect){
6261      methodSelect.addEventListener("change", function(){
6262        var v = methodSelect.value;
6263        if(v === "auto"){
6264          delete prefs.methodId;
6265        } else {
6266          var id = Number(v);
6267          if(METHOD_BY_ID[id]) prefs.methodId = id;
6268        }
6269        savePrefs();
6270        refreshCurrentTimings(L("chooseApplyMethod"));
6271      });
6272    }
6273    function handleBufferInput(kind, input){
6274      if(!input) return;
6275      input.addEventListener("change", function(){
6276        prefs[kind] = normalizeBuffer(input.value, kind === "fajrBuffer" ? 15 : 15);
6277        input.value = prefs[kind];
6278        savePrefs();
6279        refreshCurrentTimings(L("chooseApplyBuffers"));
6280      });
6281    }
6282    handleBufferInput("fajrBuffer", fajrBufferInput);
6283    handleBufferInput("asrBuffer", asrBufferInput);
6284    if(resetPrefsBtn){
6285      resetPrefsBtn.addEventListener("click", function(){
6286        prefs = {};
6287        selectedSchool = "hanafi";
6288        try {
6289          removeStoredValue(PREFS_KEY);
6290          removeStoredValue(FORMAT_KEY);
6291          removeStoredValue(LS_KEY);
6292        } catch(_){}
6293        lastLoc = null;
6294        setCityButtonLabel(null);
6295        syncSettingsFields();
6296        closeSettingsModal();
6297        setStatus(L("savedReset"));
6298        location.reload();
6299      });
6300    }
6301    for(var sb=0; sb<schoolBtns.length; sb++){
6302      schoolBtns[sb].addEventListener("click", function(){
6303        var nextSchool = this.getAttribute("data-school") || "hanafi";
6304        if(nextSchool === selectedSchool) return;
6305        var validSchools = ["hanafi","shafii","hanbali","maliki"];
6306        selectedSchool = validSchools.indexOf(nextSchool) !== -1 ? nextSchool : "hanafi";
6307        syncSelectedSchool();
6308        prefs.school = selectedSchool;
6309        savePrefs();
6310        updateSchoolButtons();
6311        if(typeof window.phUpdateMethodNote === "function") window.phUpdateMethodNote(); // reflect new school in method note immediately
6312        // Sync Prayer Windows panel madhab
6313        if(window.phWindowsMadhab !== selectedSchool){
6314          window.phWindowsMadhab = selectedSchool;
6315          try { localStorage.setItem(PH_WINDOWS_MADHAB_KEY, window.phWindowsMadhab); } catch(_){}
6316          if(typeof window.phRenderWindowsPanel === "function") window.phRenderWindowsPanel();
6317          var _wp = document.querySelector("#phWindowsPanel");
6318          if(_wp && _wp.classList.contains("is-open")){
6319            _wp.style.maxHeight = _wp.scrollHeight + "px";
6320          }
6321        }
6322        refreshCurrentTimings("Choose a city to apply " + (SCHOOL_LABELS[selectedSchool] || "madhab") + " times");
6323      });
6324    }
6325    document.addEventListener("keydown", function(e){
6326      if(!activeModal) return;
6327      if(e.key==="Escape"){
6328        e.preventDefault();
6329        if(activeModal === modal) closeModal();
6330        else if(activeModal === settingsModal) closeSettingsModal();
6331        return;
6332      }
6333      if(e.key!=="Tab") return;
6334      var focusables = modalFocusableElements(activeModal);
6335      if(!focusables.length) return;
6336      var first = focusables[0];
6337      var last = focusables[focusables.length - 1];
6338      if(e.shiftKey && document.activeElement === first){
6339        e.preventDefault();
6340        last.focus();
6341      }else if(!e.shiftKey && document.activeElement === last){
6342        e.preventDefault();
6343        first.focus();
6344      }
6345    }, true);
6346
6347    // Initial render: dates, then bootstrap times.
6348    setGregDate();
6349    setHijriLocal();
6350    applyPrayerModalLocale();
6351    window.addEventListener("ap:languagechange", function(){
6352      setGregDate();
6353      if(window.phHijriData) setHijriFromApi(window.phHijriData);
6354      else setHijriLocal();
6355      applyPrayerModalLocale();
6356      if(!lastLoc) setStatus(L("choosePrompt"));
6357    });
6358
6359    var saved = readLoc();
6360    if(saved){
6361      (function(){
6362        var _valid = ["hanafi","shafii","hanbali","maliki"];
6363        var _ps = prefs.school || saved.school;
6364        if(_ps === "standard") _ps = "shafii"; // backward compat
6365        selectedSchool = (_valid.indexOf(_ps) !== -1) ? _ps : "hanafi";
6366        syncSelectedSchool();
6367        // Always sync phWindowsMadhab — prefs.school is the single source of truth
6368        window.phWindowsMadhab = selectedSchool;
6369        try { localStorage.setItem(PH_WINDOWS_MADHAB_KEY, window.phWindowsMadhab); } catch(_){}
6370      })();
6371      updateSchoolButtons();
6372      lastLoc = saved;
6373      setCityButtonLabel(saved);
6374      fetchForCoords(saved.lat, saved.lon, saved.label || ("Lat "+saved.lat.toFixed(2)+", Lon "+saved.lon.toFixed(2)), saved);
6375    } else {
6376      // No saved city — still sync madhab from prefs so panel is ready
6377      (function(){
6378        var _valid = ["hanafi","shafii","hanbali","maliki"];
6379        var _ps = prefs.school;
6380        if(_ps === "standard") _ps = "shafii";
6381        if(_valid.indexOf(_ps) !== -1){ selectedSchool = _ps; window.phWindowsMadhab = _ps; }
6382        syncSelectedSchool();
6383        try { localStorage.setItem(PH_WINDOWS_MADHAB_KEY, window.phWindowsMadhab || "hanafi"); } catch(_){}
6384      })();
6385      updateSchoolButtons();
6386      syncSettingsFields();
6387      setStatus(L("choosePrompt"));
6388    }
6389  } catch(e){
6390    console.warn("[prayer location wiring] failed:", e);
6391  }
6392})();
6393
6394/* -------------------------------------------------
6395   Mobile sidebar drawer toggle
6396-------------------------------------------------- */
6397(function(){
6398  "use strict";
6399  try {
6400    var btn = document.getElementById("navToggle");
6401    var sidebar = document.getElementById("sidebar");
6402    var scrim = document.getElementById("scrim");
6403    if (!btn || !sidebar || !scrim) return;
6404
6405    var setOpen = function(open){
6406      sidebar.classList.toggle("is-open", open);
6407      scrim.classList.toggle("is-open", open);
6408      btn.setAttribute("aria-expanded", open ? "true" : "false");
6409      document.body.classList.toggle("mobile-nav-open", open);
6410    };
6411
6412    btn.addEventListener("click", function(){
6413      setOpen(!sidebar.classList.contains("is-open"));
6414    });
6415    scrim.addEventListener("click", function(){ setOpen(false); });
6416    document.addEventListener("keydown", function(e){
6417      if (e.key === "Escape" && sidebar.classList.contains("is-open")) setOpen(false);
6418    });
6419    sidebar.addEventListener("click", function(e){
6420      if (!e.target.closest("a")) return;
6421      if (window.matchMedia("(max-width: 1023px)").matches) setOpen(false);
6422    });
6423  } catch(_){}
6424})();
6425
6426/* -------------------------------------------------
6427   WebMCP tools for in-browser agents
6428-------------------------------------------------- */
6429(function(){
6430  "use strict";
6431
6432  var modelContext = (document && document.modelContext) || (navigator && navigator.modelContext);
6433  if (!modelContext) return;
6434
6435  var SITE_OVERVIEW = [
6436    "Akhirah Projects is a multilingual Islamic platform with prayer tools, a knowledge library, Kawn MindMap, and privacy-first utilities.",
6437    "Primary routes: /, /prayer, /library/, /kawn/, /focus/, /store/, /dua/, /tasbih, /qibla/, /zakat-calculator/.",
6438    "Public machine-readable resources: /.well-known/api-catalog, /.well-known/openapi.json, /.well-known/agent-skills/index.json, /.well-known/mcp/server-card.json, /llms.txt."
6439  ].join("\n");
6440
6441  var tools = [
6442    {
6443      name: "get_site_overview",
6444      description: "Return a concise overview of Akhirah Projects and the key routes agents should start with.",
6445      inputSchema: {
6446        type: "object",
6447        additionalProperties: false,
6448        properties: {}
6449      },
6450      execute: async function() {
6451        return {
6452          site: "Akhirah Projects",
6453          canonical: "https://akhirahprojects.com/",
6454          overview: SITE_OVERVIEW
6455        };
6456      }
6457    },
6458    {
6459      name: "build_route_url",
6460      description: "Turn a site-relative route into an absolute canonical URL on akhirahprojects.com.",
6461      inputSchema: {
6462        type: "object",
6463        additionalProperties: false,
6464        required: ["route"],
6465        properties: {
6466          route: {
6467            type: "string",
6468            description: "A site-relative path such as /library/ or /prayer-times/pakistan/karachi/."
6469          }
6470        }
6471      },
6472      execute: async function(args) {
6473        var route = String((args && args.route) || "/").trim();
6474        if (!route.startsWith("/")) route = "/" + route;
6475        return {
6476          route: route,
6477          url: "https://akhirahprojects.com" + route
6478        };
6479      }
6480    },
6481    {
6482      name: "get_prayer_times_route",
6483      description: "Construct a prayer-times URL for a country, city, and optional locale.",
6484      inputSchema: {
6485        type: "object",
6486        additionalProperties: false,
6487        required: ["country", "city"],
6488        properties: {
6489          country: { type: "string" },
6490          city: { type: "string" },
6491          locale: {
6492            type: "string",
6493            enum: ["en", "ar", "ur"]
6494          }
6495        }
6496      },
6497      execute: async function(args) {
6498        var locale = String((args && args.locale) || "en").toLowerCase();
6499        var prefix = locale === "en" ? "" : "/" + locale;
6500        var slugify = function(value) {
6501          return String(value || "")
6502            .trim()
6503            .toLowerCase()
6504            .replace(/&/g, "and")
6505            .replace(/[^a-z0-9]+/g, "-")
6506            .replace(/^-+|-+$/g, "");
6507        };
6508        var route = prefix + "/prayer-times/" + slugify(args.country) + "/" + slugify(args.city) + "/";
6509        return {
6510          route: route,
6511          url: "https://akhirahprojects.com" + route
6512        };
6513      }
6514    }
6515  ];
6516
6517  try {
6518    if (typeof modelContext.provideContext === "function") {
6519      modelContext.provideContext({ tools: tools });
6520      return;
6521    }
6522
6523    if (typeof modelContext.registerTool === "function") {
6524      for (var i = 0; i < tools.length; i += 1) {
6525        modelContext.registerTool(tools[i]);
6526      }
6527    }
6528  } catch (error) {
6529    console.warn("[webmcp] registration failed:", error);
6530  }
6531})();

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.