/* global React, diary, SpriteIcon, buildBackground */
// ===========================================================
// 작업방 (함께 작업방) — 메인 뷰
//
// 철학:
//  • "조용히 옆에 누가 같이 일하고 있는 느낌" (body doubling) — 통화/채팅/푸시 없음
//  • 흐르는 시간·방 총 작업시간은 클라이언트 시계(workStartedAt / workSecondsTotal 합산, 서버 비용 0)
//  • 레벨(roomLevel)은 오늘 할 일 전부 완료한 날마다 +1, 멤버 문서에 저장
//  • 오프라인은 회색 카드로 남김 (감시 X, 따뜻한 느낌 O)
//
// 구조:
//  RoomView
//    ├ Loading
//    ├ EmptyRoomView   (방 만들기 / 초대 코드로 들어가기)
//    └ RoomMainView    (헤더 + 멤버 카드 N개 + 빈 슬롯 + 하단 합계 바)
// ===========================================================
const { useState: useStateRoom, useEffect: useEffectRoom, useRef: useRefRoom } = React;

function roomFlat() {
  return typeof window.isOfficeSkin === "function" && window.isOfficeSkin();
}
function roomExcel() {
  return typeof window.isExcelSkin === "function" && window.isExcelSkin();
}
function roomFill(gradient, flat = "var(--paper)") {
  return roomFlat() ? flat : gradient;
}
function roomFill2(gradient, flat = "var(--paper-2)") {
  return roomFlat() ? flat : gradient;
}
function getStatusDotColor(status, fallback) {
  if (typeof window.roomStatusDotColor === "function") {
    const c = window.roomStatusDotColor(status);
    if (c) return c;
  }
  return fallback;
}
function uiIcon(key, fallback) {
  return typeof window.skinG === "function" ? window.skinG(key, fallback) : fallback;
}

