(function () { const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {}; const state = { initialized: false, cards: [], filteredCards: [], searchQuery: "", selectedCardId: "", magickDataset: null, referenceData: null, monthRefsByCardId: new Map(), courtCardByDecanId: new Map() }; let tarotLightboxOverlayEl = null; let tarotLightboxImageEl = null; let tarotLightboxZoomed = false; const LIGHTBOX_ZOOM_SCALE = 6.66; function resetTarotLightboxZoom() { if (!tarotLightboxImageEl) { return; } tarotLightboxZoomed = false; tarotLightboxImageEl.style.transform = "scale(1)"; tarotLightboxImageEl.style.transformOrigin = "center center"; tarotLightboxImageEl.style.cursor = "zoom-in"; } function updateTarotLightboxZoomOrigin(clientX, clientY) { if (!tarotLightboxZoomed || !tarotLightboxImageEl) { return; } const rect = tarotLightboxImageEl.getBoundingClientRect(); if (!rect.width || !rect.height) { return; } const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100)); const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100)); tarotLightboxImageEl.style.transformOrigin = `${x}% ${y}%`; } function isTarotLightboxPointOnCard(clientX, clientY) { if (!tarotLightboxImageEl) { return false; } const rect = tarotLightboxImageEl.getBoundingClientRect(); const naturalWidth = tarotLightboxImageEl.naturalWidth; const naturalHeight = tarotLightboxImageEl.naturalHeight; if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) { return true; } const frameAspect = rect.width / rect.height; const imageAspect = naturalWidth / naturalHeight; let renderWidth = rect.width; let renderHeight = rect.height; if (imageAspect > frameAspect) { renderHeight = rect.width / imageAspect; } else { renderWidth = rect.height * imageAspect; } const left = rect.left + (rect.width - renderWidth) / 2; const top = rect.top + (rect.height - renderHeight) / 2; const right = left + renderWidth; const bottom = top + renderHeight; return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom; } function ensureTarotImageLightbox() { if (tarotLightboxOverlayEl && tarotLightboxImageEl) { return; } tarotLightboxOverlayEl = document.createElement("div"); tarotLightboxOverlayEl.setAttribute("aria-hidden", "true"); tarotLightboxOverlayEl.style.position = "fixed"; tarotLightboxOverlayEl.style.inset = "0"; tarotLightboxOverlayEl.style.background = "rgba(0, 0, 0, 0.82)"; tarotLightboxOverlayEl.style.display = "none"; tarotLightboxOverlayEl.style.alignItems = "center"; tarotLightboxOverlayEl.style.justifyContent = "center"; tarotLightboxOverlayEl.style.zIndex = "9999"; tarotLightboxOverlayEl.style.padding = "0"; const image = document.createElement("img"); image.alt = "Tarot card enlarged image"; image.style.maxWidth = "100vw"; image.style.maxHeight = "100vh"; image.style.width = "100vw"; image.style.height = "100vh"; image.style.objectFit = "contain"; image.style.borderRadius = "0"; image.style.boxShadow = "none"; image.style.border = "none"; image.style.cursor = "zoom-in"; image.style.transform = "scale(1)"; image.style.transformOrigin = "center center"; image.style.transition = "transform 120ms ease-out"; image.style.userSelect = "none"; tarotLightboxImageEl = image; tarotLightboxOverlayEl.appendChild(image); const closeLightbox = () => { if (!tarotLightboxOverlayEl || !tarotLightboxImageEl) { return; } tarotLightboxOverlayEl.style.display = "none"; tarotLightboxOverlayEl.setAttribute("aria-hidden", "true"); tarotLightboxImageEl.removeAttribute("src"); resetTarotLightboxZoom(); }; tarotLightboxOverlayEl.addEventListener("click", (event) => { if (event.target === tarotLightboxOverlayEl) { closeLightbox(); } }); tarotLightboxImageEl.addEventListener("click", (event) => { event.stopPropagation(); if (!isTarotLightboxPointOnCard(event.clientX, event.clientY)) { closeLightbox(); return; } if (!tarotLightboxZoomed) { tarotLightboxZoomed = true; tarotLightboxImageEl.style.transform = `scale(${LIGHTBOX_ZOOM_SCALE})`; tarotLightboxImageEl.style.cursor = "zoom-out"; updateTarotLightboxZoomOrigin(event.clientX, event.clientY); return; } resetTarotLightboxZoom(); }); tarotLightboxImageEl.addEventListener("mousemove", (event) => { updateTarotLightboxZoomOrigin(event.clientX, event.clientY); }); tarotLightboxImageEl.addEventListener("mouseleave", () => { if (tarotLightboxZoomed) { tarotLightboxImageEl.style.transformOrigin = "center center"; } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeLightbox(); } }); document.body.appendChild(tarotLightboxOverlayEl); } function openTarotImageLightbox(src, altText) { if (!src) { return; } ensureTarotImageLightbox(); if (!tarotLightboxOverlayEl || !tarotLightboxImageEl) { return; } tarotLightboxImageEl.src = src; tarotLightboxImageEl.alt = altText || "Tarot card enlarged image"; resetTarotLightboxZoom(); tarotLightboxOverlayEl.style.display = "flex"; tarotLightboxOverlayEl.setAttribute("aria-hidden", "false"); } const TAROT_TRUMP_NUMBER_BY_NAME = { "the fool": 0, fool: 0, "the magus": 1, magus: 1, magician: 1, "the high priestess": 2, "high priestess": 2, "the empress": 3, empress: 3, "the emperor": 4, emperor: 4, "the hierophant": 5, hierophant: 5, "the lovers": 6, lovers: 6, "the chariot": 7, chariot: 7, strength: 8, lust: 8, "the hermit": 9, hermit: 9, fortune: 10, "wheel of fortune": 10, justice: 11, "the hanged man": 12, "hanged man": 12, death: 13, temperance: 14, art: 14, "the devil": 15, devil: 15, "the tower": 16, tower: 16, "the star": 17, star: 17, "the moon": 18, moon: 18, "the sun": 19, sun: 19, aeon: 20, judgement: 20, judgment: 20, universe: 21, world: 21, "the world": 21 }; const MINOR_NUMBER_WORD_BY_VALUE = { 1: "ace", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six", 7: "seven", 8: "eight", 9: "nine", 10: "ten" }; const MINOR_TITLE_WORD_BY_VALUE = { 1: "Ace", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten" }; const HOUSE_MINOR_NUMBER_BANDS = [ [2, 3, 4], [5, 6, 7], [8, 9, 10], [2, 3, 4], [5, 6, 7], [8, 9, 10] ]; const HOUSE_LEFT_SUITS = ["Wands", "Disks", "Swords", "Cups", "Wands", "Disks"]; const HOUSE_RIGHT_SUITS = ["Swords", "Cups", "Wands", "Disks", "Swords", "Cups"]; const HOUSE_MIDDLE_SUITS = ["Wands", "Cups", "Swords", "Disks"]; const HOUSE_MIDDLE_RANKS = ["Ace", "Knight", "Queen", "Prince", "Princess"]; const HOUSE_TRUMP_ROWS = [ [0], [20, 21, 12], [19, 10, 2, 1, 3, 16], [18, 17, 15, 14, 13, 9, 8, 7, 6, 5, 4], [11] ]; const HEBREW_LETTER_ALIASES = { aleph: "alef", alef: "alef", heh: "he", he: "he", beth: "bet", bet: "bet", cheth: "het", chet: "het", kaph: "kaf", kaf: "kaf", peh: "pe", tzaddi: "tsadi", tzadi: "tsadi", tsadi: "tsadi", qoph: "qof", qof: "qof", taw: "tav", tau: "tav" }; const CUBE_MOTHER_CONNECTOR_BY_LETTER = { alef: { connectorId: "above-below", connectorName: "Above ↔ Below" }, mem: { connectorId: "east-west", connectorName: "East ↔ West" }, shin: { connectorId: "south-north", connectorName: "South ↔ North" } }; const ELEMENT_NAME_BY_ID = { water: "Water", fire: "Fire", air: "Air", earth: "Earth" }; const ELEMENT_HEBREW_LETTER_BY_ID = { fire: "Yod", water: "Heh", air: "Vav", earth: "Heh" }; const ELEMENT_HEBREW_CHAR_BY_ID = { fire: "י", water: "ה", air: "ו", earth: "ה" }; const HEBREW_LETTER_ID_BY_TETRAGRAMMATON_LETTER = { yod: "yod", heh: "he", vav: "vav" }; const ACE_ELEMENT_BY_CARD_NAME = { "ace of cups": "water", "ace of wands": "fire", "ace of swords": "air", "ace of disks": "earth" }; const COURT_ELEMENT_BY_RANK = { knight: "fire", queen: "water", prince: "air", princess: "earth" }; const SUIT_ELEMENT_BY_SUIT = { wands: "fire", cups: "water", swords: "air", disks: "earth" }; const MINOR_RANK_NUMBER_BY_NAME = { ace: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 }; const SMALL_CARD_SIGN_BY_MODALITY_AND_SUIT = { cardinal: { wands: "aries", cups: "cancer", swords: "libra", disks: "capricorn" }, fixed: { wands: "leo", cups: "scorpio", swords: "aquarius", disks: "taurus" }, mutable: { wands: "sagittarius", cups: "pisces", swords: "gemini", disks: "virgo" } }; function slugify(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } function cardId(card) { const suitPart = card.suit ? `-${slugify(card.suit)}` : ""; return `${slugify(card.arcana)}${suitPart}-${slugify(card.name)}`; } function getElements() { return { tarotCardListEl: document.getElementById("tarot-card-list"), tarotSearchInputEl: document.getElementById("tarot-search-input"), tarotSearchClearEl: document.getElementById("tarot-search-clear"), tarotCountEl: document.getElementById("tarot-card-count"), tarotDetailImageEl: document.getElementById("tarot-detail-image"), tarotDetailNameEl: document.getElementById("tarot-detail-name"), tarotDetailTypeEl: document.getElementById("tarot-detail-type"), tarotDetailSummaryEl: document.getElementById("tarot-detail-summary"), tarotDetailUprightEl: document.getElementById("tarot-detail-upright"), tarotDetailReversedEl: document.getElementById("tarot-detail-reversed"), tarotMetaMeaningCardEl: document.getElementById("tarot-meta-meaning-card"), tarotDetailMeaningEl: document.getElementById("tarot-detail-meaning"), tarotDetailKeywordsEl: document.getElementById("tarot-detail-keywords"), tarotMetaPlanetCardEl: document.getElementById("tarot-meta-planet-card"), tarotMetaElementCardEl: document.getElementById("tarot-meta-element-card"), tarotMetaTetragrammatonCardEl: document.getElementById("tarot-meta-tetragrammaton-card"), tarotMetaZodiacCardEl: document.getElementById("tarot-meta-zodiac-card"), tarotMetaCourtDateCardEl: document.getElementById("tarot-meta-courtdate-card"), tarotMetaHebrewCardEl: document.getElementById("tarot-meta-hebrew-card"), tarotMetaCubeCardEl: document.getElementById("tarot-meta-cube-card"), tarotMetaCalendarCardEl: document.getElementById("tarot-meta-calendar-card"), tarotDetailPlanetEl: document.getElementById("tarot-detail-planet"), tarotDetailElementEl: document.getElementById("tarot-detail-element"), tarotDetailTetragrammatonEl: document.getElementById("tarot-detail-tetragrammaton"), tarotDetailZodiacEl: document.getElementById("tarot-detail-zodiac"), tarotDetailCourtDateEl: document.getElementById("tarot-detail-courtdate"), tarotDetailHebrewEl: document.getElementById("tarot-detail-hebrew"), tarotDetailCubeEl: document.getElementById("tarot-detail-cube"), tarotDetailCalendarEl: document.getElementById("tarot-detail-calendar"), tarotKabPathEl: document.getElementById("tarot-kab-path"), tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards") }; } function normalizeRelationId(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } function normalizeSearchValue(value) { return String(value || "").trim().toLowerCase(); } function normalizeTarotName(value) { return String(value || "") .trim() .toLowerCase() .replace(/\s+/g, " "); } function normalizeTarotCardLookupName(value) { const text = normalizeTarotName(value) .replace(/\b(pentacles?|coins?)\b/g, "disks"); const match = text.match(/^(\d{1,2})\s+of\s+(.+)$/i); if (!match) { return text; } const numeric = Number(match[1]); const suit = String(match[2] || "").trim(); const rankWord = MINOR_NUMBER_WORD_BY_VALUE[numeric]; if (!rankWord || !suit) { return text; } return `${rankWord} of ${suit}`; } function getDisplayCardName(cardOrName, trumpNumber) { const cardName = typeof cardOrName === "object" ? String(cardOrName?.name || "") : String(cardOrName || ""); const resolvedTrumpNumber = typeof cardOrName === "object" ? cardOrName?.number : trumpNumber; if (typeof getTarotCardDisplayName === "function") { const display = String(getTarotCardDisplayName(cardName, { trumpNumber: resolvedTrumpNumber }) || "").trim(); if (display) { return display; } } return cardName.trim(); } function toTitleCase(value) { return String(value || "") .split(" ") .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function resolveElementIdForCard(card) { if (!card) { return ""; } const cardLookupName = normalizeTarotCardLookupName(card.name); const rankKey = String(card.rank || "").trim().toLowerCase(); return ACE_ELEMENT_BY_CARD_NAME[cardLookupName] || COURT_ELEMENT_BY_RANK[rankKey] || ""; } function createElementRelation(card, elementId, sourceKind, sourceLabel) { if (!card || !elementId) { return null; } const elementName = ELEMENT_NAME_BY_ID[elementId] || toTitleCase(elementId); const hebrewLetter = ELEMENT_HEBREW_LETTER_BY_ID[elementId] || ""; const hebrewChar = ELEMENT_HEBREW_CHAR_BY_ID[elementId] || ""; const relationLabel = `${elementName}${hebrewChar ? ` (${hebrewChar})` : (hebrewLetter ? ` (${hebrewLetter})` : "")} · ${sourceLabel}`; return { type: "element", id: elementId, label: relationLabel, data: { elementId, name: elementName, tarotCard: card.name, hebrewLetter, hebrewChar, sourceKind, sourceLabel, rank: card.rank || "", suit: card.suit || "" }, __key: `element|${elementId}|${sourceKind}|${normalizeRelationId(sourceLabel)}|${card.id || normalizeTarotCardLookupName(card.name)}` }; } function buildElementRelationsForCard(card, baseElementRelations = []) { if (!card) { return []; } if (card.arcana === "Major") { return Array.isArray(baseElementRelations) ? [...baseElementRelations] : []; } const relations = []; const suitKey = String(card.suit || "").trim().toLowerCase(); const suitElementId = SUIT_ELEMENT_BY_SUIT[suitKey] || ""; if (suitElementId) { const suitRelation = createElementRelation(card, suitElementId, "suit", `Suit: ${card.suit}`); if (suitRelation) { relations.push(suitRelation); } } const rankKey = String(card.rank || "").trim().toLowerCase(); const courtElementId = COURT_ELEMENT_BY_RANK[rankKey] || ""; if (courtElementId) { const courtRelation = createElementRelation(card, courtElementId, "court", `Court: ${card.rank}`); if (courtRelation) { relations.push(courtRelation); } } return relations; } function buildTetragrammatonRelationsForCard(card) { if (!card) { return []; } const elementId = resolveElementIdForCard(card); if (!elementId) { return []; } const letter = ELEMENT_HEBREW_LETTER_BY_ID[elementId] || ""; if (!letter) { return []; } const elementName = ELEMENT_NAME_BY_ID[elementId] || elementId; const letterKey = String(letter || "").trim().toLowerCase(); const hebrewLetterId = HEBREW_LETTER_ID_BY_TETRAGRAMMATON_LETTER[letterKey] || ""; return [{ type: "tetragrammaton", id: `${letterKey}-${elementId}`, label: `${letter} · ${elementName}`, data: { letter, elementId, elementName, hebrewLetterId }, __key: `tetragrammaton|${letterKey}|${elementId}|${card.id || normalizeTarotCardLookupName(card.name)}` }]; } function getSmallCardModality(rankNumber) { const numeric = Number(rankNumber); if (!Number.isFinite(numeric) || numeric < 2 || numeric > 10) { return ""; } if (numeric <= 4) { return "cardinal"; } if (numeric <= 7) { return "fixed"; } return "mutable"; } function buildSmallCardRulershipRelation(card) { if (!card || card.arcana !== "Minor") { return null; } const rankKey = String(card.rank || "").trim().toLowerCase(); const rankNumber = MINOR_RANK_NUMBER_BY_NAME[rankKey]; const modality = getSmallCardModality(rankNumber); if (!modality) { return null; } const suitKey = String(card.suit || "").trim().toLowerCase(); const signId = SMALL_CARD_SIGN_BY_MODALITY_AND_SUIT[modality]?.[suitKey] || ""; if (!signId) { return null; } const sign = (Array.isArray(state.referenceData?.signs) ? state.referenceData.signs : []) .find((entry) => String(entry?.id || "").trim().toLowerCase() === signId); const signName = String(sign?.name || toTitleCase(signId)); const signSymbol = String(sign?.symbol || "").trim(); const modalityName = toTitleCase(modality); return { type: "zodiacRulership", id: `${signId}-${rankKey}-${suitKey}`, label: `Sign type: ${modalityName} · ${signSymbol} ${signName}`.trim(), data: { signId, signName, symbol: signSymbol, modality, rank: card.rank, suit: card.suit }, __key: `zodiacRulership|${signId}|${rankKey}|${suitKey}` }; } function buildCourtCardByDecanId(cards) { const map = new Map(); (cards || []).forEach((card) => { if (!card || card.arcana !== "Minor") { return; } const rankKey = String(card.rank || "").trim().toLowerCase(); if (rankKey !== "knight" && rankKey !== "queen" && rankKey !== "prince") { return; } const windowRelation = (Array.isArray(card.relations) ? card.relations : []) .find((relation) => relation && typeof relation === "object" && relation.type === "courtDateWindow"); const decanIds = Array.isArray(windowRelation?.data?.decanIds) ? windowRelation.data.decanIds : []; decanIds.forEach((decanId) => { const decanKey = normalizeRelationId(decanId); if (!decanKey || map.has(decanKey)) { return; } map.set(decanKey, { cardName: card.name, rank: card.rank, suit: card.suit, dateRange: String(windowRelation?.data?.dateRange || "").trim() }); }); }); return map; } function buildSmallCardCourtLinkRelations(card, relations) { if (!card || card.arcana !== "Minor") { return []; } const rankKey = String(card.rank || "").trim().toLowerCase(); const rankNumber = MINOR_RANK_NUMBER_BY_NAME[rankKey]; if (!Number.isFinite(rankNumber) || rankNumber < 2 || rankNumber > 10) { return []; } const decans = (relations || []).filter((relation) => relation?.type === "decan"); if (!decans.length) { return []; } const results = []; const seenCourtCardNames = new Set(); decans.forEach((decan) => { const signId = String(decan?.data?.signId || "").trim().toLowerCase(); const decanIndex = Number(decan?.data?.index); if (!signId || !Number.isFinite(decanIndex)) { return; } const decanId = normalizeRelationId(`${signId}-${decanIndex}`); const linkedCourt = state.courtCardByDecanId.get(decanId); if (!linkedCourt?.cardName || seenCourtCardNames.has(linkedCourt.cardName)) { return; } seenCourtCardNames.add(linkedCourt.cardName); results.push({ type: "tarotCard", id: `${decanId}-${normalizeRelationId(linkedCourt.cardName)}`, label: `Shared court date window: ${linkedCourt.cardName}${linkedCourt.dateRange ? ` · ${linkedCourt.dateRange}` : ""}`, data: { cardName: linkedCourt.cardName, dateRange: linkedCourt.dateRange || "", decanId }, __key: `tarotCard|${decanId}|${normalizeRelationId(linkedCourt.cardName)}` }); }); return results; } function normalizeHebrewLetterId(value) { const key = String(value || "") .trim() .toLowerCase() .replace(/[^a-z]/g, ""); return HEBREW_LETTER_ALIASES[key] || key; } function resolveTarotTrumpNumber(cardName) { const key = normalizeTarotName(cardName); if (!key) { return null; } if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) { return TAROT_TRUMP_NUMBER_BY_NAME[key]; } const withoutLeadingThe = key.replace(/^the\s+/, ""); if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) { return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe]; } return null; } function cardMatchesTarotAssociation(card, tarotCardName) { const associationName = normalizeTarotName(tarotCardName); if (!associationName || !card) { return false; } const cardName = normalizeTarotName(card.name); const cardBare = cardName.replace(/^the\s+/, ""); const assocBare = associationName.replace(/^the\s+/, ""); if ( associationName === cardName || associationName === cardBare || assocBare === cardName || assocBare === cardBare ) { return true; } if (card.arcana === "Major" && Number.isFinite(Number(card.number))) { const trumpNumber = resolveTarotTrumpNumber(associationName); if (trumpNumber != null) { return trumpNumber === Number(card.number); } } return false; } function parseMonthDayToken(value) { const text = String(value || "").trim(); const match = text.match(/^(\d{1,2})-(\d{1,2})$/); if (!match) { return null; } const month = Number(match[1]); const day = Number(match[2]); if (!Number.isInteger(month) || !Number.isInteger(day) || month < 1 || month > 12 || day < 1 || day > 31) { return null; } return { month, day }; } function toReferenceDate(token, year) { if (!token) { return null; } return new Date(year, token.month - 1, token.day, 12, 0, 0, 0); } function splitMonthDayRangeByMonth(startToken, endToken) { const startDate = toReferenceDate(startToken, 2025); const endBase = toReferenceDate(endToken, 2025); if (!startDate || !endBase) { return []; } const wrapsYear = endBase.getTime() < startDate.getTime(); const endDate = wrapsYear ? toReferenceDate(endToken, 2026) : endBase; if (!endDate) { return []; } const segments = []; let cursor = new Date(startDate); while (cursor.getTime() <= endDate.getTime()) { const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0); const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate; segments.push({ monthNo: cursor.getMonth() + 1, startDay: cursor.getDate(), endDay: segmentEnd.getDate() }); cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0); } return segments; } function formatMonthDayRangeLabel(monthName, startDay, endDay) { const start = Number(startDay); const end = Number(endDay); if (!Number.isFinite(start) || !Number.isFinite(end)) { return monthName; } if (start === end) { return `${monthName} ${start}`; } return `${monthName} ${start}-${end}`; } function buildMonthReferencesByCard(referenceData, cards) { const map = new Map(); const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : []; const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : []; const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : []; const monthById = new Map(months.map((month) => [month.id, month])); function parseMonthFromDateToken(value) { const token = parseMonthDayToken(value); return token ? token.month : null; } function findMonthByNumber(monthNo) { if (!Number.isInteger(monthNo) || monthNo < 1 || monthNo > 12) { return null; } const byOrder = months.find((month) => Number(month?.order) === monthNo); if (byOrder) { return byOrder; } return months.find((month) => parseMonthFromDateToken(month?.start) === monthNo) || null; } function pushRef(card, month, options = {}) { if (!card?.id || !month?.id) { return; } if (!map.has(card.id)) { map.set(card.id, []); } const rows = map.get(card.id); const monthOrder = Number.isFinite(Number(month.order)) ? Number(month.order) : 999; const startToken = parseMonthDayToken(options.startToken || month.start); const endToken = parseMonthDayToken(options.endToken || month.end); const dateRange = String(options.dateRange || "").trim() || ( startToken && endToken ? formatMonthDayRangeLabel(month.name || month.id, startToken.day, endToken.day) : "" ); const uniqueKey = [ month.id, dateRange.toLowerCase(), String(options.context || "").trim().toLowerCase(), String(options.source || "").trim().toLowerCase() ].join("|"); if (rows.some((entry) => entry.uniqueKey === uniqueKey)) { return; } rows.push({ id: month.id, name: month.name || month.id, order: monthOrder, startToken: startToken ? `${String(startToken.month).padStart(2, "0")}-${String(startToken.day).padStart(2, "0")}` : null, endToken: endToken ? `${String(endToken.month).padStart(2, "0")}-${String(endToken.day).padStart(2, "0")}` : null, dateRange, context: String(options.context || "").trim(), source: String(options.source || "").trim(), uniqueKey }); } function captureRefs(associations, month) { const tarotCardName = associations?.tarotCard; if (!tarotCardName) { return; } cards.forEach((card) => { if (cardMatchesTarotAssociation(card, tarotCardName)) { pushRef(card, month); } }); } months.forEach((month) => { captureRefs(month?.associations, month); const events = Array.isArray(month?.events) ? month.events : []; events.forEach((event) => { const tarotCardName = event?.associations?.tarotCard; if (!tarotCardName) { return; } cards.forEach((card) => { if (!cardMatchesTarotAssociation(card, tarotCardName)) { return; } pushRef(card, month, { source: "month-event", context: String(event?.name || "").trim() }); }); }); }); holidays.forEach((holiday) => { const month = monthById.get(holiday?.monthId); if (!month) { return; } const tarotCardName = holiday?.associations?.tarotCard; if (!tarotCardName) { return; } cards.forEach((card) => { if (!cardMatchesTarotAssociation(card, tarotCardName)) { return; } pushRef(card, month, { source: "holiday", context: String(holiday?.name || "").trim() }); }); }); signs.forEach((sign) => { const signTrumpNumber = Number(sign?.tarot?.number); const signTarotName = sign?.tarot?.majorArcana || sign?.tarot?.card || sign?.tarotCard; if (!Number.isFinite(signTrumpNumber) && !signTarotName) { return; } const signName = String(sign?.name || sign?.id || "").trim(); const startToken = parseMonthDayToken(sign?.start); const endToken = parseMonthDayToken(sign?.end); const monthSegments = splitMonthDayRangeByMonth(startToken, endToken); const fallbackStartMonthNo = parseMonthFromDateToken(sign?.start); const fallbackEndMonthNo = parseMonthFromDateToken(sign?.end); const fallbackStartMonth = findMonthByNumber(fallbackStartMonthNo); const fallbackEndMonth = findMonthByNumber(fallbackEndMonthNo); cards.forEach((card) => { const cardTrumpNumber = Number(card?.number); const matchesByTrump = card?.arcana === "Major" && Number.isFinite(cardTrumpNumber) && Number.isFinite(signTrumpNumber) && cardTrumpNumber === signTrumpNumber; const matchesByName = signTarotName ? cardMatchesTarotAssociation(card, signTarotName) : false; if (!matchesByTrump && !matchesByName) { return; } if (monthSegments.length) { monthSegments.forEach((segment) => { const month = findMonthByNumber(segment.monthNo); if (!month) { return; } pushRef(card, month, { source: "zodiac-window", context: signName ? `${signName} window` : "", startToken: `${String(segment.monthNo).padStart(2, "0")}-${String(segment.startDay).padStart(2, "0")}`, endToken: `${String(segment.monthNo).padStart(2, "0")}-${String(segment.endDay).padStart(2, "0")}`, dateRange: formatMonthDayRangeLabel(month.name || month.id, segment.startDay, segment.endDay) }); }); return; } if (fallbackStartMonth) { pushRef(card, fallbackStartMonth, { source: "zodiac-window", context: signName ? `${signName} window` : "" }); } if (fallbackEndMonth && (!fallbackStartMonth || fallbackEndMonth.id !== fallbackStartMonth.id)) { pushRef(card, fallbackEndMonth, { source: "zodiac-window", context: signName ? `${signName} window` : "" }); } }); }); map.forEach((rows, key) => { const monthIdsWithZodiacWindows = new Set( rows .filter((entry) => entry?.source === "zodiac-window" && entry?.id) .map((entry) => entry.id) ); const filteredRows = rows.filter((entry) => { if (!entry?.id || entry?.source === "zodiac-window") { return true; } if (!monthIdsWithZodiacWindows.has(entry.id)) { return true; } const month = monthById.get(entry.id); if (!month) { return true; } const isFullMonth = String(entry.startToken || "") === String(month.start || "") && String(entry.endToken || "") === String(month.end || ""); return !isFullMonth; }); filteredRows.sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } const startLeft = parseMonthDayToken(left.startToken); const startRight = parseMonthDayToken(right.startToken); const dayLeft = startLeft ? startLeft.day : 999; const dayRight = startRight ? startRight.day : 999; if (dayLeft !== dayRight) { return dayLeft - dayRight; } return String(left.dateRange || left.name || "").localeCompare(String(right.dateRange || right.name || "")); }); map.set(key, filteredRows); }); return map; } function relationToSearchText(relation) { if (!relation) { return ""; } if (typeof relation === "string") { return relation; } const relationParts = [ relation.label, relation.type, relation.id, relation.data && typeof relation.data === "object" ? Object.values(relation.data).join(" ") : "" ]; return relationParts.filter(Boolean).join(" "); } function buildCardSearchText(card) { const displayName = getDisplayCardName(card); const tarotAliases = typeof getTarotCardSearchAliases === "function" ? getTarotCardSearchAliases(card?.name, { trumpNumber: card?.number }) : []; const parts = [ card.name, displayName, card.arcana, card.rank, card.suit, card.summary, ...tarotAliases, ...(Array.isArray(card.keywords) ? card.keywords : []), ...(Array.isArray(card.relations) ? card.relations.map(relationToSearchText) : []) ]; return normalizeSearchValue(parts.join(" ")); } function applySearchFilter(elements) { const query = normalizeSearchValue(state.searchQuery); state.filteredCards = query ? state.cards.filter((card) => buildCardSearchText(card).includes(query)) : [...state.cards]; if (elements?.tarotSearchClearEl) { elements.tarotSearchClearEl.disabled = !query; } renderList(elements); if (!state.filteredCards.some((card) => card.id === state.selectedCardId)) { if (state.filteredCards.length > 0) { selectCardById(state.filteredCards[0].id, elements); } return; } updateListSelection(elements); } function clearChildren(element) { if (element) { element.replaceChildren(); } } function getCardLookupMap(cards) { const map = new Map(); (cards || []).forEach((card) => { const key = normalizeTarotCardLookupName(card?.name); if (key) { map.set(key, card); } }); return map; } function buildMinorCardName(rankNumber, suit) { const rank = MINOR_TITLE_WORD_BY_VALUE[Number(rankNumber)]; const suitName = String(suit || "").trim(); if (!rank || !suitName) { return ""; } return `${rank} of ${suitName}`; } function buildCourtCardName(rank, suit) { const rankName = String(rank || "").trim(); const suitName = String(suit || "").trim(); if (!rankName || !suitName) { return ""; } return `${rankName} of ${suitName}`; } function findCardByLookupName(cardLookupMap, cardName) { const key = normalizeTarotCardLookupName(cardName); if (!key) { return null; } return cardLookupMap.get(key) || null; } function findMajorCardByTrumpNumber(cards, trumpNumber) { const target = Number(trumpNumber); if (!Number.isFinite(target)) { return null; } return (cards || []).find((card) => card?.arcana === "Major" && Number(card?.number) === target) || null; } function createHouseCardButton(card, elements) { const button = document.createElement("button"); button.type = "button"; button.className = "tarot-house-card-btn"; if (!card) { button.disabled = true; const fallback = document.createElement("span"); fallback.className = "tarot-house-card-fallback"; fallback.textContent = "Missing"; button.appendChild(fallback); return button; } const cardDisplayName = getDisplayCardName(card); button.title = cardDisplayName || card.name; button.setAttribute("aria-label", cardDisplayName || card.name); button.dataset.houseCardId = card.id; const imageUrl = typeof resolveTarotCardImage === "function" ? resolveTarotCardImage(card.name) : null; if (imageUrl) { const image = document.createElement("img"); image.className = "tarot-house-card-image"; image.src = imageUrl; image.alt = cardDisplayName || card.name; button.appendChild(image); } else { const fallback = document.createElement("span"); fallback.className = "tarot-house-card-fallback"; fallback.textContent = cardDisplayName || card.name; button.appendChild(fallback); } button.addEventListener("click", () => { selectCardById(card.id, elements); elements?.tarotCardListEl ?.querySelector(`[data-card-id="${card.id}"]`) ?.scrollIntoView({ block: "nearest" }); }); return button; } function updateHouseSelection(elements) { if (!elements?.tarotHouseOfCardsEl) { return; } const buttons = elements.tarotHouseOfCardsEl.querySelectorAll(".tarot-house-card-btn[data-house-card-id]"); buttons.forEach((button) => { const isSelected = button.dataset.houseCardId === state.selectedCardId; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-current", isSelected ? "true" : "false"); }); } function appendHouseMinorRow(columnEl, cardLookupMap, numbers, suit, elements) { const rowEl = document.createElement("div"); rowEl.className = "tarot-house-row"; numbers.forEach((rankNumber) => { const cardName = buildMinorCardName(rankNumber, suit); const card = findCardByLookupName(cardLookupMap, cardName); rowEl.appendChild(createHouseCardButton(card, elements)); }); columnEl.appendChild(rowEl); } function appendHouseCourtRow(columnEl, cardLookupMap, rank, elements) { const rowEl = document.createElement("div"); rowEl.className = "tarot-house-row"; HOUSE_MIDDLE_SUITS.forEach((suit) => { const cardName = buildCourtCardName(rank, suit); const card = findCardByLookupName(cardLookupMap, cardName); rowEl.appendChild(createHouseCardButton(card, elements)); }); columnEl.appendChild(rowEl); } function appendHouseTrumpRow(containerEl, trumpNumbers, elements) { const rowEl = document.createElement("div"); rowEl.className = "tarot-house-trump-row"; (trumpNumbers || []).forEach((trumpNumber) => { const card = findMajorCardByTrumpNumber(state.cards, trumpNumber); rowEl.appendChild(createHouseCardButton(card, elements)); }); containerEl.appendChild(rowEl); } function renderHouseOfCards(elements) { if (!elements?.tarotHouseOfCardsEl) { return; } clearChildren(elements.tarotHouseOfCardsEl); const cardLookupMap = getCardLookupMap(state.cards); const trumpSectionEl = document.createElement("div"); trumpSectionEl.className = "tarot-house-trumps"; HOUSE_TRUMP_ROWS.forEach((trumpRow) => { appendHouseTrumpRow(trumpSectionEl, trumpRow, elements); }); const bottomGridEl = document.createElement("div"); bottomGridEl.className = "tarot-house-bottom-grid"; const leftColumnEl = document.createElement("div"); leftColumnEl.className = "tarot-house-column"; HOUSE_MINOR_NUMBER_BANDS.forEach((numbers, rowIndex) => { appendHouseMinorRow(leftColumnEl, cardLookupMap, numbers, HOUSE_LEFT_SUITS[rowIndex], elements); }); const middleColumnEl = document.createElement("div"); middleColumnEl.className = "tarot-house-column"; HOUSE_MIDDLE_RANKS.forEach((rank) => { appendHouseCourtRow(middleColumnEl, cardLookupMap, rank, elements); }); const rightColumnEl = document.createElement("div"); rightColumnEl.className = "tarot-house-column"; HOUSE_MINOR_NUMBER_BANDS.forEach((numbers, rowIndex) => { appendHouseMinorRow(rightColumnEl, cardLookupMap, numbers, HOUSE_RIGHT_SUITS[rowIndex], elements); }); bottomGridEl.append(leftColumnEl, middleColumnEl, rightColumnEl); elements.tarotHouseOfCardsEl.append(trumpSectionEl, bottomGridEl); updateHouseSelection(elements); } function buildTypeLabel(card) { if (card.arcana === "Major") { return typeof card.number === "number" ? `Major Arcana · ${card.number}` : "Major Arcana"; } const parts = ["Minor Arcana"]; if (card.rank) { parts.push(card.rank); } if (card.suit) { parts.push(card.suit); } return parts.join(" · "); } const MINOR_PLURAL_BY_RANK = { ace: "aces", two: "twos", three: "threes", four: "fours", five: "fives", six: "sixes", seven: "sevens", eight: "eights", nine: "nines", ten: "tens" }; function findSephirahForMinorCard(card, kabTree) { if (!card || card.arcana !== "Minor" || !kabTree) { return null; } const rankKey = String(card.rank || "").trim().toLowerCase(); const plural = MINOR_PLURAL_BY_RANK[rankKey]; if (!plural) { return null; } const matcher = new RegExp(`\\b4\\s+${plural}\\b`, "i"); return (kabTree.sephiroth || []).find((seph) => matcher.test(String(seph?.tarot || ""))) || null; } function formatRelation(relation) { if (typeof relation === "string") { return relation; } if (!relation || typeof relation !== "object") { return ""; } if (typeof relation.label === "string" && relation.label.trim()) { return relation.label; } if (relation.type === "hebrewLetter" && relation.data) { const glyph = relation.data.glyph || ""; const name = relation.data.name || relation.id || "Unknown"; const latin = relation.data.latin ? ` (${relation.data.latin})` : ""; const index = Number.isFinite(relation.data.index) ? relation.data.index : "?"; const value = Number.isFinite(relation.data.value) ? relation.data.value : "?"; const meaning = relation.data.meaning ? ` · ${relation.data.meaning}` : ""; return `Hebrew Letter: ${glyph} ${name}${latin} (index ${index}, value ${value})${meaning}`.trim(); } if (typeof relation.type === "string" && typeof relation.id === "string") { return `${relation.type}: ${relation.id}`; } return ""; } function relationKey(relation, index) { const safeType = String(relation?.type || "relation"); const safeId = String(relation?.id || index || "0"); const safeLabel = String(relation?.label || relation?.text || ""); return `${safeType}|${safeId}|${safeLabel}`; } function normalizeRelationObject(relation, index) { if (relation && typeof relation === "object") { const label = formatRelation(relation); if (!label) { return null; } return { ...relation, label, __key: relationKey(relation, index) }; } const text = formatRelation(relation); if (!text) { return null; } return { type: "text", id: `text-${index}`, label: text, data: { value: text }, __key: relationKey({ type: "text", id: `text-${index}`, label: text }, index) }; } function formatRelationDataLines(relation) { if (!relation || typeof relation !== "object") { return "--"; } const data = relation.data; if (!data || typeof data !== "object") { return "(no additional relation data)"; } const lines = Object.entries(data) .filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== "") .map(([key, value]) => `${key}: ${value}`); return lines.length ? lines.join("\n") : "(no additional relation data)"; } function buildCubeFaceRelationsForCard(card) { const cube = state.magickDataset?.grouped?.kabbalah?.cube; const walls = Array.isArray(cube?.walls) ? cube.walls : []; if (!card || !walls.length) { return []; } return walls .map((wall, index) => { const wallTarot = wall?.associations?.tarotCard || wall?.tarotCard; if (!wallTarot || !cardMatchesTarotAssociation(card, wallTarot)) { return null; } const wallId = String(wall?.id || "").trim(); const wallName = String(wall?.name || wallId || "").trim(); if (!wallId) { return null; } return { type: "cubeFace", id: wallId, label: `Cube: ${wallName} Wall - Face`, data: { wallId, wallName, edgeId: "" }, __key: `cubeFace|${wallId}|${index}` }; }) .filter(Boolean); } function cardMatchesPathTarot(card, path) { if (!card || !path) { return false; } const trumpNumber = Number(path?.tarot?.trumpNumber); if (card?.arcana === "Major" && Number.isFinite(Number(card?.number)) && Number.isFinite(trumpNumber)) { if (Number(card.number) === trumpNumber) { return true; } } return cardMatchesTarotAssociation(card, path?.tarot?.card); } function buildCubeEdgeRelationsForCard(card) { const cube = state.magickDataset?.grouped?.kabbalah?.cube; const tree = state.magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]; const edges = Array.isArray(cube?.edges) ? cube.edges : []; const paths = Array.isArray(tree?.paths) ? tree.paths : []; if (!card || !edges.length || !paths.length) { return []; } const pathByLetterId = new Map( paths .map((path) => [normalizeHebrewLetterId(path?.hebrewLetter?.transliteration), path]) .filter(([letterId]) => Boolean(letterId)) ); return edges .map((edge, index) => { const edgeLetterId = normalizeHebrewLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId); if (!edgeLetterId) { return null; } const pathMatch = pathByLetterId.get(edgeLetterId); if (!pathMatch || !cardMatchesPathTarot(card, pathMatch)) { return null; } const edgeId = String(edge?.id || "").trim(); if (!edgeId) { return null; } const edgeName = String(edge?.name || edgeId).trim(); const wallId = String(Array.isArray(edge?.walls) ? (edge.walls[0] || "") : "").trim(); return { type: "cubeEdge", id: edgeId, label: `Cube: ${edgeName} Edge`, data: { edgeId, edgeName, wallId: wallId || undefined, hebrewLetterId: edgeLetterId }, __key: `cubeEdge|${edgeId}|${index}` }; }) .filter(Boolean); } function buildCubeMotherConnectorRelationsForCard(card) { const tree = state.magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]; const paths = Array.isArray(tree?.paths) ? tree.paths : []; const relations = Array.isArray(card?.relations) ? card.relations : []; return Object.entries(CUBE_MOTHER_CONNECTOR_BY_LETTER) .map(([letterId, connector]) => { const pathMatch = paths.find((path) => normalizeHebrewLetterId(path?.hebrewLetter?.transliteration) === letterId) || null; const matchesByPath = cardMatchesPathTarot(card, pathMatch); const matchesByHebrewRelation = relations.some((relation) => { if (relation?.type !== "hebrewLetter") { return false; } const relationLetterId = normalizeHebrewLetterId( relation?.data?.id || relation?.id || relation?.data?.latin || relation?.data?.name ); return relationLetterId === letterId; }); if (!matchesByPath && !matchesByHebrewRelation) { return null; } return { type: "cubeConnector", id: connector.connectorId, label: `Cube: ${connector.connectorName}`, data: { connectorId: connector.connectorId, connectorName: connector.connectorName, hebrewLetterId: letterId }, __key: `cubeConnector|${connector.connectorId}|${letterId}` }; }) .filter(Boolean); } function buildCubePrimalPointRelationsForCard(card) { const center = state.magickDataset?.grouped?.kabbalah?.cube?.center; if (!center || !card) { return []; } const centerTarot = center?.associations?.tarotCard || center?.tarotCard; const centerTrump = Number(center?.associations?.tarotTrumpNumber); const matchesByName = cardMatchesTarotAssociation(card, centerTarot); const matchesByTrump = card?.arcana === "Major" && Number.isFinite(Number(card?.number)) && Number.isFinite(centerTrump) && Number(card.number) === centerTrump; if (!matchesByName && !matchesByTrump) { return []; } return [{ type: "cubeCenter", id: "primal-point", label: "Cube: Primal Point", data: { nodeType: "center", primalPoint: true }, __key: "cubeCenter|primal-point" }]; } function buildCubeRelationsForCard(card) { return [ ...buildCubeFaceRelationsForCard(card), ...buildCubeEdgeRelationsForCard(card), ...buildCubePrimalPointRelationsForCard(card), ...buildCubeMotherConnectorRelationsForCard(card) ]; } // Returns nav dispatch config for relations that have a corresponding section, // null for informational-only relations. function getRelationNavTarget(relation) { const t = relation?.type; const d = relation?.data || {}; if ((t === "planetCorrespondence" || t === "decanRuler") && d.planetId) { return { event: "nav:planet", detail: { planetId: d.planetId }, label: `Open ${d.name || d.planetId} in Planets` }; } if (t === "planet") { const planetId = normalizeRelationId(d.name || relation?.id || ""); if (!planetId) return null; return { event: "nav:planet", detail: { planetId }, label: `Open ${d.name || planetId} in Planets` }; } if (t === "element") { const elementId = d.elementId || relation?.id; if (!elementId) { return null; } return { event: "nav:elements", detail: { elementId }, label: `Open ${d.name || elementId} in Elements` }; } if (t === "tetragrammaton") { if (!d.hebrewLetterId) { return null; } return { event: "nav:alphabet", detail: { alphabet: "hebrew", hebrewLetterId: d.hebrewLetterId }, label: `Open ${d.letter || d.hebrewLetterId} in Alphabet` }; } if (t === "tarotCard") { const cardName = d.cardName || relation?.id; if (!cardName) { return null; } return { event: "nav:tarot-trump", detail: { cardName }, label: `Open ${cardName} in Tarot` }; } if (t === "zodiacRulership") { const signId = d.signId || relation?.id; if (!signId) { return null; } return { event: "nav:zodiac", detail: { signId }, label: `Open ${d.signName || signId} in Zodiac` }; } if (t === "zodiacCorrespondence" || t === "zodiac") { const signId = d.signId || relation?.id || normalizeRelationId(d.name || ""); if (!signId) { return null; } return { event: "nav:zodiac", detail: { signId }, label: `Open ${d.name || signId} in Zodiac` }; } if (t === "decan") { const signId = d.signId || normalizeRelationId(d.signName || relation?.id || ""); if (!signId) { return null; } return { event: "nav:zodiac", detail: { signId }, label: `Open ${d.signName || signId} in Zodiac` }; } if (t === "hebrewLetter") { const hebrewLetterId = d.id || relation?.id; if (!hebrewLetterId) { return null; } return { event: "nav:alphabet", detail: { alphabet: "hebrew", hebrewLetterId }, label: `Open ${d.name || hebrewLetterId} in Alphabet` }; } if (t === "calendarMonth") { const monthId = d.monthId || relation?.id; if (!monthId) { return null; } return { event: "nav:calendar-month", detail: { monthId }, label: `Open ${d.name || monthId} in Calendar` }; } if (t === "cubeFace") { const wallId = d.wallId || relation?.id; if (!wallId) { return null; } return { event: "nav:cube", detail: { wallId, edgeId: "" }, label: `Open ${d.wallName || wallId} face in Cube` }; } if (t === "cubeEdge") { const edgeId = d.edgeId || relation?.id; if (!edgeId) { return null; } return { event: "nav:cube", detail: { edgeId, wallId: d.wallId || undefined }, label: `Open ${d.edgeName || edgeId} edge in Cube` }; } if (t === "cubeConnector") { const connectorId = d.connectorId || relation?.id; if (!connectorId) { return null; } return { event: "nav:cube", detail: { connectorId }, label: `Open ${d.connectorName || connectorId} connector in Cube` }; } if (t === "cubeCenter") { return { event: "nav:cube", detail: { nodeType: "center", primalPoint: true }, label: "Open Primal Point in Cube" }; } return null; } function createRelationListItem(relation) { const item = document.createElement("li"); const navTarget = getRelationNavTarget(relation); const button = document.createElement("button"); button.type = "button"; button.className = "tarot-relation-btn"; button.dataset.relationKey = relation.__key; button.textContent = relation.label; item.appendChild(button); if (!navTarget) { button.classList.add("tarot-relation-btn-static"); } button.addEventListener("click", () => { if (navTarget) { document.dispatchEvent(new CustomEvent(navTarget.event, { detail: navTarget.detail })); } }); if (navTarget) { item.className = "tarot-rel-item"; const navBtn = document.createElement("button"); navBtn.type = "button"; navBtn.className = "tarot-rel-nav-btn"; navBtn.title = navTarget.label; navBtn.textContent = "\u2197"; navBtn.addEventListener("click", (e) => { e.stopPropagation(); document.dispatchEvent(new CustomEvent(navTarget.event, { detail: navTarget.detail })); }); item.appendChild(navBtn); } return item; } function renderStaticRelationGroup(targetEl, cardEl, relations) { clearChildren(targetEl); if (!targetEl || !cardEl) return; if (!relations.length) { cardEl.hidden = true; return; } cardEl.hidden = false; relations.forEach((relation) => { targetEl.appendChild(createRelationListItem(relation)); }); } function renderDetail(card, elements) { if (!card || !elements) { return; } const cardDisplayName = getDisplayCardName(card); const imageUrl = typeof resolveTarotCardImage === "function" ? resolveTarotCardImage(card.name) : null; if (elements.tarotDetailImageEl) { if (imageUrl) { elements.tarotDetailImageEl.src = imageUrl; elements.tarotDetailImageEl.alt = cardDisplayName || card.name; elements.tarotDetailImageEl.style.display = "block"; elements.tarotDetailImageEl.style.cursor = "zoom-in"; elements.tarotDetailImageEl.title = "Click to enlarge"; } else { elements.tarotDetailImageEl.removeAttribute("src"); elements.tarotDetailImageEl.alt = ""; elements.tarotDetailImageEl.style.display = "none"; elements.tarotDetailImageEl.style.cursor = "default"; elements.tarotDetailImageEl.removeAttribute("title"); } } if (elements.tarotDetailNameEl) { elements.tarotDetailNameEl.textContent = cardDisplayName || card.name; } if (elements.tarotDetailTypeEl) { elements.tarotDetailTypeEl.textContent = buildTypeLabel(card); } if (elements.tarotDetailSummaryEl) { elements.tarotDetailSummaryEl.textContent = card.summary || "--"; } if (elements.tarotDetailUprightEl) { elements.tarotDetailUprightEl.textContent = card.meanings?.upright || "--"; } if (elements.tarotDetailReversedEl) { elements.tarotDetailReversedEl.textContent = card.meanings?.reversed || "--"; } const meaningText = String(card.meaning || card.meanings?.upright || "").trim(); if (elements.tarotMetaMeaningCardEl && elements.tarotDetailMeaningEl) { if (meaningText) { elements.tarotMetaMeaningCardEl.hidden = false; elements.tarotDetailMeaningEl.textContent = meaningText; } else { elements.tarotMetaMeaningCardEl.hidden = true; elements.tarotDetailMeaningEl.textContent = "--"; } } clearChildren(elements.tarotDetailKeywordsEl); (card.keywords || []).forEach((keyword) => { const chip = document.createElement("span"); chip.className = "tarot-keyword-chip"; chip.textContent = keyword; elements.tarotDetailKeywordsEl?.appendChild(chip); }); const allRelations = (card.relations || []) .map((relation, index) => normalizeRelationObject(relation, index)) .filter(Boolean); const uniqueByKey = new Set(); const dedupedRelations = allRelations.filter((relation) => { const key = `${relation.type || "relation"}|${relation.id || ""}|${relation.label || ""}`; if (uniqueByKey.has(key)) return false; uniqueByKey.add(key); return true; }); const planetRelations = dedupedRelations.filter((relation) => relation.type === "planetCorrespondence" || relation.type === "decanRuler" || relation.type === "planet" ); const zodiacRelations = dedupedRelations.filter((relation) => relation.type === "zodiacCorrespondence" || relation.type === "zodiac" || relation.type === "decan" ); const courtDateRelations = dedupedRelations.filter((relation) => relation.type === "courtDateWindow"); const hebrewRelations = dedupedRelations.filter((relation) => relation.type === "hebrewLetter"); const baseElementRelations = dedupedRelations.filter((relation) => relation.type === "element"); const elementRelations = buildElementRelationsForCard(card, baseElementRelations); const tetragrammatonRelations = buildTetragrammatonRelationsForCard(card); const smallCardRulershipRelation = buildSmallCardRulershipRelation(card); const zodiacRelationsWithRulership = smallCardRulershipRelation ? [...zodiacRelations, smallCardRulershipRelation] : zodiacRelations; const smallCardCourtLinkRelations = buildSmallCardCourtLinkRelations(card, dedupedRelations); const mergedCourtDateRelations = [...courtDateRelations, ...smallCardCourtLinkRelations]; const cubeRelations = buildCubeRelationsForCard(card); const monthRelations = (state.monthRefsByCardId.get(card.id) || []).map((month, index) => { const dateRange = String(month?.dateRange || "").trim(); const context = String(month?.context || "").trim(); const labelBase = dateRange || month.name; const label = context ? `${labelBase} · ${context}` : labelBase; return { type: "calendarMonth", id: month.id, label, data: { monthId: month.id, name: month.name, monthOrder: Number.isFinite(Number(month.order)) ? Number(month.order) : null, dateRange: dateRange || null, dateStart: month.startToken || null, dateEnd: month.endToken || null, context: context || null, source: month.source || null }, __key: `calendarMonth|${month.id}|${month.uniqueKey || index}` }; }); const relationMonthRows = dedupedRelations .filter((relation) => relation.type === "calendarMonth") .map((relation) => { const dateRange = String(relation?.data?.dateRange || "").trim(); const baseName = relation?.data?.name || relation.label; const label = dateRange && baseName ? `${baseName} · ${dateRange}` : baseName; return { type: "calendarMonth", id: relation?.data?.monthId || relation.id, label, data: { monthId: relation?.data?.monthId || relation.id, name: relation?.data?.name || relation.label, monthOrder: Number.isFinite(Number(relation?.data?.monthOrder)) ? Number(relation.data.monthOrder) : null, dateRange: dateRange || null, dateStart: relation?.data?.dateStart || null, dateEnd: relation?.data?.dateEnd || null, context: relation?.data?.signName || null }, __key: relation.__key }; }) .filter((entry) => entry.data.monthId); const mergedMonthMap = new Map(); [...monthRelations, ...relationMonthRows].forEach((entry) => { const monthId = entry?.data?.monthId; if (!monthId) { return; } const key = [ monthId, String(entry?.data?.dateRange || "").trim().toLowerCase(), String(entry?.data?.context || "").trim().toLowerCase(), String(entry?.label || "").trim().toLowerCase() ].join("|"); if (!mergedMonthMap.has(key)) { mergedMonthMap.set(key, entry); } }); const mergedMonthRelations = [...mergedMonthMap.values()].sort((left, right) => { const orderLeft = Number.isFinite(Number(left?.data?.monthOrder)) ? Number(left.data.monthOrder) : 999; const orderRight = Number.isFinite(Number(right?.data?.monthOrder)) ? Number(right.data.monthOrder) : 999; if (orderLeft !== orderRight) { return orderLeft - orderRight; } const startLeft = parseMonthDayToken(left?.data?.dateStart); const startRight = parseMonthDayToken(right?.data?.dateStart); const dayLeft = startLeft ? startLeft.day : 999; const dayRight = startRight ? startRight.day : 999; if (dayLeft !== dayRight) { return dayLeft - dayRight; } return String(left.label || "").localeCompare(String(right.label || "")); }); renderStaticRelationGroup(elements.tarotDetailPlanetEl, elements.tarotMetaPlanetCardEl, planetRelations); renderStaticRelationGroup(elements.tarotDetailElementEl, elements.tarotMetaElementCardEl, elementRelations); renderStaticRelationGroup(elements.tarotDetailTetragrammatonEl, elements.tarotMetaTetragrammatonCardEl, tetragrammatonRelations); renderStaticRelationGroup(elements.tarotDetailZodiacEl, elements.tarotMetaZodiacCardEl, zodiacRelationsWithRulership); renderStaticRelationGroup(elements.tarotDetailCourtDateEl, elements.tarotMetaCourtDateCardEl, mergedCourtDateRelations); renderStaticRelationGroup(elements.tarotDetailHebrewEl, elements.tarotMetaHebrewCardEl, hebrewRelations); renderStaticRelationGroup(elements.tarotDetailCubeEl, elements.tarotMetaCubeCardEl, cubeRelations); renderStaticRelationGroup(elements.tarotDetailCalendarEl, elements.tarotMetaCalendarCardEl, mergedMonthRelations); // ── Kabbalah Tree path cross-reference ───────────────────────────────── const kabPathEl = elements.tarotKabPathEl; if (kabPathEl) { const kabTree = state.magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]; const kabPath = (card.arcana === "Major" && typeof card.number === "number" && kabTree) ? kabTree.paths.find(p => p.tarot?.trumpNumber === card.number) : null; const kabSeph = !kabPath ? findSephirahForMinorCard(card, kabTree) : null; if (kabPath) { const letter = kabPath.hebrewLetter || {}; const fromName = kabTree.sephiroth.find(s => s.number === kabPath.connects.from)?.name || kabPath.connects.from; const toName = kabTree.sephiroth.find(s => s.number === kabPath.connects.to)?.name || kabPath.connects.to; const astro = kabPath.astrology ? `${kabPath.astrology.name} (${kabPath.astrology.type})` : ""; kabPathEl.innerHTML = ` Kabbalah Tree — Path ${kabPath.pathNumber}
${letter.char || ""} ${letter.transliteration || ""} — “${letter.meaning || ""}” · ${letter.letterType || ""} ${fromName} → ${toName}${astro ? " · " + astro : ""}
`; const btn = document.createElement("button"); btn.type = "button"; btn.className = "kab-tarot-link"; btn.textContent = `View Path ${kabPath.pathNumber} in Kabbalah Tree`; btn.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("tarot:view-kab-path", { detail: { pathNumber: kabPath.pathNumber } })); }); kabPathEl.appendChild(btn); kabPathEl.hidden = false; } else if (kabSeph) { const hebrewName = kabSeph.nameHebrew ? ` (${kabSeph.nameHebrew})` : ""; const translation = kabSeph.translation ? ` — ${kabSeph.translation}` : ""; const planetInfo = kabSeph.planet || ""; const tarotInfo = kabSeph.tarot ? ` · ${kabSeph.tarot}` : ""; kabPathEl.innerHTML = ` Kabbalah Tree — Sephirah ${kabSeph.number}
${kabSeph.number} ${kabSeph.name || ""}${hebrewName}${translation} ${planetInfo}${tarotInfo}
`; const btn = document.createElement("button"); btn.type = "button"; btn.className = "kab-tarot-link"; btn.textContent = `View Sephirah ${kabSeph.number} in Kabbalah Tree`; btn.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("tarot:view-kab-path", { detail: { pathNumber: kabSeph.number } })); }); kabPathEl.appendChild(btn); kabPathEl.hidden = false; } else { kabPathEl.hidden = true; kabPathEl.innerHTML = ""; } } } function updateListSelection(elements) { if (!elements?.tarotCardListEl) { return; } const buttons = elements.tarotCardListEl.querySelectorAll(".tarot-list-item"); buttons.forEach((button) => { const isSelected = button.dataset.cardId === state.selectedCardId; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); }); } function selectCardById(cardIdToSelect, elements) { const card = state.cards.find((entry) => entry.id === cardIdToSelect); if (!card) { return; } state.selectedCardId = card.id; updateListSelection(elements); updateHouseSelection(elements); renderDetail(card, elements); } function renderList(elements) { if (!elements?.tarotCardListEl) { return; } clearChildren(elements.tarotCardListEl); state.filteredCards.forEach((card) => { const cardDisplayName = getDisplayCardName(card); const button = document.createElement("button"); button.type = "button"; button.className = "tarot-list-item"; button.dataset.cardId = card.id; button.setAttribute("role", "option"); const nameEl = document.createElement("span"); nameEl.className = "tarot-list-name"; nameEl.textContent = cardDisplayName || card.name; const metaEl = document.createElement("span"); metaEl.className = "tarot-list-meta"; metaEl.textContent = buildTypeLabel(card); button.append(nameEl, metaEl); elements.tarotCardListEl.appendChild(button); }); if (elements.tarotCountEl) { elements.tarotCountEl.textContent = state.searchQuery ? `${state.filteredCards.length} of ${state.cards.length} cards` : `${state.cards.length} cards`; } } function ensureTarotSection(referenceData, magickDataset = null) { state.referenceData = referenceData || state.referenceData; if (magickDataset) { state.magickDataset = magickDataset; } const elements = getElements(); if (state.initialized) { state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, state.cards); state.courtCardByDecanId = buildCourtCardByDecanId(state.cards); renderHouseOfCards(elements); if (state.selectedCardId) { const selected = state.cards.find((card) => card.id === state.selectedCardId); if (selected) { renderDetail(selected, elements); } } return; } if (!elements.tarotCardListEl || !elements.tarotDetailNameEl) { return; } const databaseBuilder = window.TarotCardDatabase?.buildTarotDatabase; if (typeof databaseBuilder !== "function") { return; } const cards = databaseBuilder(referenceData, magickDataset).map((card) => ({ ...card, id: cardId(card) })); state.cards = cards; state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, cards); state.courtCardByDecanId = buildCourtCardByDecanId(cards); state.filteredCards = [...cards]; renderList(elements); renderHouseOfCards(elements); if (cards.length > 0) { selectCardById(cards[0].id, elements); } elements.tarotCardListEl.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Node)) { return; } const button = target instanceof Element ? target.closest(".tarot-list-item") : null; if (!(button instanceof HTMLButtonElement)) { return; } const selectedId = button.dataset.cardId; if (!selectedId) { return; } selectCardById(selectedId, elements); }); if (elements.tarotSearchInputEl) { elements.tarotSearchInputEl.addEventListener("input", () => { state.searchQuery = elements.tarotSearchInputEl.value || ""; applySearchFilter(elements); }); } if (elements.tarotSearchClearEl && elements.tarotSearchInputEl) { elements.tarotSearchClearEl.addEventListener("click", () => { elements.tarotSearchInputEl.value = ""; state.searchQuery = ""; applySearchFilter(elements); elements.tarotSearchInputEl.focus(); }); } if (elements.tarotDetailImageEl) { elements.tarotDetailImageEl.addEventListener("click", () => { const src = elements.tarotDetailImageEl.getAttribute("src") || ""; if (!src || elements.tarotDetailImageEl.style.display === "none") { return; } openTarotImageLightbox(src, elements.tarotDetailImageEl.alt || "Tarot card enlarged image"); }); } state.initialized = true; } function selectCardByTrump(trumpNumber) { if (!state.initialized) return; const el = getElements(); const card = state.cards.find(c => c.arcana === "Major" && c.number === trumpNumber); if (!card) return; selectCardById(card.id, el); const listItem = el.tarotCardListEl?.querySelector(`[data-card-id="${card.id}"]`); listItem?.scrollIntoView({ block: "nearest" }); } function selectCardByName(name) { if (!state.initialized) return; const el = getElements(); const needle = normalizeTarotCardLookupName(name); const card = state.cards.find((entry) => normalizeTarotCardLookupName(entry.name) === needle); if (!card) return; selectCardById(card.id, el); el.tarotCardListEl ?.querySelector(`[data-card-id="${card.id}"]`) ?.scrollIntoView({ block: "nearest" }); } window.TarotSectionUi = { ensureTarotSection, selectCardByTrump, selectCardByName, getCards: () => state.cards }; })();