const { getCenteredWeekStartDay, getDateKey, getMoonPhaseName } = window.TarotCalc; const { loadReferenceData, loadMagickDataset } = window.TarotDataService; const { buildWeekEvents } = window.TarotEventBuilder; const { updateNowPanel } = window.TarotNowUi; const { ensureTarotSection } = window.TarotSectionUi || {}; const { ensurePlanetSection } = window.PlanetSectionUi || {}; const { ensureCyclesSection } = window.CyclesSectionUi || {}; const { ensureElementsSection } = window.ElementsSectionUi || {}; const { ensureIChingSection } = window.IChingSectionUi || {}; const { ensureKabbalahSection } = window.KabbalahSectionUi || {}; const { ensureCubeSection } = window.CubeSectionUi || {}; const { ensureAlphabetSection } = window.AlphabetSectionUi || {}; const { ensureZodiacSection } = window.ZodiacSectionUi || {}; const { ensureQuizSection, registerQuizCategory } = window.QuizSectionUi || {}; const { ensureGodsSection } = window.GodsSectionUi || {}; const { ensureEnochianSection } = window.EnochianSectionUi || {}; const { ensureCalendarSection } = window.CalendarSectionUi || {}; const { ensureHolidaySection } = window.HolidaySectionUi || {}; const { ensureNatalPanel } = window.TarotNatalUi || {}; const statusEl = document.getElementById("status"); const monthStripEl = document.getElementById("month-strip"); const calendarEl = document.getElementById("calendar"); const calendarSectionEl = document.getElementById("calendar-section"); const holidaySectionEl = document.getElementById("holiday-section"); const tarotSectionEl = document.getElementById("tarot-section"); const astronomySectionEl = document.getElementById("astronomy-section"); const natalSectionEl = document.getElementById("natal-section"); const planetSectionEl = document.getElementById("planet-section"); const cyclesSectionEl = document.getElementById("cycles-section"); const elementsSectionEl = document.getElementById("elements-section"); const ichingSectionEl = document.getElementById("iching-section"); const kabbalahSectionEl = document.getElementById("kabbalah-section"); const kabbalahTreeSectionEl = document.getElementById("kabbalah-tree-section"); const cubeSectionEl = document.getElementById("cube-section"); const alphabetSectionEl = document.getElementById("alphabet-section"); const numbersSectionEl = document.getElementById("numbers-section"); const zodiacSectionEl = document.getElementById("zodiac-section"); const quizSectionEl = document.getElementById("quiz-section"); const godsSectionEl = document.getElementById("gods-section"); const enochianSectionEl = document.getElementById("enochian-section"); const openCalendarEl = document.getElementById("open-calendar"); const openCalendarMonthsEl = document.getElementById("open-calendar-months"); const openHolidaysEl = document.getElementById("open-holidays"); const openTarotEl = document.getElementById("open-tarot"); const openTarotCardsEl = document.getElementById("open-tarot-cards"); const openTarotSpreadEl = document.getElementById("open-tarot-spread"); const openAstronomyEl = document.getElementById("open-astronomy"); const openPlanetsEl = document.getElementById("open-planets"); const openCyclesEl = document.getElementById("open-cycles"); const openElementsEl = document.getElementById("open-elements"); const openIChingEl = document.getElementById("open-iching"); const openKabbalahEl = document.getElementById("open-kabbalah"); const openKabbalahTreeEl = document.getElementById("open-kabbalah-tree"); const openKabbalahCubeEl = document.getElementById("open-kabbalah-cube"); const openAlphabetEl = document.getElementById("open-alphabet"); const openNumbersEl = document.getElementById("open-numbers"); const openZodiacEl = document.getElementById("open-zodiac"); const openNatalEl = document.getElementById("open-natal"); const openQuizEl = document.getElementById("open-quiz"); const openGodsEl = document.getElementById("open-gods"); const openEnochianEl = document.getElementById("open-enochian"); const openSettingsEl = document.getElementById("open-settings"); const closeSettingsEl = document.getElementById("close-settings"); const settingsPopupEl = document.getElementById("settings-popup"); const settingsPopupCardEl = document.getElementById("settings-popup-card"); const topbarDropdownEls = Array.from(document.querySelectorAll(".topbar-dropdown")); const latEl = document.getElementById("lat"); const lngEl = document.getElementById("lng"); const timeFormatEl = document.getElementById("time-format"); const birthDateEl = document.getElementById("birth-date"); const tarotDeckEl = document.getElementById("tarot-deck"); const saveSettingsEl = document.getElementById("save-settings"); const useLocationEl = document.getElementById("use-location"); const nowSkyLayerEl = document.getElementById("now-sky-layer"); const nowPanelEl = document.getElementById("now-panel"); const tarotBrowseViewEl = document.getElementById("tarot-browse-view"); const tarotSpreadViewEl = document.getElementById("tarot-spread-view"); const tarotSpreadBackEl = document.getElementById("tarot-spread-back"); const tarotSpreadBtnThreeEl = document.getElementById("tarot-spread-btn-three"); const tarotSpreadBtnCelticEl = document.getElementById("tarot-spread-btn-celtic"); const tarotSpreadRedrawEl = document.getElementById("tarot-spread-redraw"); const tarotSpreadMeaningsEl = document.getElementById("tarot-spread-meanings"); const tarotSpreadBoardEl = document.getElementById("tarot-spread-board"); const numbersCountEl = document.getElementById("numbers-count"); const numbersListEl = document.getElementById("numbers-list"); const numbersDetailNameEl = document.getElementById("numbers-detail-name"); const numbersDetailTypeEl = document.getElementById("numbers-detail-type"); const numbersDetailSummaryEl = document.getElementById("numbers-detail-summary"); const numbersDetailBodyEl = document.getElementById("numbers-detail-body"); const numbersSpecialPanelEl = document.getElementById("numbers-special-panel"); const nowElements = { nowHourEl: document.getElementById("now-hour"), nowHourTarotEl: document.getElementById("now-hour-tarot"), nowCountdownEl: document.getElementById("now-countdown"), nowHourNextEl: document.getElementById("now-hour-next"), nowHourCardEl: document.getElementById("now-hour-card"), nowMoonEl: document.getElementById("now-moon"), nowMoonTarotEl: document.getElementById("now-moon-tarot"), nowMoonCountdownEl: document.getElementById("now-moon-countdown"), nowMoonNextEl: document.getElementById("now-moon-next"), nowMoonCardEl: document.getElementById("now-moon-card"), nowDecanEl: document.getElementById("now-decan"), nowDecanTarotEl: document.getElementById("now-decan-tarot"), nowDecanCountdownEl: document.getElementById("now-decan-countdown"), nowDecanNextEl: document.getElementById("now-decan-next"), nowDecanCardEl: document.getElementById("now-decan-card"), nowStatsSabianEl: document.getElementById("now-stats-sabian"), nowStatsPlanetsEl: document.getElementById("now-stats-planets") }; const baseWeekOptions = { hourStart: 0, hourEnd: 24, eventView: ["allday", "time"], taskView: false }; const PLANET_CALENDAR_ORDER = ["saturn", "jupiter", "mars", "sol", "venus", "mercury", "luna"]; const SETTINGS_STORAGE_KEY = "tarot-time-settings-v1"; const DEFAULT_TAROT_DECK = "ceremonial-magick"; const SIDEBAR_COLLAPSE_STORAGE_PREFIX = "tarot-sidebar-collapsed:"; const DETAIL_COLLAPSE_STORAGE_PREFIX = "tarot-detail-collapsed:"; const DEFAULT_DATASET_ENTRY_COLLAPSED = true; const DEFAULT_DATASET_DETAIL_COLLAPSED = false; const DEFAULT_SETTINGS = { latitude: 51.5074, longitude: -0.1278, timeFormat: "minutes", birthDate: "", tarotDeck: DEFAULT_TAROT_DECK }; const PLANET_CALENDAR_STYLES = { saturn: { name: "♄ Saturn", color: "#f4f4f5", backgroundColor: "#0a0a0a", borderColor: "#0a0a0a" }, jupiter: { name: "♃ Jupiter", color: "#eff6ff", backgroundColor: "#1d4ed8", borderColor: "#1d4ed8" }, mars: { name: "♂ Mars", color: "#fff1f2", backgroundColor: "#dc2626", borderColor: "#dc2626" }, sol: { name: "☉ Sol", color: "#111827", backgroundColor: "#facc15", borderColor: "#eab308" }, venus: { name: "♀ Venus", color: "#ecfdf5", backgroundColor: "#16a34a", borderColor: "#15803d" }, mercury: { name: "☿ Mercury", color: "#111827", backgroundColor: "#fb923c", borderColor: "#f97316" }, luna: { name: "☾ Luna", color: "#111827", backgroundColor: "#e2e8f0", borderColor: "#cbd5e1" } }; const planetaryCalendars = PLANET_CALENDAR_ORDER.map((planetId) => { const style = PLANET_CALENDAR_STYLES[planetId]; return { id: `planet-${planetId}`, name: style.name, color: style.color, backgroundColor: style.backgroundColor, dragBackgroundColor: style.backgroundColor, borderColor: style.borderColor }; }); const calendar = new tui.Calendar("#calendar", { defaultView: "week", usageStatistics: false, isReadOnly: true, useFormPopup: false, useDetailPopup: false, gridSelection: false, calendars: [ ...planetaryCalendars, { id: "planetary", name: "Planetary (Fallback)", color: "#f4f4f5", backgroundColor: "#52525b", dragBackgroundColor: "#52525b", borderColor: "#52525b" }, { id: "astrology", name: "Astrology & Tarot", color: "#18181b", backgroundColor: "#fcd34d", dragBackgroundColor: "#fcd34d", borderColor: "#fcd34d" } ], week: { ...baseWeekOptions, startDayOfWeek: getCenteredWeekStartDay(new Date()) } }); let referenceData = null; let magickDataset = null; let currentGeo = null; let nowInterval = null; let centeredDayKey = getDateKey(new Date()); let renderInProgress = false; let currentTimeFormat = "minutes"; let currentSettings = { ...DEFAULT_SETTINGS }; let monthStripResizeFrame = null; let lastNowSkyGeoKey = ""; let lastNowSkySourceUrl = ""; let activeSection = "home"; let activeTarotSpread = null; // null = browse view; "three-card" | "celtic-cross" = spread view let activeTarotSpreadDraw = []; let numbersSectionInitialized = false; let activeNumberValue = 0; const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4]; const numbersSpecialFlipState = new Map(); const DEFAULT_NUMBER_ENTRIES = Array.from({ length: 10 }, (_, value) => ({ value, label: `${value}`, opposite: 9 - value, digitalRoot: value, summary: "", keywords: [], associations: { kabbalahNode: value === 0 ? 10 : value, playingSuit: "hearts" } })); function normalizeNumberValue(value) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return 0; } const normalized = Math.trunc(parsed); if (normalized < 0) { return 0; } if (normalized > 9) { return 9; } return normalized; } function normalizeNumberEntry(rawEntry) { if (!rawEntry || typeof rawEntry !== "object") { return null; } const value = normalizeNumberValue(rawEntry.value); const oppositeRaw = Number(rawEntry.opposite); const opposite = Number.isFinite(oppositeRaw) ? normalizeNumberValue(oppositeRaw) : (9 - value); const digitalRootRaw = Number(rawEntry.digitalRoot); const digitalRoot = Number.isFinite(digitalRootRaw) ? normalizeNumberValue(digitalRootRaw) : value; const kabbalahNodeRaw = Number(rawEntry?.associations?.kabbalahNode); const kabbalahNode = Number.isFinite(kabbalahNodeRaw) ? Math.max(1, Math.trunc(kabbalahNodeRaw)) : (value === 0 ? 10 : value); const tarotTrumpNumbersRaw = Array.isArray(rawEntry?.associations?.tarotTrumpNumbers) ? rawEntry.associations.tarotTrumpNumbers : []; const tarotTrumpNumbers = Array.from(new Set( tarotTrumpNumbersRaw .map((item) => Number(item)) .filter((item) => Number.isFinite(item)) .map((item) => Math.trunc(item)) )); const playingSuitRaw = String(rawEntry?.associations?.playingSuit || "").trim().toLowerCase(); const playingSuit = ["hearts", "diamonds", "clubs", "spades"].includes(playingSuitRaw) ? playingSuitRaw : "hearts"; return { value, label: String(rawEntry.label || value), opposite, digitalRoot, summary: String(rawEntry.summary || ""), keywords: Array.isArray(rawEntry.keywords) ? rawEntry.keywords.map((keyword) => String(keyword || "").trim()).filter(Boolean) : [], associations: { kabbalahNode, tarotTrumpNumbers, playingSuit } }; } function getNumbersDatasetEntries() { const numbersData = magickDataset?.grouped?.numbers; const rawEntries = Array.isArray(numbersData) ? numbersData : (Array.isArray(numbersData?.entries) ? numbersData.entries : []); const normalizedEntries = rawEntries .map((entry) => normalizeNumberEntry(entry)) .filter(Boolean) .sort((left, right) => left.value - right.value); return normalizedEntries.length ? normalizedEntries : DEFAULT_NUMBER_ENTRIES; } function getNumberEntryByValue(value) { const entries = getNumbersDatasetEntries(); const normalized = normalizeNumberValue(value); return entries.find((entry) => entry.value === normalized) || entries[0] || null; } function getCalendarMonthLinksForNumber(value) { const normalized = normalizeNumberValue(value); const calendarGroups = [ { calendarId: "gregorian", calendarLabel: "Gregorian", months: Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [] }, { calendarId: "hebrew", calendarLabel: "Hebrew", months: Array.isArray(referenceData?.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : [] }, { calendarId: "islamic", calendarLabel: "Islamic", months: Array.isArray(referenceData?.islamicCalendar?.months) ? referenceData.islamicCalendar.months : [] }, { calendarId: "wheel-of-year", calendarLabel: "Wheel of the Year", months: Array.isArray(referenceData?.wheelOfYear?.months) ? referenceData.wheelOfYear.months : [] } ]; const links = []; calendarGroups.forEach((group) => { group.months.forEach((month) => { const monthOrder = Number(month?.order); const normalizedOrder = Number.isFinite(monthOrder) ? Math.trunc(monthOrder) : null; const monthRoot = normalizedOrder != null ? computeDigitalRoot(normalizedOrder) : null; if (monthRoot !== normalized) { return; } links.push({ calendarId: group.calendarId, calendarLabel: group.calendarLabel, monthId: String(month.id || "").trim(), monthName: String(month.name || month.id || "Month").trim(), monthOrder: normalizedOrder }); }); }); return links.filter((link) => link.monthId); } const PLAYING_SUIT_SYMBOL = { hearts: "♥", diamonds: "♦", clubs: "♣", spades: "♠" }; const PLAYING_SUIT_LABEL = { hearts: "Hearts", diamonds: "Diamonds", clubs: "Clubs", spades: "Spades" }; const PLAYING_SUIT_TO_TAROT = { hearts: "Cups", diamonds: "Pentacles", clubs: "Wands", spades: "Swords" }; const PLAYING_RANKS = [ { rank: "A", rankLabel: "Ace", rankValue: 1 }, { rank: "2", rankLabel: "Two", rankValue: 2 }, { rank: "3", rankLabel: "Three", rankValue: 3 }, { rank: "4", rankLabel: "Four", rankValue: 4 }, { rank: "5", rankLabel: "Five", rankValue: 5 }, { rank: "6", rankLabel: "Six", rankValue: 6 }, { rank: "7", rankLabel: "Seven", rankValue: 7 }, { rank: "8", rankLabel: "Eight", rankValue: 8 }, { rank: "9", rankLabel: "Nine", rankValue: 9 }, { rank: "10", rankLabel: "Ten", rankValue: 10 }, { rank: "J", rankLabel: "Jack", rankValue: null }, { rank: "Q", rankLabel: "Queen", rankValue: null }, { rank: "K", rankLabel: "King", rankValue: null } ]; function rankLabelToTarotMinorRank(rankLabel) { const key = String(rankLabel || "").trim().toLowerCase(); if (key === "10" || key === "ten") return "Princess"; if (key === "j" || key === "jack") return "Prince"; if (key === "q" || key === "queen") return "Queen"; if (key === "k" || key === "king") return "Knight"; return String(rankLabel || "").trim(); } function buildFallbackPlayingDeckEntries() { const entries = []; Object.keys(PLAYING_SUIT_SYMBOL).forEach((suit) => { PLAYING_RANKS.forEach((rank) => { const tarotSuit = PLAYING_SUIT_TO_TAROT[suit]; const tarotRank = rankLabelToTarotMinorRank(rank.rankLabel); entries.push({ id: `${rank.rank}${PLAYING_SUIT_SYMBOL[suit]}`, suit, suitLabel: PLAYING_SUIT_LABEL[suit], suitSymbol: PLAYING_SUIT_SYMBOL[suit], rank: rank.rank, rankLabel: rank.rankLabel, rankValue: rank.rankValue, tarotSuit, tarotCard: `${tarotRank} of ${tarotSuit}` }); }); }); return entries; } function getPlayingDeckEntries() { const deckData = magickDataset?.grouped?.["playing-cards-52"]; const rawEntries = Array.isArray(deckData) ? deckData : (Array.isArray(deckData?.entries) ? deckData.entries : []); if (!rawEntries.length) { return buildFallbackPlayingDeckEntries(); } return rawEntries .map((entry) => { const suit = String(entry?.suit || "").trim().toLowerCase(); const rankLabel = String(entry?.rankLabel || "").trim(); const rank = String(entry?.rank || "").trim(); if (!suit || !rank) { return null; } const suitSymbol = String(entry?.suitSymbol || PLAYING_SUIT_SYMBOL[suit] || "").trim(); const tarotSuit = String(entry?.tarotSuit || PLAYING_SUIT_TO_TAROT[suit] || "").trim(); const tarotCard = String(entry?.tarotCard || "").trim(); const rankValueRaw = Number(entry?.rankValue); const rankValue = Number.isFinite(rankValueRaw) ? Math.trunc(rankValueRaw) : null; return { id: String(entry?.id || `${rank}${suitSymbol}`).trim(), suit, suitLabel: String(entry?.suitLabel || PLAYING_SUIT_LABEL[suit] || suit).trim(), suitSymbol, rank, rankLabel: rankLabel || rank, rankValue, tarotSuit, tarotCard: tarotCard || `${rankLabelToTarotMinorRank(rankLabel || rank)} of ${tarotSuit}` }; }) .filter(Boolean); } function findPlayingCardBySuitAndValue(entries, suit, value) { const normalizedSuit = String(suit || "").trim().toLowerCase(); const targetValue = Number(value); return entries.find((entry) => entry.suit === normalizedSuit && Number(entry.rankValue) === targetValue) || null; } function buildNumbersSpecialCardSlots(playingSuit) { const suit = String(playingSuit || "hearts").trim().toLowerCase(); const selectedSuit = ["hearts", "diamonds", "clubs", "spades"].includes(suit) ? suit : "hearts"; const deckEntries = getPlayingDeckEntries(); const cardEl = document.createElement("div"); cardEl.className = "numbers-detail-card numbers-special-card-section"; const headingEl = document.createElement("strong"); headingEl.textContent = "4 Card Arrangement"; const subEl = document.createElement("div"); subEl.className = "numbers-detail-text numbers-detail-text--muted"; subEl.textContent = `Click a card to flip to its opposite (${PLAYING_SUIT_LABEL[selectedSuit]} ↔ ${PLAYING_SUIT_TO_TAROT[selectedSuit]}).`; const boardEl = document.createElement("div"); boardEl.className = "numbers-special-board"; NUMBERS_SPECIAL_BASE_VALUES.forEach((baseValue) => { const oppositeValue = 9 - baseValue; const frontCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, baseValue); const backCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, oppositeValue); if (!frontCard || !backCard) { return; } const slotKey = `${selectedSuit}:${baseValue}`; const isFlipped = Boolean(numbersSpecialFlipState.get(slotKey)); const faceBtn = document.createElement("button"); faceBtn.type = "button"; faceBtn.className = `numbers-special-card${isFlipped ? " is-flipped" : ""}`; faceBtn.setAttribute("aria-pressed", isFlipped ? "true" : "false"); faceBtn.setAttribute("aria-label", `${frontCard.rankLabel} of ${frontCard.suitLabel}. Click to flip to ${backCard.rankLabel}.`); faceBtn.dataset.suit = selectedSuit; const innerEl = document.createElement("div"); innerEl.className = "numbers-special-card-inner"; const frontFaceEl = document.createElement("div"); frontFaceEl.className = "numbers-special-card-face numbers-special-card-face--front"; const frontRankEl = document.createElement("div"); frontRankEl.className = "numbers-special-card-rank"; frontRankEl.textContent = frontCard.rankLabel; const frontSuitEl = document.createElement("div"); frontSuitEl.className = "numbers-special-card-suit"; frontSuitEl.textContent = frontCard.suitSymbol; const frontMetaEl = document.createElement("div"); frontMetaEl.className = "numbers-special-card-meta"; frontMetaEl.textContent = frontCard.tarotCard; frontFaceEl.append(frontRankEl, frontSuitEl, frontMetaEl); const backFaceEl = document.createElement("div"); backFaceEl.className = "numbers-special-card-face numbers-special-card-face--back"; const backTagEl = document.createElement("div"); backTagEl.className = "numbers-special-card-tag"; backTagEl.textContent = "Opposite"; const backRankEl = document.createElement("div"); backRankEl.className = "numbers-special-card-rank"; backRankEl.textContent = backCard.rankLabel; const backSuitEl = document.createElement("div"); backSuitEl.className = "numbers-special-card-suit"; backSuitEl.textContent = backCard.suitSymbol; const backMetaEl = document.createElement("div"); backMetaEl.className = "numbers-special-card-meta"; backMetaEl.textContent = backCard.tarotCard; backFaceEl.append(backTagEl, backRankEl, backSuitEl, backMetaEl); innerEl.append(frontFaceEl, backFaceEl); faceBtn.append(innerEl); faceBtn.addEventListener("click", () => { const next = !Boolean(numbersSpecialFlipState.get(slotKey)); numbersSpecialFlipState.set(slotKey, next); faceBtn.classList.toggle("is-flipped", next); faceBtn.setAttribute("aria-pressed", next ? "true" : "false"); }); boardEl.appendChild(faceBtn); }); if (!boardEl.childElementCount) { const emptyEl = document.createElement("div"); emptyEl.className = "numbers-detail-text numbers-detail-text--muted"; emptyEl.textContent = "No card slots available for this mapping yet."; boardEl.appendChild(emptyEl); } cardEl.append(headingEl, subEl, boardEl); return cardEl; } function renderNumbersSpecialPanel(value) { if (!numbersSpecialPanelEl) { return; } const entry = getNumberEntryByValue(value); const playingSuit = entry?.associations?.playingSuit || "hearts"; const boardCardEl = buildNumbersSpecialCardSlots(playingSuit); numbersSpecialPanelEl.replaceChildren(boardCardEl); } function computeDigitalRoot(value) { let current = Math.abs(Math.trunc(Number(value))); if (!Number.isFinite(current)) { return null; } while (current >= 10) { current = String(current) .split("") .reduce((sum, digit) => sum + Number(digit), 0); } return current; } function describeDigitalRootReduction(value) { let current = Math.abs(Math.trunc(Number(value))); if (!Number.isFinite(current)) { return ""; } if (current < 10) { return `${current} → ${current}`; } const parts = [`${current}`]; while (current >= 10) { const digits = String(current).split("").map((digit) => Number(digit)); const sum = digits.reduce((acc, digit) => acc + digit, 0); parts.push(`${digits.join(" + ")} = ${sum}`); current = sum; } return parts.join(" → "); } function parseTarotCardNumber(rawValue) { if (typeof rawValue === "number") { return Number.isFinite(rawValue) ? Math.trunc(rawValue) : null; } if (typeof rawValue === "string") { const trimmed = rawValue.trim(); if (!trimmed || !/^-?\d+$/.test(trimmed)) { return null; } return Number(trimmed); } return null; } const TAROT_RANK_NUMBER_MAP = { ace: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 }; function extractTarotCardNumericValue(card) { const directNumber = parseTarotCardNumber(card?.number); if (directNumber !== null) { return directNumber; } const rankKey = String(card?.rank || "").trim().toLowerCase(); if (Object.prototype.hasOwnProperty.call(TAROT_RANK_NUMBER_MAP, rankKey)) { return TAROT_RANK_NUMBER_MAP[rankKey]; } const numerologyRelation = Array.isArray(card?.relations) ? card.relations.find((relation) => String(relation?.type || "").trim().toLowerCase() === "numerology") : null; const relationValue = Number(numerologyRelation?.data?.value); if (Number.isFinite(relationValue)) { return Math.trunc(relationValue); } return null; } function getAlphabetPositionLinksForDigitalRoot(targetRoot) { const alphabets = magickDataset?.grouped?.alphabets; if (!alphabets || typeof alphabets !== "object") { return []; } const links = []; const addLink = (alphabetLabel, entry, buttonLabel, detail) => { const index = Number(entry?.index); if (!Number.isFinite(index)) { return; } const normalizedIndex = Math.trunc(index); if (computeDigitalRoot(normalizedIndex) !== targetRoot) { return; } links.push({ alphabet: alphabetLabel, index: normalizedIndex, label: buttonLabel, detail }); }; const toTitle = (value) => String(value || "") .trim() .replace(/[_-]+/g, " ") .replace(/\s+/g, " ") .toLowerCase() .replace(/\b([a-z])/g, (match, ch) => ch.toUpperCase()); const englishEntries = Array.isArray(alphabets.english) ? alphabets.english : []; englishEntries.forEach((entry) => { const letter = String(entry?.letter || "").trim(); if (!letter) { return; } addLink( "English", entry, `${letter}`, { alphabet: "english", englishLetter: letter } ); }); const greekEntries = Array.isArray(alphabets.greek) ? alphabets.greek : []; greekEntries.forEach((entry) => { const greekName = String(entry?.name || "").trim(); if (!greekName) { return; } const glyph = String(entry?.char || "").trim(); const displayName = String(entry?.displayName || toTitle(greekName)).trim(); addLink( "Greek", entry, glyph ? `${displayName} - ${glyph}` : displayName, { alphabet: "greek", greekName } ); }); const hebrewEntries = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : []; hebrewEntries.forEach((entry) => { const hebrewLetterId = String(entry?.hebrewLetterId || "").trim(); if (!hebrewLetterId) { return; } const glyph = String(entry?.char || "").trim(); const name = String(entry?.name || hebrewLetterId).trim(); const displayName = toTitle(name); addLink( "Hebrew", entry, glyph ? `${displayName} - ${glyph}` : displayName, { alphabet: "hebrew", hebrewLetterId } ); }); const arabicEntries = Array.isArray(alphabets.arabic) ? alphabets.arabic : []; arabicEntries.forEach((entry) => { const arabicName = String(entry?.name || "").trim(); if (!arabicName) { return; } const glyph = String(entry?.char || "").trim(); const displayName = toTitle(arabicName); addLink( "Arabic", entry, glyph ? `${displayName} - ${glyph}` : displayName, { alphabet: "arabic", arabicName } ); }); const enochianEntries = Array.isArray(alphabets.enochian) ? alphabets.enochian : []; enochianEntries.forEach((entry) => { const enochianId = String(entry?.id || "").trim(); if (!enochianId) { return; } const title = String(entry?.title || enochianId).trim(); const displayName = toTitle(title); addLink( "Enochian", entry, `${displayName}`, { alphabet: "enochian", enochianId } ); }); return links.sort((left, right) => { if (left.index !== right.index) { return left.index - right.index; } const alphabetCompare = left.alphabet.localeCompare(right.alphabet); if (alphabetCompare !== 0) { return alphabetCompare; } return left.label.localeCompare(right.label); }); } function getTarotCardsForDigitalRoot(targetRoot, numberEntry = null) { if (typeof ensureTarotSection === "function" && referenceData) { ensureTarotSection(referenceData, magickDataset); } const allCards = window.TarotSectionUi?.getCards?.() || []; const explicitTrumpNumbers = Array.isArray(numberEntry?.associations?.tarotTrumpNumbers) ? numberEntry.associations.tarotTrumpNumbers .map((value) => Number(value)) .filter((value) => Number.isFinite(value)) .map((value) => Math.trunc(value)) : []; const filteredCards = explicitTrumpNumbers.length ? allCards.filter((card) => { const numberValue = parseTarotCardNumber(card?.number); return card?.arcana === "Major" && numberValue !== null && explicitTrumpNumbers.includes(numberValue); }) : allCards.filter((card) => { const numberValue = extractTarotCardNumericValue(card); return numberValue !== null && computeDigitalRoot(numberValue) === targetRoot; }); return filteredCards .sort((left, right) => { const leftNumber = extractTarotCardNumericValue(left); const rightNumber = extractTarotCardNumericValue(right); if (leftNumber !== rightNumber) { return (leftNumber ?? 0) - (rightNumber ?? 0); } if (left?.arcana !== right?.arcana) { return left?.arcana === "Major" ? -1 : 1; } return String(left?.name || "").localeCompare(String(right?.name || "")); }); } function renderNumbersList() { if (!numbersListEl) { return; } const entries = getNumbersDatasetEntries(); if (!entries.some((entry) => entry.value === activeNumberValue)) { activeNumberValue = entries[0]?.value ?? 0; } const fragment = document.createDocumentFragment(); entries.forEach((entry) => { const button = document.createElement("button"); button.type = "button"; button.className = `planet-list-item${entry.value === activeNumberValue ? " is-selected" : ""}`; button.dataset.numberValue = String(entry.value); button.setAttribute("role", "option"); button.setAttribute("aria-selected", entry.value === activeNumberValue ? "true" : "false"); const nameEl = document.createElement("span"); nameEl.className = "planet-list-name"; nameEl.textContent = `${entry.label}`; const metaEl = document.createElement("span"); metaEl.className = "planet-list-meta"; metaEl.textContent = `Opposite ${entry.opposite}`; button.append(nameEl, metaEl); fragment.appendChild(button); }); numbersListEl.replaceChildren(fragment); if (numbersCountEl) { numbersCountEl.textContent = `${entries.length} entries`; } } function renderNumberDetail(value) { const entry = getNumberEntryByValue(value); if (!entry) { return; } const normalized = entry.value; const opposite = entry.opposite; const rootTarget = normalizeNumberValue(entry.digitalRoot); if (numbersDetailNameEl) { numbersDetailNameEl.textContent = `Number ${normalized} · ${entry.label}`; } if (numbersDetailTypeEl) { numbersDetailTypeEl.textContent = `Opposite: ${opposite}`; } if (numbersDetailSummaryEl) { numbersDetailSummaryEl.textContent = entry.summary || ""; } renderNumbersSpecialPanel(normalized); if (!numbersDetailBodyEl) { return; } numbersDetailBodyEl.replaceChildren(); const pairCardEl = document.createElement("div"); pairCardEl.className = "numbers-detail-card"; const pairHeadingEl = document.createElement("strong"); pairHeadingEl.textContent = "Number Pair"; const pairTextEl = document.createElement("div"); pairTextEl.className = "numbers-detail-text"; pairTextEl.textContent = `Opposite: ${opposite}`; const keywordText = entry.keywords.length ? `Keywords: ${entry.keywords.join(", ")}` : "Keywords: --"; const pairKeywordsEl = document.createElement("div"); pairKeywordsEl.className = "numbers-detail-text numbers-detail-text--muted"; pairKeywordsEl.textContent = keywordText; const oppositeBtn = document.createElement("button"); oppositeBtn.type = "button"; oppositeBtn.className = "numbers-nav-btn"; oppositeBtn.textContent = `Open Opposite Number ${opposite}`; oppositeBtn.addEventListener("click", () => { selectNumberEntry(opposite); }); pairCardEl.append(pairHeadingEl, pairTextEl, pairKeywordsEl, oppositeBtn); const kabbalahCardEl = document.createElement("div"); kabbalahCardEl.className = "numbers-detail-card"; const kabbalahHeadingEl = document.createElement("strong"); kabbalahHeadingEl.textContent = "Kabbalah Link"; const kabbalahNode = Number(entry?.associations?.kabbalahNode); const kabbalahTextEl = document.createElement("div"); kabbalahTextEl.className = "numbers-detail-text"; kabbalahTextEl.textContent = `Tree node target: ${kabbalahNode}`; const kabbalahBtn = document.createElement("button"); kabbalahBtn.type = "button"; kabbalahBtn.className = "numbers-nav-btn"; kabbalahBtn.textContent = `Open Kabbalah Tree Node ${kabbalahNode}`; kabbalahBtn.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("nav:kabbalah-path", { detail: { pathNo: kabbalahNode } })); }); kabbalahCardEl.append(kabbalahHeadingEl, kabbalahTextEl, kabbalahBtn); const alphabetCardEl = document.createElement("div"); alphabetCardEl.className = "numbers-detail-card"; const alphabetHeadingEl = document.createElement("strong"); alphabetHeadingEl.textContent = "Alphabet Links"; const alphabetLinksWrapEl = document.createElement("div"); alphabetLinksWrapEl.className = "numbers-links-wrap"; const alphabetLinks = getAlphabetPositionLinksForDigitalRoot(rootTarget); if (!alphabetLinks.length) { const emptyAlphabetEl = document.createElement("div"); emptyAlphabetEl.className = "numbers-detail-text numbers-detail-text--muted"; emptyAlphabetEl.textContent = "No alphabet position entries found for this digital root yet."; alphabetLinksWrapEl.appendChild(emptyAlphabetEl); } else { alphabetLinks.forEach((link) => { const button = document.createElement("button"); button.type = "button"; button.className = "numbers-nav-btn"; button.textContent = `${link.alphabet}: ${link.label}`; button.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("nav:alphabet", { detail: link.detail })); }); alphabetLinksWrapEl.appendChild(button); }); } alphabetCardEl.append(alphabetHeadingEl, alphabetLinksWrapEl); const tarotCardEl = document.createElement("div"); tarotCardEl.className = "numbers-detail-card"; const tarotHeadingEl = document.createElement("strong"); tarotHeadingEl.textContent = "Tarot Links"; const tarotLinksWrapEl = document.createElement("div"); tarotLinksWrapEl.className = "numbers-links-wrap"; const tarotCards = getTarotCardsForDigitalRoot(rootTarget, entry); if (!tarotCards.length) { const emptyEl = document.createElement("div"); emptyEl.className = "numbers-detail-text numbers-detail-text--muted"; emptyEl.textContent = "No tarot numeric entries found yet for this root. Add card numbers to map them."; tarotLinksWrapEl.appendChild(emptyEl); } else { tarotCards.forEach((card) => { const button = document.createElement("button"); button.type = "button"; button.className = "numbers-nav-btn"; button.textContent = `${card.name}`; button.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("nav:tarot-trump", { detail: { cardName: card.name } })); }); tarotLinksWrapEl.appendChild(button); }); } tarotCardEl.append(tarotHeadingEl, tarotLinksWrapEl); const calendarCardEl = document.createElement("div"); calendarCardEl.className = "numbers-detail-card"; const calendarHeadingEl = document.createElement("strong"); calendarHeadingEl.textContent = "Calendar Links"; const calendarLinksWrapEl = document.createElement("div"); calendarLinksWrapEl.className = "numbers-links-wrap"; const calendarLinks = getCalendarMonthLinksForNumber(normalized); if (!calendarLinks.length) { const emptyCalendarEl = document.createElement("div"); emptyCalendarEl.className = "numbers-detail-text numbers-detail-text--muted"; emptyCalendarEl.textContent = "No calendar months currently mapped to this number."; calendarLinksWrapEl.appendChild(emptyCalendarEl); } else { calendarLinks.forEach((link) => { const button = document.createElement("button"); button.type = "button"; button.className = "numbers-nav-btn"; button.textContent = `${link.calendarLabel}: ${link.monthName} (Month ${link.monthOrder})`; button.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("nav:calendar-month", { detail: { calendarId: link.calendarId, monthId: link.monthId } })); }); calendarLinksWrapEl.appendChild(button); }); } calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl); numbersDetailBodyEl.append(pairCardEl, kabbalahCardEl, alphabetCardEl, tarotCardEl, calendarCardEl); } function selectNumberEntry(value) { const entry = getNumberEntryByValue(value); activeNumberValue = entry ? entry.value : 0; renderNumbersList(); renderNumberDetail(activeNumberValue); } function ensureNumbersSection() { if (!numbersListEl) { return; } if (!numbersSectionInitialized) { numbersListEl.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Node)) { return; } const button = target instanceof Element ? target.closest(".planet-list-item") : null; if (!(button instanceof HTMLButtonElement)) { return; } const value = Number(button.dataset.numberValue); if (!Number.isFinite(value)) { return; } selectNumberEntry(value); }); numbersSectionInitialized = true; } renderNumbersList(); renderNumberDetail(activeNumberValue); } const THREE_CARD_POSITIONS = [ { pos: "past", label: "Past" }, { pos: "present", label: "Present" }, { pos: "future", label: "Future" } ]; const CELTIC_CROSS_POSITIONS = [ { pos: "crown", label: "Crown" }, { pos: "out", label: "Outcome" }, { pos: "past", label: "Recent Past" }, { pos: "present", label: "Present" }, { pos: "near-fut", label: "Near Future" }, { pos: "hope", label: "Hopes & Fears" }, { pos: "chall", label: "Challenge" }, { pos: "env", label: "Environment" }, { pos: "found", label: "Foundation" }, { pos: "self", label: "Self" } ]; function normalizeTarotSpread(value) { return value === "celtic-cross" ? "celtic-cross" : "three-card"; } function drawNFromDeck(n) { const allCards = window.TarotSectionUi?.getCards?.() || []; if (!allCards.length) return []; const shuffled = [...allCards]; for (let index = shuffled.length - 1; index > 0; index -= 1) { const swapIndex = Math.floor(Math.random() * (index + 1)); [shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]]; } return shuffled.slice(0, n).map((card) => ({ ...card, reversed: Math.random() < 0.3 })); } function escapeHtml(value) { return String(value || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function getSpreadPositions(spreadId) { return spreadId === "celtic-cross" ? CELTIC_CROSS_POSITIONS : THREE_CARD_POSITIONS; } function regenerateTarotSpreadDraw() { const normalizedSpread = normalizeTarotSpread(activeTarotSpread); const positions = getSpreadPositions(normalizedSpread); const cards = drawNFromDeck(positions.length); activeTarotSpreadDraw = positions.map((position, index) => ({ position, card: cards[index] || null })); } function renderTarotSpreadMeanings() { if (!tarotSpreadMeaningsEl) { return; } if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) { tarotSpreadMeaningsEl.innerHTML = ""; return; } tarotSpreadMeaningsEl.innerHTML = activeTarotSpreadDraw.map((entry) => { const positionLabel = escapeHtml(entry.position.label).toUpperCase(); const card = entry.card; const cardName = escapeHtml(card.name || "Unknown Card"); const meaningText = escapeHtml(card.reversed ? (card.meanings?.reversed || card.summary || "--") : (card.meanings?.upright || card.summary || "--")); const keywords = Array.isArray(card.keywords) ? card.keywords.map((keyword) => String(keyword || "").trim()).filter(Boolean) : []; const keywordMarkup = keywords.length ? `
Keywords: ${escapeHtml(keywords.join(", "))}
` : ""; const orientationMarkup = card.reversed ? " (Reversed)" : ""; return `
` + `
${positionLabel}: ${cardName}${orientationMarkup}
` + `
${meaningText}
` + keywordMarkup + `
`; }).join(""); } function renderTarotSpread() { if (!tarotSpreadBoardEl) return; const normalizedSpread = normalizeTarotSpread(activeTarotSpread); const isCeltic = normalizedSpread === "celtic-cross"; if (!activeTarotSpreadDraw.length) { regenerateTarotSpreadDraw(); } tarotSpreadBoardEl.className = `tarot-spread-board tarot-spread-board--${isCeltic ? "celtic" : "three"}`; if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) { tarotSpreadBoardEl.innerHTML = `
Tarot deck not loaded yet — open Cards first, then return to Spread.
`; if (tarotSpreadMeaningsEl) { tarotSpreadMeaningsEl.innerHTML = ""; } return; } renderTarotSpreadMeanings(); tarotSpreadBoardEl.innerHTML = activeTarotSpreadDraw.map((entry) => { const position = entry.position; const card = entry.card; const imgSrc = window.TarotCardImages?.resolveTarotCardImage?.(card.name); const reversed = card.reversed; const wrapClass = reversed ? "spread-card-wrap is-reversed" : "spread-card-wrap"; const imgHtml = imgSrc ? `${escapeHtml(card.name)}` : `
${escapeHtml(card.name)}
`; const reversedTag = reversed ? `Reversed` : ""; return `
` + `
${escapeHtml(position.label)}
` + `
${imgHtml}
` + `
${escapeHtml(card.name)}${reversedTag}
` + `
`; }).join(""); } function applyTarotSpreadViewState() { const isSpreadOpen = activeTarotSpread !== null; const isCeltic = activeTarotSpread === "celtic-cross"; const isTarotActive = activeSection === "tarot"; if (tarotBrowseViewEl) tarotBrowseViewEl.hidden = isSpreadOpen; if (tarotSpreadViewEl) tarotSpreadViewEl.hidden = !isSpreadOpen; if (tarotSpreadBtnThreeEl) tarotSpreadBtnThreeEl.classList.toggle("is-active", isSpreadOpen && !isCeltic); if (tarotSpreadBtnCelticEl) tarotSpreadBtnCelticEl.classList.toggle("is-active", isSpreadOpen && isCeltic); if (openTarotCardsEl) openTarotCardsEl.classList.toggle("is-active", isTarotActive && !isSpreadOpen); if (openTarotSpreadEl) openTarotSpreadEl.classList.toggle("is-active", isTarotActive && isSpreadOpen); } function showTarotCardsView() { activeTarotSpread = null; activeTarotSpreadDraw = []; applyTarotSpreadViewState(); if (typeof ensureTarotSection === "function" && referenceData) { ensureTarotSection(referenceData, magickDataset); } const detailPanelEl = document.querySelector("#tarot-browse-view .tarot-detail-panel"); if (detailPanelEl instanceof HTMLElement) { detailPanelEl.scrollTop = 0; } } function showTarotSpreadView(spreadId = "three-card") { activeTarotSpread = normalizeTarotSpread(spreadId); regenerateTarotSpreadDraw(); applyTarotSpreadViewState(); if (typeof ensureTarotSection === "function" && referenceData) { ensureTarotSection(referenceData, magickDataset); } renderTarotSpread(); } function setTarotSpread(spreadId, openTarotSection = false) { if (openTarotSection) { setActiveSection("tarot"); } showTarotSpreadView(spreadId); } const DEFAULT_WEEKDAY_RULERS = { 0: { symbol: "☉", name: "Sol" }, 1: { symbol: "☾", name: "Luna" }, 2: { symbol: "♂", name: "Mars" }, 3: { symbol: "☿", name: "Mercury" }, 4: { symbol: "♃", name: "Jupiter" }, 5: { symbol: "♀", name: "Venus" }, 6: { symbol: "♄", name: "Saturn" } }; function getWeekdayIndexFromName(weekdayName) { const normalized = String(weekdayName || "").trim().toLowerCase(); if (normalized === "sunday") return 0; if (normalized === "monday") return 1; if (normalized === "tuesday") return 2; if (normalized === "wednesday") return 3; if (normalized === "thursday") return 4; if (normalized === "friday") return 5; if (normalized === "saturday") return 6; return null; } function buildWeekdayRulerLookup(planets) { const lookup = { ...DEFAULT_WEEKDAY_RULERS }; if (!planets || typeof planets !== "object") { return lookup; } Object.values(planets).forEach((planet) => { const weekdayIndex = getWeekdayIndexFromName(planet?.weekday); if (weekdayIndex === null) { return; } lookup[weekdayIndex] = { symbol: planet?.symbol || lookup[weekdayIndex].symbol, name: planet?.name || lookup[weekdayIndex].name }; }); return lookup; } function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } function lerp(start, end, t) { return start + (end - start) * t; } function lerpRgb(from, to, t) { return [ Math.round(lerp(from[0], to[0], t)), Math.round(lerp(from[1], to[1], t)), Math.round(lerp(from[2], to[2], t)) ]; } function rgbString(rgb) { return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; } function getActiveGeoForRuler() { if (currentGeo) { return currentGeo; } try { return parseGeoInput(); } catch { return null; } } function buildSunRulerGradient(geo, date) { if (!window.SunCalc || !geo || !date) { return null; } const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); const sampleCount = 48; const samples = []; for (let index = 0; index <= sampleCount; index += 1) { const sampleDate = new Date(dayStart.getTime() + index * 30 * 60 * 1000); const position = window.SunCalc.getPosition(sampleDate, geo.latitude, geo.longitude); const altitudeDeg = (position.altitude * 180) / Math.PI; samples.push(altitudeDeg); } const maxAltitude = Math.max(...samples); const NIGHT = [6, 7, 10]; const PRE_DAWN = [22, 26, 38]; const SUN_RED = [176, 45, 36]; const SUN_ORANGE = [246, 133, 54]; const SKY_BLUE = [58, 134, 255]; const nightFloor = -8; const twilightEdge = -2; const redToOrangeEdge = 2; const orangeToBlueEdge = 8; const daylightRange = Math.max(1, maxAltitude - orangeToBlueEdge); const stops = samples.map((altitudeDeg, index) => { let color; if (altitudeDeg <= nightFloor) { color = NIGHT; } else if (altitudeDeg <= twilightEdge) { const t = clamp((altitudeDeg - nightFloor) / (twilightEdge - nightFloor), 0, 1); color = lerpRgb(NIGHT, PRE_DAWN, t); } else if (altitudeDeg <= redToOrangeEdge) { const t = clamp((altitudeDeg - twilightEdge) / (redToOrangeEdge - twilightEdge), 0, 1); color = lerpRgb(PRE_DAWN, SUN_RED, t); } else if (altitudeDeg <= orangeToBlueEdge) { const t = clamp((altitudeDeg - redToOrangeEdge) / (orangeToBlueEdge - redToOrangeEdge), 0, 1); color = lerpRgb(SUN_RED, SUN_ORANGE, t); } else { const t = clamp((altitudeDeg - orangeToBlueEdge) / daylightRange, 0, 1); color = lerpRgb(SUN_ORANGE, SKY_BLUE, t); } const pct = ((index / sampleCount) * 100).toFixed(2); return `${rgbString(color)} ${pct}%`; }); return `linear-gradient(to bottom, ${stops.join(", ")})`; } function applySunRulerGradient(referenceDate = new Date()) { const geo = getActiveGeoForRuler(); if (!geo) { return; } const gradient = buildSunRulerGradient(geo, referenceDate); if (!gradient) { return; } const rulerColumns = document.querySelectorAll(".toastui-calendar-timegrid-time-column"); rulerColumns.forEach((column) => { column.style.backgroundImage = gradient; column.style.backgroundRepeat = "no-repeat"; column.style.backgroundSize = "100% 100%"; }); } function normalizeDateLike(value) { if (value instanceof Date) { return value; } if (value && typeof value.getTime === "function") { return new Date(value.getTime()); } return new Date(value); } function getTimeParts(dateLike) { const date = normalizeDateLike(dateLike); const hours = date.getHours(); const minutes = date.getMinutes(); return { hours, minutes, totalMinutes: hours * 60 + minutes }; } function formatHourStyle(dateLike) { const { totalMinutes } = getTimeParts(dateLike); return `${Math.floor(totalMinutes / 60)}hr`; } function formatMinuteStyle(dateLike) { const { totalMinutes } = getTimeParts(dateLike); return `${totalMinutes}m`; } function formatSecondStyle(dateLike) { const { totalMinutes } = getTimeParts(dateLike); const totalSeconds = totalMinutes * 60; return `${totalSeconds}s`; } function formatCalendarTime(dateLike) { if (currentTimeFormat === "hours") { return formatHourStyle(dateLike); } if (currentTimeFormat === "seconds") { return formatSecondStyle(dateLike); } return formatMinuteStyle(dateLike); } function formatCalendarTimeFromTemplatePayload(payload) { if (payload && typeof payload.hour === "number") { const hours = payload.hour; const minutes = typeof payload.minutes === "number" ? payload.minutes : 0; const totalMinutes = hours * 60 + minutes; if (currentTimeFormat === "hours") { return `${Math.floor(totalMinutes / 60)}hr`; } if (currentTimeFormat === "seconds") { return `${totalMinutes * 60}s`; } return `${totalMinutes}m`; } if (payload && payload.time) { return formatCalendarTime(payload.time); } if (currentTimeFormat === "hours") { return "12am"; } if (currentTimeFormat === "seconds") { return "0s"; } return "0m"; } function getMoonPhaseGlyph(phaseName) { if (phaseName === "New Moon") return "🌑"; if (phaseName === "Waxing Crescent") return "🌒"; if (phaseName === "First Quarter") return "🌓"; if (phaseName === "Waxing Gibbous") return "🌔"; if (phaseName === "Full Moon") return "🌕"; if (phaseName === "Waning Gibbous") return "🌖"; if (phaseName === "Last Quarter") return "🌗"; return "🌘"; } function applyDynamicNowIndicatorVisual(referenceDate = new Date()) { if (!currentGeo || !window.SunCalc) { return; } const labelEl = document.querySelector( ".toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-current-time" ); const markerEl = document.querySelector( ".toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator .toastui-calendar-timegrid-now-indicator-marker" ); if (!labelEl || !markerEl) { return; } const sunPosition = window.SunCalc.getPosition(referenceDate, currentGeo.latitude, currentGeo.longitude); const sunAltitudeDeg = (sunPosition.altitude * 180) / Math.PI; const isSunMode = sunAltitudeDeg >= -4; let icon = "☀️"; let visualKey = "sun-0"; labelEl.classList.remove("is-sun", "is-moon"); markerEl.classList.remove("is-sun", "is-moon"); if (isSunMode) { const intensity = clamp((sunAltitudeDeg + 4) / 70, 0, 1); const intensityPercent = Math.round(intensity * 100); icon = "☀️"; visualKey = `sun-${intensityPercent}`; labelEl.classList.add("is-sun"); markerEl.classList.add("is-sun"); labelEl.style.setProperty("--sun-glow-size", `${Math.round(8 + intensity * 16)}px`); labelEl.style.setProperty("--sun-glow-alpha", (0.35 + intensity * 0.55).toFixed(2)); markerEl.style.setProperty("--sun-marker-glow-size", `${Math.round(10 + intensity * 24)}px`); markerEl.style.setProperty("--sun-marker-ray-opacity", (0.45 + intensity * 0.5).toFixed(2)); labelEl.title = `Sun altitude ${sunAltitudeDeg.toFixed(1)}°`; } else { const moonIllum = window.SunCalc.getMoonIllumination(referenceDate); const moonPct = Math.round(moonIllum.fraction * 100); const moonPhaseName = getMoonPhaseName(moonIllum.phase); icon = getMoonPhaseGlyph(moonPhaseName); visualKey = `moon-${moonPct}-${moonPhaseName}`; labelEl.classList.add("is-moon"); markerEl.classList.add("is-moon"); labelEl.style.setProperty("--moon-glow-alpha", (0.2 + moonIllum.fraction * 0.45).toFixed(2)); markerEl.style.setProperty("--moon-glow-alpha", (0.2 + moonIllum.fraction * 0.45).toFixed(2)); labelEl.title = `${moonPhaseName} (${moonPct}%)`; } if (labelEl.dataset.celestialKey !== visualKey) { labelEl.innerHTML = [ '', `${icon}`, "" ].join(""); labelEl.dataset.celestialKey = visualKey; } } function convertAxisTimeToMinutes(text) { const normalized = String(text || "").trim().toLowerCase(); if (!normalized) { return null; } const minuteMatch = normalized.match(/^(\d{1,4})m$/); if (minuteMatch) { return `${Number(minuteMatch[1])}m`; } const secondMatch = normalized.match(/^(\d{1,6})s$/); if (secondMatch) { return `${Math.floor(Number(secondMatch[1]) / 60)}m`; } const hourMatch = normalized.match(/^(\d{1,2})hr$/); if (hourMatch) { return `${Number(hourMatch[1]) * 60}m`; } const ampmMatch = normalized.match(/^(\d{1,2})(?::(\d{2}))?(?::(\d{2}))?\s*(am|pm)$/); if (ampmMatch) { let hour = Number(ampmMatch[1]) % 12; const minutes = Number(ampmMatch[2] || "0"); const suffix = ampmMatch[3]; if (suffix === "pm") { hour += 12; } return `${hour * 60 + minutes}m`; } const twentyFourMatch = normalized.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/); if (twentyFourMatch) { const hour = Number(twentyFourMatch[1]); const minutes = Number(twentyFourMatch[2]); return `${hour * 60 + minutes}m`; } return null; } function convertAxisTimeToSeconds(text) { const minuteLabel = convertAxisTimeToMinutes(text); if (!minuteLabel) { return null; } const minutes = Number(minuteLabel.replace("m", "")); if (Number.isNaN(minutes)) { return null; } return `${minutes * 60}s`; } function convertAxisTimeToHours(text) { const minuteLabel = convertAxisTimeToMinutes(text); if (!minuteLabel) { return null; } const minutes = Number(minuteLabel.replace("m", "")); if (Number.isNaN(minutes)) { return null; } return `${Math.floor(minutes / 60)}hr`; } function forceAxisLabelFormat() { const labelNodes = document.querySelectorAll( ".toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time-label" ); labelNodes.forEach((node) => { if (!node.dataset.originalLabel) { node.dataset.originalLabel = node.textContent; } if (currentTimeFormat === "minutes") { const converted = convertAxisTimeToMinutes(node.dataset.originalLabel); if (converted) { node.textContent = converted; } } else if (currentTimeFormat === "seconds") { const converted = convertAxisTimeToSeconds(node.dataset.originalLabel); if (converted) { node.textContent = converted; } } else if (currentTimeFormat === "hours") { const converted = convertAxisTimeToHours(node.dataset.originalLabel); if (converted) { node.textContent = converted; } } else { node.textContent = node.dataset.originalLabel; } }); } function getVisibleWeekDates() { if (typeof calendar.getDateRangeStart !== "function") { return []; } const rangeStart = calendar.getDateRangeStart(); if (!rangeStart) { return []; } const startDateLike = normalizeDateLike(rangeStart); const startDate = new Date( startDateLike.getFullYear(), startDateLike.getMonth(), startDateLike.getDate(), 0, 0, 0, 0 ); return Array.from({ length: 7 }, (_, dayOffset) => { const day = new Date(startDate); day.setDate(startDate.getDate() + dayOffset); return day; }); } function buildMonthSpans(days) { if (!Array.isArray(days) || days.length === 0) { return []; } const monthFormatter = new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" }); const spans = []; let currentStart = 1; let currentMonth = days[0].getMonth(); let currentYear = days[0].getFullYear(); for (let index = 1; index <= days.length; index += 1) { const day = days[index]; const monthChanged = !day || day.getMonth() !== currentMonth || day.getFullYear() !== currentYear; if (!monthChanged) { continue; } const spanEnd = index; spans.push({ start: currentStart, end: spanEnd, label: monthFormatter.format(new Date(currentYear, currentMonth, 1)) }); if (day) { currentStart = index + 1; currentMonth = day.getMonth(); currentYear = day.getFullYear(); } } return spans; } function syncMonthStripGeometry() { if (!monthStripEl) { return; } const calendarEl = document.getElementById("calendar"); if (!calendarEl) { return; } const dayNameItems = calendarEl.querySelectorAll( ".toastui-calendar-week-view-day-names .toastui-calendar-day-name-item.toastui-calendar-week" ); if (dayNameItems.length < 7) { monthStripEl.style.paddingLeft = "0"; monthStripEl.style.paddingRight = "0"; return; } const calendarRect = calendarEl.getBoundingClientRect(); const firstRect = dayNameItems[0].getBoundingClientRect(); const lastRect = dayNameItems[6].getBoundingClientRect(); const leftPad = Math.max(0, firstRect.left - calendarRect.left); const rightPad = Math.max(0, calendarRect.right - lastRect.right); monthStripEl.style.paddingLeft = `${leftPad}px`; monthStripEl.style.paddingRight = `${rightPad}px`; } function updateMonthStrip() { if (!monthStripEl) { return; } const days = getVisibleWeekDates(); const spans = buildMonthSpans(days); monthStripEl.replaceChildren(); if (!spans.length) { return; } const trackEl = document.createElement("div"); trackEl.className = "month-strip-track"; spans.forEach((span) => { const segmentEl = document.createElement("div"); segmentEl.className = "month-strip-segment"; segmentEl.style.gridColumn = `${span.start} / ${span.end + 1}`; segmentEl.textContent = span.label; trackEl.appendChild(segmentEl); }); monthStripEl.appendChild(trackEl); syncMonthStripGeometry(); } function createCalendarTemplates() { const weekdayRulerLookup = buildWeekdayRulerLookup(referenceData?.planets); // TIME / SIGN / NAME formatter for week time plates. // This intentionally keeps each event compact and visually consistent. const getPlateFields = (event) => { const fromRawSign = event?.raw?.planetSymbol; const fromRawName = event?.raw?.planetName; if (fromRawSign || fromRawName) { return { sign: fromRawSign || "", name: fromRawName || "" }; } // Fallback parser for any time event that does not provide `raw` planet metadata. // Example title pattern: "♂ Mars · The Tower" const title = String(event?.title || "").trim(); const beforeTarot = title.split("·")[0].trim(); const parts = beforeTarot.split(/\s+/).filter(Boolean); if (parts.length >= 2) { return { sign: parts[0], name: parts.slice(1).join(" ") }; } return { sign: "", name: beforeTarot }; }; // Returns exactly three lines for the event block text: // 1) TIME 2) SIGN 3) NAME const formatEventPlateText = (event) => { const timeLabel = formatCalendarTime(event.start); const { sign, name } = getPlateFields(event); const safeName = name || String(event?.title || "").trim(); const safeSign = sign || "•"; return `${timeLabel}\n${safeSign}\n${safeName}`; }; const renderWeekDayHeader = (weekDayNameData) => { const dateNumber = String(weekDayNameData?.date ?? "").padStart(2, "0"); const dayLabel = String(weekDayNameData?.dayName || ""); const ruler = weekdayRulerLookup[weekDayNameData?.day] || { symbol: "•", name: "" }; return [ '
', `${dateNumber}`, `${dayLabel}`, `${ruler.symbol}`, "
" ].join(""); }; return { timegridDisplayPrimaryTime: (props) => formatCalendarTimeFromTemplatePayload(props), timegridDisplayTime: (props) => formatCalendarTimeFromTemplatePayload(props), timegridNowIndicatorLabel: (props) => formatCalendarTimeFromTemplatePayload(props), weekDayName: (weekDayNameData) => renderWeekDayHeader(weekDayNameData), time: (event) => formatEventPlateText(event) }; } function applyTimeFormatTemplates() { calendar.setOptions({ template: createCalendarTemplates() }); calendar.render(); requestAnimationFrame(() => { forceAxisLabelFormat(); applySunRulerGradient(); applyDynamicNowIndicatorVisual(); updateMonthStrip(); requestAnimationFrame(() => { forceAxisLabelFormat(); applySunRulerGradient(); applyDynamicNowIndicatorVisual(); updateMonthStrip(); }); }); } function setStatus(text) { if (!statusEl) { return; } statusEl.textContent = text; } function normalizeGeoForSky(geo) { const latitude = Number(geo?.latitude); const longitude = Number(geo?.longitude); if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { return null; } return { latitude: Number(latitude.toFixed(4)), longitude: Number(longitude.toFixed(4)) }; } function buildStellariumObserverUrl(geo) { const normalizedGeo = normalizeGeoForSky(geo); if (!normalizedGeo) { return ""; } const stellariumUrl = new URL("https://stellarium-web.org/"); stellariumUrl.searchParams.set("lat", String(normalizedGeo.latitude)); stellariumUrl.searchParams.set("lng", String(normalizedGeo.longitude)); stellariumUrl.searchParams.set("elev", "0"); stellariumUrl.searchParams.set("date", new Date().toISOString()); stellariumUrl.searchParams.set("az", "0"); stellariumUrl.searchParams.set("alt", "90"); stellariumUrl.searchParams.set("fov", "180"); return stellariumUrl.toString(); } function syncNowSkyBackground(geo, force = false) { if (!nowSkyLayerEl || !geo) { return; } const normalizedGeo = normalizeGeoForSky(geo); if (!normalizedGeo) { return; } const geoKey = `${normalizedGeo.latitude.toFixed(4)},${normalizedGeo.longitude.toFixed(4)}`; const stellariumUrl = buildStellariumObserverUrl(normalizedGeo); if (!stellariumUrl) { return; } if (!force && geoKey === lastNowSkyGeoKey && stellariumUrl === lastNowSkySourceUrl) { return; } if (stellariumUrl === lastNowSkySourceUrl) { return; } nowSkyLayerEl.src = stellariumUrl; lastNowSkyGeoKey = geoKey; lastNowSkySourceUrl = stellariumUrl; } function syncNowPanelTheme(referenceDate = new Date()) { if (!nowPanelEl) { return; } if (!currentGeo || !window.SunCalc) { nowPanelEl.classList.remove("is-day"); nowPanelEl.classList.add("is-night"); return; } const sunPosition = window.SunCalc.getPosition(referenceDate, currentGeo.latitude, currentGeo.longitude); const sunAltitudeDeg = (sunPosition.altitude * 180) / Math.PI; const isDaytime = sunAltitudeDeg >= -4; nowPanelEl.classList.toggle("is-day", isDaytime); nowPanelEl.classList.toggle("is-night", !isDaytime); } function openSettingsPopup() { if (!settingsPopupEl) { return; } settingsPopupEl.hidden = false; if (openSettingsEl) { openSettingsEl.setAttribute("aria-expanded", "true"); } } function closeSettingsPopup() { if (!settingsPopupEl) { return; } settingsPopupEl.hidden = true; if (openSettingsEl) { openSettingsEl.setAttribute("aria-expanded", "false"); } } function loadSidebarCollapsedState(storageKey) { try { const raw = window.localStorage?.getItem(storageKey); if (raw === "1") { return true; } if (raw === "0") { return false; } return null; } catch { return null; } } function saveSidebarCollapsedState(storageKey, collapsed) { try { window.localStorage?.setItem(storageKey, collapsed ? "1" : "0"); } catch { // Ignore storage failures silently. } } function initializeSidebarPopouts() { const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout"); layouts.forEach((layout, index) => { if (!(layout instanceof HTMLElement)) { return; } const panel = Array.from(layout.children).find((child) => ( child instanceof HTMLElement && child.matches("aside.planet-list-panel, aside.tarot-list-panel, aside.kab-tree-panel") )); if (!(panel instanceof HTMLElement) || panel.dataset.sidebarPopoutReady === "1") { return; } const header = panel.querySelector(".planet-list-header, .tarot-list-header"); if (!(header instanceof HTMLElement)) { return; } panel.dataset.sidebarPopoutReady = "1"; const sectionId = layout.closest("section")?.id || `layout-${index + 1}`; const panelId = panel.id || `${sectionId}-entry-panel`; panel.id = panelId; const storageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`; const collapseBtn = document.createElement("button"); collapseBtn.type = "button"; collapseBtn.className = "sidebar-toggle-inline"; collapseBtn.textContent = "Hide Panel"; collapseBtn.setAttribute("aria-label", "Hide entry panel"); collapseBtn.setAttribute("aria-controls", panelId); header.appendChild(collapseBtn); const openBtn = document.createElement("button"); openBtn.type = "button"; openBtn.className = "sidebar-popout-open"; openBtn.textContent = "Show Panel"; openBtn.setAttribute("aria-label", "Show entry panel"); openBtn.setAttribute("aria-controls", panelId); openBtn.hidden = true; layout.appendChild(openBtn); const applyCollapsedState = (collapsed, persist = true) => { layout.classList.toggle("layout-sidebar-collapsed", collapsed); collapseBtn.setAttribute("aria-expanded", collapsed ? "false" : "true"); openBtn.setAttribute("aria-expanded", collapsed ? "false" : "true"); openBtn.hidden = !collapsed; if (persist) { saveSidebarCollapsedState(storageKey, collapsed); } }; collapseBtn.addEventListener("click", () => { applyCollapsedState(true); }); openBtn.addEventListener("click", () => { applyCollapsedState(false); }); const storedCollapsed = loadSidebarCollapsedState(storageKey); applyCollapsedState(storedCollapsed == null ? DEFAULT_DATASET_ENTRY_COLLAPSED : storedCollapsed, false); }); } function initializeDetailPopouts() { const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout"); layouts.forEach((layout, index) => { if (!(layout instanceof HTMLElement)) { return; } const detailPanel = Array.from(layout.children).find((child) => ( child instanceof HTMLElement && child.matches("section.planet-detail-panel, section.tarot-detail-panel, section.kab-detail-panel") )); if (!(detailPanel instanceof HTMLElement) || detailPanel.dataset.detailPopoutReady === "1") { return; } const heading = detailPanel.querySelector(".planet-detail-heading, .tarot-detail-heading"); if (!(heading instanceof HTMLElement)) { return; } detailPanel.dataset.detailPopoutReady = "1"; const sectionId = layout.closest("section")?.id || `layout-${index + 1}`; const panelId = detailPanel.id || `${sectionId}-detail-panel`; detailPanel.id = panelId; const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`; const sidebarStorageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`; const collapseBtn = document.createElement("button"); collapseBtn.type = "button"; collapseBtn.className = "detail-toggle-inline"; collapseBtn.textContent = "Hide Detail"; collapseBtn.setAttribute("aria-label", "Hide detail panel"); collapseBtn.setAttribute("aria-controls", panelId); heading.appendChild(collapseBtn); const openBtn = document.createElement("button"); openBtn.type = "button"; openBtn.className = "detail-popout-open"; openBtn.textContent = "Show Detail"; openBtn.setAttribute("aria-label", "Show detail panel"); openBtn.setAttribute("aria-controls", panelId); openBtn.hidden = true; layout.appendChild(openBtn); const applyCollapsedState = (collapsed, persist = true) => { if (collapsed && layout.classList.contains("layout-sidebar-collapsed")) { layout.classList.remove("layout-sidebar-collapsed"); const sidebarOpenBtn = layout.querySelector(".sidebar-popout-open"); if (sidebarOpenBtn instanceof HTMLButtonElement) { sidebarOpenBtn.hidden = true; sidebarOpenBtn.setAttribute("aria-expanded", "true"); } const sidebarCollapseBtn = layout.querySelector(".sidebar-toggle-inline"); if (sidebarCollapseBtn instanceof HTMLButtonElement) { sidebarCollapseBtn.setAttribute("aria-expanded", "true"); } saveSidebarCollapsedState(sidebarStorageKey, false); } layout.classList.toggle("layout-detail-collapsed", collapsed); collapseBtn.setAttribute("aria-expanded", collapsed ? "false" : "true"); openBtn.setAttribute("aria-expanded", collapsed ? "false" : "true"); openBtn.hidden = !collapsed; if (persist) { saveSidebarCollapsedState(detailStorageKey, collapsed); } }; collapseBtn.addEventListener("click", () => { applyCollapsedState(true); }); openBtn.addEventListener("click", () => { applyCollapsedState(false); }); const storedCollapsed = loadSidebarCollapsedState(detailStorageKey); const shouldForceOpenForTarot = sectionId === "tarot-section"; const initialCollapsed = shouldForceOpenForTarot ? false : (storedCollapsed == null ? DEFAULT_DATASET_DETAIL_COLLAPSED : storedCollapsed); applyCollapsedState(initialCollapsed, false); }); } function setActiveSection(nextSection) { const normalized = nextSection === "home" || nextSection === "calendar" || nextSection === "holidays" || nextSection === "tarot" || nextSection === "astronomy" || nextSection === "planets" || nextSection === "cycles" || nextSection === "natal" || nextSection === "elements" || nextSection === "iching" || nextSection === "kabbalah" || nextSection === "kabbalah-tree" || nextSection === "cube" || nextSection === "alphabet" || nextSection === "numbers" || nextSection === "zodiac" || nextSection === "quiz" || nextSection === "gods" || nextSection === "enochian" ? nextSection : "home"; activeSection = normalized; const isHomeOpen = activeSection === "home"; const isCalendarOpen = activeSection === "calendar"; const isHolidaysOpen = activeSection === "holidays"; const isCalendarMenuOpen = isCalendarOpen || isHolidaysOpen; const isTarotOpen = activeSection === "tarot"; const isAstronomyOpen = activeSection === "astronomy"; const isPlanetOpen = activeSection === "planets"; const isCyclesOpen = activeSection === "cycles"; const isNatalOpen = activeSection === "natal"; const isZodiacOpen = activeSection === "zodiac"; const isAstronomyMenuOpen = isAstronomyOpen || isPlanetOpen || isCyclesOpen || isZodiacOpen || isNatalOpen; const isElementsOpen = activeSection === "elements"; const isIChingOpen = activeSection === "iching"; const isKabbalahOpen = activeSection === "kabbalah"; const isKabbalahTreeOpen = activeSection === "kabbalah-tree"; const isCubeOpen = activeSection === "cube"; const isKabbalahMenuOpen = isKabbalahOpen || isKabbalahTreeOpen || isCubeOpen; const isAlphabetOpen = activeSection === "alphabet"; const isNumbersOpen = activeSection === "numbers"; const isQuizOpen = activeSection === "quiz"; const isGodsOpen = activeSection === "gods"; const isEnochianOpen = activeSection === "enochian"; if (calendarSectionEl) { calendarSectionEl.hidden = !isCalendarOpen; } if (holidaySectionEl) { holidaySectionEl.hidden = !isHolidaysOpen; } if (tarotSectionEl) { tarotSectionEl.hidden = !isTarotOpen; } if (astronomySectionEl) { astronomySectionEl.hidden = !isAstronomyOpen; } if (planetSectionEl) { planetSectionEl.hidden = !isPlanetOpen; } if (cyclesSectionEl) { cyclesSectionEl.hidden = !isCyclesOpen; } if (natalSectionEl) { natalSectionEl.hidden = !isNatalOpen; } if (elementsSectionEl) { elementsSectionEl.hidden = !isElementsOpen; } if (ichingSectionEl) { ichingSectionEl.hidden = !isIChingOpen; } if (kabbalahSectionEl) { kabbalahSectionEl.hidden = !isKabbalahOpen; } if (kabbalahTreeSectionEl) { kabbalahTreeSectionEl.hidden = !isKabbalahTreeOpen; } if (cubeSectionEl) { cubeSectionEl.hidden = !isCubeOpen; } if (alphabetSectionEl) { alphabetSectionEl.hidden = !isAlphabetOpen; } if (numbersSectionEl) { numbersSectionEl.hidden = !isNumbersOpen; } if (zodiacSectionEl) { zodiacSectionEl.hidden = !isZodiacOpen; } if (quizSectionEl) { quizSectionEl.hidden = !isQuizOpen; } if (godsSectionEl) { godsSectionEl.hidden = !isGodsOpen; } if (enochianSectionEl) { enochianSectionEl.hidden = !isEnochianOpen; } if (nowPanelEl) { nowPanelEl.hidden = !isHomeOpen; } if (monthStripEl) { monthStripEl.hidden = !isHomeOpen; } if (calendarEl) { calendarEl.hidden = !isHomeOpen; } if (openCalendarEl) { openCalendarEl.setAttribute("aria-pressed", isCalendarMenuOpen ? "true" : "false"); } if (openCalendarMonthsEl) { openCalendarMonthsEl.classList.toggle("is-active", isCalendarOpen); } if (openHolidaysEl) { openHolidaysEl.classList.toggle("is-active", isHolidaysOpen); } if (openTarotEl) { openTarotEl.setAttribute("aria-pressed", isTarotOpen ? "true" : "false"); } applyTarotSpreadViewState(); if (openAstronomyEl) { openAstronomyEl.setAttribute("aria-pressed", isAstronomyMenuOpen ? "true" : "false"); } if (openPlanetsEl) { openPlanetsEl.classList.toggle("is-active", isPlanetOpen); } if (openCyclesEl) { openCyclesEl.classList.toggle("is-active", isCyclesOpen); } if (openElementsEl) { openElementsEl.setAttribute("aria-pressed", isElementsOpen ? "true" : "false"); } if (openIChingEl) { openIChingEl.setAttribute("aria-pressed", isIChingOpen ? "true" : "false"); } if (openKabbalahEl) { openKabbalahEl.setAttribute("aria-pressed", isKabbalahMenuOpen ? "true" : "false"); } if (openKabbalahTreeEl) { openKabbalahTreeEl.classList.toggle("is-active", isKabbalahTreeOpen); } if (openKabbalahCubeEl) { openKabbalahCubeEl.classList.toggle("is-active", isCubeOpen); } if (openAlphabetEl) { openAlphabetEl.setAttribute("aria-pressed", isAlphabetOpen ? "true" : "false"); } if (openNumbersEl) { openNumbersEl.setAttribute("aria-pressed", isNumbersOpen ? "true" : "false"); } if (openZodiacEl) { openZodiacEl.classList.toggle("is-active", isZodiacOpen); } if (openNatalEl) { openNatalEl.classList.toggle("is-active", isNatalOpen); } if (openQuizEl) { openQuizEl.setAttribute("aria-pressed", isQuizOpen ? "true" : "false"); } if (openGodsEl) { openGodsEl.setAttribute("aria-pressed", isGodsOpen ? "true" : "false"); } if (openEnochianEl) { openEnochianEl.setAttribute("aria-pressed", isEnochianOpen ? "true" : "false"); } if (!isHomeOpen) { closeSettingsPopup(); } if (isCalendarOpen) { if (typeof ensureCalendarSection === "function" && referenceData) { ensureCalendarSection(referenceData, magickDataset); } return; } if (isHolidaysOpen) { if (typeof ensureHolidaySection === "function" && referenceData) { ensureHolidaySection(referenceData, magickDataset); } return; } if (isTarotOpen) { if (typeof ensureTarotSection === "function" && referenceData) { ensureTarotSection(referenceData, magickDataset); } if (activeTarotSpread !== null) { renderTarotSpread(); } return; } if (isPlanetOpen) { if (typeof ensurePlanetSection === "function" && referenceData) { ensurePlanetSection(referenceData, magickDataset); } return; } if (isCyclesOpen) { if (typeof ensureCyclesSection === "function" && referenceData) { ensureCyclesSection(referenceData); } return; } if (isElementsOpen) { if (typeof ensureElementsSection === "function" && magickDataset) { ensureElementsSection(magickDataset); } return; } if (isIChingOpen) { if (typeof ensureIChingSection === "function" && referenceData) { ensureIChingSection(referenceData); } return; } if (isKabbalahTreeOpen) { if (typeof ensureKabbalahSection === "function" && magickDataset) { ensureKabbalahSection(magickDataset); } return; } if (isCubeOpen) { if (typeof ensureCubeSection === "function" && magickDataset) { ensureCubeSection(magickDataset, referenceData); } return; } if (isAlphabetOpen) { if (typeof ensureAlphabetSection === "function" && magickDataset) { ensureAlphabetSection(magickDataset, referenceData); } return; } if (isNumbersOpen) { ensureNumbersSection(); return; } if (isZodiacOpen) { if (typeof ensureZodiacSection === "function" && referenceData && magickDataset) { ensureZodiacSection(referenceData, magickDataset); } return; } if (isNatalOpen) { if (typeof ensureNatalPanel === "function") { ensureNatalPanel(referenceData); } return; } if (isQuizOpen) { if (typeof ensureQuizSection === "function" && referenceData && magickDataset) { ensureQuizSection(referenceData, magickDataset); } return; } if (isGodsOpen) { if (typeof ensureGodsSection === "function" && magickDataset) { ensureGodsSection(magickDataset, referenceData); } return; } if (isEnochianOpen) { if (typeof ensureEnochianSection === "function" && magickDataset) { ensureEnochianSection(magickDataset, referenceData); } return; } requestAnimationFrame(() => { calendar.render(); updateMonthStrip(); syncNowPanelTheme(new Date()); }); } function applyCenteredWeekWindow(date) { const startDayOfWeek = getCenteredWeekStartDay(date); calendar.setOptions({ week: { ...baseWeekOptions, startDayOfWeek } }); applyTimeFormatTemplates(); calendar.changeView("week"); calendar.setDate(date); } function parseGeoInput() { const latitude = Number(latEl.value); const longitude = Number(lngEl.value); if (Number.isNaN(latitude) || Number.isNaN(longitude)) { throw new Error("Latitude/Longitude must be valid numbers."); } return { latitude, longitude }; } function normalizeTimeFormat(value) { if (value === "hours") { return "hours"; } if (value === "seconds") { return "seconds"; } return "minutes"; } function normalizeBirthDate(value) { const normalized = String(value || "").trim(); if (!normalized) { return ""; } return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : ""; } function getKnownTarotDeckIds() { const knownDeckIds = new Set(); const deckOptions = window.TarotCardImages?.getDeckOptions?.(); if (Array.isArray(deckOptions)) { deckOptions.forEach((option) => { const id = String(option?.id || "").trim().toLowerCase(); if (id) { knownDeckIds.add(id); } }); } if (!knownDeckIds.size) { knownDeckIds.add(DEFAULT_TAROT_DECK); } return knownDeckIds; } function getFallbackTarotDeckId() { const deckOptions = window.TarotCardImages?.getDeckOptions?.(); if (Array.isArray(deckOptions)) { for (let i = 0; i < deckOptions.length; i += 1) { const id = String(deckOptions[i]?.id || "").trim().toLowerCase(); if (id) { return id; } } } return DEFAULT_TAROT_DECK; } function normalizeTarotDeck(value) { const normalized = String(value || "").trim().toLowerCase(); const knownDeckIds = getKnownTarotDeckIds(); if (knownDeckIds.has(normalized)) { return normalized; } return getFallbackTarotDeckId(); } function parseStoredNumber(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function normalizeSettings(settings) { return { latitude: parseStoredNumber(settings?.latitude, DEFAULT_SETTINGS.latitude), longitude: parseStoredNumber(settings?.longitude, DEFAULT_SETTINGS.longitude), timeFormat: normalizeTimeFormat(settings?.timeFormat), birthDate: normalizeBirthDate(settings?.birthDate), tarotDeck: normalizeTarotDeck(settings?.tarotDeck) }; } function getResolvedTimeZone() { try { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; return String(timeZone || ""); } catch { return ""; } } function buildBirthDateParts(birthDate) { const normalized = normalizeBirthDate(birthDate); if (!normalized) { return null; } const [year, month, day] = normalized.split("-").map((value) => Number(value)); if (!year || !month || !day) { return null; } const localNoon = new Date(year, month - 1, day, 12, 0, 0, 0); const utcNoon = new Date(Date.UTC(year, month - 1, day, 12, 0, 0, 0)); return { year, month, day, isoDate: normalized, localNoonIso: localNoon.toISOString(), utcNoonIso: utcNoon.toISOString(), timezoneOffsetMinutesAtNoon: localNoon.getTimezoneOffset() }; } function buildNatalContext(settings) { const normalized = normalizeSettings(settings); const birthDateParts = buildBirthDateParts(normalized.birthDate); const timeZone = getResolvedTimeZone(); return { latitude: normalized.latitude, longitude: normalized.longitude, birthDate: normalized.birthDate || null, birthDateParts, timeZone: timeZone || "UTC", timezoneOffsetMinutesNow: new Date().getTimezoneOffset(), timezoneOffsetMinutesAtBirthDateNoon: birthDateParts?.timezoneOffsetMinutesAtNoon ?? null }; } function emitSettingsUpdated(settings) { const normalized = normalizeSettings(settings); const natalContext = buildNatalContext(normalized); document.dispatchEvent(new CustomEvent("settings:updated", { detail: { settings: normalized, natalContext } })); } function loadSavedSettings() { try { const raw = window.localStorage.getItem(SETTINGS_STORAGE_KEY); if (!raw) { return { ...DEFAULT_SETTINGS }; } const parsed = JSON.parse(raw); return normalizeSettings(parsed); } catch { return { ...DEFAULT_SETTINGS }; } } function saveSettings(settings) { try { const normalized = normalizeSettings(settings); window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized)); return true; } catch { return false; } } function syncTarotDeckInputOptions() { if (!tarotDeckEl) { return; } const deckOptions = window.TarotCardImages?.getDeckOptions?.(); const previousValue = String(tarotDeckEl.value || "").trim().toLowerCase(); tarotDeckEl.innerHTML = ""; if (!Array.isArray(deckOptions) || !deckOptions.length) { const emptyOption = document.createElement("option"); emptyOption.value = DEFAULT_TAROT_DECK; emptyOption.textContent = "No deck manifests found"; tarotDeckEl.appendChild(emptyOption); tarotDeckEl.disabled = true; return; } tarotDeckEl.disabled = false; deckOptions.forEach((option) => { const id = String(option?.id || "").trim().toLowerCase(); if (!id) { return; } const label = String(option?.label || id); const optionEl = document.createElement("option"); optionEl.value = id; optionEl.textContent = label; tarotDeckEl.appendChild(optionEl); }); const normalizedPrevious = normalizeTarotDeck(previousValue); tarotDeckEl.value = normalizedPrevious; } function applySettingsToInputs(settings) { syncTarotDeckInputOptions(); const normalized = normalizeSettings(settings); latEl.value = String(normalized.latitude); lngEl.value = String(normalized.longitude); timeFormatEl.value = normalized.timeFormat; birthDateEl.value = normalized.birthDate; if (tarotDeckEl) { tarotDeckEl.value = normalized.tarotDeck; } if (window.TarotCardImages?.setActiveDeck) { window.TarotCardImages.setActiveDeck(normalized.tarotDeck); } currentTimeFormat = normalized.timeFormat; currentSettings = normalized; } function getSettingsFromInputs() { const latitude = Number(latEl.value); const longitude = Number(lngEl.value); if (Number.isNaN(latitude) || Number.isNaN(longitude)) { throw new Error("Latitude/Longitude must be valid numbers."); } return normalizeSettings({ latitude, longitude, timeFormat: normalizeTimeFormat(timeFormatEl.value), birthDate: normalizeBirthDate(birthDateEl.value), tarotDeck: normalizeTarotDeck(tarotDeckEl?.value) }); } function handleSaveSettings() { try { const settings = getSettingsFromInputs(); applySettingsToInputs(settings); syncNowSkyBackground({ latitude: settings.latitude, longitude: settings.longitude }, true); const didPersist = saveSettings(settings); emitSettingsUpdated(currentSettings); if (activeSection !== "home") { setActiveSection(activeSection); } closeSettingsPopup(); void renderWeek(); if (!didPersist) { setStatus("Settings applied for this session. Browser storage is unavailable."); } } catch (error) { setStatus(error.message || "Unable to save settings."); } } function startNowTicker() { if (nowInterval) { clearInterval(nowInterval); } const tick = () => { if (!referenceData || !currentGeo || renderInProgress) { return; } const now = new Date(); syncNowPanelTheme(now); const currentDayKey = getDateKey(now); if (currentDayKey !== centeredDayKey) { centeredDayKey = currentDayKey; void renderWeek(); return; } updateNowPanel(referenceData, currentGeo, nowElements, currentTimeFormat); applyDynamicNowIndicatorVisual(now); }; tick(); nowInterval = setInterval(tick, 1000); } async function renderWeek() { if (renderInProgress) { return; } renderInProgress = true; try { currentGeo = parseGeoInput(); syncNowPanelTheme(new Date()); syncNowSkyBackground(currentGeo); if (!referenceData || !magickDataset) { setStatus("Loading planetary, sign and decan tarot correspondences..."); const [loadedReference, loadedMagick] = await Promise.all([ referenceData ? Promise.resolve(referenceData) : loadReferenceData(), magickDataset ? Promise.resolve(magickDataset) : loadMagickDataset().catch(() => null) ]); referenceData = loadedReference; magickDataset = loadedMagick; } if (typeof ensureTarotSection === "function") { ensureTarotSection(referenceData, magickDataset); } if (typeof ensurePlanetSection === "function") { ensurePlanetSection(referenceData, magickDataset); } if (typeof ensureCyclesSection === "function") { ensureCyclesSection(referenceData); } if (typeof ensureIChingSection === "function") { ensureIChingSection(referenceData); } if (typeof ensureCalendarSection === "function") { ensureCalendarSection(referenceData, magickDataset); } if (typeof ensureHolidaySection === "function") { ensureHolidaySection(referenceData, magickDataset); } if (typeof ensureNatalPanel === "function") { ensureNatalPanel(referenceData); } if (typeof ensureQuizSection === "function") { ensureQuizSection(referenceData, magickDataset); } const anchorDate = new Date(); centeredDayKey = getDateKey(anchorDate); applyCenteredWeekWindow(anchorDate); const events = buildWeekEvents(currentGeo, referenceData, anchorDate); calendar.clear(); calendar.createEvents(events); applySunRulerGradient(anchorDate); updateMonthStrip(); requestAnimationFrame(updateMonthStrip); setStatus(`Rendered ${events.length} planetary + tarot events for lat ${currentGeo.latitude}, lng ${currentGeo.longitude}.`); startNowTicker(); } catch (error) { setStatus(error.message || "Failed to render calendar."); } finally { renderInProgress = false; } } function requestGeoLocation() { if (!navigator.geolocation) { setStatus("Geolocation not available in this browser."); return; } setStatus("Getting your location..."); navigator.geolocation.getCurrentPosition( ({ coords }) => { latEl.value = coords.latitude.toFixed(4); lngEl.value = coords.longitude.toFixed(4); syncNowSkyBackground({ latitude: coords.latitude, longitude: coords.longitude }, true); setStatus("Location set from browser. Click Save Settings to refresh."); }, (err) => { const detail = err?.message || `code ${err?.code ?? "unknown"}`; setStatus(`Could not get location (${detail}).`); }, { enableHighAccuracy: true, timeout: 10000 } ); } function setTopbarDropdownOpen(dropdownEl, isOpen) { if (!(dropdownEl instanceof HTMLElement)) { return; } dropdownEl.classList.toggle("is-open", Boolean(isOpen)); const trigger = dropdownEl.querySelector("button[aria-haspopup='menu']"); if (trigger) { trigger.setAttribute("aria-expanded", isOpen ? "true" : "false"); } } function closeTopbarDropdowns(exceptEl = null) { topbarDropdownEls.forEach((dropdownEl) => { if (exceptEl && dropdownEl === exceptEl) { return; } setTopbarDropdownOpen(dropdownEl, false); }); } function bindTopbarDropdownInteractions() { if (!topbarDropdownEls.length) { return; } topbarDropdownEls.forEach((dropdownEl) => { const trigger = dropdownEl.querySelector("button[aria-haspopup='menu']"); if (!(trigger instanceof HTMLElement)) { return; } setTopbarDropdownOpen(dropdownEl, false); dropdownEl.addEventListener("mouseenter", () => { setTopbarDropdownOpen(dropdownEl, true); }); dropdownEl.addEventListener("mouseleave", () => { setTopbarDropdownOpen(dropdownEl, false); }); dropdownEl.addEventListener("focusout", (event) => { const nextTarget = event.relatedTarget; if (!(nextTarget instanceof Node) || !dropdownEl.contains(nextTarget)) { setTopbarDropdownOpen(dropdownEl, false); } }); trigger.addEventListener("click", (event) => { event.stopPropagation(); const nextOpen = !dropdownEl.classList.contains("is-open"); closeTopbarDropdowns(dropdownEl); setTopbarDropdownOpen(dropdownEl, nextOpen); }); const menuItems = dropdownEl.querySelectorAll(".topbar-dropdown-menu [role='menuitem']"); menuItems.forEach((menuItem) => { menuItem.addEventListener("click", () => { closeTopbarDropdowns(); }); }); }); } if (saveSettingsEl) { saveSettingsEl.addEventListener("click", handleSaveSettings); } useLocationEl.addEventListener("click", requestGeoLocation); if (openSettingsEl) { openSettingsEl.addEventListener("click", (event) => { event.stopPropagation(); if (settingsPopupEl?.hidden) { openSettingsPopup(); } else { closeSettingsPopup(); } }); } if (openTarotEl) { openTarotEl.addEventListener("click", () => { if (activeSection === "tarot") { setActiveSection("home"); } else { setActiveSection("tarot"); showTarotCardsView(); } }); } if (openTarotCardsEl) { openTarotCardsEl.addEventListener("click", () => { setActiveSection("tarot"); showTarotCardsView(); }); } if (openTarotSpreadEl) { openTarotSpreadEl.addEventListener("click", () => { setTarotSpread("three-card", true); }); } if (tarotSpreadBackEl) { tarotSpreadBackEl.addEventListener("click", () => { showTarotCardsView(); }); } if (tarotSpreadBtnThreeEl) { tarotSpreadBtnThreeEl.addEventListener("click", () => { showTarotSpreadView("three-card"); }); } if (tarotSpreadBtnCelticEl) { tarotSpreadBtnCelticEl.addEventListener("click", () => { showTarotSpreadView("celtic-cross"); }); } if (tarotSpreadRedrawEl) { tarotSpreadRedrawEl.addEventListener("click", () => { regenerateTarotSpreadDraw(); renderTarotSpread(); }); } if (openAstronomyEl) { openAstronomyEl.addEventListener("click", () => { setActiveSection(activeSection === "astronomy" ? "home" : "astronomy"); }); } if (openPlanetsEl) { openPlanetsEl.addEventListener("click", () => { setActiveSection(activeSection === "planets" ? "home" : "planets"); }); } if (openCyclesEl) { openCyclesEl.addEventListener("click", () => { setActiveSection(activeSection === "cycles" ? "home" : "cycles"); }); } if (openElementsEl) { openElementsEl.addEventListener("click", () => { setActiveSection(activeSection === "elements" ? "home" : "elements"); }); } if (openIChingEl) { openIChingEl.addEventListener("click", () => { setActiveSection(activeSection === "iching" ? "home" : "iching"); }); } if (openKabbalahEl) { openKabbalahEl.addEventListener("click", () => { setActiveSection(activeSection === "kabbalah" ? "home" : "kabbalah"); }); } if (openKabbalahTreeEl) { openKabbalahTreeEl.addEventListener("click", () => { setActiveSection(activeSection === "kabbalah-tree" ? "home" : "kabbalah-tree"); }); } if (openKabbalahCubeEl) { openKabbalahCubeEl.addEventListener("click", () => { setActiveSection(activeSection === "cube" ? "home" : "cube"); }); } if (openAlphabetEl) { openAlphabetEl.addEventListener("click", () => { setActiveSection(activeSection === "alphabet" ? "home" : "alphabet"); }); } if (openNumbersEl) { openNumbersEl.addEventListener("click", () => { setActiveSection(activeSection === "numbers" ? "home" : "numbers"); }); } if (openZodiacEl) { openZodiacEl.addEventListener("click", () => { setActiveSection(activeSection === "zodiac" ? "home" : "zodiac"); }); } if (openNatalEl) { openNatalEl.addEventListener("click", () => { setActiveSection(activeSection === "natal" ? "home" : "natal"); }); } if (openQuizEl) { openQuizEl.addEventListener("click", () => { setActiveSection(activeSection === "quiz" ? "home" : "quiz"); }); } if (openGodsEl) { openGodsEl.addEventListener("click", () => { setActiveSection(activeSection === "gods" ? "home" : "gods"); }); } if (openEnochianEl) { openEnochianEl.addEventListener("click", () => { setActiveSection(activeSection === "enochian" ? "home" : "enochian"); }); } if (openCalendarEl) { openCalendarEl.addEventListener("click", () => { const isCalendarMenuActive = activeSection === "calendar" || activeSection === "holidays"; setActiveSection(isCalendarMenuActive ? "home" : "calendar"); }); } if (openCalendarMonthsEl) { openCalendarMonthsEl.addEventListener("click", () => { setActiveSection(activeSection === "calendar" ? "home" : "calendar"); }); } if (openHolidaysEl) { openHolidaysEl.addEventListener("click", () => { setActiveSection(activeSection === "holidays" ? "home" : "holidays"); }); } bindTopbarDropdownInteractions(); document.addEventListener("nav:cube", (e) => { if (typeof ensureCubeSection === "function" && magickDataset) { ensureCubeSection(magickDataset, referenceData); } setActiveSection("cube"); const detail = e?.detail || {}; requestAnimationFrame(() => { const ui = window.CubeSectionUi; const selected = ui?.selectPlacement?.(detail); if (!selected && detail?.wallId) { ui?.selectWallById?.(detail.wallId); } }); }); document.addEventListener("nav:zodiac", (e) => { if (typeof ensureZodiacSection === "function" && referenceData && magickDataset) { ensureZodiacSection(referenceData, magickDataset); } setActiveSection("zodiac"); const signId = e?.detail?.signId; if (signId) { requestAnimationFrame(() => { window.ZodiacSectionUi?.selectBySignId?.(signId); }); } }); document.addEventListener("nav:alphabet", (e) => { if (typeof ensureAlphabetSection === "function" && magickDataset) { ensureAlphabetSection(magickDataset, referenceData); } setActiveSection("alphabet"); const alphabet = e?.detail?.alphabet; const hebrewLetterId = e?.detail?.hebrewLetterId; const greekName = e?.detail?.greekName; const englishLetter = e?.detail?.englishLetter; const arabicName = e?.detail?.arabicName; const enochianId = e?.detail?.enochianId; requestAnimationFrame(() => { const ui = window.AlphabetSectionUi; if ((alphabet === "hebrew" || (!alphabet && hebrewLetterId)) && hebrewLetterId) { ui?.selectLetterByHebrewId?.(hebrewLetterId); return; } if (alphabet === "greek" && greekName) { ui?.selectGreekLetterByName?.(greekName); return; } if (alphabet === "english" && englishLetter) { ui?.selectEnglishLetter?.(englishLetter); return; } if (alphabet === "arabic" && arabicName) { ui?.selectArabicLetter?.(arabicName); return; } if (alphabet === "enochian" && enochianId) { ui?.selectEnochianLetter?.(enochianId); } }); }); document.addEventListener("nav:number", (e) => { const rawValue = e?.detail?.value; const normalizedValue = normalizeNumberValue(rawValue); if (normalizedValue === null) { return; } setActiveSection("numbers"); requestAnimationFrame(() => { selectNumberEntry(normalizedValue); }); }); document.addEventListener("nav:iching", (e) => { if (typeof ensureIChingSection === "function" && referenceData) { ensureIChingSection(referenceData); } setActiveSection("iching"); const hexagramNumber = e?.detail?.hexagramNumber; const planetaryInfluence = e?.detail?.planetaryInfluence; requestAnimationFrame(() => { const ui = window.IChingSectionUi; if (hexagramNumber != null) { ui?.selectByHexagramNumber?.(hexagramNumber); return; } if (planetaryInfluence) { ui?.selectByPlanetaryInfluence?.(planetaryInfluence); } }); }); document.addEventListener("nav:gods", (e) => { if (typeof ensureGodsSection === "function" && magickDataset) { ensureGodsSection(magickDataset, referenceData); } setActiveSection("gods"); const godId = e?.detail?.godId; const godName = e?.detail?.godName; const pathNo = e?.detail?.pathNo; requestAnimationFrame(() => { const ui = window.GodsSectionUi; const viaId = godId ? ui?.selectById?.(godId) : false; const viaName = !viaId && godName ? ui?.selectByName?.(godName) : false; if (!viaId && !viaName && pathNo != null) { ui?.selectByPathNo?.(pathNo); } }); }); document.addEventListener("nav:calendar-month", (e) => { const calendarId = e?.detail?.calendarId; const monthId = e?.detail?.monthId; if (!monthId) return; if (typeof ensureCalendarSection === "function" && referenceData) { ensureCalendarSection(referenceData, magickDataset); } setActiveSection("calendar"); requestAnimationFrame(() => { if (calendarId) { window.CalendarSectionUi?.selectCalendarType?.(calendarId); } window.CalendarSectionUi?.selectByMonthId?.(monthId); }); }); document.addEventListener("nav:kabbalah-path", (e) => { const pathNo = e?.detail?.pathNo; const { ensureKabbalahSection } = window.KabbalahSectionUi || {}; if (typeof ensureKabbalahSection === "function" && magickDataset) { ensureKabbalahSection(magickDataset); } setActiveSection("kabbalah-tree"); if (pathNo != null) { requestAnimationFrame(() => { window.KabbalahSectionUi?.selectNode?.(pathNo); }); } }); document.addEventListener("nav:planet", (e) => { const planetId = e?.detail?.planetId; if (!planetId) return; if (typeof ensurePlanetSection === "function" && referenceData) { ensurePlanetSection(referenceData, magickDataset); } setActiveSection("planets"); requestAnimationFrame(() => { window.PlanetSectionUi?.selectByPlanetId?.(planetId); }); }); document.addEventListener("nav:elements", (e) => { const elementId = e?.detail?.elementId; if (!elementId) { return; } if (typeof ensureElementsSection === "function" && magickDataset) { ensureElementsSection(magickDataset); } setActiveSection("elements"); requestAnimationFrame(() => { window.ElementsSectionUi?.selectByElementId?.(elementId); }); }); document.addEventListener("nav:tarot-trump", (e) => { if (typeof ensureTarotSection === "function" && referenceData) { ensureTarotSection(referenceData, magickDataset); } setActiveSection("tarot"); const { trumpNumber, cardName } = e?.detail || {}; requestAnimationFrame(() => { if (trumpNumber != null) { window.TarotSectionUi?.selectCardByTrump?.(trumpNumber); } else if (cardName) { window.TarotSectionUi?.selectCardByName?.(cardName); } }); }); document.addEventListener("kab:view-trump", (e) => { setActiveSection("tarot"); const trumpNumber = e?.detail?.trumpNumber; if (trumpNumber != null) { if (typeof ensureTarotSection === "function" && referenceData) { ensureTarotSection(referenceData, magickDataset); } requestAnimationFrame(() => { window.TarotSectionUi?.selectCardByTrump?.(trumpNumber); }); } }); document.addEventListener("tarot:view-kab-path", (e) => { setActiveSection("kabbalah-tree"); const pathNumber = e?.detail?.pathNumber; if (pathNumber != null) { requestAnimationFrame(() => { const kabbalahUi = window.KabbalahSectionUi; if (typeof kabbalahUi?.selectNode === "function") { kabbalahUi.selectNode(pathNumber); } else { kabbalahUi?.selectPathByNumber?.(pathNumber); } }); } }); if (closeSettingsEl) { closeSettingsEl.addEventListener("click", closeSettingsPopup); } document.addEventListener("click", (event) => { const clickTarget = event.target; if (clickTarget instanceof Node && topbarDropdownEls.some((dropdownEl) => dropdownEl.contains(clickTarget))) { return; } closeTopbarDropdowns(); if (!settingsPopupEl || settingsPopupEl.hidden) { return; } if (!(clickTarget instanceof Node)) { return; } if (settingsPopupCardEl?.contains(clickTarget) || openSettingsEl?.contains(clickTarget)) { return; } closeSettingsPopup(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeTopbarDropdowns(); closeSettingsPopup(); } }); window.addEventListener("resize", () => { if (monthStripResizeFrame) { cancelAnimationFrame(monthStripResizeFrame); } monthStripResizeFrame = requestAnimationFrame(() => { monthStripResizeFrame = null; updateMonthStrip(); }); }); window.TarotNatal = { ...(window.TarotNatal || {}), getSettings() { return { ...currentSettings }; }, getContext() { return buildNatalContext(currentSettings); }, buildContextFromSettings(settings) { return buildNatalContext(settings); } }; const initialSettings = loadSavedSettings(); applySettingsToInputs(initialSettings); emitSettingsUpdated(currentSettings); initializeSidebarPopouts(); initializeDetailPopouts(); syncNowSkyBackground({ latitude: initialSettings.latitude, longitude: initialSettings.longitude }, true); setActiveSection("home"); void renderWeek();