// ---- 로컬 유틸 ----
function fmtHMS(sec) {
  sec = Math.max(0, Math.floor(sec));
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = sec % 60;
  if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
function fmtHM(sec) {
  sec = Math.max(0, Math.floor(sec));
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  if (h > 0) return `${h}h ${m}m`;
  return `${m}m`;
}
function fmtRoomClock(ts) {
  if (!ts) return "";
  const d = new Date(ts);
  const h = d.getHours();
  const m = String(d.getMinutes()).padStart(2, "0");
  const ampm = h < 12 ? L("time.am") : L("time.pm");
  const h12 = ((h + 11) % 12) + 1;
  return `${ampm} ${h12}:${m}`;
}

const ROOM_LOG_KST_OFFSET_MS = 9 * 60 * 60 * 1000;
const ROOM_LOG_CLAP_GAP_MS = 90 * 1000;

function kstYmd(ts) {
  const d = new Date((ts || Date.now()) + ROOM_LOG_KST_OFFSET_MS);
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  return `${y}-${m}-${dd}`;
}

function kstDayStartMs(ts) {
  const ymd = kstYmd(ts);
  return Date.parse(`${ymd}T00:00:00+09:00`);
}

function logIsToday(at, now) {
  return (at || 0) >= kstDayStartMs(now);
}

function tsLikeToMs(t) {
  if (t == null) return null;
  if (typeof t === "number") return t;
  if (typeof t.toMillis === "function") return t.toMillis();
  if (typeof t.seconds === "number") return t.seconds * 1000;
  if (typeof t._seconds === "number") return t._seconds * 1000;
  if (typeof t === "string") {
    const n = Date.parse(t);
    return Number.isNaN(n) ? null : n;
  }
  return null;
}

/** 방 생성 시각 — room.createdAt 우선, 없으면 가장 이른 멤버 joinedAt */
function roomCreatedAtMs(meta, members) {
  const fromMeta = tsLikeToMs(meta && meta.createdAt);
  if (fromMeta != null) return fromMeta;
  if (!Array.isArray(members) || !members.length) return null;
  const joins = members.map((m) => tsLikeToMs(m.joinedAt)).filter((x) => x != null);
  return joins.length ? Math.min(...joins) : null;
}

/** KST 달력일 기준 경과일 — 생성 당일 D+0, 다음날부터 D+1… */
function kstCalendarDaysSince(startMs, endMs) {
  const a = Date.parse(`${kstYmd(startMs)}T00:00:00+09:00`);
  const b = Date.parse(`${kstYmd(endMs)}T00:00:00+09:00`);
  return Math.max(0, Math.round((b - a) / 86400000));
}

function roomDaysSinceCreated(meta, now, members) {
  const ms = roomCreatedAtMs(meta, members);
  if (ms == null) return 0;
  return kstCalendarDaysSince(ms, now);
}

/** 오늘 할 일 전부 완료 */
function memberTodayAllDone(done, total) {
  const t = total || 0;
  const d = done || 0;
  return t > 0 && d >= t;
}

/** 저장된 roomLevel — 오늘을 정복했을 때만 표시 (완료한 날마다 +1) */
function memberDisplayLevel(member, done, total) {
  if (!memberTodayAllDone(done, total)) return null;
  const lv = member?.roomLevel || 0;
  return lv > 0 ? lv : null;
}

/** 방 누적 작업시간(초) — 멤버 문서 workSecondsTotal + 진행 중 세션(클라이언트 시계) */
function memberRoomWorkSeconds(m, now, myUid, myStatus) {
  const committed = m.workSecondsTotal || 0;
  return committed + memberActiveSessionSeconds(m, now, myUid, myStatus);
}

function MemberExpOrLevel({ member, done, total, barWidth = 48, barHeight = 5 }) {
  const level = memberDisplayLevel(member, done, total);
  if (level != null) {
    return (
      <span style={{
        fontFamily: "var(--hand)", fontSize: 11, fontWeight: 800,
        color: "var(--point)", letterSpacing: "0.02em",
      }}>
        {L("room.levelShort", { n: level })}
      </span>
    );
  }
  const progress = total > 0 ? Math.min(1, done / total) : 0;
  return (
    <span title={L("room.expHint", { done, total })} style={{
      width: barWidth, height: barHeight, borderRadius: 99,
      border: "1px solid var(--ink)",
      background: "var(--paper)", overflow: "hidden", display: "inline-block",
    }}>
      <span style={{
        display: "block", width: `${progress * 100}%`, height: "100%",
        background: roomFill("linear-gradient(90deg, var(--point), var(--hi))", "var(--ink-3)"),
        transition: "width 0.35s",
      }} />
    </span>
  );
}

const ROOM_SUN_SPRITE_IDX = 6; // Sprite-0002 sun (view-today MOODS "happy")
const ROOM_DISMISS_NEW_DAY_KEY = "todoary.room.dismissedNewDay";
const ROOM_STATUS_MSG_MAX = 25;
const ROOM_STATUS_MSG_COOLDOWN_MS = 5 * 60 * 1000;

function clampStatusMessage(raw) {
  if (raw == null) return "";
  return String(raw).slice(0, ROOM_STATUS_MSG_MAX);
}

function statusMessageCooldownRemaining(member, now) {
  const at = member?.statusMessageAt || 0;
  if (!at) return 0;
  const left = ROOM_STATUS_MSG_COOLDOWN_MS - (now - at);
  return left > 0 ? left : 0;
}

function formatStatusCooldown(ms) {
  if (ms <= 0) return "";
  const totalSec = Math.ceil(ms / 1000);
  const min = Math.floor(totalSec / 60);
  const sec = totalSec % 60;
  if (min > 0 && sec > 0) return L("room.statusMessageCooldownMinSec", { min, sec });
  if (min > 0) return L("room.statusMessageCooldownMin", { min });
  return L("room.statusMessageCooldownSec", { sec });
}

function roomNoticesStorageKey(roomId) {
  return `todoary.room.notices.${roomId || "none"}`;
}

function loadRoomNotices(roomId) {
  try {
    const raw = localStorage.getItem(roomNoticesStorageKey(roomId));
    if (!raw) return { celebrations: [], dismissed: [] };
    const data = JSON.parse(raw);
    return {
      celebrations: Array.isArray(data.celebrations) ? data.celebrations : [],
      dismissed: Array.isArray(data.dismissed) ? data.dismissed : [],
    };
  } catch (_) {
    return { celebrations: [], dismissed: [] };
  }
}

function saveRoomNotices(roomId, data) {
  try {
    localStorage.setItem(roomNoticesStorageKey(roomId), JSON.stringify(data));
  } catch (_) {}
}

function roomNoticeTickerStyle() {
  return {
    position: "relative",
    marginBottom: 6,
    height: 26,
    minHeight: 26,
    borderRadius: roomFlat() ? 3 : 6,
    overflow: "hidden",
    display: "flex",
    alignItems: "center",
    background: roomFlat()
      ? "var(--paper-2)"
      : "linear-gradient(90deg, color-mix(in srgb, #fff 94%, var(--paper)) 0%, color-mix(in srgb, #fff 80%, var(--point)) 50%, color-mix(in srgb, #fff 90%, var(--hi-soft)) 100%)",
    border: roomFlat()
      ? "1px solid var(--ink-soft)"
      : "1px solid color-mix(in srgb, var(--ink-soft) 45%, var(--point))",
    boxShadow: roomFlat() ? "none" : "0 1px 0 color-mix(in srgb, #fff 75%, var(--paper)) inset",
    fontFamily: "var(--hand)", fontSize: 12, fontWeight: roomFlat() ? 600 : 700,
    color: "var(--ink)",
  };
}

function RoomDismissibleBanner({ onClose, children, leadingIcon }) {
  const [hover, setHover] = useStateRoom(false);
  const label = children == null ? "" : String(children);
  const segment = (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
      {leadingIcon}
      <span>{label}</span>
    </span>
  );

  return (
    <div
      className={"room-notice-ticker" + (roomFlat() ? " room-flat-ticker" : "")}
      style={{ ...roomNoticeTickerStyle(), gap: 4, paddingLeft: 4, paddingRight: 6 }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
      <button
        type="button"
        onClick={onClose}
        title={L("room.dismissNotice")}
        style={{
          all: "unset",
          flexShrink: 0,
          cursor: hover ? "pointer" : "default",
          width: hover ? 18 : 0,
          height: 18,
          overflow: "hidden",
          borderRadius: 4,
          display: "grid",
          placeItems: "center",
          border: hover ? "1px solid color-mix(in srgb, var(--ink-soft) 70%, var(--point))" : "none",
          background: hover ? "color-mix(in srgb, #fff 90%, var(--paper))" : "transparent",
          fontFamily: "var(--mono)",
          fontSize: 12,
          lineHeight: 1,
          color: "var(--ink-2)",
          opacity: hover ? 1 : 0,
          pointerEvents: hover ? "auto" : "none",
          transition: "opacity 0.15s ease, width 0.15s ease",
        }}
      >
        ×
      </button>
      <div
        className="marquee"
        style={{ flex: 1, minWidth: 0, height: "100%", display: "flex", alignItems: "center" }}
      >
        <span className="marquee-inner room-notice-marquee-inner">
          {segment}
          <span aria-hidden>　　</span>
          {segment}
        </span>
      </div>
    </div>
  );
}

function isClapLogText(text) {
  if (!text) return false;
  const t = String(text);
  if (/박수|clap|拍手|掌声|👏/i.test(t)) return true;
  try {
    const hint = typeof L === "function" ? L("room.clapHint") : "";
    if (hint && t.includes(hint)) return true;
  } catch (_) {}
  return false;
}

function isClapLogEvent(ev) {
  return ev && (ev.kind === "clap" || isClapLogText(ev.text));
}

function celebrationMessage(displayName) {
  return L("room.celebrate", { name: displayName || L("room.friend") });
}

function formatClapLogText(fromUid, toUid, nameByUid) {
  const from = nameByUid.get(fromUid) || L("room.friend");
  const to = nameByUid.get(toUid) || L("room.friend");
  return L("room.clapDirected", { from, to });
}

function collapseClapLogEvents(events) {
  const out = [];
  let i = 0;
  while (i < events.length) {
    const ev = events[i];
    if (!isClapLogEvent(ev)) {
      out.push({ ...ev, collapsed: false });
      i += 1;
      continue;
    }
    let j = i + 1;
    while (j < events.length) {
      const next = events[j];
      if (!isClapLogEvent(next) || next.uid !== ev.uid) break;
      if ((next.at || 0) - (events[j - 1].at || 0) > ROOM_LOG_CLAP_GAP_MS) break;
      j += 1;
    }
    const count = j - i;
    if (count >= 2) {
      out.push({
        ...ev,
        collapsed: true,
        count,
        id: `clap-run-${ev.uid}-${ev.at || 0}`,
        children: events.slice(i, j),
      });
      i = j;
    } else {
      out.push({ ...ev, collapsed: false });
      i += 1;
    }
  }
  return out;
}

// presence — heartbeat write·오프라인 판정 (Firestore 비용과 연동, 값 함께 조정)
const ROOM_HEARTBEAT_INTERVAL_MS = 4 * 60 * 1000; // setInterval tick
const ROOM_HEARTBEAT_MIN_AGE_MS = 4 * 60 * 1000;  // 로컬 전송 간격 가드
const ROOM_MEMBER_OFFLINE_MS = 10 * 60 * 1000;    // 초과 시 오프라인 (interval 2회분 + 여유)
const ROOM_MEMBER_GHOST_KICK_MS = 60 * 60 * 1000; // 호스트가 유령 멤버 자동 제거 (#19)

function isRoomAppActive() {
  return typeof navigator !== "undefined" && navigator.onLine
    && (typeof document === "undefined" || !document.hidden);
}

function isMemberOffline(m, now) {
  if (!m.lastActiveAt) return true;
  return (now - m.lastActiveAt) > ROOM_MEMBER_OFFLINE_MS;
}

// 멤버의 현재 누적 작업시간(초) — 로컬 계산.
// accumulatedSecondsToday(커밋된 활성 구간) + 진행 중 세션(workStartedAt~cutoff).
// away/paused 는 진행 중 세션을 더하지 않음(서버에 commit 되었거나 곧 commit).
// 본인 commit 못 한 채로 끊기면 lastActiveAt 을 cutoff 로 사용(오프라인 표시용).
function memberWorkTimePaused(m, myUid, myStatus) {
  const st = (myUid && m.uid === myUid && myStatus) ? myStatus : (m.status || "working");
  return st === "away" || st === "paused";
}

/** 진행 중 세션만 — workSecondsTotal 에 아직 반영되지 않은 구간 */
function memberActiveSessionSeconds(m, now, myUid, myStatus) {
  if (memberWorkTimePaused(m, myUid, myStatus) || !m.workStartedAt) return 0;
  const off = isMemberOffline(m, now);
  const cutoff = off ? Math.min(m.lastActiveAt || m.workStartedAt, now) : now;
  if (cutoff <= m.workStartedAt) return 0;
  return Math.floor((cutoff - m.workStartedAt) / 1000);
}

function memberLiveSeconds(m, now, myUid, myStatus) {
  const acc = m.accumulatedSecondsToday || 0;
  if (memberWorkTimePaused(m, myUid, myStatus) || !m.workStartedAt) return acc;
  const off = isMemberOffline(m, now);
  const cutoff = off ? Math.min(m.lastActiveAt || m.workStartedAt, now) : now;
  if (cutoff <= m.workStartedAt) return acc;
  return acc + Math.floor((cutoff - m.workStartedAt) / 1000);
}
function roomAvatarIdx(value) {
  return Number.isInteger(value) && value >= 0 && value < 24 ? value : 22;
}
function RoomAvatarIcon({ value, size = 24 }) {
  return <SpriteIcon idx={roomAvatarIdx(value)} size={size} />;
}
function visibleTodoTitle(item) {
  if (typeof item === "string") return item;
  if (item && typeof item.title === "string") return item.title;
  return "";
}
function visibleTodoDone(item) {
  return typeof item === "object" && item ? !!item.done : false;
}
function roomVisibleSubTasks(todo) {
  return (todo?.subTasks || [])
    .map((st) => {
      const title = String(st?.title || st || "").trim();
      if (!title) return null;
      return { id: st?.id, title, done: !!st.done };
    })
    .filter(Boolean);
}
function roomVisibleTodoPayload(todo) {
  return {
    title: todo.title,
    done: !!todo.done,
    subTasks: roomVisibleSubTasks(todo),
  };
}
function visibleTodoSig(item) {
  const title = visibleTodoTitle(item);
  const done = visibleTodoDone(item) ? 1 : 0;
  const subs = (item?.subTasks || []).map((st) => {
    const stId = typeof st === "object" && st?.id ? st.id : "";
    const stTitle = typeof st === "string" ? st : (st?.title || "");
    const stDone = typeof st === "object" && st && st.done ? 1 : 0;
    return `${stId}:${stDone}:${stTitle}`;
  }).join(",");
  return `${done}:${title}|${subs}`;
}
function roomTodoView(item, id) {
  const subs = Array.isArray(item?.subTasks)
    ? item.subTasks.map((st, i) => {
      const title = typeof st === "string" ? st : (st?.title || "");
      const trimmed = String(title).trim();
      const subId = (typeof st === "object" && st?.id) ? st.id : `${id}-s${i}`;
      return {
        id: subId,
        title: trimmed,
        done: typeof st === "object" && st ? !!st.done : false,
      };
    }).filter((s) => s.title)
    : [];
  const parentDone = visibleTodoDone(item);
  const allSubsDone = subs.length > 0 && subs.every((s) => s.done);
  return {
    id,
    title: visibleTodoTitle(item),
    done: subs.length > 0 ? (parentDone || allSubsDone) : parentDone,
    subTasks: subs,
  };
}

/** 공개 할 일 + 세부목록 행 (채팅·멤버 카드 공용) */
function RoomPublicTodoRows({ todos, isMe, onToggleTodo, onToggleSub, subIndent = 18 }) {
  return todos.map((t) => (
    <React.Fragment key={t.id}>
      <div style={{
        display: "flex", alignItems: "flex-start", gap: 6,
        padding: "2px 0",
        fontFamily: "var(--hand)", fontSize: 12.5, lineHeight: 1.35,
        color: "var(--ink)",
      }}>
        <button
          onClick={isMe && onToggleTodo ? () => onToggleTodo(t.id) : undefined}
          title={isMe ? L("room.toggleDone") : ""}
          style={{
            all: "unset", cursor: isMe && onToggleTodo ? "pointer" : "default",
            width: 10, height: 10, marginTop: 3, flexShrink: 0,
            border: "1px solid var(--ink)", borderRadius: 2,
            background: t.done ? "var(--mint)" : "var(--paper)",
            display: "grid", placeItems: "center",
            fontSize: 8, color: "#fff",
          }}
        >{t.done ? "✓" : ""}</button>
        <span style={{
          textDecoration: t.done ? "line-through" : "none",
          wordBreak: "break-word",
        }}>{t.title}</span>
      </div>
      {(t.subTasks || []).map((st) => (
        <div key={st.id} style={{
          display: "flex", alignItems: "flex-start", gap: 5,
          padding: "1px 0 1px " + subIndent + "px",
          fontFamily: "var(--hand)", fontSize: 11, lineHeight: 1.3,
          color: "var(--ink-2)",
        }}>
          <button
            onClick={isMe && onToggleSub ? () => onToggleSub(t.id, st.id) : undefined}
            title={isMe ? L("room.toggleSubDone") : ""}
            style={{
              all: "unset", cursor: isMe && onToggleSub ? "pointer" : "default",
              width: 8, height: 8, marginTop: 2, flexShrink: 0,
              border: "1px solid var(--ink-soft)", borderRadius: 2,
              background: st.done ? "var(--mint)" : "var(--paper)",
              display: "grid", placeItems: "center",
              fontSize: 7, color: "#fff",
            }}
          >{st.done ? "✓" : ""}</button>
          <span style={{
            textDecoration: st.done ? "line-through" : "none",
            wordBreak: "break-word",
          }}>{st.title}</span>
        </div>
      ))}
    </React.Fragment>
  ));
}
function memberRoomStatus(member, now, myUid, myStatus) {
  const isMe = member.uid === myUid;
  const effectiveStatus = isMe ? myStatus : (member.status || "working");
  if (effectiveStatus === "away") return "away";
  if (effectiveStatus === "paused") return "paused";
  // 복귀·재연결 직후: heartbeat 가 멈춰 lastActiveAt 이 오래됐어도 앱 활성+working 이면 오프라인 깜빡임 방지
  if (isMe && myStatus === "working" && isRoomAppActive() && isMemberOffline(member, now)) {
    return member.workStartedAt ? "working" : "online";
  }
  if (isMemberOffline(member, now)) return "offline";
  if (member.workStartedAt) return "working";
  return "online";
}
function roomStatusText(status) {
  if (status === "away") return L("room.statusAwayLog");
  if (status === "offline") return L("room.statusOfflineLog");
  if (status === "online" || status === "working") return L("room.statusOnlineLog");
  if (status === "paused") return L("room.statusPausedLog");
  return "";
}
function shouldLogRoomStatusChange(uid, myUid, prev, next) {
  if (!prev || prev === next) return false;
  // 내 자동 온라인/오프라인은 재접속·pending timestamp 때 흔들릴 수 있어 로그로 쌓지 않는다.
  if (uid === myUid && (prev === "online" || prev === "working" || prev === "offline") && (next === "online" || next === "working" || next === "offline")) {
    return false;
  }
  return true;
}
const STATUS_EVENT_COALESCE_MS = 30000;
const NOTICE_VISIBLE_MS = 7000;
function mergeRoomStatusEvents(events, added) {
  let next = [...events];
  added.forEach((ev) => {
    let idx = -1;
    for (let i = next.length - 1; i >= 0; i--) {
      const old = next[i];
      if (old.kind === "status" && old.uid === ev.uid && ev.at - old.at < STATUS_EVENT_COALESCE_MS) {
        idx = i;
        break;
      }
    }
    if (idx >= 0) next[idx] = ev;
    else next.push(ev);
  });
  return next.slice(-50);
}
function roomThemeBackground(tweaks) {
  if (roomFlat()) return "var(--paper-2)";
  if (typeof buildBackground !== "function") {
    return "linear-gradient(180deg, var(--paper-2) 0%, var(--paper) 100%)";
  }
  return buildBackground(
    tweaks?.bgType ?? "linear",
    tweaks?.bgAngle ?? 180,
    tweaks?.bgStops ?? [{ c: "#a9cdf5", p: 0 }, { c: "#ffffff", p: 100 }],
    tweaks?.bgShape ?? "none"
  );
}

// ---- 1초마다 리렌더 트리거 (작업시간 표시용) ----
function useNowTick() {
  const [now, setNow] = useStateRoom(Date.now());
  useEffectRoom(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
  return now;
}

// ===========================================================
// 최상위 RoomView
// ===========================================================
function RoomView({ tweaks } = {}) {
  const room = window.roomStore.useRoom();
  useI18n();

  // 부팅 — 마운트 시 1회
  useEffectRoom(() => { room.bootstrap(); }, []);

  // 탭 진입 시 watch 붙이기 / 이탈 시 떼기 (Firestore 읽기 비용 최소화)
  const roomId = room.state.currentRoomId;
  const status = room.state.bridgeStatus;
  useEffectRoom(() => {
    if (!roomId || (status !== "ready" && status !== "local")) return;
    window.roomStore.attachToRoom(roomId);
    return () => window.roomStore.detachAll();
  }, [roomId, status]);

  // (Self → 서버 동기화는 RoomSyncBridge 에서 항상 실행 — 다른 탭에서 토글해도 반영)

  const bs = room.state.bridgeStatus;

  if (bs === "idle" || bs === "loading") {
    return (
      <div style={{ height: "100%", display: "grid", placeItems: "center", padding: 24 }}>
        <div className="sk-cap" style={{ fontSize: 14 }}>{L("room.connecting")}</div>
      </div>
    );
  }

  if (!room.state.currentRoomId) {
    return <EmptyRoomView tweaks={tweaks} />;
  }
  return <RoomMainView tweaks={tweaks} />;
}

// ===========================================================
// 빈 상태 — 방 만들기 / 초대 코드 입장
// ===========================================================
function EmptyRoomView({ tweaks } = {}) {
  const room = window.roomStore.useRoom();
  const { state: ds } = diary.useDiary();
  const themeBg = roomThemeBackground(tweaks);
  const [viewportH, setViewportH] = useStateRoom(
    typeof window !== "undefined" ? window.innerHeight : 800
  );
  useEffectRoom(() => {
    const on = () => setViewportH(window.innerHeight);
    window.addEventListener("resize", on);
    return () => window.removeEventListener("resize", on);
  }, []);
  const minH = (window.TODOARY_WINDOW && window.TODOARY_WINDOW.MIN_H) || 520;
  const compactEntry = viewportH < minH + 40;
  const [mode, setMode] = useStateRoom(null);  // null | 'create' | 'join'
  const [name, setName] = useStateRoom("");
  const [code, setCode] = useStateRoom("");
  const [err, setErr] = useStateRoom("");
  const [busy, setBusy] = useStateRoom(false);
  const [profileOpen, setProfileOpen] = useStateRoom(false);
  const isLocal = room.state.bridgeStatus === "local";

  // 미리보기용 본인 todo 리스트 — RoomMainView 와 동일 로직
  const today = diary.today();
  const myTodos = (ds.todos || [])
    .filter((t) => t.projectId === ds.currentProjectId)
    .filter((t) => {
      if (!t.dueDate) return true;
      if (t.dueDate === today) return true;
      if (t.dueDate < today && !t.done) return true;
      return false;
    })
    .slice()
    .sort((a, b) => {
      if (a.done !== b.done) return a.done ? 1 : -1;
      if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
      return (a.order ?? 0) - (b.order ?? 0);
    });

  const submit = async (nextMode = mode) => {
    setErr(""); setBusy(true);
    try {
      if (nextMode === "create") {
        await room.createRoom({ name: name.trim() || L("room.title") });
      } else if (nextMode === "join") {
        await room.joinByCode(code.trim());
      }
    } catch (e) {
      const m = e && e.message;
      if (m === "ROOM_FULL") setErr(L("room.full"));
      else if (m === "ROOM_NOT_FOUND") setErr(L("room.notFound"));
      else if (m === "INVALID_CODE") setErr(L("room.codeInvalid"));
      else setErr(m || L("common.error"));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div
      className="room-view-root todoary-dock-view-scroll"
      style={{
        height: "100%", minHeight: 0, display: "flex", flexDirection: "column",
        boxSizing: "border-box", overflow: "hidden",
        background: themeBg,
        backgroundSize: "200% 200%",
        backgroundPosition: "center",
      }}
    >
      <div style={{
        flex: 1, minHeight: 0, overflowY: "auto", overflowX: "hidden",
        padding: compactEntry ? "10px 12px" : "18px 16px",
        gap: 10, display: "flex", flexDirection: "column",
        WebkitOverflowScrolling: "touch",
      }}>
      {isLocal && (
        <div style={{
          padding: "6px 10px",
          background: "var(--cream-soft)",
          border: "1.1px dashed var(--ink-soft)",
          borderRadius: 8,
          fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink-2)",
          textAlign: "center",
        }}>
          {uiIcon("lock", "🔒")} {L("room.localMode")}
        </div>
      )}
      <div style={{ textAlign: "center" }}>
        <div style={{ fontFamily: "var(--hand)", fontSize: 18, fontWeight: 800, color: "var(--ink)", letterSpacing: "0.01em" }}>
          {L("room.emptyTitle")}
        </div>
        <div className="sk-cap" style={{ marginTop: 6, fontSize: 12.5, lineHeight: 1.45, whiteSpace: "pre-line" }}>
          {L("room.emptyHint")}
        </div>
      </div>

      <div className={roomFlat() ? "room-flat-panel" : ""} style={{
        border: "1.1px solid var(--ink)",
        borderRadius: roomFlat() ? 4 : 4,
        background: roomFlat() ? "var(--paper)" : "color-mix(in srgb, var(--paper) 88%, var(--blue-soft))",
        boxShadow: roomFlat() ? "none" : "3px 3px 0 color-mix(in srgb, var(--ink-soft) 70%, transparent)",
        overflow: "hidden",
      }}>
        <div className={roomFlat() ? "room-flat-header" : ""} style={{
          height: 28,
          display: "flex", alignItems: "center", gap: 6,
          padding: "0 8px",
          background: roomFill2("linear-gradient(180deg, color-mix(in srgb, var(--paper) 42%, var(--point)), color-mix(in srgb, var(--paper) 64%, var(--point)))"),
          borderBottom: "1.1px solid var(--ink)",
          color: "var(--ink)",
          fontFamily: "var(--hand)", fontWeight: roomFlat() ? 700 : 800, fontSize: 14,
        }}>
          {uiIcon("heart", "♡") && <span>{uiIcon("heart", "♡")}</span>}
          <span style={{ flex: 1 }}>{L("room.title")}</span>
          <button onClick={() => setProfileOpen((v) => !v)} style={{
            all: "unset", cursor: "pointer",
            width: 18, height: 18,
            display: "grid", placeItems: "center",
            background: "var(--paper)",
            border: "1px solid var(--ink)",
            color: "var(--ink)",
            fontFamily: "var(--mono)", fontSize: 12,
          }}>×</button>
        </div>
        <div style={{ padding: 12, display: "grid", gap: 10 }}>
          <RoomEntryScene
            profile={room.state.profile}
            todoCount={myTodos.length}
            doneCount={myTodos.filter((t) => t.done).length}
            onProfileClick={() => setProfileOpen((v) => !v)}
            compact={compactEntry}
          />
          <div style={{ display: "grid", gap: 7 }}>
            <div style={{ display: "grid", gridTemplateColumns: "88px 1fr", alignItems: "center", gap: 8 }}>
              <span style={{ fontFamily: "var(--hand)", fontSize: 13, color: "var(--ink)" }}>{L("room.roomNameLabel")}</span>
              <input
                value={name} onChange={(e) => setName(e.target.value)}
                placeholder={L("room.namePh")}
                onKeyDown={(e) => { if (e.key === "Enter") { setMode("create"); submit("create"); } }}
                style={{ ...formInput, height: 28, borderColor: "var(--ink)" }}
              />
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "88px 1fr", alignItems: "center", gap: 8 }}>
              <span style={{ fontFamily: "var(--hand)", fontSize: 13, color: "var(--ink)" }}>{L("room.inviteCodeLabel")}</span>
              <input
                value={code} onChange={(e) => setCode(e.target.value.toUpperCase())}
                placeholder={L("room.codePh")}
                maxLength={6}
                onKeyDown={(e) => { if (e.key === "Enter") { setMode("join"); submit("join"); } }}
                style={{ ...formInput, height: 28, borderColor: "var(--ink)", fontFamily: "var(--mono)", letterSpacing: "0.12em", textAlign: "center" }}
              />
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginTop: 2 }}>
              <button onClick={() => { setMode("create"); submit("create"); }} disabled={busy} style={{ ...bigBtn, padding: "8px", background: roomFlat() ? "var(--paper-2)" : "color-mix(in srgb, var(--paper) 82%, var(--pink-soft))", opacity: busy ? 0.5 : 1, boxShadow: roomFlat() ? "none" : bigBtn.boxShadow }}>
                ＋ {L("room.create")}
              </button>
              <button onClick={() => { setMode("join"); submit("join"); }} disabled={busy || code.length < 4} style={{ ...bigBtn, padding: "8px", background: roomFlat() ? "var(--paper-2)" : "color-mix(in srgb, var(--paper) 82%, var(--blue-soft))", opacity: (busy || code.length < 4) ? 0.5 : 1, boxShadow: roomFlat() ? "none" : bigBtn.boxShadow }}>
                {uiIcon("mailLetter", "✉")} {L("room.join")}
              </button>
            </div>
          </div>
          {err && <div className="sk-cap" style={{ color: "var(--bad)", textAlign: "center" }}>{err}</div>}
        </div>
        <div style={{
          display: "grid", gridTemplateColumns: "52px 1fr",
          borderTop: "1.1px solid var(--ink)",
          background: "color-mix(in srgb, var(--paper) 84%, var(--pink-soft))",
        }}>
          <div style={{
            display: "grid", placeItems: "center",
            borderRight: "1.1px solid var(--ink)",
            fontSize: 28,
            color: "var(--point)",
          }}>♥</div>
          <div style={{
            padding: "8px 10px",
            fontFamily: "var(--hand)", fontSize: 15, fontWeight: 800,
            color: "var(--point)",
            textShadow: "0 1px 0 var(--paper)",
          }}>{L("room.tagline")}</div>
        </div>
        <div style={{
          padding: "7px 10px",
          borderTop: "1.1px solid var(--ink)",
          background: "color-mix(in srgb, var(--paper) 90%, transparent)",
          fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink-2)",
        }}>{L("room.profileHint")}</div>
      </div>

      <div className="sk-cap" style={{ textAlign: "center", lineHeight: 1.45, padding: "0 8px" }}>
        {L("room.description")}
      </div>

      <div style={{
        padding: "9px 11px",
        border: "1.1px dashed var(--ink)",
        borderRadius: 10,
        background: "color-mix(in srgb, var(--paper) 84%, var(--cream-soft))",
        fontFamily: "var(--hand)",
        fontSize: 12.5,
        lineHeight: 1.45,
        color: "var(--ink-2)",
      }}>
        <div style={{ fontWeight: 800, color: "var(--ink)", marginBottom: 3 }}>{L("room.betaTitle")}</div>
        {L("room.betaBody", { email: "dinglediary@gmail.com" })}
      </div>

      {profileOpen && (
        <div style={{ marginTop: -4 }}>
          <RoomEntryProfilePanel />
        </div>
      )}
      </div>
    </div>
  );
}

