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,"<") 58 .replace(/>/g,">") 59 .replace(/"/g,""") 60 .replace(/'/g,"'"); 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 {"&":"&","<":"<",">":">","\"":""","'":"'"}[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.