(function () { const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {}; const tarotHouseUi = window.TarotHouseUi || {}; const tarotRelationsUi = window.TarotRelationsUi || {}; const state = { initialized: false, cards: [], filteredCards: [], searchQuery: "", selectedCardId: "", magickDataset: null, referenceData: null, monthRefsByCardId: new Map(), courtCardByDecanId: new Map() }; 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 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) { if (typeof tarotRelationsUi.buildCourtCardByDecanId !== "function") { return new Map(); } return tarotRelationsUi.buildCourtCardByDecanId(cards); } function buildSmallCardCourtLinkRelations(card, relations) { if (typeof tarotRelationsUi.buildSmallCardCourtLinkRelations !== "function") { return []; } return tarotRelationsUi.buildSmallCardCourtLinkRelations(card, relations, state.courtCardByDecanId); } function parseMonthDayToken(value) { if (typeof tarotRelationsUi.parseMonthDayToken !== "function") { return null; } return tarotRelationsUi.parseMonthDayToken(value); } function buildMonthReferencesByCard(referenceData, cards) { if (typeof tarotRelationsUi.buildMonthReferencesByCard !== "function") { return new Map(); } return tarotRelationsUi.buildMonthReferencesByCard(referenceData, cards); } 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 updateHouseSelection(elements) { tarotHouseUi.updateSelection?.(elements); } function renderHouseOfCards(elements) { tarotHouseUi.render?.(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 buildCubeRelationsForCard(card) { if (typeof tarotRelationsUi.buildCubeRelationsForCard !== "function") { return []; } return tarotRelationsUi.buildCubeRelationsForCard(card, state.magickDataset); } // 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}