function RoomEntryScene({ profile, todoCount, doneCount, onProfileClick, compact }) {
  const progress = todoCount > 0 ? Math.min(1, doneCount / todoCount) : 0;
  const padY = compact ? 10 : 18;
  const padB = compact ? 36 : 48;
  const avatarBox = compact ? 72 : 86;
  const avatarIcon = compact ? 44 : 54;
  return (
    <div style={{
      position: "relative",
      flexShrink: 0,
      minHeight: compact ? 100 : 120,
      maxHeight: compact ? 140 : 200,
      display: "flex",
      flexDirection: "column",
      alignItems: "center",
      justifyContent: "center",
      padding: `${padY}px 12px ${padB}px`,
      boxSizing: "border-box",
      border: "1.1px solid var(--ink)",
      borderRadius: 16,
      overflow: "hidden",
      background: "color-mix(in srgb, var(--paper) 84%, transparent)",
      boxShadow: "0 3px 0 var(--paper-3)",
    }}>
      {!roomFlat() && (
        <div className="room-flat-entry-glow" style={{
          position: "absolute", inset: 0,
          background: `
            radial-gradient(circle at 50% 44%, color-mix(in srgb, var(--hi) 22%, transparent), transparent 34%),
            linear-gradient(180deg, color-mix(in srgb, var(--blue-soft) 38%, transparent), transparent 62%)
          `,
          opacity: 0.95,
        }} />
      )}
      <div style={{
        position: "relative",
        zIndex: 1,
        display: "grid",
        placeItems: "center",
        gap: 8,
      }}>
        <button onClick={onProfileClick} title={L("room.profileSettings")} style={{
          all: "unset",
          cursor: "pointer",
          width: avatarBox,
          minHeight: compact ? 84 : 102,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          gap: compact ? 4 : 7,
          border: "1.1px solid var(--ink)",
          borderRadius: 16,
          background: "color-mix(in srgb, var(--paper) 90%, transparent)",
          boxShadow: "0 4px 0 color-mix(in srgb, var(--ink-soft) 70%, transparent)",
        }}>
          <span style={{
            width: avatarIcon, height: avatarIcon,
            borderRadius: 14,
            border: "1.1px solid var(--ink)",
            background: "var(--paper)",
            display: "grid",
            placeItems: "center",
          }}>
            <RoomAvatarIcon value={profile.avatar} size={36} />
          </span>
          <span style={{
            maxWidth: 72,
            fontFamily: "var(--hand)", fontSize: 12.5, fontWeight: 800,
            color: "var(--ink)",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
          }}>{profile.displayName || L("room.you")}</span>
        </button>
        <div className="sk-cap" style={{ fontSize: 11.5, textAlign: "center" }}>
          {L("room.profileHintShort")}
        </div>
      </div>
      <div style={{
        position: "absolute", left: 14, right: 14, bottom: 12,
        display: "flex", alignItems: "center", gap: 8,
        padding: "7px 9px",
        border: "1.1px solid var(--ink)",
        borderRadius: 10,
        background: "color-mix(in srgb, var(--paper) 82%, transparent)",
        fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink-2)",
      }}>
        <span style={{ fontWeight: 700, color: "var(--ink)" }}>{L("room.todayReady")}</span>
        <span className="room-flat-progress" style={{ flex: 1, height: 7, borderRadius: 99, border: "1px solid var(--ink)", background: "var(--paper)", overflow: "hidden" }}>
          <span style={{ display: "block", height: "100%", width: `${progress * 100}%`, background: roomFill("linear-gradient(90deg, var(--point), var(--hi))", "var(--ink-3)") }} />
        </span>
        <span style={{ fontFamily: "var(--mono)", fontSize: 10 }}>{doneCount}/{todoCount}</span>
      </div>
    </div>
  );
}

function RoomEntryProfilePanel() {
  const room = window.roomStore.useRoom();
  const [name, setName] = useStateRoom(room.state.profile.displayName || "");
  return (
    <div style={{
      padding: 10,
      border: "1.1px solid var(--ink)",
      borderRadius: 12,
      background: "color-mix(in srgb, var(--paper) 82%, transparent)",
      boxShadow: "0 2px 0 var(--paper-3)",
    }}>
      <div className="sk-label" style={{ marginBottom: 8 }}>{L("room.profileSettings")}</div>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
        <span style={{ width: 36, height: 36, border: "1.1px solid var(--ink)", borderRadius: 10, background: "var(--paper)", display: "grid", placeItems: "center", flexShrink: 0 }}>
          <RoomAvatarIcon value={room.state.profile.avatar} size={24} />
        </span>
        <input
          value={name}
          onChange={(e) => setName(e.target.value)}
          onBlur={() => room.setProfile({ displayName: name.trim() })}
          placeholder={L("room.namePrompt")}
          style={{ ...formInput, flex: 1 }}
        />
      </div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
        {room.avatars.map((a) => (
          <button key={a} onClick={() => room.setProfile({ avatar: a })}
            style={{
              ...avatarBtn, width: 30, height: 30,
              borderRadius: 8,
              background: room.state.profile.avatar === a ? "var(--hi)" : "var(--paper)",
            }}><RoomAvatarIcon value={a} size={20} /></button>
        ))}
      </div>
    </div>
  );
}

// ===========================================================
// 메인 뷰 — 방 안에 있을 때 (4분할 그리드)
// ===========================================================
// ===========================================================
// 엑셀 스킨 — 작업방을 "출석부(근태표) + 작업 로그(회의록)" 표로 렌더.
// 픽셀 캐릭터·박수 기능은 유지하되 전부 셀 문법으로 (말풍선·큰 아바타 금지).
// ===========================================================
function fmtExcelLogClock(ts) {
  if (!ts) return "–";
  const d = new Date(ts);
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}

