/* ============================================================
   data.jsx — 사주 명리 계산 · 페르소나 · 운세 카피
   ============================================================ */

const ORRERY_SAJU_MODULE = import("@orrery/core/saju").then((m) => (window.__orreryCalculateSaju = m.calculateSaju, m)).catch(() => null);

/* ---- 천간(天干) ---- */
const GAN = [
  { h: "甲", k: "갑", el: "목", yin: false },
  { h: "乙", k: "을", el: "목", yin: true  },
  { h: "丙", k: "병", el: "화", yin: false },
  { h: "丁", k: "정", el: "화", yin: true  },
  { h: "戊", k: "무", el: "토", yin: false },
  { h: "己", k: "기", el: "토", yin: true  },
  { h: "庚", k: "경", el: "금", yin: false },
  { h: "辛", k: "신", el: "금", yin: true  },
  { h: "壬", k: "임", el: "수", yin: false },
  { h: "癸", k: "계", el: "수", yin: true  },
];

/* ---- 지지(地支) + 띠 ---- */
const ZHI = [
  { h: "子", k: "자", an: "쥐",     el: "수", emoji: "🐭" },
  { h: "丑", k: "축", an: "소",     el: "토", emoji: "🐮" },
  { h: "寅", k: "인", an: "호랑이", el: "목", emoji: "🐯" },
  { h: "卯", k: "묘", an: "토끼",   el: "목", emoji: "🐰" },
  { h: "辰", k: "진", an: "용",     el: "토", emoji: "🐲" },
  { h: "巳", k: "사", an: "뱀",     el: "화", emoji: "🐍" },
  { h: "午", k: "오", an: "말",     el: "화", emoji: "🐴" },
  { h: "未", k: "미", an: "양",     el: "토", emoji: "🐑" },
  { h: "申", k: "신", an: "원숭이", el: "금", emoji: "🐵" },
  { h: "酉", k: "유", an: "닭",     el: "금", emoji: "🐔" },
  { h: "戌", k: "술", an: "개",     el: "토", emoji: "🐶" },
  { h: "亥", k: "해", an: "돼지",   el: "수", emoji: "🐷" },
];

/* ---- 오행(五行) 메타 ---- */
const ELEMENTS = {
  "목": { name: "목(木)", color: "#2E9D62", aura: "성장하는 나무", love: "다정하고 헌신적인", focus: "성장과 확장",
          trait: "곁의 사람과 일을 보살피고 키워내는 힘이 강합니다. 꾸준히 배우고 넓혀갈수록 운의 결이 좋아져요.",
          career: "기획, 교육, 콘텐츠, 브랜딩처럼 가능성을 발굴하고 키우는 일에 강점이 있습니다.",
          wealth: "단기 수익보다 장기적으로 자라는 자산과 경험에 투자할 때 안정감이 커집니다.",
          health: "생각이 많아지면 몸도 긴장하기 쉬우니 산책과 수면 리듬을 일정하게 두는 게 좋아요." },
  "화": { name: "화(火)", color: "#E64C3C", aura: "타오르는 불꽃", love: "열정적이고 표현이 분명한", focus: "표현과 추진력",
          trait: "감정과 의지가 선명하고 존재감이 큰 타입입니다. 속도를 조절하면 타고난 추진력이 오래 갑니다.",
          career: "영업, 발표, 예술, 리더십처럼 사람 앞에서 에너지를 전달하는 역할에 빛이 납니다.",
          wealth: "기회 포착은 빠르지만 충동 지출도 빠를 수 있어 기준선을 먼저 정하는 편이 유리합니다.",
          health: "과열되면 피로가 한 번에 몰릴 수 있으니 휴식과 수분 보충을 의식적으로 챙겨야 합니다." },
  "토": { name: "토(土)", color: "#D4AC0D", aura: "품어주는 대지", love: "안정과 신뢰를 주는", focus: "안정과 축적",
          trait: "사람과 상황을 묵직하게 받쳐주는 힘이 있습니다. 천천히 쌓은 신뢰가 가장 큰 자산이 됩니다.",
          career: "운영, 관리, 부동산, 재무, 조직 조율처럼 기반을 다지고 지키는 일에 잘 맞습니다.",
          wealth: "크게 흔들리는 선택보다 꾸준히 모으고 관리하는 방식에서 재물운이 단단해집니다.",
          health: "스트레스를 속으로 눌러두기 쉬워 소화와 순환 리듬을 편안하게 만드는 습관이 필요합니다." },
  "금": { name: "금(金)", color: "#7F8C8D", aura: "벼려진 금속", love: "이성적이고 의리 있는", focus: "기준과 완성도",
          trait: "기준이 분명하고 약속을 지키는 힘이 강합니다. 날카로운 판단에 여유를 더하면 평판이 좋아집니다.",
          career: "법무, 분석, 개발, 디자인, 품질관리처럼 정확도와 완성도가 중요한 분야에 강합니다.",
          wealth: "불필요한 손실을 줄이는 감각이 좋습니다. 원칙 있는 분산과 기록이 재물운을 지켜줍니다.",
          health: "긴장과 완벽주의가 몸을 굳게 만들 수 있어 호흡, 스트레칭, 규칙적인 휴식이 중요합니다." },
  "수": { name: "수(水)", color: "#1A252F", aura: "흐르는 물", love: "섬세하고 공감 깊은", focus: "지혜와 유연함",
          trait: "상황을 읽고 흐름에 맞추는 감각이 좋습니다. 깊이 있는 관찰력이 삶의 방향을 열어줍니다.",
          career: "연구, 상담, 데이터, 글쓰기, 전략처럼 정보를 읽고 해석하는 일에서 강점이 드러납니다.",
          wealth: "흐름을 보는 눈은 좋지만 결정을 미루기 쉬워 작은 실행 기준을 정해두면 재물운이 살아납니다.",
          health: "감정의 기복이 컨디션에 영향을 주기 쉬우니 몸을 따뜻하게 하고 생활 리듬을 안정시키세요." },
};

/* 오행 상생/상극 — 궁합 계산용 */
const SHENG = { "목":"화", "화":"토", "토":"금", "금":"수", "수":"목" }; // 상생
const KE    = { "목":"토", "토":"수", "수":"화", "화":"금", "금":"목" }; // 상극

function mod(n, m) {
  return ((n % m) + m) % m;
}

function gapjaIndex(g, z) {
  return { gan: GAN[mod(g, 10)], zhi: ZHI[mod(z, 12)] };
}

/* ---- 정밀 사주 계산 보정 상수/함수 ---- */
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
const DAY_MS = 86400000;
const DEG = Math.PI / 180;
const SOLAR_TERM_CACHE = new Map();

const SOLAR_TERMS_24 = [
  { key: "sohan",     name: "소한", month: 1,  day: 5,  angle: 285, type: "jie", monthNo: 12 },
  { key: "daehan",    name: "대한", month: 1,  day: 20, angle: 300, type: "qi" },
  { key: "ipchun",    name: "입춘", month: 2,  day: 4,  angle: 315, type: "jie", monthNo: 1  },
  { key: "usu",       name: "우수", month: 2,  day: 19, angle: 330, type: "qi" },
  { key: "gyeongchip",name: "경칩", month: 3,  day: 6,  angle: 345, type: "jie", monthNo: 2  },
  { key: "chunbun",   name: "춘분", month: 3,  day: 21, angle: 0,   type: "qi" },
  { key: "cheongmyeong", name: "청명", month: 4, day: 5, angle: 15, type: "jie", monthNo: 3 },
  { key: "gogu",      name: "곡우", month: 4,  day: 20, angle: 30,  type: "qi" },
  { key: "ipha",      name: "입하", month: 5,  day: 6,  angle: 45,  type: "jie", monthNo: 4  },
  { key: "soman",     name: "소만", month: 5,  day: 21, angle: 60,  type: "qi" },
  { key: "mangjong",  name: "망종", month: 6,  day: 6,  angle: 75,  type: "jie", monthNo: 5  },
  { key: "haji",      name: "하지", month: 6,  day: 21, angle: 90,  type: "qi" },
  { key: "soseo",     name: "소서", month: 7,  day: 7,  angle: 105, type: "jie", monthNo: 6  },
  { key: "daeseo",    name: "대서", month: 7,  day: 23, angle: 120, type: "qi" },
  { key: "ipchu",     name: "입추", month: 8,  day: 8,  angle: 135, type: "jie", monthNo: 7  },
  { key: "cheoseo",   name: "처서", month: 8,  day: 23, angle: 150, type: "qi" },
  { key: "baengno",   name: "백로", month: 9,  day: 8,  angle: 165, type: "jie", monthNo: 8  },
  { key: "chubun",    name: "추분", month: 9,  day: 23, angle: 180, type: "qi" },
  { key: "hallo",     name: "한로", month: 10, day: 8,  angle: 195, type: "jie", monthNo: 9  },
  { key: "sanggang",  name: "상강", month: 10, day: 23, angle: 210, type: "qi" },
  { key: "ipdong",    name: "입동", month: 11, day: 7,  angle: 225, type: "jie", monthNo: 10 },
  { key: "soseol",    name: "소설", month: 11, day: 22, angle: 240, type: "qi" },
  { key: "daeseol",   name: "대설", month: 12, day: 7,  angle: 255, type: "jie", monthNo: 11 },
  { key: "dongji",    name: "동지", month: 12, day: 22, angle: 270, type: "qi" },
];

// 월주는 24절기 중 12절(節) 절입 기준: 입춘부터 寅월, 이후 매 30도마다 다음 월.
const MONTH_JIE_TERMS = SOLAR_TERMS_24.filter((term) => term.type === "jie");
const IPCHUN_TERM = SOLAR_TERMS_24.find((term) => term.key === "ipchun");

