{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "jinjie",
  "type": "registry:block",
  "title": "进阶 · 健身 App",
  "description": "把每一次训练，变成看得见的进步。从今日计划到组间记录，再到完成反馈，一套完整的移动端体验。 Version 1.0.0. Complete 3-screen mobile fitness template.",
  "author": "UI Lab",
  "categories": [
    "健身",
    "深色",
    "iOS 风格",
    "H5"
  ],
  "dependencies": [
    "lucide-react@1.61.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/jinjie/jinjie.tsx",
      "type": "registry:file",
      "target": "@components/jinjie/jinjie.tsx",
      "content": "\"use client\";\nimport { useEffect, useReducer, useRef, useState } from \"react\";\nimport {\n  ArrowLeft,\n  ArrowRight,\n  BarChart3,\n  Check,\n  ChevronRight,\n  Dumbbell,\n  Flame,\n  Play,\n  RotateCcw,\n  Timer,\n  TrendingUp,\n  User,\n  X,\n  Zap,\n} from \"lucide-react\";\nimport {\n  exercises,\n  initialState,\n  isComplete,\n  restoreState,\n  trainingReducer,\n  type TrainingState,\n} from \"./session\";\nimport styles from \"./jinjie.module.css\";\nconst STORAGE = \"ui-lab:jinjie:1\";\nexport function Jinjie({\n  initialScreen = \"home\",\n  persist = true,\n}: {\n  initialScreen?: TrainingState[\"screen\"];\n  persist?: boolean;\n}) {\n  const [state, dispatch] = useReducer(\n    trainingReducer,\n    initialScreen,\n    initialState,\n  );\n  const [ready, setReady] = useState(false);\n  const [weight, setWeight] = useState(\"30\");\n  const [reps, setReps] = useState(\"8\");\n  const [rir, setRir] = useState(2);\n  const [rest, setRest] = useState(90);\n  const [seconds, setSeconds] = useState(0);\n  const [dialog, setDialog] = useState<\n    \"tips\" | \"exit\" | \"details\" | \"reset\" | null\n  >(null);\n  const [storageError, setStorageError] = useState(false);\n  const dialogRef = useRef<HTMLDialogElement>(null);\n  const canPersist = persist && initialScreen === \"home\";\n  // Seed previews never write to or read the real demo session.\n  const [restored, setRestored] = useState<TrainingState | null>(null);\n  useEffect(() => {\n    if (canPersist) {\n      try {\n        setRestored(restoreState(localStorage.getItem(STORAGE)));\n      } catch {\n        setStorageError(true);\n      }\n    }\n    setReady(true);\n  }, [canPersist]);\n  const active = restored ?? state;\n  function act(action: Parameters<typeof trainingReducer>[1]) {\n    if (restored) setRestored(trainingReducer(restored, action));\n    else dispatch(action);\n  }\n  useEffect(() => {\n    if (ready && canPersist) {\n      try {\n        localStorage.setItem(STORAGE, JSON.stringify(active));\n      } catch {\n        setStorageError(true);\n      }\n    }\n  }, [active, ready, canPersist]);\n  useEffect(() => {\n    const tick = () =>\n      setSeconds(\n        active.restUntil\n          ? Math.max(0, Math.ceil((active.restUntil - Date.now()) / 1000))\n          : 0,\n      );\n    tick();\n    const timer = window.setInterval(tick, 500);\n    return () => window.clearInterval(timer);\n  }, [active.restUntil]);\n  useEffect(() => {\n    if (dialog) dialogRef.current?.showModal();\n    else dialogRef.current?.close();\n  }, [dialog]);\n  const session = active.session;\n  const records = session?.records ?? [];\n  const currentRecords = records.filter((r) => r.exercise === active.exercise);\n  const complete = isComplete(session);\n  const week = Math.min(4, 3 + active.completedIds.length);\n  const previewComplete = session?.demo && active.screen === \"complete\";\n  const shownWeek = previewComplete ? 4 : week;\n  const minutes =\n    session?.endedAt !== undefined\n      ? Math.max(1, Math.round((session.endedAt - session.startedAt) / 60000))\n      : 0;\n  const start = () => {\n    act({ type: \"start\", now: Date.now(), id: crypto.randomUUID() });\n    setWeight(\"30\");\n    setReps(\"8\");\n    setRir(2);\n  };\n  const valid =\n    weight.trim() !== \"\" &&\n    reps.trim() !== \"\" &&\n    Number.isFinite(Number(weight)) &&\n    Number(weight) >= 0 &&\n    Number(weight) <= 500 &&\n    Number.isInteger(Number(reps)) &&\n    Number(reps) >= 1 &&\n    Number(reps) <= 100;\n  return (\n    <div className={styles.app}>\n      <div className={styles.statusbar}>\n        <span>9:41</span>\n        <div className={styles.statusIcons}>\n          <span>▮▮▮</span>\n          <span>◔</span>\n          <span className={styles.battery} />\n        </div>\n      </div>\n      <div className={styles.demo}>\n        {initialScreen !== \"home\"\n          ? \"独立演示 · 预置示例状态\"\n          : \"进阶 · 本地演示数据\"}\n      </div>\n      {!ready ? (\n        <main className={styles.main}>\n          <p>正在载入训练…</p>\n        </main>\n      ) : (\n        <>\n          {active.screen === \"home\" && (\n            <>\n              <main className={`${styles.main} ${styles.homeMain}`}>\n                <header className={styles.homeHeader}>\n                  <span className={styles.brand}>\n                    <span className={styles.brandMark}>↗</span> 进阶\n                  </span>\n                  <span className={styles.avatar}>J</span>\n                </header>\n                {active.tab === \"training\" && (\n                  <>\n                    <p className={styles.eyebrow}>每一次训练，都算数</p>\n                    <h1>\n                      今天，\n                      <br />\n                      练得更好<span className={styles.green}>。</span>\n                    </h1>\n                    <div className={styles.week}>\n                      <div>\n                        <span>本周训练</span>\n                        <strong>已完成 {week} / 4 次</strong>\n                      </div>\n                      <div className={styles.days}>\n                        {[\"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"日\"].map(\n                          (day, i) => (\n                            <span\n                              key={day}\n                              className={i < week ? styles.dayDone : \"\"}\n                            >\n                              {i < week ? <Check size={15} /> : day}\n                            </span>\n                          ),\n                        )}\n                      </div>\n                    </div>\n                    <div className={styles.plan}>\n                      <div className={styles.planTop}>\n                        <span>\n                          <Flame size={14} /> 今日计划\n                        </span>\n                        <span>力量训练</span>\n                      </div>\n                      <h2>胸肩增肌</h2>\n                      <p>4 个动作 · 12 组 · 预计 50 分钟</p>\n                      <div className={styles.equipment} aria-hidden=\"true\">\n                        <Dumbbell size={100} strokeWidth={1.1} />\n                      </div>\n                      <div className={styles.planFoot}>\n                        <span>上肢训练 / Upper body</span>\n                        <ArrowRight size={20} />\n                      </div>\n                    </div>\n                    <div className={styles.sectionTitle}>\n                      <h3>训练动作</h3>\n                      <span>4 个动作</span>\n                    </div>\n                    <div className={styles.exerciseList}>\n                      {exercises.map((e, i) => (\n                        <div className={styles.exerciseRow} key={e.name}>\n                          <span className={styles.exerciseIcon}>\n                            <Dumbbell size={21} />\n                          </span>\n                          <div>\n                            <strong>{e.name}</strong>\n                            <span>\n                              {e.muscle} · {e.sets} 组 × 8–12 次\n                            </span>\n                          </div>\n                          <span className={styles.rowNumber}>\n                            {String(i + 1).padStart(2, \"0\")}\n                          </span>\n                        </div>\n                      ))}\n                    </div>\n                    <button className={styles.primary} onClick={start}>\n                      <Play size={18} fill=\"currentColor\" />\n                      开始训练\n                    </button>\n                    <p className={styles.helper}>专注于今天的自己。</p>\n                  </>\n                )}\n                {active.tab === \"progress\" && (\n                  <>\n                    <p className={styles.eyebrow}>坚持，正在发生</p>\n                    <h1>看见你的进步</h1>\n                    <div className={styles.week}>\n                      <span>本周训练</span>\n                      <h2>\n                        {week} <small>/ 4 次</small>\n                      </h2>\n                    </div>\n                    <Chart last={week} />\n                    <div className={styles.note}>\n                      <TrendingUp size={24} />\n                      <h3>每一组，都是积累</h3>\n                      <p>\n                        本次演示已完成 {active.completedIds.length}{\" \"}\n                        次完整训练。历史三周为示例数据。\n                      </p>\n                    </div>\n                  </>\n                )}\n                {active.tab === \"profile\" && (\n                  <>\n                    <p className={styles.eyebrow}>以自己的节奏前进</p>\n                    <h1>我的训练空间</h1>\n                    <div className={styles.note}>\n                      <User size={32} />\n                      <h2>训练者 J</h2>\n                      <p>个人演示档案 · 胸肩增肌计划</p>\n                      <p>\n                        数据只保存在当前浏览器，没有账号，也不会上传。此模板用于界面演示，不构成训练建议。\n                      </p>\n                    </div>\n                    <button\n                      className={styles.secondary}\n                      onClick={() => setDialog(\"reset\")}\n                    >\n                      <RotateCcw size={18} />\n                      重置演示数据\n                    </button>\n                  </>\n                )}\n              </main>\n              <nav className={styles.nav} aria-label=\"训练导航\">\n                {(\n                  [\n                    { id: \"training\", name: \"训练\", Icon: Dumbbell },\n                    { id: \"progress\", name: \"进步\", Icon: BarChart3 },\n                    { id: \"profile\", name: \"我的\", Icon: User },\n                  ] as const\n                ).map(({ id, name, Icon }) => (\n                  <button\n                    key={id}\n                    aria-current={active.tab === id ? \"page\" : undefined}\n                    onClick={() => act({ type: \"tab\", tab: id })}\n                  >\n                    <Icon size={22} />\n                    {name}\n                  </button>\n                ))}\n              </nav>\n            </>\n          )}\n          {active.screen === \"workout\" && (\n            <main className={`${styles.main} ${styles.workoutMain}`}>\n              <div className={styles.workoutHeader}>\n                <button aria-label=\"退出训练\" onClick={() => setDialog(\"exit\")}>\n                  <ArrowLeft size={21} />\n                </button>\n                <span>训练进行中</span>\n                <button\n                  onClick={() => act({ type: \"finish\", now: Date.now() })}\n                >\n                  结束\n                </button>\n              </div>\n              <div className={styles.progressBars}>\n                {exercises.map((e, i) => (\n                  <span\n                    key={e.name}\n                    style={{\n                      background:\n                        i === active.exercise\n                          ? \"#C9F36A\"\n                          : records.filter((r) => r.exercise === i).length ===\n                              e.sets\n                            ? \"#647b36\"\n                            : undefined,\n                    }}\n                  />\n                ))}\n              </div>\n              <div className={styles.sectionTitle}>\n                <span className={styles.green}>\n                  动作 {active.exercise + 1} / 4\n                </span>\n                <label className={styles.switchLabel}>\n                  切换动作\n                  <select\n                    aria-label=\"切换动作\"\n                    value={active.exercise}\n                    onChange={(e) =>\n                      act({ type: \"exercise\", index: Number(e.target.value) })\n                    }\n                  >\n                    {exercises.map((e, i) => (\n                      <option key={e.name} value={i}>\n                        {e.name}\n                      </option>\n                    ))}\n                  </select>\n                </label>\n              </div>\n              <h1 className={styles.workoutTitle}>\n                {exercises[active.exercise].name}\n              </h1>\n              <p className={styles.subtitle}>重量按单只计算</p>\n              <div className={styles.setTable}>\n                <div className={styles.tableHead}>\n                  <span>组数</span>\n                  <span>重量 × 次数</span>\n                  <span>RIR</span>\n                  <span>状态</span>\n                </div>\n                {Array.from(\n                  { length: exercises[active.exercise].sets },\n                  (_, i) => {\n                    const r = currentRecords[i];\n                    return (\n                      <div\n                        key={i}\n                        className={`${styles.tableRow} ${!r && i === currentRecords.length ? styles.currentRow : \"\"}`}\n                      >\n                        <span>{String(i + 1).padStart(2, \"0\")}</span>\n                        <span>\n                          {r\n                            ? `${r.weight} kg × ${r.reps}`\n                            : i === currentRecords.length\n                              ? \"当前组\"\n                              : \"—\"}\n                        </span>\n                        <span>{r ? (r.rir === 4 ? \"4+\" : r.rir) : \"—\"}</span>\n                        <span>\n                          {r ? (\n                            <Check size={16} />\n                          ) : i === currentRecords.length ? (\n                            <span className={styles.dot} />\n                          ) : (\n                            <span className={styles.emptyDot} />\n                          )}\n                        </span>\n                      </div>\n                    );\n                  },\n                )}\n              </div>\n              {complete ? (\n                <div className={styles.note}>\n                  <Check size={32} />\n                  <h2>12 组，全部完成</h2>\n                  <p>做得很好，收下今天的进步。</p>\n                  <button\n                    className={styles.primary}\n                    onClick={() => act({ type: \"finish\", now: Date.now() })}\n                  >\n                    完成训练\n                    <ArrowRight size={19} />\n                  </button>\n                </div>\n              ) : (\n                <>\n                  <div className={styles.sectionTitle}>\n                    <h3>\n                      记录第{\" \"}\n                      {Math.min(\n                        currentRecords.length + 1,\n                        exercises[active.exercise].sets,\n                      )}{\" \"}\n                      组\n                    </h3>\n                    <span>{records.length} / 12 组</span>\n                  </div>\n                  <div className={styles.inputs}>\n                    <label>\n                      重量 <span>kg</span>\n                      <input\n                        aria-label=\"重量 kg\"\n                        type=\"number\"\n                        inputMode=\"decimal\"\n                        min=\"0\"\n                        max=\"500\"\n                        step=\"0.5\"\n                        value={weight}\n                        onChange={(e) => setWeight(e.target.value)}\n                      />\n                    </label>\n                    <label>\n                      次数 <span>次</span>\n                      <input\n                        aria-label=\"次数\"\n                        type=\"number\"\n                        inputMode=\"numeric\"\n                        min=\"1\"\n                        max=\"100\"\n                        value={reps}\n                        onChange={(e) => setReps(e.target.value)}\n                      />\n                    </label>\n                  </div>\n                  <div className={styles.sectionTitle}>\n                    <h3>这一组，还能做几次？</h3>\n                  </div>\n                  <div\n                    className={styles.rir}\n                    role=\"group\"\n                    aria-label=\"RIR 选择\"\n                  >\n                    {[0, 1, 2, 3, 4].map((v) => (\n                      <button\n                        key={v}\n                        aria-pressed={rir === v}\n                        onClick={() => setRir(v)}\n                      >\n                        {v === 4 ? \"4+\" : v}\n                      </button>\n                    ))}\n                  </div>\n                  <p className={styles.helper}>RIR：还能完成的次数</p>\n                  <label className={styles.restSetting}>\n                    <span>\n                      <Timer size={17} />\n                      组间休息\n                    </span>\n                    <select\n                      aria-label=\"休息时间\"\n                      value={rest}\n                      onChange={(e) => setRest(Number(e.target.value))}\n                    >\n                      <option value={30}>30 秒</option>\n                      <option value={60}>60 秒</option>\n                      <option value={90}>90 秒</option>\n                      <option value={120}>120 秒</option>\n                    </select>\n                  </label>\n                  {!valid && (\n                    <p className={styles.error} role=\"alert\">\n                      请输入 0–500 kg 的重量和 1–100 的整数次数。\n                    </p>\n                  )}\n                  <button\n                    className={styles.primary}\n                    disabled={\n                      !valid ||\n                      seconds > 0 ||\n                      currentRecords.length >= exercises[active.exercise].sets\n                    }\n                    onClick={() =>\n                      act({\n                        type: \"record\",\n                        record: {\n                          weight: Number(weight),\n                          reps: Number(reps),\n                          rir,\n                        },\n                        now: Date.now(),\n                        rest,\n                      })\n                    }\n                  >\n                    <Check size={20} />\n                    记录本组\n                  </button>\n                </>\n              )}\n              <button\n                className={styles.textButton}\n                onClick={() => setDialog(\"tips\")}\n              >\n                查看动作要点\n                <ChevronRight size={16} />\n              </button>\n              {seconds > 0 && (\n                <div className={styles.restOverlay}>\n                  <div className={styles.restCard} role=\"status\">\n                    <Timer size={26} />\n                    <h2>休息一下，继续变强</h2>\n                    <span className={styles.countdown}>\n                      {Math.floor(seconds / 60)}:\n                      {String(seconds % 60).padStart(2, \"0\")}\n                    </span>\n                    <p>下一组，保持专注。</p>\n                    <button\n                      className={styles.primary}\n                      onClick={() => act({ type: \"skip-rest\" })}\n                    >\n                      跳过休息\n                      <ArrowRight size={18} />\n                    </button>\n                  </div>\n                </div>\n              )}\n            </main>\n          )}\n          {active.screen === \"complete\" && (\n            <main className={`${styles.main} ${styles.complete}`}>\n              <div className={styles.finishMark}>\n                <Check size={38} strokeWidth={2.5} />\n              </div>\n              <p className={styles.eyebrow}>\n                {complete ? \"训练完成\" : \"本次训练已结束\"}\n              </p>\n              <h1>\n                {complete ? (\n                  <>\n                    今天又\n                    <br />\n                    前进了一步<span className={styles.green}>。</span>\n                  </>\n                ) : (\n                  <>\n                    每一组努力，\n                    <br />\n                    都值得记录。\n                  </>\n                )}\n              </h1>\n              <p className={styles.subtitle}>\n                胸肩增肌 · {session?.demo ? \"完整示例\" : \"本次实际记录\"}\n              </p>\n              <div className={styles.stats}>\n                <div>\n                  <strong>\n                    {new Set(records.map((r) => r.exercise)).size}\n                  </strong>\n                  <span>个动作</span>\n                </div>\n                <div>\n                  <strong>{records.length}</strong>\n                  <span>组训练</span>\n                </div>\n                <div>\n                  <strong>{minutes}</strong>\n                  <span>分钟</span>\n                </div>\n              </div>\n              <div className={styles.completedWeek}>\n                <span>\n                  <Flame size={18} />\n                  本周进度\n                </span>\n                <strong>已完成 {shownWeek} / 4 次</strong>\n              </div>\n              <div className={styles.highlight}>\n                <span>\n                  <Zap size={16} />\n                  关键记录\n                </span>\n                <h3>\n                  {records[0]\n                    ? exercises[records[0].exercise].name\n                    : \"尚未记录训练组\"}\n                </h3>\n                <strong>\n                  {records[0]\n                    ? `单只 ${records[0].weight}kg × ${records[0].reps} 次`\n                    : \"下次从第一组开始\"}\n                </strong>\n              </div>\n              <Chart last={shownWeek} />\n              <button\n                className={styles.primary}\n                onClick={() => act({ type: \"return\" })}\n              >\n                完成并返回\n                <ArrowRight size={19} />\n              </button>\n              <button\n                className={styles.textButton}\n                onClick={() => setDialog(\"details\")}\n              >\n                查看训练详情\n                <ChevronRight size={16} />\n              </button>\n              {!complete && (\n                <p className={styles.helper}>\n                  未完成全部 12 组，本周完成次数未增加。\n                </p>\n              )}\n            </main>\n          )}\n        </>\n      )}\n      {storageError && (\n        <p className={styles.error} role=\"status\">\n          浏览器存储不可用，本次记录仅在页面内保留。\n        </p>\n      )}\n      <dialog\n        ref={dialogRef}\n        className={styles.dialog}\n        onCancel={() => setDialog(null)}\n        onClick={(e) => {\n          if (e.target === e.currentTarget) setDialog(null);\n        }}\n      >\n        <div className={styles.dialogHead}>\n          <h2>\n            {dialog === \"tips\"\n              ? \"动作要点\"\n              : dialog === \"exit\"\n                ? \"退出本次训练？\"\n                : dialog === \"reset\"\n                  ? \"重置演示数据？\"\n                  : \"本次训练详情\"}\n          </h2>\n          <button aria-label=\"关闭弹窗\" onClick={() => setDialog(null)}>\n            <X size={20} />\n          </button>\n        </div>\n        {dialog === \"tips\" && <p>{exercises[active.exercise].cue}</p>}\n        {dialog === \"exit\" && (\n          <>\n            <p>\n              已记录的组会保留。重新开始训练会创建新的会话，本周完成次数不会增加。\n            </p>\n            <button\n              className={styles.primary}\n              onClick={() => {\n                act({ type: \"exit\" });\n                setDialog(null);\n              }}\n            >\n              确认退出\n            </button>\n          </>\n        )}\n        {dialog === \"reset\" && (\n          <>\n            <p>\n              清除本浏览器的演示会话与完成次数，回到本周 3 / 4\n              次。此操作无法撤销。\n            </p>\n            <button\n              className={styles.primary}\n              onClick={() => {\n                act({ type: \"reset\" });\n                setDialog(null);\n              }}\n            >\n              确认重置\n            </button>\n          </>\n        )}\n        {dialog === \"details\" && (\n          <div>\n            {records.length === 0 ? (\n              <p>本次没有记录训练组。</p>\n            ) : (\n              exercises.map((e, i) => (\n                <div key={e.name}>\n                  <h3>{e.name}</h3>\n                  {records\n                    .filter((r) => r.exercise === i)\n                    .map((r, j) => (\n                      <p key={j}>\n                        第 {j + 1} 组 · {r.weight} kg × {r.reps} 次 · RIR{\" \"}\n                        {r.rir === 4 ? \"4+\" : r.rir}\n                      </p>\n                    ))}\n                </div>\n              ))\n            )}\n          </div>\n        )}\n      </dialog>\n    </div>\n  );\n}\nfunction Chart({ last }: { last: number }) {\n  return (\n    <div className={styles.chart}>\n      <div className={styles.sectionTitle}>\n        <h3>稳定，比完美更重要</h3>\n        <span>最近四周</span>\n      </div>\n      <div className={styles.bars}>\n        {[3, 3, 4, last].map((n, i) => (\n          <div key={i}>\n            <span>{n}</span>\n            <i\n              style={{\n                height: `${n * 12}px`,\n                background: i === 3 ? \"#C9F36A\" : undefined,\n              }}\n            />\n            <small>{[\"三周前\", \"两周前\", \"上周\", \"本周\"][i]}</small>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/jinjie/session.ts",
      "type": "registry:file",
      "target": "@components/jinjie/session.ts",
      "content": "export const exercises = [\n  {\n    name: \"哑铃卧推\",\n    sets: 4,\n    muscle: \"胸部\",\n    cue: \"双脚踩稳地面，肩胛后收下沉。控制下放，呼气推起。重量按单只计算。\",\n  },\n  {\n    name: \"上斜哑铃卧推\",\n    sets: 3,\n    muscle: \"上胸\",\n    cue: \"凳背调至约 30°，保持手腕中立，缓慢下放，不要耸肩。\",\n  },\n  {\n    name: \"哑铃侧平举\",\n    sets: 3,\n    muscle: \"肩部\",\n    cue: \"手肘微屈，向身体两侧抬起至肩高，避免借力摆动。\",\n  },\n  {\n    name: \"绳索下压\",\n    sets: 2,\n    muscle: \"手臂\",\n    cue: \"上臂贴近躯干，保持肩膀稳定，伸直肘部后缓慢还原。\",\n  },\n] as const;\nexport type SetRecord = {\n  exercise: number;\n  weight: number;\n  reps: number;\n  rir: number;\n};\nexport type Session = {\n  id: string;\n  records: SetRecord[];\n  startedAt: number;\n  endedAt?: number;\n  demo: boolean;\n};\nexport type TrainingState = {\n  version: 1;\n  screen: \"home\" | \"workout\" | \"complete\";\n  tab: \"training\" | \"progress\" | \"profile\";\n  session: Session | null;\n  exercise: number;\n  restUntil: number | null;\n  completedIds: string[];\n};\nexport type Action =\n  | { type: \"start\"; now: number; id: string }\n  | {\n      type: \"record\";\n      record: Omit<SetRecord, \"exercise\">;\n      now: number;\n      rest: number;\n    }\n  | { type: \"skip-rest\" }\n  | { type: \"exercise\"; index: number }\n  | { type: \"finish\"; now: number }\n  | { type: \"exit\" }\n  | { type: \"return\" }\n  | { type: \"tab\"; tab: TrainingState[\"tab\"] }\n  | { type: \"reset\" };\nexport function initialState(\n  screen: TrainingState[\"screen\"] = \"home\",\n): TrainingState {\n  const state: TrainingState = {\n    version: 1,\n    screen,\n    tab: \"training\",\n    session: null,\n    exercise: 0,\n    restUntil: null,\n    completedIds: [],\n  };\n  if (screen !== \"home\") {\n    const records =\n      screen === \"workout\"\n        ? [0, 1].map(() => ({ exercise: 0, weight: 30, reps: 8, rir: 2 }))\n        : exercises.flatMap((e, exercise) =>\n            Array.from({ length: e.sets }, () => ({\n              exercise,\n              weight: 30,\n              reps: 8,\n              rir: 2,\n            })),\n          );\n    state.session = {\n      id: \"preview-seed-v1\",\n      records,\n      startedAt: screen === \"workout\" ? Date.now() - 20 * 60_000 : 0,\n      ...(screen === \"complete\" ? { endedAt: 50 * 60_000 } : {}),\n      demo: true,\n    };\n  }\n  return state;\n}\nexport function isComplete(session: Session | null) {\n  return (\n    !!session &&\n    exercises.every(\n      (e, i) =>\n        session.records.filter((r) => r.exercise === i).length === e.sets,\n    )\n  );\n}\nexport function trainingReducer(\n  state: TrainingState,\n  action: Action,\n): TrainingState {\n  switch (action.type) {\n    case \"start\":\n      return {\n        ...state,\n        screen: \"workout\",\n        tab: \"training\",\n        exercise: 0,\n        restUntil: null,\n        session: {\n          id: action.id,\n          records: [],\n          startedAt: action.now,\n          demo: false,\n        },\n      };\n    case \"record\": {\n      if (\n        state.screen !== \"workout\" ||\n        !state.session ||\n        state.session.endedAt !== undefined ||\n        (state.restUntil && state.restUntil > action.now)\n      )\n        return state;\n      const r = action.record;\n      if (\n        !Number.isFinite(r.weight) ||\n        r.weight < 0 ||\n        r.weight > 500 ||\n        !Number.isInteger(r.reps) ||\n        r.reps < 1 ||\n        r.reps > 100 ||\n        ![0, 1, 2, 3, 4].includes(r.rir)\n      )\n        return state;\n      if (\n        state.session.records.filter((r) => r.exercise === state.exercise)\n          .length >= exercises[state.exercise].sets\n      )\n        return state;\n      const records = [\n        ...state.session.records,\n        { ...r, exercise: state.exercise },\n      ];\n      const next = exercises.findIndex(\n        (e, i) => records.filter((r) => r.exercise === i).length < e.sets,\n      );\n      return {\n        ...state,\n        session: { ...state.session, records },\n        exercise:\n          records.filter((r) => r.exercise === state.exercise).length >=\n            exercises[state.exercise].sets && next >= 0\n            ? next\n            : state.exercise,\n        restUntil: next < 0 ? null : action.now + action.rest * 1000,\n      };\n    }\n    case \"skip-rest\":\n      return { ...state, restUntil: null };\n    case \"exercise\":\n      return Number.isInteger(action.index) &&\n        action.index >= 0 &&\n        action.index < exercises.length\n        ? { ...state, exercise: action.index }\n        : state;\n    case \"finish\": {\n      if (!state.session || state.screen !== \"workout\") return state;\n      const completedIds =\n        isComplete(state.session) &&\n        !state.session.demo &&\n        !state.completedIds.includes(state.session.id)\n          ? [...state.completedIds, state.session.id]\n          : state.completedIds;\n      return {\n        ...state,\n        screen: \"complete\",\n        restUntil: null,\n        session: { ...state.session, endedAt: action.now },\n        completedIds,\n      };\n    }\n    case \"exit\":\n      return { ...state, screen: \"home\", restUntil: null };\n    case \"return\":\n      return { ...state, screen: \"home\", tab: \"training\", restUntil: null };\n    case \"tab\":\n      return { ...state, tab: action.tab };\n    case \"reset\":\n      return initialState();\n  }\n}\nexport function restoreState(raw: string | null): TrainingState | null {\n  if (!raw) return null;\n  try {\n    const s = JSON.parse(raw) as TrainingState;\n    if (\n      s.version !== 1 ||\n      ![\"home\", \"workout\", \"complete\"].includes(s.screen) ||\n      ![\"training\", \"progress\", \"profile\"].includes(s.tab) ||\n      !Number.isInteger(s.exercise) ||\n      s.exercise < 0 ||\n      s.exercise > 3 ||\n      !Array.isArray(s.completedIds) ||\n      !s.completedIds.every((id) => typeof id === \"string\") ||\n      !(s.restUntil === null || Number.isFinite(s.restUntil))\n    )\n      return null;\n    if (s.session) {\n      const t = s.session;\n      if (\n        typeof t.id !== \"string\" ||\n        typeof t.demo !== \"boolean\" ||\n        !Number.isFinite(t.startedAt) ||\n        (t.endedAt !== undefined && !Number.isFinite(t.endedAt)) ||\n        !Array.isArray(t.records) ||\n        t.records.length > 12\n      )\n        return null;\n      if (\n        !t.records.every(\n          (r) =>\n            Number.isInteger(r.exercise) &&\n            r.exercise >= 0 &&\n            r.exercise < 4 &&\n            Number.isFinite(r.weight) &&\n            r.weight >= 0 &&\n            r.weight <= 500 &&\n            Number.isInteger(r.reps) &&\n            r.reps >= 1 &&\n            r.reps <= 100 &&\n            [0, 1, 2, 3, 4].includes(r.rir),\n        )\n      )\n        return null;\n      if (\n        exercises.some(\n          (e, i) => t.records.filter((r) => r.exercise === i).length > e.sets,\n        )\n      )\n        return null;\n    } else if (s.screen !== \"home\") return null;\n    return s;\n  } catch {\n    return null;\n  }\n}\n"
    },
    {
      "path": "registry/jinjie/jinjie.module.css",
      "type": "registry:file",
      "target": "@components/jinjie/jinjie.module.css",
      "content": ".app {\n  --bg: #121416;\n  --surface: #1d2225;\n  --ink: #f4f6f8;\n  --accent: #c9f36a;\n  color: var(--ink);\n  background: var(--bg);\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"SF Pro Display\", \"PingFang SC\",\n    \"Microsoft YaHei\", sans-serif;\n  font-size: 16px;\n  line-height: 1.5;\n  max-width: 480px;\n  min-height: 100dvh;\n  margin: 0 auto;\n  color-scheme: dark;\n  position: relative;\n  padding-bottom: env(safe-area-inset-bottom);\n  font-variant-numeric: tabular-nums;\n  isolation: isolate;\n}\n.app * {\n  box-sizing: border-box;\n}\n.app button,\n.app input,\n.app select {\n  font: inherit;\n}\n.app button {\n  cursor: pointer;\n  border: 0;\n}\n.app button:disabled {\n  cursor: not-allowed;\n  opacity: 0.45;\n}\n.app button:focus-visible,\n.app input:focus-visible,\n.app select:focus-visible {\n  outline: 2px solid var(--accent);\n  outline-offset: 4px;\n}\n.app h1,\n.app h2,\n.app h3,\n.app p {\n  margin: 0;\n}\n.app h1 {\n  font-size: 34px;\n  line-height: 1.3;\n  letter-spacing: -1.2px;\n  font-weight: 650;\n}\n.app h2 {\n  font-size: 25px;\n  font-weight: 600;\n}\n.app h3 {\n  font-size: 15px;\n  font-weight: 600;\n}\n.app small {\n  font-size: 13px;\n  font-weight: 400;\n}\n.statusbar {\n  height: 43px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 0 28px;\n  font-weight: 650;\n  font-size: 14px;\n}\n.statusIcons {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n  font-size: 11px;\n}\n.battery {\n  width: 23px;\n  height: 11px;\n  border: 1px solid #8c9193;\n  border-radius: 3px;\n  position: relative;\n}\n.battery:before {\n  content: \"\";\n  position: absolute;\n  inset: 2px;\n  background: #f4f6f8;\n  border-radius: 1px;\n}\n.demo {\n  text-align: center;\n  font-size: 10px;\n  letter-spacing: 0.6px;\n  color: #8c9296;\n  padding: 0 0 4px;\n}\n.main {\n  padding: 14px 24px 18px;\n}\n.homeHeader {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin: 0 0 22px;\n}\n.brand {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  font-size: 22px;\n  font-weight: 700;\n  letter-spacing: 1px;\n}\n.brandMark {\n  background: var(--accent);\n  color: #121416;\n  border-radius: 9px;\n  width: 27px;\n  height: 27px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 25px;\n  line-height: 1;\n}\n.avatar {\n  width: 32px;\n  height: 32px;\n  border: 1px solid #3b4144;\n  border-radius: 50%;\n  display: grid;\n  place-items: center;\n  font-size: 13px;\n  color: #bcc1c3;\n}\n.eyebrow {\n  color: #9ba1a4;\n  font-size: 13px;\n  margin-bottom: 8px !important;\n}\n.green {\n  color: var(--accent);\n}\n.week {\n  margin: 22px 0 20px;\n}\n.week > div:first-child {\n  display: flex;\n  justify-content: space-between;\n  font-size: 13px;\n  color: #aaaeb0;\n}\n.week strong {\n  color: var(--accent);\n  font-weight: 500;\n}\n.days {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 12px;\n  gap: 8px;\n}\n.days > span {\n  display: grid;\n  place-items: center;\n  width: 31px;\n  height: 31px;\n  border-radius: 50%;\n  color: #6f767a;\n  background: #23282b;\n  font-size: 11px;\n}\n.days > .dayDone {\n  background: var(--accent);\n  color: #222920;\n}\n.plan {\n  position: relative;\n  overflow: hidden;\n  background: #242b28;\n  border: 1px solid #394338;\n  border-radius: 18px;\n  padding: 18px 18px 14px;\n}\n.planTop {\n  display: flex;\n  justify-content: space-between;\n  position: relative;\n  z-index: 1;\n  color: #bdc9b5;\n  font-size: 10px;\n}\n.planTop span:first-child {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n}\n.planTop span:last-child {\n  border: 1px solid #52604a;\n  border-radius: 5px;\n  padding: 1px 6px;\n  color: #c9f36a;\n}\n.plan h2 {\n  margin-top: 12px;\n  font-size: 25px;\n  letter-spacing: 1px;\n}\n.plan p {\n  font-size: 12px;\n  color: #bcc2bd;\n  margin-top: 6px;\n  position: relative;\n  z-index: 1;\n}\n.equipment {\n  position: absolute;\n  right: 1px;\n  top: 35px;\n  transform: rotate(-28deg);\n  color: #78866d;\n  opacity: 0.3;\n}\n.planFoot {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  border-top: 1px solid #44503c;\n  margin-top: 18px;\n  padding-top: 11px;\n  color: #b4bea9;\n  font-size: 10px;\n}\n.sectionTitle {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 8px;\n  margin: 22px 0 10px;\n}\n.sectionTitle > span {\n  font-size: 11px;\n  color: #9ba2a6;\n}\n.sectionTitle > span.green {\n  color: var(--accent);\n  font-size: 13px;\n}\n.exerciseList {\n  margin-bottom: 20px;\n}\n.exerciseRow {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  padding: 11px 0;\n  border-bottom: 1px solid #282d30;\n}\n.exerciseRow:last-child {\n  border-bottom: 0;\n}\n.exerciseIcon {\n  width: 38px;\n  height: 38px;\n  background: #23282b;\n  display: grid;\n  place-items: center;\n  border-radius: 11px;\n  color: #b8bebf;\n  flex-shrink: 0;\n}\n.exerciseRow > div {\n  display: flex;\n  flex-direction: column;\n  gap: 3px;\n}\n.exerciseRow strong {\n  font-size: 14px;\n  font-weight: 500;\n}\n.exerciseRow div span {\n  font-size: 11px;\n  color: #969c9f;\n}\n.rowNumber {\n  margin-left: auto;\n  color: #686f72;\n  font-size: 13px;\n}\n.primary {\n  height: 52px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 9px;\n  width: 100%;\n  background: var(--accent);\n  color: #1c2613;\n  border-radius: 13px;\n  font-weight: 650 !important;\n  font-size: 16px !important;\n  transition: background 0.15s;\n}\n.primary:hover {\n  background: #d5fa88;\n}\n.helper {\n  font-size: 11px;\n  color: #969da0;\n  text-align: center;\n  margin-top: 10px !important;\n}\n.nav {\n  position: sticky;\n  bottom: 0;\n  display: flex;\n  justify-content: space-around;\n  background: #15191bf2;\n  border-top: 1px solid #2a3032;\n  padding: 10px 10px calc(10px + env(safe-area-inset-bottom));\n  backdrop-filter: blur(10px);\n  z-index: 2;\n}\n.nav button {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n  gap: 3px;\n  color: #81898d;\n  background: none;\n  font-size: 10px;\n  min-width: 65px;\n}\n.nav button[aria-current] {\n  color: var(--accent);\n}\n.workoutHeader {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  font-size: 15px;\n  margin: 6px 0 24px;\n}\n.workoutHeader button {\n  padding: 5px;\n  background: none;\n  color: #b9bfc2;\n  font-size: 13px;\n  display: flex;\n  align-items: center;\n}\n.progressBars {\n  display: flex;\n  gap: 5px;\n}\n.progressBars span {\n  height: 3px;\n  flex: 1;\n  border-radius: 2px;\n  background: #303639;\n}\n.workoutTitle {\n  font-size: 29px !important;\n  margin-top: 14px !important;\n}\n.subtitle {\n  color: #949da1;\n  font-size: 13px;\n  margin-top: 8px !important;\n}\n.switchLabel {\n  position: relative;\n  color: #9da5a9;\n  font-size: 11px;\n}\n.switchLabel select {\n  position: absolute;\n  inset: 0;\n  opacity: 0;\n  width: 100%;\n  cursor: pointer;\n}\n.switchLabel:focus-within {\n  outline: 2px solid var(--accent);\n  outline-offset: 4px;\n}\n.setTable {\n  margin: 24px 0 28px;\n}\n.tableHead,\n.tableRow {\n  display: grid;\n  grid-template-columns: 45px 1fr 40px 38px;\n  gap: 8px;\n  align-items: center;\n  padding: 11px 12px;\n}\n.tableHead {\n  font-size: 10px;\n  color: #858e93;\n}\n.tableRow {\n  font-size: 14px;\n  color: #cdd3d6;\n  border-bottom: 1px solid #292f32;\n  min-height: 44px;\n}\n.tableRow > span:last-child {\n  display: flex;\n  justify-content: center;\n  color: var(--accent);\n}\n.currentRow {\n  background: #293323;\n  border: 1px solid #455734;\n  border-radius: 9px;\n  color: var(--accent);\n}\n.dot {\n  width: 7px;\n  height: 7px;\n  border-radius: 50%;\n  background: var(--accent);\n}\n.emptyDot {\n  width: 7px;\n  height: 7px;\n  border-radius: 50%;\n  border: 1px solid #4b5357;\n}\n.inputs {\n  display: flex;\n  gap: 12px;\n  margin-top: 14px;\n}\n.inputs label {\n  padding: 14px 16px 10px;\n  background: var(--surface);\n  border: 1px solid #333b40;\n  border-radius: 14px;\n  width: 50%;\n  font-size: 12px;\n  color: #aeb6ba;\n}\n.inputs label > span {\n  float: right;\n  color: #7d878d;\n}\n.inputs input {\n  font-size: 38px;\n  font-weight: 600;\n  letter-spacing: -1px;\n  width: 100%;\n  background: none;\n  border: 0;\n  outline: 0 !important;\n  color: var(--ink);\n  margin-top: 5px;\n  padding: 0;\n  appearance: textfield;\n  min-height: 51px;\n}\n.inputs input::-webkit-inner-spin-button {\n  appearance: none;\n}\n.inputs label:focus-within {\n  border-color: var(--accent);\n}\n.rir {\n  display: flex;\n  gap: 8px;\n}\n.rir button {\n  flex: 1;\n  height: 42px;\n  background: var(--surface);\n  border: 1px solid #343b3f;\n  border-radius: 10px;\n  color: #c8cdd0;\n  font-size: 15px;\n}\n.rir button[aria-pressed=\"true\"] {\n  background: var(--accent);\n  color: #1c2514;\n  border-color: var(--accent);\n}\n.restSetting {\n  margin: 24px 0 18px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  font-size: 13px;\n  color: #aeb8bd;\n}\n.restSetting > span {\n  display: flex;\n  align-items: center;\n  gap: 7px;\n}\n.restSetting select {\n  background: transparent;\n  border: 0;\n  color: var(--ink);\n  font-size: 13px;\n  padding: 8px 0;\n}\n.textButton {\n  display: flex;\n  gap: 3px;\n  align-items: center;\n  justify-content: center;\n  margin: 17px auto 0;\n  color: #aeb8bd;\n  background: none;\n  font-size: 12px !important;\n  padding: 7px;\n}\n.note {\n  margin: 24px 0;\n  background: var(--surface);\n  border-radius: 16px;\n  padding: 24px;\n  color: #bac2c5;\n}\n.note h2,\n.note h3 {\n  margin: 16px 0;\n  color: var(--ink);\n}\n.note p {\n  font-size: 14px;\n  line-height: 1.8;\n  margin: 10px 0;\n}\n.note svg {\n  color: var(--accent);\n}\n.note .primary {\n  margin-top: 24px;\n}\n.secondary {\n  display: flex;\n  justify-content: center;\n  gap: 10px;\n  width: 100%;\n  padding: 15px;\n  border-radius: 12px;\n  background: #2a3135;\n  color: var(--ink);\n}\n.restOverlay {\n  position: fixed;\n  inset: 0;\n  display: flex;\n  align-items: flex-end;\n  padding: 20px;\n  background: #000a;\n  backdrop-filter: blur(4px);\n  z-index: 5;\n}\n.restCard {\n  max-width: 400px;\n  margin: auto auto 16px;\n  width: 100%;\n  background: #22292d;\n  border: 1px solid #3c474c;\n  border-radius: 24px;\n  text-align: center;\n  padding: 32px 24px;\n}\n.restCard > svg {\n  color: var(--accent);\n}\n.restCard h2 {\n  font-size: 21px;\n  margin: 16px 0 8px;\n}\n.countdown {\n  display: block;\n  font-size: 62px;\n  color: var(--accent);\n  font-weight: 650;\n  letter-spacing: -3px;\n}\n.restCard p {\n  font-size: 13px;\n  color: #a5afb3;\n  margin: 4px 0 28px;\n}\n.complete {\n  padding-top: 25px;\n}\n.finishMark {\n  display: grid;\n  place-items: center;\n  width: 66px;\n  height: 66px;\n  background: var(--accent);\n  color: #28351d;\n  border-radius: 50%;\n  margin: 2px 0 25px;\n}\n.complete h1 {\n  font-size: 33px;\n}\n.stats {\n  display: flex;\n  margin: 27px 0;\n  border-top: 1px solid #32383b;\n  border-bottom: 1px solid #32383b;\n  padding: 19px 0;\n}\n.stats > div {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 3px;\n  border-right: 1px solid #343a3d;\n}\n.stats > div:last-child {\n  border: 0;\n}\n.stats strong {\n  font-size: 30px;\n  font-weight: 600;\n  line-height: 1.2;\n}\n.stats span {\n  font-size: 11px;\n  color: #a0a9ae;\n}\n.completedWeek {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  color: #c8d4be;\n  font-size: 12px;\n  background: #283124;\n  padding: 16px;\n  border-radius: 12px;\n}\n.completedWeek > span {\n  display: flex;\n  align-items: center;\n  gap: 7px;\n}\n.completedWeek strong {\n  font-weight: 500;\n  color: var(--accent);\n}\n.highlight {\n  margin: 16px 0;\n  background: var(--surface);\n  border-radius: 14px;\n  padding: 16px;\n}\n.highlight > span {\n  font-size: 11px;\n  color: #a3adb2;\n  display: flex;\n  align-items: center;\n  gap: 6px;\n}\n.highlight h3 {\n  margin: 12px 0 4px;\n}\n.highlight strong {\n  font-size: 19px;\n  color: var(--accent);\n  font-weight: 500;\n}\n.chart {\n  margin: 20px 0 25px;\n}\n.chart h3 {\n  font-size: 13px;\n}\n.bars {\n  display: flex;\n  align-items: flex-end;\n  gap: 18px;\n  padding: 6px 10px 0;\n}\n.bars > div {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 7px;\n}\n.bars span {\n  font-size: 12px;\n  color: #a8b1b5;\n}\n.bars i {\n  display: block;\n  width: 100%;\n  background: #3b4449;\n  border-radius: 5px 5px 1px 1px;\n}\n.bars small {\n  font-size: 10px;\n  color: #828d93;\n}\n.dialog {\n  background: var(--surface);\n  color: var(--ink);\n  border: 1px solid #465057;\n  border-radius: 20px;\n  padding: 24px;\n  width: calc(100% - 40px);\n  max-width: 400px;\n  max-height: 80dvh;\n  overflow: auto;\n  font-family: inherit;\n}\n.dialog::backdrop {\n  background: #000b;\n  backdrop-filter: blur(3px);\n}\n.dialogHead {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  margin-bottom: 20px;\n}\n.dialogHead h2 {\n  font-size: 20px;\n}\n.dialogHead button {\n  background: transparent;\n  color: #b7c1c5;\n  display: flex;\n  padding: 4px;\n}\n.dialog p {\n  color: #b8c2c7;\n  font-size: 14px;\n  line-height: 1.8;\n  margin: 12px 0;\n}\n.dialog h3 {\n  margin-top: 18px;\n}\n.dialog .primary {\n  margin-top: 24px;\n}\n.error {\n  color: #ffbdab;\n  font-size: 12px;\n  padding: 8px 0;\n  line-height: 1.6;\n}\n@media (min-width: 481px) {\n  .app {\n    border-left: 1px solid #292e31;\n    border-right: 1px solid #292e31;\n  }\n}\n@media (max-width: 350px) {\n  .main {\n    padding-left: 16px;\n    padding-right: 16px;\n  }\n  .app h1 {\n    font-size: 30px;\n  }\n  .rir {\n    gap: 5px;\n  }\n}\n/* Keep the home screen's primary action within the 390 × 844 reference. */\n.statusbar {\n  height: 35px;\n}\n.homeMain {\n  padding-top: 8px;\n  padding-bottom: 12px;\n}\n.homeHeader {\n  margin-bottom: 12px;\n}\n.homeMain h1 {\n  font-size: 30px;\n  line-height: 1.25;\n}\n.homeMain .eyebrow {\n  font-size: 12px;\n  margin-bottom: 6px !important;\n}\n.homeMain .week {\n  margin: 16px 0;\n}\n.days {\n  margin-top: 8px;\n}\n.days > span {\n  width: 28px;\n  height: 28px;\n}\n.plan {\n  padding: 14px 16px 12px;\n}\n.plan h2 {\n  margin-top: 8px;\n  font-size: 24px;\n}\n.planFoot {\n  margin-top: 12px;\n  padding-top: 8px;\n}\n.homeMain .sectionTitle {\n  margin: 16px 0 6px;\n}\n.exerciseRow {\n  padding: 7px 0;\n  gap: 10px;\n}\n.exerciseIcon {\n  width: 34px;\n  height: 34px;\n  border-radius: 10px;\n}\n.exerciseRow > div {\n  gap: 1px;\n}\n.exerciseRow strong {\n  font-size: 15px;\n}\n.exerciseRow div span {\n  font-size: 10px;\n}\n.exerciseList {\n  margin-bottom: 14px;\n}\n.homeMain .helper {\n  font-size: 10px;\n  margin-top: 7px !important;\n}\n.workoutMain {\n  padding-top: 10px;\n}\n.workoutHeader {\n  margin-bottom: 12px;\n}\n.workoutMain .sectionTitle {\n  margin-top: 16px;\n  margin-bottom: 8px;\n}\n.setTable {\n  margin: 16px 0;\n}\n.tableHead {\n  padding-top: 6px;\n  padding-bottom: 6px;\n}\n.tableRow {\n  padding-top: 8px;\n  padding-bottom: 8px;\n  min-height: 40px;\n}\n.inputs label {\n  padding: 10px 16px 8px;\n}\n.inputs input {\n  font-size: 36px;\n  min-height: 43px;\n}\n.workoutMain .restSetting {\n  margin: 16px 0 12px;\n}\n.textButton {\n  margin-top: 10px;\n}\n.complete {\n  padding-top: 16px;\n}\n.finishMark {\n  width: 56px;\n  height: 56px;\n  margin-bottom: 16px;\n}\n.complete h1 {\n  font-size: 30px;\n}\n.stats {\n  margin: 20px 0;\n  padding: 14px 0;\n}\n.highlight {\n  margin: 12px 0;\n  padding: 14px;\n}\n.chart {\n  margin: 16px 0 18px;\n}\n.complete .sectionTitle {\n  margin-top: 16px;\n}\n"
    },
    {
      "path": "registry/jinjie/version.json",
      "type": "registry:file",
      "target": "@components/jinjie/version.json",
      "content": "{\"id\":\"jinjie\",\"version\":\"1.0.0\",\"license\":\"MIT\",\"source\":\"UI Lab original\",\"installedFrom\":\"ui-lab registry\",\"updatePolicy\":\"Review diffs; never overwrite business changes automatically\"}\n"
    },
    {
      "path": "registry/jinjie/LICENSE",
      "type": "registry:file",
      "target": "@components/jinjie/LICENSE",
      "content": "MIT License\n\nCopyright (c) 2026 UI Lab contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "license": "MIT",
    "parentId": null,
    "preview": "/preview/home"
  }
}
