Files
TaroTime/app.js
2026-03-07 01:09:00 -08:00

3493 lines
103 KiB
JavaScript

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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
? `<div class=\"tarot-spread-meaning-keywords\">Keywords: ${escapeHtml(keywords.join(", "))}</div>`
: "";
const orientationMarkup = card.reversed
? " <span class=\"tarot-spread-meaning-orientation\">(Reversed)</span>"
: "";
return `<div class=\"tarot-spread-meaning-item\">`
+ `<div class=\"tarot-spread-meaning-head\">${positionLabel}: <span class=\"tarot-spread-meaning-card\">${cardName}</span>${orientationMarkup}</div>`
+ `<div class=\"tarot-spread-meaning-text\">${meaningText}</div>`
+ keywordMarkup
+ `</div>`;
}).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 = `<div class="spread-empty">Tarot deck not loaded yet — open Cards first, then return to Spread.</div>`;
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
? `<img class="spread-card-img" src="${imgSrc}" alt="${escapeHtml(card.name)}" loading="lazy">`
: `<div class="spread-card-placeholder">${escapeHtml(card.name)}</div>`;
const reversedTag = reversed ? `<span class="spread-reversed-tag">Reversed</span>` : "";
return `<div class="spread-position" data-pos="${position.pos}">`
+ `<div class="spread-pos-label">${escapeHtml(position.label)}</div>`
+ `<div class="${wrapClass}">${imgHtml}</div>`
+ `<div class="spread-card-name">${escapeHtml(card.name)}${reversedTag}</div>`
+ `</div>`;
}).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 = [
'<span class="toastui-calendar-template-timegridNowIndicatorLabel now-celestial-chip">',
`<span class="now-celestial-icon">${icon}</span>`,
"</span>"
].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 [
'<div class="weekday-header-template">',
`<span class="weekday-header-number">${dateNumber}</span>`,
`<span class="weekday-header-name">${dayLabel}</span>`,
`<span class="weekday-header-ruler" title="${ruler.name}">${ruler.symbol}</span>`,
"</div>"
].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();