function ExcelRoomAttendance({ members, myUid, myStatus, now, room }) {
  return (
    <section className="excel-room-section">
      <div className="excel-room-group-head">{L("room.excelMembersHead", { n: members.length })}</div>
      <table className="excel-room-att">
        <tbody>
          {members.map((m) => {
            const st = memberRoomStatus(m, now, myUid, myStatus);
            const secs = memberLiveSeconds(m, now, myUid, myStatus);
            const isMe = m.uid === myUid;
            const label = isMe ? (m.displayName || L("room.you")) : (m.displayName || L("room.friend"));
            const stLabel = st === "working" ? L("room.workingStatus")
              : st === "away" ? L("room.away")
              : st === "offline" ? L("room.offlineStatus")
              : st === "paused" ? L("room.pausedStatus")
              : L("room.onlineStatus");
            return (
              <tr
                key={m.uid}
                className={"excel-room-att-row is-" + st + (isMe ? " is-me" : "")}
                onClick={isMe
                  ? () => window.dispatchEvent(new CustomEvent("room-edit-profile"))
                  : () => room.sendClap(m.uid)}
                title={isMe ? L("room.profileSettings") : L("room.clap")}
              >
                <td className="excel-room-att-avatar">
                  <RoomAvatarIcon value={m.avatar} size={18} />
                </td>
                <td className="excel-room-att-name">{label}</td>
                <td className={"excel-room-att-status is-" + st}>{stLabel}</td>
                <td className="excel-room-att-time">{fmtHMS(secs)}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </section>
  );
}

function ExcelRoomLogRows({ ev, expanded, onToggleExpand }) {
  const isSystem = ev.kind === "status";
  const isClap = isClapLogEvent(ev);
  const clapCount = ev.collapsed ? (ev.count || (ev.children || []).length || 1) : (isClap ? 1 : 0);
  return (
    <>
      <tr className={"excel-room-log-row"
        + (isSystem || isClap ? " is-system" : "")
        + (ev.isMe ? " is-me" : "")}>
        <td className="excel-room-log-time">{fmtExcelLogClock(ev.at)}</td>
        <td className="excel-room-log-member">{ev.label || ev.nickname || ""}</td>
        <td className="excel-room-log-text">
          <span className="excel-room-log-text-inner">{ev.text}</span>
          {isClap && clapCount > 0 && (
            <button
              type="button"
              className="excel-room-clap-chip"
              onClick={ev.collapsed ? onToggleExpand : undefined}
              title={ev.collapsed ? (expanded ? L("room.logCollapse") : L("room.logExpand")) : undefined}
              style={{ cursor: ev.collapsed ? "pointer" : "default" }}
            >👏{clapCount > 1 ? ` ${clapCount}` : ""}</button>
          )}
        </td>
      </tr>
      {ev.collapsed && expanded && (ev.children || []).map((ch, j) => (
        <tr key={ch.id || `${ch.uid}-${j}`} className="excel-room-log-row is-system is-child">
          <td className="excel-room-log-time">{fmtExcelLogClock(ch.at)}</td>
          <td className="excel-room-log-member">{ch.nickname || ""}</td>
          <td className="excel-room-log-text"><span className="excel-room-log-text-inner">{ch.text}</span></td>
        </tr>
      ))}
    </>
  );
}

function ExcelRoomLogTable({ rows, expandedClaps, setExpandedClaps, logDraft, setLogDraft, onSubmitLog }) {
  return (
    <section className="excel-room-section">
      <table className="excel-room-log">
        <thead>
          <tr>
            <th className="excel-room-log-time">{L("room.excelLogTimeCol")}</th>
            <th className="excel-room-log-member">{L("room.excelLogMemberCol")}</th>
            <th className="excel-room-log-text">{L("room.excelLogCol")}</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((ev, i) => (
            <ExcelRoomLogRows
              key={ev.id || `${ev.uid}-${ev.at || 0}-${i}`}
              ev={ev}
              expanded={!!expandedClaps[ev.id]}
              onToggleExpand={() => setExpandedClaps((s) => ({ ...s, [ev.id]: !s[ev.id] }))}
            />
          ))}
          <tr className="excel-room-log-input-row">
            <td className="excel-room-log-time" aria-hidden />
            <td className="excel-room-log-member">{L("room.you")}</td>
            <td className="excel-room-log-text">
              <input
                className="excel-room-log-input"
                value={logDraft}
                placeholder={L("room.excelLogInputPh")}
                onChange={(e) => setLogDraft(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === "Enter") { e.preventDefault(); onSubmitLog(); }
                }}
              />
            </td>
          </tr>
        </tbody>
      </table>
    </section>
  );
}

function ExcelRoomTodoTable({ label, count, todos, isMe, onToggleTodo, onToggleSub, emptyText }) {
  return (
    <section className="excel-room-section">
      <div className="excel-room-group-head">{L("room.excelSharedHead", { name: label, n: count })}</div>
      <table className="excel-room-todos">
        <tbody>
          {todos.map((t) => (
            <React.Fragment key={t.id}>
              <tr className={"excel-room-todo-row" + (t.done ? " is-done" : "")}>
                <td className="excel-room-todo-check">
                  <button
                    type="button"
                    className={"sk-check" + (t.done ? " done" : "")}
                    onClick={isMe && onToggleTodo ? () => onToggleTodo(t.id) : undefined}
                    title={isMe ? L("room.toggleDone") : undefined}
                    style={{ cursor: isMe && onToggleTodo ? "pointer" : "default" }}
                  />
                </td>
                <td className="excel-room-todo-title">{t.title}</td>
              </tr>
              {(t.subTasks || []).map((st) => (
                <tr key={st.id} className={"excel-room-todo-row is-sub" + (st.done ? " is-done" : "")}>
                  <td className="excel-room-todo-check">
                    <button
                      type="button"
                      className={"sk-check" + (st.done ? " done" : "")}
                      onClick={isMe && onToggleSub ? () => onToggleSub(t.id, st.id) : undefined}
                      title={isMe ? L("room.toggleSubDone") : undefined}
                      style={{ cursor: isMe && onToggleSub ? "pointer" : "default", transform: "scale(0.85)" }}
                    />
                  </td>
                  <td className="excel-room-todo-title">↳ {st.title}</td>
                </tr>
              ))}
            </React.Fragment>
          ))}
          {todos.length === 0 && (
            <tr className="excel-room-todo-row is-empty">
              <td className="excel-room-todo-check" aria-hidden />
              <td className="excel-room-todo-title">{emptyText}</td>
            </tr>
          )}
        </tbody>
      </table>
    </section>
  );
}

function ExcelRoomMain({
  room, roomMeta, members, myUid, now, roomLog, myTodos,
  totalSeconds, onlineCount, isLocal, onCopyCode, onLeave, today,
  onToggleTodo, onToggleSub,
}) {
  const myStatus = room.state.myStatus;
  const [expandedClaps, setExpandedClaps] = useStateRoom({});
  const [logDraft, setLogDraft] = useStateRoom("");
  const chatTimeline = buildChatTimeline(roomLog, members, myUid);

  // 멤버별 "한마디" — 말풍선 대신 로그 표 행으로
  const noteRows = members
    .filter((m) => (m.statusMessage || "").trim())
    .map((m) => ({
      id: `note-${m.uid}`,
      kind: "note",
      isMe: m.uid === myUid,
      label: m.uid === myUid ? L("room.you") : (m.displayName || L("room.friend")),
      text: m.statusMessage.trim(),
      at: null,
    }));
  const logRows = [
    ...(isLocal ? [{ id: "local-mode", kind: "status", label: "", text: L("room.localMode"), at: null }] : []),
    ...chatTimeline,
    ...noteRows,
  ];

  const submitLog = () => {
    const v = logDraft.trim();
    if (!v) return;
    room.patchMyMemberImmediate({ statusMessage: v });
    setLogDraft("");
    window.dispatchEvent(new CustomEvent("room-flash", { detail: { msg: L("room.excelLogSaved") } }));
  };

  const days = roomDaysSinceCreated(roomMeta, now, members);
  const roomName = roomMeta?.name || L("room.title");

  return (
    <div className="excel-room-sheet">
      <div className="excel-room-titlebar">
        <span className="excel-room-titlebar-name">{L("room.excelTitle", { name: roomName })}</span>
        <span className="excel-room-titlebar-meta">
          {L("room.excelTotal", { t: fmtHMS(totalSeconds || 0) })} · D+{days}
        </span>
        <span className="excel-room-titlebar-actions">
          <button type="button" onClick={onCopyCode} title={L("room.copyInviteCode")}>↗</button>
          <button
            type="button"
            onClick={() => room.setMyStatus(myStatus === "away" ? "working" : "away")}
            title={myStatus === "away" ? L("room.awayOff") : L("room.away")}
          >{myStatus === "away" ? L("room.return") : L("room.away")}</button>
          <button type="button" onClick={onLeave} title={L("room.leave")}>{L("room.leave")}</button>
        </span>
      </div>
      <div className="excel-room-scroll">
        <ExcelRoomAttendance members={members} myUid={myUid} myStatus={myStatus} now={now} room={room} />
        <ExcelRoomLogTable
          rows={logRows}
          expandedClaps={expandedClaps}
          setExpandedClaps={setExpandedClaps}
          logDraft={logDraft}
          setLogDraft={setLogDraft}
          onSubmitLog={submitLog}
        />
        {members.map((m) => {
          const isMe = m.uid === myUid;
          const todos = isMe
            ? myTodos
              .filter((t) => t.roomVisible)
              .filter((t, idx, arr) => arr.findIndex((x) => x.id === t.id) === idx)
              .slice()
              .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
              .map((t) => roomTodoView(roomVisibleTodoPayload(t), t.id))
            : Array.isArray(m.visibleTodos)
              ? m.visibleTodos.map((item, i) => roomTodoView(item, `${m.uid}-${i}`)).filter((t) => t.title)
              : [];
          const label = isMe ? L("room.you") : (m.displayName || L("room.friend"));
          return (
            <ExcelRoomTodoTable
              key={m.uid}
              label={label}
              count={todos.length}
              todos={todos}
              isMe={isMe}
              onToggleTodo={onToggleTodo}
              onToggleSub={onToggleSub}
              emptyText={isMe ? L("room.noTodo") : L("room.noPublicTitle")}
            />
          );
        })}
        <section className="excel-room-section excel-room-register">
          <RoomTodoRegisterPanel todos={myTodos.filter((t) => !t.dueDate || t.dueDate === today)} />
        </section>
      </div>
      <div className="excel-status-row">
        <div className="excel-sheet-tabs" role="tablist" aria-label={L("room.title")}>
          <button type="button" role="tab" aria-selected className="excel-sheet-tab is-active">
            {roomName}
          </button>
          <button
            type="button"
            role="tab"
            aria-selected={false}
            className="excel-sheet-tab"
            title={L("room.leaveConfirm")}
            onClick={onLeave}
          >+ {L("room.create")}</button>
        </div>
        <div className="excel-statusbar">
          <span className="excel-statusbar-item">{L("room.excelConnected", { n: onlineCount })}</span>
          <span className="excel-statusbar-sep" aria-hidden>·</span>
          <span className="excel-statusbar-item">{L("room.excelTotal", { t: fmtHMS(totalSeconds || 0) })}</span>
        </div>
      </div>
      <ProfileEditModal />
      <FlashToast />
    </div>
  );
}

function RoomMainView({ tweaks } = {}) {
  const room = window.roomStore.useRoom();
  const { state: ds } = diary.useDiary();
  const now = useNowTick();
  const { roomMeta, members } = room.state;
  const recentClaps = roomMeta?.recentClaps || [];
  const themeBg = roomThemeBackground(tweaks);
  const myUid = window.firebaseBridge.uid();
  const nameByUid = new Map(members.map((m) => [m.uid, m.displayName || L("room.friend")]));
  const [statusEvents, setStatusEvents] = useStateRoom([]);
  const prevStatusesRef = useRefRoom(new Map());
  const statusSig = members
    .map((m) => `${m.uid}:${memberRoomStatus(m, now, myUid, room.state.myStatus)}`)
    .join("|");
  useEffectRoom(() => {
    if (!members.length) return;
    const prevMap = prevStatusesRef.current;
    const nextMap = new Map();
    const added = [];
    members.forEach((m) => {
      const status = memberRoomStatus(m, now, myUid, room.state.myStatus);
      nextMap.set(m.uid, status);
      const prev = prevMap.get(m.uid);
      if (!shouldLogRoomStatusChange(m.uid, myUid, prev, status)) return;
      const text = roomStatusText(status);
      if (!text) return;
      added.push({
        id: `status-${m.uid}-${now}`,
        kind: "status",
        uid: m.uid,
        nickname: m.displayName || (m.uid === myUid ? L("room.you") : L("room.friend")),
        text,
        at: now,
      });
    });
    prevStatusesRef.current = nextMap;
    if (added.length) setStatusEvents((events) => mergeRoomStatusEvents(events, added));
  }, [statusSig]);
  const completionLog = (() => {
    const byKey = new Map();
    members.forEach((m) => {
      (Array.isArray(m.recentLog) ? m.recentLog : []).forEach((entry) => {
        if (!entry?.text) return;
        const uid = entry.uid || m.uid;
        const todoId = entry.todoId ? String(entry.todoId) : null;
        const key = todoId ? `${uid}:${todoId}` : `${uid}:${entry.text}`;
        const at = entry.at || 0;
        if (!logIsToday(at, now)) return;
        const row = {
          ...entry,
          uid,
          todoId,
          id: entry.id || (todoId ? `${uid}:${todoId}` : `${uid}-${at}`),
          nickname: entry.nickname || m.displayName || L("room.friend"),
          at,
        };
        const prev = byKey.get(key);
        if (!prev || at >= (prev.at || 0)) byKey.set(key, row);
      });
    });
    return [...byKey.values()];
  })();
  const clapLog = recentClaps
    .filter((c) => logIsToday(c.ts, now))
    .map((c) => ({
      id: c.id || `clap-${c.fromUid}-${c.toUid}-${c.ts}`,
      kind: "clap",
      uid: c.fromUid,
      toUid: c.toUid,
      nickname: nameByUid.get(c.fromUid) || L("room.friend"),
      text: formatClapLogText(c.fromUid, c.toUid, nameByUid),
      at: c.ts,
    }));
  const roomLog = [...completionLog, ...clapLog, ...statusEvents.filter((ev) => logIsToday(ev.at, now))]
    .sort((a, b) => (a.at || 0) - (b.at || 0))
    .slice(-50);
  const noticeByUid = new Map();
  roomLog.forEach((ev) => {
    if (!ev?.uid || !ev?.text || now - (ev.at || 0) > NOTICE_VISIBLE_MS) return;
    noticeByUid.set(ev.uid, ev);
  });
  const isLocal = room.state.bridgeStatus === "local";

  // 본인 todo 리스트 — diary 직접 (자기 카드에서만 보임)
  const today = diary.today();
  const myTodos = (ds.todos || [])
    .filter((t) => t.projectId === ds.currentProjectId)
    .filter((t) => {
      if (!t.dueDate) return true;
      if (t.dueDate === today) return true;
      if (t.dueDate < today && !t.done) return true;
      return false;
    })
    .slice()
    .sort((a, b) => {
      if (a.done !== b.done) return a.done ? 1 : -1;
      if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
      return (a.order ?? 0) - (b.order ?? 0);
    });

  // 멤버 정렬: 나 먼저, 그 다음 입장 순
  const sorted = [...members].sort((a, b) => {
    if (a.uid === myUid) return -1;
    if (b.uid === myUid) return 1;
    return (a.joinedAt || 0) - (b.joinedAt || 0);
  });

  const onlineCount = members.filter((m) => !isMemberOffline(m, now)).length;
  const roomTotalDisplaySeconds = members.reduce(
    (sum, m) => sum + memberRoomWorkSeconds(m, now, myUid, room.state.myStatus),
    0,
  );

  // 박수 묶음
  const clapsByMember = recentClaps.reduce((acc, c) => {
    (acc[c.toUid] = acc[c.toUid] || []).push(c);
    return acc;
  }, {});

  const copyCode = () => {
    const code = roomMeta?.inviteCode;
    if (!code) return;
    try {
      navigator.clipboard.writeText(code);
      window.dispatchEvent(new CustomEvent("room-flash", { detail: { msg: L("room.codeCopied", { code }) } }));
    } catch (_) {
      // 클립보드 차단된 환경 — 적어도 코드를 보여주기
      window.dispatchEvent(new CustomEvent("room-flash", { detail: { msg: L("room.inviteCodeFlash", { code }) } }));
    }
  };
  const onLeave = async (e) => {
    e?.preventDefault?.();
    e?.stopPropagation?.();
    const msg = L("room.leaveConfirm");
    const ok = window.dialog
      ? await window.dialog.confirm(msg)
      : window.confirm(msg);
    if (ok) await room.leaveRoom();
  };
  const excel = roomExcel();
  if (excel) {
    return (
      <ExcelRoomMain
        room={room}
        roomMeta={roomMeta}
        members={sorted}
        myUid={myUid}
        now={now}
        roomLog={roomLog}
        myTodos={myTodos}
        totalSeconds={roomTotalDisplaySeconds}
        onlineCount={onlineCount}
        isLocal={isLocal}
        onCopyCode={copyCode}
        onLeave={onLeave}
        today={today}
        onToggleTodo={(id) => diary.actions.toggleTodo(id)}
        onToggleSub={(todoId, subId) => diary.actions.toggleSubTask(todoId, subId)}
      />
    );
  }
  return (
    <div
      className="room-view-root"
      style={{
      height: "100%", display: "flex", flexDirection: "column", minHeight: 0,
      overflow: "hidden",
      padding: 8, boxSizing: "border-box",
      background: themeBg,
      backgroundSize: "200% 200%",
      backgroundPosition: "center",
    }}
    >
      <div className={roomFlat() ? "room-flat-header" : ""} style={{
        flexShrink: 0,
        display: "flex", alignItems: "center", gap: 6,
        padding: "5px 8px",
        border: "1.1px solid var(--ink)",
        borderRadius: "9px 9px 0 0",
        background: roomFill("linear-gradient(180deg, var(--paper) 0%, var(--blue-soft) 100%)"),
        boxShadow: roomFlat() ? "none" : "0 1px 0 rgba(255,255,255,0.45) inset",
      }}>
        {uiIcon("heart", "♡") && <span style={{ fontSize: 13, color: "var(--ink-2)" }}>{uiIcon("heart", "♡")}</span>}
        <span style={{
          flex: 1, minWidth: 0,
          fontFamily: "var(--hand)", fontSize: 14, fontWeight: 700,
          color: "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
        }}>{roomMeta?.name || L("room.title")}</span>
        <button onClick={copyCode} title={L("room.copyInviteCode")} style={roomChromeBtnStyle()}>↗</button>
        <button
          onClick={() => room.setMyStatus(room.state.myStatus === "away" ? "working" : "away")}
          title={room.state.myStatus === "away" ? L("room.awayOff") : L("room.away")}
          style={{
            ...roomChromeBtnStyle(),
            width: "auto",
            padding: "0 7px",
            background: room.state.myStatus === "away"
              ? (roomFlat() ? "var(--paper-3)" : "var(--cream-soft)")
              : roomChromeBtnStyle().background,
            color: "var(--ink)",
          }}
        >{room.state.myStatus === "away" ? L("room.return") : L("room.away")}</button>
        <button
          onClick={onLeave}
          onPointerDown={(e) => e.stopPropagation()}
          title={L("room.leave")}
          style={{ ...roomChromeBtnStyle(), width: "auto", padding: "0 7px" }}
        >{L("room.leave")}</button>
      </div>

      <RoomNoticeStrip
        roomId={room.state.currentRoomId}
        members={sorted}
        now={now}
        isLocal={isLocal}
      />

      <div style={{
        flex: 1, minHeight: 0,
        overflowY: "auto", overflowX: "hidden",
        WebkitOverflowScrolling: "touch",
        display: "grid", gridTemplateColumns: "1fr 82px", gap: 8,
        padding: "8px 8px 6px",
        borderLeft: "1.1px solid var(--ink)",
        borderRight: "1.1px solid var(--ink)",
        background: "color-mix(in srgb, var(--paper) 78%, transparent)",
      }}>
        <RoomChatLog
          roomId={room.state.currentRoomId}
          members={sorted}
          myUid={myUid}
          myStatus={room.state.myStatus}
          myTodos={myTodos}
          roomLog={roomLog}
          roomMeta={roomMeta}
          now={now}
          totalWorkSeconds={roomTotalDisplaySeconds}
          onToggleTodo={(id) => diary.actions.toggleTodo(id)}
          onToggleSub={(todoId, subId) => diary.actions.toggleSubTask(todoId, subId)}
        />
        <div style={{ display: "grid", gridTemplateRows: "repeat(4, minmax(64px, 1fr))", gap: 6, minHeight: 0 }}>
          {[0, 1, 2, 3].map((i) => {
            const m = sorted[i];
            return m ? (
              <RoomProfileSlot
                key={m.uid}
                member={m}
                isMe={m.uid === myUid}
                now={now}
                myUid={myUid}
                myStatus={room.state.myStatus}
                claps={clapsByMember[m.uid] || []}
                displayStatus={memberRoomStatus(m, now, myUid, room.state.myStatus)}
                notice={noticeByUid.get(m.uid)}
                onClick={m.uid === myUid ? () => window.dispatchEvent(new CustomEvent("room-edit-profile")) : () => room.sendClap(m.uid)}
              />
            ) : <RoomEmptySlot key={i} />;
          })}
        </div>
      </div>

      <div className={roomFlat() ? "room-flat-footer" : ""} style={{
        flexShrink: 0,
        padding: "7px 8px 8px",
        border: "1.1px solid var(--ink)",
        borderRadius: "0 0 9px 9px",
        background: roomFill("linear-gradient(180deg, var(--paper) 0%, var(--blue-soft) 100%)"),
      }}>
        <RoomTodoRegisterPanel todos={myTodos.filter((t) => !t.dueDate || t.dueDate === today)} />
      </div>

      <ProfileEditModal />
      <FlashToast />
    </div>
  );
}

/** 채팅 하단 박스 — 약 3줄 높이, 휠로 이전 대화 탐색 */
const ROOM_CHAT_LOG_BOX_HEIGHT = 80;

function buildChatTimeline(roomLog, members, myUid) {
  const nameByUid = new Map(members.map((m) => [
    m.uid,
    m.uid === myUid ? L("room.you") : (m.displayName || L("room.friend")),
  ]));
  const raw = (roomLog || [])
    .map((ev) => ({
      ...ev,
      isMe: ev.uid === myUid,
      label: ev.nickname || nameByUid.get(ev.uid) || L("room.friend"),
    }))
    .sort((a, b) => (a.at || 0) - (b.at || 0));
  return collapseClapLogEvents(raw);
}

function RoomMemberLogEvents({ events, expandedClaps, setExpandedClaps }) {
  return events.map((ev, i) => {
    const isMe = !!ev.isMe;
    const label = ev.label || ev.nickname || L("room.friend");
    if (ev.collapsed && isClapLogEvent(ev)) {
      const open = !!expandedClaps[ev.id];
      return (
        <div key={ev.id || `${ev.uid}-${ev.at || 0}-${i}`} style={{ padding: "3px 0 2px 10px" }}>
          <button
            type="button"
            onClick={() => setExpandedClaps((s) => ({ ...s, [ev.id]: !s[ev.id] }))}
            style={{
              all: "unset", cursor: "pointer", width: "100%",
              fontFamily: "var(--hand)", fontSize: 12.5, lineHeight: 1.35, color: "var(--ink-2)",
            }}
          >
            <span style={{ color: isMe ? "var(--point)" : "var(--ink-2)", fontWeight: 700 }}>
              {ev.nickname || label} say :
            </span>{" "}
            {ev.text || L("room.clapCollapsed", { n: ev.count })}{" "}
            <span style={{ fontSize: 10, color: "var(--ink-3)" }}>{open ? L("room.logCollapse") : L("room.logExpand")}</span>{" "}
            <span style={{ fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)" }}>
              {fmtRoomClock(ev.at)}
            </span>
          </button>
          {open && (ev.children || []).map((ch, j) => (
            <div key={ch.id || `${ch.uid}-${j}`} style={{
              padding: "2px 0 2px 18px",
              fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink-3)",
            }}>
              <span style={{ fontWeight: 600 }}>{ch.nickname || label} say :</span> {ch.text}{" "}
              <span style={{ fontFamily: "var(--mono)", fontSize: 10 }}>{fmtRoomClock(ch.at)}</span>
            </div>
          ))}
        </div>
      );
    }
    return (
      <div key={ev.id || `${ev.uid}-${ev.at || 0}-${i}`} style={{
        padding: "3px 0 2px 10px",
        fontFamily: "var(--hand)", fontSize: 12.5, lineHeight: 1.35,
        color: isClapLogEvent(ev) ? "var(--ink-2)" : "var(--ink)",
      }}>
        <span style={{ color: isMe ? "var(--point)" : "var(--ink-2)", fontWeight: 700 }}>
          {ev.nickname || label} say :
        </span>{" "}
        <span>{ev.text}</span>{" "}
        <span style={{ fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)" }}>
          {fmtRoomClock(ev.at)}
        </span>
      </div>
    );
  });
}

function RoomNoticeStrip({ roomId, members, now, isLocal }) {
  const dayKey = kstYmd(now);
  const [newDayHidden, setNewDayHidden] = useStateRoom(() => {
    try { return localStorage.getItem(ROOM_DISMISS_NEW_DAY_KEY) === dayKey; } catch (_) { return false; }
  });
  const [celebrations, setCelebrations] = useStateRoom([]);
  const [dismissedNoticeIds, setDismissedNoticeIds] = useStateRoom(() => new Set());
  const prevAllDoneRef = useRefRoom({});

  useEffectRoom(() => {
    try {
      setNewDayHidden(localStorage.getItem(ROOM_DISMISS_NEW_DAY_KEY) === dayKey);
    } catch (_) {
      setNewDayHidden(false);
    }
  }, [dayKey]);

  useEffectRoom(() => {
    if (!roomId) {
      setCelebrations([]);
      setDismissedNoticeIds(new Set());
      prevAllDoneRef.current = {};
      return;
    }
    const stored = loadRoomNotices(roomId);
    setCelebrations(stored.celebrations.filter((ev) => logIsToday(ev.at, now)));
    setDismissedNoticeIds(new Set(stored.dismissed));
    prevAllDoneRef.current = {};
  }, [roomId]);

  const persistNotices = (nextCelebrations, nextDismissed) => {
    if (!roomId) return;
    saveRoomNotices(roomId, {
      celebrations: nextCelebrations,
      dismissed: [...nextDismissed],
    });
  };

  const membersProgressSig = members.map((m) => `${m.uid}:${m.todoDone || 0}/${m.todoTotal || 0}`).join("|");
  useEffectRoom(() => {
    if (!roomId) return;
    const added = [];
    members.forEach((m) => {
      const allDone = (m.todoTotal || 0) > 0 && m.todoDone === m.todoTotal;
      const had = prevAllDoneRef.current[m.uid];
      if (had === undefined) {
        prevAllDoneRef.current[m.uid] = allDone;
        return;
      }
      if (allDone && !had) {
        added.push({
          id: `celebrate-${m.uid}-${dayKey}`,
          kind: "celebration",
          uid: m.uid,
          text: celebrationMessage(m.displayName),
          at: Date.now(),
        });
      }
      prevAllDoneRef.current[m.uid] = allDone;
    });
    if (!added.length) return;
    setCelebrations((prev) => {
      const byId = new Map(prev.map((ev) => [ev.id, ev]));
      added.forEach((ev) => byId.set(ev.id, ev));
      const next = [...byId.values()].filter((ev) => logIsToday(ev.at, now));
      setDismissedNoticeIds((dismissed) => {
        persistNotices(next, dismissed);
        return dismissed;
      });
      return next;
    });
  }, [membersProgressSig, roomId, dayKey]);

  const dismissNewDay = () => {
    setNewDayHidden(true);
    try { localStorage.setItem(ROOM_DISMISS_NEW_DAY_KEY, dayKey); } catch (_) {}
  };

  const dismissNotice = (id) => {
    setDismissedNoticeIds((prev) => {
      const next = new Set(prev);
      next.add(id);
      setCelebrations((celebs) => {
        persistNotices(celebs, next);
        return celebs;
      });
      return next;
    });
  };

  const visibleCelebrations = celebrations.filter((ev) => !dismissedNoticeIds.has(ev.id));
  const hasNotices = isLocal || !newDayHidden || visibleCelebrations.length > 0;
  if (!hasNotices) return null;

  return (
    <div style={{
      flexShrink: 0,
      borderTop: "1.1px dashed var(--ink-soft)",
      borderBottom: "1.1px dashed var(--ink-soft)",
      background: roomFlat() ? "var(--paper-2)" : "var(--cream-soft)",
    }}>
      {isLocal && (
        <div style={{
          padding: "6px 10px",
          fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink-2)",
          textAlign: "center",
          borderBottom: visibleCelebrations.length || !newDayHidden
            ? "1px dashed var(--ink-soft)" : "none",
        }}>
          {uiIcon("lock", "🔒")} {L("room.localMode")}
        </div>
      )}
      <div style={{ padding: isLocal ? "4px 6px 6px" : "6px 6px 6px" }}>
        {!newDayHidden && (
          <RoomDismissibleBanner
            onClose={dismissNewDay}
            leadingIcon={roomFlat() ? null : <SpriteIcon idx={ROOM_SUN_SPRITE_IDX} size={18} style={{ flexShrink: 0 }} />}
          >
            {L("room.newDay")}
          </RoomDismissibleBanner>
        )}
        {visibleCelebrations.map((ev) => (
          <RoomDismissibleBanner key={ev.id} onClose={() => dismissNotice(ev.id)}>
            {ev.text}
          </RoomDismissibleBanner>
        ))}
      </div>
    </div>
  );
}

function RoomChatLog({ members, myUid, myStatus, myTodos, roomLog, roomMeta, now, totalWorkSeconds, onToggleTodo, onToggleSub }) {
  const [expandedClaps, setExpandedClaps] = useStateRoom({});
  const chatLogBoxRef = useRefRoom(null);

  const chatTimeline = buildChatTimeline(roomLog, members, myUid);
  const timelineSig = chatTimeline.map((ev) => ev.id || `${ev.uid}-${ev.at}`).join("|");

  useEffectRoom(() => {
    const el = chatLogBoxRef.current;
    if (!el) return;
    el.scrollTop = el.scrollHeight;
  }, [timelineSig]);

  return (
    <div className={roomFlat() ? "room-flat-chat" : ""} style={{
      minHeight: 0,
      display: "flex", flexDirection: "column",
      overflow: "hidden",
      border: "1.1px solid var(--ink)",
      borderRadius: 8,
      background: roomFlat() ? "var(--paper)" : "color-mix(in srgb, var(--paper) 80%, transparent)",
      backgroundSize: roomFlat() ? undefined : "200% 200%",
      backgroundPosition: roomFlat() ? undefined : "center",
      boxShadow: roomFlat() ? "none" : "0 1px 0 rgba(255,255,255,0.45) inset",
    }}>
      <div style={{
        flex: 1, minHeight: 0, overflowY: "auto", overflowX: "hidden",
        padding: "10px 10px 0",
        background: roomFlat() ? "var(--paper)" : "color-mix(in srgb, var(--paper) 80%, transparent)",
      }}>
        {members.map((m) => {
          const isMe = m.uid === myUid;
          const todos = isMe
            ? myTodos
              .filter((t) => t.roomVisible)
              .filter((t, idx, arr) => arr.findIndex((x) => x.id === t.id) === idx)
              .slice()
              .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
              .map((t) => roomTodoView(roomVisibleTodoPayload(t), t.id))
            : Array.isArray(m.visibleTodos)
              ? m.visibleTodos.map((item, i) => roomTodoView(item, `${m.uid}-${i}`))
                  .filter((t) => t.title)
              : [];
          const label = isMe ? L("room.you") : (m.displayName || L("room.friend"));
          const offline = memberRoomStatus(m, now, myUid, myStatus) === "offline";
          return (
            <div key={m.uid} style={{ marginBottom: 12, opacity: offline ? 0.55 : 1 }}>
              <div style={{
                display: "flex", alignItems: "center", gap: 5,
                fontFamily: "var(--hand)", fontSize: 12, fontWeight: 700,
                color: isMe ? "var(--point)" : "var(--ink-2)", marginBottom: 4,
              }}>
                <span style={{
                  width: 18, height: 18, borderRadius: 5,
                  display: "grid", placeItems: "center",
                  border: "1px solid var(--ink)",
                  background: "var(--paper)",
                  flexShrink: 0,
                }}>
                  <RoomAvatarIcon value={m.avatar} size={14} />
                </span>
                <span>{label} says:</span>
              </div>
              {todos.length > 0 ? (
                <div style={{ paddingLeft: 10 }}>
                  <RoomPublicTodoRows
                    todos={todos}
                    isMe={isMe}
                    onToggleTodo={onToggleTodo}
                    onToggleSub={onToggleSub}
                  />
                </div>
              ) : (
                <div className="sk-cap" style={{ paddingLeft: 10, fontSize: 11 }}>
                  {isMe ? L("room.noTodo") : L("room.noPublicTitle")}
                </div>
              )}
            </div>
          );
        })}
      </div>
      <div style={{
        flexShrink: 0,
        height: 24,
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        gap: 8,
        padding: "0 8px",
        borderTop: "1.1px solid var(--ink)",
        background: roomFill2("linear-gradient(180deg, color-mix(in srgb, var(--paper) 92%, var(--hi-soft)) 0%, color-mix(in srgb, var(--paper) 88%, var(--blue-soft)) 100%)"),
        fontFamily: "var(--hand)",
        fontSize: 11,
        color: "var(--ink-2)",
        boxShadow: roomFlat() ? "none" : "0 1px 0 rgba(255,255,255,0.35) inset",
      }}>
        <span style={{ fontWeight: 700, color: "var(--ink)", whiteSpace: "nowrap" }}>
          {L("room.totalWork")}
        </span>
        <span style={{
          fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 700,
          color: "var(--ink)", whiteSpace: "nowrap",
        }}>
          {fmtHMS(totalWorkSeconds || 0)} · D+{roomDaysSinceCreated(roomMeta, now, members)}
        </span>
      </div>
      <div
        ref={chatLogBoxRef}
        style={{
          flexShrink: 0,
          height: ROOM_CHAT_LOG_BOX_HEIGHT,
          maxHeight: ROOM_CHAT_LOG_BOX_HEIGHT,
          overflowY: "auto",
          overflowX: "hidden",
          margin: 0,
          padding: "6px 0 0",
          borderTop: "1.1px solid var(--ink)",
          borderRadius: 0,
          background: "color-mix(in srgb, var(--paper) 92%, var(--blue-soft))",
          boxShadow: "0 1px 0 rgba(255,255,255,0.35) inset",
          WebkitOverflowScrolling: "touch",
        }}
      >
        {chatTimeline.length > 0 ? (
          <div style={{ padding: "4px 0 0" }}>
            <RoomMemberLogEvents
              events={chatTimeline}
              expandedClaps={expandedClaps}
              setExpandedClaps={setExpandedClaps}
            />
          </div>
        ) : (
          <div className="sk-cap" style={{
            fontSize: 11, color: "var(--ink-3)", padding: "6px 0 0",
          }}
          >
            {L("room.logEmpty")}
          </div>
        )}
      </div>
    </div>
  );
}

function RoomProfileSlot({ member, isMe, now, claps, displayStatus, notice, onClick, myUid, myStatus }) {
  const offline = displayStatus === "offline";
  const seconds = memberLiveSeconds(member, now, myUid, myStatus);
  const working = displayStatus === "working";
  const away = displayStatus === "away";
  const latestClap = claps.length > 0 ? claps[claps.length - 1] : null;
  const clapShow = latestClap && (now - latestClap.ts < 3000);
  const total = member.todoTotal || 0;
  const done = member.todoDone || 0;
  const iconHops = displayStatus === "online" || displayStatus === "working";
  return (
    <button onClick={onClick} title={isMe ? L("room.changeProfile") : L("room.clapHint")} style={{
      all: "unset", cursor: "pointer",
      position: "relative",
      display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
      gap: 3, minHeight: 0,
      border: "1.1px solid var(--ink)", borderRadius: 8,
      background: roomFlat()
        ? (away ? "var(--paper-3)" : "var(--paper)")
        : (away ? "linear-gradient(180deg, var(--cream-soft), var(--paper))" : (isMe ? "linear-gradient(180deg, var(--pink-soft), var(--paper))" : "linear-gradient(180deg, var(--blue-soft), var(--paper))")),
      boxShadow: roomFlat() ? "none" : "0 1px 0 rgba(255,255,255,0.45) inset",
      opacity: offline ? 0.62 : 1,
      filter: offline ? "grayscale(0.7)" : "none",
    }}>
      {clapShow && <ClapBurst />}
      {member.statusMessage?.trim() && (
        <RoomStatusBubble text={member.statusMessage.trim()} offsetTop={notice ? 34 : 6} />
      )}
      {notice && <RoomNoticeBubble text={notice.text} />}
      <span style={{
        width: 40, height: 40, borderRadius: 8,
        border: "1px solid var(--ink)",
        background: "var(--paper)", display: "grid", placeItems: "center",
      }}>
        <span style={{
          display: "grid", placeItems: "center",
          animation: iconHops ? "room-avatar-hop 1.1s steps(1, end) infinite" : "none",
        }}>
          <RoomAvatarIcon value={member.avatar} size={28} />
        </span>
      </span>
      <span style={{
        maxWidth: "90%",
        fontFamily: "var(--hand)", fontSize: 11.5, fontWeight: 700,
        color: "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
      }}>{member.displayName || L("room.noName")}</span>
      <span style={{ display: "flex", alignItems: "center", gap: 3 }}>
        <span style={{
          width: 6, height: 6, borderRadius: "50%",
          background: getStatusDotColor(
            working ? "working" : (away ? "away" : (offline ? "offline" : "online")),
            working ? "#35c45a" : (away ? "#f59e0b" : (offline ? "#9aa8bb" : "#f0c14d")),
          ),
        }} />
        <span style={{ fontFamily: "var(--mono)", fontSize: 9, color: "var(--ink-2)" }}>
          {offline ? L("room.offlineStatus") : (away ? L("room.away") : (working ? L("room.workingStatus") : L("room.onlineStatus")))}
        </span>
      </span>
      <span style={{ fontFamily: "var(--mono)", fontSize: 9, color: "var(--ink-2)" }}>{fmtHMS(seconds)}</span>
      <MemberExpOrLevel member={member} done={done} total={total} />
    </button>
  );
}

/** 상태 메시지 말풍선 (프로필 슬롯 왼쪽) */
function RoomStatusBubble({ text, offsetTop = 6 }) {
  return (
    <div style={{
      position: "absolute",
      left: -118,
      top: offsetTop,
      zIndex: 18,
      maxWidth: 112,
      boxSizing: "border-box",
      padding: "5px 7px",
      border: "1.5px solid var(--ink)",
      borderRadius: 2,
      background: "var(--paper)",
      color: "var(--ink)",
      fontFamily: "var(--hand)",
      fontSize: 10.5,
      lineHeight: 1.28,
      textAlign: "left",
      wordBreak: "break-all",
      boxShadow: "2px 2px 0 color-mix(in srgb, var(--ink-soft) 55%, transparent)",
      pointerEvents: "none",
    }}>
      <span style={{
        position: "absolute",
        right: -7,
        top: 14,
        width: 10,
        height: 10,
        background: "var(--paper)",
        borderRight: "1.5px solid var(--ink)",
        borderBottom: "1.5px solid var(--ink)",
        transform: "rotate(-45deg)",
      }} />
      <span style={{ position: "relative", zIndex: 1 }}>{text}</span>
    </div>
  );
}

function RoomNoticeBubble({ text }) {
  return (
    <div style={{
      position: "absolute",
      right: 56,
      top: 8,
      zIndex: 20,
      width: 142,
      boxSizing: "border-box",
      padding: "6px 8px",
      border: "2px solid var(--ink)",
      borderRadius: 2,
      background: "var(--paper)",
      color: "var(--ink)",
      fontFamily: "var(--hand)",
      fontSize: 11.5,
      lineHeight: 1.25,
      textAlign: "left",
      boxShadow: "3px 3px 0 color-mix(in srgb, var(--ink-soft) 60%, transparent)",
      pointerEvents: "none",
      imageRendering: "pixelated",
    }}>
      <span style={{
        position: "absolute",
        right: -8,
        top: 22,
        width: 12,
        height: 12,
        background: "var(--paper)",
        borderRight: "2px solid var(--ink)",
        borderBottom: "2px solid var(--ink)",
        transform: "rotate(-45deg)",
      }} />
      <span style={{
        position: "absolute",
        left: -9,
        top: -14,
        fontSize: 20,
        lineHeight: 1,
        color: "var(--hi)",
        textShadow: "1px 0 var(--ink), -1px 0 var(--ink), 0 1px var(--ink), 0 -1px var(--ink)",
      }}>{uiIcon("sparkle", "✦")}</span>
      <span style={{ position: "relative", zIndex: 1 }}>{text}</span>
    </div>
  );
}

function RoomEmptySlot() {
  return <div style={{ minHeight: 0 }} />;
}

function RoomTodoRegisterPanel({ todos }) {
  const [, force] = useStateRoom(0);
  const selectedRef = useRefRoom(new Set());
  const sig = todos.map(t => `${t.id}:${t.roomVisible ? 1 : 0}`).join("|");
  useEffectRoom(() => {
    selectedRef.current = new Set(todos.filter(t => t.roomVisible).map(t => t.id));
    force(x => x + 1);
  }, [sig]);

  const selected = selectedRef.current;
  const toggle = (id) => {
    const next = new Set(selectedRef.current);
    if (next.has(id)) next.delete(id);
    else next.add(id);
    selectedRef.current = next;
    force(x => x + 1);
  };
  const register = () => {
    todos.forEach((t) => {
      const shouldShow = selectedRef.current.has(t.id);
      if (!!t.roomVisible !== shouldShow) diary.actions.toggleTodoVisible(t.id);
    });
    window.dispatchEvent(new CustomEvent("room-flash", { detail: { msg: L("room.registered") } }));
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 6,
        fontFamily: "var(--hand)", fontSize: 11.5, color: "var(--ink-2)",
      }}>
        <span style={{ fontWeight: 700, color: "var(--ink)" }}>{L("room.shareToday")}</span>
        <span style={{ flex: 1 }} />
        <span style={{ fontFamily: "var(--mono)", fontSize: 10 }}>{selected.size}/{todos.length}</span>
      </div>
      <div style={{
        maxHeight: 82,
        overflowY: "auto", overflowX: "hidden",
        display: "flex", flexDirection: "column", gap: 4,
        paddingRight: 2,
      }}>
        {todos.length ? todos.map(t => {
          const on = selected.has(t.id);
          const subs = t.subTasks || [];
          return (
            <div key={t.id} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
              <button onClick={() => toggle(t.id)} style={{
                all: "unset", cursor: "pointer",
                display: "flex", alignItems: "center", gap: 6,
                padding: "3px 7px",
                border: "1px solid var(--ink)",
                borderRadius: 6,
                background: on ? "color-mix(in srgb, var(--paper) 80%, var(--blue-soft))" : "color-mix(in srgb, var(--paper) 80%, transparent)",
                fontFamily: "var(--hand)", fontSize: 12, color: t.done ? "var(--ink-3)" : "var(--ink)",
              }}>
                <span style={{
                  width: 11, height: 11, borderRadius: 2,
                  border: "1px solid var(--ink)",
                  background: on ? "var(--mint)" : "var(--paper)",
                  display: "grid", placeItems: "center",
                  fontSize: 8, color: "#fff", flexShrink: 0,
                }}>{on ? "✓" : ""}</span>
                <span style={{
                  flex: 1, minWidth: 0,
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                  textDecoration: t.done ? "line-through" : "none",
                }}>{t.title}</span>
              </button>
              {on && subs.length > 0 && subs.map((st) => (
                <div key={st.id} style={{
                  paddingLeft: 22, paddingRight: 4,
                  fontFamily: "var(--hand)", fontSize: 10.5, lineHeight: 1.25,
                  color: st.done ? "var(--ink-3)" : "var(--ink-2)",
                  textDecoration: st.done ? "line-through" : "none",
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                }}>
                  · {st.title}
                </div>
              ))}
            </div>
          );
        }) : (
          <div className="sk-cap" style={{ padding: "4px 2px", fontSize: 11 }}>{L("room.noTodayTasks")}</div>
        )}
      </div>
      <button onClick={register} style={{
        all: "unset", cursor: "pointer",
        display: "grid", placeItems: "center",
        minHeight: 24,
        border: "1.1px solid var(--ink)", borderRadius: 6,
        background: "color-mix(in srgb, var(--paper) 78%, var(--blue-soft))",
        fontFamily: "var(--hand)", fontSize: 12, fontWeight: 700, color: "var(--ink)",
        boxShadow: "0 1px 0 rgba(255,255,255,0.45) inset",
      }}>{L("room.register")}</button>
    </div>
  );
}

