(function () { function createTarotDetailRenderer(dependencies) { const { getMonthRefsByCardId, getMagickDataset, resolveTarotCardImage, resolveTarotCardThumbnail, getDisplayCardName, buildTypeLabel, clearChildren, normalizeRelationObject, buildElementRelationsForCard, buildTetragrammatonRelationsForCard, buildSmallCardRulershipRelation, buildSmallCardCourtLinkRelations, buildCubeRelationsForCard, buildIChingRelationsForCard, parseMonthDayToken, createRelationListItem, findSephirahForMinorCard } = dependencies || {}; function buildDecanRelationKey(signId, decanIndex) { const normalizedSignId = String(signId || "").trim().toLowerCase(); const normalizedIndex = Number(decanIndex); if (!normalizedSignId || !Number.isFinite(normalizedIndex)) { return ""; } return `${normalizedSignId}-${normalizedIndex}`; } function buildSignRelationKey(signId) { return String(signId || "").trim().toLowerCase(); } function buildDecanSummaryRelations(relations) { const decanRelations = (relations || []).filter((relation) => relation?.type === "decan"); const signWindowRelations = (relations || []).filter((relation) => relation?.type === "signWindow"); if (!decanRelations.length && !signWindowRelations.length) { return []; } function normalizeInlineDateRange(value) { return String(value || "") .replace(/[–—]/g, " - ") .replace(/\s*-\s*/g, " - ") .replace(/\s+/g, " ") .trim(); } const rulerByDecanKey = new Map(); const cardByDecanKey = new Map(); const cardsBySignKey = new Map(); (relations || []).forEach((relation) => { if (relation?.type === "decanRuler") { const key = buildDecanRelationKey(relation?.data?.signId, relation?.data?.decanIndex); if (key && !rulerByDecanKey.has(key)) { rulerByDecanKey.set(key, relation); } } if (relation?.type === "tarotCard") { const decanKey = buildDecanRelationKey(relation?.data?.signId, relation?.data?.decanIndex); if (decanKey && !cardByDecanKey.has(decanKey)) { cardByDecanKey.set(decanKey, relation); return; } const signKey = buildSignRelationKey(relation?.data?.signId); if (!signKey) { return; } const entries = cardsBySignKey.get(signKey) || []; entries.push(relation); cardsBySignKey.set(signKey, entries); } }); const decanSummaries = decanRelations.map((relation) => { const signId = relation?.data?.signId; const signName = String(relation?.data?.signName || "").trim(); const signSymbol = String(relation?.data?.signSymbol || relation?.data?.symbol || "").trim(); const decanIndex = Number(relation?.data?.index); const startDegree = Number(relation?.data?.startDegree); const endDegree = Number(relation?.data?.endDegree); const dateRange = String(relation?.data?.dateRange || "").trim(); const decanKey = buildDecanRelationKey(signId, decanIndex); const rulerRelation = rulerByDecanKey.get(decanKey) || null; const cardRelation = cardByDecanKey.get(decanKey) || null; const rulerSymbol = String(rulerRelation?.data?.symbol || "").trim(); const rulerName = String(rulerRelation?.data?.name || "").trim(); const rulerLabel = `${rulerSymbol} ${rulerName}`.replace(/\s+/g, " ").trim(); const decanCardName = String(cardRelation?.data?.cardName || "").trim(); const decanCardLabel = decanCardName ? String(getDisplayCardName?.(decanCardName) || decanCardName).trim() : ""; const signLabel = `${signSymbol} ${signName}`.replace(/\s+/g, " ").trim(); const degreeLabel = Number.isFinite(startDegree) && Number.isFinite(endDegree) ? `(${startDegree}°-${endDegree}°)` : ""; const dateLabel = normalizeInlineDateRange(dateRange); const summaryParts = [ rulerLabel, decanCardLabel, [signLabel, degreeLabel].filter(Boolean).join(" ").trim(), dateLabel ].filter(Boolean); return { type: decanCardName ? "tarotCard" : "decan", id: cardRelation?.id || `${decanKey}-summary`, label: summaryParts.join(": "), data: { ...(cardRelation?.data || relation?.data || {}), signId, signName, signSymbol, decanIndex, dateRange, cardName: decanCardName || cardRelation?.data?.cardName || "" }, __key: `decanSummary|${decanKey}|${cardRelation?.data?.cardName || ""}` }; }); const signSummaries = signWindowRelations.map((relation) => { const signId = relation?.data?.signId; const signKey = buildSignRelationKey(signId); const signName = String(relation?.data?.signName || "").trim(); const signSymbol = String(relation?.data?.signSymbol || relation?.data?.symbol || "").trim(); const startDegree = Number(relation?.data?.startDegree); const endDegree = Number(relation?.data?.endDegree); const dateRange = String(relation?.data?.dateRange || "").trim(); const cardLabels = [...new Set((cardsBySignKey.get(signKey) || []) .map((entry) => String(getDisplayCardName?.(entry?.data?.cardName) || entry?.data?.cardName || "").trim()) .filter(Boolean))]; const signLabel = `${signSymbol} ${signName}`.replace(/\s+/g, " ").trim(); const degreeLabel = Number.isFinite(startDegree) && Number.isFinite(endDegree) ? `(${startDegree}°-${endDegree}°)` : ""; const dateLabel = normalizeInlineDateRange(dateRange); const summaryParts = [ cardLabels.join(", "), [signLabel, degreeLabel].filter(Boolean).join(" ").trim(), dateLabel ].filter(Boolean); return { type: "signWindow", id: relation?.id || `${signKey}-summary`, label: summaryParts.join(": "), data: { ...(relation?.data || {}), signId, signName, signSymbol, cardNames: cardLabels }, __key: `signSummary|${signKey}` }; }); return [...decanSummaries, ...signSummaries]; } 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 collectDetailRelations(card) { 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 decanSummaryRelations = buildDecanSummaryRelations(dedupedRelations); const hasDecanSummaryRelations = decanSummaryRelations.length > 0; const hasSignWindowRelations = decanSummaryRelations.some((relation) => relation?.type === "signWindow"); const planetRelations = dedupedRelations.filter((relation) => relation.type === "planetCorrespondence" || relation.type === "planet" || (!hasDecanSummaryRelations && relation.type === "decanRuler") ); const zodiacRelations = dedupedRelations.filter((relation) => relation.type === "zodiacCorrespondence" || relation.type === "zodiac" || (!hasDecanSummaryRelations && 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 = hasDecanSummaryRelations ? [...decanSummaryRelations, ...(smallCardRulershipRelation ? [smallCardRulershipRelation] : [])] : [...zodiacRelations, ...(smallCardRulershipRelation ? [smallCardRulershipRelation] : [])]; const smallCardCourtLinkRelations = buildSmallCardCourtLinkRelations(card, dedupedRelations); const mergedCourtDateRelations = [...courtDateRelations, ...smallCardCourtLinkRelations]; const cubeRelations = buildCubeRelationsForCard(card); const iChingRelations = buildIChingRelationsForCard(card); const monthRelations = (getMonthRefsByCardId().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 || "")); }); return { planetRelations, elementRelations, tetragrammatonRelations, zodiacRelationsWithRulership, hasDecanSummaryRelations, hasSignWindowRelations, mergedCourtDateRelations, hebrewRelations, cubeRelations, iChingRelations, mergedMonthRelations }; } function buildCompareDetails(card) { if (!card) { return []; } const detailRelations = collectDetailRelations(card); const groups = [ { title: "Letter", relations: detailRelations.hebrewRelations }, { title: "Planet / Ruler", relations: detailRelations.planetRelations }, { title: detailRelations.hasSignWindowRelations ? "Signs" : (detailRelations.hasDecanSummaryRelations ? "Decans" : "Sign / Decan"), relations: detailRelations.zodiacRelationsWithRulership }, { title: "Element", relations: detailRelations.elementRelations }, { title: "Tetragrammaton", relations: detailRelations.tetragrammatonRelations }, { title: "Dates", relations: detailRelations.mergedCourtDateRelations }, { title: "Calendar", relations: detailRelations.mergedMonthRelations } ]; return groups .map((group) => ({ title: group.title, items: [...new Set((group.relations || []).map((relation) => String(relation?.label || "").trim()).filter(Boolean))] })) .filter((group) => group.items.length); } function renderDetail(card, elements) { if (!card || !elements) { return; } const cardDisplayName = getDisplayCardName(card); const imageUrl = typeof resolveTarotCardThumbnail === "function" ? resolveTarotCardThumbnail(card.name) : (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"; elements.tarotDetailImageEl.decoding = "async"; } 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 detailRelations = collectDetailRelations(card); renderStaticRelationGroup(elements.tarotDetailPlanetEl, elements.tarotMetaPlanetCardEl, detailRelations.planetRelations); renderStaticRelationGroup(elements.tarotDetailElementEl, elements.tarotMetaElementCardEl, detailRelations.elementRelations); renderStaticRelationGroup(elements.tarotDetailTetragrammatonEl, elements.tarotMetaTetragrammatonCardEl, detailRelations.tetragrammatonRelations); renderStaticRelationGroup(elements.tarotDetailZodiacEl, elements.tarotMetaZodiacCardEl, detailRelations.zodiacRelationsWithRulership); renderStaticRelationGroup(elements.tarotDetailCourtDateEl, elements.tarotMetaCourtDateCardEl, detailRelations.mergedCourtDateRelations); renderStaticRelationGroup(elements.tarotDetailHebrewEl, elements.tarotMetaHebrewCardEl, detailRelations.hebrewRelations); renderStaticRelationGroup(elements.tarotDetailCubeEl, elements.tarotMetaCubeCardEl, detailRelations.cubeRelations); renderStaticRelationGroup(elements.tarotDetailIChingEl, elements.tarotMetaIChingCardEl, detailRelations.iChingRelations); renderStaticRelationGroup(elements.tarotDetailCalendarEl, elements.tarotMetaCalendarCardEl, detailRelations.mergedMonthRelations); const kabPathEl = elements.tarotKabPathEl; if (kabPathEl) { const kabTree = getMagickDataset()?.grouped?.kabbalah?.["kabbalah-tree"]; const kabPath = (card.arcana === "Major" && typeof card.number === "number" && kabTree) ? kabTree.paths.find((path) => path.tarot?.trumpNumber === card.number) : null; const kabSeph = !kabPath ? findSephirahForMinorCard(card, kabTree) : null; if (kabPath) { const letter = kabPath.hebrewLetter || {}; const fromName = kabTree.sephiroth.find((seph) => seph.number === kabPath.connects.from)?.name || kabPath.connects.from; const toName = kabTree.sephiroth.find((seph) => seph.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}