{"id":26239,"date":"2025-12-20T12:10:00","date_gmt":"2025-12-20T03:10:00","guid":{"rendered":"http:\/\/www.tyosuke20xx.com\/blog\/?p=26239"},"modified":"2025-12-20T12:10:03","modified_gmt":"2025-12-20T03:10:03","slug":"%e3%82%b2%e3%83%bc%e3%83%a0%e6%8a%95%e7%a8%bf%e3%82%b5%e3%82%a4%e3%83%88-html","status":"publish","type":"post","link":"http:\/\/www.tyosuke20xx.com\/blog\/?p=26239","title":{"rendered":"\u30b2\u30fc\u30e0\u6295\u7a3f\u30b5\u30a4\u30c8.html"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code>&lt;!doctype html>\n&lt;html lang=\"ja\">\n&lt;head>\n  &lt;meta charset=\"utf-8\" \/>\n  &lt;meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" \/>\n  &lt;title>GameWorks \u2014 \u30b2\u30fc\u30e0\u4f5c\u54c1\u6295\u7a3f&lt;\/title>\n  &lt;meta name=\"description\" content=\"\u30b2\u30fc\u30e0\u4f5c\u54c1\u3092\u6295\u7a3f\u30fb\u7ba1\u7406\u3067\u304d\u308b\u30ed\u30fc\u30ab\u30eb\u4fdd\u5b58\u578b\u30dd\u30fc\u30c8\u30d5\u30a9\u30ea\u30aa\u30b5\u30a4\u30c8\" \/>\n  &lt;style>\n    :root{\n      --bg:#0b0d12; --panel:#111520; --panel2:#0f1320; --card:#0f1524;\n      --text:#e9edf7; --muted:#aab3c7; --line:#1f2a44;\n      --accent:#7c5cff; --accent2:#22c55e; --warn:#f59e0b; --danger:#ef4444;\n      --shadow: 0 12px 30px rgba(0,0,0,.35);\n      --r: 18px;\n      --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n      --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, \"Apple Color Emoji\",\"Segoe UI Emoji\";\n    }\n    *{box-sizing:border-box}\n    html,body{height:100%}\n    body{\n      margin:0;\n      background:\n        radial-gradient(900px 600px at 20% -10%, rgba(124,92,255,.25), transparent 60%),\n        radial-gradient(900px 600px at 85% 0%, rgba(34,197,94,.18), transparent 60%),\n        var(--bg);\n      color:var(--text);\n      font-family:var(--sans);\n      letter-spacing:.2px;\n    }\n    a{color:inherit}\n    .app{\n      min-height:100%;\n      display:grid;\n      grid-template-columns: 360px 1fr;\n      gap:16px;\n      padding:16px;\n      max-width:1400px;\n      margin:0 auto;\n    }\n    @media (max-width: 980px){\n      .app{grid-template-columns: 1fr; }\n    }\n    header{\n      grid-column: 1 \/ -1;\n      display:flex;\n      align-items:center;\n      justify-content:space-between;\n      gap:12px;\n      padding:14px 16px;\n      border:1px solid var(--line);\n      border-radius: var(--r);\n      background: linear-gradient(180deg, rgba(17,21,32,.9), rgba(15,19,32,.75));\n      box-shadow: var(--shadow);\n      position:sticky;\n      top:16px;\n      z-index:10;\n      backdrop-filter: blur(10px);\n    }\n    .brand{\n      display:flex; align-items:center; gap:12px;\n    }\n    .logo{\n      width:38px; height:38px; border-radius:14px;\n      background: conic-gradient(from 180deg, var(--accent), #3b82f6, var(--accent2), var(--accent));\n      box-shadow: 0 10px 20px rgba(124,92,255,.25);\n    }\n    .brand h1{font-size:16px; margin:0}\n    .brand p{margin:2px 0 0 0; color:var(--muted); font-size:12px}\n    .headerActions{display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end}\n    .chip{\n      display:inline-flex; align-items:center; gap:8px;\n      padding:8px 10px;\n      border:1px solid var(--line);\n      border-radius: 999px;\n      color:var(--muted);\n      background: rgba(10,12,18,.35);\n      font-size:12px;\n      user-select:none;\n    }\n    .btn{\n      appearance:none; border:1px solid var(--line);\n      background: rgba(15,19,32,.85);\n      color:var(--text);\n      padding:10px 12px;\n      border-radius: 12px;\n      cursor:pointer;\n      font-weight:600;\n      transition: transform .08s ease, border-color .2s ease, background .2s ease, opacity .2s ease;\n    }\n    .btn:hover{border-color: rgba(124,92,255,.55)}\n    .btn:active{transform: translateY(1px)}\n    .btn.primary{\n      border-color: rgba(124,92,255,.6);\n      background: linear-gradient(180deg, rgba(124,92,255,.35), rgba(124,92,255,.15));\n    }\n    .btn.good{\n      border-color: rgba(34,197,94,.55);\n      background: linear-gradient(180deg, rgba(34,197,94,.25), rgba(34,197,94,.10));\n    }\n    .btn.danger{\n      border-color: rgba(239,68,68,.6);\n      background: linear-gradient(180deg, rgba(239,68,68,.25), rgba(239,68,68,.10));\n    }\n    .btn.ghost{background: transparent}\n    .btn.small{padding:8px 10px; border-radius: 10px; font-size:12px}\n    .btn:disabled{opacity:.55; cursor:not-allowed}\n    .panel{\n      border:1px solid var(--line);\n      border-radius: var(--r);\n      background: linear-gradient(180deg, rgba(17,21,32,.92), rgba(15,19,32,.78));\n      box-shadow: var(--shadow);\n      overflow:hidden;\n    }\n    .panelHeader{\n      padding:14px 16px;\n      border-bottom:1px solid var(--line);\n      display:flex; align-items:center; justify-content:space-between; gap:10px;\n    }\n    .panelHeader h2{font-size:14px; margin:0}\n    .panelHeader .hint{font-size:12px; color:var(--muted)}\n    .panelBody{padding:14px 16px}\n    .field{display:flex; flex-direction:column; gap:6px; margin-bottom:12px}\n    .field label{font-size:12px; color:var(--muted)}\n    .row{display:grid; grid-template-columns: 1fr 1fr; gap:10px}\n    @media (max-width: 980px){ .row{grid-template-columns: 1fr} }\n    input, select, textarea{\n      width:100%;\n      padding:10px 12px;\n      border-radius: 12px;\n      border:1px solid var(--line);\n      background: rgba(10,12,18,.35);\n      color:var(--text);\n      outline:none;\n    }\n    textarea{min-height:110px; resize:vertical; line-height:1.5}\n    input:focus, select:focus, textarea:focus{border-color: rgba(124,92,255,.55)}\n    .help{font-size:11px; color:var(--muted); line-height:1.5}\n    .divider{height:1px; background: var(--line); margin:14px 0}\n    .toolbar{\n      display:flex; gap:10px; align-items:center; flex-wrap:wrap;\n      padding:12px 16px;\n      border-bottom:1px solid var(--line);\n      background: rgba(10,12,18,.18);\n    }\n    .toolbar input, .toolbar select{\n      padding:10px 12px; border-radius: 999px;\n      min-width: 200px;\n    }\n    .toolbar .grow{flex:1}\n    .stats{\n      display:flex; gap:8px; flex-wrap:wrap;\n      padding:0 16px 14px 16px;\n      color:var(--muted);\n      font-size:12px;\n    }\n    .grid{\n      padding:16px;\n      display:grid;\n      grid-template-columns: repeat(3, minmax(0, 1fr));\n      gap:14px;\n    }\n    @media (max-width: 1200px){ .grid{grid-template-columns: repeat(2, minmax(0, 1fr));} }\n    @media (max-width: 680px){ .grid{grid-template-columns: 1fr;} }\n    .card{\n      border:1px solid var(--line);\n      border-radius: 18px;\n      overflow:hidden;\n      background: linear-gradient(180deg, rgba(15,21,36,.95), rgba(12,16,28,.88));\n      box-shadow: 0 10px 25px rgba(0,0,0,.22);\n      display:flex; flex-direction:column;\n      min-height: 260px;\n    }\n    .thumb{\n      height:160px;\n      background: radial-gradient(1200px 260px at 10% 0%, rgba(124,92,255,.18), transparent 60%),\n                  radial-gradient(1200px 260px at 80% 0%, rgba(34,197,94,.12), transparent 60%),\n                  rgba(10,12,18,.25);\n      border-bottom:1px solid var(--line);\n      display:flex; align-items:center; justify-content:center;\n      position:relative;\n      overflow:hidden;\n    }\n    .thumb img{\n      width:100%; height:100%;\n      object-fit:cover;\n      display:block;\n      filter:saturate(1.02) contrast(1.02);\n    }\n    .badgeRow{\n      position:absolute; left:10px; top:10px;\n      display:flex; gap:8px; flex-wrap:wrap;\n    }\n    .badge{\n      font-size:11px; color:var(--text);\n      padding:6px 9px;\n      border-radius: 999px;\n      border:1px solid rgba(255,255,255,.12);\n      background: rgba(0,0,0,.35);\n      backdrop-filter: blur(8px);\n    }\n    .badge.good{border-color: rgba(34,197,94,.35)}\n    .badge.warn{border-color: rgba(245,158,11,.35)}\n    .badge.muted{color:var(--muted)}\n    .cardBody{padding:12px 12px 10px 12px; display:flex; flex-direction:column; gap:8px; flex:1}\n    .titleRow{display:flex; align-items:flex-start; justify-content:space-between; gap:10px}\n    .card h3{margin:0; font-size:15px; line-height:1.25}\n    .tagline{margin:0; color:var(--muted); font-size:12px; line-height:1.45}\n    .meta{\n      display:flex; gap:8px; flex-wrap:wrap;\n      color:var(--muted); font-size:12px;\n    }\n    .pill{\n      border:1px solid var(--line);\n      background: rgba(10,12,18,.25);\n      padding:6px 9px; border-radius:999px;\n      font-size:12px;\n    }\n    .cardFooter{\n      padding:10px 12px 12px 12px;\n      display:flex; align-items:center; justify-content:space-between; gap:10px;\n      border-top:1px solid var(--line);\n      background: rgba(10,12,18,.18);\n    }\n    .actions{display:flex; gap:8px; flex-wrap:wrap}\n    .muted{color:var(--muted)}\n    .mono{font-family:var(--mono)}\n    .empty{\n      padding:28px 16px 40px 16px;\n      text-align:center;\n      color:var(--muted);\n    }\n    dialog{\n      border:none;\n      border-radius: 18px;\n      padding:0;\n      width:min(920px, calc(100vw - 24px));\n      background: rgba(15,19,32,.96);\n      color:var(--text);\n      box-shadow: 0 30px 80px rgba(0,0,0,.55);\n    }\n    dialog::backdrop{background: rgba(0,0,0,.6)}\n    .modalHeader{\n      padding:14px 16px;\n      border-bottom:1px solid var(--line);\n      display:flex; align-items:center; justify-content:space-between; gap:10px;\n    }\n    .modalHeader h3{margin:0; font-size:14px}\n    .modalBody{padding:14px 16px}\n    .modalGrid{\n      display:grid; grid-template-columns: 1.2fr .8fr; gap:14px;\n    }\n    @media (max-width: 900px){ .modalGrid{grid-template-columns: 1fr} }\n    .gallery{\n      display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:10px;\n    }\n    @media (max-width: 680px){ .gallery{grid-template-columns: repeat(2, minmax(0, 1fr));} }\n    .gimg{\n      border:1px solid var(--line);\n      border-radius: 14px;\n      overflow:hidden;\n      background: rgba(10,12,18,.25);\n      aspect-ratio: 16 \/ 10;\n      display:flex; align-items:center; justify-content:center;\n    }\n    .gimg img{width:100%; height:100%; object-fit:cover}\n    .kvs{display:grid; grid-template-columns: 110px 1fr; gap:8px; align-items:start; font-size:12px; color:var(--muted)}\n    .kvs b{color:var(--text); font-weight:700}\n    .note{\n      padding:10px 12px;\n      border:1px solid var(--line);\n      border-radius: 14px;\n      background: rgba(10,12,18,.25);\n      color:var(--muted);\n      font-size:12px;\n      line-height:1.55;\n    }\n    .toast{\n      position: fixed;\n      left:50%;\n      bottom:16px;\n      transform: translateX(-50%);\n      background: rgba(15,19,32,.95);\n      border: 1px solid var(--line);\n      border-radius: 999px;\n      padding: 10px 14px;\n      color: var(--text);\n      box-shadow: var(--shadow);\n      opacity:0;\n      pointer-events:none;\n      transition: opacity .2s ease, transform .2s ease;\n      display:flex; gap:8px; align-items:center;\n      z-index:100;\n      max-width: calc(100vw - 24px);\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n    .toast.show{opacity:1; transform: translateX(-50%) translateY(-2px)}\n    .smallLink{\n      color: var(--muted);\n      font-size:12px;\n      text-decoration:none;\n      border-bottom:1px dashed rgba(170,179,199,.35);\n    }\n    .smallLink:hover{color: var(--text); border-bottom-color: rgba(233,237,247,.55)}\n    .dangerText{color: #ffb4b4}\n    .goodText{color: #b5ffd1}\n    .warnText{color: #ffe2b0}\n  &lt;\/style>\n&lt;\/head>\n&lt;body>\n  &lt;div class=\"app\">\n    &lt;header>\n      &lt;div class=\"brand\">\n        &lt;div class=\"logo\" aria-hidden=\"true\">&lt;\/div>\n        &lt;div>\n          &lt;h1>GameWorks &lt;span class=\"muted\">\u2014 \u30b2\u30fc\u30e0\u4f5c\u54c1\u6295\u7a3f&lt;\/span>&lt;\/h1>\n          &lt;p>\u30b5\u30fc\u30d0\u30fc\u4e0d\u8981 \/ \u30d6\u30e9\u30a6\u30b6\u306b\u4fdd\u5b58 \/ \u4f5c\u54c1\u30dd\u30fc\u30c8\u30d5\u30a9\u30ea\u30aa\u3092\u5373\u4f5c\u308b&lt;\/p>\n        &lt;\/div>\n      &lt;\/div>\n      &lt;div class=\"headerActions\">\n        &lt;span class=\"chip\" id=\"chipCount\">\u4f5c\u54c1: 0&lt;\/span>\n        &lt;button class=\"btn small\" id=\"btnSeed\" type=\"button\">\u30b5\u30f3\u30d7\u30eb\u8ffd\u52a0&lt;\/button>\n        &lt;button class=\"btn small\" id=\"btnExport\" type=\"button\">\u30a8\u30af\u30b9\u30dd\u30fc\u30c8&lt;\/button>\n        &lt;button class=\"btn small\" id=\"btnImport\" type=\"button\">\u30a4\u30f3\u30dd\u30fc\u30c8&lt;\/button>\n        &lt;button class=\"btn small danger\" id=\"btnWipe\" type=\"button\">\u5168\u524a\u9664&lt;\/button>\n      &lt;\/div>\n    &lt;\/header>\n\n    &lt;!-- Left: \u6295\u7a3f\u30d5\u30a9\u30fc\u30e0 -->\n    &lt;section class=\"panel\" aria-label=\"\u6295\u7a3f\u30d5\u30a9\u30fc\u30e0\">\n      &lt;div class=\"panelHeader\">\n        &lt;div>\n          &lt;h2 id=\"formTitle\">\u65b0\u898f\u6295\u7a3f&lt;\/h2>\n          &lt;div class=\"hint\" id=\"formHint\">\u4f5c\u54c1\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u4fdd\u5b58&lt;\/div>\n        &lt;\/div>\n        &lt;button class=\"btn small ghost\" id=\"btnResetForm\" type=\"button\">\u30ea\u30bb\u30c3\u30c8&lt;\/button>\n      &lt;\/div>\n      &lt;div class=\"panelBody\">\n        &lt;div class=\"field\">\n          &lt;label for=\"author\">\u6295\u7a3f\u8005\u540d\uff08\u4efb\u610f\uff09&lt;\/label>\n          &lt;input id=\"author\" type=\"text\" maxlength=\"40\" placeholder=\"\u4f8b\uff1aYuhei\" \/>\n          &lt;div class=\"help\">\u540c\u3058\u30d6\u30e9\u30a6\u30b6\u5185\u3060\u3051\u306e\u8868\u793a\u3067\u3059\u3002&lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"field\">\n          &lt;label for=\"title\">\u4f5c\u54c1\u30bf\u30a4\u30c8\u30eb *&lt;\/label>\n          &lt;input id=\"title\" type=\"text\" maxlength=\"60\" placeholder=\"\u4f8b\uff1aElder Chronicle VR\" required \/>\n        &lt;\/div>\n\n        &lt;div class=\"field\">\n          &lt;label for=\"tagline\">\u3072\u3068\u3053\u3068\uff08\u30ad\u30e3\u30c3\u30c1\u30b3\u30d4\u30fc\uff09&lt;\/label>\n          &lt;input id=\"tagline\" type=\"text\" maxlength=\"80\" placeholder=\"\u4f8b\uff1a\u63a2\u7d22\u3068\u6226\u95d8\u304c\u6c17\u6301\u3061\u3044\u3044VR\u30a2\u30af\u30b7\u30e7\u30f3RPG\" \/>\n        &lt;\/div>\n\n        &lt;div class=\"row\">\n          &lt;div class=\"field\">\n            &lt;label for=\"engine\">\u4f7f\u7528\u30a8\u30f3\u30b8\u30f3&lt;\/label>\n            &lt;select id=\"engine\">\n              &lt;option value=\"\">\u672a\u8a2d\u5b9a&lt;\/option>\n              &lt;option>Unity&lt;\/option>\n              &lt;option>Unreal Engine&lt;\/option>\n              &lt;option>Godot&lt;\/option>\n              &lt;option>RPG\u30c4\u30af\u30fc\u30eb&lt;\/option>\n              &lt;option>\u81ea\u4f5c&lt;\/option>\n              &lt;option>\u305d\u306e\u4ed6&lt;\/option>\n            &lt;\/select>\n          &lt;\/div>\n          &lt;div class=\"field\">\n            &lt;label for=\"status\">\u72b6\u614b&lt;\/label>\n            &lt;select id=\"status\">\n              &lt;option>\u958b\u767a\u4e2d&lt;\/option>\n              &lt;option>\u4f53\u9a13\u7248\u3042\u308a&lt;\/option>\n              &lt;option>\u516c\u958b\u4e2d&lt;\/option>\n              &lt;option>\u51cd\u7d50&lt;\/option>\n            &lt;\/select>\n          &lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"row\">\n          &lt;div class=\"field\">\n            &lt;label for=\"platform\">\u5bfe\u5fdc\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0&lt;\/label>\n            &lt;input id=\"platform\" type=\"text\" maxlength=\"60\" placeholder=\"\u4f8b\uff1aPC \/ Web \/ Quest \/ Android\" \/>\n          &lt;\/div>\n          &lt;div class=\"field\">\n            &lt;label for=\"genre\">\u30b8\u30e3\u30f3\u30eb&lt;\/label>\n            &lt;input id=\"genre\" type=\"text\" maxlength=\"60\" placeholder=\"\u4f8b\uff1a\u30a2\u30af\u30b7\u30e7\u30f3 \/ VR \/ \u30b5\u30d0\u30a4\u30d0\u30eb\" \/>\n          &lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"field\">\n          &lt;label for=\"tags\">\u30bf\u30b0\uff08\u30ab\u30f3\u30de\u533a\u5207\u308a\uff09&lt;\/label>\n          &lt;input id=\"tags\" type=\"text\" maxlength=\"120\" placeholder=\"\u4f8b\uff1aVR, \u63a2\u7d22, \u30c0\u30f3\u30b8\u30e7\u30f3, \u30dc\u30b9\u6226\" \/>\n        &lt;\/div>\n\n        &lt;div class=\"field\">\n          &lt;label for=\"desc\">\u8aac\u660e *&lt;\/label>\n          &lt;textarea id=\"desc\" maxlength=\"2000\" placeholder=\"\u4f5c\u54c1\u306e\u9b45\u529b\u3001\u904a\u3073\u65b9\u3001\u7279\u5fb4\u3001\u4eca\u5f8c\u306e\u4e88\u5b9a\u306a\u3069\">&lt;\/textarea>\n          &lt;div class=\"help\">\u6700\u59272000\u6587\u5b57\u3002\u9577\u3044\u5834\u5408\u306f\u8981\u70b9\u2192\u8a73\u7d30\u306e\u9806\u3067\u66f8\u304f\u3068\u5f37\u3044\u3002&lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"row\">\n          &lt;div class=\"field\">\n            &lt;label for=\"linkPlay\">\u30d7\u30ec\u30a4URL \/ \u516c\u958b\u30da\u30fc\u30b8&lt;\/label>\n            &lt;input id=\"linkPlay\" type=\"url\" placeholder=\"https:\/\/...\" \/>\n          &lt;\/div>\n          &lt;div class=\"field\">\n            &lt;label for=\"linkRepo\">GitHub \/ \u30ea\u30dd\u30b8\u30c8\u30ea&lt;\/label>\n            &lt;input id=\"linkRepo\" type=\"url\" placeholder=\"https:\/\/github.com\/...\" \/>\n          &lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"row\">\n          &lt;div class=\"field\">\n            &lt;label for=\"linkVideo\">\u52d5\u753bURL\uff08YouTube\u306a\u3069\uff09&lt;\/label>\n            &lt;input id=\"linkVideo\" type=\"url\" placeholder=\"https:\/\/www.youtube.com\/watch?v=...\" \/>\n          &lt;\/div>\n          &lt;div class=\"field\">\n            &lt;label for=\"rating\">\u81ea\u5df1\u8a55\u4fa1\uff081\u301c5\uff09&lt;\/label>\n            &lt;select id=\"rating\">\n              &lt;option value=\"0\">\u672a\u8a2d\u5b9a&lt;\/option>\n              &lt;option value=\"1\">\u26051&lt;\/option>\n              &lt;option value=\"2\">\u26052&lt;\/option>\n              &lt;option value=\"3\">\u26053&lt;\/option>\n              &lt;option value=\"4\">\u26054&lt;\/option>\n              &lt;option value=\"5\">\u26055&lt;\/option>\n            &lt;\/select>\n          &lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"field\">\n          &lt;label for=\"shots\">\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8\uff08\u8907\u6570\u53ef \/ \u81ea\u52d5\u3067\u30c7\u30fc\u30bf\u5316\u3057\u3066\u4fdd\u5b58\uff09&lt;\/label>\n          &lt;input id=\"shots\" type=\"file\" accept=\"image\/*\" multiple \/>\n          &lt;div class=\"help\">\n            \u753b\u50cf\u306f\u30d6\u30e9\u30a6\u30b6\u5185\u306b\u4fdd\u5b58\u3055\u308c\u307e\u3059\uff08\u5bb9\u91cf\u304c\u5927\u304d\u3044\u3068\u91cd\u304f\u306a\u308a\u307e\u3059\uff09\u3002&lt;br\/>\n            \u76ee\u5b89\uff1a1\u679a 500KB\u301c1MB\u7a0b\u5ea6\u306b\u5727\u7e2e\u3059\u308b\u3068\u5feb\u9069\u3002\n          &lt;\/div>\n        &lt;\/div>\n\n        &lt;div class=\"divider\">&lt;\/div>\n\n        &lt;div style=\"display:flex; gap:10px; flex-wrap:wrap; align-items:center\">\n          &lt;button class=\"btn primary\" id=\"btnSave\" type=\"button\">\u4fdd\u5b58&lt;\/button>\n          &lt;button class=\"btn\" id=\"btnDraft\" type=\"button\">\u4e0b\u66f8\u304d\u4fdd\u5b58&lt;\/button>\n          &lt;span class=\"help\" id=\"saveHelp\">* \u5fc5\u9808\uff1a\u30bf\u30a4\u30c8\u30eb \/ \u8aac\u660e&lt;\/span>\n        &lt;\/div>\n\n        &lt;div class=\"divider\">&lt;\/div>\n\n        &lt;div class=\"note\">\n          &lt;b>\u4fdd\u5b58\u65b9\u5f0f\uff1a&lt;\/b>\u3053\u306e\u30da\u30fc\u30b8\u306f &lt;span class=\"mono\">localStorage&lt;\/span> \u306b\u4fdd\u5b58\u3057\u307e\u3059\u3002&lt;br\/>\n          \u3064\u307e\u308a\u300c\u3042\u306a\u305f\u306e\u30d6\u30e9\u30a6\u30b6\u5185\u3060\u3051\u300d\u306b\u6b8b\u308a\u307e\u3059\u3002\u516c\u958b\u30b5\u30a4\u30c8\u3067\u904b\u7528\u3057\u305f\u3044\u306a\u3089\u3001\u6b21\u306f\u30b5\u30fc\u30d0\u30fc\u4fdd\u5b58\uff08PHP\/DB\uff09\u306b\u5207\u308a\u66ff\u3048\u308b\u3002\n        &lt;\/div>\n      &lt;\/div>\n    &lt;\/section>\n\n    &lt;!-- Right: \u4e00\u89a7 -->\n    &lt;main class=\"panel\" aria-label=\"\u4f5c\u54c1\u4e00\u89a7\">\n      &lt;div class=\"toolbar\">\n        &lt;input class=\"grow\" id=\"q\" type=\"search\" placeholder=\"\u691c\u7d22\uff1a\u30bf\u30a4\u30c8\u30eb\/\u8aac\u660e\/\u30bf\u30b0\/\u30a8\u30f3\u30b8\u30f3\/\u30b8\u30e3\u30f3\u30eb...\" \/>\n        &lt;select id=\"filterStatus\" title=\"\u72b6\u614b\">\n          &lt;option value=\"\">\u72b6\u614b\uff1a\u3059\u3079\u3066&lt;\/option>\n          &lt;option>\u958b\u767a\u4e2d&lt;\/option>\n          &lt;option>\u4f53\u9a13\u7248\u3042\u308a&lt;\/option>\n          &lt;option>\u516c\u958b\u4e2d&lt;\/option>\n          &lt;option>\u51cd\u7d50&lt;\/option>\n        &lt;\/select>\n        &lt;select id=\"sort\" title=\"\u4e26\u3073\u66ff\u3048\">\n          &lt;option value=\"new\">\u65b0\u3057\u3044\u9806&lt;\/option>\n          &lt;option value=\"old\">\u53e4\u3044\u9806&lt;\/option>\n          &lt;option value=\"likes\">\u3044\u3044\u306d\u9806&lt;\/option>\n          &lt;option value=\"rating\">\u8a55\u4fa1\u9806&lt;\/option>\n          &lt;option value=\"title\">\u30bf\u30a4\u30c8\u30eb\u9806&lt;\/option>\n        &lt;\/select>\n        &lt;button class=\"btn small good\" id=\"btnNew\" type=\"button\">\uff0b \u65b0\u898f\u6295\u7a3f&lt;\/button>\n      &lt;\/div>\n\n      &lt;div class=\"stats\" id=\"stats\">&lt;\/div>\n\n      &lt;div id=\"list\" class=\"grid\" aria-live=\"polite\">&lt;\/div>\n      &lt;div id=\"empty\" class=\"empty\" hidden>\n        \u307e\u3060\u4f5c\u54c1\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u5de6\u306e\u30d5\u30a9\u30fc\u30e0\u304b\u3089\u6295\u7a3f\u3057\u3066\u3001\u3042\u306a\u305f\u306e\u4f5c\u54c1\u96c6\u3092\u5b8c\u6210\u3055\u305b\u3088\u3046\u3002\n      &lt;\/div>\n    &lt;\/main>\n  &lt;\/div>\n\n  &lt;!-- \u8a73\u7d30\u30e2\u30fc\u30c0\u30eb -->\n  &lt;dialog id=\"modal\">\n    &lt;div class=\"modalHeader\">\n      &lt;h3 id=\"mTitle\">\u8a73\u7d30&lt;\/h3>\n      &lt;div style=\"display:flex; gap:8px; align-items:center\">\n        &lt;button class=\"btn small\" id=\"mEdit\" type=\"button\">\u7de8\u96c6&lt;\/button>\n        &lt;button class=\"btn small\" id=\"mLike\" type=\"button\">\u3044\u3044\u306d&lt;\/button>\n        &lt;button class=\"btn small danger\" id=\"mDelete\" type=\"button\">\u524a\u9664&lt;\/button>\n        &lt;button class=\"btn small ghost\" id=\"mClose\" type=\"button\">\u9589\u3058\u308b&lt;\/button>\n      &lt;\/div>\n    &lt;\/div>\n    &lt;div class=\"modalBody\">\n      &lt;div class=\"modalGrid\">\n        &lt;div>\n          &lt;div class=\"note\" id=\"mTagline\">&lt;\/div>\n          &lt;div class=\"divider\">&lt;\/div>\n\n          &lt;div class=\"gallery\" id=\"mGallery\">&lt;\/div>\n\n          &lt;div class=\"divider\">&lt;\/div>\n\n          &lt;div class=\"note\" id=\"mDesc\">&lt;\/div>\n\n          &lt;div class=\"divider\">&lt;\/div>\n\n          &lt;div class=\"panel\" style=\"background: rgba(10,12,18,.12); box-shadow:none\">\n            &lt;div class=\"panelHeader\" style=\"border-bottom:1px solid var(--line)\">\n              &lt;div>\n                &lt;h2 style=\"margin:0;font-size:14px\">\u30b3\u30e1\u30f3\u30c8&lt;\/h2>\n                &lt;div class=\"hint\">\u30ed\u30fc\u30ab\u30eb\u4fdd\u5b58\uff08\u3053\u306e\u30d6\u30e9\u30a6\u30b6\u3060\u3051\uff09&lt;\/div>\n              &lt;\/div>\n            &lt;\/div>\n            &lt;div class=\"panelBody\">\n              &lt;div class=\"field\">\n                &lt;label for=\"cName\">\u540d\u524d\uff08\u4efb\u610f\uff09&lt;\/label>\n                &lt;input id=\"cName\" type=\"text\" maxlength=\"40\" placeholder=\"\u4f8b\uff1aAnonymous\" \/>\n              &lt;\/div>\n              &lt;div class=\"field\">\n                &lt;label for=\"cText\">\u30b3\u30e1\u30f3\u30c8&lt;\/label>\n                &lt;textarea id=\"cText\" maxlength=\"400\" placeholder=\"\u611f\u60f3 \/ \u30d5\u30a3\u30fc\u30c9\u30d0\u30c3\u30af\">&lt;\/textarea>\n                &lt;div class=\"help\">\u6700\u5927400\u6587\u5b57&lt;\/div>\n              &lt;\/div>\n              &lt;div style=\"display:flex; gap:10px; align-items:center; flex-wrap:wrap\">\n                &lt;button class=\"btn good\" id=\"cAdd\" type=\"button\">\u30b3\u30e1\u30f3\u30c8\u8ffd\u52a0&lt;\/button>\n                &lt;span class=\"help\" id=\"cHint\">&lt;\/span>\n              &lt;\/div>\n              &lt;div class=\"divider\">&lt;\/div>\n              &lt;div id=\"cList\" style=\"display:flex;flex-direction:column;gap:10px\">&lt;\/div>\n            &lt;\/div>\n          &lt;\/div>\n        &lt;\/div>\n\n        &lt;div>\n          &lt;div class=\"note\">\n            &lt;div class=\"kvs\" id=\"mMeta\">&lt;\/div>\n          &lt;\/div>\n          &lt;div class=\"divider\">&lt;\/div>\n\n          &lt;div class=\"note\" id=\"mLinks\">&lt;\/div>\n\n          &lt;div class=\"divider\">&lt;\/div>\n\n          &lt;div class=\"note\" id=\"mSystem\">&lt;\/div>\n        &lt;\/div>\n      &lt;\/div>\n    &lt;\/div>\n  &lt;\/dialog>\n\n  &lt;div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\">&lt;\/div>\n\n  &lt;input id=\"importFile\" type=\"file\" accept=\"application\/json\" hidden \/>\n\n  &lt;script>\n    \"use strict\";\n\n    \/\/ ====== Storage Keys ======\n    const KEY = \"gameworks_posts_v1\";\n    const KEY_DRAFT = \"gameworks_draft_v1\";\n\n    \/\/ ====== DOM ======\n    const el = (id) => document.getElementById(id);\n\n    const chipCount = el(\"chipCount\");\n    const listEl = el(\"list\");\n    const emptyEl = el(\"empty\");\n    const statsEl = el(\"stats\");\n\n    const qEl = el(\"q\");\n    const filterStatusEl = el(\"filterStatus\");\n    const sortEl = el(\"sort\");\n\n    const formTitleEl = el(\"formTitle\");\n    const formHintEl = el(\"formHint\");\n\n    \/\/ form fields\n    const authorEl = el(\"author\");\n    const titleEl = el(\"title\");\n    const taglineEl = el(\"tagline\");\n    const engineEl = el(\"engine\");\n    const statusEl = el(\"status\");\n    const platformEl = el(\"platform\");\n    const genreEl = el(\"genre\");\n    const tagsEl = el(\"tags\");\n    const descEl = el(\"desc\");\n    const linkPlayEl = el(\"linkPlay\");\n    const linkRepoEl = el(\"linkRepo\");\n    const linkVideoEl = el(\"linkVideo\");\n    const ratingEl = el(\"rating\");\n    const shotsEl = el(\"shots\");\n\n    const btnSave = el(\"btnSave\");\n    const btnDraft = el(\"btnDraft\");\n    const btnResetForm = el(\"btnResetForm\");\n\n    const btnSeed = el(\"btnSeed\");\n    const btnExport = el(\"btnExport\");\n    const btnImport = el(\"btnImport\");\n    const btnWipe = el(\"btnWipe\");\n    const btnNew = el(\"btnNew\");\n\n    const toastEl = el(\"toast\");\n    const importFileEl = el(\"importFile\");\n\n    \/\/ modal\n    const modal = el(\"modal\");\n    const mTitle = el(\"mTitle\");\n    const mTagline = el(\"mTagline\");\n    const mGallery = el(\"mGallery\");\n    const mDesc = el(\"mDesc\");\n    const mMeta = el(\"mMeta\");\n    const mLinks = el(\"mLinks\");\n    const mSystem = el(\"mSystem\");\n\n    const mEdit = el(\"mEdit\");\n    const mLike = el(\"mLike\");\n    const mDelete = el(\"mDelete\");\n    const mClose = el(\"mClose\");\n\n    \/\/ comments\n    const cName = el(\"cName\");\n    const cText = el(\"cText\");\n    const cAdd = el(\"cAdd\");\n    const cHint = el(\"cHint\");\n    const cList = el(\"cList\");\n\n    \/\/ ====== State ======\n    let posts = loadPosts();\n    let editingId = null;\n    let pendingImages = &#91;]; \/\/ base64 array for current form\n    let currentModalId = null;\n\n    \/\/ ====== Utils ======\n    const nowISO = () => new Date().toISOString();\n    const uid = () => \"p_\" + Math.random().toString(16).slice(2) + Date.now().toString(16);\n\n    function toast(msg){\n      toastEl.textContent = msg;\n      toastEl.classList.add(\"show\");\n      clearTimeout(toastEl._t);\n      toastEl._t = setTimeout(()=> toastEl.classList.remove(\"show\"), 1700);\n    }\n\n    function safeText(str){\n      \/\/ basic escape for text rendering into HTML\n      return String(str ?? \"\")\n        .replaceAll(\"&amp;\",\"&amp;amp;\")\n        .replaceAll(\"&lt;\",\"&amp;lt;\")\n        .replaceAll(\">\",\"&amp;gt;\")\n        .replaceAll('\"',\"&amp;quot;\")\n        .replaceAll(\"'\",\"&amp;#39;\");\n    }\n\n    function normalizeTags(input){\n      return String(input ?? \"\")\n        .split(\",\")\n        .map(s => s.trim())\n        .filter(Boolean)\n        .slice(0, 20);\n    }\n\n    function stars(n){\n      const v = Number(n || 0);\n      if(!v) return \"\u672a\u8a2d\u5b9a\";\n      return \"\u2605\".repeat(v) + \"\u2606\".repeat(5 - v);\n    }\n\n    function fmtDate(iso){\n      try{\n        const d = new Date(iso);\n        const y = d.getFullYear();\n        const m = String(d.getMonth()+1).padStart(2,\"0\");\n        const dd = String(d.getDate()).padStart(2,\"0\");\n        const hh = String(d.getHours()).padStart(2,\"0\");\n        const mm = String(d.getMinutes()).padStart(2,\"0\");\n        return `${y}\/${m}\/${dd} ${hh}:${mm}`;\n      }catch{\n        return String(iso);\n      }\n    }\n\n    function loadPosts(){\n      try{\n        const raw = localStorage.getItem(KEY);\n        const arr = raw ? JSON.parse(raw) : &#91;];\n        return Array.isArray(arr) ? arr : &#91;];\n      }catch{\n        return &#91;];\n      }\n    }\n\n    function savePosts(){\n      localStorage.setItem(KEY, JSON.stringify(posts));\n      chipCount.textContent = `\u4f5c\u54c1: ${posts.length}`;\n    }\n\n    function loadDraft(){\n      try{\n        const raw = localStorage.getItem(KEY_DRAFT);\n        return raw ? JSON.parse(raw) : null;\n      }catch{\n        return null;\n      }\n    }\n\n    function saveDraft(draft){\n      localStorage.setItem(KEY_DRAFT, JSON.stringify(draft));\n    }\n\n    function clearDraft(){\n      localStorage.removeItem(KEY_DRAFT);\n    }\n\n    function bytesApprox(){\n      \/\/ rough localStorage usage for our key\n      const raw = localStorage.getItem(KEY) || \"\";\n      return raw.length;\n    }\n\n    function buildStats(filteredCount){\n      const likeSum = posts.reduce((a,p)=> a + (p.likes||0), 0);\n      const bytes = bytesApprox();\n      const mb = (bytes \/ (1024*1024)).toFixed(2);\n      statsEl.innerHTML = `\n        &lt;span class=\"chip\">\u8868\u793a: &lt;b>${filteredCount}&lt;\/b>&lt;\/span>\n        &lt;span class=\"chip\">\u7dcf\u3044\u3044\u306d: &lt;b>${likeSum}&lt;\/b>&lt;\/span>\n        &lt;span class=\"chip\">\u4fdd\u5b58\u30b5\u30a4\u30ba\u76ee\u5b89: &lt;b>${mb} MB&lt;\/b>&lt;\/span>\n      `;\n    }\n\n    \/\/ ====== Image handling (compress to JPEG) ======\n    async function fileToDataUrlCompressed(file, maxW=1280, quality=0.82){\n      const img = await new Promise((res, rej)=>{\n        const i = new Image();\n        i.onload = ()=> res(i);\n        i.onerror = rej;\n        i.src = URL.createObjectURL(file);\n      });\n\n      const scale = Math.min(1, maxW \/ img.width);\n      const w = Math.round(img.width * scale);\n      const h = Math.round(img.height * scale);\n\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = w; canvas.height = h;\n      const ctx = canvas.getContext(\"2d\");\n      ctx.drawImage(img, 0, 0, w, h);\n\n      URL.revokeObjectURL(img.src);\n\n      return canvas.toDataURL(\"image\/jpeg\", quality);\n    }\n\n    shotsEl.addEventListener(\"change\", async () => {\n      const files = Array.from(shotsEl.files || &#91;]);\n      if(!files.length) return;\n\n      toast(\"\u753b\u50cf\u3092\u51e6\u7406\u4e2d\u2026\");\n      const max = 9;\n      const slice = files.slice(0, max);\n\n      const out = &#91;];\n      for(const f of slice){\n        if(!f.type.startsWith(\"image\/\")) continue;\n        try{\n          const d = await fileToDataUrlCompressed(f);\n          out.push(d);\n        }catch{}\n      }\n      pendingImages = out;\n      toast(`\u30b9\u30af\u30b7\u30e7 ${pendingImages.length}\u679a \u6e96\u5099OK`);\n    });\n\n    \/\/ ====== Render ======\n    function getFiltered(){\n      const q = (qEl.value || \"\").trim().toLowerCase();\n      const st = filterStatusEl.value || \"\";\n\n      let arr = &#91;...posts];\n\n      if(q){\n        arr = arr.filter(p=>{\n          const blob = &#91;\n            p.title, p.tagline, p.desc, p.engine, p.platform, p.genre,\n            ...(p.tags||&#91;]),\n          ].join(\" \").toLowerCase();\n          return blob.includes(q);\n        });\n      }\n      if(st){\n        arr = arr.filter(p => (p.status || \"\") === st);\n      }\n\n      \/\/ sort\n      const s = sortEl.value;\n      arr.sort((a,b)=>{\n        if(s === \"new\") return (b.createdAt||\"\").localeCompare(a.createdAt||\"\");\n        if(s === \"old\") return (a.createdAt||\"\").localeCompare(b.createdAt||\"\");\n        if(s === \"likes\") return (b.likes||0) - (a.likes||0);\n        if(s === \"rating\") return (Number(b.rating||0) - Number(a.rating||0)) || (b.likes||0)-(a.likes||0);\n        if(s === \"title\") return (a.title||\"\").localeCompare(b.title||\"\", \"ja\");\n        return 0;\n      });\n\n      return arr;\n    }\n\n    function statusBadgeClass(status){\n      if(status === \"\u516c\u958b\u4e2d\") return \"good\";\n      if(status === \"\u4f53\u9a13\u7248\u3042\u308a\") return \"warn\";\n      return \"\";\n    }\n\n    function render(){\n      savePosts(); \/\/ also updates chipCount\n      const arr = getFiltered();\n      buildStats(arr.length);\n\n      listEl.innerHTML = \"\";\n      emptyEl.hidden = arr.length !== 0;\n\n      for(const p of arr){\n        const cover = (p.images &amp;&amp; p.images&#91;0]) ? `&lt;img alt=\"cover\" src=\"${p.images&#91;0]}\">` : \"\";\n        const tags = (p.tags||&#91;]).slice(0,3).map(t=>`&lt;span class=\"pill\">#${safeText(t)}&lt;\/span>`).join(\"\");\n        const engine = p.engine ? `&lt;span class=\"pill\">${safeText(p.engine)}&lt;\/span>` : \"\";\n        const genre = p.genre ? `&lt;span class=\"pill\">${safeText(p.genre)}&lt;\/span>` : \"\";\n        const rating = Number(p.rating||0) ? `&lt;span class=\"pill\">${safeText(stars(p.rating))}&lt;\/span>` : \"\";\n\n        const card = document.createElement(\"article\");\n        card.className = \"card\";\n        card.innerHTML = `\n          &lt;div class=\"thumb\">\n            ${cover || `&lt;div class=\"muted\">No Image&lt;\/div>`}\n            &lt;div class=\"badgeRow\">\n              &lt;span class=\"badge ${statusBadgeClass(p.status)}\">${safeText(p.status || \"\u672a\u8a2d\u5b9a\")}&lt;\/span>\n              ${p.platform ? `&lt;span class=\"badge muted\">${safeText(p.platform)}&lt;\/span>` : \"\"}\n            &lt;\/div>\n          &lt;\/div>\n          &lt;div class=\"cardBody\">\n            &lt;div class=\"titleRow\">\n              &lt;div style=\"min-width:0\">\n                &lt;h3 title=\"${safeText(p.title)}\">${safeText(p.title)}&lt;\/h3>\n                &lt;p class=\"tagline\">${safeText(p.tagline || \"\u2014\")}&lt;\/p>\n              &lt;\/div>\n              &lt;div class=\"muted\" style=\"font-size:12px; white-space:nowrap\">\n                \u2764 ${p.likes||0}\n              &lt;\/div>\n            &lt;\/div>\n            &lt;div class=\"meta\">\n              ${engine}\n              ${genre}\n              ${rating}\n              ${tags}\n            &lt;\/div>\n            &lt;div class=\"muted\" style=\"font-size:12px; line-height:1.55; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden\">\n              ${safeText(p.desc || \"\")}\n            &lt;\/div>\n          &lt;\/div>\n          &lt;div class=\"cardFooter\">\n            &lt;div class=\"muted\" style=\"font-size:12px\">\u66f4\u65b0: ${safeText(fmtDate(p.updatedAt || p.createdAt))}&lt;\/div>\n            &lt;div class=\"actions\">\n              &lt;button class=\"btn small\" data-act=\"open\" data-id=\"${p.id}\">\u8a73\u7d30&lt;\/button>\n              &lt;button class=\"btn small good\" data-act=\"like\" data-id=\"${p.id}\">\u3044\u3044\u306d&lt;\/button>\n            &lt;\/div>\n          &lt;\/div>\n        `;\n        listEl.appendChild(card);\n      }\n    }\n\n    listEl.addEventListener(\"click\", (e)=>{\n      const btn = e.target.closest(\"button\");\n      if(!btn) return;\n      const id = btn.getAttribute(\"data-id\");\n      const act = btn.getAttribute(\"data-act\");\n      if(!id || !act) return;\n\n      if(act === \"open\") openModal(id);\n      if(act === \"like\") { likePost(id); render(); }\n    });\n\n    \/\/ ====== CRUD ======\n    function validateForm(){\n      const title = titleEl.value.trim();\n      const desc = descEl.value.trim();\n      if(!title || !desc){\n        toast(\"\u30bf\u30a4\u30c8\u30eb\u3068\u8aac\u660e\u306f\u5fc5\u9808\");\n        return false;\n      }\n      return true;\n    }\n\n    function readFormAsPost(){\n      const tags = normalizeTags(tagsEl.value);\n      return {\n        id: editingId || uid(),\n        author: authorEl.value.trim(),\n        title: titleEl.value.trim(),\n        tagline: taglineEl.value.trim(),\n        engine: engineEl.value,\n        status: statusEl.value,\n        platform: platformEl.value.trim(),\n        genre: genreEl.value.trim(),\n        tags,\n        desc: descEl.value.trim(),\n        linkPlay: linkPlayEl.value.trim(),\n        linkRepo: linkRepoEl.value.trim(),\n        linkVideo: linkVideoEl.value.trim(),\n        rating: Number(ratingEl.value || 0),\n        images: (pendingImages &amp;&amp; pendingImages.length) ? pendingImages : &#91;],\n        likes: 0,\n        comments: &#91;],\n        createdAt: nowISO(),\n        updatedAt: nowISO()\n      };\n    }\n\n    function resetForm(keepDraft=false){\n      editingId = null;\n      pendingImages = &#91;];\n      if(!keepDraft){\n        authorEl.value = \"\";\n        titleEl.value = \"\";\n        taglineEl.value = \"\";\n        engineEl.value = \"\";\n        statusEl.value = \"\u958b\u767a\u4e2d\";\n        platformEl.value = \"\";\n        genreEl.value = \"\";\n        tagsEl.value = \"\";\n        descEl.value = \"\";\n        linkPlayEl.value = \"\";\n        linkRepoEl.value = \"\";\n        linkVideoEl.value = \"\";\n        ratingEl.value = \"0\";\n        shotsEl.value = \"\";\n      }\n      formTitleEl.textContent = \"\u65b0\u898f\u6295\u7a3f\";\n      formHintEl.textContent = \"\u4f5c\u54c1\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u4fdd\u5b58\";\n      btnSave.textContent = \"\u4fdd\u5b58\";\n    }\n\n    function setFormFromPost(p){\n      editingId = p.id;\n      pendingImages = Array.isArray(p.images) ? &#91;...p.images] : &#91;];\n\n      authorEl.value = p.author || \"\";\n      titleEl.value = p.title || \"\";\n      taglineEl.value = p.tagline || \"\";\n      engineEl.value = p.engine || \"\";\n      statusEl.value = p.status || \"\u958b\u767a\u4e2d\";\n      platformEl.value = p.platform || \"\";\n      genreEl.value = p.genre || \"\";\n      tagsEl.value = (p.tags || &#91;]).join(\", \");\n      descEl.value = p.desc || \"\";\n      linkPlayEl.value = p.linkPlay || \"\";\n      linkRepoEl.value = p.linkRepo || \"\";\n      linkVideoEl.value = p.linkVideo || \"\";\n      ratingEl.value = String(p.rating || 0);\n      shotsEl.value = \"\";\n\n      formTitleEl.textContent = \"\u7de8\u96c6\";\n      formHintEl.textContent = \"\u5185\u5bb9\u3092\u66f4\u65b0\u3057\u3066\u4fdd\u5b58\";\n      btnSave.textContent = \"\u66f4\u65b0\";\n      toast(\"\u7de8\u96c6\u30e2\u30fc\u30c9\");\n      window.scrollTo({top:0, behavior:\"smooth\"});\n    }\n\n    function upsertPost(p){\n      const idx = posts.findIndex(x=>x.id===p.id);\n      if(idx >= 0){\n        const prev = posts&#91;idx];\n        posts&#91;idx] = {\n          ...prev,\n          ...p,\n          likes: prev.likes || 0,\n          comments: Array.isArray(prev.comments) ? prev.comments : &#91;],\n          createdAt: prev.createdAt || nowISO(),\n          updatedAt: nowISO()\n        };\n      }else{\n        posts.unshift(p);\n      }\n      savePosts();\n    }\n\n    btnSave.addEventListener(\"click\", ()=>{\n      if(!validateForm()) return;\n\n      const p = readFormAsPost();\n      \/\/ if editing, keep previous likes\/comments\/createdAt\n      if(editingId){\n        const prev = posts.find(x=>x.id===editingId);\n        if(prev){\n          p.likes = prev.likes || 0;\n          p.comments = Array.isArray(prev.comments) ? prev.comments : &#91;];\n          p.createdAt = prev.createdAt || nowISO();\n          p.updatedAt = nowISO();\n          \/\/ if user did not reselect images, keep old images\n          if(!pendingImages.length &amp;&amp; Array.isArray(prev.images)) p.images = prev.images;\n        }\n      }\n\n      upsertPost(p);\n      clearDraft();\n      resetForm();\n      render();\n      toast(editingId ? \"\u66f4\u65b0\u3057\u305f\" : \"\u4fdd\u5b58\u3057\u305f\");\n    });\n\n    btnDraft.addEventListener(\"click\", ()=>{\n      const draft = {\n        author: authorEl.value,\n        title: titleEl.value,\n        tagline: taglineEl.value,\n        engine: engineEl.value,\n        status: statusEl.value,\n        platform: platformEl.value,\n        genre: genreEl.value,\n        tags: tagsEl.value,\n        desc: descEl.value,\n        linkPlay: linkPlayEl.value,\n        linkRepo: linkRepoEl.value,\n        linkVideo: linkVideoEl.value,\n        rating: ratingEl.value,\n        images: pendingImages\n      };\n      saveDraft(draft);\n      toast(\"\u4e0b\u66f8\u304d\u3092\u4fdd\u5b58\u3057\u305f\");\n    });\n\n    btnResetForm.addEventListener(\"click\", ()=>{\n      resetForm();\n      toast(\"\u30d5\u30a9\u30fc\u30e0\u3092\u30ea\u30bb\u30c3\u30c8\");\n    });\n\n    btnNew.addEventListener(\"click\", ()=>{\n      resetForm();\n      toast(\"\u65b0\u898f\u6295\u7a3f\u30e2\u30fc\u30c9\");\n      window.scrollTo({top:0, behavior:\"smooth\"});\n    });\n\n    \/\/ ====== Like \/ Delete ======\n    function likePost(id){\n      const p = posts.find(x=>x.id===id);\n      if(!p) return;\n      p.likes = (p.likes||0) + 1;\n      p.updatedAt = nowISO();\n      savePosts();\n      toast(\"\u3044\u3044\u306d +1\");\n      if(currentModalId === id) refreshModal();\n    }\n\n    function deletePost(id){\n      const idx = posts.findIndex(x=>x.id===id);\n      if(idx &lt; 0) return;\n      posts.splice(idx, 1);\n      savePosts();\n      toast(\"\u524a\u9664\u3057\u305f\");\n    }\n\n    \/\/ ====== Modal ======\n    function openModal(id){\n      const p = posts.find(x=>x.id===id);\n      if(!p) return;\n      currentModalId = id;\n      \/\/ count view? (optional)\n      modal.showModal();\n      refreshModal();\n    }\n\n    function refreshModal(){\n      const p = posts.find(x=>x.id===currentModalId);\n      if(!p) return;\n\n      mTitle.textContent = p.title || \"\u8a73\u7d30\";\n      mTagline.innerHTML = `&lt;b>${safeText(p.tagline || \"\u2014\")}&lt;\/b>&lt;br>&lt;span class=\"muted\">\u2764 ${p.likes||0} \/ \u8a55\u4fa1: ${safeText(stars(p.rating))}&lt;\/span>`;\n\n      \/\/ gallery\n      mGallery.innerHTML = \"\";\n      const imgs = Array.isArray(p.images) ? p.images : &#91;];\n      if(imgs.length){\n        for(const src of imgs){\n          const div = document.createElement(\"div\");\n          div.className = \"gimg\";\n          div.innerHTML = `&lt;img alt=\"screenshot\" src=\"${src}\">`;\n          mGallery.appendChild(div);\n        }\n      }else{\n        mGallery.innerHTML = `&lt;div class=\"note\">\u30b9\u30af\u30b7\u30e7\u306a\u3057&lt;\/div>`;\n      }\n\n      mDesc.innerHTML = safeText(p.desc || \"\").replaceAll(\"\\n\",\"&lt;br>\");\n\n      \/\/ meta\n      const t = (p.tags||&#91;]).map(x=>`#${x}`).join(\" \");\n      mMeta.innerHTML = `\n        &lt;b>\u6295\u7a3f\u8005&lt;\/b>&lt;div>${safeText(p.author || \"\u2014\")}&lt;\/div>\n        &lt;b>\u72b6\u614b&lt;\/b>&lt;div>${safeText(p.status || \"\u2014\")}&lt;\/div>\n        &lt;b>\u30a8\u30f3\u30b8\u30f3&lt;\/b>&lt;div>${safeText(p.engine || \"\u2014\")}&lt;\/div>\n        &lt;b>\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0&lt;\/b>&lt;div>${safeText(p.platform || \"\u2014\")}&lt;\/div>\n        &lt;b>\u30b8\u30e3\u30f3\u30eb&lt;\/b>&lt;div>${safeText(p.genre || \"\u2014\")}&lt;\/div>\n        &lt;b>\u30bf\u30b0&lt;\/b>&lt;div>${safeText(t || \"\u2014\")}&lt;\/div>\n        &lt;b>\u4f5c\u6210&lt;\/b>&lt;div>${safeText(fmtDate(p.createdAt))}&lt;\/div>\n        &lt;b>\u66f4\u65b0&lt;\/b>&lt;div>${safeText(fmtDate(p.updatedAt || p.createdAt))}&lt;\/div>\n      `;\n\n      \/\/ links\n      const links = &#91;];\n      if(p.linkPlay) links.push(`&lt;a class=\"smallLink\" href=\"${safeText(p.linkPlay)}\" target=\"_blank\" rel=\"noopener\">\u25b6 \u516c\u958b\/\u30d7\u30ec\u30a4\u30da\u30fc\u30b8&lt;\/a>`);\n      if(p.linkRepo) links.push(`&lt;a class=\"smallLink\" href=\"${safeText(p.linkRepo)}\" target=\"_blank\" rel=\"noopener\">\u2302 \u30ea\u30dd\u30b8\u30c8\u30ea&lt;\/a>`);\n      if(p.linkVideo) links.push(`&lt;a class=\"smallLink\" href=\"${safeText(p.linkVideo)}\" target=\"_blank\" rel=\"noopener\">\ud83c\udfac \u52d5\u753b&lt;\/a>`);\n      mLinks.innerHTML = links.length ? links.join(\"&lt;br>\") : `&lt;span class=\"muted\">\u30ea\u30f3\u30af\u306a\u3057&lt;\/span>`;\n\n      mSystem.innerHTML = `\n        &lt;b>\u64cd\u4f5c&lt;\/b>&lt;br>\n        \u30fb\u7de8\u96c6\u3067\u5de6\u30d5\u30a9\u30fc\u30e0\u306b\u8aad\u307f\u8fbc\u307f&lt;br>\n        \u30fb\u3044\u3044\u306d\u306f\u30ed\u30fc\u30ab\u30eb\u30ab\u30a6\u30f3\u30c8&lt;br>\n        \u30fb\u524a\u9664\u306f\u53d6\u308a\u6d88\u3057\u4e0d\u53ef\n      `;\n\n      \/\/ buttons\n      mLike.textContent = `\u3044\u3044\u306d (${p.likes||0})`;\n\n      renderComments(p);\n    }\n\n    mClose.addEventListener(\"click\", ()=> modal.close());\n    modal.addEventListener(\"click\", (e)=>{\n      const rect = modal.getBoundingClientRect();\n      const inDialog = (\n        rect.top &lt;= e.clientY &amp;&amp; e.clientY &lt;= rect.top + rect.height &amp;&amp;\n        rect.left &lt;= e.clientX &amp;&amp; e.clientX &lt;= rect.left + rect.width\n      );\n      if(!inDialog) modal.close();\n    });\n\n    mLike.addEventListener(\"click\", ()=>{\n      if(!currentModalId) return;\n      likePost(currentModalId);\n      render();\n    });\n\n    mDelete.addEventListener(\"click\", ()=>{\n      if(!currentModalId) return;\n      const p = posts.find(x=>x.id===currentModalId);\n      if(!p) return;\n      const ok = confirm(`\u300c${p.title}\u300d\u3092\u524a\u9664\u3057\u307e\u3059\u3002\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f`);\n      if(!ok) return;\n      deletePost(currentModalId);\n      modal.close();\n      currentModalId = null;\n      render();\n    });\n\n    mEdit.addEventListener(\"click\", ()=>{\n      const p = posts.find(x=>x.id===currentModalId);\n      if(!p) return;\n      setFormFromPost(p);\n      modal.close();\n    });\n\n    \/\/ ====== Comments ======\n    function renderComments(p){\n      const arr = Array.isArray(p.comments) ? p.comments : &#91;];\n      cHint.textContent = `\u30b3\u30e1\u30f3\u30c8\u6570: ${arr.length}`;\n\n      cList.innerHTML = \"\";\n      if(!arr.length){\n        cList.innerHTML = `&lt;div class=\"note\">\u307e\u3060\u30b3\u30e1\u30f3\u30c8\u306f\u3042\u308a\u307e\u305b\u3093\u3002&lt;\/div>`;\n        return;\n      }\n      for(const c of arr.slice().reverse()){\n        const box = document.createElement(\"div\");\n        box.className = \"note\";\n        box.innerHTML = `\n          &lt;b>${safeText(c.name || \"Anonymous\")}&lt;\/b>\n          &lt;span class=\"muted\"> \/ ${safeText(fmtDate(c.at))}&lt;\/span>&lt;br>\n          ${safeText(c.text || \"\").replaceAll(\"\\n\",\"&lt;br>\")}\n        `;\n        cList.appendChild(box);\n      }\n    }\n\n    cAdd.addEventListener(\"click\", ()=>{\n      const p = posts.find(x=>x.id===currentModalId);\n      if(!p) return;\n\n      const text = (cText.value || \"\").trim();\n      if(!text){\n        toast(\"\u30b3\u30e1\u30f3\u30c8\u3092\u5165\u529b\");\n        return;\n      }\n      const name = (cName.value || \"\").trim();\n\n      p.comments = Array.isArray(p.comments) ? p.comments : &#91;];\n      p.comments.push({ name, text, at: nowISO() });\n      p.updatedAt = nowISO();\n      savePosts();\n      cText.value = \"\";\n      toast(\"\u30b3\u30e1\u30f3\u30c8\u8ffd\u52a0\");\n      refreshModal();\n      render();\n    });\n\n    \/\/ ====== Search \/ filter \/ sort ======\n    &#91;qEl, filterStatusEl, sortEl].forEach(x => x.addEventListener(\"input\", render));\n\n    \/\/ ====== Export \/ Import ======\n    btnExport.addEventListener(\"click\", ()=>{\n      const payload = {\n        version: 1,\n        exportedAt: nowISO(),\n        posts\n      };\n      const blob = new Blob(&#91;JSON.stringify(payload, null, 2)], {type:\"application\/json\"});\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = `gameworks_export_${new Date().toISOString().slice(0,10)}.json`;\n      a.click();\n      URL.revokeObjectURL(url);\n      toast(\"\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u3057\u305f\");\n    });\n\n    btnImport.addEventListener(\"click\", ()=>{\n      importFileEl.value = \"\";\n      importFileEl.click();\n    });\n\n    importFileEl.addEventListener(\"change\", async ()=>{\n      const file = importFileEl.files &amp;&amp; importFileEl.files&#91;0];\n      if(!file) return;\n\n      try{\n        const text = await file.text();\n        const data = JSON.parse(text);\n        const arr = data &amp;&amp; data.posts;\n        if(!Array.isArray(arr)) throw new Error(\"invalid\");\n        \/\/ merge (by id)\n        const map = new Map(posts.map(p=>&#91;p.id, p]));\n        for(const p of arr){\n          if(!p || !p.id) continue;\n          const prev = map.get(p.id);\n          map.set(p.id, prev ? ({...prev, ...p}) : p);\n        }\n        posts = Array.from(map.values());\n        \/\/ normalize\n        posts = posts.map(p=>({\n          id: p.id || uid(),\n          author: p.author || \"\",\n          title: p.title || \"Untitled\",\n          tagline: p.tagline || \"\",\n          engine: p.engine || \"\",\n          status: p.status || \"\u958b\u767a\u4e2d\",\n          platform: p.platform || \"\",\n          genre: p.genre || \"\",\n          tags: Array.isArray(p.tags) ? p.tags : normalizeTags(p.tags),\n          desc: p.desc || \"\",\n          linkPlay: p.linkPlay || \"\",\n          linkRepo: p.linkRepo || \"\",\n          linkVideo: p.linkVideo || \"\",\n          rating: Number(p.rating || 0),\n          images: Array.isArray(p.images) ? p.images : &#91;],\n          likes: Number(p.likes || 0),\n          comments: Array.isArray(p.comments) ? p.comments : &#91;],\n          createdAt: p.createdAt || nowISO(),\n          updatedAt: p.updatedAt || p.createdAt || nowISO()\n        }));\n\n        savePosts();\n        render();\n        toast(\"\u30a4\u30f3\u30dd\u30fc\u30c8\u5b8c\u4e86\");\n      }catch{\n        toast(\"\u30a4\u30f3\u30dd\u30fc\u30c8\u5931\u6557\uff08JSON\u5f62\u5f0f\u3092\u78ba\u8a8d\uff09\");\n      }\n    });\n\n    \/\/ ====== Wipe ======\n    btnWipe.addEventListener(\"click\", ()=>{\n      const ok = confirm(\"\u5168\u4f5c\u54c1\u30c7\u30fc\u30bf\u3092\u524a\u9664\u3057\u307e\u3059\u3002\u53d6\u308a\u6d88\u3057\u3067\u304d\u307e\u305b\u3093\u3002\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f\");\n      if(!ok) return;\n      posts = &#91;];\n      savePosts();\n      render();\n      resetForm();\n      clearDraft();\n      toast(\"\u5168\u524a\u9664\u3057\u305f\");\n    });\n\n    \/\/ ====== Seed ======\n    btnSeed.addEventListener(\"click\", ()=>{\n      const seed = &#91;\n        {\n          id: uid(),\n          author: \"Yuhei\",\n          title: \"KnightSurvivors\",\n          tagline: \"\u77ed\u6642\u9593\u3067\u71b1\u304f\u306a\u308c\u308b\u30b5\u30d0\u30a4\u30d0\u30eb\u30a2\u30af\u30b7\u30e7\u30f3\",\n          engine: \"Unity\",\n          status: \"\u4f53\u9a13\u7248\u3042\u308a\",\n          platform: \"PC \/ Web\",\n          genre: \"\u30b5\u30d0\u30a4\u30d0\u30eb \/ \u30a2\u30af\u30b7\u30e7\u30f3\",\n          tags: &#91;\"\u30b5\u30d0\u30a4\u30d0\u30eb\",\"\u723d\u5feb\",\"\u30ed\u30fc\u30b0\u30e9\u30a4\u30c8\"],\n          desc: \"\u6575\u306e\u6ce2\u3092\u3055\u3070\u304d\u3001\u30d3\u30eb\u30c9\u3092\u7d44\u307f\u66ff\u3048\u3066\u6700\u9069\u89e3\u3092\u63a2\u3059\u3002\\n\u77ed\u6642\u9593\u3067\u3082\u6c17\u6301\u3061\u3088\u304f\u7d42\u308f\u308c\u308b\u30c6\u30f3\u30dd\u3092\u610f\u8b58\u3002\",\n          linkPlay: \"\",\n          linkRepo: \"\",\n          linkVideo: \"\",\n          rating: 4,\n          images: &#91;],\n          likes: 12,\n          comments: &#91;],\n          createdAt: nowISO(),\n          updatedAt: nowISO()\n        },\n        {\n          id: uid(),\n          author: \"Yuhei\",\n          title: \"Elder Chronicle VR\",\n          tagline: \"\u63a2\u7d22\u30fb\u30af\u30a8\u30b9\u30c8\u30fb\u6226\u95d8\u30921\u3064\u306b\u307e\u3068\u3081\u305fVR\u4e16\u754c\",\n          engine: \"A-Frame\",\n          status: \"\u958b\u767a\u4e2d\",\n          platform: \"Quest \/ WebXR\",\n          genre: \"VR \/ RPG\",\n          tags: &#91;\"VR\",\"RPG\",\"\u30c0\u30f3\u30b8\u30e7\u30f3\",\"\u30af\u30a8\u30b9\u30c8\"],\n          desc: \"\u5834\u6240\u79fb\u52d5\u30fb\u30af\u30a8\u30b9\u30c8\u53d7\u6ce8\u30fb\u6226\u95d8\u306e\u30eb\u30fc\u30d7\u3092\u78e8\u3044\u3066\u3044\u304f\u3002\\nUI\u3068\u4e16\u754c\u89b3\u306e\u4e00\u4f53\u611f\u3092\u6700\u512a\u5148\u3002\",\n          linkPlay: \"\",\n          linkRepo: \"\",\n          linkVideo: \"\",\n          rating: 5,\n          images: &#91;],\n          likes: 30,\n          comments: &#91;],\n          createdAt: nowISO(),\n          updatedAt: nowISO()\n        }\n      ];\n      posts = &#91;...seed, ...posts];\n      savePosts();\n      render();\n      toast(\"\u30b5\u30f3\u30d7\u30eb\u3092\u8ffd\u52a0\u3057\u305f\");\n    });\n\n    \/\/ ====== Load draft on start ======\n    (function init(){\n      chipCount.textContent = `\u4f5c\u54c1: ${posts.length}`;\n\n      const draft = loadDraft();\n      if(draft){\n        authorEl.value = draft.author || \"\";\n        titleEl.value = draft.title || \"\";\n        taglineEl.value = draft.tagline || \"\";\n        engineEl.value = draft.engine || \"\";\n        statusEl.value = draft.status || \"\u958b\u767a\u4e2d\";\n        platformEl.value = draft.platform || \"\";\n        genreEl.value = draft.genre || \"\";\n        tagsEl.value = draft.tags || \"\";\n        descEl.value = draft.desc || \"\";\n        linkPlayEl.value = draft.linkPlay || \"\";\n        linkRepoEl.value = draft.linkRepo || \"\";\n        linkVideoEl.value = draft.linkVideo || \"\";\n        ratingEl.value = String(draft.rating || 0);\n        pendingImages = Array.isArray(draft.images) ? draft.images : &#91;];\n        formHintEl.textContent = \"\u4e0b\u66f8\u304d\u3092\u5fa9\u5143\u3057\u307e\u3057\u305f\uff08\u4fdd\u5b58\u3092\u62bc\u3059\u3068\u6295\u7a3f\u306b\u306a\u308a\u307e\u3059\uff09\";\n        toast(\"\u4e0b\u66f8\u304d\u3092\u5fa9\u5143\");\n      }\n\n      render();\n    })();\n  &lt;\/script>\n&lt;\/body>\n&lt;\/html>\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"footnotes":""},"categories":[87],"tags":[],"class_list":["post-26239","post","type-post","status-publish","format-standard","hentry","category-web"],"aioseo_notices":[],"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/26239","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=26239"}],"version-history":[{"count":1,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/26239\/revisions"}],"predecessor-version":[{"id":26240,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/26239\/revisions\/26240"}],"wp:attachment":[{"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=26239"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=26239"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.tyosuke20xx.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=26239"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}