// ===========================================================
// MemberCard — 왼쪽 todo 리스트 / 오른쪽 프로필 / 하단 진행도 바
//   ┌─ header ─────────────────────────┐
//   │ todo list (left)   │ avatar+name │
//   │                    │ 03:14 🟢   │
//   │ ▓▓▓▓░░░░░░  4/8                  │
//   └──────────────────────────────────┘
// ===========================================================
function MemberCard({ member, isMe, now, onClap, claps, selfTodos, onEditProfile, myUid, myStatus }) {
  const displayStatus = memberRoomStatus(member, now, myUid, myStatus);

  const isWorking = displayStatus === "working";
  const isOffline = displayStatus === "offline";
  const isPaused = displayStatus === "paused";
  const isAway = displayStatus === "away";
  // 카드를 흐리게 처리할지 — paused/offline 둘 다 회색이지만 paused 는 살짝 더 선명
  const dim = isOffline || isPaused;
  const seconds = memberLiveSeconds(member, now, myUid, myStatus);

  // 완료 이벤트 디텍션
  const prevDoneRef = useRefRoom(member.todoDone);
  const [sparkle, setSparkle] = useStateRoom(false);
  useEffectRoom(() => {
    if (member.todoDone > prevDoneRef.current) {
      setSparkle(true);
      const id = setTimeout(() => setSparkle(false), 2400);
      prevDoneRef.current = member.todoDone;
      return () => clearTimeout(id);
    }
    prevDoneRef.current = member.todoDone;
  }, [member.todoDone]);

  const latestClap = claps.length > 0 ? claps[claps.length - 1] : null;
  const clapShow = latestClap && (now - latestClap.ts < 3000);

  const total = member.todoTotal || 0;
  const done = member.todoDone || 0;
  const progress = total > 0 ? Math.min(1, done / total) : 0;
  const cardStatusDot = getStatusDotColor(
    isWorking ? "working" : (isAway ? "away" : (isOffline ? "offline" : (isPaused ? "paused" : "idle"))),
    isWorking ? "#60c47a" : (isAway ? "#f0c14d" : "#cbd5e0"),
  );
  const statusLabel =
    isPaused  ? L("room.pausedStatus")
    : isOffline ? L("room.offlineStatus")
    : isAway   ? L("room.away")
    : "";  // working / idle 은 라벨 없음

  // todo 리스트 결정
  // - 본인(isMe): diary 의 모든 오늘 할일 + 각 항목에 roomVisible 표시
  // - 남: member.visibleTodos 의 제목들만 (비공개 제목은 서버에 없음)
  let visibleTodos = null;
  if (isMe && selfTodos) {
    visibleTodos = selfTodos.map((t) => ({
      id: t.id,
      title: t.title,
      done: t.done,
      roomVisible: !!t.roomVisible,
      subTasks: (t.subTasks || []).map((st, i) => ({
        id: st.id || `${t.id}-s${i}`,
        title: st.title,
        done: !!st.done,
      })),
    }));
  } else if (!isMe && Array.isArray(member.visibleTodos) && member.visibleTodos.length > 0) {
    visibleTodos = member.visibleTodos
      .map((item, i) => roomTodoView(item, "v" + i))
      .filter((t) => t.title);
  }

  const tone = isMe ? "var(--pink-soft)" : "var(--blue-soft)";
  const headerTone = isMe ? "var(--pink)" : "var(--blue)";

  return (
    <div style={{
      position: "relative",
      background: dim ? "var(--paper-2)" : "var(--paper)",
      border: "1.1px solid " + (dim ? "var(--ink-soft)" : "var(--ink)"),
      borderRadius: 12,
      overflow: "hidden",
      opacity: isOffline ? 0.65 : (isPaused ? 0.85 : 1),
      filter: isOffline ? "grayscale(0.7)" : (isPaused ? "grayscale(0.4)" : "none"),
      boxShadow: dim ? "none" : "0 2px 0 var(--paper-3)",
      outline: sparkle ? "1.8px solid var(--hi)" : "none",
      outlineOffset: 1,
      display: "flex", flexDirection: "column",
      minHeight: 0,
    }}>
      {sparkle && <Sparkle />}
      {clapShow && <ClapBurst />}

      {/* 카드 헤더 — 미니 타이틀바 */}
      <div style={{
        flexShrink: 0,
        height: 18,
        background: dim ? "var(--paper-3)" : roomFill(`linear-gradient(180deg, ${headerTone} 0%, ${tone} 100%)`, "var(--paper-2)"),
        borderBottom: "1px solid " + (dim ? "var(--ink-soft)" : "var(--ink)"),
        padding: "0 8px",
        display: "flex", alignItems: "center", gap: 5,
        fontSize: 9, color: "var(--ink)",
        letterSpacing: "0.04em",
      }}>
        <span style={{ flex: 1 }} />
        {isMe && <StatusToggle currentStatus={member.status} />}
        {!isMe && !isOffline && !isPaused && (
          <button onClick={onClap} title={L("room.clapHint")} style={miniHeaderBtn}>{uiIcon("clap", "👏")}</button>
        )}
      </div>

      {/* 본체 — 좌(todo) / 우(프로필) 좌우 분할 */}
      <div style={{
        flex: 1, minHeight: 0,
        display: "flex", gap: 8,
        padding: "7px 9px 3px",
      }}>
        {/* 왼쪽: todo 리스트 (대화창 느낌) */}
        <div style={{
          flex: 1, minWidth: 0, minHeight: 0,
          overflowY: "auto", overflowX: "hidden",
          paddingRight: 2,
        }}>
          {visibleTodos && visibleTodos.length > 0 ? (
            visibleTodos.map((t, i) => {
              const isPrivate = isMe && !t.roomVisible;
              const showSubs = !isPrivate && (t.subTasks || []).length > 0;
              return (
                <div key={t.id || i} style={{ marginBottom: 2, opacity: isPrivate ? 0.55 : 1 }}>
                  <div style={{
                    display: "flex", alignItems: "center", gap: 5,
                    fontFamily: "var(--hand)", fontSize: 12, lineHeight: 1.25,
                    color: t.done ? "var(--ink-3)" : "var(--ink-2)",
                    textDecoration: t.done ? "line-through" : "none",
                  }}>
                    <span style={{
                      flexShrink: 0,
                      width: 9, height: 9, borderRadius: 2,
                      border: "1.1px solid " + (t.done ? "var(--ink-3)" : "var(--ink-2)"),
                      background: t.done ? "var(--mint)" : "transparent",
                      display: "grid", placeItems: "center",
                      fontSize: 8,
                    }}>{t.done ? "✓" : ""}</span>
                    <span style={{
                      flex: 1, minWidth: 0,
                      whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                    }}>{t.title}</span>
                    {isMe && (
                      <button
                        onClick={() => diary.actions.toggleTodoVisible(t.id)}
                        title={t.roomVisible ? L("room.visibleTipSub") : L("room.privateTipSub")}
                        style={{
                          all: "unset", cursor: "pointer",
                          width: 16, height: 14, borderRadius: 4,
                          display: "grid", placeItems: "center",
                          fontSize: 9, flexShrink: 0,
                          background: t.roomVisible ? "var(--hi)" : "transparent",
                          border: t.roomVisible ? "1px solid var(--ink)" : "1px solid var(--ink-soft)",
                          filter: t.roomVisible ? "none" : "grayscale(1)",
                          opacity: t.roomVisible ? 1 : 0.45,
                        }}
                      >
                        {t.roomVisible ? uiIcon("visible", "●") : uiIcon("hidden", "○")}
                      </button>
                    )}
                  </div>
                  {showSubs && (t.subTasks || []).map((st) => (
                    <div key={st.id} style={{
                      display: "flex", alignItems: "center", gap: 4,
                      paddingLeft: 14, marginTop: 1,
                      fontFamily: "var(--hand)", fontSize: 10.5, lineHeight: 1.2,
                      color: st.done ? "var(--ink-3)" : "var(--ink-2)",
                      textDecoration: st.done ? "line-through" : "none",
                    }}>
                      <span style={{
                        width: 7, height: 7, borderRadius: 2, flexShrink: 0,
                        border: "1px solid var(--ink-soft)",
                        background: st.done ? "var(--mint)" : "transparent",
                        display: "grid", placeItems: "center", fontSize: 6,
                      }}>{st.done ? "✓" : ""}</span>
                      <span style={{
                        flex: 1, minWidth: 0,
                        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                      }}>{st.title}</span>
                    </div>
                  ))}
                </div>
              );
            })
          ) : (
            <div className="sk-cap" style={{ fontSize: 11, color: "var(--ink-3)", padding: "4px 2px", lineHeight: 1.3 }}>
              {isMe
                ? (total > 0 ? L("room.privateWithTasks") : L("room.noTodo"))
                : (total > 0 ? L("room.privateProgress", { n: total - done }) : L("room.noTodo"))}
            </div>
          )}
        </div>

        {/* 오른쪽: 프로필 (둥근사각 아바타 + 닉네임 + 작업시간) */}
        <div style={{
          flexShrink: 0, width: 78,
          display: "flex", flexDirection: "column", alignItems: "center", gap: 3,
        }}>
          <button
            onClick={isMe && onEditProfile ? onEditProfile : undefined}
            title={isMe ? L("room.changeNameAvatar") : member.displayName}
            style={{
              all: "unset",
              cursor: isMe ? "pointer" : "default",
              width: 48, height: 48, borderRadius: 10,
              border: "1.1px solid " + (dim ? "var(--ink-soft)" : "var(--ink)"),
              background: isWorking ? "var(--hi-soft)" : "var(--paper-2)",
              display: "grid", placeItems: "center",
              fontSize: 28, flexShrink: 0,
              boxShadow: "0 1.5px 0 var(--paper-3)",
            }}>
            <RoomAvatarIcon value={member.avatar} size={32} />
          </button>
          <div style={{
            fontFamily: "var(--hand)", fontSize: 12, fontWeight: 700,
            color: "var(--ink)", textAlign: "center",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
            maxWidth: "100%",
          }}>
            {member.displayName || L("room.noName")}
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 3 }}>
            <span style={{ width: 6, height: 6, borderRadius: "50%", background: cardStatusDot }} />
            <span className="sk-mono" style={{ fontSize: 9, color: "var(--ink-2)" }}>
              {isWorking ? fmtHMS(seconds) : (seconds > 0 ? fmtHMS(seconds) : "--:--")}
            </span>
            {statusLabel && (
              <div style={{
                fontFamily: "var(--hand)", fontSize: 9, color: "var(--ink-3)",
                marginTop: 1, letterSpacing: "0.02em",
              }}>{statusLabel}</div>
            )}
          </div>
        </div>
      </div>

      {/* 하단: 경험치 바 또는 레벨 */}
      <div style={{
        flexShrink: 0,
        padding: "5px 9px 7px",
        borderTop: "1px dashed var(--ink-soft)",
        display: "flex", alignItems: "center", justifyContent: "center", gap: 7,
        minHeight: 22,
      }}>
        {memberDisplayLevel(member, done, total) != null ? (
          <span style={{
            fontFamily: "var(--hand)", fontSize: 13, fontWeight: 800,
            color: "var(--point)",
          }}>
            {L("room.levelShort", { n: memberDisplayLevel(member, done, total) })}
          </span>
        ) : (
          <>
            <div style={{
              flex: 1, height: 8, borderRadius: 99,
              background: "var(--paper-3)", overflow: "hidden",
              border: "1px solid var(--ink-soft)",
            }}>
              <div style={{
                width: `${progress * 100}%`, height: "100%",
                background: roomFill(
                  isWorking
                    ? "linear-gradient(90deg, var(--pink), var(--hi))"
                    : "linear-gradient(90deg, var(--point), var(--hi))",
                  "var(--ink-3)",
                ),
                transition: "width 0.4s",
              }} />
            </div>
            <span style={{
              fontFamily: "var(--mono)", fontSize: 10, fontWeight: 700, color: "var(--ink-2)",
              minWidth: 32, textAlign: "right",
            }}>
              {done}/{total}
            </span>
          </>
        )}
      </div>
    </div>
  );
}

