const { useState, useEffect, useRef, Component } = React;

const MARK = "data:image/png;base64,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";

/* 극회 일곱 파트의 색 — 오프닝에서 모이고, 앱 배경에 흐리게 남는다 */
const PARTS = [
  { id: "direct", n: "연출", c: "#E8503A", sx: "-210px", sy: "-250px" },
  { id: "head",   n: "회장", c: "#F2A93B", sx: "230px",  sy: "-190px" },
  { id: "plan",   n: "기획", c: "#6FBF73", sx: "300px",  sy: "20px" },
  { id: "actor",  n: "배우", c: "#D8C63F", sx: "230px",  sy: "230px" },
  { id: "light",  n: "조명", c: "#45B4D6", sx: "40px",   sy: "310px" },
  { id: "sound",  n: "음향", c: "#3D5AC9", sx: "-180px", sy: "250px" },
  { id: "art",    n: "미술", c: "#B77FD4", sx: "-290px", sy: "60px" },
  { id: "stage",  n: "무대", c: "#E86FA8", sx: "-60px",  sy: "-300px" },
];
const partOf = (id) => PARTS.find((p) => p.id === id) || PARTS[0];
const MORE_LABEL = {
  letters: "받은 편지", approve: "일지 승인", sugg: "익명 건의함", notice: "공지 · 숙제", attend: "출석 체크", points: "포인트 지급",
  roles: "단원 · 역할", codes: "권한 코드", art: "그림 · 이름 관리", chars: "단원 캐릭터", link: "연결 상태", props: "무대 물품", me: "내 정보", look: "화면", sound: "소리",
};
const ROLES = { owner: "관리자", exec: "집행부", lead: "팀장", member: "부원" };
const MOODS = [
  { id: "great", e: "😆", n: "최고", c: "#F2A93B" },
  { id: "good",  e: "🙂", n: "좋음", c: "#6FBF73" },
  { id: "soso",  e: "😐", n: "그냥", c: "#7FA8C0" },
  { id: "tired", e: "😪", n: "피곤", c: "#B77FD4" },
  { id: "down",  e: "😔", n: "울적", c: "#5B7FC7" },
  { id: "sick",  e: "🤒", n: "아픔", c: "#E8503A" },
];
const moodOf = (id) => MOODS.find((m) => m.id === id);
const LOG_PT = 15;
const PROP_PT = 10;   /* 무대 물품을 등록하면 주는 포인트 */
const FORTUNE_COST = 5;

const ZODIAC = [
  { n: "물병자리", from: [1, 20], to: [2, 18] }, { n: "물고기자리", from: [2, 19], to: [3, 20] },
  { n: "양자리", from: [3, 21], to: [4, 19] }, { n: "황소자리", from: [4, 20], to: [5, 20] },
  { n: "쌍둥이자리", from: [5, 21], to: [6, 21] }, { n: "게자리", from: [6, 22], to: [7, 22] },
  { n: "사자자리", from: [7, 23], to: [8, 22] }, { n: "처녀자리", from: [8, 23], to: [9, 22] },
  { n: "천칭자리", from: [9, 23], to: [10, 23] }, { n: "전갈자리", from: [10, 24], to: [11, 22] },
  { n: "사수자리", from: [11, 23], to: [12, 21] }, { n: "염소자리", from: [12, 22], to: [1, 19] },
];
const zodiacOf = (m, d) => {
  for (const z of ZODIAC) {
    const [fm, fd] = z.from, [tm, td] = z.to;
    if (fm <= tm) { if ((m === fm && d >= fd) || (m === tm && d <= td) || (m > fm && m < tm)) return z.n; }
    else if ((m === fm && d >= fd) || (m === tm && d <= td)) return z.n;
  }
  return "염소자리";
};
const ANIMALS = ["원숭이", "닭", "개", "돼지", "쥐", "소", "호랑이", "토끼", "용", "뱀", "말", "양"];
const animalOf = (y) => ANIMALS[y % 12] + "띠";

/* 무대 용어로 쓴 운세 조각들 */
const F_HEAD = [
  "오늘은 조명이 당신을 따라옵니다", "무대가 조용히 당신을 기다립니다", "대사 한 줄이 마음에 남는 날",
  "리허설처럼 흘러가는 하루", "막이 오르기 직전의 설렘", "객석 어딘가에서 응원이 오는 날",
  "무대 뒤가 분주한 하루", "커튼콜을 미리 그려보아도 좋은 날",
];
const F_BODY = [
  "서두르면 큐를 놓칩니다. 한 박자만 늦춰보세요.",
  "먼저 건넨 한마디가 오래 남는 하루가 됩니다.",
  "어제 안 풀리던 동선이 오늘은 자연스럽게 잡힙니다.",
  "작은 실수는 아무도 눈치채지 못합니다. 이어가면 됩니다.",
  "혼자 짊어지지 말고 옆 사람에게 한 번만 물어보세요.",
  "준비한 것보다 즉흥이 빛나는 날입니다.",
  "목소리를 조금 낮추면 더 잘 들립니다.",
  "오늘 남긴 기록이 나중에 큰 도움이 됩니다.",
  "쉬어가는 것도 연습의 일부입니다.",
  "누군가 당신의 이름을 좋게 말하고 있습니다.",
];

/* 별자리별 오늘의 흐름 — 별자리마다 결이 다르게 */
const Z_TEXT = {
  "물병자리": ["남들과 다른 방식이 오히려 정답이 되는 날이에요. 이상하다는 말을 들어도 밀어붙여 보세요.",
    "혼자 있는 시간이 필요합니다. 거리를 두는 게 차가운 게 아니라 정비하는 거예요.",
    "새로운 제안이 들어옵니다. 익숙하지 않다고 바로 접지 마세요."],
  "물고기자리": ["감정이 평소보다 깊게 들어옵니다. 남의 기분까지 떠안지 않게 조심하세요.",
    "상상한 게 실제로 맞아떨어지는 날입니다. 직감을 믿어도 좋아요.",
    "울고 싶으면 울어도 괜찮습니다. 비운 만큼 다시 채워집니다."],
  "양자리": ["시작하기 좋은 날입니다. 미뤄둔 걸 오늘 첫 삽만 떠보세요.",
    "속도가 앞섭니다. 한 번만 뒤돌아보면 놓친 게 보여요.",
    "부딪히는 상황이 생겨도 물러서지 않는 편이 낫습니다."],
  "황소자리": ["꾸준히 해온 게 드디어 표가 나는 날이에요.",
    "바꾸지 않아도 됩니다. 지금 방식이 맞습니다.",
    "먹는 것과 자는 것부터 챙기세요. 몸이 먼저입니다."],
  "쌍둥이자리": ["말이 술술 풀립니다. 설득해야 할 일이 있다면 오늘이에요.",
    "관심사가 여러 갈래로 흩어집니다. 하나만 끝내고 넘어가세요.",
    "오랜만의 연락이 반가운 소식을 데려옵니다."],
  "게자리": ["가까운 사람 때문에 마음이 오르내립니다. 너무 깊게 담아두지 마세요.",
    "누군가를 챙기는 일이 자연스럽게 맡겨집니다. 그게 잘 어울리는 날이에요.",
    "집이나 익숙한 자리로 돌아가면 회복이 빠릅니다."],
  "사자자리": ["시선이 모입니다. 주저하지 말고 앞에 서세요.",
    "칭찬을 받으면 그대로 받으세요. 겸손이 지나치면 아쉬워집니다.",
    "자존심을 세울 자리와 접을 자리를 구분하면 완벽한 하루입니다."],
  "처녀자리": ["디테일이 눈에 들어옵니다. 점검하는 일에 강한 날이에요.",
    "완벽하지 않아도 넘어가는 연습을 해보세요. 80이면 충분합니다.",
    "정리정돈이 기분까지 바꿔줍니다."],
  "천칭자리": ["결정을 미루면 더 어려워집니다. 오늘 안에 하나만 정하세요.",
    "사이에 껴서 중재할 일이 생깁니다. 당신이 제일 잘하는 일이에요.",
    "보기 좋은 것에 마음이 갑니다. 그 감각을 믿어도 됩니다."],
  "전갈자리": ["숨기고 있던 게 드러나도 손해는 아닙니다.",
    "한 가지에 깊게 파고들면 오늘 답이 나옵니다.",
    "믿을 사람과 아닌 사람이 또렷하게 갈립니다."],
  "사수자리": ["멀리 가고 싶어지는 날입니다. 짧게라도 움직여 보세요.",
    "솔직한 말이 힘이 됩니다. 다만 한 번만 고르고 말하세요.",
    "배우고 싶은 게 생깁니다. 바로 찾아보면 오래갑니다."],
  "염소자리": ["맡은 몫이 늘어납니다. 버겁다면 나눠달라고 말해도 됩니다.",
    "오늘 쌓은 게 나중에 정확히 돌아옵니다.",
    "성과보다 과정이 눈에 띄는 날이에요."],
};
/* 띠별 한 줄 */
const A_TEXT = {
  "쥐띠": "기회를 알아보는 눈이 밝습니다. 작은 정보 하나를 흘려듣지 마세요.",
  "소띠": "서두르지 않아도 제자리에 도착합니다. 조급함만 내려놓으면 됩니다.",
  "호랑이띠": "기세가 좋습니다. 먼저 나서면 흐름이 따라옵니다.",
  "토끼띠": "부드럽게 넘어가는 편이 이깁니다. 정면으로 부딪히지 마세요.",
  "용띠": "눈에 띄는 날입니다. 준비한 걸 보여줄 자리가 생깁니다.",
  "뱀띠": "말수를 줄이면 얻는 게 많아집니다. 듣는 쪽에 서보세요.",
  "말띠": "움직일수록 풀립니다. 가만히 있으면 답답해집니다.",
  "양띠": "주변이 챙겨줍니다. 도움을 받는 데 미안해하지 마세요.",
  "원숭이띠": "재치로 넘길 일이 생깁니다. 너무 진지하게 받지 마세요.",
  "닭띠": "꼼꼼함이 빛납니다. 남들이 놓친 걸 당신이 찾아냅니다.",
  "개띠": "누군가를 지켜주는 자리에 섭니다. 든든한 하루예요.",
  "돼지띠": "인복이 있습니다. 함께 하면 훨씬 수월해집니다.",
};
const F_LUCK = [
  { k: "행운의 색", v: [
    { n: "앰버", c: "#E9A13B" }, { n: "코발트", c: "#3D5AC9" }, { n: "로즈", c: "#E86FA8" },
    { n: "포레스트", c: "#4F6B52" }, { n: "라벤더", c: "#B77FD4" }, { n: "딥레드", c: "#8E2F35" },
    { n: "아이보리", c: "#EDE4D2" }, { n: "터콰이즈", c: "#45B4D6" },
  ] },
  { k: "행운의 소품", v: ["낡은 우산", "손거울", "회중시계", "붉은 장갑", "빈 액자", "종이꽃", "가죽 수첩", "은색 열쇠"] },
  { k: "어울리는 자리", v: ["무대 중앙", "하수 쪽", "상수 쪽", "객석 맨 앞줄", "조명실", "분장실", "무대 뒤", "연습실 거울 앞"] },
];
const F_RANK = [
  { n: "대성황", c: "#E9A13B", min: 88 }, { n: "기립박수", c: "#6FBF73", min: 70 },
  { n: "무난한 공연", c: "#7FB0C9", min: 48 }, { n: "리허설", c: "#B77FD4", min: 26 },
  { n: "암전", c: "#8A97A3", min: 0 },
];

/* 같은 사람·같은 날이면 항상 같은 결과가 나오도록 섞는다 */
const seedOf = (str) => {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
  return h;
};
const pickBy = (seed, arr, salt) => arr[(seed ^ (salt * 2654435761)) % arr.length];

function readFortune(birth, dateStr, id) {
  const parts = String(birth || "").split("-").map(Number);
  const by = parts[0] || 2000, bm = parts[1] || 1, bd = parts[2] || 1;
  const seed = seedOf(`${id}:${birth}:${dateStr}`);
  const score = seed % 101;
  const rank = F_RANK.find((r) => score >= r.min) || F_RANK[F_RANK.length - 1];
  const zodiac = zodiacOf(bm, bd) || "양자리";
  const animal = animalOf(by) || "쥐띠";
  const color = pickBy(seed, F_LUCK[0].v, 11);
  return {
    zodiac, animal, score, rank,
    head: pickBy(seed, F_HEAD, 3),
    body: pickBy(seed, F_BODY, 7),
    zText: pickBy(seed, Z_TEXT[zodiac] || Z_TEXT["양자리"] || ["오늘도 무대는 당신을 기다립니다."], 17),
    aText: A_TEXT[animal] || "오늘 하루도 당신답게 보내면 충분합니다.",
    color,
    prop: pickBy(seed, F_LUCK[1].v, 23),
    place: pickBy(seed, F_LUCK[2].v, 31),
  };
}
const ATT = {
  present: { n: "출석", pt: 10, c: "#6FBF73" },
  late:    { n: "지각", pt: -5, c: "#E9A13B" },
  absent:  { n: "결석", pt: -15, c: "#C4645C" },
};

const THEMES = {
  black: { n: "블랙", dark: true, bg: "#12181E", surface: "#1A222B", edge: "#2B3742", accent: "#E9A13B", on: "#20160A", text: "#E4E9ED", muted: "#8A97A3", swatch: "#12181E" },
  sky: { n: "하늘색", dark: false, bg: "#EDF3F8", surface: "#FFFFFF", edge: "#D2DFEA", accent: "#3F8FC2", on: "#FFFFFF", text: "#1C2A34", muted: "#6E8494", swatch: "#7FC0E4" },
  pink: { n: "연핑크", dark: false, bg: "#FAF0F2", surface: "#FFFFFF", edge: "#EDD9DE", accent: "#CE6C89", on: "#FFFFFF", text: "#33232A", muted: "#967B84", swatch: "#F0BDCB" },
  brown: { n: "브라운", dark: true, bg: "#221A15", surface: "#2E241D", edge: "#443629", accent: "#C98A52", on: "#241A10", text: "#EFE4D7", muted: "#9C8975", swatch: "#7A5638" },
  rainbow: { n: "무지개", dark: true, rainbow: true, bg: "#14101E", surface: "#241C36", edge: "#4A3A6E", accent: "#FF4FD8", on: "#1A0A18", text: "#FFF4FF", muted: "#B79BD8", swatch: "#FF4FD8" },
  olive: { n: "올리브", dark: false, bg: "#F8F9F4", surface: "#FFFFFF", edge: "#DEE3D0", accent: "#9AA16A", on: "#FFFFFF", text: "#2B2F23", muted: "#7C8370", swatch: "#9AA16A" },
  gray: { n: "그레이", dark: false, bg: "#EDEDEC", surface: "#FFFFFF", edge: "#D7D7D5", accent: "#525C65", on: "#FFFFFF", text: "#23272B", muted: "#7B838B", swatch: "#9AA1A7" },
};
const SCRIPT = "#F2EDE3", SCRIPT_INK = "#26221C";


const RARITY = {
  common: { n: "일반", c: "#8A97A3", rate: 60 },
  rare:   { n: "희귀", c: "#45B4D6", rate: 27 },
  epic:   { n: "특별", c: "#B77FD4", rate: 12 },
  legend: { n: "전설", c: "#E9A13B", rate: 1 },
};
const GACHA_COST = 30, DUP_REFUND = 15;

/* 무대 소품 — 색은 테마와 무관하게 고정된다. 진짜 소품처럼 */
const W = "#8B6A4A", WD = "#54402F", MT = "#7C868E", CR = "#E8DFCE",
      RD = "#8E2F35", BR = "#C09A4E", GN = "#4F6B52", BK = "#232A30";

const DEFAULT_ITEMS = [
  { id: "chair", n: "접이의자", tier: "common", zone: "floor" },
  { id: "crate", n: "나무상자", tier: "common", zone: "floor" },
  { id: "plant", n: "화분", tier: "common", zone: "floor" },
  { id: "rug", n: "러그", tier: "common", zone: "floor" },
  { id: "frame", n: "빈 액자", tier: "common", zone: "wall" },
  { id: "lamp", n: "스탠드 조명", tier: "rare", zone: "floor" },
  { id: "table", n: "원형 테이블", tier: "rare", zone: "floor" },
  { id: "trunk", n: "여행 트렁크", tier: "rare", zone: "floor" },
  { id: "mirror", n: "전신 거울", tier: "rare", zone: "floor" },
  { id: "curtain", n: "창가 커튼", tier: "rare", zone: "wall" },
  { id: "piano", n: "업라이트 피아노", tier: "epic", zone: "floor" },
  { id: "rack", n: "의상 행거", tier: "epic", zone: "floor" },
  { id: "gramo", n: "축음기", tier: "epic", zone: "floor" },
  { id: "spot", n: "스포트라이트", tier: "epic", zone: "wall" },
  { id: "velvet", n: "벨벳 막", tier: "legend", zone: "wall" },
  { id: "plaque", n: "한울 현판", tier: "legend", zone: "wall" },
];
let ITEMS = DEFAULT_ITEMS;
const itemOf = (id) => ITEMS.find((i) => i.id === id) || DEFAULT_ITEMS[0];

/* ── 캐릭터 ── */
const HAIRS = [
  { id: "longwave", n: "긴 웨이브" },
  { id: "layered", n: "짧은 레이어드" },
  { id: "bob", n: "단발" },
  { id: "tied", n: "묶은 머리" },
  { id: "curl", n: "곱슬 숏" },
];
const FITS = [
  { id: "street", n: "올블랙 스트릿", crop: true, bag: true, chain: true,
    c: { j: "#1C1C21", jh: "#2E2E35", i: "#141418", p: "#1A1A1F", ph: "#28282F", s: "#F2F2EF", a: "#C9CDD3" } },
  { id: "hip", n: "힙 스트릿",
    c: { j: "#2E3238", jh: "#3E444C", i: "#EDE9E2", p: "#6E6C4F", ph: "#82805F", s: "#F0EFEA", a: "#C09A4E" } },
  { id: "black", n: "무대 올블랙",
    c: { j: "#1A1C1F", jh: "#2A2E33", i: "#26292D", p: "#202225", ph: "#2E3135", s: "#151719", a: "#8A97A3" } },
  { id: "denim", n: "데님 캐주얼",
    c: { j: "#4E6E8C", jh: "#6288A8", i: "#EDE9E2", p: "#39506B", ph: "#486483", s: "#E8E4DC", a: "" } },
  { id: "coat", n: "롱코트", long: true,
    c: { j: "#7A5B44", jh: "#967256", i: "#EDE9E2", p: "#2E3238", ph: "#3C4149", s: "#3A2E26", a: "" } },
];
const HAIRC = ["#1B1A1C", "#4A3428", "#8C4A3A", "#B98A4E", "#6E7480"];
const SKINS = ["#F7DFCB", "#E3BC9A", "#BB8B67"];
const DEFAULT_CH = { hair: "layered", fit: "hip", hc: 0, sk: 0 };
const OWNER_CH = { hair: "longwave", fit: "street", hc: 0, sk: 0 };

function Character({ ch, viewBox = "0 0 120 180", className = "" }) {
  const c = { ...DEFAULT_CH, ...(ch || {}) };
  const F = FITS.find((f) => f.id === c.fit) || FITS[1];
  const K = F.c, HC = HAIRC[c.hc] || HAIRC[0], SK = SKINS[c.sk] || SKINS[0];
  const HL = HC === "#1B1A1C" ? "#38363C" : HC + "";
  const hair = c.hair;
  const legTop = F.long ? 148 : 120;

  return (
    <svg viewBox={viewBox} xmlns="http://www.w3.org/2000/svg" className={className}>
      {/* 뒤로 넘어간 머리 */}
      {hair === "longwave" && <g>
        <path d="M60,10 C99,10 117,42 115,82 C113,112 107,136 99,158 C97,133 100,108 97,86 C94,54 83,38 60,38 C37,38 26,54 23,86 C20,108 23,133 21,158 C13,136 7,112 5,82 C3,42 21,10 60,10 Z" fill={HC} />
        <path d="M104,64 C108,92 104,122 98,150 C102,120 103,90 100,66 Z" fill={HL} opacity=".55" />
        <path d="M16,64 C12,92 16,122 22,150 C18,120 17,90 20,66 Z" fill={HL} opacity=".4" />
      </g>}
      {hair === "bob" && <path d="M60,12 C94,12 108,38 106,80 C105,92 103,98 100,104 L88,100 C92,74 88,50 60,50 C32,50 28,74 32,100 L20,104 C17,98 15,92 14,80 C12,38 26,12 60,12 Z" fill={HC} />}
      {hair === "tied" && <g>
        <ellipse cx="60" cy="10" rx="17" ry="14" fill={HC} />
        <path d="M60,14 C92,14 104,38 102,72 C88,58 76,52 60,52 C44,52 32,58 18,72 C16,38 28,14 60,14 Z" fill={HC} />
      </g>}
      {(hair === "layered" || hair === "curl") && <path d="M60,12 C93,12 105,38 103,74 C88,60 76,54 60,54 C44,54 32,60 17,74 C15,38 27,12 60,12 Z" fill={HC} />}

      {/* 다리 · 신발 */}
      <path d={`M40,${legTop} L58,${legTop} L59,158 L30,158 Z`} fill={K.p} />
      <path d={`M62,${legTop} L80,${legTop} L90,158 L61,158 Z`} fill={K.p} />
      <path d={`M40,${legTop} L48,${legTop} L46,158 L34,158 Z`} fill={K.ph} opacity=".5" />
      <path d={`M62,${legTop} L70,${legTop} L72,158 L60,158 Z`} fill={K.ph} opacity=".5" />
      {F.crop && <><rect x="33" y="140" width="6" height="9" rx="2" fill={K.ph} /><rect x="80" y="140" width="6" height="9" rx="2" fill={K.ph} /></>}
      <rect x="27" y="152" width="33" height="19" rx="9" fill={K.s} />
      <rect x="60" y="152" width="33" height="19" rx="9" fill={K.s} />
      <path d="M27,165 h33 v4 a9,9 0 0 1 -9,4 h-15 a9,9 0 0 1 -9,-8 Z" fill="#00000022" />
      <path d="M93,165 h-33 v4 a9,9 0 0 0 9,4 h15 a9,9 0 0 0 9,-8 Z" fill="#00000022" />

      {/* 몸통 */}
      <rect x="43" y="86" width="34" height={F.crop ? 26 : 38} rx="5" fill={K.i} />
      {F.crop && <rect x="45" y="110" width="30" height="12" fill={SK} />}
      {/* 소매 */}
      <path d="M38,88 q-13,20 -9,40 l15,3 q-3,-22 8,-40 Z" fill={K.j} />
      <path d="M82,88 q13,20 9,40 l-15,3 q3,-22 -8,-40 Z" fill={K.j} />
      <path d="M38,88 q-13,20 -9,40 l6,1 q-2,-23 9,-41 Z" fill={K.jh} opacity=".55" />
      <ellipse cx="34" cy="133" rx="7" ry="6" fill={SK} />
      <ellipse cx="86" cy="133" rx="7" ry="6" fill={SK} />
      {/* 겉옷 */}
      <path d={`M36,88 q10,-7 18,-3 l-3,${(F.long ? 62 : 40)} h-19 Z`} fill={K.j} />
      <path d={`M84,88 q-10,-7 -18,-3 l3,${(F.long ? 62 : 40)} h19 Z`} fill={K.j} />
      <path d="M36,88 q10,-7 18,-3 l-1,7 q-8,-3 -16,3 Z" fill={K.jh} opacity=".7" />
      {F.bag && <>
        <path d="M52,88 L40,112" stroke={K.jh} strokeWidth="3" fill="none" />
        <rect x="28" y="108" width="24" height="17" rx="6" fill="#17171B" />
        <rect x="28" y="108" width="24" height="5" rx="3" fill="#2B2B31" />
      </>}
      {F.chain && <>
        <path d="M50,84 Q60,95 70,84" stroke={K.a} strokeWidth="1.6" fill="none" />
        <path d="M58,93 h4 v4 h3 v3 h-3 v5 h-4 v-5 h-3 v-3 h3 Z" fill={K.a} />
      </>}

      {/* 목 · 얼굴 */}
      <rect x="53" y="76" width="14" height="14" rx="4" fill={SK} />
      <rect x="53" y="76" width="14" height="6" fill="#00000022" />
      <ellipse cx="60" cy="54" rx="32" ry="33" fill={SK} />
      <ellipse cx="28" cy="60" rx="5" ry="6.5" fill={SK} />
      <ellipse cx="92" cy="60" rx="5" ry="6.5" fill={SK} />

      {/* 앞머리 */}
      {hair === "longwave" && <g>
        <path d="M60,13 C93,13 106,40 104,68 C97,48 86,35 71,33 C67,44 64,48 60,50 C56,48 53,44 49,33 C34,35 23,48 16,68 C14,40 27,13 60,13 Z" fill={HC} />
        <path d="M18,62 C13,92 15,124 20,150 C27,126 27,92 25,66 Z" fill={HC} />
        <path d="M102,62 C107,92 105,124 100,150 C93,126 93,92 95,66 Z" fill={HC} />
        <path d="M60,15 C78,15 90,26 96,42 C86,30 74,24 60,24 Z" fill={HL} opacity=".45" />
      </g>}
      {hair === "layered" && <g>
        <path d="M60,13 C94,13 106,40 103,72 L96,54 L90,68 L82,48 L73,66 L64,46 L55,66 L46,48 L38,66 L31,52 L17,72 C14,40 26,13 60,13 Z" fill={HC} />
        <path d="M18,58 L12,96 L26,78 Z" fill={HC} />
        <path d="M102,58 L108,96 L94,78 Z" fill={HC} />
      </g>}
      {hair === "bob" && <path d="M60,13 C94,13 107,40 104,70 C96,50 82,40 60,40 C38,40 24,50 16,70 C13,40 26,13 60,13 Z" fill={HC} />}
      {hair === "tied" && <path d="M60,15 C90,15 102,38 100,64 C88,50 76,44 60,44 C44,44 32,50 20,64 C18,38 30,15 60,15 Z" fill={HC} />}
      {hair === "curl" && <g>
        {[[30, 34], [44, 22], [60, 18], [76, 22], [90, 34]].map(([x, y], n) => <circle key={n} cx={x} cy={y} r="17" fill={HC} />)}
        <path d="M60,20 C88,20 100,40 98,62 C86,46 76,40 60,40 C44,40 34,46 22,62 C20,40 32,20 60,20 Z" fill={HC} />
      </g>}

      {/* 이목구비 */}
      <ellipse cx="44" cy="62" rx="9" ry="11.5" fill="#241F1B" />
      <ellipse cx="76" cy="62" rx="9" ry="11.5" fill="#241F1B" />
      <ellipse cx="41" cy="57" rx="3.4" ry="4" fill="#FFFFFF" opacity=".92" />
      <ellipse cx="73" cy="57" rx="3.4" ry="4" fill="#FFFFFF" opacity=".92" />
      <ellipse cx="47" cy="68" rx="2" ry="2.4" fill="#FFFFFF" opacity=".45" />
      <ellipse cx="79" cy="68" rx="2" ry="2.4" fill="#FFFFFF" opacity=".45" />
      <ellipse cx="31" cy="72" rx="6" ry="3.4" fill="#F0A0A0" opacity=".45" />
      <ellipse cx="89" cy="72" rx="6" ry="3.4" fill="#F0A0A0" opacity=".45" />
      <path d="M57,74 Q60,76.5 63,74" stroke="#B07A6A" strokeWidth="1.6" fill="none" strokeLinecap="round" />
    </svg>
  );
}

/* ── 소리 ──────────────────────────────────────────────
   음원 파일 없이 브라우저가 직접 합성한다. 나중에 라이선스 받은
   음원이 생기면 BGM 부분만 <audio> 재생으로 갈아끼우면 된다. */
const N = (m) => 440 * Math.pow(2, (m - 69) / 12);   // 미디 음번호 → 주파수

/* 세 곡 모두 베이스 · 화음 · 멜로디 세 성부로 짜여 있다 */
const BGM = {
  backstage: {
    n: "무대 뒤", d: "낮게 깔리는 어두운 화음", step: .85,
    ch: [[57,60,64,67],[53,57,60,65],[48,52,55,60],[55,59,62,67]],
    bs: [45,41,36,43],
    mel: [69,72,74,76,79,76,74,72],
    play(A, s, t) {
      const bar = Math.floor(s / 4) % 4;
      if (s % 4 === 0) { A.pad(t, this.ch[bar], 3.9, .085); A.bass(t, N(this.bs[bar]), 3.4, .16); }
      if (s % 4 === 2) A.bass(t, N(this.bs[bar] + 12), 1.5, .07);
      if (s % 8 === 6) A.bell(t, N(this.mel[Math.floor(s / 8) % 8]), 2.6, .055);
    },
  },
  practice: {
    n: "연습실", d: "가볍게 튕기는 아르페지오", step: .3,
    ch: [[60,64,67,72],[57,60,64,69],[53,57,60,65],[55,59,62,67]],
    bs: [36,33,29,31],
    play(A, s, t) {
      const bar = Math.floor(s / 8) % 4, c = this.ch[bar];
      const order = [0,1,2,3,2,1,2,3];
      A.pluck(t, N(c[order[s % 8]]), .85, .055);
      if (s % 8 === 0) { A.bass(t, N(this.bs[bar]), 2.2, .13); A.pad(t, c, 2.4, .035); }
      if (s % 32 === 16) A.bell(t, N(c[3] + 12), 2.2, .04);
    },
  },
  curtain: {
    n: "커튼콜", d: "밝게 번지는 화음과 종소리", step: .5,
    ch: [[53,57,60,65],[50,53,57,62],[46,50,53,58],[48,52,55,60]],
    bs: [41,38,34,36],
    mel: [77,81,79,84,81,79,77,72],
    play(A, s, t) {
      const bar = Math.floor(s / 8) % 4;
      if (s % 8 === 0) { A.pad(t, this.ch[bar], 4.4, .07); A.bass(t, N(this.bs[bar]), 3.6, .13); }
      if (s % 4 === 0) A.bell(t, N(this.mel[Math.floor(s / 4) % 8]), 2.8, .05);
      if (s % 8 === 5) A.pluck(t, N(this.ch[bar][2] + 12), 1.1, .035);
    },
  },
  custom: { n: "내가 넣은 음원", d: "파일이나 링크를 직접 지정해요", custom: true },
};

const A = {
  ctx: null, master: null, bgmG: null, sfxG: null,
  timer: null, step: 0, next: 0, track: "off", sfxOn: true, vol: .5,

  start() {
    if (this.ctx) { if (this.ctx.state === "suspended") this.ctx.resume(); return; }
    const C = window.AudioContext || window.webkitAudioContext;
    if (!C) return;
    this.ctx = new C();
    this.master = this.ctx.createGain(); this.master.gain.value = .9; this.master.connect(this.ctx.destination);
    this.bgmG = this.ctx.createGain(); this.bgmG.gain.value = this.vol * .55;
    this.sfxG = this.ctx.createGain(); this.sfxG.gain.value = .7;
    const d = this.ctx.createDelay(1), fb = this.ctx.createGain(), lp = this.ctx.createBiquadFilter();
    d.delayTime.value = .33; fb.gain.value = .28; lp.type = "lowpass"; lp.frequency.value = 2000;
    d.connect(fb); fb.connect(lp); lp.connect(d); d.connect(this.master);
    this.bgmG.connect(this.master); this.bgmG.connect(d); this.sfxG.connect(this.master);
    if (this.track !== "off") this.run();
  },

  tone(t, freq, dur, o = {}) {
    if (!this.ctx) return;
    const { type = "sine", vol = .1, atk = .02, lp = 3000, det = 0 } = o;
    const g = this.ctx.createGain(), f = this.ctx.createBiquadFilter();
    f.type = "lowpass"; f.frequency.value = lp;
    g.gain.setValueAtTime(.0001, t);
    g.gain.exponentialRampToValueAtTime(Math.max(vol, .0002), t + atk);
    g.gain.exponentialRampToValueAtTime(.0001, t + dur);
    f.connect(g); g.connect(this.bgmG);
    const cents = det ? [-det, det] : [0];
    cents.forEach((c) => {
      const osc = this.ctx.createOscillator();
      osc.type = type; osc.frequency.value = freq; osc.detune.value = c;
      osc.connect(f); osc.start(t); osc.stop(t + dur + .05);
    });
  },

  /* 음색 넷 */
  pad(t, midis, dur, vol) { midis.forEach((m, i) => this.tone(t + i * .06, N(m), dur, { vol, atk: dur * .28, lp: 1300, det: 6 })); },
  bass(t, f, dur, vol) { this.tone(t, f, dur, { vol, atk: .08, lp: 420 }); },
  pluck(t, f, dur, vol) { this.tone(t, f, dur, { type: "triangle", vol, atk: .006, lp: 2400, det: 4 }); },
  bell(t, f, dur, vol) { this.tone(t, f, dur, { vol, atk: .006, lp: 6000 }); this.tone(t, f * 2.01, dur * .5, { vol: vol * .35, atk: .004, lp: 8000 }); },

  /* 뾱 — 주파수를 짧게 끌어올리는 물방울 소리 */
  pop(t, f0, f1, dur, vol = 1, type = "sine") {
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator(), g = this.ctx.createGain(), f = this.ctx.createBiquadFilter();
    osc.type = type;
    f.type = "lowpass"; f.frequency.value = 5200;
    osc.frequency.setValueAtTime(f0, t);
    osc.frequency.exponentialRampToValueAtTime(f1, t + dur * .55);
    g.gain.setValueAtTime(.0001, t);
    g.gain.exponentialRampToValueAtTime(.55 * vol, t + .006);
    g.gain.exponentialRampToValueAtTime(.0001, t + dur);
    osc.connect(f); f.connect(g); g.connect(this.sfxG);
    osc.start(t); osc.stop(t + dur + .03);
  },

  sfx(name) {
    if (!this.sfxOn) return;
    this.start();
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    if (name === "tap") this.pop(t, 540, 1120, .10);
    else if (name === "confirm") { this.pop(t, 620, 1180, .10); this.pop(t + .085, 800, 1520, .11); }
    else if (name === "back") this.pop(t, 900, 440, .12);
    else if (name === "deny") this.pop(t, 300, 190, .2, .8, "triangle");
    else if (name === "reward") [0, .1, .2, .34].forEach((d, i) =>
      this.pop(t + d, 520 + i * 190, 1150 + i * 320, .16, .85));
    else if (name === "door") {
      this.pop(t, 210, 150, .22, .8, "triangle");
      this.tone(t + .18, 392, 1.4, { vol: .1, atk: .02, lp: 2600 });
      this.tone(t + .34, 523.25, 1.6, { vol: .09, atk: .02, lp: 3400 });
    }
    else if (name === "curtain") {
      this.tone(t, 82.41, 1.4, { vol: .16, atk: .04, lp: 500 });
      this.tone(t + .12, 123.47, 1.2, { vol: .1, atk: .05, lp: 620 });
    }
    else if (name.startsWith("tier_")) {
      const k = name.slice(5);
      const seq = { common: [0], rare: [0, .12], epic: [0, .1, .2, .34], legend: [0, .09, .18, .27, .4, .56] }[k] || [0];
      const base = { common: 480, rare: 540, epic: 600, legend: 660 }[k] || 500;
      seq.forEach((d, i) => this.pop(t + d, base + i * 165, (base + i * 165) * 2.2, .17, .9));
      if (k === "legend" || k === "epic") this.tone(t + .6, k === "legend" ? 1046.5 : 783.99, 2.4, { vol: .13, atk: .01, lp: 7000 });
    }
    else if (name === "bell") this.tone(t, 523.25, 2.4, { vol: .16, atk: .01, lp: 5000 });
  },

  run() {
    clearInterval(this.timer);
    this.next = this.ctx.currentTime + .1;
    this.timer = setInterval(() => {
      const T = BGM[this.track];
      if (!T || !this.ctx) return;
      while (this.next < this.ctx.currentTime + .6) { T.play(this, this.step, this.next); this.step++; this.next += T.step; }
    }, 200);
  },

  setTrack(id, url) {
    clearInterval(this.timer); this.timer = null;
    if (this.el) { this.el.pause(); this.el = null; }
    this.track = id; this.step = 0;
    if (id === "off") return;
    if (id === "custom") {
      if (!url) return;
      this.el = new Audio(url);
      this.el.loop = true; this.el.volume = this.vol;
      this.el.play().catch(() => { this.onFail && this.onFail(); });
      return;
    }
    this.start();
    if (this.ctx) this.run();
  },

  el: null, onFail: null,
  setVol(v) { this.vol = v; if (this.bgmG) this.bgmG.gain.value = v * .55; if (this.el) this.el.volume = v; },
  setSfx(on) { this.sfxOn = on; },
};

/* 넓은 방 — 벽 두 면이 가운데(240,108)에서 만나고 바닥이 좌우 끝까지 퍼진다. 화면 480×300 */
const ROOM = {
  corner: [240, 108], lw: [32, 188], rw: [448, 188],
  floor: "32,188 240,108 448,188 448,248 416,276 64,276 32,248",
  wallL: "32,188 240,108 240,12 32,12",
  wallR: "448,188 240,108 240,12 448,12",
  edge: "32,12 32,248 64,276 416,276 448,248 448,12",
};
const FLOOR_ROWS = [
  { y: 152, s: .84, xs: [168, 240, 312] },
  { y: 192, s: .94, xs: [78, 159, 240, 321, 402] },
  { y: 232, s: 1.04, xs: [62, 151, 329, 418] },
  { y: 266, s: 1.14, xs: [92, 166, 314, 388] },
];
const CHAR_POS = { x: 240, y: 266 };
const SLOTS = [
  ...FLOOR_ROWS.flatMap((r, ri) => r.xs.map((x, ci) => ({ id: `f${ri}${ci}`, zone: "floor", x, y: r.y, s: r.s }))),
  ...[0.20, 0.50, 0.80].map((t, i) => ({ id: `wl${i}`, zone: "wall", side: "l", t, h: [58, 76, 52][i] })),
  ...[0.20, 0.50, 0.80].map((t, i) => ({ id: `wr${i}`, zone: "wall", side: "r", t, h: [58, 76, 52][i] })),
];
const wallPos = (sl) => {
  const [cx, cy] = ROOM.corner;
  const [ex, ey] = sl.side === "l" ? ROOM.lw : ROOM.rw;
  return { x: ex + (cx - ex) * sl.t, y: ey + (cy - ey) * sl.t - sl.h, skew: sl.side === "l" ? -0.381 : 0.381 };
};

/* 세트 — 벽·바닥·창을 통째로 바꾼다 */
const DEFAULT_SETS = {
  none: { n: "기본 무대", cost: 0, d: "아무것도 없는 빈 무대",
    wl: "#2E3841", wr: "#26303A", fl: "#3C444D", ln: "#4A545E", tone: "#8A97A3", back: null },
  library: { n: "잊혀진 서재", cost: 1500, d: "넝쿨 덮인 책장과 낡은 나무 바닥",
    wl: "#6B5642", wr: "#57442F", fl: "#7A5C3C", ln: "#8A6D4C", tone: "#E9C08A", back: "books" },
  chapel: { n: "색유리 예배당", cost: 4000, d: "스테인드글라스로 빛이 쏟아진다",
    wl: "#5D6775", wr: "#4C5563", fl: "#6E6A66", ln: "#7C8494", tone: "#E4A6C8", back: "glass" },
  moonlit: { n: "달빛 서재", cost: 10000, d: "고딕 창 너머로 달빛이 든다",
    wl: "#27406A", wr: "#1D3053", fl: "#3B3652", ln: "#3E5686", tone: "#9FC4E8", back: "gothic" },
};
let SETS = DEFAULT_SETS;
const setOf = (id) => SETS[id] || SETS.none;

const K_MEMBERS = "hanul:members", K_LOGS = "hanul:logs", K_PREF = "hanul:pref", K_ROOMS = "hanul:rooms",
      K_EVENTS = "hanul:events", K_COM = "hanul:comments", K_ATT = "hanul:attend",
      K_SUGG = "hanul:sugg", K_LETTER = "hanul:letters", K_CODES = "hanul:codes", K_ADJ = "hanul:adjust",
      K_LIKE = "hanul:likes", K_GUEST = "hanul:guest", K_MOOD = "hanul:mood",
      K_NOTICE = "hanul:notice", K_QUOTE = "hanul:quotes", K_PROPS = "hanul:props", K_SEEN = "hanul:seen", K_FORT = "hanul:fortune", K_GAME = "hanul:game", K_PORT = "hanul:port", K_ADJ2 = "hanul:mktadj";
const DEFAULT_CODES = { exec: "HANUL-EXEC-81", lead: "HANUL-LEAD-81" };

/* 비밀번호는 해시로만 저장한다. 프로토타입 수준의 보호이므로
   다른 곳에서 쓰는 비밀번호를 그대로 넣으면 안 된다. */
async function hashPw(id, pw) {
  const src = `hanul:${id}:${pw}`;
  try {
    if (typeof crypto !== "undefined" && crypto.subtle) {
      const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(src));
      return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
    }
  } catch (e) { /* 샌드박스에서 막히면 아래로 */ }
  let a = 0x811c9dc5, b = 0x01000193;
  for (let i = 0; i < src.length; i++) {
    const c = src.charCodeAt(i);
    a = ((a ^ c) * 16777619) >>> 0;
    b = ((b + c * 131) ^ (b << 7)) >>> 0;
  }
  return "f" + a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
}

/* 예전 형식으로 저장된 단원 데이터를 새 형식으로 맞춰준다 */
const migrate = (list) => {
  if (!Array.isArray(list) || list.length === 0) return SEED;
  const out = list.filter((m) => m && m.id && m.name).map((m) => ({
    id: m.id,
    name: m.name,
    gen: Number(m.gen) || 1,
    part: m.part || "plan",
    role: m.role || (m.admin ? "exec" : "member"),
    pw: m.pw ?? null,
    ch: m.ch,
    birth: m.birth || null,
  }));
  if (out.length === 0) return SEED;
  if (!out.some((m) => m.role === "owner")) {
    const i = out.findIndex((m) => m.role === "exec");
    out[i >= 0 ? i : 0].role = "owner";
  }
  const ow = out.find((m) => m.role === "owner");
  if (ow && !ow.pw) { ow.name = SEED[0].name; ow.gen = SEED[0].gen; }
  return out;
};

/* 파일을 직접 읽어 데이터로 바꾼다. 임시 주소(blob) 방식은 격리 화면에서 막힐 수 있다 */
const readData = (blob) => new Promise((res, rej) => {
  const fr = new FileReader();
  fr.onload = () => res(fr.result);
  fr.onerror = () => rej(new Error("파일을 읽지 못했어요"));
  fr.readAsDataURL(blob);
});
const loadImg = (src) => new Promise((res, rej) => {
  const im = new Image();
  im.onload = () => res(im);
  im.onerror = () => rej(new Error("decode"));
  im.src = src;
});

/* 그림을 줄인다. 줄이지 못하면 원본을 그대로 쓴다 */
async function fitImage(file, max, mode) {
  if (file.type && !file.type.startsWith("image/"))
    throw new Error(`이미지 파일이 아니에요 (${file.type || "형식 모름"})`);
  const raw = await readData(file);
  let img;
  try { img = await loadImg(raw); }
  catch (e) {
    if (/heic|heif/i.test(file.type || file.name || ""))
      throw new Error("아이폰 HEIC 형식은 브라우저가 못 읽어요. PNG나 JPG로 바꿔서 올려주세요.");
    if (raw.length > 4000000) throw new Error("그림을 줄이지 못했고 용량도 커요. 다른 파일로 올려주세요.");
    return raw;   // 못 줄여도 원본 그대로
  }
  const r = Math.min(1, max / Math.max(img.width, img.height));
  const w = Math.max(1, Math.round(img.width * r)), h = Math.max(1, Math.round(img.height * r));
  const cv = document.createElement("canvas"); cv.width = w; cv.height = h;
  cv.getContext("2d").drawImage(img, 0, 0, w, h);
  try {
    if (mode === "photo") return cv.toDataURL("image/jpeg", .62);
    const webp = cv.toDataURL("image/webp", .86);
    return webp.startsWith("data:image/webp") ? webp : cv.toDataURL("image/png");
  } catch (e) { return raw; }
}
const shrink = (file) => fitImage(file, 720, "photo");
const shrinkArt = (file, max = 640) => fitImage(file, max, "art");

const IMG_CACHE = new Map();

/* 직접 올린 그림 — 있으면 그림, 없으면 그려둔 도형을 쓴다 */
const ART = { idx: new Set(), img: new Map() };
const K_ARTIDX = "hanul:artIndex";
const K_CAT = "hanul:catalog";
const artKey = (kind, id) => `${kind}:${id}`;

function ImagePicker({ has, busy, onPicked, onClear }) {
  const ref = useRef(null);
  const [over, setOver] = useState(false);
  const [url, setUrl] = useState("");
  const [showUrl, setShowUrl] = useState(false);

  const take = (f) => { if (f) onPicked(f); };
  const fromUrl = async () => {
    const u = url.trim();
    if (!u) return;
    try {
      const r = await fetch(u, { mode: "cors" });
      const b = await r.blob();
      if (!b.type.startsWith("image/")) throw new Error("이미지가 아니에요");
      take(b);
    } catch (e) { onPicked(null, `링크에서 그림을 못 가져왔어요: ${e?.message || e}`); }
  };

  return (
    <div className={`hl-pick2 ${over ? "over" : ""}`}
      onDragOver={(e) => { e.preventDefault(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={(e) => {
        e.preventDefault(); setOver(false);
        const dt = e.dataTransfer;
        const f = dt?.files?.[0];
        if (f && f.size > 0) return take(f);
        const uri = dt?.getData("text/uri-list") || dt?.getData("text/plain") || "";
        if (/^https?:/.test(uri.trim())) { setUrl(uri.trim()); setShowUrl(true); onPicked(null, "링크를 넣었어요. '가져오기'를 눌러주세요."); return; }
        onPicked(null, "그림 파일을 찾지 못했어요. 파일로 저장한 뒤 올려주세요.");
      }}
      onPaste={(e) => {
        const it = [...(e.clipboardData?.items || [])].find((x) => x.type.startsWith("image/"));
        if (it) { e.preventDefault(); take(it.getAsFile()); }
      }}
      tabIndex={0}>
      <input ref={ref} type="file" accept="image/*" className="hl-hiddenFile"
        onChange={(e) => { const f = e.target.files?.[0]; e.target.value = ""; take(f); }} />
      <div className="hl-artBtns">
        <button className="hl-attBtn buy" disabled={busy} onClick={() => ref.current?.click()}>
          {busy ? "올리는 중…" : has ? "다른 그림" : "그림 고르기"}
        </button>
        <button className="hl-attBtn" onClick={() => setShowUrl(!showUrl)}>링크로</button>
        {has && <button className="hl-attBtn" onClick={onClear}>그림 빼기</button>}
      </div>
      {showUrl && (
        <div className="hl-addRow" style={{ marginTop: 7 }}>
          <input className="hl-in" placeholder="https://… .png" value={url} onChange={(e) => setUrl(e.target.value)} />
          <button className="hl-add" onClick={fromUrl}>가져오기</button>
        </div>
      )}
      <span className="hl-pickHint">이 상자를 한 번 누른 뒤 붙여넣기(Ctrl/⌘+V)하거나, 그림을 끌어다 놓아도 돼요</span>
    </div>
  );
}

function Art({ kind, id, className = "", children }) {
  const key = artKey(kind, id);
  const [src, setSrc] = useState(ART.img.get(key) || null);
  useEffect(() => {
    if (src || !ART.idx.has(key)) return;
    let live = true;
    (async () => {
      try {
        const r = await window.storage.get(`hanul:art:${key}`, true);
        ART.img.set(key, r.value);
        if (live) setSrc(r.value);
      } catch (e) {}
    })();
    return () => { live = false; };
  }, [key, src]);
  if (!ART.idx.has(key) || !src) return children;
  return <img src={src} alt="" className={`hl-artImg ${className}`} />;
}
const WEEK = ["일","월","화","수","목","금","토"];
const ymOf = (d) => d.slice(0, 7);
const dstr = (y, m, d) => `${y}-${String(m).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
const SEED = [{ id: "m_owner", name: "이다빈", gen: 45, part: "plan", role: "owner", pw: null }];

const today = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; };
const fmtDate = (s) => { const [y, m, d] = s.split("-"); const wd = ["SUN","MON","TUE","WED","THU","FRI","SAT"][new Date(s).getDay()]; return `${y}. ${m}. ${d}.  ${wd}`; };
const scn = (n) => `SCENE ${String(n).padStart(3, "0")}`;

/* 며칠 전인지 · 몇 시인지 — 오늘이면 상대시간, 아니면 날짜와 시각 */
const when = (ms) => {
  if (!ms) return "";
  const d = new Date(ms), now = new Date();
  const diff = Math.floor((now - d) / 1000);
  const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
  if (diff < 60) return "방금";
  if (diff < 3600) return `${Math.floor(diff / 60)}분 전`;
  const sameDay = d.toDateString() === now.toDateString();
  if (sameDay) return `오늘 ${hm}`;
  const y = new Date(now); y.setDate(y.getDate() - 1);
  if (d.toDateString() === y.toDateString()) return `어제 ${hm}`;
  const sameYear = d.getFullYear() === now.getFullYear();
  return sameYear
    ? `${d.getMonth() + 1}.${d.getDate()} ${hm}`
    : `${d.getFullYear()}.${d.getMonth() + 1}.${d.getDate()} ${hm}`;
};

function HanulApp() {
  const [ready, setReady] = useState(false);
  const [splash, setSplash] = useState(true);
  const [members, setMembers] = useState(SEED);
  const [logs, setLogs] = useState([]);
  const [rooms, setRooms] = useState({});
  const [events, setEvents] = useState([]);
  const [comments, setComments] = useState([]);
  const [attend, setAttend] = useState({});
  const [sugg, setSugg] = useState([]);
  const [letters, setLetters] = useState([]);
  const [codes, setCodes] = useState(DEFAULT_CODES);
  const [adjust, setAdjust] = useState([]);
  const [likes, setLikes] = useState({});
  const [guest, setGuest] = useState([]);
  const [mood, setMood] = useState({});
  const [notice, setNotice] = useState([]);
  const [quotes, setQuotes] = useState([]);
  const [manageLog, setManageLog] = useState(null);
  const [props, setProps] = useState([]);
  const [fort, setFort] = useState({});
  const [game, setGame] = useState({});
  const [gameOpen, setGameOpen] = useState(false);
  const [oeOpen, setOeOpen] = useState(false);
  const [mktOpen, setMktOpen] = useState(false);
  const [port, setPort] = useState({});
  const [mktAdj, setMktAdj] = useState({});
  const [tour, setTour] = useState(false);
  const [visit, setVisit] = useState(null);
  const [morePage, setMorePage] = useState(null);
  /* 로그인은 기기에 바로 저장한다. 서버를 안 거치므로 즉시 복원된다 */
  const SESSION_KEY = "hanul:session";
  const [session, setSession] = useState(() => {
    try { return localStorage.getItem(SESSION_KEY) || null; } catch (e) { return null; }
  });
  const keepSession = (id) => {
    try {
      if (id) localStorage.setItem(SESSION_KEY, id);
      else localStorage.removeItem(SESSION_KEY);
    } catch (e) {}
    setSession(id);
  };
  const [fab, setFab] = useState(null);
  const [recView, setRecView] = useState("cal");
  const [custom, setCustom] = useState({ url: "", name: "" });
  const [selDate, setSelDate] = useState(today());
  const [pref, setPref] = useState({ meId: "m_dabin", theme: "black", ambient: true, sfx: true, bgm: "off", vol: 50 });
  const [tab, setTab] = useState("home");
  const [switching, setSwitching] = useState(false);
  const [toast, setToast] = useState(null);

  const [syncing, setSyncing] = useState(false);
  const [syncAt, setSyncAt] = useState(null);

  const loadAll = async (first) => {
    if (busyRef.current > 0 && !first) return;
    if (!first) setSyncing(true);
    try {
      let memOk = false;
      let mem = SEED, lg = [], pf = { meId: null, theme: "black", ambient: true, sfx: true, bgm: "off", vol: 50 };
      try { mem = JSON.parse((await window.storage.get(K_MEMBERS, true)).value); memOk = true; } catch (e) {}
      try { lg = JSON.parse((await window.storage.get(K_LOGS, true)).value); } catch (e) {}
      let rm = {};
      try { rm = JSON.parse((await window.storage.get(K_ROOMS, true)).value); } catch (e) {}
      let ev = [], cm = [], at = {}, sg = [], lt = [], cd = DEFAULT_CODES;
      try { ev = JSON.parse((await window.storage.get(K_EVENTS, true)).value); } catch (e) {}
      try { cm = JSON.parse((await window.storage.get(K_COM, true)).value); } catch (e) {}
      try { at = JSON.parse((await window.storage.get(K_ATT, true)).value); } catch (e) {}
      try { sg = JSON.parse((await window.storage.get(K_SUGG, true)).value); } catch (e) {}
      try { lt = JSON.parse((await window.storage.get(K_LETTER, true)).value); } catch (e) {}
      try { cd = { ...cd, ...JSON.parse((await window.storage.get(K_CODES, true)).value) }; } catch (e) {}
      let aj = [], lk = {}, gb = [], md = {};
      try { aj = JSON.parse((await window.storage.get(K_ADJ, true)).value); } catch (e) {}
      try { lk = JSON.parse((await window.storage.get(K_LIKE, true)).value); } catch (e) {}
      try { gb = JSON.parse((await window.storage.get(K_GUEST, true)).value); } catch (e) {}
      try { md = JSON.parse((await window.storage.get(K_MOOD, true)).value); } catch (e) {}
      let nt = [], qt = [];
      try { nt = JSON.parse((await window.storage.get(K_NOTICE, true)).value); } catch (e) {}
      try { qt = JSON.parse((await window.storage.get(K_QUOTE, true)).value); } catch (e) {}
      let pr = [], ft = {}, gm = {};
      try { pr = JSON.parse((await window.storage.get(K_PROPS, true)).value); } catch (e) {}
      try { ft = JSON.parse((await window.storage.get(K_FORT, true)).value); } catch (e) {}
      try { gm = JSON.parse((await window.storage.get(K_GAME, true)).value); } catch (e) {}
      let pf2 = {};
      try { pf2 = JSON.parse((await window.storage.get(K_PORT, true)).value); } catch (e) {}
      let ad = {};
      try { ad = JSON.parse((await window.storage.get(K_ADJ2, true)).value); } catch (e) {}
      try {
        const ix = JSON.parse((await window.storage.get(K_ARTIDX, true)).value);
        ART.idx = new Set(ix || []);
      } catch (e) {}
      try {
        const ct = JSON.parse((await window.storage.get(K_CAT, true)).value);
        if (ct?.items?.length) ITEMS = ct.items;
        if (ct?.houses?.length) HOUSES = ct.houses;
        if (ct?.sets?.length) SETS = Object.fromEntries(ct.sets.map((x) => [x.id, x]));
      } catch (e) {}
      try { pf = { ...pf, ...JSON.parse((await window.storage.get(K_PREF, false)).value) }; } catch (e) {}
      const list = migrate(mem);
      if (memOk || first) setMembers(list);
      setLogs(lg || []);
      setRooms(rm || {});
      setEvents(ev || []);
      setComments(cm || []); setAttend(at || {}); setSugg(sg || []);
      setLetters(lt || []); setCodes(cd); setAdjust(aj || []);
      setLikes(lk || {}); setGuest(gb || []); setMood(md || {});
      setNotice(nt || []); setQuotes(qt || []); setProps(pr || []); setFort(ft || {}); setGame(gm || {}); setPort(pf2 || {}); setMktAdj(ad || {}); setAdj(ad || {});
      /* 로그인 복원 — 명단을 제대로 불러왔을 때만 판단한다.
         불러오기에 실패했는데 지워버리면 잠깐 끊겼다고 로그아웃되어 버린다 */
      let keep = null;
      try { keep = localStorage.getItem(SESSION_KEY); } catch (e) {}
      const want = keep || pf.meId;
      if (memOk && want) {
        if (list.some((m) => m.id === want)) {
          setSession(want);
          if (!keep) { try { localStorage.setItem(SESSION_KEY, want); } catch (e) {} }
        } else {
          try { localStorage.removeItem(SESSION_KEY); } catch (e) {}
          setSession(null);
        }
      } else if (want) {
        setSession(want);   /* 연결이 불안정해도 로그인은 유지한다 */
      }
      if (first) setPref(pf);
      setSyncAt(Date.now());
    } finally {
      setSyncing(false);
      setReady(true);
    }
  };

  useEffect(() => { loadAll(true); }, []);

  /* 화면으로 돌아오거나 일정 시간이 지나면 조용히 새로 불러온다 */
  useEffect(() => {
    if (!ready) return;
    /* 저장이 진행 중일 때 다시 불러오면 방금 한 일이 지워져 보인다 */
    const fresh = () => {
      if (document.visibilityState !== "visible") return;
      if (busyRef.current > 0) return;
      loadAll(false);
    };
    document.addEventListener("visibilitychange", fresh);
    window.addEventListener("focus", fresh);
    const t = setInterval(fresh, 45000);
    return () => {
      document.removeEventListener("visibilitychange", fresh);
      window.removeEventListener("focus", fresh);
      clearInterval(t);
    };
  }, [ready]);

  useEffect(() => { A.setSfx(pref.sfx !== false); A.setVol((pref.vol ?? 50) / 100); }, [pref.sfx, pref.vol]);
  useEffect(() => {
    if (!ready) return;
    A.onFail = () => flash("음원을 재생할 수 없어요. 링크가 mp3 파일을 직접 가리키는지 확인해 주세요.");
    A.setTrack(pref.bgm || "off", custom.url || pref.bgmUrl || "");
  }, [ready, pref.bgm, pref.bgmUrl, custom.url]);

  /* 첫 터치가 있어야 브라우저가 소리를 허용한다 */
  useEffect(() => {
    const wake = () => { A.start(); window.removeEventListener("pointerdown", wake); };
    window.addEventListener("pointerdown", wake);
    return () => window.removeEventListener("pointerdown", wake);
  }, []);

  const clickSound = (e) => {
    const b = e.target.closest("button");
    if (!b || b.disabled) return;
    const c = b.className || "";
    if (/hl-reject|hl-del|hl-removeBtn|hl-navBtn/.test(c)) A.sfx("back");
    else if (/hl-cta|hl-submit|hl-approve|hl-add\b/.test(c)) A.sfx("confirm");
    else A.sfx("tap");
  };

  const T = THEMES[pref.theme] || THEMES.black;
  const flash = (m) => { setToast(m); setTimeout(() => setToast(null), 2200); };
  const savePref = async (next) => {
    setPref(next);
    try { await window.storage.set(K_PREF, JSON.stringify(next), false); } catch (e) {}
  };
  /* 저장하기 직전에 서버의 최신본을 다시 읽어서 그 위에 내 변경만 얹는다.
     이렇게 하지 않으면 여러 사람이 동시에 쓸 때 서로의 기록을 덮어써 버린다. */
  const saveQ = useRef(Promise.resolve());
  const busyRef = useRef(0);
  const put = (k, v, setter, fallback) => {
    busyRef.current += 1;

    /* 1) 화면은 지금 바로 바꾼다 — 기다리지 않는다 */
    if (typeof v === "function") setter((prev) => v(prev));
    else setter(v);

    /* 2) 서버 저장은 뒤에서 차례대로 처리한다 */
    const run = async () => {
      try {
        let cur = null;
        try {
          cur = JSON.parse((await window.storage.get(k, true)).value);
        } catch (e) {
          const msg = String(e?.message || e);
          if (!/not found/i.test(msg)) {
            flash("연결이 불안정해 저장을 멈췄어요. 잠시 뒤 다시 시도해 주세요.");
            return null;          /* 남의 기록을 덮어쓰지 않는다 */
          }
          cur = fallback !== undefined ? fallback : (Array.isArray(v) ? [] : {});
        }
        if (cur === null || cur === undefined) cur = fallback !== undefined ? fallback : {};
        /* 서버 최신본 위에 내 변경을 다시 얹어 저장한다 */
        const next = typeof v === "function" ? v(cur) : v;
        setter(next);
        try { await window.storage.set(k, JSON.stringify(next), true); }
        catch (e) { flash("저장에 실패했어요. 다시 시도해 주세요."); }
        return next;
      } finally {
        busyRef.current = Math.max(0, busyRef.current - 1);
      }
    };
    saveQ.current = saveQ.current.then(run, run);
    return saveQ.current;
  };
  const saveMembers = (n) => put(K_MEMBERS, n, setMembers, SEED);
  const saveLogs = (n) => put(K_LOGS, n, setLogs, []);
  const saveMembers2 = (n) => put(K_MEMBERS, n, setMembers, SEED);
  const saveComments = (n) => put(K_COM, n, setComments, []);
  const saveAttend = (n) => put(K_ATT, n, setAttend, {});
  const saveSugg = (n) => put(K_SUGG, n, setSugg, []);
  const saveLetters = (n) => put(K_LETTER, n, setLetters, []);
  const saveCodes = (n) => put(K_CODES, n, setCodes, {});
  const saveAdjust = (n) => put(K_ADJ, n, setAdjust, []);
  const saveLikes = (n) => put(K_LIKE, n, setLikes, {});
  const saveGuest = (n) => put(K_GUEST, n, setGuest, []);
  const saveMood = (n) => put(K_MOOD, n, setMood, {});
  const saveNotice = (n) => put(K_NOTICE, n, setNotice, []);
  const saveQuotes = (n) => put(K_QUOTE, n, setQuotes, []);
  const saveProps = (n) => put(K_PROPS, n, setProps, []);
  const saveFort = (n) => put(K_FORT, n, setFort, {});
  const saveGame = (n) => put(K_GAME, n, setGame, {});
  const savePort = (n) => put(K_PORT, n, setPort, {});
  const saveMktAdj = (n) => put(K_ADJ2, n, (v) =>
    setMktAdj((prev) => { const nv = typeof v === "function" ? v(prev) : v; setAdj(nv); return nv; }), {});
  const saveEvents = (n) => put(K_EVENTS, n, setEvents, []);
  const saveRooms = (n) => put(K_ROOMS, n, setRooms, {});

  const me = members.find((m) => m.id === session) || null;
  /* 역할이 정해진 뒤 처음 들어오면 한 번만 안내를 띄운다 */
  useEffect(() => {
    if (!ready || !me || splash) return;
    const key = `hanul:tour:${me.id}:${me.role}`;
    if (localStorage.getItem(key)) return;
    const t = setTimeout(() => setTour(true), 600);
    return () => clearTimeout(t);
  }, [ready, me?.id, me?.role, splash]);
  const closeTour = () => {
    if (me) { try { localStorage.setItem(`hanul:tour:${me.id}:${me.role}`, "1"); } catch (e) {} }
    setTour(false);
  };
  const isOwner = me?.role === "owner";
  const isExec = isOwner || me?.role === "exec";
  const isLead = me?.role === "lead";
  const isAdmin = isExec;
  const attPt = (id) => Object.values(attend).reduce((s, d) => s + (ATT[d?.[id]]?.pt || 0), 0);
  const adjPt = (id) => adjust.filter((a) => a.memberId === id).reduce((s, a) => s + a.pt, 0);
  const pointsOf = (id) => Math.max(0,
    logs.filter((l) => l.memberId === id && l.status === "approved" && l.kind !== "solo").reduce((s, l) => s + l.pt, 0) + attPt(id) + adjPt(id));
  const myAtt = (d) => attend[d]?.[me?.id] || null;
  const myLogs = logs.filter((l) => l.memberId === me?.id).sort((a, b) => b.scene - a.scene);
  const myPoints = me ? pointsOf(me.id) : 0;
  const pending = logs.filter((l) => l.status === "pending" && l.kind !== "solo").sort((a, b) => a.scene - b.scene);
  const wroteToday = myLogs.some((l) => l.date === today());
  const nextScene = (logs.reduce((m, l) => Math.max(m, l.scene), 0) || 0) + 1;
  const rawRoom = rooms[me?.id] || { inv: {}, slots: {}, spent: 0 };
  const effCh = rawRoom.ch || (me?.role === "owner" ? OWNER_CH : DEFAULT_CH);
  const myRoom = { ...rawRoom, ch: effCh };
  const visitMember = members.find((m) => m.id === visit) || null;
  const visitRoomRaw = visit ? (rooms[visit] || { inv: {}, slots: {}, spent: 0 }) : null;
  const visitRoom = visitRoomRaw
    ? { ...visitRoomRaw, ch: visitRoomRaw.ch || (visitMember?.role === "owner" ? OWNER_CH : DEFAULT_CH) }
    : null;
  const available = Math.max(0, myPoints - (myRoom.spent || 0));

  const writeRoom = (patch) => saveRooms((cur) => ({ ...cur, [me.id]: { ...(cur[me.id] || {}), ...patch } }));
  const roll = async (n = 1) => {
    const cost = n === 10 ? GACHA_COST * 9 : GACHA_COST * n;
    if (available < cost) return { error: `포인트가 ${cost - available}P 모자라요.` };
    const rolls = [];
    let refund = 0;
    for (let i = 0; i < n; i++) {
      const r = Math.random() * 100;
      let tier = r < 60 ? "common" : r < 87 ? "rare" : r < 99 ? "epic" : "legend";
      /* 10연차에는 희귀 이상 하나가 반드시 나온다 */
      if (n === 10 && i === n - 1 && rolls.every((x) => x.item.tier === "common")) tier = "rare";
      const pool = ITEMS.filter((x) => x.tier === tier);
      rolls.push({ item: pool[Math.floor(Math.random() * pool.length)] });
    }
    /* 보관함과 쓴 포인트는 서버 최신본 위에 얹는다 */
    await saveRooms((cur) => {
      const mine = cur[me.id] || {};
      const inv = { ...(mine.inv || {}) };
      refund = 0;
      rolls.forEach((x) => {
        const dup = (inv[x.item.id] || 0) > 0;
        x.dup = dup;
        if (dup) refund += DUP_REFUND;
        inv[x.item.id] = (inv[x.item.id] || 0) + 1;
      });
      return { ...cur, [me.id]: { ...mine, inv, spent: (mine.spent || 0) + cost - refund } };
    });
    return { rolls, refund, cost };
  };
  const visibleEvents = events.filter((e) => e.scope === "public" || e.ownerId === me?.id);
  const addEvent = async (ev) => {
    const scope = isAdmin ? ev.scope : "private";
    await saveEvents((cur) => [...cur, { ...ev, scope, id: `e_${Date.now()}`, ownerId: me.id, ownerName: me.name }]);
    flash(scope === "public" ? "전체 일정으로 올렸어요." : "나만 보는 일정으로 저장했어요.");
  };
  const removeEvent = async (id) => saveEvents((cur) => cur.filter((e) => e.id !== id));

  const setChar = (patch) => writeRoom({ ch: { ...effCh, ...patch } });
  const setMemberChar = (mid, patch) => saveRooms((cur) => {
    const r = cur[mid] || { inv: {}, slots: {}, spent: 0 };
    const base = r.ch || (members.find((m) => m.id === mid)?.role === "owner" ? OWNER_CH : DEFAULT_CH);
    return { ...cur, [mid]: { ...r, ch: { ...base, ...patch } } };
  });
  const buySet = async (key) => {
    const S = SETS[key];
    if (!S || (myRoom.sets || []).includes(key)) return;
    if (available < S.cost) { flash(`포인트가 ${S.cost - available}P 모자라요.`); return; }
    await saveRooms((cur) => {
      const mine = cur[me.id] || {};
      if ((mine.sets || []).includes(key)) return cur;
      return { ...cur, [me.id]: { ...mine, sets: [...(mine.sets || []), key], set: key, spent: (mine.spent || 0) + S.cost } };
    });
    A.sfx("tier_legend");
    flash(`${S.n} 세트를 들였어요.`);
  };
  const wearSet = (key) => writeRoom({ set: key });
  const rollHouse = async (n = 1) => {
    const cost = HOUSE_COST * n;
    if (available < cost) return { error: `포인트가 ${cost - available}P 모자라요.` };
    const rolls = []; let refund = 0;
    for (let i = 0; i < n; i++) {
      const r = Math.random() * 100;
      const tier = r < 55 ? "common" : r < 85 ? "rare" : r < 99 ? "epic" : "legend";
      const pool = HOUSES.filter((h) => h.tier === tier);
      rolls.push({ item: pool[Math.floor(Math.random() * pool.length)], kind: "house" });
    }
    await saveRooms((cur) => {
      const mine = cur[me.id] || {};
      const have = [...(mine.houses || ["tent"])];
      refund = 0;
      rolls.forEach((x) => {
        const dup = have.includes(x.item.id);
        x.dup = dup;
        if (dup) refund += 40; else have.push(x.item.id);
      });
      return { ...cur, [me.id]: { ...mine, houses: have, spent: (mine.spent || 0) + cost - refund } };
    });
    return { rolls, refund, cost };
  };
  const wearHouse = (id) => writeRoom({ house: id });
  const wearSky = (id) => writeRoom({ sky: id });
  const wearWall = (id) => writeRoom({ wall: id });
  const place = (slotId, itemId) => saveRooms((cur) => {
    const mine = cur[me.id] || {};
    return { ...cur, [me.id]: { ...mine, slots: { ...(mine.slots || {}), [slotId]: itemId } } };
  });
  const clearSlot = (slotId) => saveRooms((cur) => {
    const mine = cur[me.id] || {};
    const sl = { ...(mine.slots || {}) };
    delete sl[slotId];
    return { ...cur, [me.id]: { ...mine, slots: sl } };
  });

  const submitLog = async (text, date, kind) => {
    const solo = kind === "solo";
    const auto = solo || isExec;
    const row = {
      id: `l_${Date.now()}`, at: Date.now(), memberId: me.id, date, typeId: me.part, kind: solo ? "solo" : "team",
      text: text.trim(), pt: solo ? 0 : LOG_PT, status: auto ? "approved" : "pending",
    };
    await saveLogs((cur) => [...cur, { ...row, scene: (cur.reduce((m, l) => Math.max(m, l.scene || 0), 0) || 0) + 1 }]);
    flash(solo ? "개인 기록을 올렸어요. 모두가 볼 수 있어요."
      : auto ? `${scn(nextScene)} 게시했어요. +${LOG_PT}P`
      : `${scn(nextScene)} 올렸어요. 승인되면 ${LOG_PT}P가 들어와요.`);
    setRecView(solo ? "mine" : "cal");
    if (!solo) setTab("home");
  };
  const editLog = (id, text, date) => saveLogs((cur) => cur.map((l) => (l.id === id ? { ...l, text: text.trim(), date, editedAt: Date.now() } : l)));
  const removeLog = (id) => saveLogs((cur) => cur.filter((l) => l.id !== id));

  const addComment = async (logId, text, imgId, parent) =>
    saveComments((cur) => [...cur, { id: `c_${Date.now()}`, logId, memberId: me.id, text, imgId: imgId || null, parent: parent || null, at: Date.now() }]);
  const dropComment = (id) => saveComments((cur) => cur.filter((c) => c.id !== id && c.parent !== id));
  const mark = (date, memberId, kind) => saveAttend((cur) => {
    const day = { ...(cur[date] || {}) };
    if (day[memberId] === kind) delete day[memberId]; else day[memberId] = kind;
    return { ...cur, [date]: day };
  });
  const setRole = (id, role) => saveMembers2((cur) => cur.map((m) => (m.id === id ? { ...m, role } : m)));
  const setPart = (id, part) => saveMembers2((cur) => cur.map((m) => (m.id === id ? { ...m, part } : m)));
  const dropMember = (id) => saveMembers2((cur) => cur.filter((m) => m.id !== id));
  const [artVer, setArtVer] = useState(0);
  const setList = () => Object.entries(SETS).map(([id, v]) => ({ ...v, id }));
  const [cat, setCat] = useState({ items: DEFAULT_ITEMS, houses: DEFAULT_HOUSES, sets: [] });
  useEffect(() => { if (ready) setCat({ items: ITEMS, houses: HOUSES, sets: setList() }); }, [ready]);
  const saveCatalog = async (next) => {
    ITEMS = next.items; HOUSES = next.houses;
    if (next.sets?.length) SETS = Object.fromEntries(next.sets.map((x) => [x.id, x]));
    setCat(next);
    try { await window.storage.set(K_CAT, JSON.stringify(next), true); }
    catch (e) { flash(`저장 실패: ${e?.message || e}`); }
  };
  const putArt = async (kind, id, dataUrl) => {
    const key = artKey(kind, id);
    if (!window.storage) { flash("이 화면에서는 저장 기능을 쓸 수 없어요."); return; }
    try {
      if (dataUrl) {
        if (dataUrl.length > 4000000) { flash("그림이 너무 커요. 더 작은 파일로 올려주세요."); return; }
        await window.storage.set(`hanul:art:${key}`, dataUrl, true);
        ART.idx.add(key); ART.img.set(key, dataUrl);
      } else {
        try { await window.storage.delete(`hanul:art:${key}`, true); } catch (e) {}
        ART.idx.delete(key); ART.img.delete(key);
      }
      await window.storage.set(K_ARTIDX, JSON.stringify([...ART.idx]), true);
      setArtVer((v) => v + 1);
      flash(dataUrl ? "그림을 바꿨어요." : "그린 도형으로 되돌렸어요.");
    } catch (e) { flash(`저장 실패: ${e?.message || e}`); }
  };
  const myBirth = me?.birth || null;
  const myFort = fort[me?.id];
  const myGame = game[me?.id] || {};
  const setBirth = (v) => saveMembers2((cur) => cur.map((m) => (m.id === me.id ? { ...m, birth: v } : m)));
  const payFortune = () => saveAdjust((cur) => [...cur, {
    id: `g_${Date.now()}`, memberId: me.id, pt: -FORTUNE_COST,
    reason: "오늘의 운세", at: Date.now(), by: me.name,
  }]);
  const keepFortune = (f) => saveFort((cur) => ({ ...cur, [me.id]: f }));
  /* 모든 판이 랭킹에 반영된다. 기록을 갱신했을 때만 시각을 새로 찍어
     같은 콤보라면 먼저 도달한 사람이 앞선다. */
  const gameReward = async (pt, combo) => {
    const wk = weekKey();
    const now = Date.now();
    if (pt > 0) {
      await saveAdjust((cur) => [...cur, {
        id: `g_${now}`, memberId: me.id, pt,
        reason: `조명 큐 ${combo}콤보`, at: now, by: me.name,
      }]);
    }
    await saveGame((cur) => {
      const mine = cur[me.id] || {};
      const sameWeek = mine.week === wk;
      const prev = sameWeek ? (mine.weekBest || 0) : 0;
      const better = combo > prev;
      return {
        ...cur,
        [me.id]: {
          ...mine,
          date: today(), week: wk,
          weekBest: better ? combo : prev,
          weekAt: better ? now : (sameWeek ? mine.weekAt : now),
          plays: sameWeek ? (mine.plays || 0) + 1 : 1,
          best: Math.max(mine.best || 0, combo),
          at: now,
        },
      };
    });
  };
  /* 주식 — 산 값과 수량을 기록하고 포인트를 옮긴다 */
  const myPort = port[me?.id] || { hold: {} };
  const adjustStock = async (code, pct) => {
    if (!isOwner) return;
    await saveMktAdj((cur) => {
      const next = { ...cur };
      if (!pct) delete next[code];
      else next[code] = { pct, tick: nowTick(), by: me.name, at: Date.now() };
      return next;
    });
    flash(pct ? `${stockOf(code).n} ${pct > 0 ? "+" : ""}${pct}% 반영했어요.` : "원래 흐름으로 되돌렸어요.");
  };
  /* 레버리지 — 증거금을 걸고 방향에 베팅한다 */
  const openPos = async (code, side, lev, margin, price) => {
    const st = stockOf(code);
    const now = Date.now();
    await savePort((cur) => {
      const mine = cur[me.id] || { hold: {} };
      const pos = { id: `p_${now}`, code, side, lev, margin, entry: price, at: now };
      return { ...cur, [me.id]: { ...mine, pos: { ...(mine.pos || {}), [code]: pos } } };
    });
    await saveAdjust((cur) => [...cur, {
      id: `g_${now}`, memberId: me.id, pt: -margin,
      reason: `${st.n} ${side === "short" ? "하락" : "상승"} ${lev}배 진입 @${price.toLocaleString()}P`,
      at: now, by: me.name,
    }]);
  };
  const closePos = async (code, price, auto) => {
    const mine = port[me?.id] || {};
    const pos = mine.pos?.[code];
    if (!pos) return;
    const st = stockOf(code);
    const pl = posPL(pos, price);
    const back = Math.max(0, pos.margin + pl);
    const now = Date.now();
    await savePort((cur) => {
      const m2 = cur[me.id] || { hold: {} };
      const next = { ...(m2.pos || {}) };
      delete next[code];
      return { ...cur, [me.id]: { ...m2, pos: next } };
    });
    if (back > 0) {
      await saveAdjust((cur) => [...cur, {
        id: `g_${now}`, memberId: me.id, pt: back,
        reason: `${st.n} ${pos.side === "short" ? "하락" : "상승"} ${pos.lev}배 ${auto ? "청산" : "종료"} (${pl >= 0 ? "+" : ""}${pl.toLocaleString()}P)`,
        at: now, by: me.name,
      }]);
    }
    return { pl, back, auto };
  };

  /* 청산선에 닿으면 자동으로 정리한다 */
  useEffect(() => {
    if (!ready || !me) return;
    const check = () => {
      const mine = port[me.id];
      if (!mine?.pos) return;
      Object.values(mine.pos).forEach((pos) => {
        const st = stockOf(pos.code);
        if (!st) return;
        const price = priceNow(st);
        if (posPL(pos, price) <= -pos.margin * LIQ_AT) {
          closePos(pos.code, price, true);
          flash(`${st.n} 포지션이 청산됐어요. 증거금 ${pos.margin.toLocaleString()}P를 잃었습니다.`);
        }
      });
    };
    check();
    const t = setInterval(check, 30000);
    return () => clearInterval(t);
  }, [ready, me?.id, port]);

  const trade = async (kind, code, qty, price, settled) => {
    const st = stockOf(code);
    const amount = settled ?? price * qty;
    const now = Date.now();
    await savePort((cur) => {
      const mine = cur[me.id] || { hold: {} };
      const h = mine.hold?.[code] || { qty: 0, avg: 0 };
      let next;
      if (kind === "buy") {
        const q = h.qty + qty;
        next = { qty: q, avg: (h.qty * h.avg + price * qty) / q };
      } else {
        const q = Math.max(0, h.qty - qty);
        next = q === 0 ? { qty: 0, avg: 0 } : { qty: q, avg: h.avg };
      }
      return { ...cur, [me.id]: { ...mine, hold: { ...(mine.hold || {}), [code]: next } } };
    });
    await saveAdjust((cur) => [...cur, {
      id: `g_${now}`, memberId: me.id, pt: kind === "buy" ? -amount : amount,
      reason: `${st.n} ${qty}주 ${kind === "buy" ? "매수" : "매도"} @${price.toLocaleString()}P`,
      at: now, by: me.name,
    }]);
  };

  /* 홀짝 — 결과만큼 포인트를 더하거나 빼고, 하루 판수를 센다 */
  const oePlays = (myGame.oeDate === today() ? (myGame.oeCount || 0) : 0);
  const placeBet = async (delta, bet, mode, roll, win, stage) => {
    const now = Date.now();
    const why = stage === "start" ? `홀짝 · ${mode.n} ${bet}P 베팅`
      : stage === "cash" ? `홀짝 · ${mode.n} 정산 ${bet}P`
      : `홀짝 · ${mode.n} 실패 (${roll === 0 ? "꽝" : roll})`;
    if (delta !== 0) {
      await saveAdjust((cur) => [...cur, {
        id: `g_${now}`, memberId: me.id, pt: delta, reason: why, at: now, by: me.name,
      }]);
    }
    await saveGame((cur) => {
      const mine = cur[me.id] || {};
      const same = mine.oeDate === today();
      return { ...cur, [me.id]: { ...mine, oeDate: today(), oeCount: same ? (mine.oeCount || 0) + 1 : 1 } };
    });
  };

  /* 지난주 1등에게 상금을 준다 — 관리자가 앱을 열 때 한 번 처리 */
  const settleWeek = async () => {
    if (!isOwner) return;
    const wk = weekKey();
    const paidKey = `paid:${wk}`;
    if (game[paidKey]) return;
    const prev = new Date(wk); prev.setDate(prev.getDate() - 7);
    const pk = weekKey(prev);
    /* 콤보가 높은 순, 같으면 먼저 달성한 사람이 앞선다 */
    const board = Object.entries(game)
      .filter(([k, v]) => k.startsWith("m_") && v?.week === pk && (v.weekBest || 0) > 0)
      .sort((a, b) => (b[1].weekBest || 0) - (a[1].weekBest || 0)
        || (a[1].weekAt || 0) - (b[1].weekAt || 0));
    if (!board.length) { await saveGame((cur) => ({ ...cur, [paidKey]: { none: true } })); return; }

    /* 1위가 기준 콤보를 못 넘기면 그 주는 상금 없음 */
    if ((board[0][1].weekBest || 0) < WEEK_MIN) {
      await saveGame((cur) => ({ ...cur, [paidKey]: { short: board[0][1].weekBest } }));
      flash(`지난주는 1위가 ${WEEK_MIN}콤보에 못 미쳐 상금이 나가지 않았어요.`);
      return;
    }

    const stamp = Date.now();
    const pays = board
      .map(([id, rec], i) => ({ id, rank: i, pt: weekPrize(i), combo: rec.weekBest }))
      .filter((x) => x.pt > 0);
    await saveAdjust((cur) => [...cur, ...pays.map((x, i) => ({
      id: `g_${stamp}_${i}`, memberId: x.id, pt: x.pt,
      reason: `조명 큐 주간 ${x.rank + 1}위 (${x.combo}콤보)`, at: stamp, by: "시스템",
    }))]);
    const top = members.find((m) => m.id === board[0][0]);
    await saveGame((cur) => ({ ...cur, [paidKey]: {
      winner: board[0][0], name: top?.name, combo: board[0][1].weekBest, paid: pays.length,
    } }));
    flash(`지난주 조명 큐 정산 완료 — 1위 ${top?.name}님 외 ${Math.max(0, pays.length - 1)}명에게 지급했어요.`);
  };
  useEffect(() => { if (ready && me && isOwner) settleWeek(); }, [ready, me?.id]);
  const grant = (memberId, pt, reason) => saveAdjust((cur) => [...cur, { id: `g_${Date.now()}`, memberId, pt, reason, at: Date.now(), by: me.name }]);
  const toggleLike = (logId) => {
    const now = likes[logId] || [];
    if (!now.includes(me.id)) A.sfx("tap");
    return saveLikes((cur) => {
      const arr = cur[logId] || [];
      return { ...cur, [logId]: arr.includes(me.id) ? arr.filter((x) => x !== me.id) : [...arr, me.id] };
    });
  };
  const addGuest = (roomId, text) =>
    saveGuest((cur) => [...cur, { id: `v_${Date.now()}`, roomId, memberId: me.id, text, at: Date.now() }]);
  const dropGuest = (id) => saveGuest((cur) => cur.filter((g) => g.id !== id));
  const addProp = async (v, dataUrl) => {
    const id = `p_${Date.now()}`;
    if (dataUrl) {
      try { await window.storage.set(`hanul:art:prop:${id}`, dataUrl, true); PROP_IMG.set(id, dataUrl); } catch (e) {}
    }
    await saveProps((cur) => [...cur, { ...v, id, hasImg: !!dataUrl, at: Date.now(), by: me.name, byId: me.id }]);
    /* 등록해 준 사람에게 포인트를 준다 */
    await saveAdjust((cur) => [...cur, {
      id: `g_${Date.now()}`, memberId: me.id, pt: PROP_PT,
      reason: `무대 물품 등록 · ${v.n}`, at: Date.now(), by: me.name,
    }]);
    return true;
  };
  const editProp = (id, patch) => saveProps((cur) => cur.map((x) => (x.id === id ? { ...x, ...patch } : x)));
  const dropProp = async (id) => {
    try { await window.storage.delete(`hanul:art:prop:${id}`, true); } catch (e) {}
    PROP_IMG.delete(id);
    return saveProps((cur) => cur.filter((x) => x.id !== id));
  };
  const addNotice = (v) => saveNotice((cur) => [...cur, { ...v, id: `n_${Date.now()}`, at: Date.now(), by: me.name }]);
  const dropNotice = (id) => saveNotice((cur) => cur.filter((x) => x.id !== id));
  const addQuote = (text) => saveQuotes((cur) => [...cur, { id: `q_${Date.now()}`, text, by: me.name }]);
  const dropQuote = (id) => saveQuotes((cur) => cur.filter((x) => x.id !== id));
  const liveNotice = notice
    .filter((n) => !n.due || n.due >= today())
    .sort((a, b) => (a.kind === b.kind ? b.at - a.at : a.kind === "homework" ? -1 : 1));
  const [qSeed] = useState(() => Math.floor(Math.random() * 100000));
  const todayQuote = quotes.length ? quotes[qSeed % quotes.length]?.text : null;
  const pickMood = (id) => saveMood((cur) => ({ ...cur, [today()]: { ...(cur[today()] || {}), [me.id]: id } }));
  const sendSugg = (text) => saveSugg((cur) => [...cur, { id: `s_${Date.now()}`, text, at: Date.now() }]);
  const sendLetter = (toId, text) => saveLetters((cur) => [...cur, { id: `t_${Date.now()}`, toId, text, at: Date.now() }]);
  const myLetters = letters.filter((l) => l.toId === me?.id).sort((a, b) => b.at - a.at);
  const decide = async (id, status) => {
    const l = logs.find((x) => x.id === id);
    const author = members.find((m) => m.id === l?.memberId);
    const spread = status === "approved" && !l?.distributed && author?.role === "lead";
    await saveLogs((cur) => cur.map((x) => (x.id === id ? { ...x, status, distributed: x.distributed || spread } : x)));
    if (spread) {
      const team = members.filter((m) => m.part === l.typeId && m.id !== l.memberId);
      if (team.length) {
        const now = Date.now();
        await saveAdjust((cur) => [...cur, ...team.map((m, i) => ({
          id: `g_${now}_${i}`, memberId: m.id, pt: LOG_PT,
          reason: `${partOf(l.typeId).n}팀 활동 · ${author.name} 팀장 일지`,
          at: now, by: me.name,
        }))]);
        flash(`${partOf(l.typeId).n}팀 ${team.length}명에게 ${LOG_PT}P씩 지급했어요.`);
      } else {
        flash("같은 파트의 다른 단원이 없어요.");
      }
    }
  };

  const vars = {
    "--bg": T.bg, "--surface": T.surface, "--edge": T.edge, "--accent": T.accent,
    "--on": T.on, "--text": T.text, "--muted": T.muted,
    "--script": SCRIPT, "--scriptInk": SCRIPT_INK,
    "--ambBlend": T.dark ? "screen" : "multiply",
    "--ambOpacity": T.dark ? ".16" : ".26",
  };

  return (
    <div className={`hl-app ${T.rainbow ? "rainbow" : ""}`} style={vars} onClickCapture={clickSound}>
      <style>{CSS}</style>
      {pref.ambient && <Ambient />}
      {splash && <Splash onDone={() => setSplash(false)} />}

      {!ready ? (
        <div className="hl-load">무대 준비 중…</div>
      ) : !me ? (
        <Gate members={members} codes={codes} onSave={saveMembers2}
          onLogin={(id) => { keepSession(id); savePref({ ...pref, meId: id }); }} flash={flash} />
      ) : (
        <>
          <header className="hl-top">
            <HeaderClock />
            <div className="hl-topL">
              <div className="hl-eyebrow">한울 연극극회</div>
              <div className="hl-nameRow2">
                <span className="hl-wordmark">활동일지</span>
                <span className="hl-ptPill" title="보유 포인트">
                  <span className="hl-coin" />
                  <span className="hl-mono">{available.toLocaleString()}P</span>
                </span>
              </div>
            </div>
            <div className="hl-topR">
              <button className={`hl-syncBtn ${syncing ? "on" : ""}`} onClick={() => loadAll(false)}
                aria-label="새로 불러오기" title="새로 불러오기">
                <svg viewBox="0 0 24 24" className="hl-syncIcon">
                  <path d="M20 12a8 8 0 1 1-2.34-5.66" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" />
                  <path d="M20 3v5h-5" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </button>
              <button className="hl-who" onClick={() => setSwitching(true)}>
                <span className="hl-whoName">{me?.name}</span>
                <span className="hl-whoSub">{me?.gen}기 · {ROLES[me?.role] || "부원"}</span>
              </button>
            </div>
            {todayQuote && (
              <div className="hl-ticker" aria-label="오늘의 한마디">
                <span className="hl-tickerIn">{todayQuote}</span>
              </div>
            )}
          </header>

          <main className="hl-main" key={tab}>
            {tab === "home" && (
              <Home points={available} total={myPoints} logs={myLogs} events={visibleEvents}
                feed={logs} members={members} comments={comments} me={me} rooms={rooms}
                likes={likes} onLike={toggleLike} mood={mood[today()] || {}} onMood={pickMood} onDropComment={dropComment}
                notice={liveNotice} quote={todayQuote} onDropNotice={dropNotice} canManage={isExec} onManageLog={setManageLog}
                birth={myBirth} onSetBirth={setBirth} onPayFortune={payFortune}
                fortune={myFort} onKeepFortune={keepFortune}
                gameBest={myGame.week === weekKey() ? (myGame.weekBest || 0) : 0} gamePlayed={myGame.date === today()} onOpenGame={() => setGameOpen(true)}
                oePlays={oePlays} onOpenOE={() => setOeOpen(true)}
                stockValue={
                  Object.entries(myPort.hold || {}).reduce((n, [c, h]) => {
                    const st = stockOf(c); return st ? n + (h.qty || 0) * priceNow(st) : n;
                  }, 0)
                  + Object.values(myPort.pos || {}).reduce((n, p) => {
                    const st = stockOf(p.code); return st ? n + p.margin + posPL(p, priceNow(st)) : n;
                  }, 0)
                }
                onOpenMarket={() => setMktOpen(true)}
                onComment={addComment} wroteToday={wroteToday}
                onWrite={() => { setRecView("write"); setTab("write"); }} />
            )}
            {tab === "write" && (
              <Record view={recView} setView={setRecView} events={visibleEvents} logs={myLogs} me={me} isAdmin={isAdmin}
                selDate={selDate} setSelDate={setSelDate} onAdd={addEvent} onRemove={removeEvent}
                nextScene={nextScene} onSubmit={submitLog} attend={attend} auto={isExec} canTeam={isLead || isExec}
                onEditLog={editLog} onRemoveLog={removeLog}
                mood={mood} members={members} rooms={rooms} />
            )}
            {tab === "room" && (visit ? (
              <MyRoom room={visitRoom} visiting={visitMember} charId={visit} name={visitMember?.name}
                guest={guest.filter((g) => g.roomId === visit)} members={members} meId={me.id}
                onGuest={(t) => addGuest(visit, t)} onDropGuest={dropGuest} onExit={() => setVisit(null)} />
            ) : (
              <MyRoom room={myRoom} available={available} onRoll={roll} onPlace={place} onClear={clearSlot} onChar={setChar}
                onBuySet={buySet} onWearSet={wearSet} onRollHouse={rollHouse} onWearHouse={wearHouse} onWearSky={wearSky} onWearWall={wearWall}
                guest={guest.filter((g) => g.roomId === me.id)} members={members} meId={me.id}
                onGuest={(t) => addGuest(me.id, t)} onDropGuest={dropGuest}
                name={me?.name} charId={me?.id} />
            ))}
            {tab === "roster" && <Roster members={members} logs={logs} pointsOf={pointsOf} meId={me?.id} rooms={rooms}
              syncing={syncing} syncAt={syncAt} onSync={() => loadAll(false)}
              onVisit={(id) => { setVisit(id === me.id ? null : id); setTab("room"); }} />}
            {tab === "more" && (morePage ? (
              <>
                <button className="hl-back" onClick={() => setMorePage(null)}>‹ 더보기</button>
                <div className="hl-pageTitle">{(MORE_LABEL[morePage] || "")}</div>
                {morePage === "letters" && <Inbox letters={myLetters} />}
                {morePage === "approve" && <Approve pending={pending} members={members} onDecide={decide} />}
                {morePage === "sugg" && <SuggBox sugg={sugg} />}
                {morePage === "attend" && <Attendance members={members} attend={attend} me={me} isExec={isExec} onMark={mark} />}
                {morePage === "notice" && <NoticeAdmin notice={notice} quotes={quotes} onAdd={addNotice} onDrop={dropNotice}
                  onAddQuote={addQuote} onDropQuote={dropQuote} flash={flash} />}
                {morePage === "points" && <PointGrant members={members} adjust={adjust} onGrant={grant} pointsOf={pointsOf} flash={flash} />}
                {morePage === "roles" && <RoleBoard members={members} onRole={setRole} onDrop={dropMember} onPart={setPart} />}
                {morePage === "codes" && <CodeBox codes={codes} onCodes={saveCodes} flash={flash} />}
                {morePage === "art" && <ArtBoard cat={cat} onSaveCatalog={saveCatalog} onPut={putArt} flash={flash} />}
                {morePage === "chars" && <CharBoard members={members} rooms={rooms} onSetChar={setMemberChar} onPut={putArt} flash={flash} />}
                {morePage === "link" && <LinkStatus />}
                {morePage === "props" && <PropList props={props} canEdit={isExec} meId={me.id} onAdd={addProp} onEdit={editProp} onDrop={dropProp} flash={flash} />}
                {morePage === "me" && <MyInfo me={me} flash={flash}
                  onUpdateMe={(patch) => saveMembers2((cur) => cur.map((x) => (x.id === me.id ? { ...x, ...patch } : x)))} />}
                {morePage === "look" && <LookSet pref={pref} savePref={savePref} />}
                {morePage === "sound" && <SoundSet pref={pref} savePref={savePref} custom={custom} setCustom={setCustom} flash={flash} />}
              </>
            ) : (
              <MoreMenu
                items={[
                  { id: "letters", n: "받은 편지", d: "익명으로 온 편지", badge: myLetters.length },
                  ...(isExec ? [
                    { id: "approve", n: "일지 승인", d: "부원이 올린 기록 확인", badge: pending.length },
                    { id: "sugg", n: "익명 건의함", d: "이름 없이 들어온 의견", badge: sugg.length },
                    { id: "notice", n: "공지 · 숙제", d: "홈 화면 맨 위에 띄우기" },
                    { id: "attend", n: "출석 체크", d: "날짜별로 단원 출석 찍기" },
                  ] : []),
                  ...(isOwner ? [
                    { id: "gap1", gap: true },
                    { id: "points", n: "포인트 지급", d: "직접 주거나 회수하기" },
                    { id: "roles", n: "단원 · 역할", d: "권한 올리고 내리기" },
                    { id: "codes", n: "권한 코드", d: "집행부 · 팀장 가입 코드" },
                    { id: "art", n: "그림 · 이름 관리", d: "집과 소품을 올리고 이름 바꾸기" },
                    { id: "chars", n: "단원 캐릭터", d: "모습을 바꾸거나 그림 올리기" },
                  ] : []),
                  { id: "props", n: "무대 물품", d: `등록하면 ${PROP_PT}P · 소품과 의상 목록` },
                  { id: "gap2", gap: true },
                  ...(isOwner ? [{ id: "link", n: "연결 상태", d: "모두와 데이터가 이어져 있는지" }] : []),
                  { id: "me", n: "내 정보", d: "기수와 파트" },
                  { id: "look", n: "화면", d: "테마 색과 배경" },
                  { id: "sound", n: "소리", d: "효과음과 배경음" },
                  { id: "tour", n: "사용법 다시 보기", d: "내 역할에 맞춘 안내" },
                  { id: "opening", n: "오프닝 다시 보기", d: "3.5초" },
                  { id: "gap3", gap: true },
                  { id: "logout", n: "로그아웃", d: "" },
                ]}
                onPick={(id) => {
                  if (id === "tour") return setTour(true);
                  if (id === "opening") return setSplash(true);
                  if (id === "logout") { keepSession(null); savePref({ ...pref, meId: null }); return; }
                  setMorePage(id);
                }} />
            ))}
          </main>

          <nav className="hl-tabs">
            <Tab id="home" cur={tab} set={setTab} label="오늘" />
            <Tab id="write" cur={tab} set={setTab} label="기록" />
            <button className={`hl-homeBtn ${tab === "room" ? "on" : ""}`}
              onClick={() => { setVisit(null); setTab("room"); }} aria-label="마이룸">
              <svg viewBox="0 0 40 40" className="hl-homeIcon">
                <path d="M20 7 L34 19 L30.5 19 L30.5 32 L23.5 32 L23.5 24 L16.5 24 L16.5 32 L9.5 32 L9.5 19 L6 19 Z" fill="currentColor" />
                <rect x="25" y="10" width="3.5" height="6" rx="1.2" fill="currentColor" />
              </svg>
            </button>
            <Tab id="roster" cur={tab} set={setTab} label="단원" />
            <Tab id="more" cur={tab} set={() => { setMorePage(null); setTab("more"); }} label="더보기" badge={isExec ? pending.length + sugg.length : myLetters.length} />
          </nav>
        </>
      )}

      {switching && (
        <div className="hl-sheetWrap" onClick={() => setSwitching(false)}>
          <div className="hl-sheet" onClick={(e) => e.stopPropagation()}>
            <div className="hl-sheetTitle">{me?.name} · {me?.gen}기</div>
            <p className="hl-sheetNote">{ROLES[me?.role]} · {partOf(me?.part).n}</p>
            <button className="hl-cta done hl-block" onClick={() => { setSwitching(false); keepSession(null); savePref({ ...pref, meId: null }); }}>
              로그아웃
            </button>
          </div>
        </div>
      )}

      {me && (
        <>
          <button className="hl-fab" onClick={() => setFab(fab ? null : "menu")} aria-label="익명으로 보내기">
            {fab ? <span className="hl-fabX">×</span> : (
              <svg viewBox="0 0 40 32" className="hl-fabIcon">
                <rect x="2.5" y="4" width="35" height="24" rx="6" fill="currentColor" />
                <path d="M5 9 L20 20 L35 9" fill="none" stroke="var(--bg)" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
                <path d="M20 15.5 c-1.6-2.6 -5.4-1.6 -5.4 1.4 0 2.4 3.2 4.4 5.4 6 2.2-1.6 5.4-3.6 5.4-6 0-3 -3.8-4 -5.4-1.4 Z"
                  fill="#F2708C" stroke="var(--bg)" strokeWidth="1.2" />
              </svg>
            )}
          </button>
          {fab === "menu" && (
            <div className="hl-fabMenu">
              <button onClick={() => setFab("sugg")}>익명 건의함</button>
              <button onClick={() => setFab("letter")}>익명 편지</button>
            </div>
          )}
          {fab === "sugg" && <AnonBox title="익명 건의함"
            desc="집행부만 읽어요. 누가 보냈는지는 저장되지 않아요."
            onClose={() => setFab(null)}
            onSend={(t) => { sendSugg(t); setFab(null); flash("건의를 보냈어요."); }} />}
          {fab === "letter" && <AnonBox title="익명 편지" toList={members.filter((m) => m.id !== me.id)}
            desc="받는 사람만 읽어요. 보낸 사람은 남지 않아요."
            onClose={() => setFab(null)}
            onSend={(t, to) => { sendLetter(to, t); setFab(null); flash("편지를 보냈어요."); }} />}
        </>
      )}

      {manageLog && (
        <ManageLog log={manageLog} author={members.find((m) => m.id === manageLog.memberId)}
          onSave={(t, d) => { editLog(manageLog.id, t, d); setManageLog(null); flash("일지를 고쳤어요."); }}
          onDelete={() => { removeLog(manageLog.id); setManageLog(null); flash("일지를 지웠어요."); }}
          onClose={() => setManageLog(null)} />
      )}
      {gameOpen && me && (
        <CueGame onClose={() => setGameOpen(false)} onReward={gameReward} playedToday={myGame.date === today()}
          board={Object.entries(game)
            .filter(([k, v]) => k.startsWith("m_") && v?.week === weekKey() && (v.weekBest || 0) > 0)
            .map(([k, v]) => ({ id: k, combo: v.weekBest, at: v.weekAt || 0, plays: v.plays || 0,
              name: members.find((m) => m.id === k)?.name || "?" }))
            .sort((a, b) => b.combo - a.combo || a.at - b.at)}
          meId={me.id} myWeek={myGame.week === weekKey() ? (myGame.weekBest || 0) : 0} />
      )}
      {oeOpen && me && (
        <OddEven onClose={() => setOeOpen(false)} available={available} onBet={placeBet} />
      )}
      {mktOpen && me && (
        <Market onClose={() => setMktOpen(false)} available={available} port={myPort} onTrade={trade} flash={flash}
          isOwner={isOwner} adj={mktAdj} onAdjust={adjustStock} onOpenPos={openPos} onClosePos={closePos} />
      )}
      {tour && me && <Tour role={me.role} name={me.name} onClose={closeTour} />}
      {toast && <div className="hl-toast">{toast}</div>}
    </div>
  );
}

/* ── 배경: 오프닝에서 모였던 색이 흐리게 남는다 ── */
function Ambient() {
  return (
    <div className="hl-amb" aria-hidden="true">
      {PARTS.map((p, i) => (
        <span key={p.n} className="hl-ambBlob" style={{ "--c": p.c, "--i": i }} />
      ))}
    </div>
  );
}

/* ── 오프닝 ── */
function Splash({ onDone }) {
  const [go, setGo] = useState(false);
  useEffect(() => {
    const a = setTimeout(() => setGo(true), 20);
    const c = setTimeout(() => A.sfx("bell"), 2280);
    const b = setTimeout(onDone, 4400);
    return () => { clearTimeout(a); clearTimeout(b); clearTimeout(c); };
  }, [onDone]);
  return (
    <div className={`hl-splash ${go ? "play" : ""}`} onClick={onDone}>
      {PARTS.map((p) => <span key={p.n} className="hl-blob" style={{ "--c": p.c, "--sx": p.sx, "--sy": p.sy }} />)}
      <span className="hl-ring a" /><span className="hl-ring b" /><span className="hl-flood" />
      <span className="hl-mark" style={{ WebkitMaskImage: `url(${MARK})`, maskImage: `url(${MARK})` }} />
      <div className="hl-sig">
        <div className="hl-sigKr">하나의 울림</div>
        <div className="hl-mono hl-sigEn">SINCE 1981 · KONKUK GLOCAL</div>
      </div>
    </div>
  );
}

function Gate({ members, codes, onSave, onLogin, flash }) {
  const owner = members.find((m) => m.role === "owner");
  const needSetup = owner && !owner.pw;
  const [mode, setMode] = useState(needSetup ? "setup" : "login");
  const [f, setF] = useState({ name: "", gen: "", pw: "", pw2: "", part: "plan", code: "" });
  const [err, setErr] = useState("");
  const set = (k) => (e) => setF({ ...f, [k]: e.target.value });

  const setup = async () => {
    if (f.pw.length < 4) return setErr("비밀번호는 4자 이상으로 정해주세요.");
    if (f.pw !== f.pw2) return setErr("비밀번호가 서로 달라요.");
    const pw = await hashPw(owner.id, f.pw);
    await onSave(members.map((m) => (m.id === owner.id ? { ...m, pw } : m)));
    onLogin(owner.id);
  };
  const login = async () => {
    const m = members.find((x) => x.name === f.name.trim() && String(x.gen) === String(f.gen).trim());
    if (!m) return setErr("이름과 기수가 맞는 단원이 없어요.");
    if (!m.pw) return setErr("아직 비밀번호가 설정되지 않은 계정이에요.");
    if ((await hashPw(m.id, f.pw)) !== m.pw) return setErr("비밀번호가 맞지 않아요.");
    onLogin(m.id);
  };
  const join = async () => {
    const name = f.name.trim(), gen = String(f.gen).trim();
    if (!name || !gen) return setErr("이름과 기수를 모두 적어주세요.");
    if (members.some((x) => x.name === name && String(x.gen) === gen)) return setErr("이미 같은 이름과 기수가 있어요.");
    if (f.pw.length < 4) return setErr("비밀번호는 4자 이상으로 정해주세요.");
    if (f.pw !== f.pw2) return setErr("비밀번호가 서로 달라요.");
    const c = f.code.trim();
    const role = c === codes.exec ? "exec" : c === codes.lead ? "lead" : "member";
    if (c && role === "member") return setErr("권한 코드가 맞지 않아요. 비워두면 부원으로 가입돼요.");
    const id = `m_${Date.now()}`;
    await onSave([...members, { id, name, gen: Number(gen), part: f.part, role, pw: await hashPw(id, f.pw) }]);
    flash(`${ROLES[role]}으로 가입했어요.`);
    onLogin(id);
  };

  return (
    <div className="hl-gate">
      <div className="hl-gateMark">
        <span className="hl-gateLogo" style={{ WebkitMaskImage: `url(${MARK})`, maskImage: `url(${MARK})` }} />
        <span className="hl-mono hl-gateSince">SINCE 1981 · KONKUK GLOCAL</span>
      </div>
      <div className="hl-gateHead">
        <div className="hl-eyebrow">한울 연극극회</div>
        <div className="hl-gateTitle">{mode === "setup" ? "관리자 계정 설정" : mode === "join" ? "가입하기" : "활동일지"}</div>
        <p className="hl-gateSub">
          {mode === "setup" ? `${owner.name} 계정의 비밀번호를 먼저 정해주세요.`
            : mode === "join" ? "이름과 기수는 나중에 바꿀 수 없어요."
            : "이름 · 기수 · 비밀번호로 들어와요."}
        </p>
      </div>

      {mode !== "setup" && (
        <>
          <input className="hl-in hl-block" placeholder="이름" value={f.name} onChange={set("name")} />
          <input className="hl-in hl-block hl-mono" placeholder="기수 (숫자만)" inputMode="numeric" value={f.gen} onChange={set("gen")} />
        </>
      )}
      <input className="hl-in hl-block" type="password" placeholder="비밀번호" value={f.pw} onChange={set("pw")} />
      {mode !== "login" && <input className="hl-in hl-block" type="password" placeholder="비밀번호 확인" value={f.pw2} onChange={set("pw2")} />}

      {mode === "join" && (
        <>
          <div className="hl-scopeLabel">내 파트</div>
          <div className="hl-chips">
            {PARTS.map((p) => (
              <button key={p.id} className={`hl-chip2 ${f.part === p.id ? "on" : ""}`} onClick={() => setF({ ...f, part: p.id })}>{p.n}</button>
            ))}
          </div>
          <div className="hl-scopeLabel">권한 코드</div>
          <div className="hl-roleHint">
            {[["01", "부원", "코드 없이 그냥 비워두세요"],
              ["02", "팀장", "팀장 코드를 받아 넣으세요"],
              ["03", "집행부", "집행부 코드를 받아 넣으세요"]].map(([n, t, d]) => (
              <div key={n} className="hl-roleLine">
                <span className="hl-mono hl-roleNo">{n}</span>
                <span className="hl-roleT">{t}</span>
                <span className="hl-roleD">{d}</span>
              </div>
            ))}
          </div>
          <input className="hl-in hl-block hl-mono" placeholder="코드가 있다면 여기에" value={f.code} onChange={set("code")} />
        </>
      )}

      {err && <p className="hl-err hl-onDark">{err}</p>}
      <button className="hl-cta hl-block" onClick={mode === "setup" ? setup : mode === "join" ? join : login}>
        {mode === "setup" ? "비밀번호 정하기" : mode === "join" ? "가입하고 시작하기" : "로그인"}
      </button>
      {mode !== "setup" && (
        <button className="hl-linkText" onClick={() => { setErr(""); setMode(mode === "join" ? "login" : "join"); }}>
          {mode === "join" ? "이미 계정이 있어요" : "처음이신가요? 가입하기"}
        </button>
      )}
      <p className="hl-fine hl-left">
        시험 단계라 비밀번호는 브라우저에서 해시로만 저장돼요. 다른 곳에서 쓰는 비밀번호는 쓰지 말아주세요.
      </p>
    </div>
  );
}

function AnonBox({ title, desc, toList, onClose, onSend }) {
  const [text, setText] = useState("");
  const [to, setTo] = useState(toList?.[0]?.id || "");
  return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-sheet" onClick={(e) => e.stopPropagation()}>
        <div className="hl-sheetTitle">{title}</div>
        <p className="hl-sheetNote">{desc}</p>
        {toList && (
          <>
            <div className="hl-scopeLabel">받는 사람</div>
            <div className="hl-chips">
              {toList.map((m) => (
                <button key={m.id} className={`hl-chip2 ${to === m.id ? "on" : ""}`} onClick={() => setTo(m.id)}>{m.name}</button>
              ))}
            </div>
          </>
        )}
        <textarea className="hl-in hl-block" rows={5} placeholder="하고 싶은 말을 적어주세요." value={text} onChange={(e) => setText(e.target.value)} />
        <button className="hl-cta hl-block" disabled={text.trim().length < 2 || (toList && !to)}
          onClick={() => onSend(text.trim(), to)}>보내기</button>
      </div>
    </div>
  );
}

function Inbox({ letters }) {
  return (
    <>
      <div className="hl-sectionHead"><span>받은 편지</span><span className="hl-mono hl-dim">{letters.length}</span></div>
      {letters.length === 0 ? <div className="hl-empty">아직 받은 편지가 없어요.</div> : (
        <ul className="hl-list">
          {letters.map((l) => (
            <li key={l.id} className="hl-row">
              <div className="hl-mono hl-scene">익명</div>
              <p className="hl-rowText">{l.text}</p>
              <div className="hl-mono hl-rowDate">{when(l.at)}</div>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

function CommentImage({ id }) {
  const [src, setSrc] = useState(IMG_CACHE.get(id) || null);
  useEffect(() => {
    if (src) return;
    let live = true;
    (async () => {
      try {
        const r = await window.storage.get(`hanul:img:${id}`, true);
        IMG_CACHE.set(id, r.value);
        if (live) setSrc(r.value);
      } catch (e) {}
    })();
    return () => { live = false; };
  }, [id, src]);
  return src ? <img className="hl-comImg" src={src} alt="" /> : <div className="hl-comImg hl-imgLoad" />;
}

const WMO = (c) => {
  if (c === 0) return { e: "☀️", n: "맑음" };
  if (c <= 2) return { e: "🌤️", n: "구름 조금" };
  if (c === 3) return { e: "☁️", n: "흐림" };
  if (c <= 48) return { e: "🌫️", n: "안개" };
  if (c <= 57) return { e: "🌦️", n: "이슬비" };
  if (c <= 67) return { e: "🌧️", n: "비" };
  if (c <= 77) return { e: "🌨️", n: "눈" };
  if (c <= 82) return { e: "🌦️", n: "소나기" };
  if (c <= 86) return { e: "🌨️", n: "진눈깨비" };
  return { e: "⛈️", n: "천둥번개" };
};

function HeaderClock() {
  const [now, setNow] = useState(new Date());
  const [wx, setWx] = useState(null);
  useEffect(() => { const t = setInterval(() => setNow(new Date()), 20000); return () => clearInterval(t); }, []);
  useEffect(() => {
    let live = true;
    const get = async (lat, lon) => {
      try {
        const r = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code&timezone=Asia%2FSeoul`);
        const j = await r.json();
        if (live && j?.current) setWx({ t: Math.round(j.current.temperature_2m), c: j.current.weather_code });
      } catch (e) {}
    };
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(
        (p) => get(p.coords.latitude, p.coords.longitude),
        () => get(36.97, 127.93), { timeout: 4000 });
    } else get(36.97, 127.93);
    return () => { live = false; };
  }, []);

  const W = wx ? WMO(wx.c) : null;
  const wd = ["일", "월", "화", "수", "목", "금", "토"][now.getDay()];
  const hh = String(now.getHours()).padStart(2, "0"), mm = String(now.getMinutes()).padStart(2, "0");
  return (
    <>
      <span className="hl-clockBg" aria-hidden="true">{hh}:{mm}</span>
      <span className="hl-meta">
        {now.getMonth() + 1}월 {now.getDate()}일 <b>{wd}</b>
        {W && <><span className="hl-metaDot">·</span>{W.e} {wx.t}° {W.n}</>}
      </span>
    </>
  );
}

function ManageLog({ log, author, onSave, onDelete, onClose }) {
  const [text, setText] = useState(log.text || "");
  const [date, setDate] = useState(log.date);
  const [sure, setSure] = useState(false);
  return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-sheet" onClick={(e) => e.stopPropagation()}>
        <div className="hl-sheetTitle">일지 관리</div>
        <p className="hl-sheetNote">
          {author?.name || "?"} · {scn(log.scene)} · {partOf(log.typeId).n}
          {log.kind === "solo" ? " · 개인 기록" : ""}
        </p>
        <input className="hl-in hl-block hl-mono" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
        <textarea className="hl-in hl-block" rows={5} value={text} onChange={(e) => setText(e.target.value)} />
        <button className="hl-cta hl-block" onClick={() => onSave(text, date)}>수정 저장</button>
        {sure ? (
          <div className="hl-dayBtns">
            <button className="hl-cta done" onClick={() => setSure(false)}>그만두기</button>
            <button className="hl-cta hl-danger" onClick={onDelete}>정말 삭제</button>
          </div>
        ) : (
          <button className="hl-cta done hl-block" onClick={() => setSure(true)}>삭제하기</button>
        )}
        <p className="hl-fine hl-left">
          승인된 팀 일지를 지워도 이미 나간 포인트는 되돌아가지 않아요.
          필요하면 포인트 지급에서 회수해 주세요.
        </p>
      </div>
    </div>
  );
}

function NoticeBoard({ list, title, onDrop, canManage }) {
  if (!list.length) return null;
  return (
    <>
      <div className="hl-sectionHead"><span>{title}</span><span className="hl-mono hl-dim">{list.length}</span></div>
      <ul className="hl-list">
        {list.map((n) => (
          <li key={n.id} className={`hl-notice ${n.kind}`}>
            <div className="hl-noticeTop">
              <span className="hl-noticeTag">{n.kind === "homework" ? "숙제" : "공지"}</span>
              {n.due && <span className="hl-mono hl-noticeDue">~ {n.due.slice(5).replace("-", ".")}</span>}
              {canManage && <button className="hl-del" onClick={() => onDrop(n.id)}>삭제</button>}
            </div>
            <div className="hl-noticeTitle">{n.title}</div>
            {n.text && <p className="hl-rowText">{n.text}</p>}
            <div className="hl-mono hl-rowDate">{n.by}{n.at && <span className="hl-atTag">{when(n.at)}</span>}</div>
          </li>
        ))}
      </ul>
    </>
  );
}

function NoticeAdmin({ notice, quotes, onAdd, onDrop, onAddQuote, onDropQuote, flash }) {
  const [f, setF] = useState({ kind: "notice", title: "", text: "", due: "" });
  const [q, setQ] = useState("");
  return (
    <>
      <div className="hl-scopeLabel">종류</div>
      <div className="hl-chips">
        {[["notice", "공지"], ["homework", "숙제"]].map(([k, n]) => (
          <button key={k} className={`hl-chip2 ${f.kind === k ? "on" : ""}`} onClick={() => setF({ ...f, kind: k })}>{n}</button>
        ))}
      </div>
      <p className="hl-whereNote">
        {f.kind === "homework"
          ? "🔴 숙제 — 홈 화면의 활동일지 바로 위에 붙어요."
          : "🟡 공지 — 홈 화면 맨 위, 포인트 카드보다 먼저 떠요."}
      </p>
      <input className="hl-in hl-block" placeholder="제목 (예: 이번 주 대본 암기)" value={f.title} onChange={(e) => setF({ ...f, title: e.target.value })} />
      <textarea className="hl-in hl-block" rows={3} placeholder="내용 (선택)" value={f.text} onChange={(e) => setF({ ...f, text: e.target.value })} />
      <div className="hl-addRow">
        <input className="hl-in hl-mono" type="date" value={f.due} onChange={(e) => setF({ ...f, due: e.target.value })} />
        <button className="hl-add" onClick={() => {
          if (!f.title.trim()) { flash("제목을 먼저 적어주세요."); return; }
          onAdd({ kind: f.kind, title: f.title.trim(), text: f.text.trim(), due: f.due });
          setF({ kind: f.kind, title: "", text: "", due: "" });
          flash("올렸어요. 모두의 홈 화면 맨 위에 뜹니다.");
        }}>올리기</button>
      </div>
      <p className="hl-fine hl-left">기한을 넣으면 그날이 지난 뒤 자동으로 내려가요. 비워두면 지울 때까지 남습니다.</p>

      <div className="hl-sectionHead"><span>올라간 공지</span><span className="hl-mono hl-dim">{notice.length}</span></div>
      {notice.length === 0 ? <div className="hl-empty">아직 올린 공지가 없어요.</div> : (
        <ul className="hl-list">
          {[...notice].sort((a, b) => b.at - a.at).map((n) => (
            <li key={n.id} className="hl-row">
              <div className="hl-rowTop">
                <span className="hl-noticeTag">{n.kind === "homework" ? "숙제" : "공지"}</span>
                <button className="hl-del" onClick={() => onDrop(n.id)}>삭제</button>
              </div>
              <div className="hl-noticeTitle">{n.title}</div>
              <div className="hl-mono hl-rowDate">{n.by} · {n.due ? `~${n.due}` : "기한 없음"}</div>
            </li>
          ))}
        </ul>
      )}

      <div className="hl-sectionHead"><span>오늘의 한마디</span><span className="hl-mono hl-dim">{quotes.length}</span></div>
      <p className="hl-fine hl-left" style={{ marginTop: 0 }}>홈 화면 위쪽에 날마다 하나씩 돌아가며 뜹니다.</p>
      <div className="hl-addRow">
        <input className="hl-in" placeholder="예: 오늘 대사 한 줄만 더 외워보자" value={q} onChange={(e) => setQ(e.target.value)} />
        <button className="hl-add" onClick={() => { if (!q.trim()) return; onAddQuote(q.trim()); setQ(""); }}>추가</button>
      </div>
      {quotes.length > 0 && (
        <ul className="hl-list" style={{ marginTop: 8 }}>
          {quotes.map((x) => (
            <li key={x.id} className="hl-grantRow">
              <span className="hl-grantWhy" style={{ color: "var(--text)" }}>{x.text}</span>
              <button className="hl-del" onClick={() => onDropQuote(x.id)}>삭제</button>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

function Home({ points, total, logs, events, feed, members, comments, me, rooms, likes, onLike, mood, onMood, onComment, onDropComment,
  wroteToday, onWrite, notice, quote, onDropNotice, canManage, onManageLog,
  birth, onSetBirth, onPayFortune, fortune, onKeepFortune, gameBest, gamePlayed, onOpenGame, oePlays, onOpenOE, stockValue, onOpenMarket }) {
  const approved = logs.filter((l) => l.status === "approved").length;
  const [feedKind, setFeedKind] = useState("team");
  const [feedPart, setFeedPart] = useState("all");
  const [feedMax, setFeedMax] = useState(20);
  const shown = [...(feed || [])].filter((l) => l.status === "approved" || l.memberId === me?.id).sort((a, b) => b.scene - a.scene);
  const teamAll = shown.filter((l) => l.kind !== "solo");
  const soloAll = shown.filter((l) => l.kind === "solo");
  const partCount = (pid) => teamAll.filter((l) => l.typeId === pid).length;
  const feedList = feedKind === "solo" ? soloAll
    : feedPart === "all" ? teamAll : teamAll.filter((l) => l.typeId === feedPart);
  const up = [...(events || [])].filter((e) => e.date >= today()).sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time)).slice(0, 3);
  return (
    <>
      <NoticeBoard list={notice.filter((n) => n.kind !== "homework")} title="집행부 공지" onDrop={onDropNotice} canManage={canManage} />

      <section className="hl-meter">
        <div className="hl-meterLabel">보유 포인트</div>
        <div className="hl-meterVal">{points.toLocaleString()}<span>P</span></div>
        <div className="hl-meterSub">누적 {total.toLocaleString()}P · 승인 {approved}개 · 대기 {logs.filter((l) => l.status === "pending").length}개</div>
      </section>
      <button className={`hl-cta ${wroteToday ? "done" : ""}`} onClick={onWrite}>
        {wroteToday ? "오늘 기록을 하나 더 남기기" : "오늘 활동 기록하기"}
      </button>
      {up.length > 0 && (
        <>
          <div className="hl-sectionHead"><span>다가오는 일정</span></div>
          <ul className="hl-list">
            {up.map((e) => (
              <li key={e.id} className="hl-upRow">
                <span className="hl-mono hl-upDate">{e.date.slice(5).replace("-", ".")}</span>
                <span className="hl-upTitle">{e.title}</span>
                {e.scope === "private" && <span className="hl-mono hl-upTag">나만</span>}
                {e.time && <span className="hl-mono hl-dim">{e.time}</span>}
              </li>
            ))}
          </ul>
        </>
      )}
      <div className="hl-sectionHead"><span>오늘의 운세와 놀이</span></div>
      <Fortune me={me} birth={birth} today={today()} available={points}
        onSetBirth={onSetBirth} onPay={onPayFortune} saved={fortune} onSave={onKeepFortune} />
      <button className="hl-gameCard" onClick={onOpenGame}>
        <span className="hl-gameIcon">🎯</span>
        <span className="hl-gameBody">
          <span className="hl-gameName">조명 큐</span>
          <span className="hl-gameHint">{gamePlayed ? "오늘 포인트는 받았어요 · 연습은 자유" : "높은 콤보를 쌓으면 포인트를 받아요"}</span>
        </span>
        {gameBest > 0 && <span className="hl-mono hl-gameBest">최고 {gameBest}</span>}
      </button>
      <button className="hl-gameCard" onClick={onOpenOE}>
        <span className="hl-gameIcon">🎲</span>
        <span className="hl-gameBody">
          <span className="hl-gameName">홀짝</span>
          <span className="hl-gameHint">숫자 하나를 맞추면 열 배 · 틀리면 건 만큼 잃어요</span>
        </span>
        {oePlays > 0 && <span className="hl-mono hl-gameBest">오늘 {oePlays}판</span>}
      </button>
      <button className="hl-gameCard" onClick={onOpenMarket}>
        <span className="hl-gameIcon">📈</span>
        <span className="hl-gameBody">
          <span className="hl-gameName">한울 증권</span>
          <span className="hl-gameHint">포인트로 주식을 사고팔 수 있어요 · 5분마다 시세 변동</span>
        </span>
        {stockValue > 0 && <span className="hl-mono hl-gameBest">{stockValue.toLocaleString()}P</span>}
      </button>

      <div className="hl-sectionHead"><span>오늘의 컨디션</span></div>
      <div className="hl-moodRow">
        {MOODS.map((m) => (
          <button key={m.id} className={`hl-moodBtn ${mood?.[me?.id] === m.id ? "on" : ""}`}
            style={{ "--mc": m.c }} onClick={() => onMood(m.id)}>
            <span className="hl-moodE">{m.e}</span>
            <span className="hl-moodN">{m.n}</span>
          </button>
        ))}
      </div>

      {notice.filter((n) => n.kind === "homework").length > 0
        ? <NoticeBoard list={notice.filter((n) => n.kind === "homework")} title="이번 숙제" onDrop={onDropNotice} canManage={canManage} />
        : canManage && (
          <>
            <div className="hl-sectionHead"><span>이번 숙제</span></div>
            <div className="hl-empty">올라온 숙제가 없어요.<br />더보기 → 공지 · 숙제에서 종류를 <b>숙제</b>로 고르고 올려주세요.</div>
          </>
        )}

      <>
            <div className="hl-sectionHead"><span>활동일지</span><span className="hl-mono hl-dim">{feedList.length}</span></div>
            <div className="hl-seg">
              <button className={feedKind === "team" ? "on" : ""} onClick={() => { setFeedKind("team"); setFeedMax(20); }}>
                팀 일지 <span className="hl-mono hl-segN">{teamAll.length}</span>
              </button>
              <button className={feedKind === "solo" ? "on" : ""} onClick={() => { setFeedKind("solo"); setFeedMax(20); }}>
                개인 일지 <span className="hl-mono hl-segN">{soloAll.length}</span>
              </button>
            </div>
            {feedKind === "team" && (
              <div className="hl-partScroll">
                <button className={`hl-partChip ${feedPart === "all" ? "on" : ""}`} onClick={() => { setFeedPart("all"); setFeedMax(20); }}>
                  전체<span className="hl-mono">{teamAll.length}</span>
                </button>
                {PARTS.map((p) => (
                  <button key={p.id} className={`hl-partChip ${feedPart === p.id ? "on" : ""}`}
                    style={feedPart === p.id ? { background: p.c, borderColor: p.c } : { "--pc": p.c }}
                    onClick={() => { setFeedPart(p.id); setFeedMax(20); }}>
                    <i style={{ background: p.c }} />{p.n}<span className="hl-mono">{partCount(p.id)}</span>
                  </button>
                ))}
              </div>
            )}
      </>
      {feedList.length === 0
        ? <div className="hl-empty">
            {feedKind === "solo" ? "아직 올라온 개인 일지가 없어요."
              : feedPart === "all" ? "아직 올라온 팀 일지가 없어요."
              : `${partOf(feedPart).n}팀 일지가 아직 없어요.`}
          </div>
        : <ul className="hl-list">
            {feedList.slice(0, feedMax).map((l) => (
              <FeedCard key={l.id} log={l} author={members.find((m) => m.id === l.memberId)}
                ch={rooms?.[l.memberId]?.ch || (members.find((m) => m.id === l.memberId)?.role === "owner" ? OWNER_CH : DEFAULT_CH)}
                comments={comments.filter((c) => c.logId === l.id)} likes={likes?.[l.id] || []} onLike={onLike}
                members={members} me={me} onComment={onComment} onDropComment={onDropComment}
                canManage={canManage} onManage={onManageLog} />
            ))}
          </ul>}
      {feedList.length > feedMax && (
        <button className="hl-cta done hl-block" onClick={() => setFeedMax(feedMax + 20)}>
          더 보기 ({feedList.length - feedMax}개 남음)
        </button>
      )}
    </>
  );
}

function FeedCard({ log, author, ch, comments, likes, onLike, members, me, onComment, onDropComment, canManage, onManage }) {
  const [open, setOpen] = useState(false);
  const p = partOf(log.typeId);
  return (
    <li className={`hl-feed ${log.status}`}>
      <div className="hl-feedTop">
        <span className="hl-face sm"><CharFace id={author?.id} ch={ch} /></span>
        <div className="hl-feedWho">
          <div className="hl-feedName">
            {author?.name || "알 수 없음"}
            {author?.role && author.role !== "member" && <span className="hl-roleTag">{ROLES[author.role]}</span>}
          </div>
          <div className="hl-mono hl-dim hl-feedMeta">
            {fmtDate(log.date)}
            {log.at && <span className="hl-atTag">{when(log.at)}</span>}
            {log.editedAt && <span className="hl-atTag">수정됨</span>}
          </div>
        </div>
        <span className="hl-partTag" style={{ borderColor: p.c, color: p.c }}>{p.n}</span>
        {canManage && <button className="hl-manageBtn" onClick={() => onManage(log)} aria-label="일지 관리">⋯</button>}
      </div>
      <p className="hl-feedText">{log.text}</p>
      <div className="hl-feedBar">
        <span className="hl-mono hl-scene">{scn(log.scene)}</span>
        {log.status === "pending" && <span className="hl-chip">승인 대기</span>}
        <button className={`hl-likeBtn ${likes.includes(me?.id) ? "on" : ""}`} onClick={() => onLike(log.id)}>
          <span className="hl-heart">{likes.includes(me?.id) ? "♥" : "♡"}</span>{likes.length || ""}
        </button>
        <button className="hl-comBtn" onClick={() => setOpen(true)}>댓글 {comments.length}</button>
      </div>
      {likes.length > 0 && (
        <div className="hl-likeWho">
          {likes.slice(0, 3).map((id) => members.find((m) => m.id === id)?.name).filter(Boolean).join(" · ")}
          {likes.length > 3 ? ` 외 ${likes.length - 3}명이 좋아해요` : "님이 좋아해요"}
        </div>
      )}
      {open && (
        <CommentSheet log={log} author={author} comments={comments} members={members} me={me}
          onComment={onComment} onDrop={onDropComment} onClose={() => setOpen(false)} />
      )}
    </li>
  );
}

function CommentSheet({ log, author, comments, members, me, onComment, onDrop, onClose }) {
  const [text, setText] = useState("");
  const [img, setImg] = useState(null);
  const [busy, setBusy] = useState(false);
  const [reply, setReply] = useState(null);   // 답글 대상
  const nameOf = (id) => members.find((m) => m.id === id)?.name || "알 수 없음";
  const chOf = (id) => {
    const m = members.find((x) => x.id === id);
    return m?.role === "owner" ? OWNER_CH : DEFAULT_CH;
  };
  const roots = comments.filter((c) => !c.parent).sort((a, b) => a.at - b.at);
  const kids = (id) => comments.filter((c) => c.parent === id).sort((a, b) => a.at - b.at);

  const pickImg = async (e) => {
    const f = e.target.files?.[0]; e.target.value = "";
    if (!f) return;
    try { setImg(await shrink(f)); } catch (err) { }
  };
  const send = async () => {
    if (!text.trim() && !img) return;
    setBusy(true);
    let imgId = null;
    if (img) {
      imgId = `i_${Date.now()}`;
      try { await window.storage.set(`hanul:img:${imgId}`, img, true); IMG_CACHE.set(imgId, img); }
      catch (e) { imgId = null; }
    }
    await onComment(log.id, text.trim(), imgId, reply?.id || null);
    setText(""); setImg(null); setReply(null); setBusy(false);
  };

  const Row = ({ c, sub }) => (
    <div className={`hl-com ${sub ? "sub" : ""}`}>
      <span className="hl-face xs"><CharFace id={c.memberId} ch={chOf(c.memberId)} /></span>
      <div className="hl-comBody">
        <div className="hl-comHead">
          <strong>{nameOf(c.memberId)}</strong>
          <span className="hl-mono hl-dim">{when(c.at)}</span>
          {c.memberId === me.id && <button className="hl-del" onClick={() => onDrop(c.id)}>삭제</button>}
        </div>
        {c.text && <p className="hl-comText">{c.text}</p>}
        {c.imgId && <CommentImage id={c.imgId} />}
        {!sub && <button className="hl-replyBtn" onClick={() => setReply(c)}>답글</button>}
      </div>
    </div>
  );

  return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-sheet hl-comSheet" onClick={(e) => e.stopPropagation()}>
        <div className="hl-comTop">
          <div>
            <div className="hl-sheetTitle">댓글 {comments.length}</div>
            <p className="hl-sheetNote">{author?.name} · {scn(log.scene)}</p>
          </div>
          <button className="hl-closeX" onClick={onClose} aria-label="닫기">×</button>
        </div>

        <div className="hl-comScroll">
          {roots.length === 0
            ? <div className="hl-empty">아직 댓글이 없어요. 첫 댓글을 남겨보세요.</div>
            : roots.map((c) => (
              <div key={c.id}>
                <Row c={c} />
                {kids(c.id).map((k) => <Row key={k.id} c={k} sub />)}
              </div>
            ))}
        </div>

        {reply && (
          <div className="hl-replyTo">
            <span>{nameOf(reply.memberId)}님에게 답글</span>
            <button onClick={() => setReply(null)}>취소</button>
          </div>
        )}
        {img && <div className="hl-preview"><img src={img} alt="" /><button onClick={() => setImg(null)}>빼기</button></div>}
        <div className="hl-comInput">
          <input className="hl-in" placeholder={reply ? "답글 남기기" : "댓글 남기기"} value={text} onChange={(e) => setText(e.target.value)} />
          <label className="hl-imgBtn">＋<input type="file" accept="image/*" onChange={pickImg} /></label>
          <button className="hl-add" onClick={send} disabled={busy}>{busy ? "…" : "등록"}</button>
        </div>
      </div>
    </div>
  );
}

function LogRow({ log, who }) {
  const a = partOf(log.typeId);
  const st = { pending: "대기", approved: "승인", rejected: "반려" }[log.status];
  return (
    <li className={`hl-row ${log.status}`}>
      <div className="hl-rowTop"><span className="hl-mono hl-scene">{scn(log.scene)}</span><span className={`hl-chip ${log.status}`}>{st}</span></div>
      <div className="hl-rowMain">
        {who && <strong className="hl-who2">{who}</strong>}
        <span className="hl-act">{a.n}</span><span className="hl-mono hl-pt">+{log.pt}P</span>
      </div>
      {log.text && <p className="hl-rowText">{log.text}</p>}
      <div className="hl-mono hl-rowDate">{fmtDate(log.date)}{log.at && <span className="hl-atTag">{when(log.at)}</span>}</div>
    </li>
  );
}

function Record({ view, setView, events, logs, me, isAdmin, selDate, setSelDate, onAdd, onRemove, nextScene, onSubmit,
  attend, auto, canTeam, mood, members, rooms, onEditLog, onRemoveLog }) {
  const [edit, setEdit] = useState(null);
  const [ym, setYm] = useState(ymOf(selDate));
  const [adding, setAdding] = useState(false);
  const [y, m] = ym.split("-").map(Number);
  const lead = new Date(y, m - 1, 1).getDay();
  const total = new Date(y, m, 0).getDate();
  const cells = [...Array(lead).fill(null), ...Array.from({ length: total }, (_, i) => i + 1)];
  const shift = (n) => { const d = new Date(y, m - 1 + n, 1); setYm(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`); };

  const dayEvents = (ds) => events.filter((e) => e.date === ds);
  const dayLogs = (ds) => logs.filter((l) => l.date === ds);
  const selEvents = dayEvents(selDate).sort((a, b) => (a.time || "99").localeCompare(b.time || "99"));
  const selLogs = dayLogs(selDate);
  const canRemove = (e) => (e.scope === "public" ? isAdmin : e.ownerId === me.id);
  const att = (ds) => attend?.[ds]?.[me.id] || null;
  const moodsOn = (ds) => Object.entries(mood?.[ds] || {});
  const mCnt = { present: 0, late: 0, absent: 0 };
  Array.from({ length: total }, (_, i) => dstr(y, m, i + 1)).forEach((ds) => {
    const k = attend?.[ds]?.[me.id]; if (k) mCnt[k]++;
  });
  const mTot = mCnt.present + mCnt.late + mCnt.absent;
  const attSummary = { ...mCnt, rate: mTot ? Math.round(((mCnt.present + mCnt.late * .5) / mTot) * 100) : 0 };
  const mine = logs.filter((l) => l.memberId === me.id).sort((a, b) => b.scene - a.scene);
  const canEdit = (l) => l.kind === "solo" || l.status === "pending";

  return (
    <>
      <div className="hl-seg">
        <button className={view === "cal" ? "on" : ""} onClick={() => { setEdit(null); setView("cal"); }}>달력</button>
        <button className={view === "write" ? "on" : ""} onClick={() => setView("write")}>일지 쓰기</button>
        <button className={view === "mine" ? "on" : ""} onClick={() => { setEdit(null); setView("mine"); }}>내 일지</button>
      </div>

      {view === "write" ? (
        <Write nextScene={nextScene} onSubmit={onSubmit} auto={auto} canTeam={canTeam} part={me.part}
          attSummary={attSummary} edit={edit} onEdit={(id, t, d) => { onEditLog(id, t, d); setEdit(null); setView("mine"); }}
          onCancelEdit={() => { setEdit(null); setView("mine"); }} />
      ) : view === "mine" ? (
        <>
          <div className="hl-sectionHead"><span>내가 쓴 기록</span><span className="hl-mono hl-dim">{mine.length}</span></div>
          {mine.length === 0 ? <div className="hl-empty">아직 남긴 기록이 없어요.</div> : (
            <ul className="hl-list">
              {mine.map((l) => (
                <li key={l.id} className={`hl-row ${l.status}`}>
                  <div className="hl-rowTop">
                    <span className="hl-mono hl-scene">{scn(l.scene)}{l.kind === "solo" ? " · 개인" : ""}</span>
                    <span className="hl-chip">{l.kind === "solo" ? "개인" : { pending: "대기", approved: "승인", rejected: "반려" }[l.status]}</span>
                  </div>
                  <p className="hl-rowText">{l.text}</p>
                  <div className="hl-mono hl-rowDate">
                    {fmtDate(l.date)}
                    {l.at && <span className="hl-atTag">{when(l.at)}</span>}
                    {l.editedAt && <span className="hl-atTag">수정됨</span>}
                  </div>
                  <div className="hl-actions">
                    {canEdit(l) ? (
                      <>
                        <button className="hl-reject" onClick={() => { setEdit(l); setView("write"); }}>수정</button>
                        <button className="hl-reject" onClick={() => onRemoveLog(l.id)}>삭제</button>
                      </>
                    ) : <span className="hl-fine hl-left" style={{ margin: 0 }}>승인된 팀 일지는 고칠 수 없어요.</span>}
                  </div>
                </li>
              ))}
            </ul>
          )}
        </>
      ) : (
        <>
          <div className="hl-attMini wide">
            <span className="hl-mono hl-attMiniL">이번 달 내 출석률</span>
            <span className="hl-attMiniV">{attSummary.rate}%</span>
            <span className="hl-mono hl-attMiniS">출석 {attSummary.present} · 지각 {attSummary.late} · 결석 {attSummary.absent}</span>
          </div>
          <div className="hl-calHead">
            <button className="hl-navBtn" onClick={() => shift(-1)} aria-label="이전 달">‹</button>
            <span className="hl-mono hl-calYm">{y}. {String(m).padStart(2, "0")}</span>
            <button className="hl-navBtn" onClick={() => shift(1)} aria-label="다음 달">›</button>
          </div>

          <div className="hl-cal">
            {WEEK.map((w, i) => <div key={w} className={`hl-wd ${i === 0 ? "sun" : ""}`}>{w}</div>)}
            {cells.map((d, i) => {
              if (!d) return <div key={`b${i}`} />;
              const ds = dstr(y, m, d);
              const evs = dayEvents(ds);
              const lgs = dayLogs(ds);
              return (
                <button key={ds} className={`hl-day ${ds === selDate ? "sel" : ""} ${ds === today() ? "today" : ""}`}
                  onClick={() => setSelDate(ds)}>
                  <span className={`hl-dayNum ${i % 7 === 0 ? "sun" : ""}`}>{d}</span>
                  <span className="hl-emos">
                    {moodsOn(ds).slice(0, 4).map(([mid, v], k) =>
                      <i key={k} style={{ background: moodOf(v)?.c }} />)}
                  </span>
                  <span className="hl-dots">
                    {att(ds) && <i style={{ background: ATT[att(ds)].c }} />}
                    {evs.some((e) => e.scope === "public") && <i className="pub" />}
                    {evs.some((e) => e.scope === "private") && <i className="pri" />}
                    {lgs.length > 0 && <i className="log" />}
                  </span>
                </button>
              );
            })}
          </div>

          <div className="hl-sectionHead">
            <span>{selDate.slice(5).replace("-", "월 ")}일</span>
            <span className="hl-mono hl-dim">{fmtDate(selDate).split("  ")[1]}</span>
          </div>

          {moodsOn(selDate).length > 0 && (
            <>
              <div className="hl-sectionHead"><span>이날의 컨디션</span><span className="hl-mono hl-dim">{moodsOn(selDate).length}명</span></div>
              <div className="hl-moodList">
                {moodsOn(selDate).map(([mid, v]) => {
                  const mem = members.find((x) => x.id === mid), M = moodOf(v);
                  if (!mem || !M) return null;
                  return (
                    <div key={mid} className="hl-moodItem" style={{ borderColor: M.c }}>
                      <span className="hl-moodE" style={{ background: `color-mix(in srgb, ${M.c} 26%, transparent)` }}>{M.e}</span>
                      <span className="hl-moodWho">{mem.name}</span>
                      <span className="hl-mono hl-dim">{M.n}</span>
                    </div>
                  );
                })}
              </div>
            </>
          )}
          {att(selDate) && (
            <div className="hl-attRow" style={{ borderColor: ATT[att(selDate)].c }}>
              <span className="hl-attName" style={{ color: ATT[att(selDate)].c }}>{ATT[att(selDate)].n}</span>
              <span className="hl-mono hl-dim">{ATT[att(selDate)].pt > 0 ? "+" : ""}{ATT[att(selDate)].pt}P</span>
            </div>
          )}
          {selEvents.length === 0 && selLogs.length === 0
            ? <div className="hl-empty">이날은 아직 아무것도 없어요.</div>
            : (
              <ul className="hl-list">
                {selEvents.map((e) => (
                  <li key={e.id} className={`hl-ev ${e.scope}`}>
                    <div className="hl-evTop">
                      <span className={`hl-chip ${e.scope === "public" ? "approved" : ""}`}>{e.scope === "public" ? "전체" : "나만"}</span>
                      {e.time && <span className="hl-mono hl-dim">{e.time}</span>}
                      {canRemove(e) && <button className="hl-del" onClick={() => onRemove(e.id)}>삭제</button>}
                    </div>
                    <div className="hl-evTitle">{e.title}</div>
                    {e.note && <p className="hl-rowText">{e.note}</p>}
                    {e.scope === "public" && <div className="hl-mono hl-rowDate">{e.ownerName} 등록</div>}
                  </li>
                ))}
                {selLogs.map((l) => <LogRow key={l.id} log={l} />)}
              </ul>
            )}

          <div className="hl-dayBtns">
            <button className="hl-cta" onClick={() => setAdding(true)}>일정 추가</button>
            <button className="hl-cta done" onClick={() => setView("write")}>일지 쓰기</button>
          </div>

          {adding && <EventForm date={selDate} isAdmin={isAdmin} onClose={() => setAdding(false)}
            onSave={(ev) => { onAdd(ev); setAdding(false); }} />}
        </>
      )}
    </>
  );
}

function EventForm({ date, isAdmin, onClose, onSave }) {
  const [title, setTitle] = useState("");
  const [time, setTime] = useState("");
  const [note, setNote] = useState("");
  const [scope, setScope] = useState("private");
  const [err, setErr] = useState("");
  const go = () => {
    if (!title.trim()) { setErr("일정 이름을 적어주세요."); return; }
    onSave({ date, time, title: title.trim(), note: note.trim(), scope });
  };
  return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-sheet" onClick={(e) => e.stopPropagation()}>
        <div className="hl-sheetTitle">일정 추가</div>
        <p className="hl-sheetNote">{fmtDate(date)}</p>

        <input className="hl-in hl-block" placeholder="일정 이름 (예: 3막 동선 연습)" value={title} onChange={(e) => setTitle(e.target.value)} />
        <div className="hl-addRow">
          <input className="hl-in hl-mono" type="time" value={time} onChange={(e) => setTime(e.target.value)} />
        </div>
        <input className="hl-in hl-block" placeholder="메모 (선택)" value={note} onChange={(e) => setNote(e.target.value)} />

        <div className="hl-scopeLabel">공개 범위</div>
        <div className="hl-scopes">
          <button className={`hl-scope ${scope === "private" ? "on" : ""}`} onClick={() => setScope("private")}>
            <span className="hl-scopeName">나만 보기</span>
            <span className="hl-scopeSub">내 달력에만 표시돼요</span>
          </button>
          <button className={`hl-scope ${scope === "public" ? "on" : ""} ${isAdmin ? "" : "locked"}`}
            onClick={() => isAdmin && setScope("public")} disabled={!isAdmin}>
            <span className="hl-scopeName">전체 공개{!isAdmin && " 🔒"}</span>
            <span className="hl-scopeSub">{isAdmin ? "모든 단원의 달력에 표시돼요" : "집행부만 올릴 수 있어요"}</span>
          </button>
        </div>

        {err && <p className="hl-err hl-onDark">{err}</p>}
        <button className="hl-cta hl-block" onClick={go}>저장하기</button>
      </div>
    </div>
  );
}

function Write({ nextScene, onSubmit, auto, canTeam, part, attSummary, edit, onEdit, onCancelEdit }) {
  const [kind, setKind] = useState(canTeam ? "team" : "solo");
  const [text, setText] = useState(edit?.text || "");
  const [date, setDate] = useState(edit?.date || today());
  const [err, setErr] = useState("");
  const a = partOf(part);
  const solo = !!edit ? edit.kind === "solo" : kind === "solo";

  useEffect(() => { setText(edit?.text || ""); setDate(edit?.date || today()); }, [edit?.id]);

  const go = () => {
    if (text.trim().length < 5) { setErr("무엇을 했는지 다섯 글자 이상 적어주세요."); return; }
    setErr("");
    if (edit) onEdit(edit.id, text, date);
    else onSubmit(text, date, kind);
    setText("");
  };

  return (
    <div className="hl-script">
      <div className="hl-scriptHead">
        <span className="hl-mono">{edit ? "수정" : scn(nextScene)}</span>
        <input className="hl-mono hl-date" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
      </div>

      {!edit && (
        <>
          <div className="hl-scriptLabel">어떤 기록인가요</div>
          <div className="hl-kinds">
            <button className={`hl-kind ${kind === "team" ? "on" : ""} ${canTeam ? "" : "lock"}`}
              disabled={!canTeam} onClick={() => canTeam && setKind("team")}>
              <span className="hl-kindN">팀 활동일지{!canTeam && " 🔒"}</span>
              <span className="hl-kindD">{canTeam ? "승인되면 팀 전체에 포인트가 갑니다" : "팀장 · 집행부만 쓸 수 있어요"}</span>
            </button>
            <button className={`hl-kind ${kind === "solo" ? "on" : ""}`} onClick={() => setKind("solo")}>
              <span className="hl-kindN">개인 기록</span>
              <span className="hl-kindD">바로 올라가고 포인트는 없어요</span>
            </button>
          </div>
        </>
      )}

      <div className="hl-scriptLabel">내 파트</div>
      <div className="hl-partFix">
        <span className="hl-dotSm" style={{ background: a.c }} />
        <span className="hl-partFixN">{a.n}</span>
        <span className="hl-mono hl-partFixL">고정</span>
      </div>

      {attSummary && (
        <div className="hl-attMini">
          <span className="hl-mono hl-attMiniL">이번 달 출석</span>
          <span className="hl-attMiniV">{attSummary.rate}%</span>
          <span className="hl-mono hl-attMiniS">
            출석 {attSummary.present} · 지각 {attSummary.late} · 결석 {attSummary.absent}
          </span>
        </div>
      )}

      <div className="hl-scriptLabel">오늘 한 일</div>
      <textarea className="hl-ta" rows={5} value={text} onChange={(e) => setText(e.target.value)}
        placeholder="3막 동선 맞추고 조명 큐 정리했음. 소품 우산 하나 부러져서 교체 필요." />
      {err && <p className="hl-err">{err}</p>}

      <button className="hl-submit" onClick={go}>
        {edit ? "수정 저장" : "기록 올리기"}
        {!solo && !edit && <span className="hl-mono">+{LOG_PT}P</span>}
      </button>
      {edit && <button className="hl-cancelBtn" onClick={onCancelEdit}>수정 그만두기</button>}
      {!edit && (
        <p className="hl-fine">
          {solo ? "바로 피드에 올라가요. 포인트는 붙지 않아요."
            : auto ? "바로 피드에 올라가고 부원들이 댓글을 달 수 있어요."
            : "집행부가 승인하면 피드에 올라가고 같은 파트 팀원에게도 포인트가 들어가요."}
        </p>
      )}
    </div>
  );
}

function MyRoom({ room, available, onRoll, onPlace, onClear, onChar, onBuySet, onWearSet, onRollHouse, onWearHouse, onWearSky, onWearWall, name, charId,
  visiting, guest = [], members = [], meId, onGuest, onDropGuest, onExit }) {
  const [pick, setPick] = useState(null);   // 선택한 자리
  const [dress, setDress] = useState(false);
  const [result, setResult] = useState(null);
  const [rolling, setRolling] = useState(false);
  const [rawView, setView] = useState("stage");
  const view = visiting ? "stage" : rawView;
  const [inside, setInside] = useState(false);
  const [opening, setOpening] = useState(false);

  const owned = Object.keys(room.inv || {}).filter((k) => room.inv[k] > 0);
  const placedCount = (id) => Object.values(room.slots || {}).filter((v) => v === id).length;
  const spare = (id) => (room.inv[id] || 0) - placedCount(id);
  const isBare = Object.keys(room.slots || {}).length === 0;
  const spareTotal = owned.reduce((n, id) => n + spare(id), 0);

  const doRoll = async (n) => {
    if (rolling) return;
    setRolling(true);
    const r = await onRoll(n);
    setRolling(false);
    if (r.error) A.sfx("deny");
    setResult(r);
  };
  const doRollHouse = async (n) => {
    if (rolling) return;
    setRolling(true);
    const r = await onRollHouse(n);
    setRolling(false);
    if (r.error) A.sfx("deny");
    setResult(r);
  };
  const houseId = room.house || "tent";
  const myHouses = room.houses?.length ? room.houses : ["tent"];
  const enter = () => {
    if (opening) return;
    setOpening(true);
    A.sfx("door");
    setTimeout(() => { setInside(true); setOpening(false); }, 940);
  };

  const Nav = () => visiting ? (
    <div className="hl-visitBar">
      <button className="hl-back" onClick={onExit}>‹ 돌아가기</button>
      <span className="hl-visitName">{visiting.name}님의 방</span>
    </div>
  ) : (
    <div className="hl-seg">
      <button className={view === "stage" ? "on" : ""} onClick={() => setView("stage")}>내 무대</button>
      <button className={view === "gacha" ? "on" : ""} onClick={() => setView("gacha")}>뽑기방</button>
      <button className={view === "shop" ? "on" : ""} onClick={() => setView("shop")}>세트</button>
    </div>
  );

  const Guestbook = () => (
    <>
      <div className="hl-sectionHead"><span>방명록</span><span className="hl-mono hl-dim">{guest.length}</span></div>
      {guest.length === 0
        ? <div className="hl-empty">{visiting ? "첫 방명록을 남겨보세요." : "아직 남겨진 글이 없어요."}</div>
        : <ul className="hl-list">
            {[...guest].sort((a, b) => b.at - a.at).slice(0, 15).map((g) => (
              <li key={g.id} className="hl-guestRow">
                <div className="hl-comHead">
                  <strong>{members.find((m) => m.id === g.memberId)?.name || "?"}</strong>
                  <span className="hl-mono hl-dim">{when(g.at)}</span>
                  {(g.memberId === meId || (!visiting)) && <button className="hl-del" onClick={() => onDropGuest(g.id)}>삭제</button>}
                </div>
                <p className="hl-comText">{g.text}</p>
              </li>
            ))}
          </ul>}
      <GuestInput onSend={onGuest} />
    </>
  );

  if (view === "gacha") return (
    <><Nav />
      <GachaRoom room={room} available={available} rolling={rolling} onRoll={doRoll} onRollHouse={doRollHouse} owned={owned} />
      {result && <Reveal result={result} onClose={() => setResult(null)} />}
    </>
  );

  if (view === "shop") return (
    <><Nav />
      <SetShop room={room} available={available} onBuy={onBuySet} onWear={onWearSet} />
    </>
  );

  if (!inside) {
    const H = houseOf(houseId), D = DOORS[houseId] || DOORS.tent;
    return (
      <><Nav />
        <div className={`hl-ext ${opening ? "open" : ""}`} data-sky={room.sky || "night"}
          style={{
            "--dx": `${((D[0] + D[2] / 2) / 240) * 100}%`,
            "--dy": `${((D[1] + D[3] / 2) / 250) * 100}%`,
            "--skyA": skyOf(room.sky).a, "--skyB": skyOf(room.sky).b,
          }}>
          <div className="hl-extArt"><HouseArt id={houseId} /></div>
          <button className="hl-doorHit" onClick={enter} aria-label="문 열고 들어가기"
            style={{ left: `${(D[0] / 240) * 100}%`, top: `${(D[1] / 250) * 100}%`, width: `${(D[2] / 240) * 100}%`, height: `${(D[3] / 250) * 100}%` }}>
            <span className="hl-doorRing" />
          </button>
          <span className="hl-doorGlow" />
          <div className="hl-extName">
            <span className="hl-mono hl-extTier" style={{ color: RARITY[H.tier].c }}>{RARITY[H.tier].n}</span>
            <span className="hl-extTitle">{H.n}</span>
          </div>
        </div>
        <p className="hl-bare">문을 눌러 안으로 들어가요.</p>

        {!visiting && (
          <>
            <div className="hl-sectionHead"><span>바깥 배경</span></div>
            <div className="hl-swatchRow">
              {SKIES.map((sk) => (
                <button key={sk.id} className={`hl-skyDot ${(room.sky || "night") === sk.id ? "on" : ""}`}
                  onClick={() => onWearSky(sk.id)} title={sk.n}>
                  <span style={{ background: `linear-gradient(160deg, ${sk.a}, ${sk.b})` }} />
                  <em>{sk.n}</em>
                </button>
              ))}
            </div>
          </>
        )}

        <div className="hl-sectionHead"><span>가진 외형</span><span className="hl-mono hl-dim">{myHouses.length} / {HOUSES.length}</span></div>
        <div className="hl-houseGrid">
          {HOUSES.map((h) => {
            const has = myHouses.includes(h.id);
            return (
              <button key={h.id} className={`hl-houseCell ${houseId === h.id ? "on" : ""} ${has ? "" : "locked"}`}
                disabled={!has} onClick={() => onWearHouse(h.id)} style={has ? { borderColor: RARITY[h.tier].c } : {}}>
                <div className="hl-houseArt">{has ? <HouseArt id={h.id} /> : <span className="hl-lockMark">?</span>}</div>
                <div className="hl-invName">{has ? h.n : "미발견"}</div>
                <div className="hl-mono hl-invMeta" style={has ? { color: RARITY[h.tier].c } : {}}>{RARITY[h.tier].n}</div>
              </button>
            );
          })}
        </div>
        {result && <Reveal result={result} onClose={() => setResult(null)} />}
      </>
    );
  }

  return (
    <>
      <Nav />
      <div className="hl-sectionHead">
        <span>내 무대</span>
        <span className="hl-mono hl-dim">소품 {owned.length} / {ITEMS.length}</span>
      </div>

      <Diorama room={room} charId={charId} showSlots={!visiting && spareTotal > 0} onSlot={(sl) => { if (!visiting) setPick(sl); }} />

      {isBare && (
        <p className="hl-bare">
          아직 아무것도 없는 빈 무대예요.<br />
          뽑기로 소품을 얻어 하나씩 올려보세요.
        </p>
      )}

      <div className="hl-dayBtns hl-dress">
        <button className="hl-cta done" onClick={() => setInside(false)}>밖으로</button>
        {!visiting && <button className="hl-cta done" onClick={() => setDress(true)}>캐릭터 꾸미기</button>}
      </div>
      <Guestbook />

      {!visiting && (
        <>
          <div className="hl-sectionHead"><span>벽지 · 바닥</span></div>
          <div className="hl-swatchRow">
            {WALLS.map((wp) => (
              <button key={wp.id} className={`hl-wallDot ${(room.wall || "default") === wp.id ? "on" : ""}`}
                onClick={() => onWearWall(wp.id)} title={wp.n}>
                <span className="hl-wallChip">
                  {wp.wl ? (
                    <>
                      <i style={{ background: wp.wl }} />
                      <i style={{ background: wp.wr }} />
                      <i style={{ background: wp.fl }} />
                    </>
                  ) : <b>세트</b>}
                </span>
                <em>{wp.n}</em>
              </button>
            ))}
          </div>
          <p className="hl-fine hl-left">'기본'은 지금 쓰는 무대 세트의 색을 그대로 씁니다.</p>
        </>
      )}

      {!visiting && owned.length > 0 && (
        <>
          <div className="hl-sectionHead"><span>보관함</span></div>
          <div className="hl-invGrid">
            {owned.map((id) => {
              const it = itemOf(id);
              return (
                <div key={id} className="hl-invCell" style={{ borderColor: RARITY[it.tier].c }}>
                  <div className="hl-invArt"><ItemArt id={id} /></div>
                  <div className="hl-invName">{it.n}</div>
                  <div className="hl-mono hl-invMeta" style={{ color: RARITY[it.tier].c }}>
                    {RARITY[it.tier].n} ×{room.inv[id]}
                  </div>
                </div>
              );
            })}
          </div>
        </>
      )}

      {pick && (
        <div className="hl-sheetWrap" onClick={() => setPick(null)}>
          <div className="hl-sheet" onClick={(e) => e.stopPropagation()}>
            <div className="hl-sheetTitle">{pick.zone === "wall" ? "벽에 걸 소품" : "바닥에 놓을 소품"}</div>
            <p className="hl-sheetNote">이 자리에 올릴 소품을 고르세요.</p>
            <div className="hl-pickGrid">
              {owned.filter((id) => itemOf(id).zone === pick.zone).map((id) => {
                const left = spare(id) + (room.slots?.[pick.id] === id ? 1 : 0);
                return (
                  <button key={id} className="hl-pickCell" disabled={left <= 0}
                    onClick={() => { onPlace(pick.id, id); setPick(null); }}>
                    <div className="hl-invArt"><ItemArt id={id} /></div>
                    <div className="hl-invName">{itemOf(id).n}</div>
                    <div className="hl-mono hl-invMeta">{left > 0 ? `여유 ${left}` : "모두 배치됨"}</div>
                  </button>
                );
              })}
            </div>
            {owned.filter((id) => itemOf(id).zone === pick.zone).length === 0 &&
              <div className="hl-empty">{pick.zone === "wall" ? "벽에 걸 소품이 아직 없어요." : "바닥에 놓을 소품이 아직 없어요."}</div>}
            {room.slots?.[pick.id] &&
              <button className="hl-removeBtn" onClick={() => { onClear(pick.id); setPick(null); }}>이 자리 비우기</button>}
          </div>
        </div>
      )}

      {dress && (
        <div className="hl-sheetWrap" onClick={() => setDress(false)}>
          <div className="hl-sheet hl-dressSheet" onClick={(e) => e.stopPropagation()}>
            <div className="hl-dressTop">
              <Character ch={room.ch} className="hl-charPreview" />
              <div>
                <div className="hl-sheetTitle">{name}</div>
                <p className="hl-sheetNote">고르면 바로 무대에 반영돼요.</p>
              </div>
            </div>

            <div className="hl-scopeLabel">머리</div>
            <div className="hl-chips">
              {HAIRS.map((h) => (
                <button key={h.id} className={`hl-chip2 ${(room.ch?.hair || DEFAULT_CH.hair) === h.id ? "on" : ""}`}
                  onClick={() => onChar({ hair: h.id })}>{h.n}</button>
              ))}
            </div>

            <div className="hl-scopeLabel">머리색</div>
            <div className="hl-chips">
              {HAIRC.map((c, i) => (
                <button key={c} className={`hl-dot ${(room.ch?.hc ?? 0) === i ? "on" : ""}`}
                  style={{ background: c }} onClick={() => onChar({ hc: i })} aria-label={`머리색 ${i + 1}`} />
              ))}
            </div>

            <div className="hl-scopeLabel">옷</div>
            <div className="hl-chips">
              {FITS.map((f) => (
                <button key={f.id} className={`hl-chip2 ${(room.ch?.fit || DEFAULT_CH.fit) === f.id ? "on" : ""}`}
                  onClick={() => onChar({ fit: f.id })}>{f.n}</button>
              ))}
            </div>

            <div className="hl-scopeLabel">피부톤</div>
            <div className="hl-chips">
              {SKINS.map((c, i) => (
                <button key={c} className={`hl-dot ${(room.ch?.sk ?? 0) === i ? "on" : ""}`}
                  style={{ background: c }} onClick={() => onChar({ sk: i })} aria-label={`피부톤 ${i + 1}`} />
              ))}
            </div>

            <button className="hl-cta hl-block" onClick={() => setDress(false)}>완료</button>
          </div>
        </div>
      )}

      {result && <Reveal result={result} onClose={() => setResult(null)} />}
    </>
  );
}

const TIER_ORDER = ["legend", "epic", "rare", "common"];
const TIER_DUR = { common: 900, rare: 1400, epic: 1900, legend: 2600 };

function SetShop({ room, available, onBuy, onWear }) {
  const have = room.sets || [];
  const cur = room.set || "none";
  return (
    <>
      <div className="hl-sectionHead"><span>무대 세트</span><span className="hl-mono hl-dim">{have.length + 1} / {Object.keys(SETS).length}</span></div>
      <p className="hl-fine hl-left" style={{ marginTop: 0 }}>
        세트를 바꾸면 벽 · 바닥 · 창이 통째로 달라져요. 올려둔 소품은 그대로 남습니다.
      </p>
      <div className="hl-setList">
        {Object.entries(SETS).map(([k, S]) => {
          const owned = k === "none" || have.includes(k);
          const on = cur === k;
          return (
            <div key={k} className={`hl-setCard ${on ? "on" : ""}`}>
              <div className="hl-setPreview">
                <Art kind="set" id={k}>
                <svg viewBox="0 0 480 300" xmlns="http://www.w3.org/2000/svg">
                  <polygon points={ROOM.wallL} fill={S.wl} />
                  <polygon points={ROOM.wallR} fill={S.wr} />
                  <polygon points={ROOM.floor} fill={S.fl} />
                  <Backdrop kind={S.back} />
                  <polyline points="32,188 240,108 448,188" fill="none" stroke={S.ln} strokeWidth="2.5" opacity=".7" />
                </svg>
                </Art>
                {!owned && <span className="hl-setLock">잠김</span>}
              </div>
              <div className="hl-setBody">
                <div className="hl-setName">{S.n}</div>
                <div className="hl-setDesc">{S.d}</div>
                {owned ? (
                  <button className={`hl-attBtn ${on ? "on" : ""}`} onClick={() => onWear(k)}>{on ? "사용 중" : "적용하기"}</button>
                ) : (
                  <button className="hl-attBtn buy" disabled={available < S.cost} onClick={() => onBuy(k)}>
                    {S.cost.toLocaleString()}P{available < S.cost ? ` · ${(S.cost - available).toLocaleString()}P 부족` : " · 구입"}
                  </button>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </>
  );
}

function GuestInput({ onSend }) {
  const [t, setT] = useState("");
  return (
    <div className="hl-comInput" style={{ marginTop: 10 }}>
      <input className="hl-in" placeholder="방명록을 남겨보세요" value={t} onChange={(e) => setT(e.target.value)} />
      <button className="hl-add" disabled={!t.trim()} onClick={() => { onSend(t.trim()); setT(""); }}>남기기</button>
    </div>
  );
}

function GachaRoom({ room, available, rolling, onRoll, onRollHouse, owned }) {
  const cost10 = GACHA_COST * 9;
  return (
    <>
      <div className="hl-sectionHead">
        <span>뽑기방</span>
        <span className="hl-mono hl-dim">{owned.length} / {ITEMS.length} 수집</span>
      </div>

      <div className="hl-box">
        <div className="hl-boxGlow" />
        <div className="hl-boxInner">
          <div className="hl-mono hl-boxLabel">보유 포인트</div>
          <div className="hl-boxPt">{available.toLocaleString()}<span>P</span></div>
        </div>
      </div>

      <div className="hl-dayBtns">
        <button className="hl-cta done" onClick={() => onRoll(1)} disabled={rolling}>
          1회<span className="hl-mono hl-costTag">{GACHA_COST}P</span>
        </button>
        <button className="hl-cta" onClick={() => onRoll(10)} disabled={rolling}>
          10연속<span className="hl-mono hl-costTag">{cost10}P</span>
        </button>
      </div>
      <p className="hl-fine hl-left">
        10연속은 한 번 값이 공짜예요. 희귀 이상이 하나는 반드시 나오고, 이미 가진 소품이 뜨면 {DUP_REFUND}P씩 돌려받아요.
      </p>

      <div className="hl-sectionHead"><span>집 외형 뽑기</span><span className="hl-mono hl-dim">{room.houses?.length || 1} / {HOUSES.length}</span></div>
      <div className="hl-dayBtns">
        <button className="hl-cta done" onClick={() => onRollHouse(1)} disabled={rolling}>
          외형 1회<span className="hl-mono hl-costTag">{HOUSE_COST}P</span>
        </button>
        <button className="hl-cta done" onClick={() => onRollHouse(3)} disabled={rolling}>
          외형 3회<span className="hl-mono hl-costTag">{HOUSE_COST * 3}P</span>
        </button>
      </div>
      <div className="hl-rates" style={{ marginTop: 8 }}>
        {Object.entries(HOUSE_RATE).map(([k, v]) => (
          <div key={k} className="hl-rate">
            <span className="hl-tierDot" style={{ background: RARITY[k].c }} />
            <span className="hl-rateName">{RARITY[k].n}</span>
            <span className="hl-mono hl-dim">{v}%</span>
          </div>
        ))}
      </div>
      <p className="hl-fine hl-left">중복이면 40P를 돌려받아요.</p>

      <div className="hl-sectionHead"><span>소품 등장 확률</span></div>
      <div className="hl-rates">
        {Object.entries(RARITY).map(([k, r]) => (
          <div key={k} className="hl-rate">
            <span className="hl-tierDot" style={{ background: r.c }} />
            <span className="hl-rateName">{r.n}</span>
            <span className="hl-mono hl-dim">{r.rate}%</span>
          </div>
        ))}
      </div>

      <div className="hl-sectionHead"><span>도감</span></div>
      <div className="hl-invGrid">
        {ITEMS.map((it) => {
          const has = (room.inv?.[it.id] || 0) > 0;
          return (
            <div key={it.id} className={`hl-invCell ${has ? "" : "locked"}`} style={has ? { borderColor: RARITY[it.tier].c } : {}}>
              <div className="hl-invArt">{has ? <ItemArt id={it.id} /> : <span className="hl-lockMark">?</span>}</div>
              <div className="hl-invName">{has ? it.n : "미발견"}</div>
              <div className="hl-mono hl-invMeta" style={has ? { color: RARITY[it.tier].c } : {}}>
                {RARITY[it.tier].n}{has ? ` ×${room.inv[it.id]}` : ""}
              </div>
            </div>
          );
        })}
      </div>
    </>
  );
}

function Reveal({ result, onClose }) {
  const rolls = result.rolls || [];
  const best = TIER_ORDER.find((t) => rolls.some((r) => r.item.tier === t)) || "common";
  const [done, setDone] = useState(false);

  useEffect(() => {
    if (result.error) return;
    A.sfx("curtain");
    const t1 = setTimeout(() => A.sfx(`tier_${best}`), 900);
    const t2 = setTimeout(() => setDone(true), 1500 + rolls.length * 130);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, []);

  if (result.error) return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-reveal" onClick={(e) => e.stopPropagation()}>
        <div className="hl-revealName">{result.error}</div>
        <p className="hl-sheetNote">활동일지를 올리거나 출석하면 포인트가 쌓여요.</p>
        <button className="hl-cta" onClick={onClose}>확인</button>
      </div>
    </div>
  );

  const tint = RARITY[best].c;
  return (
    <div className={`hl-show t-${best}`} style={{ "--tint": tint }} onClick={() => done && onClose()}>
      <div className="sh-back" />
      <div className="sh-beamWrap">
        <span className="sh-beam" /><span className="sh-beam b2" /><span className="sh-beam b3" />
      </div>

      <div className="sh-stage">
        <div className="sh-floor" />
        <div className={`sh-row n${rolls.length}`}>
          {rolls.map((r, i) => (
            <div key={i} className={`sh-item t-${r.item.tier}`}
              style={{ animationDelay: `${1000 + i * 130}ms`, "--c": RARITY[r.item.tier].c }}>
              <span className="sh-halo" />
              {r.kind === "house" ? <HouseArt id={r.item.id} /> : <ItemArt id={r.item.id} />}
              <span className="sh-name">{r.item.n}</span>
              <span className="sh-tier">{RARITY[r.item.tier].n}{r.dup ? " · 중복" : ""}</span>
            </div>
          ))}
        </div>
      </div>

      <div className="sh-curtain l" /><div className="sh-curtain r" />
      <div className="sh-valance" />

      <div className={`sh-foot ${done ? "on" : ""}`}>
        {result.refund > 0 && <p className="sh-refund">중복으로 {result.refund}P를 돌려받았어요.</p>}
        <button className="hl-cta" onClick={onClose}>확인</button>
      </div>
    </div>
  );
}

/* ── 아이소메트릭 기하 ──────────────────────────
   ex = (1, .5) 오른쪽 아래 / ey = (-1, .5) 왼쪽 아래 / ez = (0, -1) 위
   빛은 왼쪽 위 앞에서 온다: 윗면 > 오른면 > 왼면 */

/* 재질: t 윗면 · r 오른면 · l 왼면 · hi 모서리 하이라이트 */
const MAT = {
  wood:   { t: "#BE9264", r: "#916D47", l: "#634A31", hi: "#DBB183" },
  woodD:  { t: "#7E6248", r: "#5D4733", l: "#3D2E21", hi: "#9A7A5C" },
  woodR:  { t: "#8E5A3C", r: "#6B412A", l: "#48291A", hi: "#AF7752" },
  metal:  { t: "#B2BAC2", r: "#848E97", l: "#5B646D", hi: "#DCE2E7" },
  dark:   { t: "#454D56", r: "#2F363D", l: "#1E242A", hi: "#616A74" },
  brass:  { t: "#EAC47E", r: "#C09A4E", l: "#8B6F35", hi: "#F8E3AC" },
  cream:  { t: "#F2EADA", r: "#D8CDB8", l: "#B3A78E", hi: "#FFFAF0" },
  velvet: { t: "#A8434A", r: "#83292F", l: "#5C1B20", hi: "#C55C62" },
  clay:   { t: "#CC8155", r: "#A5613C", l: "#77452A", hi: "#E5A075" },
  ivory:  { t: "#EFEAE0", r: "#D5CFC2", l: "#ADA79A", hi: "#FFFFFF" },
};

const bx = (a, b, c, M, dx = 0, dy = 0, k) => (
  <g key={k} transform={`translate(${dx},${dy})`}>
    <polygon points={`0,${-c} ${a},${a / 2 - c} ${a - b},${(a + b) / 2 - c} ${-b},${b / 2 - c}`} fill={M.t} />
    <polygon points={`0,0 ${a},${a / 2} ${a},${a / 2 - c} 0,${-c}`} fill={M.r} />
    <polygon points={`0,0 ${-b},${b / 2} ${-b},${b / 2 - c} 0,${-c}`} fill={M.l} />
    <polyline points={`${-b},${b / 2 - c} 0,${-c} ${a},${a / 2 - c}`} fill="none" stroke={M.hi} strokeWidth=".9" opacity=".9" />
    <line x1="0" y1={-c} x2="0" y2="0" stroke={M.hi} strokeWidth=".7" opacity=".3" />
  </g>
);

/* 위가 좁아지는 통 (화분·전등갓) */
const taper = (aB, aT, c, M, dx = 0, dy = 0, k) => {
  const o = (aB - aT) / 2;
  return (
    <g key={k} transform={`translate(${dx},${dy})`}>
      <polygon points={`0,0 ${aB},${aB / 2} 0,${aB} ${-aB},${aB / 2}`} fill="none" />
      <polygon points={`${-aB},${aB / 2} 0,${aB} ${-aT},${aT / 2 - c + aB - aT} 0,${-c + aB}`} fill={M.l} />
      <polygon points={`0,${aB} ${aB},${aB / 2} ${aT},${aT / 2 - c + aB - aT} 0,${-c + aB}`} fill={M.r} />
      <ellipse cx="0" cy={aB - c - aT / 2 + aT / 2} rx={aT} ry={aT / 2} fill={M.t} />
    </g>
  );
};

/* 바닥 마름모 */
const pad = (a, b, f, dx = 0, dy = 0, o = 1) => (
  <polygon transform={`translate(${dx},${dy})`} opacity={o}
    points={`0,0 ${a},${a / 2} ${a - b},${(a + b) / 2} ${-b},${b / 2}`} fill={f} />
);
/* 접지 그림자 */
const sdw = (r, dx = 0, dy = 2) => (
  <ellipse cx={dx} cy={dy} rx={r} ry={r / 2.4} fill="#000" opacity=".26" />
);
/* 아이소메트릭 원판 */
const disc = (r, f, dx = 0, dy = 0, k) => <ellipse key={k} cx={dx} cy={dy} rx={r} ry={r / 2} fill={f} />;

/* ── 집 외형 ── */
const DEFAULT_HOUSES = [
  { id: "tent",     n: "낡은 천막",       tier: "common", d: "무대 뒤에 쳐둔 첫 천막" },
  { id: "stump",    n: "그루터기 집",     tier: "common", d: "이끼 덮인 나무 밑동" },
  { id: "mushroom", n: "버섯 집",         tier: "rare",   d: "붉은 갓 아래 작은 창" },
  { id: "pumpkin",  n: "호박 집",         tier: "rare",   d: "속에서 등불이 새어나온다" },
  { id: "rose",     n: "장미 집",         tier: "epic",   d: "만개한 장미가 지붕이 된다" },
  { id: "pear",     n: "초콜릿 배 상점",  tier: "epic",   d: "초콜릿이 흘러내린 지붕" },
  { id: "lily",     n: "은방울꽃 등불집", tier: "legend", d: "줄기에 매달린 꽃등" },
  { id: "theater",  n: "작은 극장",       tier: "legend", d: "붉은 막이 걸린 나만의 극장" },
];
let HOUSES = DEFAULT_HOUSES;
const houseOf = (id) => HOUSES.find((h) => h.id === id) || DEFAULT_HOUSES[0];
const HOUSE_COST = 80;

/* 집 밖 하늘 — 위·아래 두 색으로 그라데이션 */
const SKIES = [
  { id: "night",  n: "밤",     a: "#1B2432", b: "#2E3A4C" },
  { id: "dawn",   n: "새벽",   a: "#3B4A78", b: "#C98A9E" },
  { id: "day",    n: "한낮",   a: "#87C3E8", b: "#DCEEF8" },
  { id: "sunset", n: "노을",   a: "#F2955E", b: "#8E5A8C" },
  { id: "forest", n: "숲",     a: "#33513C", b: "#7FA875" },
  { id: "lilac",  n: "라일락", a: "#8E7BC4", b: "#E3D5F2" },
  { id: "cream",  n: "크림",   a: "#E8DCC2", b: "#FAF4E6" },
  { id: "ink",    n: "먹",     a: "#101418", b: "#232B34" },
];
const skyOf = (id) => SKIES.find((x) => x.id === id) || SKIES[0];

/* 방 안 벽지 — 왼벽·오른벽·바닥 */
const WALLS = [
  { id: "default", n: "기본",     wl: null, wr: null, fl: null },
  { id: "warm",    n: "따뜻한 나무", wl: "#7A6048", wr: "#634D39", fl: "#8A6A47" },
  { id: "paper",   n: "한지",     wl: "#E4DBC8", wr: "#D3C9B4", fl: "#C2B49A" },
  { id: "mint",    n: "민트",     wl: "#9CC4B8", wr: "#84AFA3", fl: "#B7D2C8" },
  { id: "rose",    n: "로즈",     wl: "#D8A8B0", wr: "#C1929B", fl: "#E6C7CC" },
  { id: "navy",    n: "네이비",   wl: "#33436B", wr: "#293656", fl: "#3E4E72" },
  { id: "olive",   n: "올리브",   wl: "#7E8659", wr: "#67704A", fl: "#98A06E" },
  { id: "mono",    n: "모노",     wl: "#4A4F54", wr: "#3A3E43", fl: "#565C62" },
  { id: "black",   n: "블랙박스", wl: "#1C2026", wr: "#14181D", fl: "#252A31" },
];
const wallOf = (id) => WALLS.find((x) => x.id === id) || WALLS[0];
const HOUSE_RATE = { common: 55, rare: 30, epic: 14, legend: 1 };

/* 문 위치 (클릭 영역) — [x, y, w, h] */
const DOORS = {
  tent: [96, 148, 48, 88], stump: [98, 156, 44, 80], mushroom: [100, 160, 40, 76],
  pumpkin: [100, 156, 40, 80], rose: [100, 158, 40, 78], pear: [100, 150, 40, 86],
  lily: [102, 128, 36, 62], theater: [96, 146, 48, 90],
};

function houseShape(id) {
  const G = (n) => `hg_${id}_${n}`;
  switch (id) {
    case "tent": return <>
      <defs>
        <linearGradient id={G(1)} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#E8DCC2" /><stop offset=".5" stopColor="#D3C3A2" /><stop offset="1" stopColor="#AC9B7C" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="236" rx="86" ry="14" fill="#000" opacity=".22" />
      <path d="M120,34 L206,232 L34,232 Z" fill={`url(#${G(1)})`} />
      <path d="M120,34 L206,232 L150,232 Z" fill="#000" opacity=".12" />
      <path d="M120,34 L60,232 L34,232 Z" fill="#FFF" opacity=".14" />
      {[62, 84, 106].map((y, i) => (
        <path key={i} d={`M${120 - (y - 34) * .43},${y} L${120 + (y - 34) * .43},${y}`} stroke="#8A7A5C" strokeWidth="1.4" opacity=".35" />
      ))}
      <path d="M120,88 L156,232 L84,232 Z" fill="#6E5D45" />
      <path d="M120,88 L152,232 L120,232 Z" fill="#5A4B36" />
      <path d="M120,88 L96,168 L120,196 L144,168 Z" fill="#C9B896" />
      <path d="M120,34 l-6,-16 l12,0 Z" fill="#8E2F35" />
      <rect x="116" y="12" width="3" height="24" fill="#7C6A50" />
      <path d="M34,232 l-16,6 M206,232 l16,6" stroke="#7C6A50" strokeWidth="3" strokeLinecap="round" />
      <ellipse cx="176" cy="196" rx="13" ry="15" fill="#E9A13B" opacity=".22" />
      <path d="M176,182 v-8" stroke="#7C6A50" strokeWidth="2" />
      <rect x="169" y="186" width="14" height="16" rx="3" fill="#C09A4E" />
      <rect x="172" y="189" width="8" height="10" fill="#FFE9A8" />
      <ellipse cx="52" cy="230" rx="18" ry="5" fill="#4F6B52" opacity=".7" />
    </>;

    case "stump": return <>
      <defs>
        <linearGradient id={G(1)} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#9A7550" /><stop offset=".45" stopColor="#7C5C3C" /><stop offset="1" stopColor="#513A25" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="234" rx="82" ry="14" fill="#000" opacity=".22" />
      <path d="M52,232 C48,150 58,96 120,92 C182,96 192,150 188,232 Z" fill={`url(#${G(1)})`} />
      {[70, 96, 152, 172].map((x, i) => (
        <path key={i} d={`M${x},${110 + i * 6} C${x - 4},${160} ${x - 2},${196} ${x + 2},${230}`} stroke="#5E442C" strokeWidth="2.4" fill="none" opacity=".6" />
      ))}
      <ellipse cx="120" cy="92" rx="68" ry="22" fill="#A8814F" />
      <ellipse cx="120" cy="88" rx="68" ry="22" fill="#C29761" />
      {[52, 38, 24, 12].map((r, i) => <ellipse key={i} cx="120" cy="88" rx={r} ry={r / 3.1} fill="none" stroke="#9A7549" strokeWidth="1.6" opacity=".7" />)}
      <path d="M56,84 C74,62 108,58 128,68 C102,66 74,72 58,90 Z" fill="#4F6B52" />
      <path d="M170,80 C186,66 196,74 190,90 C182,80 176,78 170,84 Z" fill="#5E7D5F" />
      <path d="M98,236 v-52 a22,22 0 0 1 44,0 v52 Z" fill="#3E2C1C" />
      <path d="M101,236 v-50 a19,19 0 0 1 38,0 v50 Z" fill="#7A5636" />
      {[108, 116, 124, 132].map((x, i) => <line key={i} x1={x} y1="170" x2={x} y2="236" stroke="#5E4028" strokeWidth="1.2" opacity=".7" />)}
      <circle cx="133" cy="204" r="3" fill="#C09A4E" />
      <ellipse cx="164" cy="152" rx="15" ry="13" fill="#3E2C1C" />
      <ellipse cx="164" cy="152" rx="12" ry="10" fill="#FFE1A2" />
      <path d="M152,152 h24 M164,142 v20" stroke="#3E2C1C" strokeWidth="2" />
      <ellipse cx="70" cy="230" rx="20" ry="6" fill="#4F6B52" />
      {[[62, 224, 7], [80, 228, 5]].map(([x, y, r], i) => <g key={i}>
        <ellipse cx={x} cy={y} rx={r} ry={r * .55} fill="#C4514F" /><rect x={x - 1.4} y={y} width="2.8" height={r} fill="#EDE3CE" /></g>)}
    </>;

    case "mushroom": return <>
      <defs>
        <radialGradient id={G(1)} cx=".38" cy=".3">
          <stop offset="0" stopColor="#E8615C" /><stop offset=".6" stopColor="#C4353D" /><stop offset="1" stopColor="#8E2229" />
        </radialGradient>
        <linearGradient id={G(2)} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#F4EBD8" /><stop offset=".5" stopColor="#E0D3B8" /><stop offset="1" stopColor="#B7A78A" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="234" rx="74" ry="13" fill="#000" opacity=".22" />
      <path d="M72,236 C68,180 74,140 120,138 C166,140 172,180 168,236 Z" fill={`url(#${G(2)})`} />
      <path d="M28,132 C34,66 78,28 120,28 C162,28 206,66 212,132 C190,146 150,152 120,152 C90,152 50,146 28,132 Z" fill={`url(#${G(1)})`} />
      <path d="M28,132 C34,66 78,28 120,28 C104,44 92,84 90,148 C64,146 42,140 28,132 Z" fill="#FFF" opacity=".1" />
      {[[62, 96, 13], [104, 68, 10], [152, 84, 12], [180, 118, 9], [126, 112, 8], [86, 126, 7]].map(([x, y, r], i) =>
        <ellipse key={i} cx={x} cy={y} rx={r} ry={r * .82} fill="#F6EFE0" opacity=".95" />)}
      <path d="M28,132 C60,146 92,152 120,152 C148,152 180,146 212,132 C206,140 196,146 186,150 C160,158 80,158 54,150 C44,146 34,140 28,132 Z" fill="#7E1D24" />
      <path d="M100,236 v-46 a20,20 0 0 1 40,0 v46 Z" fill="#4A3423" />
      <path d="M103,236 v-44 a17,17 0 0 1 34,0 v44 Z" fill="#8A6540" />
      {[110, 120, 130].map((x, i) => <line key={i} x1={x} y1="176" x2={x} y2="236" stroke="#6B4E31" strokeWidth="1.2" opacity=".7" />)}
      <circle cx="132" cy="212" r="2.6" fill="#E7C079" />
      <ellipse cx="80" cy="182" rx="13" ry="12" fill="#4A3423" />
      <ellipse cx="80" cy="182" rx="10" ry="9" fill="#FFE1A2" />
      <ellipse cx="160" cy="182" rx="13" ry="12" fill="#4A3423" />
      <ellipse cx="160" cy="182" rx="10" ry="9" fill="#FFE1A2" />
      <ellipse cx="62" cy="232" rx="18" ry="5" fill="#4F6B52" opacity=".8" />
      <ellipse cx="182" cy="232" rx="16" ry="5" fill="#4F6B52" opacity=".8" />
    </>;

    case "pumpkin": return <>
      <defs>
        <linearGradient id={G(2)} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#F5AE4E" /><stop offset=".42" stopColor="#DE8524" /><stop offset="1" stopColor="#9E5514" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="234" rx="88" ry="14" fill="#000" opacity=".22" />
      <ellipse cx="120" cy="152" rx="94" ry="84" fill={`url(#${G(2)})`} />
      {[-62, -32, 0, 32, 62].map((o, i) => (
        <path key={i} d={`M${120 + o},70 C${120 + o * 1.5},110 ${120 + o * 1.5},194 ${120 + o},234`}
          stroke="#9E5514" strokeWidth="2.6" fill="none" opacity=".45" />
      ))}
      <ellipse cx="86" cy="112" rx="26" ry="40" fill="#FFF" opacity=".12" />
      <path d="M120,72 C116,50 112,36 122,24 C132,32 128,50 128,72 Z" fill="#5E7D5F" />
      <path d="M126,30 C142,20 152,30 146,44 C142,32 134,30 126,38 Z" fill="#6C9166" />
      <path d="M100,236 v-52 a20,20 0 0 1 40,0 v52 Z" fill="#5A3410" />
      <path d="M103,236 v-50 a17,17 0 0 1 34,0 v50 Z" fill="#9A6A2E" />
      <path d="M103,190 a17,17 0 0 1 34,0 z" fill="#FFD98A" opacity=".55" />
      <circle cx="132" cy="208" r="2.8" fill="#E7C079" />
      <path d="M62,166 l22,-14 l22,14 l-22,14 Z" fill="#5A3410" />
      <path d="M66,166 l18,-11 l18,11 l-18,11 Z" fill="#FFE1A2" />
      <path d="M176,166 l-22,-14 l-22,14 l22,14 Z" fill="#5A3410" />
      <path d="M172,166 l-18,-11 l-18,11 l18,11 Z" fill="#FFE1A2" />
      <ellipse cx="56" cy="230" rx="20" ry="6" fill="#4F6B52" opacity=".8" />
    </>;

    case "rose": return <>
      <defs>
        <radialGradient id={G(1)} cx=".4" cy=".34">
          <stop offset="0" stopColor="#E8536A" /><stop offset=".55" stopColor="#C22440" /><stop offset="1" stopColor="#7E1129" />
        </radialGradient>
        <linearGradient id={G(2)} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#9A7550" /><stop offset=".45" stopColor="#78573A" /><stop offset="1" stopColor="#4E3623" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="234" rx="78" ry="13" fill="#000" opacity=".22" />
      <path d="M64,236 C60,190 68,150 120,146 C172,150 180,190 176,236 Z" fill={`url(#${G(2)})`} />
      {[84, 104, 140, 158].map((x, i) => <path key={i} d={`M${x},156 C${x - 3},188 ${x - 1},214 ${x + 2},234`} stroke="#523A25" strokeWidth="2" fill="none" opacity=".55" />)}
      <path d="M26,120 C22,72 64,34 120,34 C176,34 218,72 214,120 C206,146 168,158 120,158 C72,158 34,146 26,120 Z" fill={`url(#${G(1)})`} />
      <path d="M40,112 C44,74 76,48 120,48 C164,48 196,74 200,112 C186,132 158,142 120,142 C82,142 54,132 40,112 Z" fill="#D8355A" opacity=".55" />
      <path d="M62,108 C66,82 90,64 120,64 C150,64 174,82 178,108 C166,124 146,132 120,132 C94,132 74,124 62,108 Z" fill="#B01F3C" opacity=".6" />
      <path d="M86,102 C90,86 104,76 120,76 C136,76 150,86 154,102 C146,114 134,118 120,118 C106,118 94,114 86,102 Z" fill="#E8536A" opacity=".7" />
      <path d="M104,98 C106,88 112,84 120,84 C128,84 134,88 136,98 C130,106 110,106 104,98 Z" fill="#8E1630" />
      <path d="M26,120 C40,140 70,150 120,152 C170,150 200,140 214,120 C208,142 170,160 120,160 C70,160 32,142 26,120 Z" fill="#7E1129" opacity=".8" />
      <path d="M40,158 C20,148 6,158 14,176 C24,164 32,162 42,166 Z" fill="#4F6B52" />
      <path d="M200,150 C222,142 234,154 226,170 C216,158 208,156 198,158 Z" fill="#5E7D5F" />
      <path d="M100,236 v-52 a20,20 0 0 1 40,0 v52 Z" fill="#3E2C1C" />
      <path d="M103,236 v-50 a17,17 0 0 1 34,0 v50 Z" fill="#7A4634" />
      {[110, 120, 130].map((x, i) => <line key={i} x1={x} y1="170" x2={x} y2="236" stroke="#5C3226" strokeWidth="1.2" opacity=".7" />)}
      <circle cx="132" cy="206" r="2.8" fill="#E7C079" />
      <rect x="150" y="176" width="24" height="24" rx="3" fill="#3E2C1C" />
      <rect x="153" y="179" width="18" height="18" fill="#FFD9A8" />
      <path d="M162,179 v18 M153,188 h18" stroke="#3E2C1C" strokeWidth="2" />
    </>;

    case "pear": return <>
      <defs>
        <radialGradient id={G(1)} cx=".36" cy=".38">
          <stop offset="0" stopColor="#CFDB7C" /><stop offset=".55" stopColor="#A3B84E" /><stop offset="1" stopColor="#6B7C2C" />
        </radialGradient>
        <linearGradient id={G(2)} x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stopColor="#6B4326" /><stop offset=".5" stopColor="#4A2A16" /><stop offset="1" stopColor="#2E190C" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="234" rx="84" ry="14" fill="#000" opacity=".22" />
      <path d="M120,44 C150,44 164,72 158,102 C176,124 190,158 188,188 C186,220 158,236 120,236 C82,236 54,220 52,188 C50,158 64,124 82,102 C76,72 90,44 120,44 Z" fill={`url(#${G(1)})`} />
      <path d="M92,110 C80,134 70,164 72,192 C74,214 92,228 116,232 C84,230 62,214 60,190 C58,160 74,128 92,110 Z" fill="#FFF" opacity=".16" />
      <path d="M120,40 C152,40 166,66 160,96 C150,110 142,88 148,72 C138,84 128,78 120,60 C112,80 100,86 92,74 C96,92 90,110 80,96 C74,66 88,40 120,40 Z" fill={`url(#${G(2)})`} />
      <path d="M120,40 C104,54 112,84 96,84 C88,74 92,56 100,46 Z" fill="#7C5030" opacity=".55" />
      <path d="M158,96 C162,116 156,132 148,124 C154,116 156,106 154,96 Z" fill={`url(#${G(2)})`} />
      <rect x="124" y="12" width="8" height="34" rx="4" fill="#6B4326" transform="rotate(9 128 30)" />
      <path d="M132,24 C158,10 186,18 190,36 C170,28 148,30 134,42 Z" fill="#4F6B52" />
      <path d="M136,28 C158,20 176,24 184,34" stroke="#3C5540" strokeWidth="2" fill="none" />
      <path d="M100,236 v-56 a20,26 0 0 1 40,0 v56 Z" fill="#E8DFC8" />
      <path d="M104,236 v-54 a16,22 0 0 1 32,0 v54 Z" fill="#8FA84A" />
      {[112, 120, 128].map((x, i) => <line key={i} x1={x} y1="166" x2={x} y2="236" stroke="#71893A" strokeWidth="1.4" opacity=".8" />)}
      <path d="M120,168 c-8,8 -12,16 -6,22 4,4 8,2 6,-4 4,6 10,4 10,-4 0,-6 -4,-10 -10,-14 Z" fill="#E8DFC8" />
      <g transform="rotate(-6 150 130)">
        <rect x="126" y="118" width="76" height="26" rx="3" fill="#6B4326" />
        <rect x="129" y="121" width="70" height="20" rx="2" fill="#8A5C36" />
        <rect x="136" y="128" width="56" height="3" rx="1.5" fill="#EFE6D2" />
        <rect x="136" y="134" width="38" height="3" rx="1.5" fill="#EFE6D2" opacity=".8" />
      </g>
      <path d="M60,166 l18,-12 l18,12 l-18,12 Z" fill="#E8DFC8" />
      <path d="M64,166 l14,-9 l14,9 l-14,9 Z" fill="#6B7C2C" />
      {[[62, 226], [80, 232], [176, 228]].map(([x, y], i) => <rect key={i} x={x} y={y} width="10" height="7" rx="1.5" fill="#4A2A16" transform={`rotate(${i * 18 - 10} ${x + 5} ${y + 3})`} />)}
    </>;

    case "lily": return <>
      <defs>
        <linearGradient id={G(1)} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#FFFDF6" /><stop offset=".55" stopColor="#F0E8DA" /><stop offset="1" stopColor="#D6C6B4" />
        </linearGradient>
        <radialGradient id={G(2)} cx=".5" cy=".2">
          <stop offset="0" stopColor="#CFEAD8" /><stop offset="1" stopColor="#8FBBA4" />
        </radialGradient>
      </defs>
      <path d="M188,0 C186,26 172,40 150,44 C158,26 166,14 176,2 Z" fill="#5E9166" />
      <path d="M150,44 C132,34 110,30 92,34" stroke="#6C9F72" strokeWidth="7" fill="none" strokeLinecap="round" />
      <path d="M120,38 C124,20 136,10 152,8" stroke="#7CB183" strokeWidth="4" fill="none" />
      <path d="M28,86 C24,54 62,32 120,32 C178,32 216,54 212,86 C206,110 172,124 120,124 C68,124 34,110 28,86 Z" fill={`url(#${G(1)})`} />
      {[-72, -40, 0, 40, 72].map((o, i) => (
        <path key={i} d={`M${120 + o},40 C${120 + o * 1.16},72 ${120 + o * 1.2},104 ${120 + o},124`} stroke="#DCCFC0" strokeWidth="2" fill="none" opacity=".8" />
      ))}
      <path d="M28,86 C58,108 88,120 120,124 C88,126 44,114 28,86 Z" fill="#FFF" opacity=".5" />
      <path d="M28,86 C36,116 66,132 120,134 C174,132 204,116 212,86 C214,120 176,148 120,148 C64,148 26,120 28,86 Z" fill="#F6C9CE" />
      <path d="M42,116 C60,138 88,148 120,150 C88,152 54,140 42,116 Z" fill="#FFF" opacity=".35" />
      {[[62, 46], [86, 34], [116, 28], [148, 34], [176, 46]].map(([x, y], i) => (
        <g key={i}>
          <circle cx={x} cy={y} r="12" fill="#B9A6E0" />
          <circle cx={x - 4} cy={y - 4} r="6" fill="#D6C9F0" />
          <circle cx={x} cy={y} r="3.4" fill="#F5EFC4" />
        </g>
      ))}
      <path d="M40,52 C72,26 108,20 140,24" stroke="#E7D9A8" strokeWidth="2" fill="none" opacity=".9" />
      <path d="M62,146 C74,166 166,166 178,146 C186,158 176,172 120,172 C64,172 54,158 62,146 Z" fill={`url(#${G(2)})`} />
      <path d="M62,152 h116" stroke="#E7C079" strokeWidth="2.5" opacity=".9" />
      {[74, 96, 120, 144, 166].map((x, i) => (
        <g key={i}>
          <line x1={x} y1="170" x2={x} y2={196 + (i % 2) * 16} stroke="#D8C58A" strokeWidth="1.4" />
          <circle cx={x} cy={202 + (i % 2) * 16} r="8" fill="#F5E7B0" opacity=".9" />
          <circle cx={x} cy={202 + (i % 2) * 16} r="12" fill="#FFF0BC" opacity=".28" />
          <circle cx={x - 2.5} cy={199 + (i % 2) * 16} r="2.6" fill="#FFFDF0" />
        </g>
      ))}
      <path d="M102,190 v-52 a18,26 0 0 1 36,0 v52 Z" fill="#8FBBA4" opacity=".35" />
      <path d="M104,190 v-50 a16,24 0 0 1 32,0 v50 Z" fill="#DCEFE4" opacity=".75" />
      <path d="M120,116 v74" stroke="#B7D6C6" strokeWidth="1.6" />
      <path d="M150,64 C168,58 182,66 180,80 C172,70 160,68 150,72 Z" fill="#C7B3EA" opacity=".9" />
    </>;

    case "theater": return <>
      <defs>
        <linearGradient id={G(1)} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#3A4450" /><stop offset=".45" stopColor="#2C3540" /><stop offset="1" stopColor="#1C232B" />
        </linearGradient>
        <linearGradient id={G(2)} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#B84048" /><stop offset="1" stopColor="#6E1F26" />
        </linearGradient>
      </defs>
      <ellipse cx="120" cy="236" rx="88" ry="14" fill="#000" opacity=".24" />
      <path d="M44,236 V78 H196 V236 Z" fill={`url(#${G(1)})`} />
      <path d="M120,236 V78 H196 V236 Z" fill="#000" opacity=".16" />
      <path d="M28,80 L120,26 L212,80 Z" fill="#404B58" />
      <path d="M120,26 L212,80 L120,80 Z" fill="#2E3843" />
      <path d="M28,80 h184 v10 h-184 Z" fill="#C09A4E" />
      <path d="M28,80 h184 v3 h-184 Z" fill="#EAC47E" />
      <circle cx="120" cy="58" r="13" fill="#1C232B" />
      <circle cx="120" cy="58" r="10" fill="#F5E7B0" />
      <path d="M114,56 h12 M120,50 v12" stroke="#1C232B" strokeWidth="1.6" />
      {[38, 60, 82, 104, 126, 148, 170, 192, 202].map((x, i) => (
        <circle key={i} cx={x} cy="96" r="3.4" fill={i % 2 ? "#FFE9A8" : "#F5D98A"} />
      ))}
      {[38, 60, 82, 104, 126, 148, 170, 192, 202].map((x, i) => (
        <circle key={`g${i}`} cx={x} cy="96" r="7" fill="#FFE9A8" opacity=".22" />
      ))}
      <rect x="52" y="104" width="136" height="16" rx="3" fill="#8B6F35" />
      <rect x="55" y="107" width="130" height="10" rx="2" fill="#241C15" />
      <rect x="66" y="110" width="108" height="4" rx="2" fill="#E7C079" opacity=".85" />
      <rect x="60" y="130" width="120" height="106" rx="4" fill="#141A20" />
      <path d="M60,130 h120 v14 h-120 Z" fill={`url(#${G(2)})`} />
      <path d="M64,144 h44 c3,30 0,58 -4,92 h-40 Z" fill={`url(#${G(2)})`} />
      <path d="M176,144 h-44 c-3,30 0,58 4,92 h40 Z" fill="#8E2F35" />
      <path d="M64,144 h14 c2,30 0,60 -2,92 h-12 Z" fill="#C24C54" opacity=".55" />
      <path d="M176,144 h-14 c-2,30 0,60 2,92 h12 Z" fill="#A03A41" opacity=".5" />
      <path d="M108,144 h24 c1,60 1,60 0,92 h-24 Z" fill="#FFE6B8" opacity=".22" />
      <circle cx="86" cy="196" r="3" fill="#EAC47E" /><circle cx="154" cy="196" r="3" fill="#EAC47E" />
      <rect x="40" y="230" width="160" height="8" rx="2" fill="#8B6F35" />
      <rect x="40" y="230" width="160" height="3" rx="1.5" fill="#C09A4E" />
      <rect x="18" y="88" width="10" height="148" fill="#2C3540" />
      <rect x="212" y="88" width="10" height="148" fill="#232B34" />
    </>;

    default: return null;
  }
}

function HouseArt({ id, className = "" }) {
  return (
    <Art kind="house" id={id} className={className}>
      <svg viewBox="0 0 240 250" xmlns="http://www.w3.org/2000/svg" className={className}>{houseShape(id)}</svg>
    </Art>
  );
}

function Backdrop({ kind }) {
  if (kind === "books") return (
    <g>
      <g transform="matrix(1,-0.381,0,1,56,178)">
        <rect x="0" y="-108" width="132" height="108" fill="#4B3A29" />
        {[0, 1, 2].map((r) => (
          <g key={r}>
            <rect x="4" y={-102 + r * 35} width="124" height="4" fill="#6B5138" />
            {[...Array(9)].map((_, k) => (
              <rect key={k} x={8 + k * 13} y={-98 + r * 35 + (k % 3) * 2} width="9" height={23 - (k % 3) * 2}
                fill={["#8E2F35", "#4F6B52", "#C09A4E", "#3B5878", "#7A4B2E"][(k + r) % 5]} />
            ))}
          </g>
        ))}
      </g>
      <path d="M214,26 q-14,30 -6,58 q10,-26 8,-58" fill="#4F6B52" opacity=".85" />
      <path d="M200,46 q-12,24 -4,46 q10,-22 6,-46" fill="#63855F" opacity=".7" />
    </g>
  );
  if (kind === "glass") return (
    <g transform="matrix(0.78,0.297,0,0.78,288,22)">
      <path d="M4,120 L4,44 Q56,-14 108,44 L108,120 Z" fill="#2A3038" />
      {[["#C63C4E", 12], ["#E9A13B", 34], ["#4FA8C9", 56], ["#7BB86A", 78]].map(([c, x], k) => (
        <path key={k} d={`M${x + 4},116 L${x + 4},50 Q${x + 14},34 ${x + 24},50 L${x + 24},116 Z`} fill={c} opacity=".9" />
      ))}
      <circle cx="56" cy="34" r="13" fill="#E4A6C8" />
      <circle cx="56" cy="34" r="6" fill="#F2D9E6" />
      <path d="M4,120 L4,44 Q56,-14 108,44 L108,120" fill="none" stroke="#8C939C" strokeWidth="4" />
    </g>
  );
  if (kind === "gothic") return (
    <g transform="matrix(0.78,0.297,0,0.78,292,20)">
      <path d="M6,116 L6,46 Q52,-10 98,46 L98,116 Z" fill="#101C30" />
      <path d="M12,112 L12,48 Q52,-2 92,48 L92,112 Z" fill="#B9D6EE" opacity=".82" />
      <circle cx="52" cy="30" r="15" fill="#EAF4FF" />
      <g stroke="#2A3B55" strokeWidth="3" fill="none">
        <path d="M52,4 L52,112 M12,66 L92,66 M32,20 L32,112 M72,20 L72,112" />
      </g>
      <path d="M6,116 L6,46 Q52,-10 98,46 L98,116" fill="none" stroke="#4A5E7E" strokeWidth="5" />
    </g>
  );
  return null;
}

/* 얼굴 (목록·피드) — 올린 그림이 있으면 그 그림 */
function CharFace({ id, ch }) {
  return (
    <Art kind="char" id={id} className="hl-faceArt">
      <Character ch={ch} viewBox="18 8 84 82" className="hl-faceArt" />
    </Art>
  );
}

/* 방 안(SVG) 캐릭터 */
function CharIso({ id, ch }) {
  const key = artKey("char", id);
  const [src, setSrc] = useState(ART.img.get(key) || null);
  useEffect(() => {
    if (src || !ART.idx.has(key)) return;
    let live = true;
    (async () => {
      try { const r = await window.storage.get(`hanul:art:${key}`, true); ART.img.set(key, r.value); if (live) setSrc(r.value); }
      catch (e) {}
    })();
    return () => { live = false; };
  }, [key, src]);
  if (!ART.idx.has(key)) return (
    <svg x="-42" y="-126" width="84" height="126" viewBox="0 0 120 180" style={{ overflow: "visible" }}>
      <Character ch={ch} />
    </svg>
  );
  if (!src) return null;
  return <image href={src} x="-48" y="-134" width="96" height="134" preserveAspectRatio="xMidYMax meet" />;
}

function Diorama({ room, charId, showSlots, onSlot }) {
  const base = setOf(room.set);
  const W = wallOf(room.wall);
  /* 벽지를 골랐으면 세트 색 위에 덮어쓴다 */
  const S = W.wl ? { ...base, wl: W.wl, wr: W.wr, fl: W.fl } : base;
  const setKey = artKey("set", room.set || "none");
  const [bg, setBg] = useState(ART.img.get(setKey) || null);
  useEffect(() => {
    if (bg || !ART.idx.has(setKey)) { if (!ART.idx.has(setKey)) setBg(null); return; }
    let live = true;
    (async () => { try { const r = await window.storage.get(`hanul:art:${setKey}`, true); ART.img.set(setKey, r.value); if (live) setBg(r.value); } catch (e) {} })();
    return () => { live = false; };
  }, [setKey, bg]);
  const floorItems = SLOTS.filter((sl) => sl.zone === "floor" && room.slots?.[sl.id])
    .map((sl) => ({ sl, id: room.slots[sl.id] }))
    .sort((a, b) => a.sl.y - b.sl.y);

  return (
    <svg viewBox="0 0 480 300" xmlns="http://www.w3.org/2000/svg" className="hl-dio">
      <defs>
        <linearGradient id="wlG" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor={S.wl} stopOpacity=".68" /><stop offset="1" stopColor={S.wl} />
        </linearGradient>
        <linearGradient id="wrG" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor={S.wr} stopOpacity=".68" /><stop offset="1" stopColor={S.wr} />
        </linearGradient>
        <linearGradient id="flG" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor={S.fl} /><stop offset="1" stopColor={S.fl} stopOpacity=".8" />
        </linearGradient>
        <radialGradient id="glow"><stop offset="0" stopColor={S.tone} stopOpacity=".3" /><stop offset="1" stopColor={S.tone} stopOpacity="0" /></radialGradient>
      </defs>

      {bg ? <image href={bg} x="0" y="0" width="480" height="300" preserveAspectRatio="xMidYMid slice" /> : (
        <>
          <polygon points={ROOM.wallL} fill="url(#wlG)" />
          <polygon points={ROOM.wallR} fill="url(#wrG)" />
          <polygon points={ROOM.floor} fill="url(#flG)" />
          <ellipse cx="240" cy="212" rx="204" ry="78" fill="url(#glow)" />
        </>
      )}

      {!bg && <>
        <polyline points="32,188 240,108 448,188" fill="none" stroke={S.ln} strokeWidth="1.8" opacity=".7" />
        <polyline points={ROOM.edge} fill="none" stroke={S.ln} strokeWidth="1.6" opacity=".6" />
        <line x1="240" y1="12" x2="240" y2="108" stroke={S.ln} strokeWidth="1.6" opacity=".6" />
      </>}
      {!bg && [172, 214].map((y, n) => {
        const k = (y - 108) / 80, hw = Math.min(208, 208 * k);
        return <line key={n} x1={240 - hw} y1={y} x2={240 + hw} y2={y} stroke={S.ln} strokeWidth="1" opacity=".22" />;
      })}

      {!bg && <Backdrop kind={S.back} />}

      {SLOTS.filter((sl) => sl.zone === "wall").map((sl) => {
        const w = wallPos(sl), id = room.slots?.[sl.id];
        if (!id && !showSlots) return null;
        return (
          <g key={sl.id} transform={`matrix(1,${w.skew},0,1,${w.x},${w.y})`}
            className="hl-slotG" onClick={() => onSlot(sl)}>
            <rect x="-48" y="-56" width="96" height="82" fill="transparent" />
            {id ? <IsoArt id={id} w={84} /> : (
              <g>
                <rect x="-24" y="-24" width="48" height="48" fill={S.ln} fillOpacity=".1" stroke={S.ln} strokeWidth="1.4" strokeDasharray="4 4" rx="5" />
                <path d="M0,-8 v16 M-8,0 h16" stroke={S.ln} strokeWidth="1.8" opacity=".8" />
              </g>
            )}
          </g>
        );
      })}

      {showSlots && SLOTS.filter((sl) => sl.zone === "floor" && !room.slots?.[sl.id]).map((sl) => (
        <g key={sl.id} className="hl-slotG" onClick={() => onSlot(sl)}>
          <polygon points={`${sl.x},${sl.y - 17 * sl.s} ${sl.x + 40 * sl.s},${sl.y} ${sl.x},${sl.y + 17 * sl.s} ${sl.x - 40 * sl.s},${sl.y}`}
            fill={S.ln} fillOpacity=".12" stroke={S.ln} strokeWidth="1.4" strokeDasharray="4 4" />
          <path d={`M${sl.x},${sl.y - 6} v12 M${sl.x - 11},${sl.y} h22`} stroke={S.ln} strokeWidth="1.7" opacity=".75" />
        </g>
      ))}

      {floorItems.map(({ sl, id }) => (
        <g key={sl.id} className="hl-slotG" transform={`translate(${sl.x},${sl.y}) scale(${sl.s})`} onClick={() => onSlot(sl)}>
          <rect x="-46" y="-88" width="92" height="110" fill="transparent" />
          <IsoArt id={id} />
        </g>
      ))}

      <g transform={`translate(${CHAR_POS.x},${CHAR_POS.y})`} style={{ pointerEvents: "none" }}>
        {sdw(19, 0, 2)}
        <CharIso id={charId} ch={room.ch} />
      </g>
    </svg>
  );
}

function itemShape(id) {
  switch (id) {

    /* ── 바닥 ── */
    case "chair": return <>
      {sdw(19, 0, 3)}
      {bx(4, 4, 19, MAT.woodD, -13, 6.5, "lg1")}{bx(4, 4, 19, MAT.woodD, 13, -6.5, "lg2")}
      {bx(4, 4, 19, MAT.woodD, 0, 13, "lg3")}{bx(4, 4, 19, MAT.woodD, 0, -13, "lg4")}
      {bx(26, 26, 3, MAT.wood, 0, -19, "st")}
      {bx(23, 23, 2.5, MAT.velvet, -1.5, -22.5, "cu")}
      {bx(4, 22, 30, MAT.woodD, 12, -30, "bkp")}
      {bx(22, 3, 3, MAT.wood, -11, -49, "bkt")}
      {bx(16, 2, 2, MAT.wood, -8, -40, "sl1")}
      {bx(16, 2, 2, MAT.wood, -8, -34, "sl2")}
      <circle cx="-11" cy="-24" r="1.1" fill="#C09A4E" />
      <circle cx="10" cy="-13" r="1.1" fill="#C09A4E" />
    </>;

    case "crate": return <>
      {sdw(22, 0, 3)}
      {bx(30, 30, 25, MAT.wood, 0, 0, "b")}
      {[7, 15, 23].map((k, n) => <g key={n}>
        <line x1={k} y1={k / 2 - 25} x2={k} y2={k / 2} stroke="#6B5138" strokeWidth="1.1" opacity=".65" />
        <line x1={-k} y1={k / 2 - 25} x2={-k} y2={k / 2} stroke="#4A3724" strokeWidth="1.1" opacity=".6" />
      </g>)}
      <polygon points="0,-25 30,-10 30,-13.5 0,-28.5" fill="#7C5C3C" />
      <polygon points="0,-25 -30,-10 -30,-13.5 0,-28.5" fill="#5C4530" />
      <polygon points="0,-6 30,9 30,5.5 0,-9.5" fill="#7C5C3C" opacity=".8" />
      <polygon points="0,-6 -30,9 -30,5.5 0,-9.5" fill="#5C4530" opacity=".8" />
      {bx(5, 5, 3, MAT.metal, 27, -14, "c1")}{bx(5, 5, 3, MAT.metal, -2, -14, "c2")}
    </>;

    case "plant": return <>
      {sdw(15, 0, 3)}
      {taper(17, 12, 15, MAT.clay, 0, -17, "pot")}
      <ellipse cx="0" cy="-17" rx="13" ry="6.5" fill="#D89263" />
      <ellipse cx="0" cy="-17" rx="10.5" ry="5.2" fill="#4A3526" />
      <path d="M0,-18 C-4,-30 -14,-36 -20,-44 C-9,-44 -1,-34 0,-24 Z" fill="#3F5C44" />
      <path d="M0,-18 C4,-31 15,-38 22,-45 C11,-46 2,-34 0,-24 Z" fill="#5B7F58" />
      <path d="M0,-20 C-2,-36 -8,-46 -8,-56 C1,-50 5,-38 2,-24 Z" fill="#6C9166" />
      <path d="M0,-20 C3,-34 10,-44 16,-52 C11,-40 6,-30 2,-22 Z" fill="#4E7150" />
      <path d="M-1,-22 C-6,-34 -14,-40 -18,-48" stroke="#8FB07F" strokeWidth="1" fill="none" opacity=".6" />
      <circle cx="-14" cy="-44" r="2.6" fill="#D9738A" />
      <circle cx="17" cy="-46" r="2.2" fill="#E8A0AF" />
    </>;

    case "rug": return <>
      {pad(40, 40, "#7A272D", 0, 0)}
      <polygon points="0,-3 34,14 0,31 -34,14" fill="#8E2F35" />
      <polygon points="0,3 25,15.5 0,28 -25,15.5" fill="none" stroke="#C09A4E" strokeWidth="1.6" />
      <polygon points="0,8 15,15.5 0,23 -15,15.5" fill="#C09A4E" opacity=".38" />
      <polygon points="0,12 8,16 0,20 -8,16" fill="#E7C079" opacity=".5" />
      {[...Array(9)].map((_, n) => (
        <line key={n} x1={-38 + n * 4.7} y1={13 + n * 2.35} x2={-40 + n * 4.7} y2={14 + n * 2.35}
          stroke="#C09A4E" strokeWidth="1" opacity=".55" />
      ))}
    </>;

    case "lamp": return <>
      {sdw(20, 0, 3)}
      <ellipse cx="0" cy="0" rx="22" ry="11" fill="#F2D89A" opacity=".16" />
      {bx(13, 13, 2.5, MAT.dark, 0, 0, "b1")}
      {bx(9, 9, 2, MAT.brass, -2, -2.5, "b2")}
      <rect x="-1.6" y="-48" width="3.2" height="45" fill="#7C868E" />
      <rect x="-1.6" y="-48" width="1.2" height="45" fill="#B2BAC2" />
      <polygon points="-17,-47 17,-47 10,-67 -10,-67" fill="#F0E2BC" />
      <polygon points="-17,-47 0,-43 0,-64 -10,-67" fill="#DCCB9E" />
      <ellipse cx="0" cy="-67" rx="10" ry="4" fill="#FFF8E4" />
      <ellipse cx="0" cy="-46" rx="17" ry="6" fill="#FFF1CE" opacity=".85" />
      <ellipse cx="0" cy="-44" rx="24" ry="9" fill="#FFE9B0" opacity=".2" />
    </>;

    case "table": return <>
      {sdw(24, 0, 3)}
      {bx(7, 7, 4, MAT.woodD, 0, 0, "ft")}
      {bx(5, 5, 20, MAT.woodD, 1, -4, "pl")}
      {bx(9, 9, 3, MAT.woodD, -1, -24, "cp")}
      <ellipse cx="0" cy="-25" rx="30" ry="15" fill="#7E6248" />
      <ellipse cx="0" cy="-28" rx="30" ry="15" fill="#BE9264" />
      <ellipse cx="0" cy="-28" rx="24" ry="12" fill="none" stroke="#916D47" strokeWidth="1" opacity=".7" />
      <path d="M-26,-32 A30,15 0 0 1 6,-42" fill="none" stroke="#DBB183" strokeWidth="1.6" opacity=".85" />
      <ellipse cx="9" cy="-31" rx="7" ry="3.5" fill="#8E2F35" opacity=".55" />
    </>;

    case "trunk": return <>
      {sdw(24, 0, 3)}
      {bx(32, 22, 14, MAT.woodR, 0, 0, "b")}
      <path d="M0,-14 L32,2 L32,-2 A17,9 0 0 0 0,-19 Z" fill="#8E5A3C" />
      <path d="M0,-14 L-22,-3 L-22,-7 A15,8 0 0 1 0,-19 Z" fill="#6B412A" />
      <ellipse cx="5" cy="-16.5" rx="21" ry="7" fill="#A26D48" />
      <ellipse cx="5" cy="-17.5" rx="21" ry="7" fill="#B27A52" />
      <polygon points="0,-16 32,0 32,-3.5 0,-19.5" fill="#E0BB7E" opacity=".9" />
      <polygon points="0,-16 -22,-5 -22,-8.5 0,-19.5" fill="#B2924F" opacity=".9" />
      <polygon points="10,-11 16,-8 16,4 10,1" fill="#C09A4E" opacity=".85" />
      <polygon points="-8,-10 -14,-7 -14,5 -8,2" fill="#96773A" opacity=".85" />
      {bx(5, 4, 4, MAT.brass, 4, -13, "lock")}
      {[[30, 2], [-20, -4]].map(([x, y], n) => <circle key={n} cx={x} cy={y} r="1.6" fill="#EAC47E" />)}
    </>;

    case "mirror": return <>
      {sdw(15, 0, 3)}
      {bx(16, 8, 3, MAT.brass, 0, 0, "b")}
      <g transform="translate(0,-3)">
        <path d="M-14,-4 L-14,-46 Q0,-60 14,-46 L14,-4 Q0,6 -14,-4 Z" fill="#8B6F35" />
        <path d="M-12,-6 L-12,-45 Q0,-57 12,-45 L12,-6 Q0,3 -12,-6 Z" fill="#C09A4E" />
        <path d="M-9,-8 L-9,-44 Q0,-53 9,-44 L9,-8 Q0,-1 -9,-8 Z" fill="#B9CDD8" />
        <path d="M-9,-20 L4,-46 L8,-44 L-6,-16 Z" fill="#E4F0F6" opacity=".55" />
        <path d="M2,-12 L9,-28 L9,-22 L5,-11 Z" fill="#E4F0F6" opacity=".35" />
        <circle cx="0" cy="-57" r="3.4" fill="#EAC47E" />
      </g>
    </>;

    case "piano": return <>
      {sdw(28, 0, 3)}
      {bx(6, 6, 15, MAT.dark, -16, 8, "l1")}{bx(6, 6, 15, MAT.dark, 16, -8, "l2")}
      {bx(6, 6, 15, MAT.dark, 0, 16, "l3")}
      {bx(42, 18, 7, { t: "#4E3B2C", r: "#3A2A1E", l: "#261B13", hi: "#6B5340" }, 0, -15, "body")}
      {bx(42, 18, 26, { t: "#4A3728", r: "#36271B", l: "#231910", hi: "#67503C" }, 0, -22, "up")}
      <polygon points="0,-22 42,-1 42,-5 0,-26" fill="#EFEAE0" />
      <polygon points="0,-22 -18,-13 -18,-17 0,-26" fill="#CFC8BA" />
      {[...Array(7)].map((_, n) => (
        <line key={n} x1={5 + n * 5.5} y1={-24 + (5 + n * 5.5) / 2} x2={5 + n * 5.5} y2={-21 + (5 + n * 5.5) / 2}
          stroke="#241F1B" strokeWidth="2.2" />
      ))}
      <polygon points="0,-48 42,-27 42,-25 0,-46" fill="#7A5E44" />
      <polygon points="0,-48 -18,-39 -18,-37 0,-46" fill="#54402F" />
      <rect x="-1" y="-46" width="2" height="20" fill="#3A2A1E" opacity=".5" />
      {bx(4, 3, 2, MAT.brass, 18, -2, "pd")}
    </>;

    case "rack": return <>
      {sdw(24, 0, 3)}
      {bx(22, 5, 2.5, MAT.metal, -13, 7, "b1")}{bx(5, 22, 2.5, MAT.metal, 15, 7, "b2")}
      <g transform="translate(-13,6)"><rect x="-1.4" y="-52" width="2.8" height="52" fill="#848E97" /><rect x="-1.4" y="-52" width="1" height="52" fill="#C2CAD2" /></g>
      <g transform="translate(15,6)"><rect x="-1.4" y="-52" width="2.8" height="52" fill="#848E97" /><rect x="-1.4" y="-52" width="1" height="52" fill="#C2CAD2" /></g>
      <polygon points="-14,-46 16,-31 16,-34 -14,-49" fill="#B2BAC2" />
      <path d="M-10,-42 l7,3 -3,4 8,26 -18,-8 6,-25 Z" fill="#8E2F35" />
      <path d="M-10,-42 l7,3 -3,4 3,24 -8,-4 1,-27 Z" fill="#A8434A" />
      <path d="M0,-37 l7,3 -3,4 8,24 -18,-8 6,-23 Z" fill="#33526F" />
      <path d="M0,-37 l7,3 -3,4 3,22 -8,-4 1,-25 Z" fill="#43678A" />
      <path d="M9,-32 l6,3 -2,4 6,20 -15,-7 5,-20 Z" fill="#42604A" />
      <ellipse cx="-13" cy="-54" rx="8" ry="3.4" fill="#7E6248" />
      <ellipse cx="-13" cy="-56" rx="5" ry="2.6" fill="#9A7A5C" />
    </>;

    case "gramo": return <>
      {sdw(19, 0, 3)}
      {bx(26, 24, 12, MAT.woodD, 0, 0, "b")}
      <ellipse cx="1" cy="-12" rx="15" ry="7.5" fill="#5D4733" />
      <ellipse cx="1" cy="-13.5" rx="14" ry="7" fill="#2A2420" />
      <ellipse cx="1" cy="-13.5" rx="4" ry="2" fill="#C09A4E" />
      <rect x="-1.6" y="-34" width="3.2" height="21" fill="#C09A4E" />
      <path d="M0,-32 C13,-35 29,-51 39,-70 C17,-66 4,-50 0,-36 Z" fill="#EAC47E" />
      <path d="M0,-32 C11,-36 24,-49 33,-64 C16,-60 5,-47 0,-35 Z" fill="#C09A4E" />
      <path d="M2,-34 C12,-38 22,-49 30,-61" stroke="#F8E3AC" strokeWidth="1.2" fill="none" opacity=".8" />
      <ellipse cx="36" cy="-67" rx="7" ry="9" fill="#8B6F35" opacity=".55" transform="rotate(28 36 -67)" />
      <path d="M-13,-10 l-7,5 3,3" stroke="#C09A4E" strokeWidth="2" fill="none" strokeLinecap="round" />
    </>;

    /* ── 벽 (평면으로 그린 뒤 벽면에 눕힌다) ── */
    case "frame": return <>
      <rect x="-24" y="-33" width="48" height="42" rx="2" fill="#8B6F35" />
      <rect x="-21.5" y="-30.5" width="43" height="37" fill="#C09A4E" />
      <rect x="-18" y="-27" width="36" height="30" fill="#EFEAE0" />
      <rect x="-15" y="-24" width="30" height="24" fill="#3E5468" />
      <path d="M-15,-6 L-5,-16 L2,-9 L9,-18 L15,-10 L15,0 L-15,0 Z" fill="#31485C" />
      <path d="M-15,0 L15,0 L15,-4 L-15,-4 Z" fill="#25384A" />
      <circle cx="7" cy="-18" r="3" fill="#E7C079" opacity=".8" />
      <rect x="-21.5" y="-30.5" width="43" height="2" fill="#EAC47E" opacity=".7" />
    </>;

    case "curtain": return <>
      <rect x="-32" y="-44" width="64" height="30" fill="#2C3A46" />
      <rect x="-29" y="-41" width="58" height="24" fill="#6E93A6" opacity=".8" />
      <path d="M-29,-17 L-8,-41 L2,-41 L-19,-17 Z" fill="#C8E2EE" opacity=".45" />
      <rect x="-34" y="-47" width="68" height="4" rx="2" fill="#C09A4E" />
      <circle cx="-35" cy="-45" r="3" fill="#EAC47E" /><circle cx="35" cy="-45" r="3" fill="#EAC47E" />
      <path d="M-31,-43 h13 c3,20 0,32 -3,46 -5,2 -9,1 -13,-1 Z" fill="#5D7F8E" />
      <path d="M-31,-43 h6 c2,20 0,32 -2,46 -2,1 -3,1 -4,0 Z" fill="#6E93A6" />
      <path d="M31,-43 h-13 c-3,20 0,32 3,46 5,2 9,1 13,-1 Z" fill="#4E6D7B" />
      <path d="M31,-43 h-6 c-2,20 0,32 2,46 2,1 3,1 4,0 Z" fill="#5D7F8E" />
    </>;

    case "spot": return <>
      <polygon points="0,-14 -30,50 30,50" fill="#FFE9A8" opacity=".22" />
      <polygon points="0,-14 -14,50 14,50" fill="#FFF3CC" opacity=".18" />
      <rect x="-3" y="-46" width="6" height="10" fill="#848E97" />
      <circle cx="0" cy="-36" r="3.4" fill="#5B646D" />
      <rect x="-13" y="-34" width="26" height="20" rx="4" fill="#2F363D" />
      <rect x="-13" y="-34" width="26" height="5" rx="2" fill="#454D56" />
      <rect x="-15" y="-19" width="4" height="9" rx="1" fill="#1E242A" />
      <rect x="11" y="-19" width="4" height="9" rx="1" fill="#1E242A" />
      <ellipse cx="0" cy="-14" rx="12" ry="4" fill="#FFF1C6" />
      <ellipse cx="0" cy="-14" rx="7" ry="2.4" fill="#FFFDF2" />
    </>;

    case "velvet": return <>
      <path d="M-46,-52 h92 v10 h-92 Z" fill="#8B6F35" />
      <path d="M-46,-52 h92 v3 h-92 Z" fill="#EAC47E" />
      <path d="M-44,-42 h30 c2,10 2,16 0,22 -10,4 -20,4 -30,1 Z" fill="#83292F" />
      <path d="M44,-42 h-30 c-2,10 -2,16 0,22 10,4 20,4 30,1 Z" fill="#6E2228" />
      <path d="M-44,-42 h26 c3,28 -1,44 -5,64 -8,3 -15,1 -21,-2 Z" fill="#8E2F35" />
      <path d="M-44,-42 h9 c2,28 0,46 -2,64 -3,1 -5,0 -7,-2 Z" fill="#A8434A" />
      <path d="M-26,-42 h8 c2,26 0,42 -2,60 -3,1 -5,1 -7,0 Z" fill="#A03A41" opacity=".7" />
      <path d="M44,-42 h-26 c-3,28 1,44 5,64 8,3 15,1 21,-2 Z" fill="#7A272D" />
      <path d="M44,-42 h-9 c-2,28 0,46 2,64 3,1 5,0 7,-2 Z" fill="#8E2F35" />
      <circle cx="-16" cy="20" r="3" fill="#EAC47E" /><circle cx="16" cy="20" r="3" fill="#EAC47E" />
    </>;

    case "plaque": return <>
      <rect x="-27" y="-50" width="54" height="64" rx="3" fill="#241C15" />
      <rect x="-27" y="-50" width="54" height="3" fill="#3C2E22" />
      <rect x="-23" y="-46" width="46" height="56" rx="2" fill="none" stroke="#C09A4E" strokeWidth="1.6" />
      <rect x="-21" y="-44" width="42" height="52" fill="#2E241B" />
      <image href={MARK} x="-15" y="-41" width="30" height="42" preserveAspectRatio="xMidYMid meet" style={{ filter: "invert(1)" }} />
      <rect x="-13" y="3" width="26" height="6" rx="1" fill="#C09A4E" />
      <rect x="-13" y="3" width="26" height="2" rx="1" fill="#EAC47E" />
    </>;

    default: return null;
  }
}

function ItemArt({ id }) {
  return (
    <Art kind="item" id={id} className="hl-art">
      <svg viewBox="-48 -78 96 96" xmlns="http://www.w3.org/2000/svg" className="hl-art">{itemShape(id)}</svg>
    </Art>
  );
}

/* 방 안(SVG) 에서 쓰는 소품 — 올린 그림이 있으면 그 그림을 붙인다 */
function IsoArt({ id, w = 92 }) {
  const key = artKey("item", id);
  const [src, setSrc] = useState(ART.img.get(key) || null);
  useEffect(() => {
    if (src || !ART.idx.has(key)) return;
    let live = true;
    (async () => {
      try { const r = await window.storage.get(`hanul:art:${key}`, true); ART.img.set(key, r.value); if (live) setSrc(r.value); }
      catch (e) {}
    })();
    return () => { live = false; };
  }, [key, src]);
  if (!ART.idx.has(key)) return itemShape(id);
  if (!src) return null;
  return <image href={src} x={-w / 2} y={-w} width={w} height={w} preserveAspectRatio="xMidYMax meet" />;
}

function Attendance({ members, attend, me, isExec, onMark }) {
  const [view, setView] = useState("mine");
  const [ym, setYm] = useState(ymOf(today()));
  const [day, setDay] = useState(today());
  const [y, m] = ym.split("-").map(Number);
  const lead = new Date(y, m - 1, 1).getDay();
  const total = new Date(y, m, 0).getDate();
  const cells = [...Array(lead).fill(null), ...Array.from({ length: total }, (_, i) => i + 1)];
  const shift = (n) => { const d = new Date(y, m - 1 + n, 1); setYm(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`); };
  const kindOf = (ds, id) => attend?.[ds]?.[id] || null;

  const days = Array.from({ length: total }, (_, i) => dstr(y, m, i + 1));
  const tally = { present: 0, late: 0, absent: 0 };
  days.forEach((ds) => { const k = kindOf(ds, me.id); if (k) tally[k]++; });
  const pt = tally.present * ATT.present.pt + tally.late * ATT.late.pt + tally.absent * ATT.absent.pt;
  const marked = Object.keys(attend?.[day] || {}).length;

  return (
    <>
      {isExec && (
        <div className="hl-seg">
          <button className={view === "mine" ? "on" : ""} onClick={() => setView("mine")}>내 출석</button>
          <button className={view === "check" ? "on" : ""} onClick={() => setView("check")}>체크하기</button>
        </div>
      )}

      {view === "check" && isExec ? (
        <>
          <div className="hl-sectionHead"><span>출석 체크</span><span className="hl-mono hl-dim">{marked} / {members.length}</span></div>
          <input className="hl-in hl-block hl-mono" type="date" value={day} onChange={(e) => setDay(e.target.value)} />
          <ul className="hl-list hl-attList">
            {members.map((mem) => (
              <li key={mem.id} className="hl-attItem">
                <div className="hl-attWho">
                  <span className="hl-attNm">{mem.name}</span>
                  <span className="hl-mono hl-dim">{mem.gen}기 · {partOf(mem.part).n}</span>
                </div>
                <div className="hl-attBtns">
                  {Object.entries(ATT).map(([k, v]) => (
                    <button key={k} className={`hl-attBtn ${kindOf(day, mem.id) === k ? "on" : ""}`}
                      style={kindOf(day, mem.id) === k ? { background: v.c, borderColor: v.c, color: "#12181E" } : {}}
                      onClick={() => onMark(day, mem.id, k)}>{v.n}</button>
                  ))}
                </div>
              </li>
            ))}
          </ul>
          <p className="hl-fine hl-left">같은 버튼을 다시 누르면 취소돼요. 출석 +10P · 지각 −5P · 결석 −15P.</p>
        </>
      ) : (
        <>
          <div className="hl-calHead">
            <button className="hl-navBtn" onClick={() => shift(-1)} aria-label="이전 달">‹</button>
            <span className="hl-mono hl-calYm">{y}. {String(m).padStart(2, "0")}</span>
            <button className="hl-navBtn" onClick={() => shift(1)} aria-label="다음 달">›</button>
          </div>

          <div className="hl-tally">
            {Object.entries(ATT).map(([k, v]) => (
              <div key={k} className="hl-tallyCell">
                <span className="hl-tallyN" style={{ color: v.c }}>{tally[k]}</span>
                <span className="hl-tallyL">{v.n}</span>
              </div>
            ))}
            <div className="hl-tallyCell">
              <span className="hl-tallyN">{pt > 0 ? "+" : ""}{pt}</span>
              <span className="hl-tallyL">이번 달 P</span>
            </div>
          </div>

          <div className="hl-cal hl-attCal">
            {WEEK.map((w, i) => <div key={w} className={`hl-wd ${i === 0 ? "sun" : ""}`}>{w}</div>)}
            {cells.map((d, i) => {
              if (!d) return <div key={`b${i}`} />;
              const ds = dstr(y, m, d), k = kindOf(ds, me.id);
              return (
                <div key={ds} className={`hl-day ${ds === today() ? "today" : ""}`}
                  style={k ? { background: ATT[k].c, borderColor: ATT[k].c } : {}}>
                  <span className="hl-dayNum" style={k ? { color: "#12181E" } : (i % 7 === 0 ? { color: "#C4645C" } : {})}>{d}</span>
                </div>
              );
            })}
          </div>

          <div className="hl-legend2">
            {Object.entries(ATT).map(([k, v]) => (
              <span key={k} className="hl-lg2"><i style={{ background: v.c }} />{v.n} {v.pt > 0 ? "+" : ""}{v.pt}P</span>
            ))}
          </div>
          <p className="hl-fine hl-left">집행부가 체크하면 여기에 바로 반영돼요.</p>
        </>
      )}
    </>
  );
}

function Roster({ members, logs, pointsOf, meId, rooms, onVisit, syncing, syncAt, onSync }) {
  const [by, setBy] = useState("have");
  const ago = syncAt ? Math.floor((Date.now() - syncAt) / 1000) : null;
  const month = today().slice(0, 7);
  /* 보유 = 누적 − 쓴 만큼 */
  const spentOf = (id) => rooms?.[id]?.spent || 0;
  const haveOf = (id) => Math.max(0, pointsOf(id) - spentOf(id));
  const key = by === "have" ? haveOf : pointsOf;
  const ranked = [...members].sort((a, b) => key(b.id) - key(a.id));
  return (
    <>
      <div className="hl-sectionHead">
        <span>단원 현황</span>
        <span className="hl-mono hl-dim">{members.length}명</span>
      </div>
      <div className="hl-seg">
        <button className={by === "have" ? "on" : ""} onClick={() => setBy("have")}>보유 포인트</button>
        <button className={by === "total" ? "on" : ""} onClick={() => setBy("total")}>누적 포인트</button>
      </div>
      <p className="hl-fine hl-left" style={{ marginTop: 8, marginBottom: 10 }}>
        보유는 지금 쓸 수 있는 양, 누적은 지금까지 번 총량이에요.
      </p>
      <button className="hl-syncRow" onClick={onSync} disabled={syncing}>
        <svg viewBox="0 0 24 24" className={`hl-syncIcon ${syncing ? "spin" : ""}`}>
          <path d="M20 12a8 8 0 1 1-2.34-5.66" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" />
          <path d="M20 3v5h-5" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
        <span className="hl-syncTxt">{syncing ? "불러오는 중…" : "새로 불러오기"}</span>
        {ago !== null && !syncing && (
          <span className="hl-mono hl-dim">{ago < 60 ? "방금 갱신" : `${Math.floor(ago / 60)}분 전`}</span>
        )}
      </button>
      <ul className="hl-list">
        {ranked.map((m, i) => {
          const cnt = logs.filter((l) => l.memberId === m.id && l.date.startsWith(month) && l.status === "approved").length;
          return (
            <li key={m.id} className={`hl-member ${m.id === meId ? "self" : ""}`}>
              <button className="hl-visitHit" onClick={() => onVisit(m.id)} aria-label={`${m.name}님의 방 보기`} />
              <span className="hl-mono hl-rank">{String(i + 1).padStart(2, "0")}</span>
              <span className="hl-face"><CharFace id={m.id} ch={rooms?.[m.id]?.ch || (m.role === "owner" ? OWNER_CH : DEFAULT_CH)} /></span>
              <div className="hl-memberBody">
                <div className="hl-memberName">{m.name}<span className="hl-dim"> · {m.gen}기</span></div>
                <div className="hl-mono hl-dim hl-memberSub">
                  이번 달 기록 {cnt}개
                  {spentOf(m.id) > 0 && ` · 사용 ${spentOf(m.id).toLocaleString()}P`}
                </div>
              </div>
              <span className="hl-ptCol">
                <span className="hl-mono hl-memberPt">{key(m.id).toLocaleString()}P</span>
                <span className="hl-mono hl-memberPt2">
                  {by === "have" ? `누적 ${pointsOf(m.id).toLocaleString()}` : `보유 ${haveOf(m.id).toLocaleString()}`}
                </span>
              </span>
              <span className="hl-menuArrow">›</span>
            </li>
          );
        })}
      </ul>
    </>
  );
}

function Approve({ pending, members, onDecide }) {
  const nameOf = (id) => members.find((m) => m.id === id)?.name || "?";
  const roleOf = (id) => members.find((m) => m.id === id)?.role;
  const teamSize = (part, id) => members.filter((m) => m.part === part && m.id !== id).length;
  if (!pending.length) return <div className="hl-empty">확인할 기록이 없어요.</div>;
  return (
    <ul className="hl-list">
      {pending.map((l) => (
        <li key={l.id} className="hl-row pending">
          <div className="hl-rowTop"><span className="hl-mono hl-scene">{scn(l.scene)}</span><span className="hl-mono hl-dim">{fmtDate(l.date)}{l.at && ` · ${when(l.at)}`}</span></div>
          <div className="hl-rowMain"><strong className="hl-who2">{nameOf(l.memberId)}</strong><span className="hl-act">{partOf(l.typeId).n}</span><span className="hl-mono hl-pt">+{l.pt}P</span></div>
          {l.text && <p className="hl-rowText">{l.text}</p>}
          {roleOf(l.memberId) === "lead" && (
            <p className="hl-teamNote">
              팀장 일지예요. 승인하면 {partOf(l.typeId).n}팀 {teamSize(l.typeId, l.memberId)}명에게도 {LOG_PT}P씩 들어갑니다.
            </p>
          )}
          <div className="hl-actions">
            <button className="hl-approve" onClick={() => onDecide(l.id, "approved")}>승인</button>
            <button className="hl-reject" onClick={() => onDecide(l.id, "rejected")}>반려</button>
          </div>
        </li>
      ))}
    </ul>
  );
}

function SuggBox({ sugg }) {
  if (!sugg.length) return <div className="hl-empty">들어온 건의가 없어요.</div>;
  return (
    <ul className="hl-list">
      {[...sugg].sort((a, b) => b.at - a.at).map((sg) => (
        <li key={sg.id} className="hl-row">
          <p className="hl-rowText">{sg.text}</p>
          <div className="hl-mono hl-rowDate">{when(sg.at)}</div>
        </li>
      ))}
    </ul>
  );
}

function RoleBoard({ members, onRole, onDrop, onPart }) {
  return (
    <ul className="hl-list">
      {members.map((m) => (
        <li key={m.id} className="hl-roleRow">
          <div className="hl-attWho">
            <span className="hl-attNm">{m.name}<span className="hl-dim"> · {m.gen}기</span></span>
            <select className="hl-sel" value={m.part || "plan"} onChange={(e) => onPart(m.id, e.target.value)}>
              {PARTS.map((p) => <option key={p.id} value={p.id}>{p.n}</option>)}
            </select>
          </div>
          {m.role === "owner" ? <span className="hl-mono hl-dim">관리자</span> : (
            <div className="hl-attBtns">
              {["member", "lead", "exec"].map((r) => (
                <button key={r} className={`hl-attBtn ${m.role === r ? "on" : ""}`} onClick={() => onRole(m.id, r)}>{ROLES[r]}</button>
              ))}
              <button className="hl-del" onClick={() => onDrop(m.id)}>삭제</button>
            </div>
          )}
        </li>
      ))}
    </ul>
  );
}

function CodeBox({ codes, onCodes, flash }) {
  const [ec, setEc] = useState(codes.exec);
  const [lc, setLc] = useState(codes.lead);
  return (
    <>
      <div className="hl-scopeLabel">집행부 코드</div>
      <input className="hl-in hl-block hl-mono" value={ec} onChange={(e) => setEc(e.target.value)} />
      <div className="hl-scopeLabel">팀장 코드</div>
      <input className="hl-in hl-block hl-mono" value={lc} onChange={(e) => setLc(e.target.value)} />
      <button className="hl-cta hl-block" onClick={() => { onCodes({ exec: ec.trim(), lead: lc.trim() }); flash("권한 코드를 바꿨어요."); }}>저장하기</button>
      <p className="hl-fine hl-left">가입할 때 이 코드를 넣으면 해당 역할로 들어와요. 코드가 새면 여기서 바꾸고, 이미 들어온 사람은 단원·역할에서 내리면 됩니다.</p>
    </>
  );
}

function PointGrant({ members, adjust, onGrant, pointsOf, flash }) {
  const [who, setWho] = useState(members[0]?.id || "");
  const [amt, setAmt] = useState("10");
  const [why, setWhy] = useState("");
  const go = (sign) => {
    const n = Math.abs(Number(amt) || 0);
    if (!who || !n) return;
    onGrant(who, sign * n, why.trim() || (sign > 0 ? "지급" : "회수"));
    setWhy("");
    flash(`${members.find((m) => m.id === who)?.name}에게 ${sign > 0 ? "+" : "−"}${n}P`);
  };
  const log = [...adjust].sort((a, b) => b.at - a.at).slice(0, 12);
  return (
    <>
      <div className="hl-scopeLabel">누구에게</div>
      <div className="hl-chips">
        {members.map((m) => (
          <button key={m.id} className={`hl-chip2 ${who === m.id ? "on" : ""}`} onClick={() => setWho(m.id)}>
            {m.name}<span className="hl-mono hl-chipPt">{pointsOf(m.id)}P</span>
          </button>
        ))}
      </div>

      <div className="hl-scopeLabel">얼마나</div>
      <div className="hl-addRow">
        <input className="hl-in hl-inSm hl-mono" value={amt} inputMode="numeric" onChange={(e) => setAmt(e.target.value)} />
        <input className="hl-in" placeholder="사유 (예: 공연 준비 수고)" value={why} onChange={(e) => setWhy(e.target.value)} />
      </div>
      <div className="hl-quick">
        {[10, 30, 50, 100].map((v) => (
          <button key={v} className={`hl-attBtn ${String(v) === amt ? "on" : ""}`} onClick={() => setAmt(String(v))}>{v}P</button>
        ))}
      </div>
      <div className="hl-dayBtns">
        <button className="hl-cta" onClick={() => go(1)}>지급하기</button>
        <button className="hl-cta done" onClick={() => go(-1)}>회수하기</button>
      </div>

      <div className="hl-sectionHead"><span>최근 내역</span><span className="hl-mono hl-dim">{adjust.length}</span></div>
      {log.length === 0 ? <div className="hl-empty">아직 지급한 내역이 없어요.</div> : (
        <ul className="hl-list">
          {log.map((a) => (
            <li key={a.id} className="hl-grantRow">
              <span className="hl-attNm">{members.find((m) => m.id === a.memberId)?.name || "?"}</span>
              <span className="hl-grantWhy">{a.reason}</span>
              <span className="hl-ptCol">
                <span className="hl-mono hl-grantPt" style={{ color: a.pt > 0 ? "var(--accent)" : "#C4645C" }}>
                  {a.pt > 0 ? "+" : ""}{a.pt}P
                </span>
                <span className="hl-mono hl-memberPt2">{when(a.at)}</span>
              </span>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

function MyInfo({ me, onUpdateMe, flash }) {
  const [gen, setGen] = useState(String(me?.gen ?? ""));
  return (
    <>
      <div className="hl-scopeLabel">이름 · 기수</div>
      <div className="hl-addRow">
        <input className="hl-in" value={me?.name || ""} readOnly />
        <input className="hl-in hl-inSm hl-mono" value={gen} inputMode="numeric" onChange={(e) => setGen(e.target.value)} />
        <button className="hl-add" onClick={() => { onUpdateMe({ gen: Number(gen) || me.gen }); flash("기수를 바꿨어요."); }}>저장</button>
      </div>
      <div className="hl-scopeLabel">내 파트</div>
      <div className="hl-lockedRow">
        <span className="hl-dotSm" style={{ background: partOf(me?.part).c }} />
        <span className="hl-lockedName">{partOf(me?.part).n}</span>
        <span className="hl-mono hl-dim">잠김</span>
      </div>
      <p className="hl-fine hl-left">이름은 로그인에 쓰여서 바꿀 수 없어요. 파트는 한 번 정하면 관리자만 바꿀 수 있습니다.</p>
    </>
  );
}

function LookSet({ pref, savePref }) {
  return (
    <>
      <div className="hl-scopeLabel">테마 색상</div>
      <div className="hl-themes">
        {Object.entries(THEMES).map(([k, t]) => (
          <button key={k} className={`hl-theme ${pref.theme === k ? "on" : ""}`} onClick={() => savePref({ ...pref, theme: k })}>
            <span className="hl-swatch" style={{
              background: t.rainbow
                ? "conic-gradient(#FF4F4F,#FFB020,#FFF34F,#4FE07A,#4FD8FF,#7A6BFF,#FF4FD8,#FF4F4F)"
                : t.swatch,
              borderColor: t.accent,
            }}><i style={{ background: t.accent }} /></span>
            <span className="hl-themeName">{t.n}</span>
          </button>
        ))}
      </div>
      <div className="hl-scopeLabel">배경</div>
      <button className="hl-toggle" onClick={() => savePref({ ...pref, ambient: !pref.ambient })}>
        <span>
          <span className="hl-toggleName">파트 색 배경</span>
          <span className="hl-toggleSub">오프닝에서 모였던 여덟 색을 흐리게 깔아요</span>
        </span>
        <span className={`hl-sw ${pref.ambient ? "on" : ""}`}><i /></span>
      </button>
    </>
  );
}

function SoundSet({ pref, savePref, custom, setCustom, flash }) {
  const [url, setUrl] = useState(pref.bgmUrl || "");
  const pickFile = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    if (custom.url) URL.revokeObjectURL(custom.url);
    setCustom({ url: URL.createObjectURL(f), name: f.name });
    savePref({ ...pref, bgm: "custom" });
    flash("이 브라우저에서만 재생돼요. 새로고침하면 다시 골라야 해요.");
  };
  return (
    <>
      <div className="hl-scopeLabel">효과음</div>
      <button className="hl-toggle" onClick={() => savePref({ ...pref, sfx: !(pref.sfx !== false) })}>
        <span>
          <span className="hl-toggleName">버튼 소리</span>
          <span className="hl-toggleSub">누를 때마다 뾱 하고 울려요</span>
        </span>
        <span className={`hl-sw ${pref.sfx !== false ? "on" : ""}`}><i /></span>
      </button>

      <div className="hl-scopeLabel">배경음</div>
      <div className="hl-bgmList">
        <button className={`hl-bgm ${(pref.bgm || "off") === "off" ? "on" : ""}`} onClick={() => savePref({ ...pref, bgm: "off" })}>
          <span className="hl-bgmName">끄기</span><span className="hl-bgmDesc">소리 없이</span>
        </button>
        {Object.entries(BGM).map(([k, t]) => (
          <button key={k} className={`hl-bgm ${pref.bgm === k ? "on" : ""}`} onClick={() => savePref({ ...pref, bgm: k })}>
            <span className="hl-bgmName">{t.n}{k === "custom" && custom.name ? ` · ${custom.name}` : ""}</span>
            <span className="hl-bgmDesc">{t.d}</span>
          </button>
        ))}
      </div>
      <div className="hl-volRow">
        <span className="hl-mono hl-dim">음량</span>
        <input className="hl-range" type="range" min="0" max="100" value={pref.vol ?? 50}
          onChange={(e) => savePref({ ...pref, vol: Number(e.target.value) })} />
        <span className="hl-mono hl-dim">{pref.vol ?? 50}</span>
      </div>

      {pref.bgm === "custom" && (
        <div className="hl-customBox">
          <label className="hl-fileBtn">음원 파일 고르기<input type="file" accept="audio/*" onChange={pickFile} /></label>
          <div className="hl-orLine"><span>또는 링크</span></div>
          <input className="hl-in hl-block" placeholder="https://… .mp3" value={url} onChange={(e) => setUrl(e.target.value)} />
          <button className="hl-cta hl-block" onClick={() => { setCustom({ url: "", name: "" }); savePref({ ...pref, bgmUrl: url.trim(), bgm: "custom" }); }}>링크 적용</button>
          <p className="hl-fine hl-left">Pixabay Music · Free Music Archive 같은 곳에서 자유 이용 음원을 받을 수 있어요. 링크는 mp3 파일을 직접 가리켜야 해요.</p>
        </div>
      )}
    </>
  );
}

const ART_TABS = [
  { id: "house", n: "집 외형", guide: "정면에서 본 집. 정사각형에 가까운 비율로, 집이 캔버스 아래쪽에 서 있게 그려주세요." },
  { id: "floor", n: "바닥 소품", guide: "2:1 아이소메트릭. 위에서 비스듬히 내려다본 각도로, 물체의 왼쪽 면과 오른쪽 면이 함께 보이게 그려주세요. 빛은 왼쪽 위에서 옵니다. 바닥에 닿는 지점을 캔버스 아래 가운데에 두세요." },
  { id: "set", n: "무대 세트", guide: "방 전체 배경 그림. 가로:세로 8:5 (예: 1440×900)로 그리고, 벽 두 면과 바닥이 이미 그려진 한 장이면 됩니다. 소품과 캐릭터는 그 위에 올라가요." },
  { id: "wall", n: "벽 소품", guide: "정면에서 본 모습. 앱이 벽 기울기에 맞춰 알아서 눕혀줘요. 벽에 닿는 뒷면은 그리지 않아도 됩니다." },
];

function AngleGuide({ kind }) {
  if (kind === "wall") return (
    <svg viewBox="0 0 200 96" className="hl-guide">
      <path d="M20,10 L100,10 L100,86 L20,86 Z" fill="none" stroke="var(--muted)" strokeWidth="1.4" strokeDasharray="4 3" />
      <text x="60" y="52" textAnchor="middle" fontSize="11" fill="var(--muted)">정면</text>
      <path d="M112,48 l16,0 m-5,-5 l5,5 -5,5" stroke="var(--accent)" strokeWidth="1.6" fill="none" />
      <path d="M140,22 L188,46 L188,86 L140,62 Z" fill="none" stroke="var(--accent)" strokeWidth="1.4" />
      <text x="164" y="60" textAnchor="middle" fontSize="9" fill="var(--accent)">벽에 붙음</text>
    </svg>
  );
  if (kind === "house") return (
    <svg viewBox="0 0 200 96" className="hl-guide">
      <path d="M60,86 L60,44 L100,20 L140,44 L140,86 Z" fill="none" stroke="var(--accent)" strokeWidth="1.6" />
      <line x1="40" y1="86" x2="160" y2="86" stroke="var(--muted)" strokeWidth="1.4" strokeDasharray="4 3" />
      <text x="100" y="12" textAnchor="middle" fontSize="9" fill="var(--muted)">정면 · 아래가 바닥</text>
    </svg>
  );
  return (
    <svg viewBox="0 0 200 96" className="hl-guide">
      <polygon points="100,20 148,44 100,68 52,44" fill="none" stroke="var(--accent)" strokeWidth="1.6" />
      <polygon points="100,68 148,44 148,76 100,100" fill="none" stroke="var(--accent)" strokeWidth="1.2" opacity=".7" />
      <polygon points="100,68 52,44 52,76 100,100" fill="none" stroke="var(--accent)" strokeWidth="1.2" opacity=".7" />
      <text x="100" y="14" textAnchor="middle" fontSize="9" fill="var(--muted)">윗면</text>
      <text x="30" y="60" fontSize="9" fill="var(--muted)">왼면</text>
      <text x="156" y="60" fontSize="9" fill="var(--muted)">오른면</text>
      <path d="M18,10 l14,14 m0,-10 l0,10 -10,0" stroke="#E9A13B" strokeWidth="1.4" fill="none" />
      <text x="36" y="20" fontSize="8" fill="#E9A13B">빛</text>
    </svg>
  );
}

function SetThumb({ set, id }) {
  return (
    <Art kind="set" id={id}>
      <svg viewBox="0 0 480 300" xmlns="http://www.w3.org/2000/svg">
        <polygon points={ROOM.wallL} fill={set.wl} />
        <polygon points={ROOM.wallR} fill={set.wr} />
        <polygon points={ROOM.floor} fill={set.fl} />
        <Backdrop kind={set.back} />
      </svg>
    </Art>
  );
}

function ArtBoard({ cat, onSaveCatalog, onPut, flash }) {
  const [tab, setTab] = useState("house");
  const [busy, setBusy] = useState("");
  const [names, setNames] = useState({});
  const [nw, setNw] = useState({ n: "", tier: "common", cost: "1000" });
  const T = ART_TABS.find((t) => t.id === tab);

  const list = tab === "house" ? cat.houses
    : tab === "set" ? (cat.sets || [])
    : cat.items.filter((it) => it.zone === (tab === "wall" ? "wall" : "floor"));
  const kind = tab === "house" ? "house" : tab === "set" ? "set" : "item";

  const upload = async (id, file, err) => {
    if (err) { flash(err); return; }
    if (!file) return;
    setBusy(id);
    try { await onPut(kind, id, await shrinkArt(file)); }
    catch (e) { flash(`그림을 읽지 못했어요: ${e?.message || e}`); }
    setBusy("");
  };
  const rename = (id) => {
    const v = (names[id] ?? "").trim();
    if (!v) return;
    const next = tab === "house"
      ? { ...cat, houses: cat.houses.map((h) => (h.id === id ? { ...h, n: v } : h)) }
      : tab === "set"
      ? { ...cat, sets: (cat.sets || []).map((x) => (x.id === id ? { ...x, n: v } : x)) }
      : { ...cat, items: cat.items.map((it) => (it.id === id ? { ...it, n: v } : it)) };
    onSaveCatalog(next);
    setNames({ ...names, [id]: undefined });
    flash("이름을 바꿨어요.");
  };
  const add = () => {
    const n = nw.n.trim();
    if (!n) return;
    const id = `c_${Date.now()}`;
    const next = tab === "house"
      ? { ...cat, houses: [...cat.houses, { id, n, tier: nw.tier, d: "직접 추가한 외형", custom: true }] }
      : tab === "set"
      ? { ...cat, sets: [...(cat.sets || []), { id, n, d: "직접 추가한 세트", cost: Number(nw.cost) || 1000,
          wl: "#2E3841", wr: "#26303A", fl: "#3C444D", ln: "#4A545E", tone: "#8A97A3", back: null, custom: true }] }
      : { ...cat, items: [...cat.items, { id, n, tier: nw.tier, zone: tab === "wall" ? "wall" : "floor", custom: true }] };
    onSaveCatalog(next);
    setNw({ n: "", tier: "common" });
    flash("추가했어요. 이제 그림을 올려주세요.");
  };
  const drop = (id) => {
    const next = tab === "house"
      ? { ...cat, houses: cat.houses.filter((h) => h.id !== id) }
      : tab === "set"
      ? { ...cat, sets: (cat.sets || []).filter((x) => x.id !== id) }
      : { ...cat, items: cat.items.filter((it) => it.id !== id) };
    onPut(kind, id, null);
    onSaveCatalog(next);
  };

  return (
    <>
      <div className="hl-seg">
        {ART_TABS.map((t) => (
          <button key={t.id} className={tab === t.id ? "on" : ""} onClick={() => setTab(t.id)}>{t.n}</button>
        ))}
      </div>

      <div className="hl-guideBox">
        <AngleGuide kind={tab} />
        <p className="hl-guideText">{T.guide}</p>
        <p className="hl-guideText dim">배경이 비치는 <b>투명 PNG</b>를 권해요. 640px로 자동으로 줄여 담습니다.</p>
      </div>

      <div className="hl-sectionHead"><span>{T.n}</span><span className="hl-mono hl-dim">{list.length}</span></div>
      <div className="hl-artList">
        {list.map((it) => {
          const has = ART.idx.has(artKey(kind, it.id));
          return (
            <div key={it.id} className="hl-artRow">
              <div className={`hl-artThumb ${kind === "set" ? "wide" : ""}`}>
                {kind === "house" ? <HouseArt id={it.id} />
                  : kind === "set" ? <SetThumb set={it} id={it.id} />
                  : <ItemArt id={it.id} />}
              </div>
              <div className="hl-artBody">
                <div className="hl-nameRow">
                  <input className="hl-in" value={names[it.id] ?? it.n} onChange={(e) => setNames({ ...names, [it.id]: e.target.value })} />
                  <button className="hl-add" onClick={() => rename(it.id)}>저장</button>
                </div>
                <div className="hl-mono hl-invMeta" style={kind === "set" ? {} : { color: RARITY[it.tier].c }}>
                  {kind === "set" ? `${(it.cost || 0).toLocaleString()}P` : RARITY[it.tier].n}
                  {" · "}{has ? "올린 그림" : it.custom ? "그림 없음" : "그려둔 도형"}
                </div>
                <ImagePicker has={has} busy={busy === it.id}
                  onPicked={(f, err) => upload(it.id, f, err)}
                  onClear={() => onPut(kind, it.id, null)} />
                {it.custom && <button className="hl-del" onClick={() => drop(it.id)}>삭제</button>}
              </div>
            </div>
          );
        })}
      </div>

      <div className="hl-sectionHead"><span>새로 추가</span></div>
      <div className="hl-addRow">
        <input className="hl-in" placeholder={tab === "house" ? "집 이름" : tab === "set" ? "세트 이름" : "소품 이름"}
          value={nw.n} onChange={(e) => setNw({ ...nw, n: e.target.value })} />
        {tab === "set" && <input className="hl-in hl-inSm hl-mono" inputMode="numeric" placeholder="값"
          value={nw.cost} onChange={(e) => setNw({ ...nw, cost: e.target.value })} />}
        <button className="hl-add" onClick={add}>추가</button>
      </div>
      {tab !== "set" && (
        <div className="hl-chips" style={{ marginTop: 8 }}>
          {Object.entries(RARITY).map(([k, r]) => (
            <button key={k} className={`hl-chip2 ${nw.tier === k ? "on" : ""}`} onClick={() => setNw({ ...nw, tier: k })}>
              <span className="hl-dotSm" style={{ background: r.c }} />{r.n}
            </button>
          ))}
        </div>
      )}
      <p className="hl-fine hl-left">
        직접 추가한 것은 그림을 올려야 보여요. 추가하면 바로 뽑기 목록에 들어갑니다.
        남이 그린 그림은 저작권 문제가 생길 수 있으니 직접 만든 그림이나 자유 이용이 허락된 그림만 써주세요.
      </p>
    </>
  );
}

function CharBoard({ members, rooms, onSetChar, onPut, flash }) {
  const [sel, setSel] = useState(members[0]?.id || "");
  const [busy, setBusy] = useState(false);
  const m = members.find((x) => x.id === sel);
  const ch = rooms?.[sel]?.ch || (m?.role === "owner" ? OWNER_CH : DEFAULT_CH);
  const has = ART.idx.has(artKey("char", sel));

  const upload = async (file, err) => {
    if (err) { flash(err); return; }
    if (!file) return;
    setBusy(true);
    try { await onPut("char", sel, await shrinkArt(file, 560)); }
    catch (e) { flash(`그림을 읽지 못했어요: ${e?.message || e}`); }
    setBusy(false);
  };

  return (
    <>
      <div className="hl-scopeLabel">누구</div>
      <div className="hl-chips">
        {members.map((x) => (
          <button key={x.id} className={`hl-chip2 ${sel === x.id ? "on" : ""}`} onClick={() => setSel(x.id)}>
            {x.name}<span className="hl-mono hl-chipPt">{x.gen}기</span>
          </button>
        ))}
      </div>

      {m && (
        <>
          <div className="hl-charBoard">
            <div className="hl-charBig">
              <Art kind="char" id={sel}><Character ch={ch} /></Art>
            </div>
            <div className="hl-charSide">
              <div className="hl-setName">{m.name}</div>
              <div className="hl-mono hl-invMeta">{ROLES[m.role]} · {partOf(m.part).n}</div>
              <ImagePicker has={has} busy={busy} onPicked={upload} onClear={() => onPut("char", sel, null)} />
            </div>
          </div>
          <p className="hl-fine hl-left" style={{ marginTop: 8 }}>
            전신이 나오는 세로로 긴 <b>투명 PNG</b>가 좋아요. 발이 그림 아래 가운데에 오도록 맞춰주세요.
            그림을 올리면 아래 설정 대신 그 그림이 방과 목록에 나옵니다.
          </p>

          <div className={has ? "hl-dim2" : ""}>
            <div className="hl-scopeLabel">머리</div>
            <div className="hl-chips">
              {HAIRS.map((h) => (
                <button key={h.id} className={`hl-chip2 ${ch.hair === h.id ? "on" : ""}`} onClick={() => onSetChar(sel, { hair: h.id })}>{h.n}</button>
              ))}
            </div>
            <div className="hl-scopeLabel">머리색</div>
            <div className="hl-chips">
              {HAIRC.map((c, i) => (
                <button key={c} className={`hl-dot ${(ch.hc ?? 0) === i ? "on" : ""}`} style={{ background: c }}
                  onClick={() => onSetChar(sel, { hc: i })} aria-label={`머리색 ${i + 1}`} />
              ))}
            </div>
            <div className="hl-scopeLabel">옷</div>
            <div className="hl-chips">
              {FITS.map((f) => (
                <button key={f.id} className={`hl-chip2 ${ch.fit === f.id ? "on" : ""}`} onClick={() => onSetChar(sel, { fit: f.id })}>{f.n}</button>
              ))}
            </div>
            <div className="hl-scopeLabel">피부톤</div>
            <div className="hl-chips">
              {SKINS.map((c, i) => (
                <button key={c} className={`hl-dot ${(ch.sk ?? 0) === i ? "on" : ""}`} style={{ background: c }}
                  onClick={() => onSetChar(sel, { sk: i })} aria-label={`피부톤 ${i + 1}`} />
              ))}
            </div>
          </div>
        </>
      )}
    </>
  );
}

function LinkStatus() {
  const [d, setD] = useState(() => (typeof window !== "undefined" ? window.HANUL_DIAG : null));
  useEffect(() => {
    const t = setInterval(() => setD(window.HANUL_DIAG ? { ...window.HANUL_DIAG } : null), 700);
    return () => clearInterval(t);
  }, []);

  if (!d) return (
    <div className="hl-empty">
      지금은 미리보기로 열려 있어요.<br />배포한 주소에서 열면 연결 상태가 여기 나옵니다.
    </div>
  );

  const state = !d.hasUrl || !d.hasKey ? "none" : d.ok === true ? "ok" : d.ok === false ? "fail" : "wait";
  const label = { none: "이 기기에만 저장", ok: "모두와 연결됨", fail: "연결 실패", wait: "확인 중…" }[state];
  const color = { none: "#E9A13B", ok: "#6FBF73", fail: "#E8503A", wait: "#8A97A3" }[state];
  const sub = {
    ok: "모든 부원이 같은 기록을 봅니다.",
    none: "config.js가 비어 있어요. 지금은 이 기기 안에만 저장됩니다.",
    fail: "주소와 키는 넣었지만 서버가 거절했어요.",
    wait: "서버에 연결해 보는 중이에요.",
  }[state];

  return (
    <>
      <div className="hl-linkCard" style={{ borderColor: color }}>
        <span className="hl-linkDot" style={{ background: color }} />
        <div>
          <div className="hl-linkState" style={{ color }}>{label}</div>
          <div className="hl-linkSub">{sub}</div>
        </div>
      </div>

      <div className="hl-scopeLabel">지금 설정</div>
      <div className="hl-list">
        <div className="hl-linkRow2"><span className="hl-attNm">주소</span><span className="hl-mono">{d.url || "비어 있음"}</span></div>
        <div className="hl-linkRow2"><span className="hl-attNm">키</span><span className="hl-mono">{d.keyHead || "비어 있음"}</span></div>
      </div>

      {d.error && (
        <>
          <div className="hl-scopeLabel">서버가 알려준 이유</div>
          <div className="hl-errBox">{d.error}</div>
        </>
      )}

      <div className="hl-scopeLabel">확인할 것</div>
      <div className="hl-guideBox">
        <p className="hl-guideText">
          <b>1.</b> config.js의 따옴표 <b>사이</b>에 주소와 키를 넣었나요<br />
          <b>2.</b> 고친 뒤 폴더를 <b>다시 올렸나요</b><br />
          <b>3.</b> Supabase에서 SQL을 실행해 kv 표를 만들었나요<br />
          <b>4.</b> 키가 <b>Publishable</b>(또는 legacy anon)인가요 — secret 키는 쓰면 안 돼요
        </p>
      </div>
    </>
  );
}

const PROP_IMG = new Map();
const PROP_CATS = [
  { id: "prop", n: "소품" }, { id: "costume", n: "의상" }, { id: "set", n: "무대 · 세트" },
  { id: "light", n: "조명 · 음향" }, { id: "makeup", n: "분장" }, { id: "etc", n: "기타" },
];
const PROP_STATES = [
  { id: "ok", n: "정상", c: "#6FBF73" },
  { id: "fix", n: "수리 필요", c: "#E9A13B" },
  { id: "lost", n: "분실", c: "#E8503A" },
];

function PropImage({ id }) {
  const [src, setSrc] = useState(PROP_IMG.get(id) || null);
  useEffect(() => {
    if (src) return;
    let live = true;
    (async () => {
      try { const r = await window.storage.get(`hanul:art:prop:${id}`, true); PROP_IMG.set(id, r.value); if (live) setSrc(r.value); }
      catch (e) {}
    })();
    return () => { live = false; };
  }, [id, src]);
  return src ? <img className="hl-propImg" src={src} alt="" /> : <div className="hl-propImg hl-imgLoad" />;
}

function PropList({ props, canEdit, meId, onAdd, onEdit, onDrop, flash }) {
  const [cat, setCat] = useState("all");
  const [open, setOpen] = useState(false);
  const [f, setF] = useState({ n: "", cat: "prop", qty: "1", where: "", note: "", state: "ok" });
  const [img, setImg] = useState(null);
  const [busy, setBusy] = useState(false);

  const list = props.filter((p) => cat === "all" || p.cat === cat)
    .sort((a, b) => b.at - a.at);
  const total = props.reduce((n, p) => n + (Number(p.qty) || 0), 0);

  const pick = async (file, err) => {
    if (err) { flash(err); return; }
    if (!file) return;
    try { setImg(await shrinkArt(file, 720)); } catch (e) { flash(`그림을 읽지 못했어요: ${e?.message || e}`); }
  };
  const save = async () => {
    if (!f.n.trim()) { flash("물품 이름을 적어주세요."); return; }
    setBusy(true);
    await onAdd({ ...f, n: f.n.trim(), qty: Number(f.qty) || 1 }, img);
    setF({ n: "", cat: f.cat, qty: "1", where: "", note: "", state: "ok" });
    setImg(null); setBusy(false); setOpen(false);
    flash(`물품을 등록했어요. +${PROP_PT}P`);
  };

  return (
    <>
      <div className="hl-sectionHead">
        <span>무대 물품</span>
        <span className="hl-mono hl-dim">{props.length}종 · 총 {total}개</span>
      </div>

      <div className="hl-chips">
        <button className={`hl-chip2 ${cat === "all" ? "on" : ""}`} onClick={() => setCat("all")}>전체</button>
        {PROP_CATS.map((c) => (
          <button key={c.id} className={`hl-chip2 ${cat === c.id ? "on" : ""}`} onClick={() => setCat(c.id)}>{c.n}</button>
        ))}
      </div>

      <button className="hl-cta hl-block" onClick={() => setOpen(!open)}>
        {open ? "닫기" : "물품 등록하기"}
        {!open && <span className="hl-mono hl-costTag">+{PROP_PT}P</span>}
      </button>
      {!open && <p className="hl-fine hl-left">물품을 하나 등록할 때마다 {PROP_PT}P를 받아요. 사진까지 넣어주면 더 좋아요.</p>}

      {open && (
        <div className="hl-propForm">
          <div className="hl-addRow">
            <input className="hl-in" placeholder="물품 이름 (예: 접이의자)" value={f.n} onChange={(e) => setF({ ...f, n: e.target.value })} />
            <input className="hl-in hl-inSm hl-mono" inputMode="numeric" placeholder="수량" value={f.qty} onChange={(e) => setF({ ...f, qty: e.target.value })} />
          </div>
          <div className="hl-scopeLabel">분류</div>
          <div className="hl-chips">
            {PROP_CATS.map((c) => (
              <button key={c.id} className={`hl-chip2 ${f.cat === c.id ? "on" : ""}`} onClick={() => setF({ ...f, cat: c.id })}>{c.n}</button>
            ))}
          </div>
          <div className="hl-scopeLabel">상태</div>
          <div className="hl-chips">
            {PROP_STATES.map((st) => (
              <button key={st.id} className={`hl-chip2 ${f.state === st.id ? "on" : ""}`} onClick={() => setF({ ...f, state: st.id })}>
                <span className="hl-dotSm" style={{ background: st.c }} />{st.n}
              </button>
            ))}
          </div>
          <input className="hl-in hl-block" placeholder="보관 위치 (예: 동아리방 창고 2번 선반)" value={f.where} onChange={(e) => setF({ ...f, where: e.target.value })} />
          <input className="hl-in hl-block" placeholder="메모 (선택)" value={f.note} onChange={(e) => setF({ ...f, note: e.target.value })} />

          <div className="hl-scopeLabel">사진</div>
          {img && <div className="hl-preview"><img src={img} alt="" /><button onClick={() => setImg(null)}>빼기</button></div>}
          <ImagePicker has={!!img} busy={busy} onPicked={pick} onClear={() => setImg(null)} />
          <button className="hl-cta hl-block" disabled={busy} onClick={save}>{busy ? "올리는 중…" : "등록하기"}</button>
        </div>
      )}

      {list.length === 0 ? (
        <div className="hl-empty">아직 등록된 물품이 없어요.<br />위에서 하나씩 등록하면 포인트도 받아요.</div>
      ) : (
        <div className="hl-propGrid">
          {list.map((p) => {
            const st = PROP_STATES.find((x) => x.id === p.state) || PROP_STATES[0];
            return (
              <div key={p.id} className="hl-propCard">
                <div className="hl-propThumb">
                  {p.hasImg ? <PropImage id={p.id} /> : <span className="hl-propNo">사진 없음</span>}
                  <span className="hl-propState" style={{ background: st.c }}>{st.n}</span>
                </div>
                <div className="hl-propBody">
                  <div className="hl-propName">{p.n}<span className="hl-mono hl-propQty">×{p.qty}</span></div>
                  <div className="hl-mono hl-propMeta">{PROP_CATS.find((c) => c.id === p.cat)?.n}</div>
                  {p.where && <div className="hl-propWhere">📍 {p.where}</div>}
                  {p.note && <div className="hl-propNote">{p.note}</div>}
                  <div className="hl-mono hl-propMeta">{p.by} · {when(p.at)}</div>
                  {(canEdit || p.byId === meId) && (
                    <div className="hl-propBtns">
                      {PROP_STATES.map((x) => (
                        <button key={x.id} className={`hl-attBtn tiny ${p.state === x.id ? "on" : ""}`}
                          onClick={() => onEdit(p.id, { state: x.id })}>{x.n}</button>
                      ))}
                      <button className="hl-del" onClick={() => onDrop(p.id)}>삭제</button>
                    </div>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </>
  );
}

/* 역할마다 다른 튜토리얼 — 공통 4장 + 역할별 추가 */
const TOUR_BASE = [
  { i: "👋", t: "한울 활동일지에 오신 걸 환영해요",
    d: "연습하고, 기록하고, 포인트를 모아 내 무대를 꾸미는 앱이에요.\n앞으로 다섯 장 정도만 보면 다 알 수 있어요." },
  { i: "🏠", t: "아래 가운데 집이 마이룸이에요",
    d: "포인트로 소품을 뽑아 내 무대를 꾸며요.\n집 바깥에서 문을 누르면 안으로 들어가요." },
  { i: "📅", t: "기록 탭은 세 칸이에요",
    d: "달력에서 일정과 출석을 보고,\n일지 쓰기로 오늘 한 일을 남기고,\n내 일지에서 지난 기록을 고치거나 지워요." },
  { i: "😊", t: "오늘의 컨디션을 눌러주세요",
    d: "홈에서 이모지 하나만 누르면 돼요.\n달력에 남아서 이번 주 팀 분위기가 한눈에 보여요." },
  { i: "✉️", t: "오른쪽 아래 편지 버튼",
    d: "익명 건의함과 익명 편지를 보낼 수 있어요.\n누가 보냈는지는 저장되지 않아요." },
];
const TOUR_ROLE = {
  member: [
    { i: "📝", t: "일지는 개인 기록으로 써요",
      d: "쓰면 바로 피드에 올라가고 모두가 볼 수 있어요.\n포인트는 붙지 않지만 댓글로 이야기할 수 있어요." },
    { i: "🪙", t: "포인트는 이렇게 쌓여요",
      d: "출석 +10P · 지각 −5P · 결석 −15P\n우리 팀장이 올린 일지가 승인되면 팀원 모두 +15P" },
  ],
  lead: [
    { i: "📣", t: "팀장은 팀 활동일지를 씁니다",
      d: "일지 쓰기에서 '팀 활동일지'를 고르세요.\n집행부가 승인하면 같은 파트 팀원 전체에게 15P가 들어가요." },
    { i: "⏰", t: "팀장이 안 쓰면 팀이 손해예요",
      d: "팀 포인트는 팀장 일지가 승인될 때만 나갑니다.\n연습이 끝나면 잊지 말고 한 줄 남겨주세요." },
  ],
  exec: [
    { i: "✅", t: "일지 승인은 집행부의 일이에요",
      d: "더보기 → 일지 승인에서 확인해요.\n팀장 일지를 승인하면 그 파트 전원에게 포인트가 나갑니다." },
    { i: "📋", t: "출석은 연습이 끝나고 바로",
      d: "더보기 → 출석 체크에서 날짜를 고르고 눌러요.\n같은 버튼을 다시 누르면 취소돼요." },
    { i: "📢", t: "공지와 숙제를 올릴 수 있어요",
      d: "더보기 → 공지 · 숙제에서 올리면\n모두의 홈 화면 맨 위에 뜹니다. 오늘의 한마디도 여기서." },
    { i: "🎭", t: "무대 물품도 관리해요",
      d: "더보기 → 무대 물품에서 소품과 의상을 사진과 함께 기록해요.\n수리가 필요하거나 잃어버린 것도 표시할 수 있어요." },
  ],
  owner: [
    { i: "🔑", t: "관리자만 할 수 있는 일",
      d: "더보기 아래쪽에 관리자 전용 칸이 있어요.\n포인트 지급 · 역할 변경 · 권한 코드 · 그림 교체." },
    { i: "🎟️", t: "권한 코드를 관리하세요",
      d: "집행부와 팀장은 가입할 때 코드를 넣어야 해요.\n코드가 새면 더보기 → 권한 코드에서 바꾸면 됩니다." },
    { i: "🖼️", t: "그림을 직접 올릴 수 있어요",
      d: "집 외형 · 소품 · 세트 · 캐릭터 전부\n더보기 → 그림 · 이름 관리에서 사진으로 바꿀 수 있어요." },
    { i: "🔗", t: "연결 상태를 꼭 확인하세요",
      d: "더보기 → 연결 상태가 '모두와 연결됨'이어야\n부원들이 같은 기록을 봅니다." },
  ],
};

function Tour({ role, name, onClose }) {
  const pages = [...TOUR_BASE, ...(TOUR_ROLE[role] || TOUR_ROLE.member)];
  const [i, setI] = useState(0);
  const last = i === pages.length - 1;
  const p = pages[i];

  return (
    <div className="hl-tourWrap">
      <div className="hl-tourCard" key={i}>
        <div className="hl-tourTop">
          <span className="hl-mono hl-tourNo">{String(i + 1).padStart(2, "0")} / {String(pages.length).padStart(2, "0")}</span>
          <button className="hl-tourSkip" onClick={onClose}>건너뛰기</button>
        </div>
        <div className="hl-tourIcon">{p.i}</div>
        <div className="hl-tourTitle">{i === 0 ? `${name}님, ${p.t}` : p.t}</div>
        <p className="hl-tourDesc">{p.d}</p>
        <div className="hl-tourDots">
          {pages.map((_, k) => <i key={k} className={k === i ? "on" : ""} onClick={() => setI(k)} />)}
        </div>
        <div className="hl-dayBtns">
          {i > 0 && <button className="hl-cta done" onClick={() => setI(i - 1)}>이전</button>}
          <button className="hl-cta" onClick={() => (last ? onClose() : setI(i + 1))}>
            {last ? "시작하기" : "다음"}
          </button>
        </div>
      </div>
    </div>
  );
}

function Fortune({ me, birth, today: ds, onSetBirth, onPay, available, saved, onSave }) {
  const [edit, setEdit] = useState(false);
  const [b, setB] = useState(birth || "");
  const [busy, setBusy] = useState(false);
  const [saving, setSaving] = useState(false);
  const done = saved && saved.date === ds;

  /* 서버에 생일이 들어오면 입력칸을 닫는다 */
  useEffect(() => { if (birth) { setB(birth); setEdit(false); } }, [birth]);

  const keepBirth = async () => {
    if (!b || saving) return;
    setSaving(true);
    let ok = false;
    try { ok = (await onSetBirth(b)) !== null; } catch (e) {}
    setSaving(false);
    if (ok) setEdit(false);   /* 저장이 안 됐으면 입력칸을 그대로 둔다 */
  };

  const open = async () => {
    if (busy || !birth) return;
    if (available < FORTUNE_COST) return;
    setBusy(true);
    try {
      const f = readFortune(birth, ds, me.id);
      const paid = await onPay();
      if (paid === null) { setBusy(false); return; }   /* 연결이 끊겼으면 포인트도 안 빠진다 */
      await onSave({ date: ds, ...f });
      A.sfx("reward");
    } catch (e) {}
    setBusy(false);
  };

  if (!birth || edit) {
    return (
      <section className="hl-fortune">
        <div className="hl-fortTop">
          <span className="hl-fortIcon">🔮</span>
          <div>
            <div className="hl-fortTitle">오늘의 운세</div>
            <div className="hl-fortSub">생일을 한 번만 알려주세요</div>
          </div>
          {birth && <button className="hl-fortEdit" onClick={() => setEdit(false)}>취소</button>}
        </div>
        <input className="hl-in hl-block hl-mono" type="date" value={b} onChange={(e) => setB(e.target.value)} />
        {b && (
          <p className="hl-fortPeek">
            {zodiacOf(Number(b.split("-")[1]), Number(b.split("-")[2]))} · {animalOf(Number(b.split("-")[0]))}
          </p>
        )}
        <button className="hl-cta hl-block" disabled={!b || saving} onClick={keepBirth}>
          {saving ? "저장 중…" : "저장하기"}
        </button>
        <p className="hl-fine hl-left">별자리와 띠를 계산하는 데만 씁니다. 언제든 다시 바꿀 수 있어요.</p>
      </section>
    );
  }

  const [by, bm, bd] = birth.split("-").map(Number);

  if (!done) {
    return (
      <section className="hl-fortune">
        <div className="hl-fortTop">
          <span className="hl-fortIcon">🔮</span>
          <div>
            <div className="hl-fortTitle">오늘의 운세</div>
            <div className="hl-fortSub">{zodiacOf(bm, bd)} · {animalOf(by)}</div>
          </div>
          <button className="hl-fortEdit" onClick={() => setEdit(true)}>생일 수정</button>
        </div>
        <button className="hl-cta hl-block" disabled={busy || available < FORTUNE_COST} onClick={open}>
          {busy ? "펼치는 중…" : available < FORTUNE_COST ? `포인트가 ${FORTUNE_COST - available}P 모자라요` : "운세 펼치기"}
          {available >= FORTUNE_COST && !busy && <span className="hl-mono hl-costTag">{FORTUNE_COST}P</span>}
        </button>
        <p className="hl-fine hl-left">하루에 한 번만 볼 수 있어요. 같은 날은 몇 번을 봐도 같은 결과입니다.</p>
      </section>
    );
  }

  const f = saved;
  const col = typeof f.color === "object" ? f.color : { n: f.color || "앰버", c: "#E9A13B" };
  return (
    <section className="hl-fortune done">
      <div className="hl-fortTop">
        <span className="hl-fortIcon">🔮</span>
        <div>
          <div className="hl-fortTitle">오늘의 운세</div>
          <div className="hl-fortSub">{f.zodiac} · {f.animal}</div>
        </div>
        <span className="hl-fortRank" style={{ background: f.rank.c }}>{f.rank.n}</span>
      </div>

      <div className="hl-fortBar"><span className="hl-fortFill" style={{ width: `${f.score}%`, background: f.rank.c }} /></div>
      <div className="hl-mono hl-fortScore" style={{ color: f.rank.c }}>{f.score}</div>

      <div className="hl-fortHead">{f.head}</div>
      <p className="hl-fortBody">{f.body}</p>

      {f.zText && (
        <div className="hl-fortSect">
          <div className="hl-fortSectHead"><span className="hl-fortDot" style={{ background: f.rank.c }} />{f.zodiac}</div>
          <p className="hl-fortSectBody">{f.zText}</p>
        </div>
      )}
      {f.aText && (
        <div className="hl-fortSect">
          <div className="hl-fortSectHead"><span className="hl-fortDot" style={{ background: col.c }} />{f.animal}</div>
          <p className="hl-fortSectBody">{f.aText}</p>
        </div>
      )}

      <div className="hl-fortLucks">
        <div className="hl-luck">
          <span className="hl-luckK">행운의 색</span>
          <span className="hl-luckSwatch" style={{ background: col.c }} />
          <span className="hl-luckV">{col.n}</span>
        </div>
        <div className="hl-luck">
          <span className="hl-luckK">행운의 소품</span>
          <span className="hl-luckV big">{f.prop || "—"}</span>
        </div>
        <div className="hl-luck">
          <span className="hl-luckK">어울리는 자리</span>
          <span className="hl-luckV big">{f.place || "—"}</span>
        </div>
      </div>
    </section>
  );
}

const CUE_PAY = [
  { min: 15, pt: 60 }, { min: 11, pt: 35 }, { min: 8, pt: 18 }, { min: 5, pt: 8 }, { min: 3, pt: 3 },
];
const cuePay = (n) => (CUE_PAY.find((x) => n >= x.min)?.pt) || 0;
/* 주간 순위별 상금 — 1위부터 차례로, 그 뒤는 0P */
const WEEK_PRIZES = [2000, 500, 300, 200, 100, 50];
const weekPrize = (rank) => WEEK_PRIZES[rank] || 0;
const WEEK_MIN = 16;       /* 1위가 이 콤보를 넘겨야 상금이 나간다 */

/* 홀짝 — 하루 판수와 한 판 한도를 둔다 */
const OE_MIN = 5;          /* 한 판에 걸 수 있는 최소 */
/* 걸 수 있는 방식 — 맞출 확률이 낮을수록 배수가 크다 */
/* 연승 도전 — 이어갈수록 배수가 크게 뛴다. 한 번이라도 틀리면 전부 잃는다 */
const OE_STREAK = [2, 5, 12, 30, 80];
const streakMult = (n) => OE_STREAK[Math.min(n, OE_STREAK.length - 1)];

/* 0은 꽝 — 어느 쪽에도 속하지 않는다. 0~10 열한 칸에서 하나가 나온다. */
const OE_FACES = 11;
const OE_MODES = [
  { id: "oddeven", n: "홀 · 짝",   d: "둘 중 하나",       mult: 2, odds: "45%" },
  { id: "half",    n: "큰 · 작은", d: "1~5 / 6~10",       mult: 2, odds: "45%" },
  { id: "trio",    n: "세 칸 찍기", d: "숫자 세 개",       mult: 3, odds: "27%" },
  { id: "exact",   n: "숫자 하나", d: "딱 맞추면 아홉 배", mult: 9, odds: "9%" },
];
/* 연승 — 받지 않고 이어가면 배수가 불어난다. 틀리면 전부 잃는다. */
const OE_STREAK_MAX = 4;

/* ── 한울 증권거래소 ──────────────────────────
   실제 시세가 아니라 앱이 만들어 내는 가상의 장.
   모두가 같은 시각에 같은 값을 보도록 시간만으로 계산한다. */
const TICK_MS = 5 * 60 * 1000;          /* 5분마다 한 칸 움직인다 */
const STOCKS = [
  { code: "005930", n: "다빈컴퍼니",        en: "DABIN Co., Ltd.",       sector: "지주", base: 1200, vol: 1.10, mkt: "KOSPI" },
  { code: "012450", n: "정세은홀딩스",      en: "SEEUN Holdings",        sector: "기획", base: 860,  vol: 0.95, mkt: "KOSPI" },
  { code: "042660", n: "한정연중공업",      en: "HJY Heavy Industries",  sector: "무대", base: 1540, vol: 0.78, mkt: "KOSPI" },
  { code: "352820", n: "도연엔터테인먼트",  en: "DOYEON ENT",            sector: "연예", base: 2100, vol: 1.42, mkt: "KOSPI" },
  { code: "035760", n: "서정훈제작소",      en: "JH Works",              sector: "제작", base: 640,  vol: 1.05, mkt: "KOSDAQ" },
  { code: "247540", n: "신다미디자인랩",    en: "DAMI Design Lab",       sector: "디자인", base: 980, vol: 1.22, mkt: "KOSDAQ" },
  { code: "086520", n: "신진희물산",        en: "SJH Trading",           sector: "유통", base: 420,  vol: 0.70, mkt: "KOSDAQ" },
  { code: "091990", n: "권영준조명",        en: "YJ Lighting",           sector: "조명", base: 1380, vol: 1.08, mkt: "KOSPI" },
  { code: "000660", n: "한울전자",          en: "HANUL Electronics",     sector: "음향", base: 3200, vol: 0.58, mkt: "KOSPI" },
  { code: "196170", n: "커튼콜바이오",      en: "CurtainCall Bio",       sector: "제약", base: 5400, vol: 1.85, mkt: "KOSDAQ" },
  { code: "251270", n: "스포트라이트소프트", en: "Spotlight Soft",       sector: "IT",   base: 2650, vol: 1.35, mkt: "KOSDAQ" },
  { code: "097950", n: "프롭앤씨어터",      en: "Prop & Theater F&B",    sector: "식품", base: 760,  vol: 0.66, mkt: "KOSPI" },
];

/* ── 시장 규칙 ──────────────────────────────
   호가 단위 · 상하한가 · 거래시간 · 수수료를 실제 방식대로 둔다 */
const FEE_RATE = 0.00015;   /* 매수·매도 수수료 0.015% */
const TAX_RATE = 0.0018;    /* 매도 시 거래세 0.18% */
const LIMIT = 0.30;         /* 상하한가 ±30% */
const LEV_MAX = 3;          /* 최대 배수 */
const LIQ_AT = 0.90;        /* 증거금의 90%를 잃으면 자동 청산 */

/* 포지션 손익 — 상승(long)이면 오를 때, 하락(short)이면 내릴 때 번다 */
const posPL = (pos, price) => {
  const move = (price - pos.entry) / pos.entry;
  const dir = pos.side === "short" ? -1 : 1;
  return Math.round(pos.margin * move * dir * pos.lev);
};
const posRate = (pos, price) => {
  const move = (price - pos.entry) / pos.entry;
  return move * (pos.side === "short" ? -1 : 1) * pos.lev * 100;
};
/* 청산가 — 여기에 닿으면 증거금이 거의 사라진다 */
const liqPrice = (pos) => {
  const d = (LIQ_AT / pos.lev) * pos.entry;
  return Math.round(pos.side === "short" ? pos.entry + d : pos.entry - d);
};

/* 호가 단위 — 가격대마다 다르다 */
const tickSize = (p) =>
  p < 2000 ? 1 : p < 5000 ? 5 : p < 20000 ? 10 : p < 50000 ? 50 : 100;
const toTick = (p) => { const t = tickSize(p); return Math.max(t, Math.round(p / t) * t); };

/* 장 운영 시간 — 평일 09:00~15:30 (한국시간) */
const marketState = () => {
  const now = new Date();
  const kst = new Date(now.getTime() + (now.getTimezoneOffset() * 60000) + 9 * 3600000);
  const day = kst.getDay(), mins = kst.getHours() * 60 + kst.getMinutes();
  if (day === 0 || day === 6) return { open: false, why: "주말 휴장" };
  if (mins < 540) return { open: false, why: "장 시작 전 (09:00 개장)" };
  if (mins >= 1140) return { open: false, why: "장 마감 (19:00)" };
  if (mins >= 1120) return { open: true, why: "장 마감 임박", closing: true };
  return { open: true, why: "정규장" };
};

const OLD_CODE = { DB: "005930", SE: "012450", JY: "042660", DY: "352820", JH: "035760",
  DM: "247540", SJ: "086520", YJ: "091990", HW: "000660", CT: "196170" };
const stockOf = (c) => STOCKS.find((x) => x.code === c)
  || STOCKS.find((x) => x.code === OLD_CODE[c]) || null;

/* 시세 — 여러 주기의 물결을 겹쳐 자연스럽게 흔들리게 만든다 */
/* 관리자가 손본 값 — 종목별 보정 배율. 앱 전체가 같은 값을 쓴다. */
let MKT_ADJ = {};
const setAdj = (o) => { MKT_ADJ = o || {}; };
const adjOf = (code, tick) => {
  const a = MKT_ADJ[code];
  if (!a) return 1;
  /* 손본 시점부터 서서히 스며들고, 하루(288칸)에 걸쳐 옅어진다 */
  const age = tick - (a.tick || 0);
  if (age < 0) return 1;
  const fade = Math.max(0, 1 - age / 288);
  const ramp = Math.min(1, age / 3 + 0.34);
  return 1 + (a.pct / 100) * fade * ramp;
};

const priceAt = (st, tick) => {
  const h = seedOf(st.code);
  const p1 = 41 + (h % 17), p2 = 13 + ((h >> 4) % 11), p3 = 137 + ((h >> 8) % 61);
  const f1 = ((h >> 3) % 100) / 100, f2 = ((h >> 7) % 100) / 100, f3 = ((h >> 11) % 100) / 100;
  const wave =
    0.16 * Math.sin(tick / p1 + f1 * 6.28) +
    0.07 * Math.sin(tick / p2 + f2 * 6.28) +
    0.24 * Math.sin(tick / p3 + f3 * 6.28) +
    0.04 * Math.sin(tick / 3.1 + f1 * 3.14);
  const jolt = (((seedOf(`${st.code}:${tick}`) % 1000) / 1000) - 0.5) * 0.035;
  const v = 1 + (wave + jolt) * st.vol;
  const raw = Math.max(10, st.base * v);
  /* 전일 종가 대비 ±30%를 넘지 않게 자르고, 호가 단위에 맞춘다 */
  const prevC = prevClose(st, tick);
  const moved = raw * adjOf(st.code, tick);
  const capped = Math.min(prevC * (1 + LIMIT), Math.max(prevC * (1 - LIMIT), moved));
  return toTick(capped);
};
/* 전일 종가 — 하루는 288칸(5분×288=24시간) */
const DAY_TICKS = 288;
const prevClose = (st, tick) => {
  const d = Math.floor(tick / DAY_TICKS) - 1;
  const h = seedOf(`${st.code}:d${d}`);
  const wave = 0.16 * Math.sin(d * 1.7) + 0.09 * Math.sin(d * 0.41) + ((h % 1000) / 1000 - 0.5) * 0.12;
  return Math.max(10, toTick(st.base * (1 + wave * st.vol)));
};
const nowTick = () => Math.floor(Date.now() / TICK_MS);
const priceNow = (st) => priceAt(st, nowTick());
const changeOf = (st) => {
  const t = nowTick();
  const a = priceAt(st, t), b = prevClose(st, t);      /* 전일 종가 대비 */
  const rate = b ? ((a - b) / b) * 100 : 0;
  return {
    price: a, prev: b, diff: a - b, rate,
    upper: rate >= LIMIT * 100 - 0.05,
    lower: rate <= -LIMIT * 100 + 0.05,
  };
};

/* 이번 주가 몇 번째 주인지 — 월요일 기준 */
const weekKey = (d = new Date()) => {
  const x = new Date(d);
  const day = (x.getDay() + 6) % 7;          /* 월=0 */
  x.setDate(x.getDate() - day);
  return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}-${String(x.getDate()).padStart(2, "0")}`;
};
/* 이번 주가 끝나기까지 남은 시간 */
const weekLeft = () => {
  const now = new Date();
  const end = new Date(weekKey());
  end.setDate(end.getDate() + 7);
  const ms = end - now;
  const d = Math.floor(ms / 86400000);
  const h = Math.floor((ms % 86400000) / 3600000);
  return d > 0 ? `${d}일 ${h}시간 남음` : `${h}시간 남음`;
};

function Sparkline({ st, n = 40 }) {
  const t = nowTick();
  const pts = Array.from({ length: n }, (_, i) => priceAt(st, t - (n - 1 - i)));
  const lo = Math.min(...pts), hi = Math.max(...pts), sp = hi - lo || 1;
  const up = pts[n - 1] >= pts[0];
  const d = pts.map((p, i) => `${(i / (n - 1)) * 100},${34 - ((p - lo) / sp) * 32}`).join(" ");
  const col = up ? "#D94B5A" : "#3E7BD9";   /* 오르면 붉게, 내리면 파랗게 */
  return (
    <svg viewBox="0 0 100 36" preserveAspectRatio="none" className="hl-spark">
      <polyline points={d} fill="none" stroke={col} strokeWidth="1.6"
        vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
    </svg>
  );
}

/* 큰 그래프 — 기간을 골라 볼 수 있다 */
/* 주식을 처음 해보는 사람을 위한 안내 */
const MKT_GUIDE = [
  { i: "📈", t: "주식이 뭔가요",
    d: "회사의 조각을 사두는 거예요.\n싸게 사서 비싸게 팔면 그 차액이 이익이 됩니다.\n여기 있는 회사는 전부 가짜이고, 진짜 돈은 오가지 않아요." },
  { i: "🔴", t: "빨강이 오른 거예요",
    d: "한국 증시는 오르면 빨강, 내리면 파랑이에요.\n▲는 올랐다는 뜻, ▼는 내렸다는 뜻입니다.\n퍼센트는 어제 마감 가격과 비교한 거예요." },
  { i: "🕘", t: "장이 열려야 살 수 있어요",
    d: "평일 09시부터 19시까지만 사고팔 수 있어요.\n그 외 시간에는 가격만 볼 수 있습니다.\n화면 위에 초록 점이 깜빡이면 열린 거예요." },
  { i: "💸", t: "사고팔 때 수수료가 붙어요",
    d: "살 때 0.015%, 팔 때는 거기에 세금 0.18%가 더 붙어요.\n그래서 사자마자 팔면 조금 손해예요.\n오를 때까지 기다렸다 파는 게 기본입니다." },
  { i: "🚦", t: "하루에 30%까지만 움직여요",
    d: "너무 많이 오르거나 내리지 않도록 막아둔 선이에요.\n위로 닿으면 상한가, 아래로 닿으면 하한가.\n상한가에서는 살 수 없고 하한가에서는 팔 수 없어요." },
  { i: "⚖️", t: "방향 걸기 — 오를지 내릴지",
    d: "주식을 직접 사지 않고 방향만 고르는 방법도 있어요.\n'상승'에 걸면 오를 때, '하락'에 걸면 내릴 때 벌어요.\n장이 안 좋을 때도 기회가 생기는 거죠." },
  { i: "🔥", t: "배수를 걸면 커집니다",
    d: "2배를 걸면 주가가 10% 움직일 때 20%를 벌거나 잃어요.\n3배면 30%. 이익도 손해도 그만큼 커집니다.\n크게 벌 수 있는 만큼 크게 잃을 수도 있어요." },
  { i: "⚠️", t: "청산 — 가장 조심할 것",
    d: "반대 방향으로 많이 가면 맡긴 증거금이 거의 사라져요.\n그 지점을 '청산가'라고 하고, 닿으면 자동으로 정리되면서\n증거금을 전부 잃습니다. 화면에 미리 표시되니 꼭 확인하세요." },
  { i: "🌱", t: "처음이라면 이렇게",
    d: "적은 금액으로 한 종목만 사보세요.\n방향 걸기는 익숙해진 다음에 해도 늦지 않아요.\n잃어도 괜찮은 만큼만 — 이게 제일 중요합니다." },
];

function MarketGuide({ onClose }) {
  const [i, setI] = useState(0);
  const last = i === MKT_GUIDE.length - 1;
  const p = MKT_GUIDE[i];
  return (
    <div className="hl-tourWrap" style={{ zIndex: 80 }}>
      <div className="hl-tourCard" key={i}>
        <div className="hl-tourTop">
          <span className="hl-mono hl-tourNo">{String(i + 1).padStart(2, "0")} / {String(MKT_GUIDE.length).padStart(2, "0")}</span>
          <button className="hl-tourSkip" onClick={onClose}>건너뛰기</button>
        </div>
        <div className="hl-tourIcon">{p.i}</div>
        <div className="hl-tourTitle">{p.t}</div>
        <p className="hl-tourDesc">{p.d}</p>
        <div className="hl-tourDots">
          {MKT_GUIDE.map((_, k) => <i key={k} className={k === i ? "on" : ""} onClick={() => setI(k)} />)}
        </div>
        <div className="hl-dayBtns">
          {i > 0 && <button className="hl-cta done" onClick={() => setI(i - 1)}>이전</button>}
          <button className="hl-cta" onClick={() => (last ? onClose() : setI(i + 1))}>
            {last ? "시작하기" : "다음"}
          </button>
        </div>
      </div>
    </div>
  );
}

function StockChart({ st, range }) {
  const t = nowTick();
  const n = range === "1D" ? 120 : range === "1W" ? 168 : 200;
  const step = range === "1D" ? 1 : range === "1W" ? 12 : 72;
  const pts = Array.from({ length: n }, (_, i) => priceAt(st, t - (n - 1 - i) * step));
  const lo = Math.min(...pts), hi = Math.max(...pts), sp = hi - lo || 1;
  const up = pts[n - 1] >= pts[0];
  const col = up ? "#D94B5A" : "#3E7BD9";
  const X = (i) => (i / (n - 1)) * 300;
  const Y = (p) => 118 - ((p - lo) / sp) * 104;
  const line = pts.map((p, i) => `${X(i)},${Y(p)}`).join(" ");
  const area = `0,124 ${line} 300,124`;
  return (
    <div className="hl-chartWrap">
      <svg viewBox="0 0 300 130" preserveAspectRatio="none" className="hl-chart">
        <defs>
          <linearGradient id={`cg_${st.code}`} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stopColor={col} stopOpacity=".28" />
            <stop offset="1" stopColor={col} stopOpacity="0" />
          </linearGradient>
        </defs>
        {[0.25, 0.5, 0.75].map((k) => (
          <line key={k} x1="0" y1={14 + 104 * k} x2="300" y2={14 + 104 * k}
            stroke="currentColor" strokeWidth=".5" opacity=".16" />
        ))}
        <polygon points={area} fill={`url(#cg_${st.code})`} />
        <polyline points={line} fill="none" stroke={col} strokeWidth="1.8"
          vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
      </svg>
      <div className="hl-chartAxis">
        <span className="hl-mono">{hi.toLocaleString()}</span>
        <span className="hl-mono">{lo.toLocaleString()}</span>
      </div>
    </div>
  );
}

function StockDetail({ st, hold, pos, available, mkt, isOwner, adj, onAdjust, onTrade, onOpenPos, onClosePos, onBack, flash }) {
  const [range, setRange] = useState("1D");
  const [qty, setQty] = useState(1);
  const [mode, setMode] = useState("spot");      /* spot | lev */
  const [side, setSide] = useState("long");
  const [lev, setLev] = useState(2);
  const [margin, setMargin] = useState(100);
  const [, bump] = useState(0);
  useEffect(() => { const t = setInterval(() => bump((x) => x + 1), 20000); return () => clearInterval(t); }, []);

  const c = changeOf(st);
  const h = hold[st.code] || { qty: 0, avg: 0 };
  const buyCost = Math.round(c.price * qty * (1 + FEE_RATE));
  const sellNet = Math.round(c.price * qty * (1 - FEE_RATE - TAX_RATE));
  const maxBuy = Math.floor(available / (c.price * (1 + FEE_RATE)));
  const t = nowTick();
  const day = Array.from({ length: 120 }, (_, i) => priceAt(st, t - i));
  const hi = Math.max(...day), lo = Math.min(...day);

  const buy = async () => {
    if (!mkt.open) { flash(`지금은 거래할 수 없어요 — ${mkt.why}`); return; }
    if (c.upper) { flash("상한가라 살 수 없어요."); return; }
    if (buyCost > available) { flash("포인트가 모자라요."); return; }
    await onTrade("buy", st.code, qty, c.price, buyCost);
    flash(`${st.n} ${qty}주 매수 · ${buyCost.toLocaleString()}P`);
    setQty(1);
  };
  const sell = async () => {
    if (!mkt.open) { flash(`지금은 거래할 수 없어요 — ${mkt.why}`); return; }
    if (c.lower) { flash("하한가라 팔 수 없어요."); return; }
    if (qty > h.qty) return;
    await onTrade("sell", st.code, qty, c.price, sellNet);
    flash(`${st.n} ${qty}주 매도 · +${sellNet.toLocaleString()}P`);
    setQty(1);
  };

  const up = c.rate >= 0;
  const myPos = pos || null;
  const plNow = myPos ? posPL(myPos, c.price) : 0;
  const danger = myPos ? plNow <= -myPos.margin * 0.6 : false;
  return (
    <>
      <button className="hl-back" onClick={onBack}>‹ 종목 목록</button>

      <div className="hl-detHead">
        <div>
          <div className="hl-detName">
            {st.n}
            {c.upper && <b className="hl-limitTag up">상한</b>}
            {c.lower && <b className="hl-limitTag down">하한</b>}
          </div>
          <div className="hl-mono hl-detEn">{st.en}</div>
          <div className="hl-mono hl-detCode">{st.code} · {st.mkt} · {st.sector}</div>
        </div>
      </div>

      <div className="hl-detPrice">
        <span className="hl-mono hl-detNow">{c.price.toLocaleString()}</span>
        <span className="hl-mono hl-detDiff" style={{ color: up ? "#D94B5A" : "#3E7BD9" }}>
          {up ? "▲" : "▼"} {Math.abs(c.diff).toLocaleString()} ({up ? "+" : ""}{c.rate.toFixed(2)}%)
        </span>
      </div>

      <div className="hl-rangeRow">
        {["1D", "1W", "1M"].map((r) => (
          <button key={r} className={`hl-rangeBtn ${range === r ? "on" : ""}`} onClick={() => setRange(r)}>
            {r === "1D" ? "하루" : r === "1W" ? "일주일" : "한 달"}
          </button>
        ))}
      </div>
      <StockChart st={st} range={range} />

      <div className="hl-detGrid">
        <div><span>전일 종가</span><b className="hl-mono">{c.prev.toLocaleString()}</b></div>
        <div><span>오늘 고가</span><b className="hl-mono" style={{ color: "#D94B5A" }}>{hi.toLocaleString()}</b></div>
        <div><span>오늘 저가</span><b className="hl-mono" style={{ color: "#3E7BD9" }}>{lo.toLocaleString()}</b></div>
        <div><span>상한가</span><b className="hl-mono">{toTick(c.prev * 1.3).toLocaleString()}</b></div>
        <div><span>하한가</span><b className="hl-mono">{toTick(c.prev * 0.7).toLocaleString()}</b></div>
        <div><span>변동성</span><b className="hl-mono">{st.vol.toFixed(2)}</b></div>
      </div>

      {h.qty > 0 && (
        <div className="hl-myHold">
          <div><span>보유</span><b>{h.qty}주</b></div>
          <div><span>평균 단가</span><b className="hl-mono">{Math.round(h.avg).toLocaleString()}P</b></div>
          <div><span>평가손익</span>
            <b className="hl-mono" style={{ color: c.price >= h.avg ? "#D94B5A" : "#3E7BD9" }}>
              {c.price >= h.avg ? "+" : ""}{Math.round((c.price - h.avg) * h.qty).toLocaleString()}P
            </b>
          </div>
        </div>
      )}

      <div className="hl-scopeLabel">수량</div>
      <div className="hl-qtyRow">
        <button className="hl-qtyBtn" onClick={() => setQty(Math.max(1, qty - 1))}>−</button>
        <span className="hl-mono hl-qtyNum">{qty}주</span>
        <button className="hl-qtyBtn" onClick={() => setQty(qty + 1)}>＋</button>
        <button className="hl-attBtn" onClick={() => setQty(Math.max(1, maxBuy))}>최대</button>
        {h.qty > 0 && <button className="hl-attBtn" onClick={() => setQty(h.qty)}>전량</button>}
      </div>
      <div className="hl-feeRow">
        <span>주문금액</span><span className="hl-mono">{(c.price * qty).toLocaleString()}P</span>
      </div>
      <div className="hl-feeRow dim">
        <span>수수료 · 세금 반영</span>
        <span className="hl-mono">매수 {buyCost.toLocaleString()} / 매도 {sellNet.toLocaleString()}</span>
      </div>
      <div className="hl-dayBtns">
        <button className="hl-cta hl-buyBtn" disabled={!mkt.open || c.upper || buyCost > available} onClick={buy}>매수</button>
        <button className="hl-cta hl-sellBtn" disabled={!mkt.open || c.lower || qty > h.qty} onClick={sell}>매도</button>
      </div>

      {/* ── 레버리지 · 공매도 ── */}
      <div className="hl-sectionHead"><span>방향 걸기</span><span className="hl-mono hl-dim">최대 {LEV_MAX}배</span></div>

      {myPos ? (
        <div className={`hl-posBox ${plNow >= 0 ? "up" : "down"} ${danger ? "danger" : ""}`}>
          <div className="hl-posTop">
            <span className={`hl-posSide ${myPos.side}`}>{myPos.side === "short" ? "하락" : "상승"} {myPos.lev}배</span>
            <span className="hl-mono hl-dim">진입 {myPos.entry.toLocaleString()}P</span>
          </div>
          <div className="hl-posPL" style={{ color: plNow >= 0 ? "#D94B5A" : "#3E7BD9" }}>
            {plNow >= 0 ? "+" : ""}{plNow.toLocaleString()}<em>P</em>
            <span className="hl-mono hl-posRate">{plNow >= 0 ? "+" : ""}{posRate(myPos, c.price).toFixed(1)}%</span>
          </div>
          <div className="hl-posGrid">
            <div><span>증거금</span><b className="hl-mono">{myPos.margin.toLocaleString()}P</b></div>
            <div><span>청산가</span><b className="hl-mono" style={{ color: "#C4645C" }}>{liqPrice(myPos).toLocaleString()}P</b></div>
            <div><span>지금 닫으면</span><b className="hl-mono">{Math.max(0, myPos.margin + plNow).toLocaleString()}P</b></div>
          </div>
          {danger && (
            <div className="hl-posWarn">
              청산가에 가까워요. 지금 닫지 않으면 증거금을 모두 잃을 수 있어요.
            </div>
          )}
          <button className="hl-cta hl-block" onClick={async () => {
            const r = await onClosePos(st.code, c.price, false);
            flash(`포지션을 닫았어요. ${r?.pl >= 0 ? "+" : ""}${(r?.pl || 0).toLocaleString()}P`);
          }}>포지션 닫기</button>
        </div>
      ) : (
        <div className="hl-levBox">
          <div className="hl-oeBtns">
            <button className={`hl-oeBtn ${side === "long" ? "on" : ""}`}
              style={side === "long" ? { borderColor: "#D94B5A", color: "#D94B5A" } : {}}
              onClick={() => setSide("long")}>
              <span className="hl-oeBig">상승</span>
              <span className="hl-mono">오르면 번다</span>
            </button>
            <button className={`hl-oeBtn ${side === "short" ? "on" : ""}`}
              style={side === "short" ? { borderColor: "#3E7BD9", color: "#3E7BD9" } : {}}
              onClick={() => setSide("short")}>
              <span className="hl-oeBig">하락</span>
              <span className="hl-mono">내리면 번다</span>
            </button>
          </div>

          <div className="hl-scopeLabel">배수</div>
          <div className="hl-oeAmts">
            {[1, 2, 3].map((v) => (
              <button key={v} className={`hl-attBtn ${lev === v ? "on" : ""}`} onClick={() => setLev(v)}>{v}배</button>
            ))}
          </div>

          <div className="hl-scopeLabel">증거금</div>
          <div className="hl-oeAmts">
            {[50, 100, 300, 500].filter((v) => v <= available).map((v) => (
              <button key={v} className={`hl-attBtn ${margin === v ? "on" : ""}`} onClick={() => setMargin(v)}>{v}P</button>
            ))}
            <button className={`hl-attBtn allin ${margin === available ? "on" : ""}`}
              onClick={() => setMargin(available)}>전부 {available.toLocaleString()}P</button>
          </div>

          <div className="hl-levPreview">
            <div><span>주가 10% {side === "short" ? "하락" : "상승"} 시</span>
              <b style={{ color: "#D94B5A" }}>+{Math.round(margin * 0.1 * lev).toLocaleString()}P</b></div>
            <div><span>반대로 가면</span>
              <b style={{ color: "#3E7BD9" }}>−{Math.round(margin * 0.1 * lev).toLocaleString()}P</b></div>
            <div><span>청산가</span>
              <b className="hl-mono">{liqPrice({ side, lev, entry: c.price }).toLocaleString()}P</b></div>
          </div>

          <button className="hl-cta hl-block"
            disabled={!mkt.open || margin > available || margin < 10}
            onClick={async () => {
              await onOpenPos(st.code, side, lev, Math.min(margin, available), c.price);
              flash(`${side === "short" ? "하락" : "상승"} ${lev}배 진입 · 증거금 ${margin.toLocaleString()}P`);
            }}>
            {mkt.open ? `${side === "short" ? "하락" : "상승"}에 걸기` : "장이 닫혀 있어요"}
          </button>
          <p className="hl-fine hl-left">
            배수만큼 이익도 손실도 커져요. 청산가에 닿으면 증거금을 모두 잃고 자동으로 정리됩니다.
          </p>
        </div>
      )}

      {isOwner && (
        <>
          <div className="hl-sectionHead"><span>주가 손보기</span><span className="hl-mono hl-dim">관리자</span></div>
          <div className="hl-admBox">
            <p className="hl-admNote">
              누르면 이 종목의 시세가 곧바로 움직이고, 하루에 걸쳐 원래 흐름으로 돌아갑니다.
              모든 부원에게 똑같이 보여요.
            </p>
            <div className="hl-admBtns">
              {[-20, -10, -5, 5, 10, 20].map((v) => (
                <button key={v} className={`hl-admBtn ${v > 0 ? "up" : "down"}`}
                  onClick={() => onAdjust(st.code, v)}>{v > 0 ? "+" : ""}{v}%</button>
              ))}
            </div>
            {adj && (
              <div className="hl-admNow">
                지금 {adj.pct > 0 ? "+" : ""}{adj.pct}% 적용 중
                <button className="hl-del" onClick={() => onAdjust(st.code, 0)}>되돌리기</button>
              </div>
            )}
          </div>
        </>
      )}
    </>
  );
}

function Market({ onClose, available, port, onTrade, flash, isOwner, adj, onAdjust, onOpenPos, onClosePos }) {
  const [guide, setGuide] = useState(() => {
    try { return !localStorage.getItem("hanul:mktGuide"); } catch (e) { return true; }
  });
  const closeGuide = () => {
    try { localStorage.setItem("hanul:mktGuide", "1"); } catch (e) {}
    setGuide(false);
  };
  const [sel, setSel] = useState(null);
  const [qty, setQty] = useState(1);
  const [tab, setTab] = useState("list");
  const [, bump] = useState(0);
  useEffect(() => { const t = setInterval(() => bump((x) => x + 1), 20000); return () => clearInterval(t); }, []);

  const hold = port.hold || {};
  const held = Object.entries(hold).filter(([, h]) => h.qty > 0);
  const valueOf = (code) => (hold[code]?.qty || 0) * priceNow(stockOf(code));
  const totalValue = held.reduce((n, [c]) => n + valueOf(c), 0);
  const totalCost = held.reduce((n, [c, h]) => n + h.qty * h.avg, 0);
  const pl = totalValue - totalCost;

  const mkt = marketState();
  const st = sel ? stockOf(sel) : null;
  if (st) return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-game hl-market" onClick={(e) => e.stopPropagation()}>
        <div className="hl-gameTop">
          <span className="hl-mono hl-gameNo">한울 증권</span>
          <span className="hl-topBtns">
            <button className="hl-tourSkip" onClick={() => setGuide(true)}>사용법</button>
            <button className="hl-tourSkip" onClick={onClose}>닫기</button>
          </span>
        </div>
        <div className={`hl-mktState ${mkt.open ? "open" : ""}`}>
          <span className="hl-mktDot" />
          {mkt.open ? (mkt.closing ? "장 마감 임박 · 19:00까지" : "정규장 열림 · 09:00~19:00") : mkt.why}
        </div>
        <div className="hl-mktSum">
          <div>
            <span className="hl-mono hl-mktLabel">쓸 수 있는 포인트</span>
            <span className="hl-mktBig">{available.toLocaleString()}<em>P</em></span>
          </div>
        </div>
        {guide && <MarketGuide onClose={closeGuide} />}
        <StockDetail st={st} hold={hold} pos={port.pos?.[st.code]} available={available} mkt={mkt} isOwner={isOwner}
          adj={adj?.[st.code]} onAdjust={onAdjust} onTrade={onTrade}
          onOpenPos={onOpenPos} onClosePos={onClosePos}
          onBack={() => { setSel(null); setQty(1); }} flash={flash} />
        <button className="hl-cta done hl-block" onClick={onClose}>닫기</button>
      </div>
    </div>
  );
  const cur = st ? changeOf(st) : null;
  const myQty = st ? (hold[st.code]?.qty || 0) : 0;
  /* 수수료까지 감안해 살 수 있는 최대 수량 */
  const maxBuy = st ? Math.floor(available / (cur.price * (1 + FEE_RATE))) : 0;
  const buyCost = st ? Math.round(cur.price * qty * (1 + FEE_RATE)) : 0;
  const sellNet = st ? Math.round(cur.price * qty * (1 - FEE_RATE - TAX_RATE)) : 0;

  const buy = async () => {
    if (!st || qty < 1) return;
    if (!mkt.open) { flash(`지금은 거래할 수 없어요 — ${mkt.why}`); return; }
    if (cur.upper) { flash("상한가라 살 수 없어요."); return; }
    if (buyCost > available) { flash("포인트가 모자라요."); return; }
    await onTrade("buy", st.code, qty, cur.price, buyCost);
    flash(`${st.n} ${qty}주 매수 · ${buyCost.toLocaleString()}P (수수료 포함)`);
    setQty(1);
  };
  const sell = async () => {
    if (!st || qty < 1 || qty > myQty) return;
    if (!mkt.open) { flash(`지금은 거래할 수 없어요 — ${mkt.why}`); return; }
    if (cur.lower) { flash("하한가라 팔 수 없어요."); return; }
    await onTrade("sell", st.code, qty, cur.price, sellNet);
    flash(`${st.n} ${qty}주 매도 · +${sellNet.toLocaleString()}P (세금·수수료 차감)`);
    setQty(1);
  };

  return (
    <div className="hl-sheetWrap" onClick={onClose}>
      <div className="hl-game hl-market" onClick={(e) => e.stopPropagation()}>
        <div className="hl-gameTop">
          <span className="hl-mono hl-gameNo">한울 증권</span>
          <span className="hl-topBtns">
            <button className="hl-tourSkip" onClick={() => setGuide(true)}>사용법</button>
            <button className="hl-tourSkip" onClick={onClose}>닫기</button>
          </span>
        </div>
        {guide && <MarketGuide onClose={closeGuide} />}

        <div className={`hl-mktState ${mkt.open ? "open" : ""}`}>
          <span className="hl-mktDot" />
          {mkt.open ? (mkt.closing ? "장 마감 임박 · 19:00까지" : "정규장 열림 · 09:00~19:00") : mkt.why}
        </div>

        <div className="hl-mktSum">
          <div>
            <span className="hl-mono hl-mktLabel">쓸 수 있는 포인트</span>
            <span className="hl-mktBig">{available.toLocaleString()}<em>P</em></span>
          </div>
          {held.length > 0 && (
            <div className="hl-mktRight">
              <span className="hl-mono hl-mktLabel">평가금액</span>
              <span className="hl-mktMid">{totalValue.toLocaleString()}P</span>
              <span className="hl-mono" style={{ color: pl >= 0 ? "#D94B5A" : "#3E7BD9", fontSize: 11 }}>
                {pl >= 0 ? "▲" : "▼"} {Math.abs(pl).toLocaleString()}P
                {totalCost > 0 && ` (${(pl / totalCost * 100).toFixed(1)}%)`}
              </span>
            </div>
          )}
        </div>

        <div className="hl-seg">
          <button className={tab === "list" ? "on" : ""} onClick={() => setTab("list")}>전체 종목</button>
          <button className={tab === "mine" ? "on" : ""} onClick={() => setTab("mine")}>
            내 주식 <span className="hl-mono hl-segN">{held.length}</span>
          </button>
        </div>

        {tab === "mine" && held.length === 0 ? (
          <div className="hl-empty">아직 산 주식이 없어요.<br />전체 종목에서 골라보세요.</div>
        ) : (
          <ul className="hl-stockList">
            {(tab === "mine" ? held.map(([c]) => stockOf(c)) : STOCKS).map((x) => {
              const c = changeOf(x);
              const h = hold[x.code];
              const up = c.rate >= 0;
              return (
                <li key={x.code}>
                  <button className={`hl-stockRow ${sel === x.code ? "on" : ""}`}
                    onClick={() => { setSel(x.code); setQty(1); }}>
                    <span className="hl-stockInfo">
                      <span className="hl-stockName">
                        {x.n}
                        {c.upper && <b className="hl-limitTag up">상한</b>}
                        {c.lower && <b className="hl-limitTag down">하한</b>}
                      </span>
                      <span className="hl-mono hl-stockEn">{x.en}</span>
                      <span className="hl-mono hl-stockSub">
                        {x.code} · {x.mkt}
                        {h?.qty > 0 && <b className="hl-stockHas">{h.qty}주</b>}
                      </span>
                    </span>
                    <Sparkline st={x} />
                    <span className="hl-stockPx">
                      <span className="hl-mono hl-stockPrice">{c.price.toLocaleString()}</span>
                      <span className="hl-mono hl-stockRate" style={{ color: up ? "#D94B5A" : "#3E7BD9" }}>
                        {up ? "▲" : "▼"} {Math.abs(c.rate).toFixed(2)}%
                      </span>
                    </span>
                    <span className="hl-menuArrow">›</span>
                  </button>

                </li>
              );
            })}
          </ul>
        )}

        <p className="hl-fine hl-left">
          평일 09:00~19:00에만 거래할 수 있고 5분마다 시세가 움직여요.
          전일 종가 대비 ±30%가 상하한가이고, 수수료 {(FEE_RATE * 100).toFixed(3)}% ·
          매도 시 거래세 {(TAX_RATE * 100).toFixed(2)}%가 붙습니다.
          실제 주식이 아니라 앱 안에서만 도는 가상의 장이에요.
        </p>
        <button className="hl-cta done hl-block" onClick={onClose}>닫기</button>
      </div>
    </div>
  );
}

function OddEven({ onClose, available, onBet }) {
  const [mode, setMode] = useState("oddeven");
  const [amt, setAmt] = useState(10);
  const [sel, setSel] = useState([]);
  const [rolling, setRolling] = useState(false);
  const [face, setFace] = useState(null);
  const [res, setRes] = useState(null);
  const [pot, setPot] = useState(0);        /* 쌓인 판돈 — 이어가는 중에는 손에 없다 */
  const [streak, setStreak] = useState(0);
  const M = OE_MODES.find((x) => x.id === mode) || OE_MODES[0];
  const need = mode === "trio" ? 3 : mode === "exact" ? 1 : 5;
  const running = streak > 0;
  const bet = Math.max(OE_MIN, Math.min(amt, available));
  const ready = sel.length === need && (running || available >= OE_MIN);
  const nextPot = running ? pot * M.mult : bet * M.mult;

  const toggle = (v) => {
    if (rolling) return;
    setRes(null);
    if (need === 1) { setSel([v]); return; }
    setSel((cur) => cur.includes(v) ? cur.filter((x) => x !== v)
      : cur.length >= need ? [...cur.slice(1), v] : [...cur, v]);
  };
  const switchMode = (id) => { if (!running && !rolling) { setMode(id); setSel([]); setRes(null); } };

  const play = async () => {
    if (rolling || !ready) return;
    setRolling(true); setRes(null);
    const stake = running ? pot : bet;
    if (!running) await onBet(-bet, bet, M, null, false, "start");   /* 처음 걸 때만 빠져나간다 */

    const spin = setInterval(() => setFace(Math.floor(Math.random() * OE_FACES)), 70);
    const roll = Math.floor(Math.random() * OE_FACES);               /* 0~10 */
    setTimeout(async () => {
      clearInterval(spin);
      setFace(roll);
      const win = sel.includes(roll);
      if (win) {
        const won = stake * M.mult;
        setPot(won);
        setStreak((n) => n + 1);
        A.sfx(M.mult >= 9 ? "tier_legend" : "reward");
        setRes({ win: true, roll, pot: won, streak: streak + 1 });
      } else {
        A.sfx("deny");
        setPot(0); setStreak(0);
        setRes({ win: false, roll, lost: stake });
        await onBet(0, stake, M, roll, false, "lose");
      }
      setRolling(false);
    }, 1200);
  };

  const cashing = useRef(false);
  const cashOut = async () => {
    if (rolling || pot <= 0 || cashing.current) return;
    cashing.current = true;            /* 연타로 여러 번 들어오는 걸 막는다 */
    const got = pot;
    setPot(0);
    setStreak(0);
    try { await onBet(got, got, M, res?.roll, true, "cash"); } catch (e) {}
    A.sfx("confirm");
    setRes({ cashed: got });
    cashing.current = false;
  };

  const grid = () => {
    if (mode === "oddeven") return [
      { label: "홀", nums: [1, 3, 5, 7, 9], tint: "#E8503A" },
      { label: "짝", nums: [2, 4, 6, 8, 10], tint: "#45B4D6" },
    ];
    if (mode === "half") return [
      { label: "작은", nums: [1, 2, 3, 4, 5], tint: "#6FBF73" },
      { label: "큰", nums: [6, 7, 8, 9, 10], tint: "#B77FD4" },
    ];
    return null;
  };
  const pairs = grid();
  const pickedGroup = pairs ? pairs.findIndex((g) => g.nums.every((n) => sel.includes(n))) : -1;

  const maxed = streak >= OE_STREAK_MAX;

  return (
    <div className="hl-sheetWrap" onClick={rolling || running ? undefined : onClose}>
      <div className="hl-game" onClick={(e) => e.stopPropagation()}>
        <div className="hl-gameTop">
          <span className="hl-mono hl-gameNo">홀짝{running ? ` · ${streak}연승` : ""}</span>
          <button className="hl-tourSkip" disabled={running} onClick={onClose}>
            {running ? "진행 중" : "닫기"}
          </button>
        </div>

        <div className={`hl-oeFace ${rolling ? "spin" : ""} ${res?.win ? "win" : res && res.lost ? "lose" : ""} ${face === 0 ? "zero" : ""}`}>
          <span className="hl-oeNum">{face ?? "?"}</span>
          {res && typeof res.roll === "number" && (
            <span className="hl-oeSide">{res.roll === 0 ? "꽝" : res.roll % 2 === 1 ? "홀" : "짝"}</span>
          )}
        </div>

        {res?.cashed && <div className="hl-oeResult win">받았어요 <b>+{res.cashed.toLocaleString()}P</b></div>}
        {res?.win && (
          <div className="hl-oeResult win">
            {res.streak}연승! 지금 받으면 <b>{res.pot.toLocaleString()}P</b>
          </div>
        )}
        {res && res.lost > 0 && (
          <div className="hl-oeResult lose">아쉬워요… <b>−{res.lost.toLocaleString()}P</b></div>
        )}

        <div className="hl-oeInfo">
          <span className="hl-mono">보유 {available.toLocaleString()}P</span>
          <span className="hl-mono hl-dim">{M.odds} · {M.mult}배</span>
        </div>

        {/* 연승 중 — 받을지 이어갈지 */}
        {running ? (
          <>
            <div className="hl-streakBox">
              <div className="hl-streakNow">
                <span className="hl-mono hl-streakLabel">지금 쌓인 판돈</span>
                <span className="hl-streakPot">{pot.toLocaleString()}<em>P</em></span>
              </div>
              {!maxed && (
                <div className="hl-streakNext">
                  한 번 더 맞추면 <b>{nextPot.toLocaleString()}P</b> · 틀리면 전부 잃어요
                </div>
              )}
              {maxed && <div className="hl-streakNext">최대 연승이에요. 받고 끝내세요.</div>}
            </div>
            <div className="hl-dayBtns">
              <button className="hl-cta" disabled={rolling} onClick={cashOut}>
                받기 <span className="hl-mono hl-costTag">{pot.toLocaleString()}P</span>
              </button>
              {!maxed && (
                <button className="hl-cta done hl-riskBtn" disabled={rolling || !ready} onClick={play}>
                  {rolling ? "굴리는 중…" : "한 번 더"}
                </button>
              )}
            </div>
            <p className="hl-fine hl-left">이어가는 동안에는 창을 닫을 수 없어요. 받아야 손에 들어옵니다.</p>
          </>
        ) : available < OE_MIN ? (
          <div className="hl-empty">걸 수 있는 포인트가 모자라요. 최소 {OE_MIN}P가 필요해요.</div>
        ) : (
          <>
            <div className="hl-scopeLabel">어떻게 걸까요</div>
            <div className="hl-oeModes">
              {OE_MODES.map((x) => (
                <button key={x.id} className={`hl-oeMode ${mode === x.id ? "on" : ""}`}
                  disabled={rolling} onClick={() => switchMode(x.id)}>
                  <span className="hl-oeModeN">{x.n}</span>
                  <span className="hl-mono hl-oeModeM">{x.mult}배 · {x.odds}</span>
                </button>
              ))}
            </div>

            <div className="hl-scopeLabel">
              {pairs ? "어느 쪽에 걸까요" : `숫자 ${need}개를 고르세요 (${sel.length}/${need})`}
            </div>
            {pairs ? (
              <div className="hl-oeBtns">
                {pairs.map((g, i) => (
                  <button key={g.label} className={`hl-oeBtn ${pickedGroup === i ? "on" : ""}`}
                    style={pickedGroup === i ? { borderColor: g.tint, color: g.tint } : {}}
                    disabled={rolling} onClick={() => { setSel(g.nums); setRes(null); }}>
                    <span className="hl-oeBig">{g.label}</span>
                    <span className="hl-mono">{g.nums.join(" ")}</span>
                  </button>
                ))}
              </div>
            ) : (
              <div className="hl-numGrid">
                {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((n) => (
                  <button key={n} className={`hl-numCell ${sel.includes(n) ? "on" : ""}`}
                    disabled={rolling} onClick={() => toggle(n)}>{n}</button>
                ))}
              </div>
            )}

            <div className="hl-zeroNote">
              <span className="hl-zeroChip">0</span>
              0이 나오면 어느 쪽도 맞지 않아요. 열한 칸 중 한 칸이 꽝입니다.
            </div>

            <div className="hl-scopeLabel">얼마를 걸까요</div>
            <div className="hl-oeAmts">
              {[5, 10, 50, 100, 500].filter((v) => v <= available).map((v) => (
                <button key={v} className={`hl-attBtn ${amt === v ? "on" : ""}`}
                  disabled={rolling} onClick={() => setAmt(v)}>{v}P</button>
              ))}
              <button className={`hl-attBtn allin ${amt === available ? "on" : ""}`}
                disabled={rolling} onClick={() => setAmt(available)}>
                올인 {available.toLocaleString()}P
              </button>
            </div>

            <button className="hl-cta hl-block hl-betBtn" disabled={rolling || !ready} onClick={play}>
              {rolling ? "굴리는 중…"
                : !ready ? (pairs ? "어느 쪽에 걸지 골라주세요" : `숫자를 ${Math.max(0, need - sel.length)}개 더 골라주세요`)
                : <>{bet.toLocaleString()}P 걸기<span className="hl-mono hl-costTag">적중 시 {nextPot.toLocaleString()}P</span></>}
            </button>
            <p className="hl-fine hl-left">
              최소 {OE_MIN}P · 판수 제한 없음. 맞추면 이어가서 배수를 불릴 수 있어요 (최대 {OE_STREAK_MAX}연승).
            </p>
          </>
        )}
        {!running && <button className="hl-cta done hl-block" onClick={onClose}>닫기</button>}
      </div>
    </div>
  );
}

function CueGame({ onClose, onReward, playedToday, board = [], meId, myWeek = 0 }) {
  const [, redraw] = useState(0);
  useEffect(() => { const t = setInterval(() => redraw((x) => x + 1), 5000); return () => clearInterval(t); }, []);
  const [phase, setPhase] = useState("ready");   // ready | play | over
  const [combo, setCombo] = useState(0);
  const [best, setBest] = useState(0);
  const [ang, setAng] = useState(0);
  const [zone, setZone] = useState({ start: 40, size: 66 });
  const [flash2, setFlash2] = useState(null);
  const raf = useRef(0);
  const st = useRef({ ang: 0, dir: 1, speed: 160, zone: { start: 40, size: 66 }, live: false });

  const newRound = (n) => {
    const size = Math.max(10, 66 - n * 3.6);
    const start = 20 + Math.random() * 320;
    const speed = 160 + n * 26;
    st.current.zone = { start, size };
    st.current.speed = speed;
    st.current.dir = Math.random() > 0.5 ? 1 : -1;
    setZone({ start, size });
  };

  const stop = () => { cancelAnimationFrame(raf.current); st.current.live = false; };

  const start = () => {
    setCombo(0); setFlash2(null);
    st.current.ang = 0; st.current.live = true;
    newRound(0);
    setPhase("play");
    let last = performance.now();
    const step = (t) => {
      if (!st.current.live) return;
      const dt = (t - last) / 1000; last = t;
      st.current.ang = (st.current.ang + st.current.dir * st.current.speed * dt + 360) % 360;
      setAng(st.current.ang);
      raf.current = requestAnimationFrame(step);
    };
    raf.current = requestAnimationFrame(step);
  };

  const hit = () => {
    if (phase !== "play") return;
    const { start: zs, size } = st.current.zone;
    const a = st.current.ang;
    const rel = (a - zs + 360) % 360;
    if (rel <= size) {
      const mid = size / 2;
      const off = Math.abs(rel - mid) / mid;      // 0이면 정중앙
      const perfect = off < 0.28;
      const next = combo + 1;
      setCombo(next);
      setBest((b) => Math.max(b, next));
      setFlash2(perfect ? "perfect" : "good");
      A.sfx(perfect ? "confirm" : "tap");
      st.current.dir *= -1;
      newRound(next);
      setTimeout(() => setFlash2(null), 260);
    } else {
      stop();
      A.sfx("deny");
      setPhase("over");
      const pt = playedToday ? 0 : cuePay(combo);
      Promise.resolve(onReward(pt, combo)).then(() => redraw((x) => x + 1));
    }
  };

  useEffect(() => () => stop(), []);

  const R = 86, CX = 100, CY = 100;
  const pol = (deg, r) => [CX + r * Math.cos((deg - 90) * Math.PI / 180), CY + r * Math.sin((deg - 90) * Math.PI / 180)];
  const arc = (from, size, r) => {
    const [x1, y1] = pol(from, r), [x2, y2] = pol(from + size, r);
    return `M ${x1} ${y1} A ${r} ${r} 0 ${size > 180 ? 1 : 0} 1 ${x2} ${y2}`;
  };
  const [nx, ny] = pol(ang, R);

  return (
    <div className="hl-sheetWrap" onClick={phase === "play" ? undefined : () => { stop(); onClose(); }}>
      <div className="hl-game" onClick={(e) => e.stopPropagation()}>
        <div className="hl-gameTop">
          <span className="hl-mono hl-gameNo">조명 큐</span>
          <button className="hl-tourSkip" onClick={() => { stop(); onClose(); }}>닫기</button>
        </div>

        <div className={`hl-dial ${flash2 || ""}`}>
          <svg viewBox="0 0 200 200">
            <circle cx={CX} cy={CY} r={R} fill="none" stroke="var(--edge)" strokeWidth="14" />
            {phase === "play" && (
              <path d={arc(zone.start, zone.size, R)} fill="none" stroke="var(--accent)" strokeWidth="14" strokeLinecap="round" />
            )}
            <line x1={CX} y1={CY} x2={nx} y2={ny} stroke="var(--text)" strokeWidth="3.5" strokeLinecap="round" />
            <circle cx={CX} cy={CY} r="6" fill="var(--text)" />
            <text x={CX} y={CY - 16} textAnchor="middle" className="hl-dialNum">{combo}</text>
            <text x={CX} y={CY + 30} textAnchor="middle" className="hl-dialSub">
              {phase === "play" ? "COMBO" : phase === "over" ? "FINISH" : "READY"}
            </text>
          </svg>
          {flash2 && <span className="hl-hitTag">{flash2 === "perfect" ? "완벽!" : "좋아요"}</span>}
        </div>

        {phase === "play" && (
          <button className="hl-tapBtn" onPointerDown={(e) => { e.preventDefault(); hit(); }}>
            <span className="hl-tapRing" />
            <span className="hl-tapTxt">지금!</span>
          </button>
        )}

        {phase === "ready" && (
          <>
            <p className="hl-gameDesc">
              바늘이 <b>밝은 구간</b>을 지날 때 아래 큰 버튼을 누르세요.<br />
              성공할수록 구간이 좁아지고 바늘이 빨라져요.
            </p>
            <div className="hl-payTable">
              {CUE_PAY.map((x) => (
                <div key={x.min} className="hl-payRow">
                  <span className="hl-mono">{x.min}콤보</span>
                  <span className="hl-mono hl-payPt">+{x.pt}P</span>
                </div>
              ))}
            </div>
            {playedToday && <p className="hl-fine hl-left">오늘은 이미 포인트를 받았어요. 연습은 얼마든지 할 수 있어요.</p>}
            <button className="hl-cta hl-block" onClick={start}>시작하기</button>

            <div className="hl-rankHead">
              <span>이번 주 랭킹</span>
              <span className="hl-mono hl-dim">{weekLeft()}</span>
            </div>
            <div className="hl-prize">
              1위가 <b>{WEEK_MIN}콤보</b>를 넘겨야 상금이 나가요 · 연습한 판도 모두 집계됩니다
            </div>
            <div className="hl-prizeRow">
              {WEEK_PRIZES.map((p, i) => (
                <span key={i} className={`hl-prizeChip ${i === 0 ? "gold" : ""}`}>
                  <b>{i + 1}위</b>{p.toLocaleString()}P
                </span>
              ))}
              <span className="hl-prizeChip dim"><b>그 아래</b>0P</span>
            </div>
            {board.length === 0 ? (
              <div className="hl-empty">아직 이번 주 기록이 없어요. 첫 주인공이 되어보세요.</div>
            ) : (
              <ul className="hl-rankList">
                {board.slice(0, 8).map((r, i) => (
                  <li key={r.id} className={`hl-rankRow ${r.id === meId ? "me" : ""} ${i === 0 && r.combo >= WEEK_MIN ? "top" : ""}`}>
                    <span className="hl-mono hl-rankNo">{i === 0 ? "👑" : String(i + 1).padStart(2, "0")}</span>
                    <span className="hl-rankName">
                      {r.name}
                      {r.plays > 0 && <span className="hl-mono hl-rankPlays">{r.plays}판</span>}
                    </span>
                    <span className="hl-mono hl-rankPt">
                      {r.combo}<em>콤보</em>
                      {weekPrize(i) > 0 && <b className="hl-rankPrize">{weekPrize(i).toLocaleString()}P</b>}
                    </span>
                  </li>
                ))}
              </ul>
            )}
            <p className="hl-fine hl-left">
              매주 월요일에 초기화돼요. 내 이번 주 최고 기록은 <b>{myWeek}콤보</b>예요.<br />
              같은 콤보면 먼저 달성한 사람이 앞섭니다.
            </p>
            <button className="hl-cta done hl-block" onClick={() => { stop(); onClose(); }}>닫기</button>
          </>
        )}

        {phase === "over" && (
          <>
            <div className="hl-gameResult">
              <span className="hl-mono hl-gameLabel">이번 기록</span>
              <span className="hl-gameBig">{combo}<em>콤보</em></span>
              {!playedToday && cuePay(combo) > 0 && (
                <span className="hl-gameGot">+{cuePay(combo)}P 받았어요</span>
              )}
              {!playedToday && cuePay(combo) === 0 && (
                <span className="hl-mono hl-dim">3콤보부터 포인트를 받아요</span>
              )}
              {playedToday && <span className="hl-mono hl-dim">오늘 포인트는 이미 받았어요</span>}
              {combo > myWeek && combo >= WEEK_MIN && (
                <span className="hl-gameGot">이번 주 신기록! 1위를 노려보세요</span>
              )}
            </div>
            <div className="hl-dayBtns">
              <button className="hl-cta done" onClick={() => { stop(); onClose(); }}>그만하기</button>
              <button className="hl-cta" onClick={start}>다시</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function MoreMenu({ items, onPick }) {
  return (
    <div className="hl-menu">
      {items.map((it) => (
        it.gap ? <div key={it.id} className="hl-menuGap" /> :
        <button key={it.id} className="hl-menuRow" onClick={() => onPick(it.id)}>
          <span className="hl-menuBody">
            <span className="hl-menuName">{it.n}</span>
            {it.d && <span className="hl-menuDesc">{it.d}</span>}
          </span>
          {it.badge > 0 && <span className="hl-menuBadge">{it.badge}</span>}
          <span className="hl-menuArrow">›</span>
        </button>
      ))}
    </div>
  );
}

function Tab({ id, cur, set, label, badge }) {
  return (
    <button className={`hl-tab ${cur === id ? "on" : ""}`} onClick={() => set(id)}>
      {label}{badge > 0 && <span className="hl-badge">{badge}</span>}
    </button>
  );
}

const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Black+Han+Sans&family=Do+Hyeon&family=Nanum+Pen+Script&display=swap');
.hl-app{--w:460px;
 --sat:env(safe-area-inset-top,0px);--sab:env(safe-area-inset-bottom,0px);
 --sal:env(safe-area-inset-left,0px);--sar:env(safe-area-inset-right,0px);
 --tabH:calc(72px + var(--sab));
 --mono:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
 --sans:-apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo","Malgun Gothic","Noto Sans KR",sans-serif;
 --num:"Bebas Neue","Oswald",var(--mono);
 --disp:"Black Han Sans","Apple SD Gothic Neo","Malgun Gothic",sans-serif;
 --cute:"Do Hyeon","Apple SD Gothic Neo","Malgun Gothic",sans-serif;
 --hand:"Nanum Pen Script","Apple SD Gothic Neo","Malgun Gothic",cursive;
 background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100vh;min-height:100dvh;max-width:var(--w);margin:0 auto;
 display:flex;flex-direction:column;position:relative;overflow:hidden;-webkit-font-smoothing:antialiased;
 transition:background .4s ease,color .4s ease;isolation:isolate}
.hl-app *{box-sizing:border-box}
.hl-mono{font-family:var(--mono);font-variant-numeric:tabular-nums;letter-spacing:.02em}
.hl-dim{color:var(--muted)}
.hl-load{padding:80px 20px;text-align:center;color:var(--muted);font-size:14px}

/* 배경 */
.hl-amb{position:absolute;inset:-15%;z-index:0;pointer-events:none;
 opacity:var(--ambOpacity);mix-blend-mode:var(--ambBlend);filter:blur(56px)}
.hl-ambBlob{position:absolute;width:52%;padding-top:52%;border-radius:50%;background:var(--c);
 left:calc(3% + (var(--i) * 11.5%));top:calc(2% + (var(--i) * 10.5%));
 animation:sway 46s ease-in-out infinite alternate;animation-delay:calc(var(--i) * -6s)}
@keyframes sway{from{transform:translate(-6%,4%) scale(1)}to{transform:translate(9%,-7%) scale(1.16)}}

/* 오프닝 */
.hl-splash{position:absolute;inset:0;z-index:40;background:var(--script);overflow:hidden;isolation:isolate;
 animation:splashOut .55s ease 3.85s forwards}
@keyframes splashOut{to{opacity:0;visibility:hidden}}
.hl-blob{position:absolute;left:50%;top:44%;width:290px;height:290px;margin:-145px 0 0 -145px;border-radius:50%;
 filter:blur(26px);mix-blend-mode:multiply;opacity:0;
 background:radial-gradient(circle,var(--c) 0%,var(--c) 46%,rgba(255,255,255,0) 72%)}
.play .hl-blob{animation:drift 2.1s cubic-bezier(.32,.72,.2,1) forwards}
@keyframes drift{
 0%{transform:translate(var(--sx),var(--sy)) scale(1.2);opacity:0}
 14%{opacity:1}
 58%{transform:translate(calc(var(--sx)*.3),calc(var(--sy)*.3)) scale(1);opacity:1}
 82%{transform:translate(0,0) scale(.78);opacity:1}
 100%{transform:translate(0,0) scale(.62);opacity:0}}
.hl-mark{position:absolute;left:50%;top:44%;width:164px;height:214px;margin:-107px 0 0 -82px;
 background:#12181E;opacity:0;-webkit-mask-size:contain;mask-size:contain;
 -webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}
.play .hl-mark{animation:solidify 1s cubic-bezier(.22,.9,.3,1) 1.45s forwards,tolight .45s ease 3.15s forwards}
@keyframes solidify{0%{opacity:0;filter:blur(9px);transform:scale(1.09)}60%,100%{opacity:1;filter:blur(0);transform:scale(1)}}
@keyframes tolight{to{background:var(--script)}}
.hl-ring{position:absolute;left:50%;top:44%;width:180px;height:180px;margin:-90px 0 0 -90px;border-radius:50%;
 border:1.5px solid rgba(18,24,30,.5);opacity:0}
.play .hl-ring.a{animation:echo 1.5s cubic-bezier(.15,.7,.25,1) 2.25s forwards}
.play .hl-ring.b{animation:echo 1.5s cubic-bezier(.15,.7,.25,1) 2.47s forwards;border-color:rgba(18,24,30,.26)}
@keyframes echo{0%{transform:scale(.3);opacity:0}18%{opacity:.9}100%{transform:scale(3.6);opacity:0}}
.hl-flood{position:absolute;left:50%;top:44%;width:120px;height:120px;margin:-60px 0 0 -60px;border-radius:50%;
 background:#12181E;transform:scale(0)}
.play .hl-flood{animation:spread .8s cubic-bezier(.6,0,.24,1) 3.02s forwards}
@keyframes spread{to{transform:scale(16)}}
.hl-sig{position:absolute;left:0;right:0;top:calc(44% + 126px);text-align:center;opacity:0;color:#12181E}
.play .hl-sig{animation:rise .7s ease 2.5s forwards,siglight .45s ease 3.2s forwards}
@keyframes rise{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
@keyframes siglight{to{color:var(--script)}}
.hl-sigKr{font-size:15px;font-weight:800;letter-spacing:.34em;text-indent:.34em}
.hl-sigEn{font-size:9px;letter-spacing:.24em;opacity:.55;margin-top:7px}

.hl-top{position:relative;z-index:1;display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;
 padding:calc(20px + var(--sat)) calc(20px + var(--sar)) 14px calc(20px + var(--sal));overflow:hidden;
 background:linear-gradient(165deg,color-mix(in srgb,var(--accent) 15%,var(--bg)) 0%,var(--bg) 62%);
 border-bottom:1px solid var(--edge)}
.hl-topL{position:relative;z-index:2;min-width:0}
.hl-nameRow2{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-top:5px}
.hl-nameRow2 .hl-wordmark{margin-top:0}
.hl-clockBg{order:2;flex:1 0 100%;text-align:right;margin:-10px 0 0;position:relative;z-index:6;
 pointer-events:none;user-select:none;
 font-family:var(--num);font-size:clamp(54px,16vw,88px);line-height:1.02;letter-spacing:.01em;white-space:nowrap;
 background:linear-gradient(168deg,
   color-mix(in srgb,var(--accent) 40%,#fff) 0%,
   color-mix(in srgb,var(--accent) 85%,#fff) 22%,
   color-mix(in srgb,var(--accent) 34%,transparent) 44%,
   color-mix(in srgb,var(--accent) 20%,transparent) 54%,
   color-mix(in srgb,var(--accent) 82%,#000) 74%,
   color-mix(in srgb,var(--accent) 55%,#fff) 100%);
 background-size:220% 100%;
 -webkit-background-clip:text;background-clip:text;
 color:transparent;-webkit-text-fill-color:transparent;
 -webkit-text-stroke:1.3px color-mix(in srgb,var(--accent) 62%,#fff);
 animation:glassShine 8s ease-in-out infinite}
@keyframes glassShine{0%,100%{background-position:0% 0}50%{background-position:100% 0}}
.hl-meta{order:-1;position:relative;z-index:2;flex:1 0 100%;margin-top:0;margin-bottom:12px;font-family:var(--soft);font-size:13px;font-weight:700;color:var(--accent);display:flex;align-items:center;gap:6px}
.hl-meta b{font-weight:400;color:var(--accent)}
.hl-metaDot{opacity:.4;margin:0 2px}
.hl-ticker{order:3;position:relative;z-index:2;flex:1 0 100%;margin-top:6px;overflow:hidden;white-space:nowrap;
 border-top:1px dashed var(--edge);padding-top:9px}
.hl-tickerIn{display:inline-block;padding-left:100%;font-family:var(--hand);font-weight:400;font-size:25px;
 line-height:1.2;letter-spacing:.02em;color:var(--accent);animation:tick 21s linear infinite}
@keyframes tick{from{transform:translateX(0)}to{transform:translateX(-100%)}}
.hl-ticker:hover .hl-tickerIn{animation-play-state:paused}
.hl-eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--accent);text-transform:uppercase}
.hl-wordmark{font-family:var(--disp);font-size:27px;font-weight:400;letter-spacing:-.01em;margin-top:4px}
.hl-topR{position:relative;z-index:8;display:flex;flex-direction:column;align-items:flex-end;gap:6px}
.hl-ptPill{display:flex;align-items:center;gap:5px;border:1px solid var(--accent);border-radius:999px;
 padding:4px 11px;color:var(--accent);font-size:11px;font-weight:700;background:color-mix(in srgb,var(--accent) 10%,transparent)}
.hl-coin{width:9px;height:9px;border-radius:50%;background:var(--accent);flex:none}
.hl-ptPill .hl-mono{font-size:11px;letter-spacing:.02em}
.hl-who{background:none;border:1px solid var(--edge);border-radius:10px;padding:7px 11px;text-align:right;color:var(--text);cursor:pointer;line-height:1.3}
.hl-whoName{display:block;font-size:13px;font-weight:700}
.hl-whoSub{display:block;font-family:var(--mono);font-size:10px;color:var(--muted)}

.hl-main{position:relative;z-index:1;flex:1;
 padding:4px calc(20px + var(--sar)) calc(var(--tabH) + 56px) calc(20px + var(--sal));animation:hlIn .22s ease}
@keyframes hlIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

.hl-meter{border:1px solid var(--edge);border-radius:14px;padding:18px;background:var(--surface)}
.hl-meterLabel{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
.hl-meterVal{font-family:var(--mono);font-size:40px;font-weight:700;color:var(--accent);line-height:1.1;margin-top:6px}
.hl-meterVal span{font-size:16px;margin-left:3px}
.hl-meterSub{font-size:12px;color:var(--muted);margin-top:6px}

.hl-cta{width:100%;margin-top:12px;padding:15px;border-radius:12px;border:none;background:var(--accent);color:var(--on);
 font-family:var(--sans);font-size:15px;font-weight:800;cursor:pointer}
.hl-cta.done{background:var(--surface);border:1px solid var(--edge);color:var(--text);font-weight:600}
.hl-cta:focus-visible,.hl-tab:focus-visible,.hl-type:focus-visible,.hl-submit:focus-visible,.hl-theme:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

.hl-sectionHead{display:flex;justify-content:space-between;align-items:baseline;margin:26px 0 10px;font-size:12px;font-weight:700;letter-spacing:.04em;color:var(--muted)}
.hl-empty{border:1px dashed var(--edge);border-radius:12px;padding:26px 18px;text-align:center;font-size:13px;line-height:1.7;color:var(--muted)}
.hl-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
.hl-row{border:1px solid var(--edge);border-left:2px solid var(--edge);border-radius:10px;padding:12px 13px;background:var(--surface)}
.hl-row.approved{border-left-color:var(--accent)}
.hl-row.pending{border-left-color:var(--muted)}
.hl-row.rejected{opacity:.5}
.hl-rowTop{display:flex;justify-content:space-between;align-items:center}
.hl-scene{font-size:10px;letter-spacing:.14em;color:var(--muted)}
.hl-chip{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:4px;border:1px solid var(--edge);color:var(--muted)}
.hl-chip.approved{border-color:var(--accent);color:var(--accent)}
.hl-rowMain{display:flex;align-items:baseline;gap:8px;margin-top:7px}
.hl-who2{font-size:14px;font-weight:700}
.hl-act{font-size:14px;font-weight:600}
.hl-pt{margin-left:auto;font-size:12px;color:var(--accent)}
.hl-rowText{font-size:13px;line-height:1.65;color:var(--text);opacity:.82;margin:7px 0 0;white-space:pre-wrap}
.hl-rowDate{font-size:10px;color:var(--muted);margin-top:8px}
.hl-actions{display:flex;gap:8px;margin-top:11px}
.hl-approve,.hl-reject{flex:1;padding:9px;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;font-family:var(--sans)}
.hl-approve{background:var(--accent);color:var(--on);border:none}
.hl-reject{background:transparent;color:var(--muted);border:1px solid var(--edge)}

.hl-script{background:var(--script);color:var(--scriptInk);border-radius:14px;padding:20px 18px 22px;margin-top:8px;box-shadow:0 14px 34px rgba(0,0,0,.22)}
.hl-scriptHead{display:flex;justify-content:space-between;align-items:center;padding-bottom:11px;border-bottom:1px solid rgba(38,34,28,.22)}
.hl-scriptHead .hl-mono{font-size:11px;letter-spacing:.16em;font-weight:700}
.hl-date{background:none;border:none;font-size:11px;color:var(--scriptInk);opacity:.72;text-align:right;padding:0}
.hl-scriptLabel{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;opacity:.5;margin:18px 0 9px}
.hl-types{display:flex;flex-wrap:wrap;gap:6px}
.hl-type{display:flex;align-items:center;gap:7px;padding:8px 11px;border-radius:8px;cursor:pointer;border:1px solid rgba(38,34,28,.22);background:transparent;color:var(--scriptInk);font-family:var(--sans)}
.hl-type.on{background:var(--scriptInk);color:var(--script);border-color:var(--scriptInk)}
.hl-typeName{font-size:13px;font-weight:600}
.hl-typePt{font-size:10px;opacity:.6}
.hl-hint{font-size:11px;opacity:.55;margin:9px 0 0}
.hl-ta{width:100%;border:1px solid rgba(38,34,28,.22);border-radius:9px;padding:11px;background:rgba(255,255,255,.5);font-family:var(--sans);font-size:14px;line-height:1.7;color:var(--scriptInk);resize:vertical}
.hl-ta::placeholder{color:rgba(38,34,28,.34)}
.hl-ta:focus{outline:2px solid var(--scriptInk);outline-offset:1px}
.hl-err{font-size:12px;color:#A8391F;margin:9px 0 0;font-weight:600}
.hl-submit{width:100%;margin-top:16px;padding:14px;border:none;border-radius:10px;background:var(--scriptInk);color:var(--script);font-family:var(--sans);font-size:15px;font-weight:800;cursor:pointer;display:flex;justify-content:center;gap:8px}
.hl-submit .hl-mono{font-size:12px;opacity:.65;align-self:center}
.hl-fine{font-size:11px;text-align:center;margin:9px 0 0;color:var(--scriptInk);opacity:.5}
.hl-fine.hl-left{color:var(--muted);opacity:1;text-align:left;margin-top:12px;line-height:1.7}

.hl-member{display:flex;align-items:center;gap:12px;border:1px solid var(--edge);border-radius:10px;padding:12px 13px;background:var(--surface)}
.hl-member.self{border-color:var(--accent)}
.hl-rank{font-size:11px;color:var(--muted);width:20px}
.hl-memberBody{flex:1;min-width:0}
.hl-memberName{font-size:14px;font-weight:700}
.hl-memberSub{font-size:10px;margin-top:2px}
.hl-ptCol{display:flex;flex-direction:column;align-items:flex-end;gap:2px}
.hl-memberPt{font-size:14px;font-weight:700;color:var(--accent)}
.hl-memberPt2{font-size:9px;color:var(--muted)}
.hl-atTag{margin-left:7px;padding-left:7px;border-left:1px solid var(--edge);opacity:.85}

.hl-addRow{display:flex;gap:7px}
.hl-in{flex:1;min-width:0;background:var(--surface);border:1px solid var(--edge);border-radius:9px;padding:11px;color:var(--text);font-family:var(--sans);font-size:14px}
.hl-inSm{flex:0 0 68px}
.hl-in::placeholder{color:var(--muted)}
.hl-add{padding:0 16px;border-radius:9px;border:none;background:var(--accent);color:var(--on);font-weight:800;font-size:14px;cursor:pointer;font-family:var(--sans)}

/* 설정 */
.hl-themes{display:flex;gap:8px;flex-wrap:wrap}
.hl-theme{flex:1 1 84px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:14px 6px;border-radius:12px;
 border:1px solid var(--edge);background:var(--surface);color:var(--text);font-family:var(--sans);cursor:pointer}
.hl-theme.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.hl-swatch{width:34px;height:34px;border-radius:50%;border:2px solid;display:flex;align-items:flex-end;justify-content:flex-end;padding:2px}
.hl-swatch i{width:11px;height:11px;border-radius:50%;display:block}
.hl-themeName{font-size:12px;font-weight:700}

.hl-toggle,.hl-linkRow{width:100%;display:flex;justify-content:space-between;align-items:center;gap:12px;text-align:left;
 padding:15px;border:1px solid var(--edge);border-radius:12px;background:var(--surface);color:var(--text);font-family:var(--sans);cursor:pointer}
.hl-toggleName{display:block;font-size:14px;font-weight:700}
.hl-toggleSub{display:block;font-size:11px;color:var(--muted);margin-top:3px;line-height:1.5}
.hl-sw{flex:0 0 44px;height:26px;border-radius:13px;background:var(--edge);position:relative;transition:background .2s}
.hl-sw.on{background:var(--accent)}
.hl-sw i{position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:var(--surface);transition:transform .2s}
.hl-sw.on i{transform:translateX(18px)}
.hl-linkRow{font-size:14px;font-weight:700}

.hl-tabs{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--w);z-index:5;transition:opacity .18s ease;
 display:flex;background:var(--bg);border-top:1px solid var(--edge);
 padding:8px calc(8px + var(--sar)) calc(12px + var(--sab)) calc(8px + var(--sal))}
.hl-tab{flex:1;position:relative;background:none;border:none;padding:9px 1px;color:var(--muted);font-family:var(--sans);font-size:11px;font-weight:700;cursor:pointer;border-radius:8px;white-space:nowrap}
.hl-tab.on{color:var(--accent)}
.hl-badge{position:absolute;top:2px;margin-left:3px;background:var(--accent);color:var(--on);font-family:var(--mono);font-size:9px;font-weight:700;padding:1px 5px;border-radius:7px}

.hl-sheetWrap{position:fixed;inset:0;background:rgba(8,11,14,.62);display:flex;align-items:flex-end;justify-content:center;z-index:50}
/* 창이 떠 있는 동안에는 하단 탭을 감춰서 버튼이 가리지 않게 한다 */
.hl-app:has(.hl-sheetWrap) .hl-tabs,
.hl-app:has(.hl-show) .hl-tabs,
.hl-app:has(.hl-stageFx) .hl-tabs,
.hl-app:has(.hl-sheetWrap) .hl-fab,
.hl-app:has(.hl-sheetWrap) .hl-fabMenu{opacity:0;pointer-events:none}
.hl-sheet{width:100%;max-width:var(--w);background:var(--surface);border-top:1px solid var(--edge);border-radius:16px 16px 0 0;
 padding:20px calc(18px + var(--sar)) calc(26px + var(--sab)) calc(18px + var(--sal));max-height:86dvh;overflow-y:auto}
.hl-sheetTitle{font-size:15px;font-weight:800}
.hl-sheetNote{font-size:11px;color:var(--muted);line-height:1.6;margin:6px 0 14px}
.hl-pick{width:100%;display:flex;justify-content:space-between;align-items:center;padding:13px;margin-bottom:7px;background:transparent;border:1px solid var(--edge);border-radius:10px;color:var(--text);font-family:var(--sans);font-size:14px;font-weight:600;cursor:pointer}
.hl-pick.on{border-color:var(--accent);color:var(--accent)}
.hl-pick .hl-mono{font-size:10px;color:var(--muted)}

.hl-toast{position:fixed;bottom:calc(var(--tabH) + 46px);left:50%;transform:translateX(-50%);max-width:400px;width:calc(100% - 40px);
 background:var(--script);color:var(--scriptInk);padding:12px 15px;border-radius:10px;font-size:13px;font-weight:600;line-height:1.5;z-index:80;box-shadow:0 10px 28px rgba(0,0,0,.35)}

/* 로그인 */
.hl-gate{position:relative;z-index:1;padding:calc(56px + var(--sat)) calc(24px + var(--sar)) calc(40px + var(--sab)) calc(24px + var(--sal));max-width:420px;margin:0 auto;width:100%}
.hl-gateHead{margin-bottom:22px}
.hl-gateTitle{font-size:26px;font-weight:800;letter-spacing:-.02em;margin-top:4px}
.hl-gateSub{font-size:13px;color:var(--muted);line-height:1.7;margin-top:7px}
.hl-linkText{display:block;width:100%;margin-top:12px;padding:10px;background:none;border:none;
 color:var(--muted);font-family:var(--sans);font-size:13px;font-weight:600;cursor:pointer;text-decoration:underline}
.hl-gate .hl-fine{margin-top:20px}

/* 플로팅 버튼 */
.hl-fab{position:fixed;right:max(calc(18px + var(--sar)),calc(50% - (var(--w) / 2) + 18px));bottom:calc(var(--tabH) + 24px);z-index:15;width:56px;height:56px;border-radius:50%;
 border:none;color:var(--accent);cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;
 background:
   radial-gradient(circle at 34% 26%, rgba(255,255,255,.5), rgba(255,255,255,0) 48%),
   linear-gradient(160deg,color-mix(in srgb,var(--surface) 88%,#fff),var(--surface));
 box-shadow:0 8px 20px rgba(0,0,0,.32), inset 0 1.5px 2px rgba(255,255,255,.4), inset 0 -3px 6px rgba(0,0,0,.2);
 transition:transform .16s ease}
.hl-fab:hover{transform:translateY(-3px) rotate(-4deg)}
.hl-fabIcon{width:30px;height:24px;display:block;filter:drop-shadow(0 1px 1px rgba(0,0,0,.2))}
.hl-fabX{font-size:24px;line-height:1;color:var(--muted)}
.hl-fabMenu{position:fixed;right:max(calc(18px + var(--sar)),calc(50% - (var(--w) / 2) + 18px));bottom:calc(var(--tabH) + 88px);z-index:15;display:flex;flex-direction:column;gap:7px;align-items:flex-end}
.hl-fabMenu button{padding:11px 15px;border-radius:10px;border:1px solid var(--edge);background:var(--surface);
 color:var(--text);font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;
 box-shadow:0 6px 18px rgba(0,0,0,.3)}


/* 피드 */
.hl-feed{border:1px solid var(--edge);border-radius:12px;padding:14px;background:var(--surface)}
.hl-feed.pending{border-style:dashed}
.hl-feedTop{display:flex;align-items:center;gap:10px}
.hl-face.sm{width:38px;height:38px}
.hl-feedWho{flex:1;min-width:0}
.hl-feedName{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}
.hl-roleTag{font-family:var(--mono);font-size:9px;padding:1px 6px;border-radius:4px;background:var(--accent);color:var(--on)}
.hl-feedMeta{font-size:10px;margin-top:2px}
.hl-partTag{font-size:11px;font-weight:700;border:1px solid;border-radius:6px;padding:3px 8px}
.hl-feedText{font-size:14px;line-height:1.7;margin:11px 0 0;white-space:pre-wrap}
.hl-feedBar{display:flex;align-items:center;gap:9px;margin-top:11px;padding-top:10px;border-top:1px solid var(--edge)}
.hl-comBtn{margin-left:auto;background:none;border:none;color:var(--accent);font-family:var(--sans);font-size:12px;font-weight:700;cursor:pointer}
.hl-comBox{margin-top:11px;padding-top:11px;border-top:1px solid var(--edge);display:flex;flex-direction:column;gap:11px}
.hl-com{padding-left:2px}
.hl-comHead{display:flex;gap:7px;align-items:baseline;font-size:12px}
.hl-comHead .hl-mono{font-size:9px}
.hl-comText{font-size:13px;line-height:1.6;margin-top:4px}
.hl-comImg{width:100%;border-radius:9px;margin-top:7px;display:block}
.hl-imgLoad{height:120px;background:var(--edge);opacity:.35}
.hl-preview{position:relative;margin-top:4px}
.hl-preview img{width:100%;border-radius:9px;display:block}
.hl-preview button{position:absolute;top:7px;right:7px;padding:5px 10px;border-radius:7px;border:none;
 background:rgba(0,0,0,.65);color:#fff;font-family:var(--sans);font-size:11px;font-weight:700;cursor:pointer}
.hl-comInput{display:flex;gap:6px;align-items:center}
.hl-comInput .hl-in{flex:1;min-width:0;padding:10px;font-size:13px}
.hl-imgBtn{flex:none;width:38px;height:38px;border-radius:9px;border:1px solid var(--edge);display:flex;
 align-items:center;justify-content:center;cursor:pointer;font-size:17px;color:var(--muted)}
.hl-imgBtn input{display:none}

.hl-tally{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:14px}
.hl-tallyCell{border:1px solid var(--edge);border-radius:10px;padding:12px 6px;background:var(--surface);text-align:center}
.hl-tallyN{display:block;font-family:var(--mono);font-size:20px;font-weight:700}
.hl-tallyL{display:block;font-size:10px;color:var(--muted);margin-top:3px}
.hl-attCal .hl-day{cursor:default;border:1px solid transparent}
.hl-attCal .hl-day:hover{background:transparent}
.hl-attCal .hl-day.today{border-color:var(--edge)}
.hl-legend2{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.hl-lg2{display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:10px;color:var(--muted)}
.hl-lg2 i{width:9px;height:9px;border-radius:50%;display:block}

/* 출석 */
.hl-attList{gap:6px}
.hl-attItem,.hl-roleRow{display:flex;align-items:center;gap:10px;border:1px solid var(--edge);border-radius:10px;padding:10px 12px;background:var(--surface);flex-wrap:wrap}
.hl-attWho{flex:1;min-width:90px;display:flex;flex-direction:column;gap:2px}
.hl-attNm{font-size:13px;font-weight:700}
.hl-attWho .hl-mono{font-size:9px}
.hl-attBtns{display:flex;gap:4px;align-items:center}
.hl-attBtn{padding:7px 10px;border-radius:7px;border:1px solid var(--edge);background:transparent;color:var(--muted);
 font-family:var(--sans);font-size:11px;font-weight:700;cursor:pointer}
.hl-attBtn.on{background:var(--accent);border-color:var(--accent);color:var(--on)}
.hl-attRow{display:flex;align-items:center;justify-content:space-between;border:1px solid;border-radius:10px;padding:12px 14px;margin-bottom:8px}
.hl-attName{font-size:14px;font-weight:800}
.hl-dotSm{width:8px;height:8px;border-radius:50%;display:block}

.hl-teamNote{font-size:11px;line-height:1.6;color:var(--accent);margin-top:9px;padding:8px 10px;border-radius:8px;
 background:color-mix(in srgb,var(--accent) 12%,transparent)}

/* 가운데 집 버튼 */
.hl-homeBtn{flex:0 0 64px;position:relative;top:-18px;height:58px;width:58px;margin:0 auto;aspect-ratio:1;border-radius:50%;
 border:3px solid var(--bg);cursor:pointer;padding:0;overflow:hidden;isolation:isolate;
 display:flex;align-items:center;justify-content:center;transition:transform .18s ease,box-shadow .18s ease;
 color:var(--muted);
 background:
   radial-gradient(circle at 32% 24%, rgba(255,255,255,.55), rgba(255,255,255,0) 46%),
   radial-gradient(circle at 68% 82%, rgba(255,255,255,.14), rgba(255,255,255,0) 40%),
   linear-gradient(160deg, color-mix(in srgb,var(--surface) 82%,#fff), var(--surface) 52%, color-mix(in srgb,var(--surface) 78%,#000));
 box-shadow:0 7px 16px rgba(0,0,0,.3), inset 0 1.5px 2px rgba(255,255,255,.4), inset 0 -3px 6px rgba(0,0,0,.22)}
.hl-homeBtn::after{content:"";position:absolute;left:12%;top:7%;width:56%;height:34%;border-radius:50%;
 background:linear-gradient(180deg,rgba(255,255,255,.7),rgba(255,255,255,0));pointer-events:none;filter:blur(.4px)}
.hl-homeBtn:hover{transform:translateY(-3px);box-shadow:0 11px 22px rgba(0,0,0,.34), inset 0 1.5px 2px rgba(255,255,255,.45), inset 0 -3px 6px rgba(0,0,0,.22)}
.hl-homeBtn:active{transform:translateY(0)}
.hl-homeBtn.on{color:var(--on);
 background:
   radial-gradient(circle at 32% 24%, rgba(255,255,255,.62), rgba(255,255,255,0) 46%),
   radial-gradient(circle at 70% 84%, rgba(255,255,255,.18), rgba(255,255,255,0) 40%),
   linear-gradient(160deg, color-mix(in srgb,var(--accent) 74%,#fff), var(--accent) 50%, color-mix(in srgb,var(--accent) 72%,#000));
 box-shadow:0 9px 22px color-mix(in srgb,var(--accent) 48%,transparent), inset 0 1.5px 2px rgba(255,255,255,.55), inset 0 -3px 7px rgba(0,0,0,.24)}
.hl-homeIcon{width:29px;height:29px;display:block;position:relative;z-index:1;filter:drop-shadow(0 1px 0 rgba(0,0,0,.18))}

/* 일지 종류 · 파트 고정 */
.hl-kinds{display:flex;gap:7px}
.hl-kind{flex:1;text-align:left;padding:11px 12px;border-radius:10px;border:1px solid rgba(38,34,28,.22);
 background:transparent;color:var(--scriptInk);font-family:var(--sans);cursor:pointer}
.hl-kind.on{background:var(--scriptInk);color:var(--script);border-color:var(--scriptInk)}
.hl-kindN{display:block;font-size:13px;font-weight:800}
.hl-kindD{display:block;font-size:10px;opacity:.65;margin-top:3px;line-height:1.4}
.hl-partFix{display:flex;align-items:center;gap:8px;padding:11px 13px;border-radius:10px;
 border:1px solid rgba(38,34,28,.2);background:rgba(255,255,255,.4)}
.hl-partFixN{font-size:14px;font-weight:800}
.hl-partFixL{margin-left:auto;font-size:9px;letter-spacing:.14em;opacity:.5}
.hl-cancelBtn{width:100%;margin-top:9px;padding:11px;border-radius:10px;border:1px solid rgba(38,34,28,.2);
 background:transparent;color:var(--scriptInk);font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer;opacity:.7}
.hl-lockedRow{display:flex;align-items:center;gap:8px;border:1px solid var(--edge);border-radius:10px;padding:12px 13px;background:var(--surface)}
.hl-lockedName{font-size:14px;font-weight:700}
.hl-lockedRow .hl-mono{margin-left:auto;font-size:10px}

/* 출석 요약 */
.hl-attMini{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-top:14px;padding:12px 14px;border-radius:11px;
 border:1px solid rgba(38,34,28,.2);background:rgba(255,255,255,.4)}
.hl-attMini.wide{border-color:var(--edge);background:var(--surface);margin-top:8px}
.hl-attMiniL{font-size:10px;letter-spacing:.12em;opacity:.6}
.hl-attMini.wide .hl-attMiniL{color:var(--muted);opacity:1}
.hl-attMiniV{font-family:var(--mono);font-size:20px;font-weight:700;color:#8E6A2F}
.hl-attMini.wide .hl-attMiniV{color:var(--accent)}
.hl-attMiniS{font-size:10px;opacity:.6;margin-left:auto}
.hl-attMini.wide .hl-attMiniS{color:var(--muted);opacity:1}

.hl-whereNote{font-size:12px;line-height:1.6;color:var(--muted);margin:9px 0 0;padding:9px 11px;
 border-radius:9px;background:var(--surface);border:1px solid var(--edge)}

/* 로그인 브랜드 */
.hl-gateMark{display:flex;flex-direction:column;align-items:center;gap:12px;margin-bottom:26px}
.hl-gateLogo{width:74px;height:96px;background:var(--accent);
 -webkit-mask-size:contain;mask-size:contain;-webkit-mask-position:center;mask-position:center;
 -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
 animation:markIn .8s cubic-bezier(.2,.9,.3,1)}
@keyframes markIn{from{opacity:0;transform:translateY(10px) scale(.94)}to{opacity:1;transform:none}}
.hl-gateSince{font-size:9px;letter-spacing:.24em;color:var(--muted)}
.hl-gateHead{text-align:center}
.hl-roleHint{border:1px dashed var(--edge);border-radius:11px;padding:11px 13px;margin-bottom:9px;
 display:flex;flex-direction:column;gap:8px}
.hl-roleLine{display:flex;align-items:baseline;gap:9px}
.hl-roleNo{font-size:10px;color:var(--accent);font-weight:700}
.hl-roleT{font-size:13px;font-weight:800;min-width:38px}
.hl-roleD{font-size:11px;color:var(--muted)}

/* 넓은 화면 */
@media (min-width:900px){
  .hl-app{--w:720px;box-shadow:0 0 0 1px var(--edge)}
  .hl-main{padding-left:calc(32px + var(--sal));padding-right:calc(32px + var(--sar))}
  .hl-top{padding-left:calc(32px + var(--sal));padding-right:calc(32px + var(--sar))}
  .hl-invGrid,.hl-pickGrid{grid-template-columns:repeat(4,1fr)}
  .hl-houseGrid{grid-template-columns:repeat(3,1fr)}
  .hl-rollGrid{grid-template-columns:repeat(5,1fr)}
  .hl-themes .hl-theme{flex:1 1 110px}
}
@media (min-width:1240px){
  .hl-app{--w:860px}
  .hl-invGrid,.hl-pickGrid{grid-template-columns:repeat(5,1fr)}
}

/* 튜토리얼 */
.hl-tourWrap{position:fixed;inset:0;z-index:70;background:rgba(8,11,14,.72);backdrop-filter:blur(3px);
 display:flex;align-items:center;justify-content:center;
 padding:calc(22px + var(--sat)) calc(22px + var(--sar)) calc(22px + var(--sab)) calc(22px + var(--sal))}
.hl-tourCard{width:100%;max-width:380px;background:var(--surface);border:1px solid var(--edge);border-radius:18px;
 padding:20px 20px 18px;text-align:center;animation:tourIn .32s cubic-bezier(.2,1.1,.4,1)}
@keyframes tourIn{from{opacity:0;transform:translateY(14px) scale(.97)}to{opacity:1;transform:none}}
.hl-tourTop{display:flex;align-items:center;justify-content:space-between}
.hl-tourNo{font-size:10px;letter-spacing:.16em;color:var(--muted)}
.hl-tourSkip{background:none;border:none;color:var(--muted);font-family:var(--sans);font-size:12px;font-weight:700;cursor:pointer;padding:2px 4px}
.hl-tourIcon{font-size:46px;line-height:1;margin:16px 0 12px}
.hl-tourTitle{font-size:18px;font-weight:800;line-height:1.45}
.hl-tourDesc{font-size:13px;line-height:1.85;color:var(--muted);margin:10px 0 0;white-space:pre-line}
.hl-tourDots{display:flex;justify-content:center;gap:6px;margin:18px 0 14px}
.hl-tourDots i{width:6px;height:6px;border-radius:50%;background:var(--edge);cursor:pointer;transition:width .2s ease}
.hl-tourDots i.on{width:18px;border-radius:3px;background:var(--accent)}
.hl-tourCard .hl-dayBtns{margin-top:0}
.hl-app:has(.hl-tourWrap) .hl-tabs,.hl-app:has(.hl-tourWrap) .hl-fab{opacity:0;pointer-events:none}

/* 무대 물품 */
.hl-propForm{border:1px dashed var(--edge);border-radius:12px;padding:13px;margin-top:10px}
.hl-propGrid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:10px}
.hl-propCard{border:1px solid var(--edge);border-radius:12px;overflow:hidden;background:var(--surface)}
.hl-propThumb{position:relative;aspect-ratio:4/3;background:var(--bg);display:flex;align-items:center;justify-content:center}
.hl-propImg{width:100%;height:100%;object-fit:cover;display:block}
.hl-propNo{font-family:var(--mono);font-size:10px;color:var(--muted)}
.hl-propState{position:absolute;top:7px;left:7px;font-family:var(--mono);font-size:9px;font-weight:700;
 padding:3px 7px;border-radius:5px;color:#12181E}
.hl-propBody{padding:10px 11px 11px}
.hl-propName{font-size:13px;font-weight:800;display:flex;align-items:baseline;gap:6px}
.hl-propQty{font-size:11px;color:var(--accent);margin-left:auto}
.hl-propMeta{font-size:10px;color:var(--muted);margin-top:3px}
.hl-propWhere{font-size:11px;color:var(--text);opacity:.8;margin-top:6px;line-height:1.5}
.hl-propNote{font-size:11px;color:var(--muted);margin-top:4px;line-height:1.5}
.hl-propBtns{display:flex;gap:4px;flex-wrap:wrap;margin-top:9px;align-items:center}
.hl-attBtn.tiny{padding:5px 8px;font-size:10px}
.hl-kind.lock{opacity:.5;cursor:not-allowed}
@media (min-width:900px){.hl-propGrid{grid-template-columns:repeat(3,1fr)}}

/* 무지개 테마 */
.hl-app.rainbow{position:relative}
.hl-app.rainbow::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.5;
 background:linear-gradient(135deg,#FF2D6F,#FF8A1E,#FFE93D,#3DE87A,#25CFFF,#7A5CFF,#FF3DD1,#FF2D6F);
 background-size:420% 420%;animation:rainbowFlow 14s ease infinite;
 mix-blend-mode:screen;filter:saturate(1.5)}
@keyframes rainbowFlow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
.hl-app.rainbow .hl-top{background:linear-gradient(120deg,
 rgba(255,45,111,.55),rgba(255,138,30,.45) 22%,rgba(61,232,122,.4) 48%,
 rgba(37,207,255,.45) 70%,rgba(255,61,209,.55));border-bottom:2px solid #FF4FD8}
.hl-app.rainbow .hl-wordmark,.hl-app.rainbow .hl-clockBg{
 -webkit-text-stroke:1.5px #FFF;text-shadow:0 0 14px rgba(255,255,255,.55)}
.hl-app.rainbow .hl-meter,.hl-app.rainbow .hl-today,.hl-app.rainbow .hl-feed,
.hl-app.rainbow .hl-row,.hl-app.rainbow .hl-member,.hl-app.rainbow .hl-notice{
 border-width:2px;box-shadow:0 0 16px rgba(255,79,216,.28)}
.hl-app.rainbow .hl-cta:not(.done){
 background:linear-gradient(90deg,#FF2D6F,#FFB020,#3DE87A,#25CFFF,#FF3DD1);
 background-size:300% 100%;animation:rainbowFlow 5s linear infinite;color:#fff;
 text-shadow:0 1px 3px rgba(0,0,0,.45)}
.hl-app.rainbow .hl-homeBtn.on{
 background:linear-gradient(135deg,#FF2D6F,#FFE93D,#25CFFF,#FF3DD1);
 background-size:300% 300%;animation:rainbowFlow 4s linear infinite}
.hl-app.rainbow .hl-tabs{background:linear-gradient(90deg,
 rgba(20,16,30,.97),rgba(58,28,72,.97),rgba(20,16,30,.97));border-top:2px solid #FF4FD8}
.hl-app.rainbow .hl-tab.on{text-shadow:0 0 10px currentColor}
.hl-app.rainbow .hl-amb{opacity:.42}
@media (prefers-reduced-motion:reduce){
  .hl-app.rainbow::before,.hl-app.rainbow .hl-cta:not(.done),
  .hl-app.rainbow .hl-homeBtn.on{animation:none}
}

/* 새로 불러오기 */
.hl-syncBtn{width:30px;height:30px;border-radius:50%;border:1px solid var(--edge);background:var(--surface);
 color:var(--muted);cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center}
.hl-syncBtn.on{color:var(--accent)}
.hl-syncBtn.on .hl-syncIcon{animation:spin 1s linear infinite}
.hl-syncIcon{width:15px;height:15px;display:block}
.hl-syncIcon.spin{animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.hl-syncRow{width:100%;display:flex;align-items:center;gap:8px;padding:11px 13px;margin-bottom:8px;
 border:1px solid var(--edge);border-radius:10px;background:var(--surface);color:var(--muted);
 font-family:var(--sans);font-size:12px;font-weight:700;cursor:pointer}
.hl-syncRow:disabled{opacity:.6;cursor:default}
.hl-syncRow .hl-syncIcon{width:14px;height:14px;color:var(--accent)}
.hl-syncTxt{flex:1;text-align:left;color:var(--text)}

/* 연결 상태 */
.hl-linkCard{display:flex;align-items:flex-start;gap:11px;border:1px solid;border-radius:12px;padding:14px;background:var(--surface);margin-top:8px}
.hl-linkDot{width:10px;height:10px;border-radius:50%;flex:none;margin-top:4px}
.hl-linkState{font-size:15px;font-weight:800}
.hl-linkSub{font-size:12px;color:var(--muted);line-height:1.6;margin-top:4px}
.hl-errBox{border:1px solid var(--edge);border-radius:10px;padding:12px;background:var(--surface);
 font-family:var(--mono);font-size:11px;line-height:1.6;color:#D9705F;word-break:break-all;white-space:pre-wrap}
.hl-linkRow2{display:flex;align-items:center;gap:10px;border:1px solid var(--edge);border-radius:10px;padding:11px 13px;background:var(--surface)}
.hl-linkRow2 .hl-mono{flex:1;min-width:0;font-size:11px;color:var(--muted);word-break:break-all}

/* 공지 */
.hl-notice{border:1px solid var(--edge);border-left:3px solid var(--accent);border-radius:11px;padding:13px 14px;background:var(--surface)}
.hl-notice.homework{border-left-color:#E8503A}
.hl-noticeTop{display:flex;align-items:center;gap:8px}
.hl-noticeTag{font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.1em;padding:3px 8px;border-radius:5px;
 background:var(--accent);color:var(--on)}
.hl-notice.homework .hl-noticeTag{background:#E8503A;color:#fff}
.hl-noticeDue{font-size:10px;color:var(--muted)}
.hl-noticeTitle{font-size:15px;font-weight:800;margin-top:8px}
.hl-manageBtn{background:none;border:none;color:var(--muted);font-size:17px;line-height:1;cursor:pointer;padding:2px 4px}
.hl-manageBtn:hover{color:var(--accent)}
.hl-danger{background:#B03A3A;color:#fff}

/* 컨디션 */
.hl-moodRow{display:flex;gap:5px}
.hl-moodBtn{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:9px 2px;border-radius:10px;
 border:1px solid color-mix(in srgb,var(--mc) 45%,transparent);color:var(--text);font-family:var(--sans);cursor:pointer;
 background:color-mix(in srgb,var(--mc) 12%,var(--surface))}
.hl-moodBtn.on{border-color:var(--mc);box-shadow:inset 0 0 0 1.5px var(--mc);
 background:color-mix(in srgb,var(--mc) 30%,var(--surface))}
.hl-moodBtn.on .hl-moodN{color:var(--text)}
.hl-moodE{font-size:20px;line-height:1}
.hl-moodN{font-size:10px;color:var(--muted);font-weight:700}
.hl-moodBtn.on .hl-moodN{color:var(--accent)}
.hl-emos{display:flex;gap:1px;height:11px;line-height:1}
.hl-emos i{width:5px;height:5px;border-radius:50%;display:block}
.hl-moodList{display:flex;flex-direction:column;gap:6px}
.hl-moodItem{display:flex;align-items:center;gap:9px;border:1px solid var(--edge);border-radius:10px;padding:9px 12px;background:var(--surface)}
.hl-moodItem .hl-moodE{font-size:17px;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center}
.hl-moodWho{flex:1;font-size:13px;font-weight:700}

/* 댓글 창 */
.hl-comSheet{max-height:82vh;display:flex;flex-direction:column;padding-bottom:18px}
.hl-comTop{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
.hl-comTop .hl-sheetNote{margin:4px 0 0}
.hl-closeX{background:none;border:none;color:var(--muted);font-size:24px;line-height:1;cursor:pointer;padding:0 4px}
.hl-comScroll{flex:1;overflow-y:auto;margin:14px 0 10px;display:flex;flex-direction:column;gap:14px;
 border-top:1px solid var(--edge);border-bottom:1px solid var(--edge);padding:14px 0}
.hl-com{display:flex;gap:9px;align-items:flex-start}
.hl-com.sub{margin:10px 0 0 30px;padding-left:11px;border-left:2px solid var(--edge)}
.hl-face.xs{width:30px;height:30px;flex:none}
.hl-comBody{flex:1;min-width:0}
.hl-replyBtn{background:none;border:none;color:var(--muted);font-family:var(--sans);font-size:11px;
 font-weight:700;cursor:pointer;padding:4px 0 0}
.hl-replyBtn:hover{color:var(--accent)}
.hl-replyTo{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 11px;margin-bottom:8px;
 border-radius:8px;background:color-mix(in srgb,var(--accent) 12%,transparent);font-size:12px;font-weight:600;color:var(--accent)}
.hl-replyTo button{background:none;border:none;color:var(--muted);font-size:11px;font-weight:700;cursor:pointer}
.hl-lockedRow{display:flex;align-items:center;gap:8px;border:1px solid var(--edge);border-radius:10px;padding:12px 13px;background:var(--surface)}
.hl-lockedName{flex:1;font-size:14px;font-weight:700}
.hl-sel{margin-top:3px;background:var(--bg);border:1px solid var(--edge);border-radius:7px;padding:4px 7px;
 color:var(--text);font-family:var(--sans);font-size:11px}

/* 좋아요 */
.hl-likeBtn{margin-left:auto;display:flex;align-items:center;gap:4px;background:none;border:none;cursor:pointer;
 color:var(--muted);font-family:var(--sans);font-size:12px;font-weight:700}
.hl-likeBtn.on{color:#D9536B}
.hl-heart{font-size:15px;line-height:1}
.hl-likeBtn.on .hl-heart{animation:pop .3s cubic-bezier(.2,1.4,.4,1)}
.hl-likeWho{font-size:11px;color:var(--muted);margin-top:7px}
.hl-feedBar .hl-comBtn{margin-left:0}

/* 방문 · 방명록 */
.hl-visitBar{display:flex;align-items:center;gap:10px;margin-top:4px}
.hl-visitName{font-size:15px;font-weight:800}
.hl-member{position:relative}
.hl-visitHit{position:absolute;inset:0;background:transparent;border:none;cursor:pointer;border-radius:10px}
.hl-member .hl-menuArrow{position:relative}
.hl-guestRow{border:1px solid var(--edge);border-radius:10px;padding:11px 13px;background:var(--surface)}

/* 더보기 메뉴 */
.hl-menu{display:flex;flex-direction:column;gap:6px;margin-top:10px}
.hl-menuGap{height:14px}
.hl-menuRow{display:flex;align-items:center;gap:10px;padding:15px;border:1px solid var(--edge);border-radius:12px;
 background:var(--surface);color:var(--text);font-family:var(--sans);cursor:pointer;text-align:left}
.hl-menuBody{flex:1;min-width:0}
.hl-menuName{display:block;font-size:14px;font-weight:700}
.hl-menuDesc{display:block;font-size:11px;color:var(--muted);margin-top:3px}
.hl-menuBadge{font-family:var(--mono);font-size:10px;font-weight:700;background:var(--accent);color:var(--on);
 padding:2px 7px;border-radius:9px}
.hl-menuArrow{color:var(--muted);font-size:18px;line-height:1}
.hl-back{background:none;border:none;color:var(--muted);font-family:var(--sans);font-size:13px;font-weight:700;
 cursor:pointer;padding:8px 0;margin-top:4px}
.hl-pageTitle{font-size:21px;font-weight:800;letter-spacing:-.02em;margin:2px 0 14px}
.hl-quick{display:flex;gap:6px;margin-top:8px}
.hl-quick .hl-attBtn{flex:1}
.hl-chipPt{font-size:9px;opacity:.6;margin-left:5px}
.hl-grantRow{display:flex;align-items:center;gap:10px;border:1px solid var(--edge);border-radius:10px;padding:11px 13px;background:var(--surface)}
.hl-grantWhy{flex:1;min-width:0;font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hl-grantPt{font-size:13px;font-weight:700}

/* 뽑기방 */
.hl-box{position:relative;border:1px solid var(--edge);border-radius:16px;overflow:hidden;background:var(--surface);margin-top:8px}
.hl-boxGlow{position:absolute;inset:0;background:radial-gradient(70% 90% at 50% 110%,color-mix(in srgb,var(--accent) 26%,transparent),transparent 70%)}
.hl-boxInner{position:relative;padding:26px 20px;text-align:center}
.hl-boxLabel{font-size:10px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
.hl-boxPt{font-family:var(--mono);font-size:38px;font-weight:700;color:var(--accent);line-height:1.1;margin-top:6px}
.hl-boxPt span{font-size:15px;margin-left:3px}
.hl-costTag{font-size:11px;opacity:.7;margin-left:7px}
.hl-invCell.locked{opacity:.42;border-style:dashed}
.hl-lockMark{font-family:var(--mono);font-size:22px;color:var(--muted)}
.hl-reveal.wide{max-width:400px}
.hl-rollGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin:14px 0}
.hl-rollCell{border:1px solid var(--edge);border-radius:10px;padding:8px 4px;background:var(--bg);
 animation:rollIn .34s cubic-bezier(.2,1.3,.4,1) backwards}
.hl-rollCell.t-legend,.hl-rollCell.t-epic{box-shadow:0 0 14px -2px currentColor}
@keyframes rollIn{from{opacity:0;transform:scale(.7) translateY(8px)}to{opacity:1;transform:none}}

.hl-artImg{width:100%;height:100%;object-fit:contain;display:block}
.hl-artList{display:flex;flex-direction:column;gap:9px}
.hl-artRow{display:flex;gap:12px;align-items:center;border:1px solid var(--edge);border-radius:12px;padding:10px;background:var(--surface)}
.hl-artThumb.wide{flex:0 0 116px;height:74px}
.hl-artThumb{flex:0 0 78px;height:78px;border-radius:9px;overflow:hidden;background:var(--bg);display:flex;align-items:center;justify-content:center}
.hl-artThumb svg,.hl-artThumb img{width:100%;height:100%}
.hl-artBody{flex:1;min-width:0}
.hl-artBtns{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.hl-artBtns label{cursor:pointer}
.hl-hiddenFile{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.hl-pick2{margin-top:8px;padding:9px;border:1px dashed var(--edge);border-radius:10px}
.hl-pick2.over{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 8%,transparent)}
.hl-pick2:focus{outline:2px solid var(--accent);outline-offset:2px}
.hl-pickHint{display:block;font-size:10px;color:var(--muted);margin-top:7px;line-height:1.5}
.hl-pick2 .hl-artBtns{margin-top:0}
.hl-artRow{align-items:flex-start}
.hl-nameRow{display:flex;gap:6px;margin-bottom:5px}
.hl-nameRow .hl-in{flex:1;min-width:0;padding:8px 10px;font-size:13px;font-weight:700}
.hl-nameRow .hl-add{padding:0 12px;font-size:12px}
.hl-guideBox{border:1px solid var(--edge);border-radius:12px;padding:12px;background:var(--surface);margin-top:10px}
.hl-guide{width:100%;max-width:220px;height:auto;display:block;margin:0 auto 8px}
.hl-guideText{font-size:12px;line-height:1.65;color:var(--text);margin:0}
.hl-guideText.dim{color:var(--muted);font-size:11px;margin-top:7px}
.hl-charBoard{display:flex;gap:14px;align-items:center;border:1px solid var(--edge);border-radius:13px;padding:12px;background:var(--surface);margin-top:10px}
.hl-charBig{flex:0 0 92px;height:138px;display:flex;align-items:flex-end;justify-content:center}
.hl-charBig svg,.hl-charBig img{width:100%;height:100%;object-fit:contain}
.hl-charSide{flex:1;min-width:0}
.hl-dim2{opacity:.42}

/* 오늘의 운세 */
.hl-fortune{border:1px solid var(--edge);border-radius:14px;padding:15px;background:var(--surface);margin-bottom:9px}
.hl-fortune.done{background:radial-gradient(120% 90% at 50% -10%,color-mix(in srgb,var(--accent) 14%,transparent),transparent 62%) var(--surface)}
.hl-fortTop{display:flex;align-items:center;gap:11px;margin-bottom:12px}
.hl-fortIcon{font-size:26px;line-height:1}
.hl-fortTitle{font-size:15px;font-weight:800}
.hl-fortSub{font-size:11px;color:var(--muted);margin-top:2px}
.hl-fortEdit{margin-left:auto;background:none;border:none;color:var(--muted);font-family:var(--sans);
 font-size:11px;font-weight:700;cursor:pointer}
.hl-fortRank{margin-left:auto;font-size:11px;font-weight:800;color:#12181E;padding:4px 10px;border-radius:999px}
.hl-fortBar{height:7px;border-radius:4px;background:var(--edge);overflow:hidden}
.hl-fortFill{display:block;height:100%;border-radius:4px;transition:width .6s cubic-bezier(.2,.9,.3,1)}
.hl-fortScore{font-size:26px;font-weight:700;text-align:right;margin-top:5px;line-height:1}
.hl-fortHead{font-size:15px;font-weight:800;margin-top:12px;line-height:1.5}
.hl-fortBody{font-size:13px;line-height:1.8;color:var(--muted);margin:8px 0 0}
.hl-fortLucks{display:flex;flex-wrap:wrap;gap:6px;margin-top:13px}
.hl-luck{flex:1 1 30%;border:1px solid var(--edge);border-radius:9px;padding:9px;text-align:center;background:var(--bg)}
.hl-luckK{display:block;font-size:9px;color:var(--muted)}
.hl-luckV{display:block;font-size:12px;font-weight:700;margin-top:4px}
.hl-luckV.big{font-size:12px;line-height:1.4;margin-top:8px}
.hl-luckSwatch{display:block;width:22px;height:22px;border-radius:50%;margin:7px auto 0;
 border:2px solid var(--edge)}
.hl-fortPeek{font-size:12px;font-weight:700;color:var(--accent);text-align:center;margin:9px 0 0}
.hl-fortSect{margin-top:13px;padding:12px 13px;border:1px solid var(--edge);border-radius:11px;background:var(--bg)}
.hl-fortSectHead{display:flex;align-items:center;gap:7px;font-size:12px;font-weight:800}
.hl-fortDot{width:7px;height:7px;border-radius:50%;flex:none}
.hl-fortSectBody{font-size:12.5px;line-height:1.8;color:var(--muted);margin:7px 0 0}

/* 미니게임 */
.hl-gameCard{width:100%;display:flex;align-items:center;gap:12px;padding:14px;border:1px solid var(--edge);
 border-radius:14px;background:var(--surface);color:var(--text);font-family:var(--sans);cursor:pointer;text-align:left}
.hl-gameIcon{font-size:26px;line-height:1}
.hl-gameBody{flex:1;min-width:0}
.hl-gameName{display:block;font-size:15px;font-weight:800}
.hl-gameHint{display:block;font-size:11px;color:var(--muted);margin-top:3px}
.hl-gameBest{font-size:11px;color:var(--accent);font-weight:700}
.hl-game{width:100%;max-width:380px;margin:0 auto;background:var(--surface);border:1px solid var(--edge);
 border-radius:18px;padding:16px 18px calc(20px + var(--sab));text-align:center}
.hl-sheetWrap:has(.hl-game){align-items:flex-start;overflow-y:auto;-webkit-overflow-scrolling:touch;
 overscroll-behavior:contain;
 padding:calc(16px + var(--sat)) calc(16px + var(--sar)) calc(24px + var(--sab)) calc(16px + var(--sal))}
.hl-gameTop{display:flex;align-items:center;justify-content:space-between;position:sticky;top:-16px;z-index:2;
 background:var(--surface);margin:-16px -18px 0;padding:14px 18px 10px;border-radius:18px 18px 0 0;
 border-bottom:1px solid var(--edge)}
.hl-gameTop .hl-tourSkip{font-size:13px;padding:6px 10px;border:1px solid var(--edge);border-radius:8px}
.hl-gameNo{font-size:10px;letter-spacing:.18em;color:var(--accent);font-weight:700}
.hl-dial{position:relative;width:100%;max-width:240px;margin:12px auto 4px;
 user-select:none;-webkit-user-select:none;touch-action:manipulation}
.hl-dial svg{width:100%;height:auto;display:block}
.hl-dialNum{font-family:var(--mono);font-size:40px;font-weight:700;fill:var(--text)}
.hl-dialSub{font-family:var(--mono);font-size:10px;letter-spacing:.2em;fill:var(--muted)}
.hl-dial.perfect svg circle:first-child{stroke:var(--accent)}
.hl-dial.good svg circle:first-child{stroke:var(--muted)}
.hl-hitTag{position:absolute;left:50%;top:64%;transform:translateX(-50%);font-size:13px;font-weight:800;
 color:var(--accent);animation:pop .26s ease}
.hl-gameDesc{font-size:12.5px;line-height:1.8;color:var(--muted);margin:6px 0 12px}
.hl-payTable{display:flex;flex-direction:column;gap:4px;margin-bottom:12px}
.hl-payRow{display:flex;justify-content:space-between;padding:7px 11px;border:1px solid var(--edge);
 border-radius:8px;background:var(--bg);font-size:11px;color:var(--muted)}
.hl-payPt{color:var(--accent);font-weight:700}
.hl-gameResult{display:flex;flex-direction:column;align-items:center;gap:4px;margin:6px 0 14px}
.hl-gameLabel{font-size:10px;letter-spacing:.16em;color:var(--muted)}
.hl-gameBig{font-family:var(--mono);font-size:42px;font-weight:700;color:var(--accent);line-height:1}
.hl-gameBig em{font-size:14px;font-style:normal;margin-left:5px}
.hl-gameGot{font-size:13px;font-weight:800;color:var(--accent)}

/* 활동일지 필터 */
.hl-segN{font-size:10px;opacity:.7;margin-left:4px}
.hl-partScroll{display:flex;gap:6px;overflow-x:auto;padding:10px 2px 4px;margin:0 -2px 6px;
 scrollbar-width:none;-webkit-overflow-scrolling:touch}
.hl-partScroll::-webkit-scrollbar{display:none}
.hl-partChip{flex:none;display:flex;align-items:center;gap:5px;padding:7px 11px;border-radius:999px;
 border:1px solid var(--edge);background:var(--surface);color:var(--text);font-family:var(--sans);
 font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap}
.hl-partChip i{width:7px;height:7px;border-radius:50%;display:block}
.hl-partChip .hl-mono{font-size:10px;opacity:.6}
.hl-partChip.on{color:#fff;border-color:var(--accent);background:var(--accent)}
.hl-partChip.on i{background:#fff !important}
.hl-partChip.on .hl-mono{opacity:.85}

.hl-mktState{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:9px;margin-top:10px;
 border:1px solid var(--edge);background:var(--bg);font-size:11px;font-weight:700;color:var(--muted)}
.hl-mktState.open{border-color:#4FA86A;color:#4FA86A}
.hl-mktDot{width:7px;height:7px;border-radius:50%;background:var(--muted);flex:none}
.hl-mktState.open .hl-mktDot{background:#4FA86A;animation:pulseDot 1.8s ease-in-out infinite}
@keyframes pulseDot{0%,100%{opacity:1}50%{opacity:.35}}
.hl-limitTag{font-size:9px;padding:1px 5px;border-radius:4px;margin-left:5px;color:#fff;font-weight:700}
.hl-limitTag.up{background:#D94B5A}
.hl-limitTag.down{background:#3E7BD9}
.hl-feeRow{display:flex;justify-content:space-between;align-items:baseline;gap:8px;font-size:13px;
 font-weight:700;margin-top:8px}
.hl-feeRow.dim{font-size:10px;font-weight:400;color:var(--muted);margin-top:4px}
.hl-feeRow .hl-mono{color:var(--accent)}
.hl-feeRow.dim .hl-mono{color:var(--muted)}

/* 방향 걸기 */
.hl-topBtns{display:flex;gap:6px}
.hl-levBox{border:1px solid var(--edge);border-radius:12px;padding:12px;background:var(--surface)}
.hl-levPreview{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:12px}
.hl-levPreview > div{border:1px solid var(--edge);border-radius:9px;padding:9px 6px;background:var(--bg);text-align:center}
.hl-levPreview span{display:block;font-size:9px;color:var(--muted);line-height:1.4}
.hl-levPreview b{display:block;font-size:13px;font-weight:700;margin-top:4px}
.hl-posBox{border:2px solid var(--edge);border-radius:13px;padding:14px;background:var(--surface)}
.hl-posBox.up{border-color:color-mix(in srgb,#D94B5A 55%,var(--edge))}
.hl-posBox.down{border-color:color-mix(in srgb,#3E7BD9 55%,var(--edge))}
.hl-posBox.danger{border-color:#C4645C;box-shadow:0 0 18px color-mix(in srgb,#C4645C 30%,transparent)}
.hl-posTop{display:flex;align-items:center;justify-content:space-between;gap:8px}
.hl-posSide{font-size:13px;font-weight:800;padding:4px 10px;border-radius:999px;color:#fff}
.hl-posSide.long{background:#D94B5A}
.hl-posSide.short{background:#3E7BD9}
.hl-posTop .hl-mono{font-size:10px}
.hl-posPL{font-family:var(--mono);font-size:30px;font-weight:700;line-height:1;margin:12px 0 10px;
 display:flex;align-items:baseline;gap:8px}
.hl-posPL em{font-size:14px;font-style:normal}
.hl-posRate{font-size:13px;opacity:.8}
.hl-posGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:11px}
.hl-posGrid > div{border:1px solid var(--edge);border-radius:9px;padding:8px 5px;background:var(--bg);text-align:center}
.hl-posGrid span{display:block;font-size:9px;color:var(--muted)}
.hl-posGrid b{display:block;font-size:12px;font-weight:700;margin-top:3px}
.hl-posWarn{font-size:11px;line-height:1.7;color:#C4645C;font-weight:700;padding:9px 11px;margin-bottom:11px;
 border:1px solid #C4645C;border-radius:9px;background:color-mix(in srgb,#C4645C 12%,transparent)}

/* 종목 상세 */
.hl-stockEn{display:block;font-size:9px;color:var(--muted);opacity:.85;margin-top:1px;
 white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hl-detHead{margin-top:14px}
.hl-detName{font-size:19px;font-weight:800;display:flex;align-items:center;gap:4px;flex-wrap:wrap}
.hl-detEn{font-size:11px;color:var(--muted);margin-top:3px}
.hl-detCode{font-size:10px;color:var(--muted);opacity:.8;margin-top:2px}
.hl-detPrice{display:flex;align-items:baseline;gap:9px;margin:12px 0 10px;flex-wrap:wrap}
.hl-detNow{font-size:32px;font-weight:700;line-height:1}
.hl-detDiff{font-size:13px;font-weight:700}
.hl-rangeRow{display:flex;gap:5px;margin-bottom:8px}
.hl-rangeBtn{flex:1;padding:7px;border-radius:8px;border:1px solid var(--edge);background:var(--surface);
 color:var(--muted);font-family:var(--sans);font-size:11px;font-weight:700;cursor:pointer}
.hl-rangeBtn.on{background:var(--accent);border-color:var(--accent);color:var(--on)}
.hl-chartWrap{position:relative;border:1px solid var(--edge);border-radius:11px;padding:8px;
 background:var(--bg);color:var(--text)}
.hl-chart{width:100%;height:140px;display:block}
.hl-chartAxis{position:absolute;right:10px;top:8px;bottom:8px;display:flex;flex-direction:column;
 justify-content:space-between;font-size:9px;color:var(--muted);pointer-events:none}
.hl-detGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:10px}
.hl-detGrid > div{border:1px solid var(--edge);border-radius:9px;padding:9px 8px;background:var(--surface);text-align:center}
.hl-detGrid span{display:block;font-size:9px;color:var(--muted)}
.hl-detGrid b{display:block;font-size:12px;font-weight:700;margin-top:3px}
.hl-myHold{display:flex;justify-content:space-between;gap:8px;margin-top:10px;padding:11px 13px;
 border:1px solid var(--accent);border-radius:11px;background:color-mix(in srgb,var(--accent) 9%,transparent)}
.hl-myHold > div{text-align:center;flex:1}
.hl-myHold span{display:block;font-size:9px;color:var(--muted)}
.hl-myHold b{display:block;font-size:13px;font-weight:700;margin-top:2px}
.hl-admBox{border:1px dashed var(--accent);border-radius:11px;padding:12px;background:color-mix(in srgb,var(--accent) 7%,transparent)}
.hl-admNote{font-size:11px;line-height:1.7;color:var(--muted);margin:0 0 10px}
.hl-admBtns{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
.hl-admBtn{padding:10px;border-radius:9px;border:1px solid var(--edge);background:var(--surface);
 font-family:var(--mono);font-size:13px;font-weight:700;cursor:pointer}
.hl-admBtn.up{color:#D94B5A;border-color:color-mix(in srgb,#D94B5A 45%,var(--edge))}
.hl-admBtn.down{color:#3E7BD9;border-color:color-mix(in srgb,#3E7BD9 45%,var(--edge))}
.hl-admNow{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px;
 font-size:11px;font-weight:700;color:var(--accent)}
.hl-stockRow .hl-menuArrow{flex:none}

/* 한울 증권 */
.hl-market{max-width:420px}
.hl-mktSum{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;
 border:1px solid var(--edge);border-radius:12px;padding:13px 15px;background:var(--bg);margin:10px 0}
.hl-mktLabel{display:block;font-size:10px;letter-spacing:.12em;color:var(--muted)}
.hl-mktBig{display:block;font-family:var(--mono);font-size:24px;font-weight:700;color:var(--accent);line-height:1.2}
.hl-mktBig em{font-size:13px;font-style:normal;margin-left:2px}
.hl-mktRight{text-align:right}
.hl-mktMid{display:block;font-family:var(--mono);font-size:16px;font-weight:700;line-height:1.3}
.hl-stockList{list-style:none;padding:0;margin:10px 0 0;display:flex;flex-direction:column;gap:6px}
.hl-stockRow{width:100%;display:flex;align-items:center;gap:10px;padding:11px 12px;
 border:1px solid var(--edge);border-radius:11px;background:var(--surface);color:var(--text);
 font-family:var(--sans);cursor:pointer;text-align:left}
.hl-stockRow.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.hl-stockInfo{flex:1;min-width:0}
.hl-stockName{display:block;font-size:13px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hl-stockSub{display:block;font-size:9px;color:var(--muted);margin-top:2px}
.hl-stockHas{color:var(--accent);margin-left:6px;font-weight:700}
.hl-spark{flex:none;width:58px;height:26px;display:block}
.hl-stockPx{flex:none;text-align:right;min-width:70px}
.hl-stockPrice{display:block;font-size:14px;font-weight:700}
.hl-stockRate{display:block;font-size:10px;margin-top:2px}
.hl-tradeBox{border:1px solid var(--accent);border-top:none;border-radius:0 0 11px 11px;
 padding:12px;background:color-mix(in srgb,var(--accent) 7%,transparent);margin-top:-4px}
.hl-holdInfo{display:flex;justify-content:space-between;gap:8px;font-size:11px;color:var(--muted);
 padding-bottom:9px;margin-bottom:9px;border-bottom:1px dashed var(--edge)}
.hl-holdInfo b{color:var(--text)}
.hl-qtyRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.hl-qtyBtn{width:34px;height:34px;border-radius:9px;border:1px solid var(--edge);background:var(--surface);
 color:var(--text);font-size:17px;font-weight:700;cursor:pointer;flex:none}
.hl-qtyNum{min-width:52px;text-align:center;font-size:14px;font-weight:700}
.hl-tradeSum{text-align:right;font-size:16px;font-weight:700;color:var(--accent);margin:9px 0}
.hl-buyBtn{background:#D94B5A;color:#fff}
.hl-sellBtn{background:#3E7BD9;color:#fff}
.hl-buyBtn:disabled,.hl-sellBtn:disabled{opacity:.4}

/* 연승 · 꽝 */
.hl-streakBox{border:1px solid var(--accent);border-radius:13px;padding:14px;margin-top:6px;
 background:color-mix(in srgb,var(--accent) 12%,transparent)}
.hl-streakNow{display:flex;flex-direction:column;align-items:center;gap:2px}
.hl-streakLabel{font-size:10px;letter-spacing:.14em;color:var(--muted)}
.hl-streakPot{font-family:var(--mono);font-size:34px;font-weight:700;color:var(--accent);line-height:1}
.hl-streakPot em{font-size:14px;font-style:normal;margin-left:3px}
.hl-streakNext{font-size:12px;line-height:1.7;color:var(--muted);text-align:center;margin-top:9px;
 padding-top:9px;border-top:1px dashed var(--edge)}
.hl-streakNext b{color:var(--accent)}
.hl-riskBtn{border-color:#C4645C;color:#C4645C}
.hl-zeroNote{display:flex;align-items:center;gap:8px;margin-top:10px;padding:9px 11px;
 border:1px dashed var(--edge);border-radius:9px;font-size:11px;line-height:1.6;color:var(--muted)}
.hl-zeroChip{flex:none;width:24px;height:24px;border-radius:7px;background:#C4645C;color:#fff;
 font-family:var(--mono);font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center}
.hl-oeFace.zero{border-color:#C4645C}
.hl-oeFace.zero .hl-oeNum{color:#C4645C}

/* 홀짝 */
.hl-oeFace{width:118px;height:118px;margin:14px auto 6px;border-radius:26px;display:flex;
 flex-direction:column;align-items:center;justify-content:center;gap:2px;
 border:2px solid var(--edge);background:var(--bg)}
.hl-oeFace.spin{animation:oeShake .12s linear infinite}
@keyframes oeShake{0%,100%{transform:translateY(-2px)}50%{transform:translateY(2px)}}
.hl-oeFace.win{border-color:var(--accent);box-shadow:0 0 22px color-mix(in srgb,var(--accent) 45%,transparent)}
.hl-oeFace.lose{border-color:#8A97A3;opacity:.75}
.hl-oeNum{font-family:var(--mono);font-size:50px;font-weight:700;line-height:1;color:var(--text)}
.hl-oeSide{font-size:12px;font-weight:800;color:var(--accent)}
.hl-oeResult{font-size:15px;font-weight:700;margin:4px 0 10px;color:var(--muted)}
.hl-oeResult.win{color:var(--accent)}
.hl-oeResult.lose{color:#C4645C}
.hl-oeResult b{font-family:var(--mono);margin-left:4px}
.hl-oeInfo{display:flex;justify-content:space-between;padding:10px 12px;border:1px solid var(--edge);
 border-radius:10px;background:var(--bg);font-size:11px;margin-bottom:4px}
.hl-oeInfo .hl-mono:first-child{color:var(--accent);font-weight:700}
.hl-oeAmts{display:flex;flex-wrap:wrap;gap:5px}
.hl-oeAmts .hl-attBtn{flex:1 1 60px}
.hl-oeBtns{display:flex;gap:9px;margin-top:12px}
.hl-oeBtn{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:18px 8px;
 border-radius:14px;border:2px solid var(--edge);background:var(--surface);color:var(--text);
 font-family:var(--sans);cursor:pointer}
.hl-oeBtn .hl-mono{font-size:9px;color:var(--muted)}
.hl-oeBig{font-size:24px;font-weight:800}
.hl-oeBtn.odd:active,.hl-oeBtn.odd.on{border-color:#E8503A;color:#E8503A}
.hl-oeBtn.even:active,.hl-oeBtn.even.on{border-color:#45B4D6;color:#45B4D6}
.hl-oeBtn:disabled{opacity:.55;cursor:default}

.hl-oeModes{display:grid;grid-template-columns:repeat(2,1fr);gap:6px}
.hl-oeMode{display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding:10px 11px;
 border:1px solid var(--edge);border-radius:10px;background:var(--surface);color:var(--text);
 font-family:var(--sans);cursor:pointer;text-align:left}
.hl-oeMode.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.hl-oeModeN{font-size:13px;font-weight:800}
.hl-oeModeM{font-size:10px;color:var(--accent)}
.hl-oeMode.on .hl-oeModeM{font-weight:700}
.hl-numGrid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.hl-numCell{aspect-ratio:1;border:1px solid var(--edge);border-radius:10px;background:var(--surface);
 color:var(--text);font-family:var(--mono);font-size:17px;font-weight:700;cursor:pointer}
.hl-numCell.on{background:var(--accent);border-color:var(--accent);color:var(--on)}
.hl-numCell:disabled{opacity:.6}
.hl-attBtn.allin{border-color:#C4645C;color:#C4645C}
.hl-attBtn.allin.on{background:#C4645C;border-color:#C4645C;color:#fff}
.hl-betBtn{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px}

/* 큰 터치 버튼 */
.hl-tapBtn{width:100%;margin-top:12px;padding:26px 16px;border-radius:18px;border:none;cursor:pointer;
 position:relative;overflow:hidden;color:var(--on);font-family:var(--sans);
 background:linear-gradient(160deg,color-mix(in srgb,var(--accent) 78%,#fff),var(--accent));
 box-shadow:0 8px 20px color-mix(in srgb,var(--accent) 42%,transparent),
  inset 0 2px 3px rgba(255,255,255,.45), inset 0 -4px 8px rgba(0,0,0,.2);
 touch-action:manipulation;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
.hl-tapBtn:active{transform:scale(.97);box-shadow:0 3px 10px color-mix(in srgb,var(--accent) 38%,transparent),
 inset 0 2px 3px rgba(255,255,255,.35), inset 0 -2px 5px rgba(0,0,0,.24)}
.hl-tapTxt{position:relative;z-index:1;font-size:26px;font-weight:800;letter-spacing:.04em}
.hl-tapRing{position:absolute;left:50%;top:50%;width:150px;height:150px;margin:-75px 0 0 -75px;
 border-radius:50%;border:2px solid rgba(255,255,255,.5);animation:tapPulse 1.6s ease-out infinite}
@keyframes tapPulse{0%{transform:scale(.55);opacity:.7}100%{transform:scale(1.25);opacity:0}}

/* 순위별 상금 */
.hl-prizeRow{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px}
.hl-prizeChip{flex:1 1 68px;display:flex;flex-direction:column;align-items:center;gap:2px;
 padding:7px 5px;border:1px solid var(--edge);border-radius:8px;background:var(--bg);
 font-family:var(--mono);font-size:10px;color:var(--accent)}
.hl-prizeChip b{font-family:var(--sans);font-size:10px;color:var(--muted);font-weight:700}
.hl-prizeChip.gold{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,var(--bg))}
.hl-prizeChip.dim{color:var(--muted)}
.hl-rankPlays{font-size:9px;color:var(--muted);margin-left:6px;font-weight:400}
.hl-rankPrize{display:block;font-family:var(--sans);font-size:9px;color:var(--muted);font-weight:700;margin-top:1px}

/* 주간 랭킹 */
.hl-rankHead{display:flex;justify-content:space-between;align-items:baseline;margin:20px 0 8px;
 font-size:12px;font-weight:800;color:var(--text)}
.hl-rankHead .hl-mono{font-size:10px}
.hl-prize{font-size:11px;color:var(--muted);background:color-mix(in srgb,var(--accent) 10%,transparent);
 border:1px solid var(--edge);border-radius:9px;padding:9px 11px;margin-bottom:9px;text-align:center}
.hl-prize b{color:var(--accent);font-size:13px}
.hl-rankList{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:5px}
.hl-rankRow{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--edge);
 border-radius:9px;background:var(--bg)}
.hl-rankRow.me{border-color:var(--accent)}
.hl-rankRow.top{background:color-mix(in srgb,var(--accent) 12%,var(--bg));border-color:var(--accent)}
.hl-rankNo{font-size:11px;color:var(--muted);width:22px;text-align:center}
.hl-rankName{flex:1;text-align:left;font-size:13px;font-weight:700}
.hl-rankPt{font-size:13px;font-weight:700;color:var(--accent)}
.hl-rankPt em{font-size:9px;font-style:normal;margin-left:3px;opacity:.7}

/* 색 고르기 */
.hl-swatchRow{display:flex;flex-wrap:wrap;gap:8px}
.hl-skyDot,.hl-wallDot{display:flex;flex-direction:column;align-items:center;gap:5px;padding:0;
 background:none;border:none;cursor:pointer;color:var(--muted);font-family:var(--sans)}
.hl-skyDot span{width:42px;height:42px;border-radius:50%;display:block;border:2px solid var(--edge)}
.hl-skyDot.on span{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent)}
.hl-skyDot em,.hl-wallDot em{font-size:10px;font-style:normal;font-weight:700}
.hl-skyDot.on em,.hl-wallDot.on em{color:var(--accent)}
.hl-wallChip{width:46px;height:42px;border-radius:9px;overflow:hidden;border:2px solid var(--edge);
 display:flex;flex-direction:column;background:var(--surface)}
.hl-wallDot.on .hl-wallChip{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent)}
.hl-wallChip i{flex:1;display:block}
.hl-wallChip i:nth-child(3){flex:.8}
.hl-wallChip b{margin:auto;font-size:9px;font-weight:700;color:var(--muted)}

/* 집 외형 */
.hl-ext{position:relative;width:100%;margin-top:8px;border:1px solid var(--edge);border-radius:16px;overflow:hidden;
 background:linear-gradient(170deg, var(--skyA, #1B2432) 0%, var(--skyB, #2E3A4C) 100%)}
.hl-ext::after{content:"";position:absolute;left:0;right:0;bottom:0;height:34%;pointer-events:none;
 background:linear-gradient(180deg,transparent,rgba(0,0,0,.28))}
.hl-extArt{width:100%;padding:14px 10px 6px}
.hl-extArt svg{width:100%;height:auto;display:block;filter:drop-shadow(0 12px 18px rgba(0,0,0,.32))}
.hl-ext.open .hl-extArt{transform-origin:var(--dx) var(--dy);animation:extZoom .95s cubic-bezier(.55,0,.25,1) forwards}
@keyframes extZoom{0%{transform:scale(1);opacity:1}60%{transform:scale(2.1);opacity:1}100%{transform:scale(4.6);opacity:0}}
.hl-doorHit{position:absolute;background:transparent;border:none;padding:0;cursor:pointer;z-index:2}
.hl-doorRing{position:absolute;inset:-6px;border-radius:14px;border:2px dashed var(--accent);opacity:.5;
 animation:doorPulse 2.2s ease-in-out infinite}
@keyframes doorPulse{0%,100%{opacity:.35;transform:scale(1)}50%{opacity:.85;transform:scale(1.05)}}
.hl-ext.open .hl-doorRing{opacity:0}
.hl-doorGlow{position:absolute;left:var(--dx);top:var(--dy);width:10px;height:10px;margin:-5px 0 0 -5px;border-radius:50%;
 background:radial-gradient(circle,#FFF3D2,#FFD98A 42%,transparent 72%);opacity:0;pointer-events:none;z-index:3}
.hl-ext.open .hl-doorGlow{animation:doorGlow .95s cubic-bezier(.5,0,.3,1) forwards}
@keyframes doorGlow{0%{opacity:0;transform:scale(1)}30%{opacity:1;transform:scale(14)}100%{opacity:1;transform:scale(90)}}
.hl-extName{position:absolute;left:0;right:0;bottom:0;padding:22px 14px 12px;text-align:center;
 background:linear-gradient(180deg,transparent,rgba(8,11,14,.55))}
.hl-extTier{display:block;font-size:9px;letter-spacing:.2em;font-weight:700}
.hl-extTitle{display:block;font-size:15px;font-weight:800;color:#F0F3F6;margin-top:3px}
.hl-houseGrid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
.hl-houseCell{border:1px solid var(--edge);border-radius:12px;padding:8px;background:var(--surface);
 color:var(--text);font-family:var(--sans);cursor:pointer;text-align:center}
.hl-houseCell.on{box-shadow:inset 0 0 0 2px var(--accent)}
.hl-houseCell.locked{opacity:.42;border-style:dashed;cursor:default}
.hl-houseArt{height:96px;display:flex;align-items:center;justify-content:center}
.hl-houseArt svg{width:100%;height:100%}
.hl-dress{margin-top:14px}

/* 디오라마 */
.hl-dio{width:100%;display:block;border:1px solid var(--edge);border-radius:14px;background:var(--surface);overflow:visible;aspect-ratio:480/300}
.hl-slotG{cursor:pointer}
.hl-slotG:hover{filter:brightness(1.15)}

/* 세트 상점 */
.hl-setList{display:flex;flex-direction:column;gap:10px}
.hl-setCard{display:flex;gap:12px;border:1px solid var(--edge);border-radius:13px;padding:11px;background:var(--surface)}
.hl-setCard.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.hl-setPreview{position:relative;flex:0 0 120px;height:76px;border-radius:9px;overflow:hidden;background:#0D1116}
.hl-setPreview svg{width:100%;height:100%;display:block}
.hl-setLock{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
 background:rgba(8,11,14,.62);font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:#D8DEE3}
.hl-setBody{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px;justify-content:center}
.hl-setName{font-size:14px;font-weight:800}
.hl-setDesc{font-size:11px;color:var(--muted);line-height:1.5}
.hl-setBody .hl-attBtn{align-self:flex-start;margin-top:3px}
.hl-attBtn.buy{border-color:var(--accent);color:var(--accent)}
.hl-attBtn.buy:disabled{border-color:var(--edge);color:var(--muted);cursor:default}

/* 막이 오르는 연출 */
.hl-show{position:fixed;inset:0;z-index:60;overflow:hidden;background:#070A0D;display:flex;
 align-items:center;justify-content:center;flex-direction:column}
.sh-back{position:absolute;inset:0;background:radial-gradient(60% 44% at 50% 52%,color-mix(in srgb,var(--tint) 22%,transparent),transparent 72%)}
.sh-beamWrap{position:absolute;inset:0;overflow:hidden;opacity:0;animation:shBeams .9s ease .75s forwards}
@keyframes shBeams{to{opacity:1}}
.sh-beam{position:absolute;left:50%;top:-14vh;width:64px;height:150vh;transform-origin:50% 0;filter:blur(5px);
 background:linear-gradient(180deg,color-mix(in srgb,var(--tint) 72%,transparent),transparent 78%);transform:rotate(-22deg)}
.sh-beam.b2{transform:rotate(6deg);width:44px;opacity:.7}
.sh-beam.b3{transform:rotate(26deg);width:52px;opacity:.55}

.sh-stage{position:relative;width:100%;max-width:440px;padding:0 16px}
.sh-floor{position:absolute;left:8%;right:8%;bottom:-26px;height:54px;border-radius:50%;
 background:radial-gradient(50% 60% at 50% 40%,color-mix(in srgb,var(--tint) 40%,transparent),transparent 74%)}
.sh-row{display:flex;flex-wrap:wrap;justify-content:center;gap:8px}
.sh-row.n1{gap:0}
.sh-item{position:relative;width:88px;display:flex;flex-direction:column;align-items:center;
 opacity:0;animation:shRise .5s cubic-bezier(.2,1.25,.4,1) backwards}
.sh-row.n1 .sh-item{width:190px}
@keyframes shRise{from{opacity:0;transform:translateY(26px) scale(.8)}to{opacity:1;transform:none}}
.sh-item .hl-art{width:100%;height:74px}
.sh-row.n1 .hl-art{height:150px}
.sh-halo{position:absolute;top:6px;left:50%;transform:translateX(-50%);width:72px;height:72px;border-radius:50%;
 background:radial-gradient(circle,color-mix(in srgb,var(--c) 46%,transparent),transparent 70%)}
.sh-row.n1 .sh-halo{width:150px;height:150px;top:10px}
.sh-name{font-size:11px;font-weight:700;color:#E8EDF1;margin-top:4px;text-align:center;line-height:1.3}
.sh-row.n1 .sh-name{font-size:19px;font-weight:800;margin-top:10px}
.sh-tier{font-family:var(--mono);font-size:9px;color:var(--c);margin-top:2px}
.sh-row.n1 .sh-tier{font-size:11px;letter-spacing:.16em;margin-top:6px}

.sh-curtain{position:absolute;top:0;bottom:0;width:52%;z-index:2;
 box-shadow:0 0 70px rgba(0,0,0,.7) inset;
 background:repeating-linear-gradient(90deg,#8E2F35 0 16px,#6C2228 16px 30px,#A03A41 30px 44px)}
.sh-curtain.l{left:0;animation:shOpenL 1.15s cubic-bezier(.55,0,.25,1) .25s forwards}
.sh-curtain.r{right:0;animation:shOpenR 1.15s cubic-bezier(.55,0,.25,1) .25s forwards}
@keyframes shOpenL{to{transform:translateX(-96%)}}
@keyframes shOpenR{to{transform:translateX(96%)}}
.sh-valance{position:absolute;top:0;left:0;right:0;height:38px;z-index:3;
 background:linear-gradient(180deg,#A03A41,#6C2228);
 border-bottom:3px solid #C09A4E;box-shadow:0 6px 20px rgba(0,0,0,.5)}
.sh-foot{position:absolute;bottom:34px;left:0;right:0;padding:0 28px;text-align:center;opacity:0;
 transition:opacity .4s ease;pointer-events:none}
.sh-foot.on{opacity:1;pointer-events:auto}
.sh-refund{font-size:12px;color:#9BA7B2;margin-bottom:10px}

/* 등급별 연출 */
.hl-stageFx{position:fixed;inset:0;z-index:60;background:#0B0F13;display:flex;align-items:center;justify-content:center;overflow:hidden}
.fx-label{position:absolute;bottom:16%;font-family:var(--mono);font-size:13px;font-weight:700;letter-spacing:.34em;
 text-indent:.34em;opacity:0;animation:fxLabel .7s ease .35s forwards}
@keyframes fxLabel{to{opacity:1}}

.fx-lamp{width:150px;height:150px;border-radius:50%;
 background:radial-gradient(circle,#EAF0F5 0%,rgba(234,240,245,.35) 34%,transparent 68%);
 animation:lampOn .9s ease forwards}
@keyframes lampOn{0%{transform:scale(.2);opacity:0}30%{transform:scale(1.05);opacity:1}100%{transform:scale(.9);opacity:0}}

.fx-ring{position:absolute;width:130px;height:130px;border-radius:50%;border:2px solid #45B4D6;opacity:0;
 animation:fxRing 1.4s cubic-bezier(.15,.7,.25,1) forwards}
.fx-ring.d2{animation-delay:.16s}
.fx-ring.d3{animation-delay:.32s;border-color:#7FD3EC}
@keyframes fxRing{0%{transform:scale(.25);opacity:0}20%{opacity:1}100%{transform:scale(4.2);opacity:0}}

.fx-beam{position:absolute;width:76px;height:150vh;transform-origin:50% 0;transform:rotate(var(--r)) scaleY(0);
 background:linear-gradient(180deg,rgba(183,127,212,.85),rgba(183,127,212,0));top:-8vh;
 animation:fxBeam 1.9s cubic-bezier(.2,.8,.3,1) forwards;filter:blur(3px)}
.fx-beam.d2{animation-delay:.12s}
.fx-beam.d3{animation-delay:.24s}
@keyframes fxBeam{0%{transform:rotate(var(--r)) scaleY(0);opacity:0}
 26%{transform:rotate(var(--r)) scaleY(1);opacity:1}
 70%{opacity:.9}100%{opacity:0}}
.fx-core{position:absolute;width:120px;height:120px;border-radius:50%;
 background:radial-gradient(circle,#F0E4FA,#B77FD4 40%,transparent 70%);opacity:0;
 animation:fxCore 1.1s ease .55s forwards}
@keyframes fxCore{0%{transform:scale(.3);opacity:0}40%{transform:scale(1.1);opacity:1}100%{transform:scale(1.6);opacity:0}}

.fx-curtain{position:absolute;top:0;bottom:0;width:54%;background:linear-gradient(90deg,#5E1E24,#9A3038 55%,#6E2228);
 box-shadow:0 0 60px rgba(0,0,0,.6) inset}
.fx-curtain.l{left:0;transform:translateX(-100%);animation:curL 2.6s cubic-bezier(.5,0,.2,1) forwards}
.fx-curtain.r{right:0;transform:translateX(100%);animation:curR 2.6s cubic-bezier(.5,0,.2,1) forwards}
@keyframes curL{0%{transform:translateX(-100%)}26%{transform:translateX(0)}46%{transform:translateX(0)}100%{transform:translateX(-102%)}}
@keyframes curR{0%{transform:translateX(100%)}26%{transform:translateX(0)}46%{transform:translateX(0)}100%{transform:translateX(102%)}}
.fx-gold{position:absolute;width:180px;height:180px;border-radius:50%;opacity:0;
 background:radial-gradient(circle,#FFF4D6,#E9A13B 42%,transparent 72%);
 animation:fxGold 1.5s ease 1.1s forwards}
@keyframes fxGold{0%{transform:scale(.3);opacity:0}35%{transform:scale(1.2);opacity:1}100%{transform:scale(2.4);opacity:0}}
.fx-spark{position:absolute;width:7px;height:7px;border-radius:50%;background:#F5D98A;opacity:0;
 animation:fxSpark 1.4s ease calc(1.15s + var(--i) * 40ms) forwards;
 transform:rotate(calc(var(--i) * 25.7deg)) translateY(0)}
@keyframes fxSpark{0%{opacity:0;transform:rotate(calc(var(--i) * 25.7deg)) translateY(0) scale(.4)}
 30%{opacity:1}100%{opacity:0;transform:rotate(calc(var(--i) * 25.7deg)) translateY(-260px) scale(.9)}}

/* 캐릭터 */
.hl-charSpot{position:absolute;left:50%;bottom:2%;transform:translateX(-50%);width:96px;height:172px;
 display:flex;align-items:flex-end;justify-content:center;pointer-events:none;
 filter:drop-shadow(0 6px 10px rgba(0,0,0,.28))}
.hl-charArt{width:100%;height:100%}
.hl-dress{margin-top:14px}
.hl-dressSheet{max-height:86vh;overflow-y:auto}
.hl-dressTop{display:flex;align-items:center;gap:14px;padding-bottom:6px}
.hl-charPreview{width:84px;height:126px;flex:none}
.hl-chips{display:flex;flex-wrap:wrap;gap:6px}
.hl-chip2{padding:9px 13px;border-radius:9px;border:1px solid var(--edge);background:transparent;color:var(--text);
 font-family:var(--sans);font-size:13px;font-weight:600;cursor:pointer}
.hl-chip2.on{background:var(--accent);color:var(--on);border-color:var(--accent)}
.hl-dot{width:32px;height:32px;border-radius:50%;border:2px solid var(--edge);cursor:pointer;padding:0}
.hl-dot.on{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent)}
.hl-face{width:34px;height:34px;border-radius:50%;overflow:hidden;background:var(--bg);border:1px solid var(--edge);
 flex:none;display:flex;align-items:center;justify-content:center}
.hl-faceArt{width:100%;height:100%}

/* 소리 설정 */
.hl-bgmList{display:flex;flex-direction:column;gap:7px}
.hl-bgm{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:13px 15px;border:1px solid var(--edge);
 border-radius:11px;background:var(--surface);color:var(--text);font-family:var(--sans);cursor:pointer;text-align:left}
.hl-bgm.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.hl-bgmName{font-size:14px;font-weight:700}
.hl-bgmDesc{font-size:11px;color:var(--muted)}
.hl-volRow{display:flex;align-items:center;gap:11px;margin-top:11px;padding:0 2px}
.hl-volRow .hl-mono{font-size:10px;flex:none;min-width:22px}
.hl-range{flex:1;-webkit-appearance:none;appearance:none;height:4px;border-radius:2px;background:var(--edge);outline:none}
.hl-range::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:var(--accent);cursor:pointer;border:none}
.hl-range::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:var(--accent);cursor:pointer;border:none}

.hl-customBox{margin-top:10px;padding:14px;border:1px dashed var(--edge);border-radius:12px}
.hl-fileBtn{display:block;text-align:center;padding:13px;border-radius:10px;background:var(--surface);
 border:1px solid var(--edge);font-size:13px;font-weight:700;cursor:pointer;color:var(--text)}
.hl-fileBtn input{display:none}
.hl-orLine{display:flex;align-items:center;gap:10px;margin:13px 0;color:var(--muted);font-size:10px;font-family:var(--mono)}
.hl-orLine::before,.hl-orLine::after{content:"";flex:1;height:1px;background:var(--edge)}

/* 달력 */
.hl-seg{display:flex;gap:4px;padding:4px;border:1px solid var(--edge);border-radius:11px;background:var(--surface);margin-top:8px}
.hl-seg button{flex:1;padding:10px;border:none;border-radius:8px;background:transparent;color:var(--muted);
 font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer}
.hl-seg button.on{background:var(--accent);color:var(--on)}
.hl-calHead{display:flex;align-items:center;justify-content:center;gap:16px;margin:20px 0 10px}
.hl-calYm{font-size:15px;font-weight:700;letter-spacing:.08em}
.hl-navBtn{width:34px;height:34px;border-radius:9px;border:1px solid var(--edge);background:var(--surface);
 color:var(--text);font-size:18px;line-height:1;cursor:pointer}
.hl-cal{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.hl-wd{text-align:center;font-family:var(--mono);font-size:10px;color:var(--muted);padding:4px 0 8px}
.hl-wd.sun{color:#C4645C}
.hl-day{aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
 border:1px solid transparent;border-radius:9px;background:transparent;color:var(--text);cursor:pointer;padding:0}
.hl-day:hover{background:var(--surface)}
.hl-day.today{border-color:var(--edge)}
.hl-day.sel{background:var(--accent);color:var(--on);border-color:var(--accent)}
.hl-dayNum{font-family:var(--mono);font-size:13px;font-weight:600}
.hl-dayNum.sun{color:#C4645C}
.hl-day.sel .hl-dayNum,.hl-day.sel .hl-dayNum.sun{color:var(--on)}
.hl-dots{display:flex;gap:3px;height:5px}
.hl-dots i{width:5px;height:5px;border-radius:50%;display:block}
.hl-dots i.pub{background:var(--accent)}
.hl-dots i.pri{background:var(--muted)}
.hl-dots i.log{background:#45B4D6}
.hl-day.sel .hl-dots i{background:var(--on)}
.hl-ev{border:1px solid var(--edge);border-left:2px solid var(--muted);border-radius:10px;padding:12px 13px;background:var(--surface)}
.hl-ev.public{border-left-color:var(--accent)}
.hl-evTop{display:flex;align-items:center;gap:8px}
.hl-evTitle{font-size:15px;font-weight:700;margin-top:8px}
.hl-del{margin-left:auto;background:none;border:none;color:var(--muted);font-family:var(--sans);font-size:11px;font-weight:700;cursor:pointer;padding:2px 4px}
.hl-del:hover{color:#C4645C}
.hl-dayBtns{display:flex;gap:8px;margin-top:14px}
.hl-dayBtns .hl-cta{margin-top:0}
.hl-upRow{display:flex;align-items:center;gap:9px;border:1px solid var(--edge);border-radius:10px;padding:11px 13px;background:var(--surface)}
.hl-upDate{font-size:11px;color:var(--accent);font-weight:700}
.hl-upTitle{font-size:13px;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hl-upTag{font-size:9px;color:var(--muted);border:1px solid var(--edge);border-radius:4px;padding:1px 5px}
.hl-block{width:100%;margin-top:8px}
.hl-scopeLabel{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--muted);margin:16px 0 8px;text-transform:uppercase}
.hl-scopes{display:flex;gap:7px}
.hl-scope{flex:1;text-align:left;padding:12px;border:1px solid var(--edge);border-radius:10px;background:transparent;
 color:var(--text);font-family:var(--sans);cursor:pointer}
.hl-scope.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.hl-scope.locked{opacity:.5;cursor:not-allowed}
.hl-scopeName{display:block;font-size:13px;font-weight:700}
.hl-scopeSub{display:block;font-size:10px;color:var(--muted);margin-top:4px;line-height:1.45}
.hl-err.hl-onDark{color:#D9705F}

/* 마이룸 */
.hl-stage{position:relative;width:100%;height:290px;border:1px solid var(--edge);border-radius:14px;overflow:hidden;background:var(--surface)}
.hl-stageWall{position:absolute;inset:0 0 34% 0;background:linear-gradient(180deg,color-mix(in srgb,var(--text) 7%,var(--surface)),var(--surface))}
.hl-stageFloor{position:absolute;inset:66% 0 0 0;background:linear-gradient(180deg,color-mix(in srgb,var(--text) 13%,var(--surface)),color-mix(in srgb,var(--text) 5%,var(--surface)));border-top:1px solid var(--edge)}
.hl-stage.bare::after{content:"";position:absolute;inset:0;background:radial-gradient(70% 50% at 50% 62%,color-mix(in srgb,var(--accent) 9%,transparent),transparent 70%);pointer-events:none}
.hl-slot{position:absolute;transform:translate(-50%,-100%);width:calc(74px * var(--s));height:calc(74px * var(--s));
 background:none;border:none;padding:0;cursor:pointer;display:flex;align-items:flex-end;justify-content:center}
.hl-slot .hl-art{width:100%;height:100%;overflow:visible}
.hl-slotDot{width:20px;height:20px;border-radius:50%;border:1.5px dashed var(--muted);opacity:.4;margin-bottom:16%}
.hl-slot:hover .hl-slotDot{opacity:.9;border-color:var(--accent)}
.hl-slot:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:8px}
.hl-bare{font-size:13px;line-height:1.8;color:var(--muted);text-align:center;margin:14px 0 0}
.hl-gacha{display:flex;justify-content:center;align-items:center;gap:9px;margin-top:14px}
.hl-gacha .hl-mono{font-size:12px;opacity:.7}
.hl-gacha:disabled{opacity:.55;cursor:default}
.hl-rates{display:flex;gap:6px;flex-wrap:wrap}
.hl-rate{flex:1 1 68px;display:flex;align-items:center;gap:5px;border:1px solid var(--edge);border-radius:8px;padding:8px 9px;background:var(--surface)}
.hl-tierDot{width:8px;height:8px;border-radius:50%;flex:none}
.hl-rateName{font-size:12px;font-weight:700}
.hl-rate .hl-mono{font-size:10px;margin-left:auto}
.hl-invGrid,.hl-pickGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.hl-invCell,.hl-pickCell{border:1px solid var(--edge);border-radius:11px;padding:9px 6px;background:var(--surface);text-align:center;
 font-family:var(--sans);color:var(--text);cursor:pointer}
.hl-pickCell:disabled{opacity:.4;cursor:default}
.hl-invArt{height:52px;display:flex;align-items:flex-end;justify-content:center}
.hl-invArt .hl-art{width:52px;height:52px}
.hl-invName{font-size:11px;font-weight:700;margin-top:5px;line-height:1.3}
.hl-invMeta{font-size:9px;color:var(--muted);margin-top:2px}
.hl-removeBtn{width:100%;margin-top:12px;padding:12px;border-radius:10px;border:1px solid var(--edge);background:transparent;
 color:var(--muted);font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer}
.hl-reveal{width:100%;max-width:340px;margin:auto;background:var(--surface);border:1px solid var(--edge);border-radius:16px;padding:26px 20px 20px;text-align:center}
.hl-sheetWrap:has(.hl-reveal){align-items:center;padding:20px}
.hl-revealTier{font-size:10px;letter-spacing:.2em;font-weight:700}
.hl-revealArt{width:120px;height:120px;margin:14px auto 6px;display:flex;align-items:flex-end;justify-content:center;
 border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--glow) 28%,transparent),transparent 68%);
 animation:pop .45s cubic-bezier(.2,1.3,.4,1)}
.hl-revealArt .hl-art{width:96px;height:96px}
@keyframes pop{from{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}
.hl-revealName{font-size:17px;font-weight:800;margin-top:4px}
.hl-reveal .hl-sheetNote{text-align:center;margin:8px 0 16px}

@media (prefers-reduced-motion:reduce){
 .hl-main{animation:none}
 .hl-clockBg,.hl-ambBlob,.hl-revealArt,.hl-rollCell,.sh-item,.sh-curtain,.sh-beamWrap{animation-duration:.01s}
 .play .hl-blob,.play .hl-ring,.play .hl-flood{animation:none;opacity:0}
 .play .hl-mark{animation:none;opacity:1}
 .play .hl-sig{animation:none;opacity:1}
}
`;


/* 어디선가 터져도 흰 화면 대신 원인을 보여준다 */
class Boundary extends Component {
  constructor(p) { super(p); this.state = { err: null, wiping: false }; }
  static getDerivedStateFromError(err) { return { err }; }

  async wipe() {
    /* 이 기기에 남은 것만 비운다. 서버(모두가 함께 보는 기록)는 절대 건드리지 않는다. */
    this.setState({ wiping: true });
    try {
      const keep = [];
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k && k.startsWith("hanul:")) keep.push(k);
      }
      keep.forEach((k) => { try { localStorage.removeItem(k); } catch (e) {} });
      if (window.caches) {
        const ks = await caches.keys();
        await Promise.all(ks.map((k) => caches.delete(k)));
      }
    } catch (e) {}
    location.reload();
  }

  render() {
    if (!this.state.err) return this.props.children;
    const msg = String(this.state.err?.message || this.state.err);
    const st = String(this.state.err?.stack || "").split("\n").slice(0, 4).join("\n");
    return (
      <div style={{
        minHeight: "100vh", background: "#12181E", color: "#E4E9ED", padding: "48px 24px",
        fontFamily: '-apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo","Malgun Gothic",sans-serif',
        maxWidth: 460, margin: "0 auto",
      }}>
        <div style={{ fontFamily: "ui-monospace,Menlo,monospace", fontSize: 10, letterSpacing: ".16em", color: "#E9A13B" }}>ERROR</div>
        <h2 style={{ fontSize: 22, fontWeight: 800, margin: "6px 0 10px" }}>화면을 그리다 멈췄어요</h2>
        <p style={{ fontSize: 13, lineHeight: 1.7, color: "#8A97A3", margin: 0 }}>
          아래 내용을 그대로 알려주면 어디가 문제인지 찾을 수 있어요.
        </p>
        <pre style={{
          marginTop: 16, padding: 14, borderRadius: 10, background: "#1A222B", border: "1px solid #2B3742",
          fontSize: 11, lineHeight: 1.6, color: "#E4E9ED", whiteSpace: "pre-wrap", wordBreak: "break-all",
        }}>{msg}{st ? "\n\n" + st : ""}</pre>
        <button onClick={() => location.reload()} style={{
          width: "100%", marginTop: 18, padding: 15, borderRadius: 11, border: "none",
          background: "#E9A13B", color: "#20160A", fontSize: 15, fontWeight: 800, cursor: "pointer",
        }}>새로고침</button>
        <button onClick={() => this.setState({ err: null })} style={{
          width: "100%", marginTop: 9, padding: 14, borderRadius: 11, border: "1px solid #2B3742",
          background: "transparent", color: "#E4E9ED", fontSize: 14, fontWeight: 700, cursor: "pointer",
        }}>이전 화면으로 돌아가기</button>
        <button onClick={() => this.wipe()} disabled={this.state.wiping} style={{
          width: "100%", marginTop: 22, padding: 13, borderRadius: 10, border: "1px solid #2B3742",
          background: "transparent", color: "#8A97A3", fontSize: 13, fontWeight: 700, cursor: "pointer",
        }}>{this.state.wiping ? "정리 중…" : "이 기기 설정만 정리하고 다시 열기"}</button>
        <p style={{ fontSize: 11, lineHeight: 1.7, color: "#8A97A3", marginTop: 10 }}>
          이 기기에 남은 임시 설정만 지웁니다. 모두가 함께 보는 기록(일지 · 포인트 · 단원)은 그대로 남아요.
        </p>
      </div>
    );
  }
}

function HanulRoot() {
  return <Boundary><HanulApp /></Boundary>;
}


ReactDOM.createRoot(document.getElementById("root")).render(<HanulRoot />);
