/* ============================================================
   바른셈 — 고객사 셀프입력 웹 폼 (우리가 만든 웹 채널)
   엑셀 핸드오프 제거: 고객사가 직접 표준 스키마로 입력 → 자동 정규화
   ============================================================ */
function ClientForm({ ctx }) {
  const { clients, go, toast, rejectionOf, resubmitClient, addHrChange, removeHrChange, hrChangesOf, period, statusOf, submitVariation, variationOf } = ctx;
  // 미리보기는 '활성 고객사' 기준. 첫 고객사 고정이면 다른 고객사를 보다 들어와도 도원이 뜬다(E10).
  const client = clients.find((c) => c.id === ctx.params.clientId) || clients[0];
  const ym = (period && period.ym) || "2026-05";
  // E11 — 마감일은 커널이 한 곳에서 파생한다. 고객 화면엔 지급일, 대시보드엔 지급일−5가 뜨던 자리.
  const NEXT = { label: (period && period.label) || "2026년 5월",
    due: window.BARUNSEM.submitDueDate((period && period.payDate) || "2026-06-10", client.dueOffset),
    payDate: (period && period.payDate) || "2026-06-10" };
  const rejection = rejectionOf(client.id);
  const activeReject = rejection && !rejection.resolved;

  // 이미 제출한 달이면 커밋된 값을 그대로 보여준다(제출이 상태를 바꾸므로 되돌아와도 남는다).
  const committed = variationOf(client.id, ym);
  const rowSeed = () => client.employees.map((e) => {
    const v = committed[e.id] || {};
    return { id: e.id, name: e.name, dept: e.dept, title: e.title, scheme: e.scheme,
      overtime: v.overtime || "", night: v.night || "", holiday: v.holiday || "", bonus: v.bonus || "", memo: v.memo || "" };
  });
  const [rows, setRows] = useState(rowSeed);
  useEffect(() => { setRows(rowSeed()); }, [client.id, ym]);
  const [submitted, setSubmitted] = useState(false);
  // C10 — 노무사가 입력을 잠그면 고객 채널도 닫힌다. 전엔 확정된 고객사도 포털에서 제출이 눌렸다.
  const st = statusOf(client.id, ym);
  const portalLocked = ["locked", "calculated", "pending_approval", "confirmed", "distributed"].includes(st);
  const LOCK_MSG = { locked: "노무사 검수가 끝나 입력이 잠겼습니다", calculated: "급여 계산이 실행되어 입력이 잠겼습니다",
    pending_approval: "승인 대기 중이라 입력이 잠겼습니다", confirmed: "급여가 확정되어 입력이 잠겼습니다", distributed: "명세서가 발송되어 입력이 잠겼습니다" };
  const [hrOpen, setHrOpen] = useState(false);
  // §06+ 근태 입력 — 근태 캘린더(기본) / 직접 입력 / 근태 파일 가져오기
  const mode = ctx.attendanceDraftOf(client.id).mode || "calendar";
  const setMode = (m) => ctx.setAttendanceDraft(client.id, { mode: m });
  const [autoFilled, setAutoFilled] = useState(false);
  const hasTimesheets = client.employees.some((e) => e.timesheet);
  // 산출 맵({empId:{overtime,night,holiday,absentDays}})을 변동분 표에 채움 (캘린더·파일 공용)
  const fillFromDerived = (map) => {
    setRows((rs) => rs.map((r) => {
      const d = map[r.id]; if (!d) return r;
      return { ...r, overtime: d.overtime || "", night: d.night || "", holiday: d.holiday || "",
        memo: r.memo || ("근태 자동산출" + (d.absentDays ? ` · 결근 ${d.absentDays}일` : "")) };
    }));
    setMode("manual"); setAutoFilled(true);
    toast("근태에서 변동분을 자동 산출했습니다 — 검수 후 수정 가능", "ok");
  };
  const set = (id, k, v) => setRows((rs) => rs.map((r) => r.id === id ? { ...r, [k]: v } : r));
  const filled = rows.filter((r) => r.overtime || r.night || r.holiday || r.bonus || r.memo).length;
  /* D6 입력 검증 — "1.2.3"이 NaN으로 통과하고 상한이 없어 연장 9999h가 그대로 들어가던 자리.
     한 달 물리적 상한(시간)과 숫자 형식만 막는다. 적정성 판단은 검수 룰이 한다. */
  const HOUR_CAP = { overtime: 200, night: 300, holiday: 200 };
  const numErr = (v, cap) => {
    const t = String(v).trim(); if (!t) return null;
    if (!/^\d+(\.\d+)?$/.test(t)) return "숫자만 입력해 주세요";
    const n = Number(t);
    if (cap != null && n > cap) return `한 달 ${cap}시간을 넘을 수 없습니다`;
    return null;
  };
  const rowErrs = (r) => ({ overtime: numErr(r.overtime, HOUR_CAP.overtime), night: numErr(r.night, HOUR_CAP.night),
    holiday: numErr(r.holiday, HOUR_CAP.holiday), bonus: numErr(r.bonus, null) });
  const errCount = rows.reduce((n, r) => n + Object.values(rowErrs(r)).filter(Boolean).length, 0);
  const hr = hrChangesOf(client.id);

  /* W1-1 제출 = 커밋. 이 한 줄이 빠져 있어서 고객사가 무엇을 적든 급여 결과가 같았다(B1). */
  const doSubmit = () => {
    if (portalLocked) return toast(LOCK_MSG[st] + " — 수정이 필요하면 노무사에게 알려 주세요");
    if (errCount) return toast(`입력 오류 ${errCount}건을 먼저 고쳐 주세요`, "warn");
    const n = submitVariation(client.id, rows, { ym, actorName: client.contact });
    if (activeReject) resubmitClient(client.id);
    setSubmitted(true);
    toast(`변동분 ${n}명분이 노무사에게 전달되었습니다`, "ok");
  };

  if (submitted) {
    return (
      <div style={{ minHeight: "100vh", display: "grid", placeItems: "center", background: "var(--bg)", padding: 24 }}>
        <div className="card card-pad rise" style={{ maxWidth: 460, textAlign: "center", padding: "40px 36px" }}>
          <div style={{ width: 64, height: 64, borderRadius: "50%", background: "var(--ok-soft)", display: "grid", placeItems: "center", margin: "0 auto 18px" }}><Ms name="check_circle" style={{ color: "var(--ok)", fontSize: 38 }} /></div>
          <h2 style={{ fontSize: 21, fontWeight: 750, letterSpacing: "-0.02em" }}>{rejection ? "수정분 재제출 완료" : "변동분 제출 완료"}</h2>
          <p className="muted" style={{ marginTop: 8, lineHeight: 1.6, fontSize: 13.5 }}>{NEXT.label} 귀속 변동분 <b>{filled}건</b>{hr.length ? <> · 인사변동 <b>{hr.length}건</b></> : null}이 바른셈 노무법인으로 전송되었습니다. 표준 스키마로 자동 정규화되어 노무사 검수 대기열에 올라갑니다.</p>
          <div style={{ display: "flex", gap: 9, justifyContent: "center", marginTop: 22 }}>
            <button className="btn btn-default" onClick={() => setSubmitted(false)}><Ms name="edit" />다시 수정</button>
            <button className="btn btn-primary" onClick={() => go("review", { clientId: client.id })}><Ms name="fact_check" />노무사 검수 화면 보기</button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div style={{ minHeight: "100vh", background: "var(--bg)" }}>
      {/* 데모 배너 */}
      <div style={{ background: "var(--ink)", color: "#fff", padding: "8px 20px", display: "flex", alignItems: "center", gap: 10, fontSize: 12.5 }}>
        <Ms name="visibility" style={{ fontSize: 16 }} />
        <span style={{ opacity: .85 }}>고객사 입력화면 미리보기 — 이 화면은 고객사가 별도 웹 채널로 접속하는 모습입니다</span>
        <div style={{ flex: 1 }} />
        <button className="btn-ghost btn btn-sm" style={{ color: "#fff" }} onClick={() => go("dashboard")}><Ms name="arrow_back" />노무사 워크스페이스로</button>
      </div>

      {/* 고객사 포털 헤더 */}
      <div style={{ background: "var(--surface)", borderBottom: "1px solid var(--line)" }}>
        <div className="cf-head" style={{ maxWidth: 1000, margin: "0 auto", padding: "16px 24px", display: "flex", alignItems: "center", gap: 13 }}>
          <ClientAvatar client={client} size={40} />
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700, fontSize: 16 }}>{client.name} <span className="muted" style={{ fontWeight: 500, fontSize: 13 }}>급여 포털</span></div>
            <div className="muted" style={{ fontSize: 12 }}>{client.contact}님 · 한결노동법률사무소 연결됨</div>
          </div>
          <span className="tag tag-accent"><Ms name="link" style={{ fontSize: 13 }} />엑셀 없이 직접 입력</span>
        </div>
      </div>

      <div className="cf-wrap" style={{ maxWidth: 1000, margin: "0 auto", padding: "26px 24px 80px" }}>
        {/* 반려 배너 (노무사 검수 → 고객사 연동) */}
        {activeReject && (
          <div className="reject-banner rise" style={{ marginBottom: 18 }}>
            <Ms name="assignment_return" />
            <div style={{ flex: 1 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <span style={{ fontWeight: 700, color: "var(--danger)" }}>노무사 반려 — 수정 후 재제출 필요</span>
                <span className="pill pill-warn" style={{ height: 21 }}>재제출 대기</span>
              </div>
              <div style={{ fontSize: 13, color: "color-mix(in oklab, var(--danger) 55%, black)", marginTop: 4, lineHeight: 1.5 }}>
                “{rejection.reason}”
              </div>
              <div className="muted" style={{ fontSize: 11.5, marginTop: 4 }}>반려 시각 {rejection.at} · 해당 항목을 수정하고 아래에서 다시 제출해 주세요.</div>
            </div>
          </div>
        )}

        <div className="row-between" style={{ marginBottom: 18 }}>
          <div>
            <div className="page-title">{NEXT.label} 급여 변동분 입력</div>
            <div className="page-desc" style={{ marginTop: 4 }}>이번 달 달라진 부분만 입력해 주세요. 변동 없는 항목은 비워두면 전월과 동일하게 처리됩니다.</div>
          </div>
          <div className="card card-pad" style={{ padding: "10px 16px", textAlign: "center" }}>
            <div className="muted" style={{ fontSize: 11.5 }}>제출 마감</div>
            <div className="num" style={{ fontWeight: 750, fontSize: 16, color: "var(--accent-ink)" }}>{NEXT.due}</div>
            <div className="muted" style={{ fontSize: 10.5, marginTop: 2 }}>지급일 {NEXT.payDate} − {client.dueOffset}일</div>
          </div>
        </div>

        <div className="card card-pad" style={{ display: "flex", gap: 12, marginBottom: 16, background: "var(--accent-soft)", border: "1px solid var(--accent-line)" }}>
          <Ms name="auto_fix_high" style={{ color: "var(--accent)", fontSize: 20, flex: "none" }} />
          <div style={{ fontSize: 12.5, color: "var(--accent-ink)", lineHeight: 1.55 }}>입력하신 값은 <b>표준 스키마로 자동 정규화</b>되어 노무사에게 바로 전달됩니다. 더 이상 엑셀 양식을 맞추거나 메신저로 주고받을 필요가 없습니다.</div>
        </div>

        {/* §06+ 입력 모드 세그먼트 — 근태 캘린더(기본) / 직접 입력 / 파일 가져오기 */}
        <div className="row-between" style={{ marginBottom: 14, alignItems: "center", flexWrap: "wrap", gap: 8 }}>
          <div className="seg" style={{ height: 36 }}>
            <button className={mode === "calendar" ? "on" : ""} onClick={() => setMode("calendar")}><Ms name="calendar_month" style={{ fontSize: 15, verticalAlign: "-3px" }} /> 근태 캘린더</button>
            <button className={mode === "manual" ? "on" : ""} onClick={() => setMode("manual")}><Ms name="edit" style={{ fontSize: 15, verticalAlign: "-3px" }} /> 직접 입력</button>
            {<button className={mode === "import" ? "on" : ""} onClick={() => setMode("import")}><Ms name="upload_file" style={{ fontSize: 15, verticalAlign: "-3px" }} /> 파일 가져오기</button>}
          </div>
          {autoFilled && mode === "manual" && <span className="tag tag-accent"><Ms name="auto_awesome" style={{ fontSize: 13 }} />근태에서 자동 채움 — 수정 가능</span>}
        </div>

        {mode === "calendar" ? <ShiftCalendar client={client} ym={ym} onApply={fillFromDerived} toast={toast} />
          : mode === "import" ? <AttendanceImport ctx={ctx} client={client} ym={ym} toast={toast} onApply={fillFromDerived}
              onSaveMapping={(m) => { ctx.saveAttendanceMap(client.id, m); toast("매핑 프로파일을 저장했습니다 — 다음 달부터 자동 적용", "ok"); }} />
          : (<>
        <div className="card">
          <CardHead icon="edit_calendar" title="근로자별 변동분" sub={`${client.headcount}명 중 ${rows.length}명 표시`} />
          <div style={{ overflowX: "auto" }}>
            <table className="tbl tbl-cards" style={{ minWidth: 760 }}>
              <thead><tr><th>근로자</th><th className="r" style={{ width: 110 }}>연장근로(h)</th><th className="r" style={{ width: 110 }}>야간근로(h)</th><th className="r" style={{ width: 110 }}>휴일근로(h)</th><th className="r" style={{ width: 130 }}>상여/기타지급</th><th style={{ width: 170 }}>메모</th></tr></thead>
              <tbody>
                {rows.map((r) => {
                  const er = rowErrs(r);
                  const cell = (k, ph) => (
                    <td data-label={ph}>
                      <input className="input num" style={{ height: 34, borderColor: er[k] ? "var(--danger)" : undefined }} placeholder="0" disabled={portalLocked}
                        aria-invalid={er[k] ? "true" : undefined} aria-label={`${r.name} ${ph}`}
                        value={r[k]} onChange={(e) => set(r.id, k, e.target.value.replace(/[^\d.]/g, ""))} />
                      {er[k] && <div style={{ fontSize: 10.5, color: "var(--danger)", marginTop: 3 }}>{er[k]}</div>}
                    </td>
                  );
                  return (
                  <tr key={r.id}>
                    <td className="tc-name"><div style={{ fontWeight: 650 }}>{r.name}</div><div className="muted" style={{ fontSize: 11.5 }}>{r.dept} · {r.scheme}</div></td>
                    {cell("overtime", "연장근로(h)")}
                    {cell("night", "야간근로(h)")}
                    {cell("holiday", "휴일근로(h)")}
                    {cell("bonus", "상여/기타지급")}
                    <td data-label="메모"><input className="input" style={{ height: 34 }} placeholder="예: 5/15 경조사" disabled={portalLocked} aria-label={`${r.name} 메모`} value={r.memo} onChange={(e) => set(r.id, "memo", e.target.value)} /></td>
                  </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
        </>)}

        {/* 인사변동 */}
        <div className="card" style={{ marginTop: 16 }}>
          <CardHead icon="manage_accounts" title="입·퇴사 등 인사변동" sub="신규 입사·퇴사 등록 → 4대보험 자격취득·상실 자동 연동"
            right={<button className="btn btn-default btn-sm" onClick={() => setHrOpen(true)}><Ms name="person_add" />인사변동 등록</button>} />
          {hr.length === 0
            ? <div style={{ padding: "18px 22px", display: "flex", alignItems: "center", gap: 10 }}><Ms name="info" style={{ color: "var(--ink-4)", fontSize: 18 }} /><span className="muted" style={{ fontSize: 12.5 }}>이번 달 입·퇴사가 있으면 등록해 주세요. 노무사 검수화면·신고용 서식에 바로 반영됩니다.</span></div>
            : (
              <table className="tbl tbl-cards">
                <thead><tr><th style={{ width: 80 }}>구분</th><th>근로자</th><th>부서·직급</th><th>일자</th><th>비고</th><th style={{ width: 40 }}></th></tr></thead>
                <tbody>
                  {hr.map((x) => (
                    <tr key={x.id}>
                      <td data-label="구분">{x.kind === "join"
                        ? <span className="pill pill-ok" style={{ height: 21 }}><Ms name="login" style={{ fontSize: 13 }} />입사</span>
                        : <span className="pill pill-warn" style={{ height: 21 }}><Ms name="logout" style={{ fontSize: 13 }} />퇴사</span>}</td>
                      <td data-label="근로자" style={{ fontWeight: 650 }}>{x.name}</td>
                      <td className="muted" data-label="부서·직급" style={{ fontSize: 12.5 }}>{x.dept}{x.title ? " · " + x.title : ""}</td>
                      <td className="num" data-label="일자">{x.date}</td>
                      <td className="muted" data-label="비고" style={{ fontSize: 12.5 }}>{x.kind === "join" ? `${x.scheme || ""} ${x.hourly ? "시급 " + KRW(x.hourly) + "원" : x.base ? "월 " + KRW(x.base) + "원" : ""}` : (x.reason || "—")}</td>
                      <td className="tc-action" data-label="삭제"><button className="iconbtn" aria-label={`${x.name} 인사변동 삭제`} title="인사변동 삭제" style={{ width: 30, height: 30 }} onClick={() => removeHrChange(client.id, x.id)}><Ms name="close" style={{ fontSize: 16 }} /></button></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
        </div>

        {/* 제출 바 */}
        <div className="card card-pad action-bar" style={{ marginTop: 16, display: "flex", alignItems: "center", gap: 14, position: "sticky", bottom: 16, boxShadow: "var(--sh-2)" }}>
          <div style={{ flex: 1, display: "flex", alignItems: "center", gap: 10 }}>
            <Ms name="cloud_upload" style={{ color: "var(--accent)", fontSize: 24 }} />
            <div><div style={{ fontWeight: 650 }}>{filled}명 변동분 입력됨{errCount ? <span style={{ color: "var(--danger)", marginLeft: 8 }}>· 오류 {errCount}건</span> : null}</div>
              <div className="muted" style={{ fontSize: 12 }}>{portalLocked ? LOCK_MSG[st] : "제출하면 노무사 검수가 시작됩니다."}</div></div>
          </div>
          <button className="btn btn-default" disabled={portalLocked} onClick={() => toast("임시저장했습니다")}><Ms name="save" />임시저장</button>
          <button className="btn btn-primary btn-lg" disabled={portalLocked || errCount > 0} onClick={doSubmit}><Ms name="send" />{activeReject ? "수정분 재제출" : "노무사에게 제출"}</button>
        </div>
      </div>

      {hrOpen && <HRForm onClose={() => setHrOpen(false)} onSave={(c) => { addHrChange(client.id, c); setHrOpen(false); toast(`인사변동(${c.kind === "join" ? "입사" : "퇴사"} · ${c.name}) 등록됨`, "ok"); }} />}
    </div>
  );
}
/* 인사변동 등록 폼 (모달) */
function HRForm({ onClose, onSave }) {
  const [kind, setKind] = useState("join");
  const [f, setF] = useState({ name: "", dept: "", title: "", scheme: "월급제", date: "2026-05-", base: "", reason: "자진 퇴사" });
  const upd = (k, v) => setF((p) => ({ ...p, [k]: v }));
  // D6 — date.length >= 8 이면 기본값 "2026-05-"(8자)가 그대로 통과해 일자 없는 인사변동이 등록됐다.
  const dateOk = /^\d{4}-\d{2}-\d{2}$/.test(f.date) && !isNaN(new Date(f.date + "T00:00:00").getTime());
  const baseOk = kind === "leave" || !f.base || Number(String(f.base).replace(/[^\d]/g, "")) > 0;
  const valid = f.name.trim() && f.dept.trim() && dateOk && baseOk;
  const dateErr = f.date && !dateOk ? "YYYY-MM-DD 형식으로 입력해 주세요 (예: 2026-05-12)" : null;

  const save = () => {
    if (!valid) return;
    onSave(kind === "join"
      ? (() => { const amt = Number(String(f.base).replace(/[^\d]/g, "")) || undefined;
          const base = { kind, name: f.name.trim(), dept: f.dept.trim(), title: f.title.trim(), scheme: f.scheme, date: f.date };
          return f.scheme === "시급제" ? { ...base, hourly: amt } : { ...base, base: amt }; })()
      : { kind, name: f.name.trim(), dept: f.dept.trim(), title: f.title.trim(), date: f.date, reason: f.reason });
  };

  return (
    <Modal title="인사변동 등록" onClose={onClose} width={520}
      footer={<><button className="btn btn-default" onClick={onClose}>취소</button><button className="btn btn-primary" disabled={!valid} onClick={save}><Ms name="check" />등록</button></>}>
      <div className="seg" style={{ marginBottom: 16 }}>
        <button className={kind === "join" ? "on" : ""} onClick={() => setKind("join")}>입사자 등록</button>
        <button className={kind === "leave" ? "on" : ""} onClick={() => setKind("leave")}>퇴사자 등록</button>
      </div>
      <div className="form-2">
        <div className="field"><label>성명</label><input className="input" value={f.name} onChange={(e) => upd("name", e.target.value)} placeholder="예: 김태리" /></div>
        <div className="field"><label>부서</label><input className="input" value={f.dept} onChange={(e) => upd("dept", e.target.value)} placeholder="예: 강남점" /></div>
        <div className="field"><label>직급/직책</label><input className="input" value={f.title} onChange={(e) => upd("title", e.target.value)} placeholder="예: 홀 서비스" /></div>
        <div className="field"><label>{kind === "join" ? "입사일" : "퇴사일"}</label>
          <input className="input num" value={f.date} onChange={(e) => upd("date", e.target.value)} placeholder="2026-05-12" aria-invalid={dateErr ? "true" : undefined} style={dateErr ? { borderColor: "var(--danger)" } : null} />
          {dateErr && <div style={{ fontSize: 11, color: "var(--danger)", marginTop: 4 }}>{dateErr}</div>}</div>
        {kind === "join" ? (<>
          <div className="field"><label>급여체계</label>
            <select className="select" value={f.scheme} onChange={(e) => upd("scheme", e.target.value)}>
              {["월급제", "시급제", "연봉제", "일급제"].map((s) => <option key={s} value={s}>{s}</option>)}
            </select>
          </div>
          <div className="field"><label>{f.scheme === "시급제" ? "시급" : "급여조건 (월/환산)"}</label><div className="input-affix"><input className="input num" value={f.base} onChange={(e) => upd("base", e.target.value.replace(/[^\d]/g, ""))} placeholder={f.scheme === "시급제" ? "10,320" : "1,700,000"} /><span className="suffix">원</span></div></div>
        </>) : (
          <div className="field" style={{ gridColumn: "1 / -1" }}><label>상실 사유</label>
            <select className="select" value={f.reason} onChange={(e) => upd("reason", e.target.value)}>
              {["자진 퇴사", "계약기간 만료", "권고사직", "정년", "기타"].map((s) => <option key={s} value={s}>{s}</option>)}
            </select>
          </div>
        )}
      </div>
      <div className="card card-pad" style={{ marginTop: 16, display: "flex", gap: 11, background: "var(--accent-soft)", border: "1px solid var(--accent-line)", padding: "12px 14px" }}>
        <Ms name="sync_alt" style={{ color: "var(--accent)", fontSize: 18, flex: "none" }} />
        <div style={{ fontSize: 12, color: "var(--accent-ink)", lineHeight: 1.55 }}>
          등록 시 노무사 검수화면의 <b>인사변동</b> 섹션과 <b>4대보험 {kind === "join" ? "자격취득" : "자격상실"} 신고서</b> 건수에 자동 반영됩니다.
        </div>
      </div>
    </Modal>
  );
}

window.ClientForm = ClientForm;

/* ============================================================
   §06 근태 파일 가져오기 — raw(출퇴근) → deriveVariation 자동 산출 미리보기
   (프로토타입: 시드 timesheet로 흐름 시연. 실제 파일 파서는 어댑터 — 매핑은 client.attendanceMap)
   ============================================================ */
function AttendanceImport({ ctx, client, onApply, ym, onSaveMapping, toast }) {
  const B = window.BARUNSEM;
  // E13 — 작업 중 상태(매핑·정책·업로드본)는 ctx에 둔다. 컴포넌트 state면 탭 전환 한 번에 사라진다.
  const draft = ctx.attendanceDraftOf(client.id);
  const setDraft = (patch) => ctx.setAttendanceDraft(client.id, patch);
  const loaded = !!draft.loaded;
  const setLoaded = (v) => setDraft({ loaded: v });
  // W1-3 실제 파일 파싱 — 업로드된 CSV가 있으면 시드 대신 이걸 원본으로 쓴다.
  const upload = draft.upload || null;             // { fileName, byEmp, rejected, headers }
  const setUpload = (v) => setDraft({ upload: v });
  const [parseErr, setParseErr] = useState(null);
  const fileRef = useRef(null);
  const am = client.attendanceMap || {};
  const cols0 = am.columns || {};
  const sampleEmp = client.employees.find((e) => e.timesheet);
  const src = sampleEmp && sampleEmp.timesheetSource;
  // 업로드본이 있으면 그것이 원본. 사번 우선, 없으면 성명으로 근로자에 붙인다.
  const uploadTs = useMemo(() => {
    if (!upload) return null;
    const out = {};
    client.employees.forEach((e) => { const t = upload.byEmp[e.id] || upload.byEmp[e.name]; if (t) out[e.id] = t; });
    return out;
  }, [upload, client.id]);
  const derivedEmps = uploadTs ? client.employees.filter((e) => uploadTs[e.id]) : client.employees.filter((e) => e.timesheet);
  const rawOf = (e) => (uploadTs ? uploadTs[e.id] : e.timesheetRaw || e.timesheet);
  const WEEK_CAP = (B.RULESET.weekOtCap != null ? B.RULESET.weekOtCap : 12);   // D4 — 룰셋 단일 진실

  const FIELD_LABEL = { date: "근무일", in: "출근시각", out: "퇴근시각", breakMin: "휴게(분)", type: "근무구분" };
  const typeKo = (t) => ({ work: "근무", holiday: "휴일", off: "휴무" }[t] || t);

  // §09 — 고객사별 매핑·정책을 '실제로' 다루는 상태. 변경 시 deriveVariation 재산출(라이브).
  // E13 — 이 상태들은 모드를 바꾸면 컴포넌트가 언마운트되며 사라졌다. ctx에 올려 화면 전환에 살아남게 한다.
  const mapping = draft.mapping || { date: cols0.date || "근무일", in: cols0.in || "출근", out: cols0.out || "퇴근", breakMin: cols0.breakMin || "휴게(분)", type: cols0.type || "구분" };
  const setMapping = (fn) => setDraft({ mapping: typeof fn === "function" ? fn(mapping) : fn });
  const RAW_POOL = ["근무일", "날짜", "일자", "출근", "출근시각", "IN", "퇴근", "퇴근시각", "OUT", "휴게(분)", "휴게시간", "BREAK", "구분", "근무구분", "상태"];
  const brk0 = am.breakRule || { per4h: 30, per8h: 60 }, rnd0 = am.rounding || { unit: 1, mode: "none" };
  const policy = draft.policy || { per4h: brk0.per4h, per8h: brk0.per8h, nightStart: "22:00", nightEnd: "06:00", roundUnit: rnd0.unit || 1, roundMode: rnd0.mode || "none" };
  const setP = (k, v) => setDraft({ policy: { ...policy, [k]: v }, saved: false });
  const saved = !!draft.saved;
  const setSaved = (v) => setDraft({ saved: v });

  const liveRb = useMemo(() => ({
    weekStart: am.weekStart || "mon", nightStart: policy.nightStart, nightEnd: policy.nightEnd,
    breakRule: { per4h: Number(policy.per4h) || 0, per8h: Number(policy.per8h) || 0 },
    rounding: { unit: Number(policy.roundUnit) || 1, mode: policy.roundMode },
  }), [policy, am.weekStart]);

  // 정책 바뀌면 라이브 재산출 — "정책을 바꾸면 숫자가 바뀐다"를 눈으로 보여줌
  // B4 — 원본 하나에서 정제본을 만들고, 그 정제본으로 산출한다. 별개 객체 2개면 "깨진 행 제외"가 거짓말이 된다.
  const cleaned = useMemo(() => Object.fromEntries(derivedEmps.map((e) => [e.id, B.cleanTimesheet(rawOf(e), liveRb)])), [liveRb, client.id, upload]);
  const derivedLive = useMemo(() => Object.fromEntries(derivedEmps.map((e) => [e.id, B.deriveVariation(cleaned[e.id], e, liveRb)])), [cleaned]);
  // 깨진 근태 원본(raw) 검출 — 같은 원본을 auditTimesheet로 스캔
  const rawIssues = useMemo(() => derivedEmps.map((e) => ({ e, at: cleaned[e.id].audit })).filter((x) => x.at.count > 0), [cleaned]);
  const rawIssueCount = rawIssues.reduce((s, x) => s + x.at.count, 0);

  const cols = mapping;
  // 업로드본이 있으면 그 사람의 원본을 보여준다 — 시드를 보여주면 검수자가 남의 근태를 근거로 본다.
  const previewEmp = derivedEmps[0] || sampleEmp || client.employees[0];
  const sum = (sel) => derivedEmps.reduce((s, e) => s + (sel(derivedLive[e.id]) || 0), 0);
  const overEmps = derivedEmps.filter((e) => (derivedLive[e.id].weeks || []).some((w) => w.wkOt > WEEK_CAP));
  const absentEmps = derivedEmps.filter((e) => derivedLive[e.id].absentDays > 0);
  const tardyEmps = derivedEmps.filter((e) => (derivedLive[e.id].tardyMin || 0) > 0 || (derivedLive[e.id].earlyMin || 0) > 0);
  const applyLive = () => onApply(Object.fromEntries(derivedEmps.map((e) => { const d = derivedLive[e.id]; return [e.id, { overtime: d.overtime, night: d.night, holiday: d.holiday + d.holidayOver, absentDays: d.absentDays }]; })));
  const onFile = async (file) => {
    if (!file) return;
    setParseErr(null);
    const res = await B.parseTimesheetFile(file, { ...mapping, emp: "사번", name: "성명" }, { ym, typeMap: (client.attendanceMap || {}).typeMap });
    if (!res.ok) { setParseErr(res.error); setUpload(null); return; }
    setUpload(res); setLoaded(true);
    if (toast) toast(`${res.fileName} — ${res.empCount}명 · ${res.rows.length}행 파싱 완료`, "ok");
  };

  const ANOM_LABEL = window.BARUNSEM.ANOM_LABEL;

  if (!loaded) {
    return (
      <div className="card card-pad" style={{ padding: "30px 26px" }}>
        <div style={{ border: "2px dashed var(--accent-line)", borderRadius: 14, background: "var(--accent-soft)", padding: "38px 24px", textAlign: "center" }}>
          <div style={{ width: 60, height: 60, borderRadius: 16, background: "var(--surface)", display: "grid", placeItems: "center", margin: "0 auto 16px", boxShadow: "var(--sh-1)" }}>
            <Ms name="table_view" style={{ fontSize: 30, color: "var(--accent)" }} />
          </div>
          <div style={{ fontWeight: 700, fontSize: 16, letterSpacing: "-0.02em" }}>근태 파일을 끌어다 놓거나 선택하세요</div>
          <div className="muted" style={{ fontSize: 12.5, marginTop: 6, lineHeight: 1.55, maxWidth: 460, margin: "6px auto 0" }}>
            엑셀·CSV 일별 출퇴근 기록을 올리면 <b>연장·야간·휴일·결근</b>이 근로기준법 기준으로 자동 산출됩니다. 손으로 시간을 적을 필요가 없습니다.
          </div>
          <input ref={fileRef} type="file" accept=".csv,text/csv" style={{ display: "none" }}
            onChange={(ev) => { onFile(ev.target.files && ev.target.files[0]); ev.target.value = ""; }} />
          <div style={{ display: "flex", gap: 9, justifyContent: "center", marginTop: 20, flexWrap: "wrap" }}>
            <button className="btn btn-primary btn-lg" onClick={() => fileRef.current && fileRef.current.click()}><Ms name="upload_file" />근태 파일 선택 (.csv)</button>
            <button className="btn btn-default btn-lg" onClick={() => setLoaded(true)}><Ms name="science" />예시 근태로 보기</button>
          </div>
          {parseErr && <div className="pill pill-danger" style={{ marginTop: 14, height: "auto", padding: "8px 12px", whiteSpace: "normal" }}><Ms name="error" style={{ fontSize: 15 }} />{parseErr}</div>}
          <div className="muted" style={{ fontSize: 11, marginTop: 12 }}>지원: .csv (엑셀은 CSV로 저장) · 근태기 API 연동은 후속 · 매핑 프로파일은 고객사당 1회 설정</div>
        </div>
      </div>
    );
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      {/* 원본 메타 */}
      <div className="card card-pad" style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <div style={{ width: 38, height: 38, borderRadius: 9, background: "var(--ok-soft)", display: "grid", placeItems: "center", flex: "none" }}><Ms name="description" style={{ color: "var(--ok)", fontSize: 20 }} /></div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 650, fontSize: 13.5 }}>{upload ? upload.fileName : (src ? src.fileName : "근태_" + ym + ".csv")}</div>
          <div className="muted" style={{ fontSize: 11.5 }}>
            {derivedEmps.length}명 · {upload ? upload.rows.length : (src ? src.rawRowCount : 0)}행
            {upload ? (upload.rejected.length ? ` · 읽지 못한 행 ${upload.rejected.length}` : " · 전 행 인식") : (src ? " · 불러옴 " + src.importedAt : "")}
          </div>
        </div>
        <span className={upload ? "tag tag-ok" : "tag"}><Ms name={upload ? "check" : "science"} style={{ fontSize: 13 }} />{upload ? "업로드 파일 파싱 완료" : "예시 근태"}</span>
        <input ref={fileRef} type="file" accept=".csv,text/csv" style={{ display: "none" }}
          onChange={(ev) => { onFile(ev.target.files && ev.target.files[0]); ev.target.value = ""; }} />
        <button className="btn btn-default btn-sm" onClick={() => fileRef.current && fileRef.current.click()}><Ms name="upload_file" />다른 파일</button>
      </div>

      {/* 컬럼 매핑 — 편집 가능(양식 N종 흡수) */}
      <div className="card">
        <CardHead icon="swap_horiz" title="컬럼 매핑" sub="근태기 컬럼 → 표준 필드 · 사업장마다 헤더가 다름 · 1회 설정 후 자동 적용"
          right={saved ? <span className="tag tag-ok"><Ms name="check" style={{ fontSize: 13 }} />프로파일 저장됨</span>
            : <button className="btn btn-default btn-sm" onClick={() => { onSaveMapping && onSaveMapping({ columns: mapping, breakRule: { per4h: Number(policy.per4h) || 0, per8h: Number(policy.per8h) || 0 }, rounding: { unit: Number(policy.roundUnit) || 1, mode: policy.roundMode } }); setSaved(true); }}><Ms name="bookmark" />매핑 프로파일 저장</button>} />
        <div className="card-pad" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(178px, 1fr))", gap: 9 }}>
          {Object.keys(FIELD_LABEL).map((k) => (
            <div key={k} style={{ display: "flex", alignItems: "center", gap: 7, padding: "8px 11px", background: "var(--bg-sunken)", borderRadius: 9, border: "1px solid var(--line)" }}>
              <select className="select" value={mapping[k]} onChange={(e) => { setMapping({ ...mapping, [k]: e.target.value }); setSaved(false); }}
                style={{ height: 30, padding: "0 6px", fontSize: 12, flex: 1, minWidth: 0, fontWeight: 650 }}>
                {[mapping[k], ...RAW_POOL.filter((c) => c !== mapping[k])].map((c) => <option key={c} value={c}>{c}</option>)}
              </select>
              <Ms name="arrow_forward" style={{ fontSize: 14, color: "var(--accent)", flex: "none" }} />
              <span style={{ fontSize: 11.5, color: "var(--ink-3)", flex: "none" }}>{FIELD_LABEL[k]}</span>
            </div>
          ))}
        </div>
      </div>

      {/* 사업장 근태 정책 — 휴게·야간구간·라운딩(고객사·계약 레이어). 바꾸면 산출이 즉시 달라짐 */}
      <div className="card">
        <CardHead icon="tune" title="사업장 근태 정책" sub="휴게 자동공제·야간 구간·분 라운딩 — 고객사별 설정 · 변경 시 아래 산출 즉시 반영"
          right={<span className="tag tag-accent"><Ms name="bolt" style={{ fontSize: 13 }} />법규 디폴트에 사업장 예외 머지</span>} />
        <div className="card-pad" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(168px, 1fr))", gap: 12 }}>
          <PolField label="휴게 공제 — 4h 이상" hint="breakMin 누락 시 추정">
            <div className="input-affix"><input className="input num" value={policy.per4h} onChange={(e) => setP("per4h", e.target.value.replace(/[^\d]/g, ""))} style={{ height: 32 }} /><span className="suffix">분</span></div>
          </PolField>
          <PolField label="휴게 공제 — 8h 이상" hint="근기법 §54 최소 60분">
            <div className="input-affix"><input className="input num" value={policy.per8h} onChange={(e) => setP("per8h", e.target.value.replace(/[^\d]/g, ""))} style={{ height: 32 }} /><span className="suffix">분</span></div>
          </PolField>
          {/* A18 — 야간 시간대는 근기법 §56③ 고정. 편집칸을 열어두면 바꿔도 무시되는 죽은 컨트롤이 된다. */}
          <PolField label="야간 구간" hint="근기법 §56③ — 법규 고정 (편집 불가)">
            <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
              <input className="input num" value="22:00" disabled readOnly style={{ height: 32, textAlign: "center" }} />
              <span className="muted">–</span>
              <input className="input num" value="06:00" disabled readOnly style={{ height: 32, textAlign: "center" }} />
              <Ms name="lock" style={{ fontSize: 15, color: "var(--ink-4)" }} />
            </div>
          </PolField>
          <PolField label="분 라운딩 단위" hint="타각 분 단위 정리">
            <select className="select" value={policy.roundUnit} onChange={(e) => setP("roundUnit", Number(e.target.value))} style={{ height: 32 }}>
              {[1, 5, 10, 15, 30, 60].map((u) => <option key={u} value={u}>{u === 1 ? "1분 (원분)" : u + "분"}</option>)}
            </select>
          </PolField>
          <PolField label="라운딩 방식" hint="절사는 임금체불 소지 — 선택 불가(A18)">
            <div className="seg" style={{ height: 32 }}>
              {/* 절사는 근로시간을 사업장이 깎는 수단이라 선택지에서 뺀다 — 커널도 무시한다(A18) */}
              {[["none", "안 함"], ["round", "반올림"], ["ceil", "절상"]].map(([v, l]) => (
                <button key={v} className={policy.roundMode === v ? "on" : ""} onClick={() => setP("roundMode", v)} style={{ fontSize: 11.5, padding: "0 8px" }}>{l}</button>
              ))}
            </div>
          </PolField>
        </div>
        <div className="card-pad" style={{ paddingTop: 0, display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
          <Ms name="info" style={{ fontSize: 15, color: "var(--ink-4)" }} />
          <span className="muted" style={{ fontSize: 11.5 }}>현재 적용: 휴게 4h≥{policy.per4h}분·8h≥{policy.per8h}분 · 야간 {policy.nightStart}–{policy.nightEnd} · 라운딩 {policy.roundMode === "none" || policy.roundMode === "floor" ? "원분 유지" : policy.roundUnit + "분 " + ({ round: "반올림", ceil: "절상" }[policy.roundMode])}</span>
          <span className="tag" style={{ height: 19 }}><Ms name="gavel" style={{ fontSize: 12 }} />야간 22–06·절사 금지는 법규 고정 — 사업장이 바꿀 수 없습니다</span>
        </div>
      </div>

      {/* 깨진 근태 원본 방어 — 정제 전 raw의 빈 셀·역전·중복·휴게과다를 검수 플래그로 흡수 */}
      {rawIssues.length > 0 && (
        <div className="card" style={{ borderColor: "var(--danger-line)" }}>
          <CardHead icon="report" title="이상 근태 — 깨진 원본 데이터" sub="정제 전 raw에서 검출 · 급여로 흘러가기 전에 잡아냄"
            right={<span className="tag tag-danger"><Ms name="error" style={{ fontSize: 13 }} />{rawIssueCount}건 · {rawIssues.length}명</span>} />
          <div style={{ padding: "4px 22px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
            {rawIssues.map(({ e, at }) => (
              <div key={e.id} style={{ border: "1px solid var(--line)", borderRadius: 10, overflow: "hidden" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 12px", background: "var(--danger-soft)" }}>
                  <Ms name="person" style={{ fontSize: 16, color: "var(--danger)" }} />
                  <b style={{ fontSize: 12.5 }}>{e.name}</b><span className="muted" style={{ fontSize: 11.5 }}>{e.dept}</span>
                  <div style={{ flex: 1 }} />
                  {at.errors > 0 && <span className="tag tag-danger" style={{ height: 19 }}>정정 필요 {at.errors}</span>}
                  {at.warns > 0 && <span className="tag tag-warn" style={{ height: 19 }}>확인 {at.warns}</span>}
                </div>
                <table className="tbl tbl-cards" style={{ fontSize: 12 }}>
                  <thead><tr><th style={{ width: 70 }}>날짜</th><th style={{ width: 96 }}>유형</th><th>내용</th></tr></thead>
                  <tbody>
                    {at.anomalies.map((a, i) => (
                      <tr key={i}>
                        <td className="num" data-label="날짜">{a.date.slice(5)}</td>
                        <td data-label="유형"><span className={"tag " + (a.severity === "error" ? "tag-danger" : "tag-warn")} style={{ height: 19 }}>{ANOM_LABEL[a.code] || a.code}</span></td>
                        <td className="muted" data-label="내용" style={{ fontSize: 11.5 }}>{a.message}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            ))}
            <div style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 11.5, color: "var(--ink-3)" }}>
              <Ms name="shield" style={{ fontSize: 15, color: "var(--ok)" }} />깨진 행은 산출에서 제외되고 노무사 검수 화면의 <b style={{ margin: "0 3px" }}>근태 원본 이상</b> 플래그로 올라갑니다 — 손으로 근태표를 훑지 않아도 됩니다.
            </div>
          </div>
        </div>
      )}

      {/* raw 미리보기 (샘플 근로자) */}
      {sampleEmp && (
        <div className="card">
          <CardHead icon="grid_on" title="원본 미리보기" sub={`${previewEmp.name} · ${previewEmp.dept} — 일별 출퇴근 raw${upload ? " (업로드 파일)" : " (예시 근태)"}`} />
          <div style={{ overflowX: "auto" }}>
            <table className="tbl tbl-cards" style={{ fontSize: 12.5, minWidth: 560 }}>
              <thead><tr><th>{cols.date || "근무일"}</th><th>{cols.in || "출근"}</th><th>{cols.out || "퇴근"}</th><th className="r">{cols.breakMin || "휴게(분)"}</th><th>{cols.type || "구분"}</th></tr></thead>
              <tbody>
                {((rawOf(previewEmp) || {}).days || []).filter((d) => d.type !== "off").slice(0, 8).map((d, i) => (
                  <tr key={i}>
                    <td className="num" data-label="근무일">{d.date.slice(5)}</td>
                    <td className="num" data-label="출근">{d.in || "—"}</td>
                    <td className="num" data-label="퇴근">{d.out || <span style={{ color: "var(--danger)" }}>미기록</span>}</td>
                    <td className="r num" data-label="휴게">{d.breakMin != null ? d.breakMin : "—"}</td>
                    <td data-label="구분">{d.type === "holiday" ? <span className="tag tag-warn" style={{ height: 19 }}>휴일</span> : !d.in ? <span className="tag tag-danger" style={{ height: 19 }}>결근</span> : <span className="tag" style={{ height: 19 }}>근무</span>}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {/* 산출 결과 */}
      <div className="card">
        <CardHead icon="auto_awesome" title="변동분 자동 산출" sub="근로기준법 기준 — 1일 8h 초과=연장 · 22–06시=야간 · 휴게 차감 · 소정일 미출근=결근 · 소정시각 대비 지각·조퇴"
          right={<span className="tag tag-accent"><Ms name="bolt" style={{ fontSize: 13 }} />검수 후 확정 (자동확정 아님)</span>} />
        <div style={{ padding: "10px 22px 0", display: "flex", gap: 9, flexWrap: "wrap" }}>
          <MiniStat label="연장 합계" value={Math.round(sum((d) => d.overtime) * 10) / 10 + "h"} />
          <MiniStat label="야간 합계" value={Math.round(sum((d) => d.night) * 10) / 10 + "h"} />
          <MiniStat label="휴일 합계" value={Math.round(sum((d) => (d.holiday + d.holidayOver)) * 10) / 10 + "h"} />
          {overEmps.length > 0 && <MiniStat label="주52 초과 의심" value={overEmps.length + "명"} danger />}
          {absentEmps.length > 0 && <MiniStat label="결근 발생" value={absentEmps.length + "명"} warn />}
          {tardyEmps.length > 0 && <MiniStat label="지각·조퇴" value={tardyEmps.length + "명"} warn />}
        </div>
        <div style={{ overflowX: "auto", marginTop: 8 }}>
          <table className="tbl tbl-cards" style={{ minWidth: 700 }}>
            <thead><tr><th>근로자</th><th className="r">연장(h)</th><th className="r">야간(h)</th><th className="r">휴일(h)</th><th className="r">결근</th><th className="r">지각·조퇴</th><th>주별 연장 (주52 정밀)</th></tr></thead>
            <tbody>
              {derivedEmps.map((e) => {
                const d = derivedLive[e.id];
                return (
                  <tr key={e.id}>
                    <td className="tc-name"><div style={{ fontWeight: 650 }}>{e.name}</div><div className="muted" style={{ fontSize: 11.5 }}>{e.dept}</div></td>
                    <td className="r num" data-label="연장">{d.overtime || <span className="muted">0</span>}</td>
                    <td className="r num" data-label="야간">{d.night || <span className="muted">0</span>}</td>
                    <td className="r num" data-label="휴일">{(d.holiday + d.holidayOver) || <span className="muted">0</span>}</td>
                    <td className="r num" data-label="결근">{d.absentDays ? <span style={{ color: "var(--danger)", fontWeight: 700 }}>{d.absentDays}일</span> : <span className="muted">—</span>}</td>
                    <td className="r num" data-label="지각·조퇴">{(d.tardyMin || d.earlyMin) ? <span style={{ color: "color-mix(in oklab, var(--warn) 60%, black)", fontWeight: 650 }}>{(d.tardyMin || 0) + (d.earlyMin || 0)}분</span> : <span className="muted">—</span>}</td>
                    <td data-label="주별 연장">
                      <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
                        {(d.weeks || []).map((w) => (
                          <span key={w.wk} className={"tag" + (w.wkOt > WEEK_CAP ? " tag-danger" : "")} style={{ height: 19, fontSize: 10.5 }} title={w.wk + " 주 · 근로 " + w.wkHours + "h"}>
                            {w.wkOt > WEEK_CAP && <Ms name="warning" style={{ fontSize: 11 }} />}{w.wkOt}h
                          </span>
                        ))}
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>

      <div className="card card-pad" style={{ display: "flex", alignItems: "center", gap: 14, position: "sticky", bottom: 16, boxShadow: "var(--sh-2)" }}>
        <Ms name="auto_fix_high" style={{ color: "var(--accent)", fontSize: 24 }} />
        <div style={{ flex: 1 }}><div style={{ fontWeight: 650 }}>{derivedEmps.length}명 변동분 산출됨</div><div className="muted" style={{ fontSize: 12 }}>산출값을 변동분 표에 채웁니다. 노무사·고객사가 검수 화면에서 수정·확정합니다.</div></div>
        <button className="btn btn-default" onClick={() => setLoaded(false)}><Ms name="restart_alt" />다시 불러오기</button>
        <button className="btn btn-primary btn-lg" onClick={applyLive}><Ms name="check" />변동분 표에 자동 채움</button>
      </div>
    </div>
  );
}

function PolField({ label, hint, children }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)" }}>{label}</div>
      {children}
      {hint && <div className="muted" style={{ fontSize: 10.5 }}>{hint}</div>}
    </div>
  );
}

function MiniStat({ label, value, danger, warn }) {
  const tone = danger ? { bg: "var(--danger-soft)", line: "var(--danger-line)", ink: "var(--danger)" } : warn ? { bg: "var(--warn-soft)", line: "var(--warn-line)", ink: "color-mix(in oklab, var(--warn) 60%, black)" } : { bg: "var(--surface-2)", line: "var(--line)", ink: "var(--ink)" };
  return (
    <div style={{ background: tone.bg, border: "1px solid " + tone.line, borderRadius: 10, padding: "8px 13px", minWidth: 96 }}>
      <div style={{ fontSize: 11, fontWeight: 600, color: "var(--ink-3)" }}>{label}</div>
      <div className="num" style={{ fontSize: 17, fontWeight: 750, marginTop: 1, color: tone.ink }}>{value}</div>
    </div>
  );
}