// 내 카드 헤더의 작은 상태 토글 (working / away / paused). 클릭 시 메뉴.
// offline 은 자동만 (앱 종료/heartbeat 타임아웃) — 메뉴에 없음.
function StatusToggle({ currentStatus }) {
  const room = window.roomStore.useRoom();
  const [open, setOpen] = useStateRoom(false);
  // 메뉴 바깥 클릭 시 닫기
  useEffectRoom(() => {
    if (!open) return;
    const h = () => setOpen(false);
    setTimeout(() => window.addEventListener("click", h, { once: true }), 0);
    return () => window.removeEventListener("click", h);
  }, [open]);
  const my = room.state.myStatus || "working";
  const cur = currentStatus === "paused" ? "paused"
            : currentStatus === "away" ? "away"
            : my;  // 서버 상태 우선이되, paused/away 가 아니면 로컬 my 사용
  const dot = getStatusDotColor(
    cur,
    cur === "working" ? "#60c47a" : (cur === "away" ? "#f0c14d" : "#cbd5e0"),
  );
  const choose = (next) => (e) => {
    e.stopPropagation();
    room.setMyStatus(next);
    setOpen(false);
  };
  return (
    <span style={{ position: "relative", display: "inline-flex" }}>
      <button
        onClick={(e) => { e.stopPropagation(); setOpen((o) => !o); }}
        title={L("room.changeMyStatus")}
        style={{
          ...miniHeaderBtn,
          background: "transparent",
          border: "1px solid var(--ink)",
          padding: 0,
          width: 16, height: 14,
          display: "grid", placeItems: "center",
        }}>
        <span style={{
          width: 6, height: 6, borderRadius: "50%", background: dot,
        }} />
      </button>
      {open && (
        <div onClick={(e) => e.stopPropagation()} style={{
          position: "absolute", top: 18, right: 0, zIndex: 30,
          background: "var(--paper)",
          border: "1.1px solid var(--ink)",
          borderRadius: 8,
          boxShadow: "0 4px 12px rgba(40,51,63,0.18)",
          padding: 4, minWidth: 110,
        }}>
          {[
            { v: "working", color: getStatusDotColor("working", "#60c47a"), label: L("room.workingStatus") },
            { v: "away",    color: getStatusDotColor("away", "#f0c14d"), label: L("room.away") },
            { v: "paused",  color: getStatusDotColor("paused", "#cbd5e0"), label: L("room.pausedStatus") },
          ].map((opt) => (
            <button key={opt.v} onClick={choose(opt.v)} style={{
              all: "unset", cursor: "pointer",
              display: "flex", alignItems: "center", gap: 6,
              width: "100%", boxSizing: "border-box",
              padding: "4px 8px", borderRadius: 5,
              fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink)",
              background: cur === opt.v ? "var(--hi-soft)" : "transparent",
            }}>
              <span style={{ width: 7, height: 7, borderRadius: "50%", background: opt.color }} />
              <span>{opt.label}</span>
              {cur === opt.v && <span style={{ marginLeft: "auto", fontSize: 9 }}>✓</span>}
            </button>
          ))}
        </div>
      )}
    </span>
  );
}

