import { css, html, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import type { ChromeString } from '../i18n/chrome-strings.js'; import type { CalculateBirthDayResponse, CalculateExpressionResponse, CalculateLifePathResponse, CalculateMaturityResponse, CalculatePersonalDayResponse, CalculatePersonalityResponse, CalculatePersonalMonthResponse, CalculatePersonalYearResponse, CalculateSoulUrgeResponse, GenerateNumerologyChartResponse, GetDailyNumberResponse, } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { disclosureStyles } from '../utils/disclosure.js'; import { formatDate } from '../utils/format.js'; import { type InterpSection, interpAccordionStyles, } from '../utils/interp-accordion.js'; import { humanize } from '../utils/string.js'; /** * Single-number numerology responses that share the number + meaning + calculation + karmic-debt shape. {@link RoxyNumerologyCard.renderNumberCard} renders any of them; the `type` attribute selects only the heading label. */ type NumberCardData = | CalculateLifePathResponse | CalculateExpressionResponse | CalculateSoulUrgeResponse | CalculatePersonalityResponse | CalculateBirthDayResponse | CalculateMaturityResponse; type NumerologyData = | NumberCardData | CalculatePersonalYearResponse | CalculatePersonalDayResponse | CalculatePersonalMonthResponse | GetDailyNumberResponse | GenerateNumerologyChartResponse; /** * The interpretation block a numerology number carries: archetype keywords, the long reading, the strengths and challenges lists, and the three life-area readings. Every core number, the daily number, and the birth-day profile ship this same block (the profile omits spirituality), so one renderer serves them all. Partial, because the birth-day profile is the narrower shape. */ type NumerologyReading = Partial; /** Master numbers are never reduced, and a reader has to see immediately that they are looking at one. */ const MASTER_NUMBERS = new Set([11, 22, 33]); /** The three life-area readings, in the order a numerologist gives them. */ const GUIDANCE_FIELDS: ReadonlyArray<[keyof NumerologyReading, string]> = [ ['career', 'Career'], ['relationships', 'Relationships'], ['spirituality', 'Spirituality'], ]; /** * Numerology card. Renders /numerology/{life-path,expression,soul-urge,personality,birth-day,maturity,daily,personal-day,personal-month,personal-year,chart}. * Use the `type` attribute to switch the heading; the single-number types all share one layout. */ @customElement('roxy-numerology-card') export class RoxyNumerologyCard extends RoxyDataElement { static styles = [ baseStyles, disclosureStyles, interpAccordionStyles, css` .card { background: var(--roxy-surface, #fff); color: var(--roxy-fg, #0a0a0a); border: 1px solid var(--roxy-border, #e4e4e7); border-radius: var(--roxy-radius-md, 8px); padding: var(--roxy-space-lg, 1.5rem); box-shadow: var(--roxy-shadow-sm); display: grid; /* Never an implicit auto column: it floors at min-content, so one long * unbreakable string widens the track past the padded card. */ grid-template-columns: minmax(0, 1fr); gap: var(--roxy-space-md, 1rem); } .hero { display: flex; align-items: center; gap: var(--roxy-space-md, 1rem); } .numeral { font-size: 4rem; line-height: 1; font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-accent-ink, #b45309); font-variant-numeric: tabular-nums; } .label { margin: 0; font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.06em; } .title { margin: 0; font-size: var(--roxy-text-lg, 1.125rem); font-weight: var(--roxy-weight-bold, 600); } .meaning { margin: 0; color: var(--roxy-fg, #0a0a0a); } /* The tint carries the accent; the text stays --roxy-fg, because accent * ink on a tinted chip misses WCAG AA. */ .master { display: inline-block; margin-top: 2px; background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 18%, transparent); color: var(--roxy-fg, #0a0a0a); border-radius: var(--roxy-radius-full, 9999px); padding: 2px 10px; font-size: var(--roxy-text-xs, 0.75rem); font-weight: var(--roxy-weight-bold, 600); } .calc { margin: 0; font-family: var(--roxy-font-mono); font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 30%, transparent); padding: var(--roxy-space-sm, 0.5rem); border-radius: var(--roxy-radius-sm, 4px); white-space: pre-wrap; overflow-wrap: anywhere; } .chips { display: flex; flex-wrap: wrap; gap: var(--roxy-space-xs, 0.25rem); } .chips span { background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent); color: var(--roxy-fg, #0a0a0a); padding: 2px 8px; border-radius: var(--roxy-radius-full, 9999px); font-size: var(--roxy-text-xs, 0.75rem); } .cores { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: var(--roxy-space-sm, 0.5rem); border-top: 1px solid var(--roxy-border, #e4e4e7); padding-top: var(--roxy-space-md, 1rem); } .cores .item { display: grid; gap: 2px; font-size: var(--roxy-text-sm, 0.875rem); } .cores .item .core-head { display: flex; align-items: baseline; gap: var(--roxy-space-xs, 0.25rem); } .cores .item .core-key { color: var(--roxy-muted, #71717a); text-transform: capitalize; } .cores .item strong { color: var(--roxy-accent-ink, #b45309); font-variant-numeric: tabular-nums; font-size: var(--roxy-text-base, 1rem); font-weight: var(--roxy-weight-bold, 600); } .cores .item .core-title { color: var(--roxy-fg, #0a0a0a); font-size: var(--roxy-text-xs, 0.75rem); } .lists { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: var(--roxy-space-md, 1rem); } .lists h3 { margin: 0 0 var(--roxy-space-xs, 0.25rem); font-size: var(--roxy-text-sm, 0.875rem); font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.06em; } .lists ul { margin: 0; padding-left: 1.1rem; display: grid; gap: var(--roxy-space-xs, 0.25rem); font-size: var(--roxy-text-sm, 0.875rem); line-height: 1.6; } .sub { display: grid; gap: var(--roxy-space-sm, 0.5rem); border-top: 1px solid var(--roxy-border, #e4e4e7); padding-top: var(--roxy-space-md, 1rem); } /* Direct child, and a class for the inner title: the accordion and the * strengths/challenges lists render their own h3 one level down, and a bare * element selector here would repaint both. */ .sub > h3 { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.06em; } .sub-title { margin: 0; font-size: var(--roxy-text-base, 1rem); font-weight: var(--roxy-weight-bold, 600); } .sub p { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); line-height: 1.6; } .attrs { display: grid; grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); gap: var(--roxy-space-sm, 0.5rem); margin: 0; } .attr { display: grid; gap: 2px; } .attr dt { font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.05em; } .attr dd { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); font-weight: 500; } .karmic { background: color-mix(in srgb, var(--roxy-warning, #ea580c) 12%, transparent); border: 1px solid color-mix(in srgb, var(--roxy-warning, #ea580c) 32%, transparent); padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem); border-radius: var(--roxy-radius-md, 8px); font-size: var(--roxy-text-sm, 0.875rem); color: var(--roxy-fg, #0a0a0a); } `, ]; @property({ type: String, reflect: true }) type: | 'life-path' | 'expression' | 'soul-urge' | 'personality' | 'birth-day' | 'maturity' | 'daily' | 'personal-day' | 'personal-month' | 'personal-year' | 'chart' = 'life-path'; protected renderData(d: NumerologyData) { const headerLabel = this.t(LABELS[this.type] ?? this.type); if ('coreNumbers' in d) return this.renderChart(d, headerLabel); // Period reads share a number+theme+body shape but differ in field names; // check the most specific key first (a personal-day response also carries // personalMonth/personalYear), so the order is day -> month -> year. if ('personalDay' in d) return this.renderPersonalDay(d, headerLabel); if ('personalMonth' in d) return this.renderPersonalMonth(d, headerLabel); if ('personalYear' in d) return this.renderPersonalYear(d, headerLabel); if ('dailyMessage' in d) return this.renderDailyNumber(d, headerLabel); return this.renderNumberCard(d as NumberCardData, headerLabel); } private renderNumberCard(d: NumberCardData, headerLabel: string) { const readings = !this.hideReadings; return html`
${this.renderHero(headerLabel, d.number, d.meaning?.title, isMaster(d.number, d.type))} ${d.meaning?.description && readings ? html`

${d.meaning.description}

` : nothing} ${d.calculation ? html`
${d.calculation}
` : nothing} ${ // The debt NUMBER is a fact about the chart; the sentences that follow // it are the reading of that fact. d.hasKarmicDebt && d.karmicDebtNumber ? html`
${this.t('Karmic debt')} ${d.karmicDebtNumber}. ${readings ? karmicDebtText(d.karmicDebtMeaning) : ''}
` : nothing } ${this.renderReading(d.meaning, 'numerology-guidance')}
`; } /** The daily number carries the same full interpretation as a core number, plus the message for the day. */ private renderDailyNumber(d: GetDailyNumberResponse, headerLabel: string) { const readings = !this.hideReadings; return html`
${this.renderHero(headerLabel, d.number, d.meaning?.title, isMaster(d.number, d.type))} ${d.dailyMessage && readings ? html`

${d.dailyMessage}

` : nothing} ${d.meaning?.description && readings ? html`

${d.meaning.description}

` : nothing} ${this.renderReading(d.meaning, 'numerology-daily-guidance')}
`; } /** A personal day sits inside its personal month, which sits inside its personal year. Dropping that nesting strips the day of the context that gives it meaning. */ private renderPersonalDay( d: CalculatePersonalDayResponse, headerLabel: string, ) { return html`
${this.renderHero(headerLabel, d.personalDay, d.theme)} ${d.guidance && !this.hideReadings ? html`

${d.guidance}

` : nothing}
${this.attr(this.t('Date'), formatDate(this.effectiveLang(), d.targetDate))} ${this.attr( this.t('Personal month'), joinCycle(d.personalMonth, d.personalMonthTheme), )} ${this.attr( this.t('Personal year'), joinCycle(d.personalYear, d.personalYearTheme), )}
`; } private renderPersonalMonth( d: CalculatePersonalMonthResponse, headerLabel: string, ) { return html`
${this.renderHero(headerLabel, d.personalMonth, d.theme)} ${d.focus && !this.hideReadings ? html`

${d.focus}

` : nothing}
${this.attr(this.t('Calendar month'), d.calendarMonth)} ${this.attr( this.t('Personal year'), joinCycle(d.personalYear, d.personalYearTheme), )}
`; } private renderPersonalYear( d: CalculatePersonalYearResponse, headerLabel: string, ) { const readings = !this.hideReadings; return html`
${this.renderHero(headerLabel, d.personalYear, d.theme)} ${d.cycle ? html`

${d.cycle}

` : nothing} ${d.forecast && readings ? html`

${d.forecast}

` : nothing} ${ readings ? this.renderLists( ['Opportunities', d.opportunities], ['Challenges', d.challenges], ) : nothing } ${d.advice && readings ? html`

${d.advice}

` : nothing}
`; } private renderChart(d: GenerateNumerologyChartResponse, headerLabel: string) { const cores = Object.entries(d.coreNumbers ?? {}).filter(([, v]) => v); const ins = d.additionalInsights; const lucky = d.luckyAssociations; const profile = d.birthDayProfile; const maturity = d.maturityStatus; const readings = !this.hideReadings; return html`

${headerLabel}

${d.profile?.name ? html`

${d.profile.name}

` : nothing} ${d.profile?.birthdate ? html`

${formatDate(this.effectiveLang(), d.profile.birthdate)}

` : nothing}
${d.summary && readings ? html`

${d.summary}

` : nothing} ${ cores.length > 0 ? html`
${cores.map( ([k, v]) => html`
${humanize(k)} ${v.number ?? ''}
${v.meaning?.title ? html`${v.meaning.title}` : nothing} ${isMaster(v.number, v.type) ? html`${this.t('Master')} ${v.number}` : nothing}
`, )}
` : nothing } ${this.renderInterpretation( cores.map(([k, v]) => ({ label: humanize(k), aside: [ v.number != null ? `${v.number}` : '', isMaster(v.number, v.type) ? 'master' : '', v.hasKarmicDebt && v.karmicDebtNumber ? `karmic debt ${v.karmicDebtNumber}` : '', ] .filter(Boolean) .join(' · '), body: v.meaning?.description ?? '', extra: this.renderReading(v.meaning, `numerology-core-${k}`), })), 'numerology-cores', 'Core numbers', )} ${ maturity ? html`
${this.attr(this.t('Maturity'), maturity.isActive ? this.t('Active') : this.t('Not yet active'))} ${this.attr(this.t('Current age'), maturity.currentAge)} ${this.attr(this.t('Activates'), maturity.activationRange)}
` : nothing } ${ profile ? html`

${this.t('Birth day profile')}

${ profile.title ? html`

${[profile.day ? `Day ${profile.day}` : '', profile.title].filter(Boolean).join(' · ')}

` : nothing } ${profile.description && readings ? html`

${profile.description}

` : nothing} ${this.renderReading(profile, 'numerology-birth-day-profile')}
` : nothing } ${ins ? this.renderInsights(ins) : nothing} ${ lucky ? html`

${this.t('Lucky associations')}

${this.attr(this.t('Day'), lucky.day)} ${this.attr(this.t('Element'), lucky.element)} ${this.attr(this.t('Ruling planet'), lucky.rulingPlanet)} ${this.attr(this.t('Colors'), lucky.colors?.join(', '))} ${this.attr(this.t('Gemstones'), lucky.gemstones?.join(', '))} ${this.attr(this.t('Compatible'), lucky.compatibleNumbers?.join(', '))} ${this.attr(this.t('Incompatible'), lucky.incompatibleNumbers?.join(', '))}
` : nothing }
`; } /** Everything the chart carries beyond the six core numbers: the karmic analysis, the current year and month, the four pinnacles and their paired challenges, the name-derived numbers, and the letter analysis. */ private renderInsights( ins: NonNullable, ) { const lessons = ins.karmicLessons; const debt = ins.karmicDebt; const year = ins.personalYear; const passion = ins.hiddenPassion; const subconscious = ins.subconsciousSelf; const letters = ins.nameLetters; const present = Object.entries(lessons?.presentNumbers ?? {}); const readings = !this.hideReadings; return html`${ lessons ? html`

${this.t('Karmic lessons')}

${ lessons.missingNumbers?.length ? html`
${lessons.missingNumbers.map((n) => html`${this.t('Missing')} ${n}`)}
` : html`

${this.t('No numbers are missing from the birth name.')}

` } ${ present.length > 0 ? html`
${present.map(([digit, count]) => html`${digit} x ${count}`)}
` : nothing } ${this.renderInterpretation( (lessons.lessons ?? []).map((l) => ({ label: l.lesson ?? `Lesson ${l.number}`, aside: l.number != null ? `${l.number}` : '', body: l.description ?? '', extra: l.howToOvercome ? html`

${this.t('How to overcome')}. ${l.howToOvercome}

` : nothing, })), 'numerology-karmic-lessons', 'Lessons', )}
` : nothing }${ debt?.hasKarmicDebt ? html`

${this.t('Karmic debt')}

${ debt.debtNumbers?.length ? html`
${debt.debtNumbers.map((n) => html`${this.t('Debt')} ${n}`)}
` : nothing } ${this.renderInterpretation( (debt.meanings ?? []).map((m) => ({ label: `${this.t('Karmic debt')} ${m.number}`, body: m.description ?? '', extra: html`${ m.challenge ? html`

${this.t('Challenge')}. ${m.challenge}

` : nothing }${ m.resolution ? html`

${this.t('Resolution')}. ${m.resolution}

` : nothing }`, })), 'numerology-karmic-debt', 'Debts', )}
` : nothing }${ year ? html`

${this.t('Personal year')}

${[ year.personalYear != null ? `${year.personalYear}` : '', year.theme ?? '', year.cycle ?? '', ] .filter(Boolean) .join(' · ')}

${year.forecast && readings ? html`

${year.forecast}

` : nothing} ${ readings ? this.renderLists( ['Opportunities', year.opportunities], ['Challenges', year.challenges], ) : nothing } ${year.advice && readings ? html`

${year.advice}

` : nothing} ${ // The month and its theme name where the year sits now; `focus` is // the sentence about it. year.personalMonth ? html`

${[ year.personalMonth.personalMonth != null ? `${this.t('Personal month')} ${year.personalMonth.personalMonth}` : this.t('Personal month'), year.personalMonth.theme ?? '', ] .filter(Boolean) .join(' · ')}. ${readings ? (year.personalMonth.focus ?? '') : ''}

` : nothing }
` : nothing }${ // Both period sections are an accordion and nothing else, so hiding the // readings takes the heading with it rather than leaving one over an // empty block. ins.pinnacles?.length && readings ? html`

${this.t('Pinnacles')}

${this.renderInterpretation( ins.pinnacles.map((p) => ({ label: [ p.position != null ? `Pinnacle ${p.position}` : 'Pinnacle', p.meaning?.title ?? '', ] .filter(Boolean) .join(' · '), aside: [ p.number != null ? `${p.number}` : '', ageRange(p.startAge, p.endAge), ] .filter(Boolean) .join(' · '), body: p.meaning?.description ?? '', extra: this.renderLists( ['Opportunities', p.meaning?.opportunities], ['Challenges', p.meaning?.challenges], ), })), 'numerology-pinnacles', 'Life phases', )}
` : nothing }${ ins.challenges?.length && readings ? html`

${this.t('Challenges')}

${this.renderInterpretation( ins.challenges.map((c) => ({ label: [ c.position != null ? `Challenge ${c.position}` : 'Challenge', c.meaning?.title ?? '', ] .filter(Boolean) .join(' · '), aside: [ c.number != null ? `${c.number}` : '', ageRange(c.startAge, c.endAge), ] .filter(Boolean) .join(' · '), body: c.meaning?.description ?? '', extra: html`${ c.meaning?.lesson ? html`

${this.t('Lesson')}. ${c.meaning.lesson}

` : nothing }${ c.meaning?.howToOvercome ? html`

${this.t('How to overcome')}. ${c.meaning.howToOvercome}

` : nothing }`, })), 'numerology-challenges', 'Obstacle periods', )}
` : nothing }${ passion || subconscious ? html`

${this.t('Name numbers')}

${ passion ? html`

${[ passion.number != null ? `Hidden passion ${passion.number}` : 'Hidden passion', passion.title ?? '', ] .filter(Boolean) .join(' · ')}

${passion.description && readings ? html`

${passion.description}

` : nothing} ${ typeof passion.count === 'number' ? html`

${`Appears ${passion.count} times in the name`}

` : nothing }
` : nothing } ${ subconscious ? html`

${[ subconscious.number != null ? `Subconscious self ${subconscious.number}` : 'Subconscious self', subconscious.title ?? '', ] .filter(Boolean) .join(' · ')}

${subconscious.description && readings ? html`

${subconscious.description}

` : nothing} ${ subconscious.uniqueNumbers?.length ? html`

${`Numbers present: ${subconscious.uniqueNumbers.join(', ')}`}

` : nothing }
` : nothing }
` : nothing }${ letters && readings ? html`

${this.t('Name letters')}

${this.renderInterpretation( [ { label: this.t('Cornerstone'), aside: letterAside( letters.cornerstone?.letter, letters.cornerstone?.number, ), body: letters.cornerstone?.meaning ?? '', }, { label: this.t('Capstone'), aside: letterAside( letters.capstone?.letter, letters.capstone?.number, ), body: letters.capstone?.meaning ?? '', }, { label: this.t('First vowel'), aside: letterAside(letters.firstVowel?.letter), body: letters.firstVowel?.meaning ?? '', }, ], 'numerology-name-letters', 'Letter analysis', )}
` : nothing }`; } /** Hero row shared by every single-number view: the numeral, the label, the archetype, and the master-number badge when the number is 11, 22, or 33. */ private renderHero( headerLabel: string, num: number | undefined, title: string | undefined, master = false, ) { return html`
${typeof num === 'number' ? html`
${num}
` : nothing}

${headerLabel}

${title ? html`

${title}

` : nothing} ${master ? html`${this.t('Master number')}` : nothing}
`; } /** * Keyword chips, the strengths and challenges lists, and the three life-area readings behind a disclosure. The whole interpretation, laid out as a reading. * * Every part of it is prose or the shorthand of prose, so `hide-readings` drops the block whole rather than gating three pieces. */ private renderReading(r: NumerologyReading | undefined, name: string) { if (!r || this.hideReadings) return nothing; const sections: InterpSection[] = GUIDANCE_FIELDS.map(([key, label]) => ({ label, body: (r[key] as string | undefined) ?? '', })); return html`${ r.keywords?.length ? html`
${r.keywords.map((k) => html`${k}`)}
` : nothing }${this.renderLists(['Strengths', r.strengths], ['Challenges', r.challenges])}${this.renderInterpretation(sections, name, 'Guidance')}`; } /** * Side-by-side prose lists. Each entry names a trait and explains it, so they are bulleted paragraphs, never chips. * * @remarks * `h3`, not `h4`. This renders at the TOP level of the single-number card, where the only heading above it is the `h2` title, so an `h4` skips a level and fails the axe `heading-order` rule. Inside a chart section or an accordion body the heading above is already an `h3`, and a repeated level is fine: the rule flags skips, not repeats. */ private renderLists( ...lists: Array<[ChromeString, ReadonlyArray | undefined]> ) { const shown = lists.filter(([, items]) => items && items.length > 0); if (shown.length === 0) return nothing; return html`
${shown.map( ([label, items]) => html`

${this.t(label)}

    ${(items ?? []).map((item) => html`
  • ${item}
  • `)}
`, )}
`; } private attr(label: string, value: string | number | undefined) { if (value === undefined || value === null || value === '') return nothing; return html`
${label}
${value}
`; } } const LABELS: Record = { 'life-path': 'Life Path', expression: 'Expression', 'soul-urge': 'Soul Urge', personality: 'Personality', 'birth-day': 'Birth Day', maturity: 'Maturity', daily: 'Daily Number', 'personal-day': 'Personal Day', 'personal-month': 'Personal month', 'personal-year': 'Personal year', chart: 'Numerology chart', }; type KarmicDebtMeaning = CalculateLifePathResponse['karmicDebtMeaning']; function karmicDebtText(value: KarmicDebtMeaning | undefined): string { if (!value) return ''; return [value.description, value.challenge, value.resolution] .filter(Boolean) .join(' '); } /** A number is master when the API says so, or when it is one of the three that are never reduced. Both checks, because the period cycles carry the number without the `type` flag. */ function isMaster(num: number | undefined, type?: string): boolean { return type === 'master' || (num !== undefined && MASTER_NUMBERS.has(num)); } /** "5 · Freedom and change", the cycle a smaller cycle sits inside. Empty when the API sent neither half. */ function joinCycle(num: number | undefined, theme: string | undefined): string { return [num != null ? `${num}` : '', theme ?? ''].filter(Boolean).join(' · '); } /** "ages 27 to 35", or "ages 54 onward" for the final phase, which the API closes with a null end age. */ function ageRange( start: number | null | undefined, end: number | null | undefined, ): string { if (start == null) return ''; return end == null ? `ages ${start} onward` : `ages ${start} to ${end}`; } /** "A · 1" for a letter that carries a Pythagorean value, "A" for the first vowel, which does not. */ function letterAside(letter?: string, num?: number): string { return [letter ?? '', num != null ? `${num}` : ''] .filter(Boolean) .join(' · '); } declare global { interface HTMLElementTagNameMap { 'roxy-numerology-card': RoxyNumerologyCard; } }