const KOREAN_LUNAR_BASE_YEAR = 1930;
const KOREAN_LUNAR_BASE_SOLAR = { y: 1930, m: 1, d: 30 }; // 음력 1930-01-01
const KOREAN_LUNAR_DATA = [
  0x82fe6656, 0x82c40e4a, 0xc2c60ea5, 0x830156a9, 0x82c605b5, 0x82c402b6, 0xc30138ae, 0x82c4092e,
  0x83017c8d, 0x82c40c95, 0xc2c40d4a, 0x83016d8a, 0x82c60b69, 0x82c6056d, 0xc301425b, 0x82c4025d,
  0x82c4092d, 0x83002d2b, 0xc2c40a95, 0x83007d55, 0x82c40b4a, 0x82c60b55, 0xc3015555, 0x82c604db,
  0x82c4025b, 0x83013857, 0xc2c4052b, 0x83008a9b, 0x82c40695, 0x82c406aa, 0xc3006aea, 0x82c60ab5,
  0x82c404b6, 0x83004aae, 0x82c60a57, 0x82c40527, 0x82fe3726, 0x82c60d95, 0xc30076b5, 0x82c4056a,
  0x82c609ad, 0x830054dd, 0xc2c404ae, 0x82c40a4e, 0x83004d4d, 0x82c40d25, 0xc3008d59, 0x82c40b54,
  0x82c60d6a, 0x8301695a, 0xc2c6095b, 0x82c4049b, 0x83004a9b, 0x82c40a4b, 0xc300ab27, 0x82c406a5,
  0x82c406d4, 0x83026b75, 0xc2c402b6, 0x82c6095b, 0x830054b7, 0x82c40497, 0xc2c4064b, 0x82fe374a,
  0x82c60ea5, 0x830086d9, 0xc2c605ad, 0x82c402b6, 0x8300596e, 0x82c4092e, 0xc2c40c96, 0x83004e95,
  0x82c40d4a, 0x82c60da5, 0xc3002755, 0x82c4056c, 0x83027abb, 0x82c4025d, 0xc2c4092d, 0x83005cab,
  0x82c40a95, 0x82c40b4a, 0xc3013b4a, 0x82c60b55, 0x8300955d, 0x82c404ba, 0xc2c60a5b, 0x83005557,
  0x82c4052b, 0x82c40a95, 0xc3004b95, 0x82c406aa, 0x82c60ad5, 0x830026b5, 0xc2c404b6, 0x83006a6e,
];

const KOREA_DST_PERIODS = [
  [1948, 6, 1, 0, 0, 1948, 9, 13, 0, 0],
  [1949, 4, 3, 0, 0, 1949, 9, 11, 0, 0],
  [1950, 4, 1, 0, 0, 1950, 9, 10, 0, 0],
  [1951, 5, 6, 0, 0, 1951, 9, 9, 0, 0],
  [1955, 5, 5, 0, 0, 1955, 9, 9, 0, 0],
  [1956, 5, 20, 0, 0, 1956, 9, 30, 0, 0],
  [1957, 5, 5, 0, 0, 1957, 9, 22, 0, 0],
  [1958, 5, 4, 0, 0, 1958, 9, 21, 0, 0],
  [1959, 5, 3, 0, 0, 1959, 9, 20, 0, 0],
  [1960, 5, 1, 0, 0, 1960, 9, 18, 0, 0],
  [1987, 5, 10, 2, 0, 1987, 10, 11, 3, 0],
  [1988, 5, 8, 2, 0, 1988, 10, 9, 3, 0],
];

function julianDayFromUtc(ts) {
  return ts / DAY_MS + 2440587.5;
}

function gregorianJdn(y, m, d) {
  const a = Math.floor((14 - m) / 12);
  const yy = y + 4800 - a;
  const mm = m + 12 * a - 3;
  return d + Math.floor((153 * mm + 2) / 5) + 365 * yy + Math.floor(yy / 4) - Math.floor(yy / 100) + Math.floor(yy / 400) - 32045;
}