// EmptySlot — 빈 자리. 코드는 상단에 이미 있으므로 여기선 "+ 초대" 버튼만.
function EmptySlot({ index, onCopy }) {
  return (
    <button
      onClick={onCopy}
      title={L("room.copyInviteSame")}
      style={{
        all: "unset", cursor: "pointer",
        position: "relative",
        background: "transparent",
        border: "1.1px dashed var(--ink-soft)",
        borderRadius: 12,
        display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
        minHeight: 0,
        color: "var(--ink-3)",
        fontFamily: "var(--hand)", fontSize: 12,
      }}
    >
      <span style={{
        width: 22, height: 22, borderRadius: 6,
        border: "1.1px dashed var(--ink-soft)",
        background: "var(--paper)",
        display: "grid", placeItems: "center",
        fontSize: 14,
      }}>＋</span>
      초대
    </button>
  );
}

// ---- 작은 연출 ----
function Sparkle() {
  const sym = uiIcon("sparkle", "✨");
  if (!sym) return null;
  return (
    <div style={{
      position: "absolute", top: -6, right: 10,
      fontSize: 18, animation: "room-sparkle-pop 2.2s ease-out both",
      pointerEvents: "none", color: "var(--ink-2)",
    }}>{sym}</div>
  );
}
function ClapBurst() {
  const sym = uiIcon("clap", "👏");
  if (!sym) return null;
  return (
    <div style={{
      position: "absolute", top: -4, right: 14,
      fontSize: 22, animation: "room-clap-pop 2.6s ease-out both",
      pointerEvents: "none", color: "var(--ink-2)",
    }}>{sym}</div>
  );
}

// ---- 프로필 편집 모달 ----
function ProfileEditModal() {
  const room = window.roomStore.useRoom();
  const [open, setOpen] = useStateRoom(false);
  const [name, setName] = useStateRoom(room.state.profile.displayName || "");
  const [statusMsg, setStatusMsg] = useStateRoom("");
  const [tick, setTick] = useStateRoom(0);
  const myUid = window.firebaseBridge ? window.firebaseBridge.uid() : null;
  const me = room.state.members.find((m) => m.uid === myUid);
  const now = Date.now();

  useEffectRoom(() => {
    const h = () => {
      const uid = window.firebaseBridge ? window.firebaseBridge.uid() : null;
      const member = room.state.members.find((m) => m.uid === uid);
      setName(room.state.profile.displayName || "");
      setStatusMsg(member?.statusMessage || "");
      setOpen(true);
    };
    window.addEventListener("room-edit-profile", h);
    return () => window.removeEventListener("room-edit-profile", h);
  }, [room.state.profile.displayName, room.state.members]);

  useEffectRoom(() => {
    if (!open) return;
    const id = setInterval(() => setTick((t) => t + 1), 1000);
    return () => clearInterval(id);
  }, [open]);

  if (!open) return null;
  void tick;
  const close = () => setOpen(false);
  const prevStatus = me?.statusMessage || "";
  const nextStatus = clampStatusMessage(statusMsg);
  const statusChanged = nextStatus !== prevStatus;
  const cooldownLeft = statusChanged ? statusMessageCooldownRemaining(me, now) : 0;
  const statusBlocked = statusChanged && cooldownLeft > 0;

  const save = () => {
    room.setProfile({ displayName: name.trim() });
    if (statusChanged) {
      if (statusBlocked) {
        window.dispatchEvent(new CustomEvent("room-flash", {
          detail: { msg: formatStatusCooldown(cooldownLeft) },
        }));
        return;
      }
      room.patchMyMemberImmediate({
        statusMessage: nextStatus,
        statusMessageAt: Date.now(),
      });
    }
    close();
  };

  return (
    <div onClick={close} style={{
      position: "fixed", inset: 0, background: "rgba(40,51,63,0.25)",
      backdropFilter: "blur(2px)", display: "grid", placeItems: "center",
      zIndex: 1000, padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} className="sk-box" style={{
        background: "var(--paper)", padding: 16, width: "min(320px, 92vw)",
        boxShadow: "0 8px 0 var(--paper-3), 0 12px 30px rgba(40,51,63,0.18)",
      }}>
        <div className="sk-label" style={{ marginBottom: 10 }}>{L("room.pickAvatar")}</div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 12 }}>
          {room.avatars.map((a) => (
            <button key={a} onClick={() => room.setProfile({ avatar: a })}
              style={{
                ...avatarBtn, width: 30, height: 30,
                background: room.state.profile.avatar === a ? "var(--hi)" : "var(--paper)",
              }}><RoomAvatarIcon value={a} size={20} /></button>
          ))}
        </div>
        <div className="sk-label" style={{ marginBottom: 6 }}>{L("room.namePrompt")}</div>
        <input value={name} onChange={(e) => setName(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") save(); }}
          autoFocus
          style={{ ...formInput, width: "100%" }} />
        <div className="sk-label" style={{ marginBottom: 6, marginTop: 12 }}>{L("room.statusMessageLabel")}</div>
        <input
          value={statusMsg}
          onChange={(e) => setStatusMsg(clampStatusMessage(e.target.value))}
          onKeyDown={(e) => { if (e.key === "Enter") save(); }}
          maxLength={ROOM_STATUS_MSG_MAX}
          placeholder={L("room.statusMessagePh")}
          style={{ ...formInput, width: "100%" }}
        />
        <div style={{
          marginTop: 4, fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)",
          display: "flex", justifyContent: "space-between", gap: 8,
        }}>
          <span>{L("room.statusMessageHint")}</span>
          <span>{statusMsg.length}/{ROOM_STATUS_MSG_MAX}</span>
        </div>
        {statusBlocked && (
          <div style={{
            marginTop: 6, fontFamily: "var(--hand)", fontSize: 11, color: "var(--point)",
          }}>
            {formatStatusCooldown(cooldownLeft)}
          </div>
        )}
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 6, marginTop: 12 }}>
          <button onClick={close} style={smallBtn}>{L("common.cancel")}</button>
          <button onClick={save} style={smallBtnPrimaryStyle()}>{L("common.save")}</button>
        </div>
      </div>
    </div>
  );
}

// ---- 작은 토스트 ("코드 복사됨") ----
function FlashToast() {
  const [msg, setMsg] = useStateRoom("");
  useEffectRoom(() => {
    const h = (e) => {
      setMsg(e.detail?.msg || "");
      setTimeout(() => setMsg(""), 1400);
    };
    window.addEventListener("room-flash", h);
    return () => window.removeEventListener("room-flash", h);
  }, []);
  if (!msg) return null;
  return (
    <div style={{
      position: "absolute", left: "50%", bottom: 60, transform: "translateX(-50%)",
      padding: "6px 14px", borderRadius: 99,
      background: "var(--ink)", color: "var(--paper)",
      fontFamily: "var(--hand)", fontSize: 12,
      boxShadow: "0 4px 12px rgba(40,51,63,0.25)",
      pointerEvents: "none",
      animation: "room-toast-pop 1.4s ease-out both",
    }}>
      ✓ {msg}
    </div>
  );
}

// ===========================================================
// Self → Server 동기화 — diary 상태 변화 감지해 사건 단위로 patch
// ===========================================================
function useRoomSync() {
  const room = window.roomStore.useRoom();
  const { state: ds } = diary.useDiary();
  const roomId = room.state.currentRoomId;

  // 현재 프로젝트의 오늘 진행도
  const todos = (ds.todos || []).filter((t) => t.projectId === ds.currentProjectId);
  const today = diary.today();
  const todayTodos = todos.filter((t) => {
    if (!t.dueDate) return true;
    if (t.dueDate === today) return true;
    if (t.dueDate < today && !t.done) return true;
    return false;
  });
  const todoTotal = todayTodos.length;
  const todoDone = todayTodos.filter((t) => t.done).length;
  // 공개로 표시한 할일 제목들 — 효과 deps 에 sig 형태로 들어가서 변경 감지
  const visibleTitlesSig = todayTodos
    .filter((t) => t.roomVisible)
    .map((t) => (t.order ?? 0) + ":" + (t.pinned ? "p" : "") + ":" + visibleTodoSig(roomVisibleTodoPayload(t)))
    .join("|");

  // 작업 중 여부 — 외부모드 ON 이거나, 트래커가 "활성"으로 보는 상황 ≈ workSessions가 방금 갱신됨
  // 단순화: 외부모드 ON 이면 무조건 작업중. 그 외엔 lastTs 기준 최근 3분 이내면 작업중.
  // 단, 사용자 수동 상태가 away/paused 면 트래커 무시하고 시간 흐름 멈춤.
  const myStatus = room.state.myStatus || "working";
  const trackerRunning = !!(window.workTracker && window.workTracker.isRunning());
  const todaySession = (ds.workSessions || []).find((w) => w.date === today);
  const workingNow = (myStatus === "working") && trackerRunning;

  // me (서버 상태) 가 source of truth — ref-less 설계로 앱 재시작 시에도 일관됨.
  const myUid = window.firebaseBridge ? window.firebaseBridge.uid() : null;
  const me = room.state.members.find((m) => m.uid === myUid);
  const prevDoneRef = useRefRoom(null);
  const lastHeartbeatAtRef = useRefRoom(0);
  const prevMyStatusRef = useRefRoom(myStatus);

  useEffectRoom(() => {
    prevDoneRef.current = null;
  }, [roomId]);

  const sendHeartbeat = (immediate) => {
    if (!roomId || !myUid) return;
    if (myStatus === "paused" || myStatus === "away") return;
    const now = Date.now();
    if (!immediate && lastHeartbeatAtRef.current
      && (now - lastHeartbeatAtRef.current) < ROOM_HEARTBEAT_MIN_AGE_MS) return;
    lastHeartbeatAtRef.current = now;
    if (immediate) room.patchMyMemberImmediate({});
    else room.patchMyMember({});
  };

  useEffectRoom(() => {
    if (!roomId || !myUid || !window.firebaseBridge) return;

    const patch = {};
    const meWorkStartedAt = me?.workStartedAt || null;
    const meAcc = me?.accumulatedSecondsToday || 0;
    const meLastActiveAt = me?.lastActiveAt || null;
    const meStatus = me?.status || null;
    const meTodoTotal = me?.todoTotal ?? -1;
    const meTodoDone = me?.todoDone ?? -1;
    const meVisibleSig = Array.isArray(me?.visibleTodos)
      ? me.visibleTodos.map((item) => visibleTodoSig(item)).join("|")
      : "";
    const logOps = [];
    const doneMap = new Map(todayTodos.map((t) => [t.id, { done: !!t.done, title: t.title, visible: !!t.roomVisible }]));
    const isAway = myStatus === "away";
    if (!prevDoneRef.current) {
      prevDoneRef.current = doneMap;
    } else {
      doneMap.forEach((cur, id) => {
        const prev = prevDoneRef.current.get(id);
        if (!prev) return;
        if (prev.done && !cur.done) {
          logOps.push({ action: "remove", todoId: id });
        } else if (!isAway && !prev.done && cur.done) {
          logOps.push({
            action: "upsert",
            todoId: id,
            nickname: room.state.profile.displayName || me?.displayName || L("room.you"),
            text: cur.visible && cur.title?.trim()
              ? L("room.donePublic", { title: cur.title.trim() })
              : L("room.donePrivate"),
            todoPublic: !!cur.visible,
          });
        }
      });
      prevDoneRef.current = doneMap;
    }

    // 진행도 — me 기준 비교 (서버에 이미 같은 값이면 skip)
    if (todoTotal !== meTodoTotal) patch.todoTotal = todoTotal;
    if (todoDone !== meTodoDone) patch.todoDone = todoDone;

    const todayAllDone = todoTotal > 0 && todoDone >= todoTotal;
    const meLevelDay = me?.lastLevelDay || null;
    const meRoomLevel = me?.roomLevel || 0;
    if (todayAllDone && meLevelDay !== today) {
      patch.roomLevel = meRoomLevel + 1;
      patch.lastLevelDay = today;
    }

    // ★ 공개 표시한 할일의 "제목"만. 비공개 제목은 여기서 누락 → 서버에 절대 안 올라감.
    const visibleTodosToSend = todayTodos
      .filter((t) => t.roomVisible)
      .slice()
      .sort((a, b) => {
        if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
        return (a.order ?? 0) - (b.order ?? 0);
      })
      .map((t) => roomVisibleTodoPayload(t));
    const visibleSig = visibleTodosToSend.map((t) => visibleTodoSig(t)).join("|");
    if (visibleSig !== meVisibleSig) {
      patch.visibleTodos = visibleTodosToSend;
    }

    // ===== 세션 (workStartedAt / accumulatedSecondsToday) =====
    // 활성 구간만 accumulated 에 누적. away/paused 진입 시 즉시 commit 후 시계 정지.
    const sessionShouldCommitAway = isAway && meWorkStartedAt;
    const sessionShouldEnd = !isAway && !workingNow && meWorkStartedAt;
    const sessionShouldStart = !isAway && myStatus !== "paused" && workingNow && !meWorkStartedAt;

    if (sessionShouldCommitAway || sessionShouldEnd) {
      const cutoff = sessionShouldCommitAway
        ? Date.now()
        : ((meLastActiveAt && meLastActiveAt > meWorkStartedAt)
          ? Math.min(meLastActiveAt, Date.now())
          : Date.now());
      const ranSec = Math.max(0, Math.floor((cutoff - meWorkStartedAt) / 1000));
      patch.workStartedAt = null;
      patch.accumulatedSecondsToday = meAcc + ranSec;
      patch.accumulatedDate = today;
      if (ranSec > 0) {
        patch.workSecondsTotal = (me?.workSecondsTotal || 0) + ranSec;
      }
    } else if (sessionShouldStart) {
      patch.workStartedAt = true;
    }

    if (myStatus !== meStatus) {
      patch.status = myStatus;
    }

    const logEntry = logOps.length === 1 ? logOps[0] : (logOps.length > 1 ? logOps : null);
    if (Object.keys(patch).length > 0 || logEntry) {
      room.patchMyMember(patch, logEntry);
    }
  }, [roomId, myUid, todoTotal, todoDone, workingNow, visibleTitlesSig, myStatus,
      me?.workStartedAt, me?.accumulatedSecondsToday, me?.workSecondsTotal, me?.status,
      me?.todoTotal, me?.todoDone, me?.roomLevel, me?.lastLevelDay]);

  // (showTodoTitle 변화 감지는 위 메인 effect 의 deps 에 me?.showTodoTitle 가 들어 있어
  // currentTodoTitle/recentTodos 가 같은 patch 로 묶여 함께 나감 — 별도 effect 필요 없음)

  // Heartbeat — 4분 tick, 로컬 전송 시각 가드. paused/away 는 write 없음.
  useEffectRoom(() => {
    if (!roomId || !myUid) return;
    const prevStatus = prevMyStatusRef.current;
    prevMyStatusRef.current = myStatus;
    if (myStatus === "paused" || myStatus === "away") return;
    // 복귀 직후: setMyStatus 가 이미 lastActiveAt 을 갱신 — 중복 write 방지
    const returnedFromAway = prevStatus === "away" && myStatus === "working";
    if (!returnedFromAway) sendHeartbeat(true);
    const id = setInterval(() => sendHeartbeat(false), ROOM_HEARTBEAT_INTERVAL_MS);
    return () => clearInterval(id);
  }, [roomId, myUid, myStatus]);

  // 호스트 — 1시간 이상 오프라인 유령 멤버 자동 제거 (#19)
  const ghostKickRef = useRefRoom(new Set());
  const membersSig = room.state.members.map((m) => `${m.uid}:${m.lastActiveAt || 0}`).join("|");
  useEffectRoom(() => {
    const hostUid = room.state.roomMeta?.hostUid;
    if (!roomId || !myUid || hostUid !== myUid) return;
    const prune = () => {
      const now = Date.now();
      for (const m of room.state.members) {
        if (m.uid === myUid) continue;
        if (!isMemberOffline(m, now)) continue;
        if (now - (m.lastActiveAt || 0) < ROOM_MEMBER_GHOST_KICK_MS) continue;
        if (ghostKickRef.current.has(m.uid)) continue;
        ghostKickRef.current.add(m.uid);
        room.kickMember(m.uid).finally(() => ghostKickRef.current.delete(m.uid));
      }
    };
    prune();
    const id = setInterval(prune, 5 * 60 * 1000);
    return () => clearInterval(id);
  }, [roomId, myUid, room.state.roomMeta?.hostUid, membersSig]);

  // 네트워크/탭 복귀 — lastActiveAt 즉시 갱신 (RTDB onDisconnect 재등록에 해당)
  useEffectRoom(() => {
    const fb = window.firebaseBridge;
    if (!fb?.subscribeReconnect) return;
    return fb.subscribeReconnect(() => sendHeartbeat(true));
  }, [roomId, myUid, myStatus]);

  // accumulated 날짜 자정 넘어가면 리셋 — 다음 사건성 patch 때 0 으로 세팅
  useEffectRoom(() => {
    if (!me || me.accumulatedDate === today) return;
    if (me.accumulatedDate && me.accumulatedDate !== today) {
      room.patchMyMember({ accumulatedSecondsToday: 0, accumulatedDate: today });
    }
  }, [today, me?.accumulatedDate]);
}

// ===========================================================
// 스타일
// ===========================================================
const codeChip = {
  all: "unset", cursor: "pointer",
  padding: "3px 12px", borderRadius: 99,
  border: "1.1px solid var(--ink)",
  background: "var(--paper)",
  fontFamily: "var(--hand)", fontSize: 11, color: "var(--ink)",
  display: "inline-flex", alignItems: "center", gap: 4,
};
const shareBtn = {
  all: "unset", cursor: "pointer",
  padding: "3px 11px", borderRadius: 99,
  border: "1.1px solid var(--ink)",
  background: "var(--paper)",
  fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink)",
  display: "inline-flex", alignItems: "center", gap: 5,
};
const smallBtn = {
  all: "unset", cursor: "pointer",
  padding: "4px 12px", borderRadius: 99,
  border: "1.1px solid var(--ink)",
  background: "var(--paper)",
  fontFamily: "var(--hand)", fontSize: 12, color: "var(--ink)",
};
function smallBtnPrimaryStyle() {
  return {
    ...smallBtn,
    background: roomFlat() ? "var(--paper-3)" : "linear-gradient(180deg, var(--point-soft), var(--point))",
    fontWeight: 700,
  };
}
function roomChromeBtnStyle(overrides = {}) {
  return {
    all: "unset", cursor: "pointer",
    width: 22, height: 20, borderRadius: roomFlat() ? 3 : 5,
    display: "grid", placeItems: "center",
    border: "1px solid var(--ink)",
    background: roomFlat() ? "var(--paper)" : "color-mix(in srgb, var(--paper) 78%, var(--blue-soft))",
    fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink)",
    ...overrides,
  };
}
const bigBtn = {
  all: "unset", cursor: "pointer",
  display: "block", width: "100%", boxSizing: "border-box", textAlign: "center",
  padding: "12px", borderRadius: 12,
  border: "1.1px solid var(--ink)",
  background: "var(--paper)",
  fontFamily: "var(--hand)", fontSize: 14, fontWeight: 700, color: "var(--ink)",
  boxShadow: "0 2px 0 var(--paper-3)",
};
function bigPrimaryBtnStyle() {
  return {
    ...bigBtn,
    background: roomFlat() ? "var(--paper-3)" : "linear-gradient(180deg, var(--point-soft), var(--point))",
    boxShadow: roomFlat() ? "none" : bigBtn.boxShadow,
  };
}
const inlineForm = {
  display: "flex", flexDirection: "column", gap: 8,
  padding: 12, borderRadius: 12,
  background: "var(--paper-2)",
  border: "1.1px dashed var(--ink-2)",
};
const formInput = {
  border: "1.1px solid var(--ink-soft)", outline: "none",
  background: "var(--paper)", borderRadius: 8,
  fontFamily: "var(--hand)", fontSize: 14, color: "var(--ink)",
  padding: "6px 10px",
};
const avatarBtn = {
  all: "unset", cursor: "pointer",
  width: 36, height: 36, borderRadius: "50%",
  border: "1.1px solid var(--ink)",
  background: "var(--paper-2)",
  display: "grid", placeItems: "center",
  fontSize: 22, flexShrink: 0,
};
const clapBtn = {
  all: "unset", cursor: "pointer",
  padding: "4px 9px", borderRadius: 99,
  border: "1.1px solid var(--ink)",
  background: "var(--paper)",
  fontSize: 14, color: "var(--ink)",
  flexShrink: 0,
};
const miniHeaderBtn = {
  all: "unset", cursor: "pointer",
  width: 18, height: 14, borderRadius: 4,
  background: "var(--paper)",
  border: "1px solid var(--ink)",
  display: "grid", placeItems: "center",
  fontSize: 9, color: "var(--ink)",
  flexShrink: 0,
};

// ---- 애니메이션 CSS 주입 (한 번만) ----
if (!document.getElementById("room-anim-css")) {
  const s = document.createElement("style");
  s.id = "room-anim-css";
  s.textContent = `
    @keyframes room-sparkle-pop {
      0%   { transform: translateY(4px) scale(0.6); opacity: 0; }
      30%  { transform: translateY(-4px) scale(1.1); opacity: 1; }
      80%  { transform: translateY(-8px) scale(1.0); opacity: 1; }
      100% { transform: translateY(-14px) scale(0.9); opacity: 0; }
    }
    @keyframes room-clap-pop {
      0%   { transform: translateY(8px) scale(0.5) rotate(-12deg); opacity: 0; }
      20%  { transform: translateY(-2px) scale(1.2) rotate(0deg); opacity: 1; }
      60%  { transform: translateY(-8px) scale(1.0) rotate(8deg); opacity: 1; }
      100% { transform: translateY(-22px) scale(0.85) rotate(-4deg); opacity: 0; }
    }
    @keyframes room-toast-pop {
      0%   { opacity: 0; transform: translateX(-50%) translateY(8px); }
      15%  { opacity: 1; transform: translateX(-50%) translateY(0); }
      85%  { opacity: 1; transform: translateX(-50%) translateY(0); }
      100% { opacity: 0; transform: translateX(-50%) translateY(4px); }
    }
    @keyframes room-avatar-hop {
      0%, 36%, 100% { transform: translateY(0); }
      37%, 62%      { transform: translateY(-3px); }
      63%, 72%      { transform: translateY(-1px); }
    }
    .room-notice-ticker .marquee-inner.room-notice-marquee-inner {
      display: inline-flex;
      align-items: center;
      white-space: nowrap;
      animation-duration: 18s;
    }
    .room-notice-ticker:hover .room-notice-marquee-inner {
      animation-play-state: paused;
    }
  `;
  document.head.appendChild(s);
}

// ===========================================================
// RoomSyncBridge — 보이지 않는 컴포넌트. App 에서 항상 마운트.
// 본인 → 서버 sync 만 담당하므로, 어느 탭에 있든 todo 토글이 방에 반영됨.
// 부팅도 같이 함 (구독은 RoomView 가 탭 진입 시 붙임).
// ===========================================================
function RoomSyncBridge() {
  const room = window.roomStore.useRoom();
  useEffectRoom(() => { room.bootstrap(); }, []);
  useRoomSync();
  return null;
}

window.RoomView = RoomView;
window.RoomSyncBridge = RoomSyncBridge;