function kstDatePartsFromUtc(ts) {
  const dt = new Date(ts + KST_OFFSET_MS);
  return { y: dt.getUTCFullYear(), m: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
}

function kstToUtcTs(y, m, d, hour = 12, minute = 0) {
  return Date.UTC(y, m - 1, d, hour, minute) - KST_OFFSET_MS;
}

function utcDateParts(ts) {
  const dt = new Date(ts);
  return {
    y: dt.getUTCFullYear(),
    m: dt.getUTCMonth() + 1,
    d: dt.getUTCDate(),
    hour: dt.getUTCHours(),
    minute: dt.getUTCMinutes(),
  };
}

function sunLongitude(ts) {
  const jd = julianDayFromUtc(ts);
  const t = (jd - 2451545.0) / 36525;
  const l0 = mod(280.46646 + 36000.76983 * t + 0.0003032 * t * t, 360);
  const m = mod(357.52911 + 35999.05029 * t - 0.0001537 * t * t, 360);
  const c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * Math.sin(m * DEG)
    + (0.019993 - 0.000101 * t) * Math.sin(2 * m * DEG)
    + 0.000289 * Math.sin(3 * m * DEG);
  const omega = 125.04 - 1934.136 * t;
  return mod(l0 + c - 0.00569 - 0.00478 * Math.sin(omega * DEG), 360);
}

function angleDelta(current, target) {
  return mod(current - target + 540, 360) - 180;
}

function solarTermUtc(year, term) {
  const cacheKey = `${year}:${term.key}`;
  if (SOLAR_TERM_CACHE.has(cacheKey)) return SOLAR_TERM_CACHE.get(cacheKey);

  let lo = kstToUtcTs(year, term.month, term.day - 3, 0);
  let hi = kstToUtcTs(year, term.month, term.day + 3, 23);
  for (let i = 0; i < 48; i++) {
    const mid = (lo + hi) / 2;
    if (angleDelta(sunLongitude(mid), term.angle) >= 0) hi = mid;
    else lo = mid;
  }
  SOLAR_TERM_CACHE.set(cacheKey, hi);
  return hi;
}

function monthTermAt(birthTs) {
  let matched = null;
  const baseYear = kstDatePartsFromUtc(birthTs).y;
  for (const year of [baseYear - 1, baseYear, baseYear + 1]) {
    for (const term of MONTH_JIE_TERMS) {
      const at = solarTermUtc(year, term);
      if (at <= birthTs && (!matched || at > matched.at)) matched = { ...term, at, year };
    }
  }
  return matched;
}

function sajuYearForBirth(birthTs) {
  const y = kstDatePartsFromUtc(birthTs).y;
  return birthTs < solarTermUtc(y, IPCHUN_TERM) ? y - 1 : y;
}

function dayIndexFromLocalDate(y, m, d) {
  // 1900-01-31 is 甲辰. JDN arithmetic avoids Date timezone and leap-year drift.
  const days = gregorianJdn(y, m, d) - gregorianJdn(1900, 1, 31);
  return { gan: mod(days, 10), zhi: mod(days + 4, 12) };
}

function lunarData(year) {
  return KOREAN_LUNAR_DATA[year - KOREAN_LUNAR_BASE_YEAR];
}

function lunarLeapMonth(year) {
  return (lunarData(year) >> 12) & 0x0f;
}

function lunarYearDays(year) {
  return (lunarData(year) >> 17) & 0x01ff;
}

function lunarMonthDays(year, month, isLeapLunar = false) {
  const data = lunarData(year);
  if (!data) return 0;
  if (isLeapLunar && lunarLeapMonth(year) === month) {
    return ((data >> 16) & 0x01) ? 30 : 29;
  }
  return ((data >> (12 - month)) & 0x01) ? 30 : 29;
}

function lunarToSolarDate(y, m, d, isLeapLunar = false) {
  if (y < KOREAN_LUNAR_BASE_YEAR || y >= KOREAN_LUNAR_BASE_YEAR + KOREAN_LUNAR_DATA.length) return null;
  if (m < 1 || m > 12) return null;
  const leapMonth = lunarLeapMonth(y);
  if (isLeapLunar && leapMonth !== m) return null;
  const maxDay = lunarMonthDays(y, m, !!isLeapLunar);
  if (d < 1 || d > maxDay) return null;

  let days = d - 1;
  for (let year = KOREAN_LUNAR_BASE_YEAR; year < y; year++) days += lunarYearDays(year);
  for (let month = 1; month < m; month++) {
    days += lunarMonthDays(y, month, false);
    if (lunarLeapMonth(y) === month) days += lunarMonthDays(y, month, true);
  }
  if (isLeapLunar) days += lunarMonthDays(y, m, false);

  const jdn = gregorianJdn(KOREAN_LUNAR_BASE_SOLAR.y, KOREAN_LUNAR_BASE_SOLAR.m, KOREAN_LUNAR_BASE_SOLAR.d) + days;
  return jdnToGregorian(jdn);
}

function jdnToGregorian(jdn) {
  const a = jdn + 32044;
  const b = Math.floor((4 * a + 3) / 146097);
  const c = a - Math.floor((146097 * b) / 4);
  const d = Math.floor((4 * c + 3) / 1461);
  const e = c - Math.floor((1461 * d) / 4);
  const mm = Math.floor((5 * e + 2) / 153);
  const day = e - Math.floor((153 * mm + 2) / 5) + 1;
  const month = mm + 3 - 12 * Math.floor(mm / 10);
  const year = 100 * b + d - 4800 + Math.floor(mm / 10);
  return { y: year, m: month, d: day };
}

function isKoreanDst(y, m, d, hour, minute) {
  const ts = Date.UTC(y, m - 1, d, hour, minute);
  return KOREA_DST_PERIODS.some(([sy, sm, sd, sh, sn, ey, em, ed, eh, en]) => (
    ts >= Date.UTC(sy, sm - 1, sd, sh, sn) && ts < Date.UTC(ey, em - 1, ed, eh, en)
  ));
}

function normalizeBirthInput(b) {
  const inputY = +b.y, inputM = +b.m, inputD = +b.d;
  const solar = b.isLunar ? lunarToSolarDate(inputY, inputM, inputD, !!b.isLeapLunar) : { y: inputY, m: inputM, d: inputD };
  if (!solar) throw new Error("지원 범위를 벗어난 음력 날짜입니다.");

  const hasHour = b.hour !== null && b.hour !== "" && b.hour !== undefined;
  if (!hasHour) {
    return { ...solar, hasHour: false, hour: 12, minute: 0, birthTs: kstToUtcTs(solar.y, solar.m, solar.d, 12, 0) };
  }

  const inputHour = mod(Math.trunc(+b.hour), 24);
  const inputMinute = b.minute !== undefined && b.minute !== null && b.minute !== "" ? Math.max(0, Math.min(59, Math.trunc(+b.minute))) : 0;
  const correctionMinutes = 30 + (isKoreanDst(solar.y, solar.m, solar.d, inputHour, inputMinute) ? 60 : 0);
  const corrected = utcDateParts(Date.UTC(solar.y, solar.m - 1, solar.d, inputHour, inputMinute) - correctionMinutes * 60000);

  return {
    ...corrected,
    hasHour: true,
    birthTs: kstToUtcTs(corrected.y, corrected.m, corrected.d, corrected.hour, corrected.minute),
  };
}

/* ---- 상용 만세력 확장 데이터: 원국 메타/신살/리포트 ---- */
const ZHI_HIDDEN_STEMS = {
  "子": ["癸"],
  "丑": ["己", "癸", "辛"],
  "寅": ["甲", "丙", "戊"],
  "卯": ["乙"],
  "辰": ["戊", "乙", "癸"],
  "巳": ["丙", "戊", "庚"],
  "午": ["丁", "己"],
  "未": ["己", "丁", "乙"],
  "申": ["庚", "壬", "戊"],
  "酉": ["辛"],
  "戌": ["戊", "辛", "丁"],
  "亥": ["壬", "甲"],
};

const TWELVE_STAGE_BY_DAY_GAN = {
  "甲": ["목욕", "관대", "건록", "제왕", "쇠", "병", "사", "묘", "절", "태", "양", "장생"],
  "乙": ["병", "쇠", "제왕", "건록", "관대", "목욕", "장생", "양", "태", "절", "묘", "사"],
  "丙": ["태", "양", "장생", "목욕", "관대", "건록", "제왕", "쇠", "병", "사", "묘", "절"],
  "丁": ["절", "묘", "사", "병", "쇠", "제왕", "건록", "관대", "목욕", "장생", "양", "태"],
  "戊": ["태", "양", "장생", "목욕", "관대", "건록", "제왕", "쇠", "병", "사", "묘", "절"],
  "己": ["절", "묘", "사", "병", "쇠", "제왕", "건록", "관대", "목욕", "장생", "양", "태"],
  "庚": ["사", "묘", "절", "태", "양", "장생", "목욕", "관대", "건록", "제왕", "쇠", "병"],
  "辛": ["장생", "양", "태", "절", "묘", "사", "병", "쇠", "제왕", "건록", "관대", "목욕"],
  "壬": ["제왕", "쇠", "병", "사", "묘", "절", "태", "양", "장생", "목욕", "관대", "건록"],
  "癸": ["건록", "관대", "목욕", "장생", "양", "태", "절", "묘", "사", "병", "쇠", "제왕"],
};

const NAEUM_ELEMENTS = [
  "해중금", "해중금", "노중화", "노중화", "대림목", "대림목", "노방토", "노방토", "검봉금", "검봉금",
  "산두화", "산두화", "간하수", "간하수", "성두토", "성두토", "백랍금", "백랍금", "양류목", "양류목",
  "천중수", "천중수", "옥상토", "옥상토", "벽력화", "벽력화", "송백목", "송백목", "장류수", "장류수",
  "사중금", "사중금", "산하화", "산하화", "평지목", "평지목", "벽상토", "벽상토", "금박금", "금박금",
  "복등화", "복등화", "천하수", "천하수", "대역토", "대역토", "차천금", "차천금", "상자목", "상자목",
  "대계수", "대계수", "사중토", "사중토", "천상화", "천상화", "석류목", "석류목", "대해수", "대해수",
];

const NAEUM_TO_ELEMENT = {
  "해중금": "금", "노중화": "화", "대림목": "목", "노방토": "토", "검봉금": "금",
  "산두화": "화", "간하수": "수", "성두토": "토", "백랍금": "금", "양류목": "목",
  "천중수": "수", "옥상토": "토", "벽력화": "화", "송백목": "목", "장류수": "수",
  "사중금": "금", "산하화": "화", "평지목": "목", "벽상토": "토", "금박금": "금",
  "복등화": "화", "천하수": "수", "대역토": "토", "차천금": "금", "상자목": "목",
  "대계수": "수", "사중토": "토", "천상화": "화", "석류목": "목", "대해수": "수",
};

const GEONROK_BRANCH = {
  "甲": "寅", "乙": "卯", "丙": "巳", "丁": "午", "戊": "巳",
  "己": "午", "庚": "申", "辛": "酉", "壬": "亥", "癸": "子",
};
const BAEKHO_GAPJA = new Set(["甲辰", "乙未", "丙戌", "丁丑", "戊辰", "壬戌", "癸丑"]);
const GWIMUN_PAIRS = [["子", "酉"], ["丑", "午"], ["寅", "未"], ["卯", "申"], ["辰", "亥"], ["巳", "戌"]];
const WONJIN_PAIRS = [["子", "未"], ["丑", "午"], ["寅", "酉"], ["卯", "申"], ["辰", "亥"], ["巳", "戌"]];
const FLOWER_BRANCH = {
  "申": "酉", "子": "酉", "辰": "酉",
  "寅": "卯", "午": "卯", "戌": "卯",
  "亥": "子", "卯": "子", "未": "子",
  "巳": "午", "酉": "午", "丑": "午",
};
const HWAGAE_BRANCH = {
  "申": "辰", "子": "辰", "辰": "辰",
  "寅": "戌", "午": "戌", "戌": "戌",
  "亥": "未", "卯": "未", "未": "未",
  "巳": "丑", "酉": "丑", "丑": "丑",
};

function gapjaSerial(ganIdx, zhiIdx) {
  for (let i = 0; i < 60; i++) {
    if (i % 10 === mod(ganIdx, 10) && i % 12 === mod(zhiIdx, 12)) return i;
  }
  return 0;
}

function buildPillar(label, ganIdx, zhiIdx, dayGanH) {
  const pillar = gapjaIndex(ganIdx, zhiIdx);
  const serial = gapjaSerial(ganIdx, zhiIdx);
  const hiddenStems = (ZHI_HIDDEN_STEMS[pillar.zhi.h] || []).map((h) => GAN.find((g) => g.h === h)).filter(Boolean);
  const twelveStage = TWELVE_STAGE_BY_DAY_GAN[dayGanH]?.[zhiIdx] || "";
  const naeum = NAEUM_ELEMENTS[serial] || "";
  return {
    ...pillar,
    label,
    gapja: `${pillar.gan.h}${pillar.zhi.h}`,
    meta: {
      hiddenStems,
      twelveStage,
      naeum,
      naeumElement: NAEUM_TO_ELEMENT[naeum] || "",
      serial,
    },
  };
}

function buildOriginalChart(pillars) {
  return [
    { key: "year", label: "연주", pillar: pillars.year },
    { key: "month", label: "월주", pillar: pillars.month },
    { key: "day", label: "일주", pillar: pillars.day },
    { key: "hour", label: "시주", pillar: pillars.hour },
  ].filter((item) => item.pillar);
}

function elementStatsFromChart(chart) {
  const counts = { "목": 0, "화": 0, "토": 0, "금": 0, "수": 0 };
  chart.forEach(({ pillar }) => {
    counts[pillar.gan.el] += 1;
    counts[pillar.zhi.el] += 1;
  });
  const values = Object.values(counts);
  const max = Math.max(...values);
  const strong = Object.keys(counts).filter((el) => counts[el] === max && max > 0);
  const weak = Object.keys(counts).filter((el) => counts[el] === 0);
  return {
    counts,
    totalLetters: values.reduce((sum, n) => sum + n, 0),
    strong,
    weak,
    strongLabel: strong.length ? `💪 강한 오행: ${strong.join(", ")}` : "",
    weakLabel: weak.length ? `💨 약한 오행: ${weak.join(", ")}` : "",
  };
}

function hasPair(branches, pair) {
  return branches.includes(pair[0]) && branches.includes(pair[1]);
}

function detectShinsal(chart, dayGanH, yearBranch, dayBranch) {
  const branches = chart.map(({ pillar }) => pillar.zhi.h);
  const gapjas = chart.map(({ pillar }) => pillar.gapja);
  const geonrokBranch = GEONROK_BRANCH[dayGanH];
  const sources = [yearBranch, dayBranch].filter(Boolean);
  const dohwaTargets = [...new Set(sources.map((z) => FLOWER_BRANCH[z]).filter(Boolean))];
  const hwagaeTargets = [...new Set(sources.map((z) => HWAGAE_BRANCH[z]).filter(Boolean))];
  const found = [
    {
      key: "geonrok",
      name: "건록",
      active: branches.includes(geonrokBranch),
      basis: `일간 ${dayGanH} 기준 ${geonrokBranch}`,
      message: "자기 힘으로 기반을 세우는 독립성과 실무 지속력이 강하게 작동합니다.",
    },
    {
      key: "baekho",
      name: "백호살",
      active: gapjas.some((gz) => BAEKHO_GAPJA.has(gz)),
      basis: gapjas.filter((gz) => BAEKHO_GAPJA.has(gz)).join(", "),
      message: "강한 결단, 위기 대응, 극단적 몰입성이 있으므로 과로와 충돌 관리가 중요합니다.",
    },
    {
      key: "gwimun",
      name: "귀문관살",
      active: GWIMUN_PAIRS.some((pair) => hasPair(branches, pair)),
      basis: GWIMUN_PAIRS.filter((pair) => hasPair(branches, pair)).map((pair) => pair.join("")).join(", "),
      message: "촉과 몰입이 예민하게 열리는 구조로, 창작·상담·분석에는 좋지만 생각 과잉은 조절해야 합니다.",
    },
    {
      key: "wonjin",
      name: "원진살",
      active: WONJIN_PAIRS.some((pair) => hasPair(branches, pair)),
      basis: WONJIN_PAIRS.filter((pair) => hasPair(branches, pair)).map((pair) => pair.join("")).join(", "),
      message: "가까운 관계에서 말하지 않은 서운함이 쌓이기 쉬워 기대치와 경계선을 명확히 해야 합니다.",
    },
    {
      key: "dohwa",
      name: "도화살",
      active: dohwaTargets.some((target) => branches.includes(target)),
      basis: dohwaTargets.join(", "),
      message: "호감, 표현력, 대중 접점이 살아나는 기운입니다. 노출 전략을 잘 쓰면 매력 자산이 됩니다.",
    },
    {
      key: "hwagae",
      name: "화개살",
      active: hwagaeTargets.some((target) => branches.includes(target)),
      basis: hwagaeTargets.join(", "),
      message: "혼자 깊이 파고드는 기질과 예술·종교·철학적 성향이 강합니다.",
    },
  ];
  return { found, active: found.filter((item) => item.active) };
}

function calculateGongmang(dayGanIdx, dayZhiIdx, chart) {
  const serial = gapjaSerial(dayGanIdx, dayZhiIdx);
  const xunStart = Math.floor(serial / 10) * 10;
  const branches = [ZHI[mod(xunStart + 10, 12)], ZHI[mod(xunStart + 11, 12)]];
  const overlap = chart
    .filter(({ key }) => key === "month" || key === "hour")
    .filter(({ pillar }) => branches.some((z) => z.h === pillar.zhi.h))
    .map(({ key, label, pillar }) => ({ key, label, branch: pillar.zhi.h, active: true }));
  return {
    branches,
    branchText: branches.map((z) => z.h).join(""),
    overlap,
    hasMonthOrHourImpact: overlap.length > 0,
    impactMessage: overlap.length
      ? `${overlap.map((item) => item.label).join(", ")}에 공망이 겹쳐 해당 영역의 체감 변동성이 커집니다.`
      : "월주·시주 직접 중첩은 약해 공망 영향은 배경 기운으로 해석합니다.",
  };
}

function calculateDaeun(birthTs, gender, yearGanIdx, monthGanIdx, monthZhiIdx) {
  const yearGanIsYang = !GAN[yearGanIdx].yin;
  const normalizedGender = `${gender || ""}`;
  const forward = (normalizedGender.includes("남") && yearGanIsYang) || (normalizedGender.includes("여") && !yearGanIsYang);
  let pivot = null;
  for (const year of [kstDatePartsFromUtc(birthTs).y - 1, kstDatePartsFromUtc(birthTs).y, kstDatePartsFromUtc(birthTs).y + 1]) {
    for (const term of MONTH_JIE_TERMS) {
      const at = solarTermUtc(year, term);
      if (forward && at > birthTs && (!pivot || at < pivot.at)) pivot = { ...term, at };
      if (!forward && at < birthTs && (!pivot || at > pivot.at)) pivot = { ...term, at };
    }
  }
  const distanceDays = pivot ? Math.abs(pivot.at - birthTs) / DAY_MS : 0;
  const startAge = Math.max(1, Math.round(distanceDays / 3));
  const list = Array.from({ length: 8 }, (_, i) => {
    const step = forward ? i + 1 : -(i + 1);
    const ganIdx = mod(monthGanIdx + step, 10);
    const zhiIdx = mod(monthZhiIdx + step, 12);
    const pillar = gapjaIndex(ganIdx, zhiIdx);
    return {
      order: i + 1,
      ageStart: startAge + i * 10,
      ageEnd: startAge + i * 10 + 9,
      gan: pillar.gan,
      zhi: pillar.zhi,
      gapja: `${pillar.gan.h}${pillar.zhi.h}`,
    };
  });
  return {
    direction: forward ? "순행" : "역행",
    forward,
    startAge,
    pivotTerm: pivot ? { name: pivot.name, at: new Date(pivot.at + KST_OFFSET_MS).toISOString().slice(0, 16).replace("T", " ") } : null,
    list,
  };
}

function pickElementSentence(element, field) {
  return ELEMENTS[element]?.[field] || "";
}

const FORTUNE_MARKET_DB = {
  dayMaster: {
    "甲": { core: "갑목 일간은 큰 나무처럼 방향성과 성장성이 강하고, 명예보다 '내가 뻗어갈 공간'이 중요합니다.", real: "답답한 조직에 들어가면 회의 중에는 고개를 끄덕여도 집에 와서 이직 공고를 저장해두는 타입입니다." },
    "乙": { core: "을목 일간은 풀과 덩굴처럼 유연하게 환경을 타고 오르며, 관계와 분위기를 읽는 감각이 섬세합니다.", real: "상대 카톡 말투가 한 글자만 달라져도 바로 눈치채지만, 정작 본인 서운함은 한참 돌려 말하는 편입니다." },
    "丙": { core: "병화 일간은 태양처럼 드러나는 기운이 강해 존재감, 표현력, 추진력이 원국의 중심축이 됩니다.", real: "꽂히면 바로 예약하고 결제하지만, 마음이 식으면 장바구니와 운동 앱이 동시에 방치되기 쉽습니다." },
    "丁": { core: "정화 일간은 촛불처럼 집중도와 감정의 밀도가 높고, 작은 장면에서도 의미를 찾아내는 힘이 있습니다.", real: "괜찮다고 말해놓고 새벽에 혼자 대화 캡처를 다시 읽으며 마음을 정리하는 패턴이 나옵니다." },
    "戊": { core: "무토 일간은 큰 산처럼 버티는 힘과 기준이 강하며, 쉽게 흔들리지 않는 안정감이 핵심입니다.", real: "평소에는 묵묵히 참다가 한계가 오면 갑자기 단호해져 주변 사람이 '왜 이제 말해?'라고 느끼게 만듭니다." },
    "己": { core: "기토 일간은 밭처럼 사람과 일을 품어 현실적인 결과로 길러내는 능력이 강합니다.", real: "남의 일정과 감정은 잘 챙기면서 본인 휴식은 계속 미루다가 밤에 배달앱으로 보상받기 쉽습니다." },
    "庚": { core: "경금 일간은 쇠붙이처럼 결단, 정리, 기준 설정이 강하고 불필요한 것을 잘라내는 힘이 있습니다.", real: "마음속으로 이미 손절 기준표가 있고, 상대가 선을 넘으면 대화보다 알림 끄기부터 합니다." },
    "辛": { core: "신금 일간은 보석처럼 세밀한 감각과 완성도 욕구가 강하며, 품질과 취향의 기준이 분명합니다.", real: "가성비를 따지다가도 마음에 드는 디테일 하나가 있으면 갑자기 프리미엄 옵션을 고르는 타입입니다." },
    "壬": { core: "임수 일간은 큰 물처럼 판을 읽고 이동하는 감각이 좋으며, 정보와 흐름을 연결하는 힘이 있습니다.", real: "결정은 늦어 보여도 머릿속에는 플랜 B, C까지 돌리고 있고 검색 탭만 스무 개 열려 있기 쉽습니다." },
    "癸": { core: "계수 일간은 비와 안개처럼 섬세하고 침투력 있는 지성이 강하며, 작은 신호를 오래 기억합니다.", real: "상대가 무심코 한 말을 몇 달 뒤에도 기억하고, 혼자 의미를 분석하다가 답장을 미루는 편입니다." },
  },
  elementExcess: {
    "목": { core: "목 과다는 확장 욕구와 성장 압력이 강해 계획이 계속 늘어나는 구조입니다.", real: "일단 강의, 모임, 프로젝트를 다 열어두고 나중에 일정표를 보며 본인이 본인에게 치이는 패턴이 생깁니다." },
    "화": { core: "화 과다는 표현과 반응 속도가 빨라 기회 포착은 좋지만 에너지 소모도 큽니다.", real: "기분 좋을 때는 사람을 다 챙기다가, 방전되면 카톡 알림을 봐도 답장할 힘이 없어집니다." },
    "토": { core: "토 과다는 축적과 안정 욕구가 강해 책임감은 크지만 정체와 걱정도 함께 커집니다.", real: "돈을 아끼려고 비교하다가 스트레스가 쌓이면 결국 홧김 비용으로 더 큰 결제를 하는 식입니다." },
    "금": { core: "금 과다는 기준, 통제, 완성도 욕구가 강해 판단은 정확하지만 여백이 줄어듭니다.", real: "상대의 말보다 태도와 시간 약속을 체크하고, 한 번 신뢰가 깨지면 복구 비용을 크게 봅니다." },
    "수": { core: "수 과다는 사고와 감정의 흐름이 깊어 통찰은 좋지만 결정 지연이 생기기 쉽습니다.", real: "머리로는 정답을 알면서도 리뷰, 후기, 지인 의견을 더 찾다가 타이밍을 놓치는 장면이 반복됩니다." },
  },
  elementLack: {
    "목": "목 기운이 약하면 시작과 확장의 버튼이 늦게 눌립니다. 일상에서는 하고 싶은 건 많은데 첫 메시지, 첫 지원서, 첫 운동 예약을 미루는 식으로 나타납니다.",
    "화": "화 기운이 약하면 표현과 즉시 반응이 줄어듭니다. 좋아도 티를 덜 내고, 칭찬받아도 괜히 무덤덤하게 넘겨 관계 온도가 낮아질 수 있습니다.",
    "토": "토 기운이 약하면 루틴과 축적 기반이 흔들립니다. 월급날에는 계획이 있지만 월말에는 카드 내역을 보며 '이번 달 뭐 했지'가 나오기 쉽습니다.",
    "금": "금 기운이 약하면 자르는 힘과 기준선이 약합니다. 거절해야 할 부탁을 받아주고, 구독 해지나 관계 정리를 계속 다음 달로 넘길 수 있습니다.",
    "수": "수 기운이 약하면 회복, 현금흐름, 정보 판단이 약해집니다. 피곤한데도 쉬지 못하고, 돈이 어디로 새는지 뒤늦게 확인하는 패턴이 생깁니다.",
  },
  tenStar: {
    "비견": "비견 성향은 자기 주도성과 독립성이 강합니다. 남이 정한 답보다 내가 납득한 방식을 고집해 단단하지만, 가끔은 도움 요청을 자존심 문제로 오해합니다.",
    "겁재": "겁재 성향은 경쟁심과 생존 감각이 강합니다. 친구 따라 소비하거나 비교심에 갑자기 결제하는 장면이 생기므로 감정성 지출을 분리해야 합니다.",
    "식신": "식신 성향은 꾸준한 생산성과 생활 감각이 강합니다. 루틴이 잡히면 오래 가지만, 스트레스가 쌓이면 야식·간식·소확행 소비로 풀기 쉽습니다.",
    "상관": "상관 성향은 표현력과 반골 기질이 강합니다. 틀린 말은 못 참지만, 단톡방에서 한마디 더 했다가 분위기를 애매하게 만드는 경우가 있습니다.",
    "편재": "편재 성향은 기회 포착과 외부 자원 감각이 좋습니다. 돈 냄새를 빨리 맡지만, 핫딜·공구·투자 제안에 마음이 빨리 흔들릴 수 있습니다.",
    "정재": "정재 성향은 고정 수입, 계획, 책임감에 강합니다. 엑셀로 예산을 짤 수 있지만, 예상 밖 감정 지출이 생기면 스스로에게 더 엄격해집니다.",
    "편관": "편관 성향은 압박 속 돌파력과 위기 대응력이 있습니다. 급한 일은 잘 처리하지만, 평온한 날에도 혼자 마감 직전 모드로 몸을 몰아붙입니다.",
    "정관": "정관 성향은 신뢰, 직함, 사회적 기준을 중시합니다. 평판 관리는 좋지만, 남에게 흐트러진 모습을 보이기 싫어 혼자 부담을 떠안기 쉽습니다.",
    "편인": "편인 성향은 직감, 몰입, 비정형 학습에 강합니다. 관심 분야에는 밤새 파고들지만, 현실 서류나 정산 같은 일은 이상하게 미뤄집니다.",
    "정인": "정인 성향은 보호받고 배우는 복이 있으며 이해력이 안정적입니다. 다만 준비를 더 해야 한다는 생각 때문에 시작이 늦어질 수 있습니다.",
  },
  categories: {
    total: "원국 전체는 일간, 월지, 강한 오행, 공망의 빈자리까지 함께 봐야 합니다. 앞에서는 명리 구조가 인생의 큰 골격을 만들고, 뒤에서는 그 구조가 카톡 답장 속도, 소비 습관, 관계 회피 같은 생활 습관으로 새어 나옵니다.",
    early: "초년운은 부모·학습·초기 관계의 영향을 받는 자리입니다. 강한 오행은 빠른 적응으로 보이고, 약한 오행은 숙제 미루기, 눈치 보기, 비교 피로감처럼 아주 현실적인 장면으로 나타납니다.",
    middle: "중년운은 직업과 자산 선택이 맞물리는 핵심 구간입니다. 대운이 흔들리는 시기에는 커리어 전환, 대출, 가족 책임이 겹치며 '돈은 버는데 남는 게 없는' 체감이 생길 수 있습니다.",
    senior: "말년운은 축적한 관계, 건강 루틴, 자산 구조가 결과로 돌아오는 시기입니다. 이때 운의 질은 큰 성공보다 매일 무너지지 않는 생활 시스템에서 갈립니다.",
    health: "건강운은 오행의 과다와 고립을 체질 언어로 번역해야 합니다. 과한 기운은 염증·긴장·정체로, 부족한 기운은 회복력 저하와 생활 리듬 붕괴로 나타나기 쉽습니다.",
    bodyType: "타고난 체질은 몸의 모양보다 에너지 운용 방식에 가깝습니다. 어떤 사람은 화가 나면 바로 말하고, 어떤 사람은 참고 있다가 야식이나 쇼핑으로 뒤늦게 해소합니다.",
    social: "사회운은 십성과 강한 오행이 밖으로 드러나는 방식입니다. 직장에서는 실력보다 협업 방식, 피드백 수용, 마감 대응이 운을 크게 바꿉니다.",
    socialTrait: "대인 성향은 도화·원진·귀문 같은 신살과도 연결됩니다. 호감은 빨리 얻지만 오래 유지하려면 말투, 답장 텀, 선 긋는 방식이 중요합니다.",
    personality: "성격은 일간의 본질과 오행 균형이 합쳐진 결과입니다. 겉으로 보이는 이미지와 실제 스트레스 반응이 다를 수 있어 생활 패턴까지 같이 봐야 합니다.",
    inherent: "타고난 기질은 바꾸는 것이 아니라 운용하는 것입니다. 장점은 돈과 관계를 만들고, 과용된 장점은 똑같은 방식으로 손실을 만듭니다.",
    nature: "본성은 위기 때 가장 선명해집니다. 평소엔 괜찮아 보여도 압박이 오면 답장을 미루거나, 갑자기 정리하거나, 소비로 감정을 처리하는 습관이 드러납니다.",
    romance: "연애운은 배우자운 하나로 끝나지 않습니다. 일간의 애정 표현, 도화의 매력, 원진의 서운함, 약한 오행의 불안이 함께 관계의 온도를 만듭니다.",
    affection: "애정 성향은 '좋아하는 방식'보다 '불안할 때 하는 행동'에서 더 정확히 보입니다. 확인받고 싶은데 쿨한 척하거나, 연락을 기다리면서 일부러 늦게 답하는 식입니다.",
    wealthTeasing: "재물운은 타고난 그릇과 중년 대운의 압박을 같이 봐야 합니다. 무료 구간에서는 장점과 경고를 모두 보여주고, 구체적인 손실 방어법은 프리미엄에서 열리도록 설계합니다.",
  },
};

function tenStarName(dayGan, targetGan) {
  if (!dayGan || !targetGan) return "";
  const sameYin = dayGan.yin === targetGan.yin;
  if (dayGan.el === targetGan.el) return sameYin ? "비견" : "겁재";
  if (SHENG[dayGan.el] === targetGan.el) return sameYin ? "식신" : "상관";
  if (KE[dayGan.el] === targetGan.el) return sameYin ? "편재" : "정재";
  if (KE[targetGan.el] === dayGan.el) return sameYin ? "편관" : "정관";
  if (SHENG[targetGan.el] === dayGan.el) return sameYin ? "편인" : "정인";
  return "";
}

function analyzeTenStars(dayMaster, chart) {
  const counts = {};
  chart.forEach(({ pillar }) => {
    [pillar.gan, ...(pillar.meta?.hiddenStems || [])].forEach((gan) => {
      const name = tenStarName(dayMaster, gan);
      if (name) counts[name] = (counts[name] || 0) + 1;
    });
  });
  const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || "비견";
  return {
    counts,
    dominant,
    summary: FORTUNE_MARKET_DB.tenStar[dominant] || "",
  };
}

function elementPercent(stats, element) {
  return Math.round(((stats.counts[element] || 0) / Math.max(1, stats.totalLetters || 8)) * 100);
}

function marketBlend({ title, expert, real, category }) {
  const base = FORTUNE_MARKET_DB.categories[category] || "";
  return `${expert} ${base} ${real}`;
}

function buildReport({ profile, base, originalChart, stats, shinsal, gongmang, daeun }) {
  const name = profile?.name || "사용자";
  const dayEl = base.dayMaster.el;
  const strongText = stats.strong.length ? `${stats.strong.join(", ")} 기운이 강합니다` : "강하게 치우친 오행은 약합니다";
  const weakText = stats.weak.length ? `${stats.weak.join(", ")} 기운이 비어 있습니다` : "완전히 고립된 오행은 없습니다";
  const activeShinsal = shinsal.active.length ? shinsal.active.map((s) => s.name).join(", ") : "특정 신살 과몰입보다 기본 원국 균형";
  const dayMasterCopy = FORTUNE_MARKET_DB.dayMaster[base.dayMaster.h] || { core: `${base.dayMaster.h}${base.dayMaster.k} 일간은 ${ELEMENTS[dayEl].focus}을 중심으로 움직입니다.`, real: "일상에서는 장점이 습관으로, 과한 장점이 반복 실수로 드러납니다." };
  const tenStars = analyzeTenStars(base.dayMaster, originalChart);
  const dominantElementCopy = FORTUNE_MARKET_DB.elementExcess[stats.strong[0]] || { core: `${strongText}.`, real: "강한 기운은 잘 쓰면 성과지만 과하면 같은 패턴의 피로로 돌아옵니다." };
  const lackCopies = stats.weak.map((el) => FORTUNE_MARKET_DB.elementLack[el]).filter(Boolean);
  const waterPercent = elementPercent(stats, "수");
  const earthPercent = elementPercent(stats, "토");
  const metalVoid = (gongmang.branches || []).some((z) => z.el === "금");
  const waterGap = stats.weak.includes("수");
  const earthHeavy = stats.strong.includes("토") && stats.counts["토"] >= 3;
  const giDay = base.dayMaster.h === "己";
  const currentDaeun = daeun.list[0];
  const earlyDaeun = daeun.list.slice(0, 2).map((item) => item.gapja).join(" → ");
  const middleDaeun = daeun.list.slice(2, 5).map((item) => item.gapja).join(" → ");
  const lateDaeun = daeun.list.slice(5, 8).map((item) => item.gapja).join(" → ");

  const conditionNotes = [
    giDay ? "기토 일간은 작은 밭처럼 현실 감각과 돌봄 능력이 핵심 자산입니다." : `${base.dayMaster.h}${base.dayMaster.k} 일간은 ${ELEMENTS[dayEl].focus}을 중심으로 운을 씁니다.`,
    earthHeavy ? "토 과다는 안정성은 키우지만 결정 지연과 걱정의 반복을 만들 수 있어 순환 장치가 필요합니다." : `${strongText}. 이 강점은 반복 가능한 성과 패턴으로 전환할 때 가치가 커집니다.`,
    waterGap ? "수 고립은 휴식, 유연성, 현금흐름 감각을 의식적으로 보강해야 한다는 신호입니다." : `${weakText}. 부족한 오행은 생활 습관과 역할 선택으로 보완할 수 있습니다.`,
  ];
  const dynamicWaterSentence = waterPercent === 0
    ? "수 기운이 0%라 회복력, 유동성, 정보 판단을 반드시 외부 장치로 보완해야 합니다."
    : waterPercent >= 38
      ? `수 기운이 ${waterPercent}%로 강해 생각과 정보 감각은 좋지만, 결정 지연과 감정 과몰입이 비용으로 바뀔 수 있습니다.`
      : `수 기운은 ${waterPercent}%라 큰 고립은 아니지만, 피곤할 때 현금흐름과 수면 리듬부터 흔들릴 수 있습니다.`;
  const dynamicEarthSentence = earthPercent >= 38
    ? `토 기운이 ${earthPercent}%로 높아 버티고 모으는 힘은 강하지만, 돈이 묶이고 결정이 늦어지는 병목도 같이 커집니다.`
    : `토 기운은 ${earthPercent}%로 과도한 정체보다 상황별 축적 전략이 중요합니다.`;
  const lifetimeOverview = marketBlend({
    category: "total",
    expert: `${name}님의 원국은 ${base.dayMaster.h}${base.dayMaster.k} 일간과 ${tenStars.dominant} 십성 성향을 중심으로 해석합니다. ${dayMasterCopy.core} ${dominantElementCopy.core} ${metalVoid ? "금 공망은 기준과 결단의 빈자리로 작용해 중요한 순간에 정리와 손절을 미루게 만들 수 있습니다." : ""}`,
    real: `${dayMasterCopy.real} ${dominantElementCopy.real} 그래서 겉으로는 멀쩡해 보여도 실제 생활에서는 답장을 미루거나, 비교하다가 늦게 결제하거나, 스트레스를 소비로 처리하는 식의 반복 패턴이 생깁니다.`,
  });
  const earlyLife = marketBlend({
    category: "early",
    expert: `초년운은 ${earlyDaeun || "초기 대운"} 흐름과 월주의 기운을 함께 봅니다. ${tenStars.summary} ${lackCopies[0] || weakText}`,
    real: "어릴 때부터 잘하는 것과 미루는 것이 분명해지고, 인정받고 싶은 마음 때문에 숙제를 완벽히 하려다 시작 자체가 늦어지는 장면이 나타날 수 있습니다.",
  });
  const middleLife = marketBlend({
    category: "middle",
    expert: `중년운은 ${middleDaeun || "중기 대운"} 구간에서 직업, 돈, 관계 책임이 한꺼번에 올라오는 자리입니다. ${dynamicEarthSentence} ${dynamicWaterSentence}`,
    real: "이 시기에는 월급은 오르는데 대출, 가족 지출, 자기 보상 소비가 함께 커져 카드값을 보며 '분명 아꼈는데 왜 없지'라는 체감이 강해질 수 있습니다.",
  });
  const lateLife = marketBlend({
    category: "senior",
    expert: `말년운은 ${lateDaeun || "후기 대운"} 흐름에서 강한 오행의 과용을 줄이고 공망의 빈자리를 보완하는 구간입니다. ${gongmang.impactMessage}`,
    real: "결국 말년의 안정감은 큰 한 방보다 자동이체, 수면 시간, 병원 예약, 오래 갈 사람만 남기는 관계 정리 같은 아주 구체적인 루틴에서 결정됩니다.",
  });
  const wealthTeasing = marketBlend({
    category: "wealthTeasing",
    expert: `${name}님의 재물 그릇은 ${stats.strong.join(", ") || dayEl} 기운이 만드는 추진력과 ${tenStars.dominant} 성향에서 열립니다. ${dynamicEarthSentence} 30대 중반 대운 시련기에는 돈이 묶이고 회수 기간이 길어지는 선택이 반복될 수 있습니다.`,
    real: "평소에는 가성비를 따지다가도 스트레스가 터지면 홧김 비용, 충동 여행, 프리미엄 옵션 결제로 자산 손실을 볼 수 있습니다. 여기서부터는 손실을 막는 행동 처방이 필요합니다.",
  });
  const premiumWealthLoss = `재물손실 막는 법: ${name}님은 큰돈을 잃기보다 작은 누수와 묶이는 선택이 누적될 가능성이 큽니다. 1단계로 월 고정비, 감정 지출, 투자금을 계좌 단위로 분리하고, 2단계로 24시간 숙려 없는 결제를 금지하며, 3단계로 대출·보증·장기 약정은 반드시 제3자 검토를 거쳐야 합니다. ${dynamicWaterSentence} 특히 스트레스가 올라온 날에는 배달앱, 쇼핑앱, 투자앱을 동시에 열지 않는 것이 첫 번째 방어선입니다.`;
  const premiumTech = `재테크 비법 리포트: ${name}님의 자산 전략은 '많이 버는 법'보다 '안 새게 만드는 법'에서 시작해야 합니다. ${daeun.direction} 대운의 ${currentDaeun?.gapja || "현재 구간"}에서는 자동저축 20%, 비상금 3개월분, 투자금 상한선, 월 1회 손익 회고를 고정하십시오. ${earthHeavy ? "토 과다형은 부동산·예금처럼 묶이는 자산에 치우치기 쉬우므로 현금성 자산 비율을 의식적으로 남겨야 합니다." : "강한 오행은 수입 채널에 쓰고 약한 오행은 체크리스트와 자동화로 보완해야 합니다."} 감정이 흔들리는 날의 결제 버튼을 막는 것이 곧 재테크입니다.`;
  const bodyType = marketBlend({
    category: "bodyType",
    expert: `${name}님의 체질은 ${dayEl} 일간의 기본 기운과 오행 분포로 봅니다. ${dynamicEarthSentence} ${dynamicWaterSentence}`,
    real: "몸은 거짓말을 잘 못해서, 마음으로는 괜찮다고 해도 소화, 수면, 붓기, 어깨 긴장, 야식 패턴으로 먼저 신호를 보낼 수 있습니다.",
  });
  const social = marketBlend({
    category: "social",
    expert: `사회운은 ${tenStars.dominant} 십성과 ${activeShinsal} 흐름으로 봅니다. ${tenStars.summary}`,
    real: "회사에서는 능력보다 피드백을 받는 표정, 답장 텀, 싫은 일을 처리하는 순서가 실제 평판을 좌우하기 쉽습니다.",
  });
  const socialTrait = marketBlend({
    category: "socialTrait",
    expert: `${stats.strong.join(", ") || dayEl} 기운이 대인관계의 첫인상을 만들고, ${gongmang.branchText} 공망은 비어 보이는 관계 과제를 만듭니다.`,
    real: "겉으로는 괜찮다고 넘겨도 집에 와서 대화 장면을 다시 복기하거나, 갑자기 연락을 끊고 쉬어야 회복되는 패턴이 나올 수 있습니다.",
  });
  const inherent = marketBlend({
    category: "inherent",
    expert: `${dayMasterCopy.core} ${dominantElementCopy.core} ${lackCopies.join(" ")}`,
    real: "타고난 장점이 그대로 생활 습관이 되면 성과가 나지만, 과로한 날에는 그 장점이 고집, 미루기, 충동 결제로 뒤집혀 나타납니다.",
  });
  const nature = marketBlend({
    category: "nature",
    expert: `본성은 일간 ${base.dayMaster.h}${base.dayMaster.k}, 월지, 십성 ${tenStars.dominant}의 조합으로 선명해집니다.`,
    real: "압박이 오면 사람마다 도망치는 방식이 다른데, 이 사주는 말로 풀기보다 참고 버티거나 검색, 소비, 침묵으로 우회할 가능성이 있습니다.",
  });
  const romance = marketBlend({
    category: "romance",
    expert: `${ELEMENTS[dayEl].love} 결이 애정운의 기본값입니다. ${shinsal.active.find((s) => s.key === "dohwa") ? "도화는 호감과 노출의 힘을 키웁니다." : "도화가 강하지 않다면 안정성과 반복 신뢰가 매력의 핵심입니다."}`,
    real: "호감이 생기면 자연스럽게 티가 나는 사람도 있고, 오히려 답장을 늦추며 마음을 들키지 않으려는 사람도 있습니다. 이 사주는 불안할수록 행동 패턴을 봐야 합니다.",
  });
  const affection = marketBlend({
    category: "affection",
    expert: `애정 성향은 일간의 표현 방식과 ${tenStars.dominant} 십성의 욕구가 결합됩니다. ${shinsal.active.find((s) => s.key === "wonjin") ? "원진이 있으면 서운함이 오래 남기 쉬워 확인 대화가 필요합니다." : "큰 충돌보다 생활 온도와 신뢰의 누적이 중요합니다."}`,
    real: "좋아할수록 확인받고 싶은데 쿨한 척하거나, 먼저 연락하고 싶으면서도 자존심 때문에 상대 스토리만 보는 장면이 생길 수 있습니다.",
  });

  const sections = {
    personality: {
      title: "성격과 기질",
      summary: marketBlend({
        category: "personality",
        expert: `${name}님은 ${ELEMENTS[dayEl].aura}의 결을 가진 사람입니다. ${dayMasterCopy.core} ${dominantElementCopy.core} 대표 십성은 ${tenStars.dominant}으로, ${tenStars.summary}`,
        real: `${dayMasterCopy.real} ${lackCopies[0] || "약한 오행은 생활 습관으로 보완할 때 안정됩니다."}`,
      }),
      tags: [ELEMENTS[dayEl].focus, ...stats.strong.map((el) => `${el} 강세`), ...stats.weak.map((el) => `${el} 보완`)],
    },
    wealth: {
      title: "재물운",
      summary: wealthTeasing,
    },
    career: {
      title: "직업 적성",
      summary: marketBlend({
        category: "social",
        expert: `${pickElementSentence(dayEl, "career")} 원국상 ${activeShinsal} 흐름과 ${tenStars.dominant} 성향이 관찰됩니다. ${giDay ? "기토는 운영, 큐레이션, 고객 관리, 교육처럼 복잡한 것을 실제로 굴러가게 만드는 역할과 잘 맞습니다." : ""}`,
        real: "일상 업무에서는 실력보다 답장 속도, 마감 전 집중력, 싫은 일의 우선순위 조정 방식이 평판을 크게 좌우합니다.",
      }),
    },
    loveMarriage: {
      title: "연애·결혼운",
      summary: marketBlend({
        category: "romance",
        expert: `${ELEMENTS[dayEl].love} 결이 기본 연애 톤입니다. ${shinsal.active.find((s) => s.key === "dohwa") ? "도화 기운이 있어 호감 형성과 표현력은 강하지만 관계 초반의 속도 조절이 중요합니다." : "관계에서는 화려한 이벤트보다 일관된 태도와 신뢰 축적이 더 크게 작동합니다."} ${shinsal.active.find((s) => s.key === "wonjin") ? "원진 흐름은 서운함을 쌓아두지 않는 대화 구조가 핵심입니다." : ""}`,
        real: "좋아할수록 더 쿨한 척하거나, 답장을 기다리면서도 일부러 늦게 보내는 식의 미묘한 방어가 관계의 오해를 만들 수 있습니다.",
      }),
    },
    health: {
      title: "건강",
      summary: marketBlend({
        category: "health",
        expert: `${pickElementSentence(dayEl, "health")} ${earthHeavy ? "토 과다는 소화, 부종, 정체감 관리가 중요합니다." : ""} ${dynamicWaterSentence}`,
        real: "컨디션이 무너지면 운동 부족보다 먼저 야식, 수면 밀림, 카페인 의존, 누워서 쇼츠 보기로 나타나기 쉬우므로 회복 루틴을 결제 루틴보다 먼저 고정해야 합니다.",
      }),
    },
    relationship: {
      title: "대인관계",
      summary: marketBlend({
        category: "socialTrait",
        expert: `${stats.strong.join(", ") || dayEl} 기운이 관계에서 먼저 보입니다. ${shinsal.active.find((s) => s.key === "gwimun") ? "귀문 흐름은 타인의 미묘한 기분을 잘 읽게 하지만, 확인되지 않은 추측까지 사실처럼 다루지 않도록 주의해야 합니다." : "관계 운은 무리한 확장보다 신뢰할 수 있는 소수 네트워크를 단단히 하는 쪽이 유리합니다."}`,
        real: "단톡방에서는 괜찮은 척하다가 혼자 해석을 많이 하거나, 선 넘은 부탁을 받아준 뒤 뒤늦게 피로감이 몰릴 수 있습니다.",
      }),
    },
    yearly: {
      title: "올해 운세",
      summary: `${currentDaeun ? `현재 대운 흐름은 ${daeun.direction} ${currentDaeun.gapja} 구간으로 읽습니다.` : "대운 정보는 월주를 기준으로 흐름을 읽습니다."} 강한 오행은 성과를 밀어붙이는 엔진이고, 약한 오행은 올해 반드시 관리해야 할 병목입니다. 실행은 작게, 회고는 자주 가져가는 전략이 맞습니다.`,
    },
    advice: {
      title: "종합 조언",
      summary: `${name}님의 핵심 전략은 '${ELEMENTS[dayEl].focus}'을 현실 루틴으로 바꾸는 것입니다. ${gongmang.impactMessage} 강한 것은 쓰되 과용하지 말고, 비어 있는 것은 사람·환경·습관으로 보완하는 방식이 상용 만세력 관점의 가장 실전적인 처방입니다.`,
    },
  };

  return {
    headline: `${name}님을 위한 정밀 만세력 리포트`,
    summary: `${base.dayMaster.h}${base.dayMaster.k} 일간, ${strongText}, ${weakText}.`,
    total: lifetimeOverview,
    lifetimeOverview,
    early: earlyLife,
    earlyLife,
    middle: middleLife,
    middleLife,
    senior: lateLife,
    lateLife,
    health: sections.health.summary,
    bodyType,
    social,
    socialTrait,
    personality: sections.personality.summary,
    inherent,
    nature,
    romance,
    affection,
    wealthTeasing,
    premiumWealthLoss,
    premiumTech,
    conditionNotes,
    data: {
      originalChart,
      elementStats: stats,
      tenStars,
      shinsal,
      gongmang,
      daeun,
    },
    sections,
    meta: {
      generatedAt: new Date().toISOString(),
      dominantElements: stats.strong,
      isolatedElements: stats.weak,
      activeShinsal: shinsal.active.map((s) => s.name),
    },
  };
}

/* 생년월일(+시) → 절기 기준 사주 기둥 */
function computeSaju(b) {
  // b: {y, m, d, hour, isLunar, isLeapLunar}. 입력 시각은 한국 민간 시각으로 받는다.
  const birth = normalizeBirthInput(b);
  const birthTs = birth.birthTs;

  const sajuYear = sajuYearForBirth(birthTs);
  const yearGan = mod(sajuYear - 4, 10);
  const yearZhi = mod(sajuYear - 4, 12);

  const term = monthTermAt(birthTs) || MONTH_JIE_TERMS[0];
  const monthZhi = mod(term.monthNo + 1, 12);
  const monthGan = mod(yearGan * 2 + term.monthNo + 1, 10);

  const day = dayIndexFromLocalDate(birth.y, birth.m, birth.d);
  const dayGan = day.gan;
  const dayZhi = day.zhi;

  let hourZhi = null, hourGan = null;
  if (birth.hasHour) {
    const minuteOfDay = birth.hour * 60 + birth.minute;
    hourZhi = Math.floor(((minuteOfDay + 60) % 1440) / 120) % 12;
    hourGan = mod(dayGan * 2 + hourZhi, 10);
  }

  const dayMaster = GAN[dayGan];           // 일간 = 나 자신
  const element = dayMaster.el;            // 핵심 오행
  const pillars = {
    year: buildPillar("연주", yearGan, yearZhi, dayMaster.h),
    month: buildPillar("월주", monthGan, monthZhi, dayMaster.h),
    day: buildPillar("일주", dayGan, dayZhi, dayMaster.h),
    hour: hourZhi !== null ? buildPillar("시주", hourGan, hourZhi, dayMaster.h) : null,
  };
  const originalChart = buildOriginalChart(pillars);
  const elementStats = elementStatsFromChart(originalChart);
  const shinsal = detectShinsal(originalChart, dayMaster.h, pillars.year.zhi.h, pillars.day.zhi.h);
  const gongmang = calculateGongmang(dayGan, dayZhi, originalChart);
  const daeun = calculateDaeun(birthTs, b.gender, yearGan, monthGan, monthZhi);
  const base = {
    year:  pillars.year,
    month: pillars.month,
    day:   pillars.day,
    hour:  pillars.hour,
    animal: ZHI[yearZhi],
    dayMaster, element,
    yinYang: dayMaster.yin ? "음(陰)" : "양(陽)",
    meta: ELEMENTS[element],
  };
  const freeReport = buildReport({ profile: b, base, originalChart, stats: elementStats, shinsal, gongmang, daeun });
  try {
    const calculateSaju = window.__orreryCalculateSaju;
    const orreryInput = {
      year: birth.y,
      month: birth.m,
      day: birth.d,
      hour: birth.hasHour ? birth.hour : undefined,
      minute: birth.hasHour ? birth.minute : undefined,
      gender: (b.gender || "").includes("남") ? "M" : "F",
    };
    let orrery = typeof calculateSaju === "function" ? calculateSaju(orreryInput) : null;
    if (!orrery) {
      orrery = {
        pillars: originalChart.map(({ key, label, pillar }, index) => ({ key, label, index, ...pillar })),
        daewoon: daeun.list,
        relations: null,
        specialSals: shinsal.active,
        gongmang,
        jwabeop: originalChart.map(({ key, label, pillar }) => ({ key, label, hiddenStems: pillar.meta?.hiddenStems || [], twelveStage: pillar.meta?.twelveStage })),
        injongbeop: null,
      };
    }
    if (orrery) {
      Object.assign(freeReport.data, {
        orreryPillars: orrery.pillars,
        orreryDaewoon: orrery.daewoon,
        orreryRelations: orrery.relations,
        orrerySpecialSals: orrery.specialSals,
        orreryGongmang: orrery.gongmang,
        orreryJwabeop: orrery.jwabeop,
        orreryInjongbeop: orrery.injongbeop,
      });
    }
  } catch (e) {}
  return {
    ...base,
    freeReport,
    premiumPaywall: {
      title: "프리미엄 정밀 만세력 전체 리포트",
      price: 4900,
      priceText: "4,900원",
      currency: "KRW",
      viralCouponEnabled: true,
      cta: "전체 해석 열기",
      wealthLossPreventionTitle: "재물손실 막는 법",
      wealthLossPrevention: freeReport.premiumWealthLoss,
      investmentTechTitle: "재테크 비법 리포트",
      investmentTechReport: freeReport.premiumTech,
    },
  };
}

/* 궁합 점수(나 일간 오행 vs 상대 일간 오행) */
function compatScore(a, b) {
  if (!a || !b) return null;
  const ea = a.element, eb = b.element;
  let base, label, tone;
  if (ea === eb)               { base = 78; label = "닮은 결의 인연"; tone = "비견"; }
  else if (SHENG[ea] === eb)   { base = 92; label = "내가 키워주는 인연"; tone = "상생"; }
  else if (SHENG[eb] === ea)   { base = 90; label = "나를 살려주는 인연"; tone = "상생"; }
  else if (KE[ea] === eb)      { base = 64; label = "끌림과 긴장의 인연"; tone = "상극"; }
  else if (KE[eb] === ea)      { base = 62; label = "길들여지는 인연"; tone = "상극"; }
  else                         { base = 74; label = "무난히 조화로운 인연"; tone = "중화"; }
  // 띠 삼합/육합 가산
  const harmony = [[0,4,8],[1,5,9],[2,6,10],[3,7,11]];
  const zaIdx = ZHI.findIndex(z => z.h === a.animal.h);
  const zbIdx = ZHI.findIndex(z => z.h === b.animal.h);
  let bonus = 0, note = "";
  if (harmony.some(g => g.includes(zaIdx) && g.includes(zbIdx) && zaIdx !== zbIdx)) { bonus = 7; note = "삼합 — 자연스럽게 통하는 띠"; }
  if ((zaIdx + zbIdx) % 12 === 1 % 12 && Math.abs(zaIdx - zbIdx) === 1) bonus = Math.max(bonus, 5);
  const score = Math.min(99, base + bonus);
  return { score, label, tone, note };
}

/* ---- AI 전문가 페르소나 ---- */
const PERSONAS = [
  {
    id: "doyeon",
    name: "도현",
    title: "命理",
    role: "명리학자",
    tag: "사주·운명의 흐름",
    grad: "linear-gradient(135deg,#7C5CFF,#A86BF0)",
    accent: "#7C5CFF",
    glyph: "卜",
    intro: "사주의 기둥을 읽고 두 사람 사이에 흐르는 기운을 짚어 드립니다. 운명은 정해진 게 아니라, 흐름을 아는 사람이 다루는 거예요.",
    opener: "어떤 인연이 궁금해서 오셨나요? 두 분의 생년월일을 알려주시면 기운의 결을 함께 살펴볼게요.",
    system: "당신은 '도현'이라는 이름의 따뜻하고 통찰력 있는 한국 명리학자입니다. 사주·오행·십성의 흐름으로 연애를 풀이하되, 운명론으로 겁주지 말고 '흐름을 어떻게 다룰지' 조언으로 마무리하세요. 한자 용어는 1~2개만, 쉽게 풀어서. 차분하고 품위 있는 존댓말. 3~5문장.",
  },
  {
    id: "seoyun",
    name: "서윤",
    title: "心理",
    role: "심리분석 상담사",
    tag: "그 사람의 속마음",
    grad: "linear-gradient(135deg,#5B93F0,#7C6BE8)",
    accent: "#5B93F0",
    glyph: "瞳",
    intro: "행동에는 늘 이유가 있어요. 그 사람이 보낸 신호를 함께 읽고, 그 마음의 구조를 분석해 드릴게요. 추측이 아니라 패턴으로요.",
    opener: "그 사람의 어떤 행동이 마음에 걸리세요? 상황을 구체적으로 들려주시면 그 안의 심리를 같이 짚어볼게요.",
    system: "당신은 '서윤'이라는 임상 경험이 풍부한 한국 연애 심리분석 상담사입니다. 애착유형·인지·방어기제 관점에서 '상대가 왜 그렇게 행동하는지'를 차분히 분석합니다. 단정 대신 가능성을 제시하고, 사용자가 직접 확인할 수 있는 관찰 포인트를 줍니다. 공감 먼저, 그다음 분석. 따뜻하지만 전문적인 존댓말. 3~5문장.",
  },
  {
    id: "jian",
    name: "지안",
    title: "戀愛",
    role: "연애 코치",
    tag: "지금 뭘 해야 할까",
    grad: "linear-gradient(135deg,#F56FA7,#FF9CC4)",
    accent: "#F56FA7",
    glyph: "緣",
    intro: "분석은 충분히 했잖아요. 이제 움직일 차례예요. 다음 메시지, 다음 만남에서 뭘 하면 좋을지 딱 떨어지게 코칭해 드릴게요.",
    opener: "지금 어떤 상황이에요? 솔직하게 말해줘요. 같이 다음 한 수를 정해봐요.",
    system: "당신은 '지안'이라는 현실적이고 시원시원한 한국 연애 코치입니다. 분석보다 '그래서 뭘 할지' 행동 처방에 집중합니다. 보낼 메시지 예시, 타이밍, 하지 말아야 할 것을 콕 집어줍니다. 자존감을 지키는 방향을 항상 우선합니다. 친근하고 단단한 반말 섞인 존댓말(친한 언니/형 톤). 3~5문장.",
  },
];

/* 상황별 빠른 고민 */
const QUICK_TOPICS = [
  "답장이 점점 늦어져요",
  "썸인지 아닌지 모르겠어요",
  "고백할 타이밍일까요?",
  "읽씹을 당했어요",
  "이별 후 연락이 왔어요",
  "밀당을 해야 할까요?",
  "이 관계, 계속해도 될까요?",
  "먼저 연락해도 될까요?",
];

/* 데일리 운세 (날짜+이름 시드 결정론) */
function hashStr(s) { let h = 2166136261; for (let i=0;i<s.length;i++){ h^=s.charCodeAt(i); h=Math.imul(h,16777619);} return (h>>>0); }
function seededPick(seed, arr) { return arr[seed % arr.length]; }

const FORTUNE_LINES = {
  high: [
    "오늘은 마음을 먼저 내미는 쪽이 인연을 가져갑니다.",
    "오래 망설이던 한마디가 오늘 길을 엽니다.",
    "예상치 못한 곳에서 좋은 기운이 들어옵니다.",
  ],
  mid: [
    "서두르지 마세요. 오늘은 듣는 일이 더 큰 점수를 얻습니다.",
    "작은 다정함이 큰 오해를 막는 하루입니다.",
    "마음은 분명히, 표현은 천천히. 균형이 열쇠예요.",
  ],
  low: [
    "오늘은 결정을 미루세요. 감정이 판단을 흐릴 수 있어요.",
    "연락보다 정리가 필요한 날. 나를 먼저 돌보세요.",
    "지나친 해석은 금물. 있는 그대로 두는 게 좋아요.",
  ],
};
const LUCK_COLORS = ["딥 와인","아이보리","미드나잇 블루","올리브","샴페인 골드","더스티 로즈","차콜"];
const LUCK_TIMES  = ["오전 9–11시","정오 무렵","오후 3–5시","해질 무렵","밤 9시 이후"];

function dailyFortune(name) {
  const today = new Date();
  const key = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}-${name||"guest"}`;
  const seed = hashStr(key);
  const score = 55 + (seed % 45); // 55~99
  const band = score >= 85 ? "high" : score >= 70 ? "mid" : "low";
  return {
    score,
    headline: seededPick(seed, FORTUNE_LINES[band]),
    color: seededPick(seed >> 3, LUCK_COLORS),
    time: seededPick(seed >> 6, LUCK_TIMES),
    love: 40 + (seed % 60),
    meet: 40 + ((seed >> 2) % 60),
    stable: 40 + ((seed >> 4) % 60),
  };
}

Object.assign(window, {
  GAN, ZHI, ELEMENTS, PERSONAS, QUICK_TOPICS,
  computeSaju, compatScore, dailyFortune,
});
