RSS一覧


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Tsumugi</title>

  <!-- ✅ Favicon(初期): ブラウザ互換のため複数relで用意(確実版) -->
  <!-- ★ここに貼る★ -->
  <link rel="icon" type="image/svg+xml"
  href="data:image/svg+xml,
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
  <defs>
  <linearGradient id='g' x1='0' y1='0' x2='1' y2='1'>
  <stop offset='0' stop-color='%2300eaff'/>
  <stop offset='1' stop-color='%238a5cff'/>
  </linearGradient>
  </defs>
  <polygon points='32,4 56,18 56,46 32,60 8,46 8,18' fill='url(%23g)'/>
  <circle cx='32' cy='32' r='10' fill='%230b0f2a'/>
  <circle cx='32' cy='32' r='16' fill='none' stroke='%23ffffff' stroke-width='2' opacity='0.7'/>
  <line x1='32' y1='12' x2='32' y2='20' stroke='%23ffffff' stroke-width='2'/>
  <line x1='32' y1='44' x2='32' y2='52' stroke='%23ffffff' stroke-width='2'/>
  <line x1='12' y1='32' x2='20' y2='32' stroke='%23ffffff' stroke-width='2'/>
  <line x1='44' y1='32' x2='52' y2='32' stroke='%23ffffff' stroke-width='2'/>
  </svg>">

  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>

  <!-- Tailwind CSS v2 -->
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>
  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>
  <!-- A-Frame (VR 試験) -->
  <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>

  <!-- ✅ Favicon -->
  <link id="dynamic-favicon" rel="shortcut icon"
    href="data:image/svg+xml,%3Csvg xmlns='https://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64'%3E%3Cdefs%3E%3CradialGradient id='g' cx='30%25' cy='20%25' r='90%25'%3E%3Cstop offset='0%25' stop-color='%237c3aed'/%3E%3Cstop offset='55%25' stop-color='%235b5fe6'/%3E%3Cstop offset='100%25' stop-color='%230b1020'/%3E%3C/radialGradient%3E%3ClinearGradient id='glow' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23a78bfa'/%3E%3Cstop offset='100%25' stop-color='%2360a5fa'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='2' y='2' width='60' height='60' rx='16' fill='url(%23g)'/%3E%3Cpath d='M20 18h24a2 2 0 0 1 0 4H36v26a2 2 0 0 1-4 0V22h-8a2 2 0 0 1 0-4z' fill='white'/%3E%3Cpath d='M18 20h28' stroke='url(%23glow)' stroke-width='3' stroke-linecap='round' opacity='.85'/%3E%3Cpath d='M32 22v26' stroke='url(%23glow)' stroke-width='3' stroke-linecap='round' opacity='.65'/%3E%3Crect x='2' y='2' width='60' height='60' rx='16' fill='none' stroke='rgba(255,255,255,.22)'/%3E%3C/svg%3E"/>

  <link id="favicon-shortcut" rel="shortcut icon" href="">
  <link id="favicon-any" rel="icon" sizes="any" href="">
  <link id="apple-touch" rel="apple-touch-icon" href="">
  <meta name="theme-color" content="#5b5fe6">

  <!-- Tailwind CSS v2 -->
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>
  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>
  <!-- A-Frame (VR 試験) -->
  <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>

  <style>
    :root{
      --grad-a:#5b5fe6; --grad-b:#7c3aed;
      --bg-a:#f4f7ff; --bg-b:#eef2ff;
      --ink:#0f172a; --muted:#64748b;
    }
    body{
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
      background: radial-gradient(circle at top left,var(--bg-a) 0%,var(--bg-b) 45%,#ffffff 100%);
      color: var(--ink);
      min-height:100vh;
    }
    .glass{ background: rgba(255,255,255,.78); backdrop-filter: blur(18px); border: 1px solid rgba(15,23,42,.10); border-radius: 18px; }
    .card-hover{ transition:.18s ease; }
    .card-hover:hover{ transform: translateY(-2px); box-shadow: 0 18px 35px -18px rgba(2,6,23,.25); }
    .gradient-text{ background: linear-gradient(90deg,var(--grad-a),var(--grad-b)); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
    .btn-primary{ background: linear-gradient(135deg,var(--grad-a),var(--grad-b)); color:#fff; border:none; transition:.18s ease; }
    .btn-primary:hover{ transform: translateY(-1px); box-shadow: 0 14px 26px -18px rgba(2,6,23,.35); }
    .pill{ border:1px solid rgba(15,23,42,.10); background: rgba(255,255,255,.9); border-radius: 999px; }
    .mini-avatar{ width:44px;height:44px;border-radius:999px;object-fit:cover; border:2px solid rgba(255,255,255,.95); box-shadow: 0 10px 22px rgba(2,6,23,.14); }
    .avatar{ width:96px;height:96px;border-radius:999px;object-fit:cover; border:3px solid rgba(255,255,255,.95); box-shadow: 0 14px 30px rgba(2,6,23,.18); }
    .timeline-post{ background: rgba(255,255,255,.96); border-radius: 18px; border:1px solid rgba(15,23,42,.08); box-shadow: 0 16px 30px -20px rgba(2,6,23,.25); border-left: 4px solid var(--grad-a); transition:.18s ease; }
    .timeline-post:hover{ transform: translateX(3px); }
    .badge{ font-size: 11px; padding: 2px 10px; border-radius: 999px; background: rgba(91,95,230,.10); border:1px solid rgba(91,95,230,.25); color: #3730a3; display:inline-flex;align-items:center;gap:6px; white-space:nowrap; }
    .status-dot{ width:8px;height:8px;border-radius:999px; display:inline-block; }
    .on{ background:#10b981; box-shadow:0 0 0 3px rgba(16,185,129,.15); }
    .off{ background:#94a3b8; }
    .log{ max-height: 190px; overflow:auto; background: rgba(241,245,249,.85); border: 1px solid rgba(15,23,42,.12); border-radius: 12px; padding: 10px; font-size: 12px; color:#0f172a; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
    .log .err{ background: rgba(254,202,202,.8); color:#7f1d1d; padding:6px 8px; border-radius:10px; margin:6px 0; }
    .log .ok{ background: rgba(187,247,208,.8); color:#065f46; padding:6px 8px; border-radius:10px; margin:6px 0; }
    .log .info{ color:#334155; padding:2px 0; }
    .header-bg{ background-size: cover; background-position: center; background-repeat: no-repeat; }
    .soft-input{ background:#fff; border:1px solid rgba(15,23,42,.14); border-radius: 12px; padding: 10px 12px; outline:none; width:100%; color:#0f172a; }
    .soft-input:focus{ box-shadow: 0 0 0 3px rgba(91,95,230,.18); border-color: rgba(91,95,230,.35); }
    .muted{ color: var(--muted); }

    /* Toggle (ON/OFF) */
    .toggle{
      width: 54px; height: 30px; border-radius: 999px;
      border: 1px solid rgba(15,23,42,.16);
      background: rgba(148,163,184,.35);
      position: relative; cursor: pointer; transition:.16s ease;
      display:inline-flex; align-items:center;
    }
    .toggle.on{ background: rgba(16,185,129,.22); border-color: rgba(16,185,129,.35); }
    .toggle > span{
      width: 24px; height: 24px; border-radius: 999px;
      background: #fff; position:absolute; left: 3px; transition:.16s ease;
      box-shadow: 0 12px 20px -14px rgba(2,6,23,.55);
      border: 1px solid rgba(15,23,42,.12);
    }
    .toggle.on > span{ left: 27px; }

    /* VR overlay */
    #vr-overlay{ position:fixed; inset:0; background:#000; z-index:9999; display:none; }
    #vr-topbar{ position:absolute; top:10px; left:10px; right:10px; display:flex; align-items:center; justify-content:space-between; z-index:10000; pointer-events:auto; }
    #vr-topbar .btn{ background: rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.18); color:#fff; padding:8px 12px; border-radius:999px; font-size:12px; backdrop-filter: blur(10px); }
    #vr-topbar .btn:hover{ background: rgba(255,255,255,.16); }
    #vr-hint{ color:#fff; font-size:12px; opacity:.85; background: rgba(255,255,255,.08); padding:8px 12px; border-radius:999px; border:1px solid rgba(255,255,255,.14); backdrop-filter: blur(10px); }

    /* ✅ Auth overlay (Login/Register/Logout) */
    #auth-overlay{
      position:fixed; inset:0;
      background: rgba(2,6,23,.62);
      backdrop-filter: blur(10px);
      z-index: 100000;
      display:none;
    }
    #auth-card{
      max-width: 520px;
      margin: 6vh auto;
      padding: 22px;
      border-radius: 22px;
      background: rgba(255,255,255,.92);
      border: 1px solid rgba(148,163,184,.35);
      box-shadow: 0 30px 70px rgba(2,6,23,.35);
    }
    .tab-btn{
      border:1px solid rgba(15,23,42,.10);
      background: rgba(255,255,255,.9);
      border-radius: 999px;
      padding: 8px 12px;
      font-size: 12px;
      transition:.12s ease;
    }
    .tab-btn.active{
      background: linear-gradient(135deg,var(--grad-a),var(--grad-b));
      color:#fff; border-color: transparent;
    }

    /* ✅ WEBアイコン */
    .icon-tile{
      border-radius: 16px;
      border: 1px solid rgba(15,23,42,.10);
      background: rgba(255,255,255,.92);
      padding: 10px;
      transition: .14s ease;
      cursor: pointer;
      position: relative;
      overflow: hidden;
    }
    .icon-tile:hover{
      transform: translateY(-2px);
      box-shadow: 0 18px 32px -22px rgba(2,6,23,.35);
    }
    .icon-tile.active{
      outline: 3px solid rgba(91,95,230,.28);
      box-shadow: 0 0 0 6px rgba(91,95,230,.10);
    }
    .icon-preview{
      width: 44px; height: 44px;
      border-radius: 14px;
      border: 1px solid rgba(15,23,42,.10);
      background: rgba(241,245,249,.8);
      display:flex; align-items:center; justify-content:center;
    }
    .icon-preview img{ width: 36px; height: 36px; }
    .icon-tag{
      font-size: 11px;
      color: #0f172a;
      font-weight: 700;
      letter-spacing: .02em;
    }
    .icon-sub{
      font-size: 11px;
      color: var(--muted);
    }
    .icon-apply{
      display:flex;
      gap:8px;
      margin-top:8px;
      flex-wrap: wrap;
    }
    .mini-btn{
      border-radius: 999px;
      border: 1px solid rgba(15,23,42,.12);
      background: rgba(255,255,255,.9);
      padding: 6px 10px;
      font-size: 11px;
      transition: .12s ease;
      display:inline-flex; align-items:center; gap:6px;
    }
    .mini-btn:hover{ background: rgba(248,250,252,1); }
    .mini-btn.primary{
      border-color: transparent;
      color:#fff;
      background: linear-gradient(135deg,var(--grad-a),var(--grad-b));
    }

    @media (max-width: 768px){
      .mini-avatar{ width:40px;height:40px; }
      .avatar{ width:88px;height:88px; }
      #auth-card{ margin: 4vh 14px; }
    }
  </style>
</head>

<body class="text-gray-900">

  <!-- Hidden file inputs -->
  <input id="file-profile" type="file" accept="image/*" class="hidden">
  <input id="file-headerbg" type="file" accept="image/*" class="hidden">
  <input id="file-appicon" type="file" accept="image/*" class="hidden">
  <input id="file-feedicon" type="file" accept="image/*" class="hidden">
  <input id="file-media" type="file" accept="image/*,video/*" multiple class="hidden">

  <!-- ✅ Auth Overlay -->
  <div id="auth-overlay">
    <div id="auth-card">
      <div class="flex items-center justify-between">
        <div class="flex items-center gap-3">
          <div class="w-10 h-10 rounded-2xl flex items-center justify-center"
               style="background:linear-gradient(135deg,var(--grad-a),var(--grad-b)); color:#fff;">
            <i class="fas fa-comments"></i>
          </div>
          <div>
            <div class="text-xl font-extrabold tracking-tight"><span class="gradient-text">Tsumugi</span></div>
            <div class="text-xs muted">ログイン / 会員登録 / ログアウト(localStorage保持)</div>
          </div>
        </div>
        <button id="btn-auth-close" class="pill px-4 py-2 text-xs hover:bg-slate-50">
          <i class="fas fa-times mr-2"></i>閉じる
        </button>
      </div>

      <div class="mt-4 flex items-center gap-2">
        <button id="tab-login" class="tab-btn active"><i class="fas fa-right-to-bracket mr-2"></i>ログイン</button>
        <button id="tab-register" class="tab-btn"><i class="fas fa-user-plus mr-2"></i>会員登録</button>
        <div class="ml-auto flex items-center gap-2">
          <button id="btn-guest" class="pill px-4 py-2 text-xs hover:bg-slate-50" title="ゲストで入る(登録不要)">
            <i class="fas fa-user-ninja mr-2"></i>ゲスト
          </button>
        </div>
      </div>

      <div class="mt-4 glass p-4 border border-slate-200">
        <!-- Login -->
        <div id="panel-login">
          <div class="text-sm font-bold mb-2"><i class="fas fa-right-to-bracket mr-2 text-indigo-600"></i>ログイン</div>
          <div class="space-y-3">
            <div>
              <label class="block text-xs font-semibold text-slate-700 mb-1">メールアドレス</label>
              <input id="login-email" type="email" class="soft-input" placeholder="you@example.com" autocomplete="email">
            </div>
            <div>
              <label class="block text-xs font-semibold text-slate-700 mb-1">パスワード</label>
              <input id="login-pass" type="password" class="soft-input" placeholder="password" autocomplete="current-password">
            </div>
            <div class="flex items-center justify-between gap-3">
              <label class="flex items-center gap-2 text-xs">
                <input id="login-remember" type="checkbox" class="mr-1">
                ログイン状態を保持
              </label>
              <button id="btn-login" class="btn-primary px-5 py-2 rounded-lg text-xs font-semibold">
                <i class="fas fa-unlock mr-2"></i>ログイン
              </button>
            </div>
          </div>
        </div>

        <!-- Register -->
        <div id="panel-register" style="display:none;">
          <div class="text-sm font-bold mb-2"><i class="fas fa-user-plus mr-2 text-indigo-600"></i>会員登録</div>
          <div class="space-y-3">
            <div>
              <label class="block text-xs font-semibold text-slate-700 mb-1">メールアドレス</label>
              <input id="reg-email" type="email" class="soft-input" placeholder="you@example.com" autocomplete="email">
            </div>
            <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
              <div>
                <label class="block text-xs font-semibold text-slate-700 mb-1">パスワード</label>
                <input id="reg-pass" type="password" class="soft-input" placeholder="password" autocomplete="new-password">
              </div>
              <div>
                <label class="block text-xs font-semibold text-slate-700 mb-1">確認</label>
                <input id="reg-pass2" type="password" class="soft-input" placeholder="confirm" autocomplete="new-password">
              </div>
            </div>
            <div>
              <label class="block text-xs font-semibold text-slate-700 mb-1">ユーザー名(20文字まで)</label>
              <input id="reg-username" type="text" maxlength="20" class="soft-input" placeholder="表示名">
            </div>
            <div class="flex items-center justify-between gap-3">
              <div class="text-xs muted">※パスワードは端末(localStorage)に保存されます(簡易SNS)</div>
              <button id="btn-register" class="btn-primary px-5 py-2 rounded-lg text-xs font-semibold">
                <i class="fas fa-check mr-2"></i>登録
              </button>
            </div>
          </div>
        </div>
      </div>

      <div class="mt-3">
        <div id="auth-msg" class="text-xs muted"></div>
      </div>
    </div>
  </div>

  <!-- VR Overlay (試験) -->
  <div id="vr-overlay">
    <div id="vr-topbar">
      <div class="flex items-center gap-2">
        <span id="vr-hint"><i class="fas fa-vr-cardboard mr-2"></i>VR試験モード(投稿カードを3D表示)</span>
      </div>
      <div class="flex items-center gap-2">
        <button id="btn-vr-enter" class="btn"><i class="fas fa-play mr-2"></i>VR開始</button>
        <button id="btn-vr-exit" class="btn"><i class="fas fa-times mr-2"></i>終了</button>
      </div>
    </div>

    <a-scene id="vr-scene" embedded background="color: #0b1020">
      <a-entity position="0 1.6 3">
        <a-camera wasd-controls-enabled="true" look-controls="pointerLockEnabled: true"></a-camera>
      </a-entity>
      <a-entity light="type: ambient; intensity: 0.7"></a-entity>
      <a-entity light="type: directional; intensity: 0.8" position="-1 2 1"></a-entity>

      <a-entity id="vr-wall"></a-entity>

      <a-plane position="0 0 -3" rotation="-90 0 0" width="30" height="30" color="#111827" opacity="0.9"></a-plane>
      <a-sky color="#0b1020"></a-sky>
    </a-scene>
  </div>

  <!-- HEADER -->
  <header id="app-header" class="glass mx-4 mt-4 p-6 border border-indigo-500/20 header-bg">
    <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
      <div>
        <div class="flex items-center gap-3">
          <i class="fas fa-comments text-indigo-500 text-2xl"></i>
          <h1 class="text-3xl md:text-4xl font-extrabold tracking-tight">
            <span class="gradient-text">Tsumugi</span>
            <span class="badge ml-2"><i class="fas fa-bolt"></i> LocalSNS</span>
          </h1>
        </div>
        <p class="text-sm md:text-base muted mt-1">
          フォローTL / データDL / 投稿URL / RSS投稿ON/OFF / VR試験 / ログイン
        </p>
      </div>

      <div class="flex flex-wrap items-center justify-end gap-2">
        <div class="pill px-3 py-2 flex items-center gap-3">
          <img id="header-avatar" class="mini-avatar" src="https://via.placeholder.com/80" alt="avatar">
          <div class="text-left leading-tight">
            <div class="font-semibold text-sm" id="header-username">未設定</div>
            <div class="text-xs muted" id="header-email"></div>
          </div>
        </div>

        <button id="btn-auth-open" class="pill px-4 py-2 text-xs flex items-center gap-2 hover:bg-slate-50" title="ログイン/登録">
          <i class="fas fa-user-shield text-slate-700"></i> アカウント
        </button>

        <button id="btn-logout" class="pill px-4 py-2 text-xs flex items-center gap-2 hover:bg-slate-50" title="ログアウト">
          <i class="fas fa-right-from-bracket text-slate-700"></i> ログアウト
        </button>

        <button id="btn-vr" class="pill px-4 py-2 text-xs flex items-center gap-2 hover:bg-slate-50">
          <i class="fas fa-vr-cardboard text-slate-700"></i> VR試験
        </button>

        <button id="btn-clear-all" class="bg-yellow-300 hover:bg-yellow-400 px-4 py-2 rounded-full text-black text-xs flex items-center gap-2">
          <i class="fas fa-broom"></i> 全消去
        </button>
      </div>
    </div>
  </header>

  <!-- MAIN -->
  <main class="max-w-6xl mx-auto px-4 py-6" id="main-content">
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">

      <!-- LEFT -->
      <section class="lg:col-span-1 space-y-6">

        <!-- Account / Profile -->
        <div class="glass p-6 card-hover border border-slate-200">
          <div class="flex items-center justify-between mb-4">
            <h3 class="text-xl font-bold flex items-center gap-2">
              <i class="fas fa-user-circle text-indigo-500"></i>
              アカウント
            </h3>
            <span class="badge"><i class="fas fa-home"></i> Home=Follow</span>
          </div>

          <div class="text-center mb-5">
            <img id="profile-avatar" class="avatar mx-auto mb-3" src="https://via.placeholder.com/100" alt="avatar">
            <button id="btn-change-profile" class="btn-primary px-4 py-2 rounded-full text-white text-sm">
              <i class="fas fa-camera mr-2"></i>プロフィール画像
            </button>
          </div>

          <div class="grid grid-cols-3 gap-2 mb-4">
            <div class="glass p-3 text-center border border-slate-200">
              <div class="text-xs muted">フォロー</div>
              <div id="stat-following" class="text-lg font-extrabold">0</div>
            </div>
            <div class="glass p-3 text-center border border-slate-200">
              <div class="text-xs muted">フォロワー</div>
              <div id="stat-followers" class="text-lg font-extrabold">0</div>
            </div>
            <div class="glass p-3 text-center border border-slate-200">
              <div class="text-xs muted">投稿</div>
              <div id="stat-posts" class="text-lg font-extrabold">0</div>
            </div>
          </div>

          <div class="space-y-3">
            <div>
              <label class="block text-xs font-semibold text-slate-700 mb-1">ユーザー名(20文字まで)</label>
              <input id="inp-username" type="text" maxlength="20" class="soft-input" placeholder="ユーザー名">
            </div>
            <div>
              <label class="block text-xs font-semibold text-slate-700 mb-1">自己紹介</label>
              <textarea id="inp-bio" rows="4" class="soft-input" placeholder="自己紹介"></textarea>
            </div>

            <button id="btn-save-profile" class="btn-primary w-full py-2 rounded-lg text-white text-sm font-semibold">
              <i class="fas fa-save mr-2"></i>保存
            </button>

            <div class="glass p-3 border border-slate-200">
              <div class="text-xs font-semibold text-slate-800 mb-1">プレビュー</div>
              <div id="preview-username" class="font-semibold">未設定</div>
              <div id="preview-bio" class="text-xs muted whitespace-pre-line mt-1">まだ自己紹介がありません</div>
            </div>
          </div>
        </div>

        <!-- Header / Icons -->
        <div class="glass p-6 card-hover border border-slate-200">
          <div class="flex items-center justify-between mb-3">
            <h3 class="text-lg font-bold flex items-center gap-2">
              <i class="fas fa-palette text-indigo-500"></i> 見た目(ファイル)
            </h3>
            <span class="text-xs muted">localStorage保存</span>
          </div>

          <div class="space-y-2 text-sm">
            <button id="btn-change-headerbg" class="pill w-full px-4 py-2 text-left hover:bg-slate-50">
              <i class="fas fa-image mr-2 text-slate-700"></i> ヘッダー背景を変更
            </button>
            <button id="btn-change-appicon" class="pill w-full px-4 py-2 text-left hover:bg-slate-50">
              <i class="fas fa-icons mr-2 text-slate-700"></i> Faviconを変更
            </button>
            <button id="btn-change-feedicon" class="pill w-full px-4 py-2 text-left hover:bg-slate-50">
              <i class="fas fa-rss mr-2 text-slate-700"></i> Feedアイコン変更
            </button>

            <div class="grid grid-cols-2 gap-2 mt-2">
              <button id="btn-reset-headerbg" class="pill px-4 py-2 hover:bg-slate-50">
                <i class="fas fa-undo mr-2"></i>ヘッダー戻す
              </button>
              <button id="btn-reset-appicon" class="pill px-4 py-2 hover:bg-slate-50">
                <i class="fas fa-undo mr-2"></i>Favicon戻す
              </button>
            </div>
          </div>

          <!-- ✅ WEBアイコン -->
          <div class="mt-5">
            <div class="flex items-center justify-between">
              <div class="text-sm font-extrabold flex items-center gap-2">
                <i class="fas fa-sparkles text-indigo-600"></i> WEBアイコン
              </div>
              <span class="text-xs muted">クリックで選択 → 適用</span>
            </div>

            <div class="mt-3 grid grid-cols-2 gap-2" id="icon-grid"></div>

            <div class="mt-3 glass p-4 border border-slate-200">
              <div class="flex items-center justify-between gap-2">
                <div class="text-xs font-semibold flex items-center gap-2">
                  <i class="fas fa-wand-magic-sparkles text-indigo-600"></i>
                  選択中アイコン
                </div>
                <button id="btn-icon-random" class="pill px-3 py-2 text-xs hover:bg-slate-50" title="ランダムに選ぶ">
                  <i class="fas fa-dice mr-2"></i>ランダム
                </button>
              </div>

              <div class="mt-3 flex items-center gap-3">
                <div class="icon-preview">
                  <img id="icon-selected-preview" src="" alt="selected">
                </div>
                <div class="flex-1">
                  <div id="icon-selected-name" class="icon-tag">未選択</div>
                  <div id="icon-selected-desc" class="icon-sub">アイコンをクリックしてください</div>
                </div>
              </div>

              <div class="icon-apply mt-3">
                <button id="btn-apply-favicon" class="mini-btn primary">
                  <i class="fas fa-icons"></i>Faviconに適用
                </button>
                <button id="btn-apply-feed" class="mini-btn">
                  <i class="fas fa-rss"></i>Feedに適用
                </button>
                <button id="btn-apply-profile" class="mini-btn">
                  <i class="fas fa-user-circle"></i>プロフィールに適用
                </button>
              </div>

              <div class="mt-3 text-[11px] muted">
                ※SVG(dataURL)なので、端末内(localStorage)だけで完結します(外部画像不要)
              </div>
            </div>
          </div>
        </div>

        <!-- ✅ RSS(投稿機能 ON/OFF 完全) -->
        <div class="glass p-6 card-hover border border-amber-500/20">
          <div class="flex items-center justify-between mb-2">
            <h3 class="text-lg font-bold flex items-center gap-2">
              <i class="fas fa-rss text-amber-500"></i> RSS投稿
            </h3>
            <span class="badge">
              <span class="status-dot" id="rss-dot"></span>
              <span id="rss-status-text">停止中</span>
            </span>
          </div>

          <div class="flex items-center justify-between gap-3 mb-3">
            <div class="text-xs muted">
              <b>ON</b>:一定間隔でRSSを取得し、<b>Feed投稿</b>としてタイムラインへ自動投稿します
            </div>

            <div class="flex items-center gap-2">
              <div class="text-xs font-bold text-slate-700">自動</div>
              <div id="rss-toggle" class="toggle" role="switch" aria-checked="false" tabindex="0" title="RSS自動投稿 ON/OFF">
                <span></span>
              </div>
            </div>
          </div>

          <div class="space-y-2">
            <input id="rss-url" class="soft-input text-xs" placeholder="RSS URL(https://...)">
            <div class="grid grid-cols-2 gap-2">
              <button id="btn-add-rss" class="btn-primary py-2 rounded-lg text-xs">
                <i class="fas fa-plus mr-2"></i>追加
              </button>
              <button id="btn-fetch-rss" class="pill py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-sync mr-2"></i>今すぐ取得
              </button>
            </div>

            <div class="grid grid-cols-2 gap-2">
              <input id="rss-interval" type="number" min="10" max="3600" value="120" class="soft-input text-xs" placeholder="間隔(秒)">
              <button id="btn-set-rss-interval" class="pill py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-clock mr-2"></i>間隔設定
              </button>
            </div>

            <div class="grid grid-cols-2 gap-2">
              <button id="btn-rss-clear-seen" class="pill py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-eraser mr-2"></i>既読リセット
              </button>
              <button id="btn-rss-test-one" class="pill py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-flask mr-2"></i>URL検査
              </button>
            </div>

            <div class="mt-2">
              <div class="text-xs font-semibold mb-1">RSS一覧</div>
              <div id="rss-list" class="space-y-2 text-xs"></div>
            </div>

            <div class="mt-3">
              <div class="text-xs font-semibold mb-1">RSSログ</div>
              <div id="rss-log" class="log"></div>
            </div>
          </div>
        </div>

      </section>

      <!-- RIGHT -->
      <section class="lg:col-span-2 space-y-6">

        <!-- New Post -->
        <div class="glass p-6 card-hover border border-slate-200">
          <div class="flex items-center justify-between mb-3">
            <h3 class="text-xl font-bold flex items-center gap-2">
              <i class="fas fa-edit text-indigo-500"></i> 新規投稿
            </h3>
            <div class="flex gap-2">
              <button id="btn-home" class="btn-primary px-3 py-2 rounded-full text-xs">
                <i class="fas fa-house mr-2"></i>Home
              </button>
              <button id="btn-explore" class="pill px-3 py-2 rounded-full text-xs hover:bg-slate-50">
                <i class="fas fa-compass mr-2"></i>探索
              </button>
            </div>
          </div>

          <textarea id="post-text" class="soft-input text-sm" rows="4"
            placeholder="いまどうしてる?(Ctrl+Enterで投稿)" maxlength="600"></textarea>

          <div class="mt-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
            <div class="text-xs muted flex items-center gap-2 flex-wrap">
              <i class="fas fa-circle-info"></i>
              <span id="char-count">(0/600)</span>
              <label class="pill px-3 py-2 text-xs cursor-pointer hover:bg-slate-50" id="btn-add-media">
                <i class="fas fa-paperclip mr-2"></i>画像/動画
              </label>
              <button id="btn-clear-media" class="pill px-3 py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-trash mr-2"></i>クリア
              </button>
              <span class="badge"><i class="fas fa-link"></i>投稿URL</span>
              <span class="badge"><i class="fas fa-rss"></i>RSS投稿</span>
            </div>

            <div class="flex gap-2">
              <button id="btn-post" class="btn-primary px-5 py-2 rounded-lg text-xs font-semibold">
                <i class="fas fa-paper-plane mr-2"></i>投稿
              </button>
              <button id="btn-delete-all-posts" class="bg-red-600 hover:bg-red-700 px-5 py-2 rounded-lg text-white text-xs font-semibold">
                <i class="fas fa-trash mr-2"></i>全投稿削除
              </button>
            </div>
          </div>

          <div class="mt-4 glass p-4 border border-slate-200">
            <div class="flex items-center justify-between">
              <div class="text-xs font-semibold flex items-center gap-2">
                <i class="fas fa-link text-indigo-600"></i> 直近の投稿URL
              </div>
              <button id="btn-copy-post-url" class="pill px-3 py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-copy mr-2"></i>コピー
              </button>
            </div>
            <div class="mt-2 text-sm">
              <div id="latest-post-url" class="muted break-all">(まだ投稿していません)</div>
            </div>
            <div class="mt-3 flex flex-wrap gap-2">
              <button id="btn-open-post-url" class="pill px-4 py-2 rounded-lg text-xs hover:bg-slate-50">
                <i class="fas fa-arrow-up-right-from-square mr-2"></i>開く
              </button>
            </div>
          </div>

          <div class="mt-4">
            <div class="text-xs font-semibold mb-1">添付</div>
            <div id="media-preview" class="grid grid-cols-2 md:grid-cols-3 gap-3"></div>
          </div>
        </div>

        <!-- Timeline -->
        <div class="glass p-6 border border-slate-200">
          <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-3">
            <h3 class="text-xl font-bold flex items-center gap-2">
              <i class="fas fa-stream text-indigo-500"></i> タイムライン
              <span id="post-count" class="text-sm font-normal muted">(0件)</span>
            </h3>
            <div class="flex items-center gap-2">
              <input id="search" class="soft-input text-xs w-full md:w-72" placeholder="検索(本文/ユーザー)">
              <button id="btn-refresh" class="pill px-4 py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-rotate mr-2"></i>更新
              </button>
              <button id="btn-export" class="bg-emerald-600 hover:bg-emerald-700 px-4 py-2 rounded-lg text-white text-xs">
                <i class="fas fa-download mr-2"></i>データDL
              </button>
              <button id="btn-import" class="pill px-4 py-2 text-xs hover:bg-slate-50">
                <i class="fas fa-upload mr-2"></i>読み込み
              </button>
            </div>
          </div>

          <div class="flex flex-wrap gap-2 text-xs mb-3">
            <button class="btn-primary px-3 py-2 rounded-full" data-filter="all"><i class="fas fa-globe mr-2"></i>すべて</button>
            <button class="pill px-3 py-2 rounded-full hover:bg-slate-50" data-filter="user"><i class="fas fa-user mr-2"></i>ユーザー</button>
            <button class="pill px-3 py-2 rounded-full hover:bg-slate-50" data-filter="feed"><i class="fas fa-rss mr-2"></i>Feed</button>
          </div>

          <div id="timeline" class="space-y-4"></div>
          <div id="empty" class="text-center py-14 muted">
            <i class="fas fa-comments text-4xl mb-4 text-indigo-500"></i>
            <div class="text-lg font-semibold">まだ投稿がありません</div>
            <div class="text-xs mt-1">投稿かRSS取得をすると、ここに表示されます</div>
          </div>
        </div>

      </section>
    </div>
  </main>

  <footer class="glass mx-4 mb-4 p-4 text-center border border-slate-200">
    <div class="text-xs muted">
      <i class="fas fa-copyright mr-1"></i> 2026 Tsumugi – Local SNS(ログイン/登録/ログアウト + RSS投稿ON/OFF)
    </div>
  </footer>

<script>
/* =========================================================
   ✅ 会員登録が機能しない問題を修正(欠落なし全体コード)
   主因になりやすいポイントを潰しています:
   1) crypto.subtle が http / file:// で使えず例外 → 登録が止まる
      → ✅ sha256 失敗時はフォールバック(簡易ハッシュ)で必ず進む
   2) users 読み込み前に findUser が走る / 破損データで落ちる
      → ✅ loadUsers を最初に実行&sanitize
   3) 登録後にアプリがロック解除されない/ UI未更新
      → ✅ afterAuthBootstrap を確実に呼ぶ
========================================================= */

/* ---------- Storage helpers ---------- */
const LS = {
  get(k, fb=null){ try{ const v=localStorage.getItem(k); return (v===null||v===undefined)?fb:v; }catch(e){ return fb; } },
  set(k, v){ try{ localStorage.setItem(k, v); }catch(e){} },
  del(k){ try{ localStorage.removeItem(k); }catch(e){} }
};

const KEY = {
  users:"ts_users",
  current:"ts_current_user",          // {email}
  session:"ts_session",               // {email, remember, ts}
  posts:"ts_posts",
  icons:"ts_icons",
  headerBg:"ts_header_bg",
  favicon:"ts_favicon",
  rss:"ts_rss",
  rssSeen:"ts_rss_seen",
  latest:"ts_latest_post_url",
  webIconSelected:"ts_webicon_selected" // {id}
};

const PRESET_RSS = [
  "https://uploadvr.com/feed/",
  "https://www.theverge.com/rss/index.xml",
  "https://www3.nhk.or.jp/rss/news/cat0.xml"
];

/* ---------- State ---------- */
let users = [];
let currentUser = null;   // {email}
let posts = [];
let icons = {};
let homeMode = "home";
let currentFilter = "all";
let searchQuery = "";
let pendingMedia = [];
let rssTimer = null;

/* ✅ WEBアイコン state */
let WEB_ICONS = [];
let selectedWebIconId = null;

/* ---------- Utilities ---------- */
function uid(){ return Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,8); }
function escapeHtml(s){ return (s||"").replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
function nowJp(){ return new Date().toLocaleString("ja-JP"); }
function safeUrl(u){
  const s = String(u||"").trim();
  if(!s) return "";
  if(/^https?:\/\//i.test(s)) return s;
  return "";
}
async function copyText(t){
  try{
    if(navigator.clipboard){ await navigator.clipboard.writeText(t); return true; }
  }catch(e){}
  const ta=document.createElement("textarea"); ta.value=t; document.body.appendChild(ta);
  ta.select(); try{ document.execCommand("copy"); }catch(e){}
  document.body.removeChild(ta);
  return true;
}
function openPopup(url){ window.open(url, "_blank", "noopener,noreferrer,width=980,height=740"); }
function bytesToMB(b){ return Math.round((b/1024/1024)*10)/10; }

function addLog(msg, type="info"){
  const el = document.getElementById("rss-log");
  if(!el) return;
  const div = document.createElement("div");
  div.className = type==="err" ? "err" : type==="ok" ? "ok" : "info";
  div.textContent = `[${new Date().toLocaleTimeString("ja-JP")}] ${msg}`;
  el.appendChild(div);
  el.scrollTop = el.scrollHeight;
  while(el.children.length > 160) el.removeChild(el.firstChild);
}

/* =========================================================
   ✅ WEBアイコン(SVG生成)
========================================================= */
function toDataUrlSvg(svg){
  const enc = encodeURIComponent(svg)
    .replace(/%0A/g,"")
    .replace(/%20/g," ")
    .replace(/%3D/g,"=")
    .replace(/%3A/g,":")
    .replace(/%2F/g,"/")
    .replace(/%2C/g,",");
  return "data:image/svg+xml;charset=utf-8," + enc;
}
function makeIconSvg({bg1,bg2,shape,mark,accent1,accent2}){
  const bg = `
    <defs>
      <radialGradient id="rg" cx="30%" cy="20%" r="90%">
        <stop offset="0%" stop-color="${bg2}"/>
        <stop offset="55%" stop-color="${bg1}"/>
        <stop offset="100%" stop-color="#0b1020"/>
      </radialGradient>
      <linearGradient id="lg" x1="0%" y1="0%" x2="100%" y2="100%">
        <stop offset="0%" stop-color="${accent1}"/>
        <stop offset="100%" stop-color="${accent2}"/>
      </linearGradient>
      <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%">
        <feDropShadow dx="0" dy="10" stdDeviation="8" flood-color="rgba(2,6,23,.45)"/>
      </filter>
    </defs>
  `;
  let base;
  if(shape==="hex"){
    base = `<path d="M32 4 54 16v32L32 60 10 48V16Z" fill="url(#rg)" />`;
  }else if(shape==="round"){
    base = `<circle cx="32" cy="32" r="28" fill="url(#rg)" />`;
  }else{
    base = `<rect x="4" y="4" width="56" height="56" rx="16" fill="url(#rg)" />`;
  }
  let glyph="";
  if(mark==="T"){
    glyph = `<path d="M20 18h24a2 2 0 0 1 0 4H36v26a2 2 0 0 1-4 0V22h-8a2 2 0 0 1 0-4z" fill="white"/>`;
  }else if(mark==="V"){
    glyph = `<path d="M18 18h8l6 22 6-22h8L36 50h-8L18 18z" fill="white"/>`;
  }else if(mark==="C"){
    glyph = `<path d="M44.5 22.5c-2.3-2.3-5.5-3.7-9-3.7-6.9 0-12.5 5.6-12.5 12.5S28.6 43.8 35.5 43.8c3.5 0 6.7-1.4 9-3.7l-3.2-3.2c-1.5 1.5-3.6 2.4-5.8 2.4-4.4 0-8-3.6-8-8s3.6-8 8-8c2.3 0 4.3.9 5.8 2.4l3.2-3.2z" fill="white"/>`;
  }else if(mark==="star"){
    glyph = `<path d="M32 18l3.9 8.2 9.1 1.3-6.6 6.4 1.6 9.1L32 39.7 23.9 43l1.6-9.1-6.6-6.4 9.1-1.3L32 18z" fill="white"/>`;
  }else if(mark==="bolt"){
    glyph = `<path d="M36 14 20 36h10l-2 14 16-22H34l2-14z" fill="white"/>`;
  }else if(mark==="eye"){
    glyph = `<path d="M32 22c-10 0-18 10-18 10s8 10 18 10 18-10 18-10-8-10-18-10zm0 16a6 6 0 1 1 0-12 6 6 0 0 1 0 12z" fill="white"/><circle cx="32" cy="32" r="2.6" fill="white"/>`;
  }else if(mark==="crown"){
    glyph = `<path d="M20 40l-2-16 8 8 6-10 6 10 8-8-2 16H20z" fill="white"/><path d="M22 42h20" stroke="url(#lg)" stroke-width="3" stroke-linecap="round" opacity=".8"/>`;
  }
  const accents = `
    <path d="M18 20h28" stroke="url(#lg)" stroke-width="3" stroke-linecap="round" opacity=".85"/>
    <path d="M32 22v26" stroke="url(#lg)" stroke-width="3" stroke-linecap="round" opacity=".65"/>
  `;
  const border = shape==="round"
    ? `<circle cx="32" cy="32" r="28" fill="none" stroke="rgba(255,255,255,.24)"/>`
    : shape==="hex"
      ? `<path d="M32 4 54 16v32L32 60 10 48V16Z" fill="none" stroke="rgba(255,255,255,.24)"/>`
      : `<rect x="4" y="4" width="56" height="56" rx="16" fill="none" stroke="rgba(255,255,255,.22)"/>`;
  return `
    <svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 64 64">
      ${bg}
      <g filter="url(#shadow)">
        ${base}
        ${accents}
        ${glyph}
        ${border}
      </g>
    </svg>
  `.trim();
}
function buildWebIconPack(){
  const presets = [
    { id:"tsumugi_T", name:"Tsumugi T", desc:"シグネチャー / Squircle", shape:"squircle", mark:"T", bg1:"#5b5fe6", bg2:"#7c3aed", accent1:"#a78bfa", accent2:"#60a5fa" },
    { id:"verse_V",  name:"Verse V",  desc:"SNS感 / Round", shape:"round", mark:"V", bg1:"#06b6d4", bg2:"#3b82f6", accent1:"#67e8f9", accent2:"#a78bfa" },
    { id:"chron_C",  name:"Chronicle C", desc:"アーカイブ / Squircle", shape:"squircle", mark:"C", bg1:"#22c55e", bg2:"#10b981", accent1:"#bbf7d0", accent2:"#60a5fa" },
    { id:"spark_star", name:"Spark Star", desc:"キラッ / Round", shape:"round", mark:"star", bg1:"#f59e0b", bg2:"#ef4444", accent1:"#fde68a", accent2:"#fca5a5" },
    { id:"bolt_hex", name:"Neon Bolt", desc:"稲妻 / Hex", shape:"hex", mark:"bolt", bg1:"#a855f7", bg2:"#ec4899", accent1:"#c4b5fd", accent2:"#93c5fd" },
    { id:"eye_squ",  name:"Observer", desc:"収集 / Squircle", shape:"squircle", mark:"eye", bg1:"#0ea5e9", bg2:"#22c55e", accent1:"#7dd3fc", accent2:"#86efac" },
    { id:"crown_squ", name:"Crown", desc:"上品 / Squircle", shape:"squircle", mark:"crown", bg1:"#f97316", bg2:"#a855f7", accent1:"#fdba74", accent2:"#c4b5fd" }
  ];
  WEB_ICONS = presets.map(p=>{
    const svg = makeIconSvg(p);
    return { id:p.id, name:p.name, desc:p.desc, dataUrl: toDataUrlSvg(svg) };
  });
}
function saveSelectedWebIcon(id){
  selectedWebIconId = id;
  LS.set(KEY.webIconSelected, JSON.stringify({id}));
}
function loadSelectedWebIcon(){
  const obj = JSON.parse(LS.get(KEY.webIconSelected, "null") || "null");
  selectedWebIconId = (obj && obj.id) ? obj.id : (WEB_ICONS[0]?.id || null);
}
function getSelectedWebIcon(){
  if(!selectedWebIconId) return WEB_ICONS[0] || null;
  return WEB_ICONS.find(x=>x.id===selectedWebIconId) || WEB_ICONS[0] || null;
}
function renderWebIconGrid(){
  const grid = document.getElementById("icon-grid");
  if(!grid) return;
  grid.innerHTML = "";
  WEB_ICONS.forEach(icon=>{
    const tile = document.createElement("div");
    tile.className = "icon-tile";
    tile.setAttribute("data-iconid", icon.id);
    tile.innerHTML = `
      <div class="flex items-center gap-3">
        <div class="icon-preview"><img src="${icon.dataUrl}" alt="${escapeHtml(icon.name)}"></div>
        <div class="min-w-0">
          <div class="icon-tag truncate">${escapeHtml(icon.name)}</div>
          <div class="icon-sub truncate">${escapeHtml(icon.desc)}</div>
        </div>
      </div>
    `;
    tile.onclick = ()=>{ selectWebIcon(icon.id); };
    grid.appendChild(tile);
  });
  selectWebIcon(selectedWebIconId, false);
}
function selectWebIcon(id, persist=true){
  const icon = WEB_ICONS.find(x=>x.id===id) || WEB_ICONS[0];
  if(!icon) return;
  if(persist) saveSelectedWebIcon(icon.id);
  document.querySelectorAll(".icon-tile").forEach(el=>{
    el.classList.toggle("active", el.getAttribute("data-iconid")===icon.id);
  });
  document.getElementById("icon-selected-preview").src = icon.dataUrl;
  document.getElementById("icon-selected-name").textContent = icon.name;
  document.getElementById("icon-selected-desc").textContent = icon.desc;
}

/* =========================================================
   ✅ Favicon(確実版):複数relを全部更新する
========================================================= */
function applyFavicon(dataUrl){
  const url = String(dataUrl || "").trim();
  if(!url) return;

  const setHref = (id, rel, type=null, sizes=null)=>{
    let link = document.getElementById(id);
    if(!link){
      link = document.createElement("link");
      link.id = id;
      link.rel = rel;
      if(type) link.type = type;
      if(sizes) link.sizes = sizes;
      document.head.appendChild(link);
    }
    link.href = url;
  };

  setHref("favicon-svg", "icon", "image/svg+xml", null);
  setHref("favicon-shortcut", "shortcut icon", null, null);
  setHref("favicon-any", "icon", null, "any");
  setHref("apple-touch", "apple-touch-icon", null, null);

  LS.set(KEY.favicon, url);
}
function resetFavicon(){
  LS.del(KEY.favicon);
  const base = document.getElementById("favicon-svg")?.href;
  if(base) applyFavicon(base);
  alert("Faviconを初期に戻しました(反映されない場合は Ctrl+F5)");
}

function applySelectedToFavicon(){
  const icon = getSelectedWebIcon(); if(!icon) return;
  applyFavicon(icon.dataUrl);
  alert("Faviconに適用しました");
}
function applySelectedToFeed(){
  const icon = getSelectedWebIcon(); if(!icon) return;
  icons.feed = icon.dataUrl;
  LS.set(KEY.icons, JSON.stringify(icons));
  renderTimeline();
  rebuildVRWall();
  alert("Feedアイコンに適用しました");
}
function applySelectedToProfile(){
  const icon = getSelectedWebIcon(); if(!icon) return;
  const me = getMe();
  if(!me) return alert("ログインしてください");
  me.profile.avatar = icon.dataUrl;
  saveUsers();
  syncProfileUI();
  renderTimeline();
  rebuildVRWall();
  alert("プロフィール画像に適用しました");
}
function randomPickIcon(){
  if(!WEB_ICONS.length) return;
  const i = Math.floor(Math.random()*WEB_ICONS.length);
  selectWebIcon(WEB_ICONS[i].id);
}

/* =========================================================
   ✅ Auth(会員登録/ログイン)— 修正点あり
========================================================= */
function normalizeEmail(s){ return String(s||"").trim().toLowerCase(); }

/* ✅ 修正:sha256 が使えない環境でも必ず動くようにする */
async function sha256(text){
  const str = String(text);
  try{
    if(!(window.crypto && crypto.subtle && window.isSecureContext)){
      // http / file:// だと subtle が動かない → fallback
      return simpleHash(str);
    }
    const enc = new TextEncoder().encode(str);
    const buf = await crypto.subtle.digest("SHA-256", enc);
    const arr = Array.from(new Uint8Array(buf));
    return arr.map(b=>b.toString(16).padStart(2,"0")).join("");
  }catch(e){
    // 例外が出ても登録を止めない
    return simpleHash(str);
  }
}

/* ✅ 追加:簡易ハッシュ(強度は低いが “動かない” を防ぐ) */
function simpleHash(str){
  let h1 = 0xdeadbeef ^ str.length, h2 = 0x41c6ce57 ^ str.length;
  for(let i=0;i<str.length;i++){
    const ch = str.charCodeAt(i);
    h1 = Math.imul(h1 ^ ch, 2654435761);
    h2 = Math.imul(h2 ^ ch, 1597334677);
  }
  h1 = (h1 ^ (h1>>>16)) >>> 0;
  h2 = (h2 ^ (h2>>>16)) >>> 0;
  return "fh_" + h1.toString(16).padStart(8,"0") + h2.toString(16).padStart(8,"0");
}

/* ユーザー配列の読み書き */
function loadUsers(){
  users = JSON.parse(LS.get(KEY.users, "[]") || "[]");
  if(!Array.isArray(users)) users = [];
  users = users.map(u=>{
    if(!u || !u.email) return null;
    if(!u.profile) u.profile = { username:"", bio:"", avatar:"https://via.placeholder.com/100" };
    if(!u.following) u.following = [];
    if(!u.blocked) u.blocked = [];
    if(!u.muted) u.muted = [];
    if(!u.createdAt) u.createdAt = Date.now();
    if(typeof u.passHash !== "string") u.passHash = u.passHash ? String(u.passHash) : "";
    u.email = normalizeEmail(u.email);
    return u;
  }).filter(Boolean);
  LS.set(KEY.users, JSON.stringify(users));
}
function saveUsers(){ LS.set(KEY.users, JSON.stringify(users)); }

function loadSession(){
  const s = JSON.parse(LS.get(KEY.session, "null") || "null");
  if(!s || !s.email) return null;
  if(!s.remember){
    LS.del(KEY.session);
    return null;
  }
  return s;
}
function saveSession(email, remember){
  const s = { email: normalizeEmail(email), remember: !!remember, ts: Date.now() };
  LS.set(KEY.session, JSON.stringify(s));
}
function clearSession(){ LS.del(KEY.session); }

function setCurrentUser(email){
  currentUser = { email: normalizeEmail(email) };
  LS.set(KEY.current, JSON.stringify(currentUser));
}
function clearCurrentUser(){
  currentUser = null;
  LS.del(KEY.current);
}

function ensureGuest(){
  const guestEmail="guest@local";
  if(!users.find(u=>u.email===guestEmail)){
    users.push({
      email:guestEmail,
      passHash:"",
      profile:{ username:"ゲスト", bio:"", avatar:"https://via.placeholder.com/100" },
      following:[], blocked:[], muted:[],
      createdAt: Date.now()
    });
    saveUsers();
  }
  setCurrentUser(guestEmail);
}
function findUser(email){
  const e = normalizeEmail(email);
  return users.find(u=>normalizeEmail(u.email)===e) || null;
}
function getMe(){
  if(!currentUser || !currentUser.email) return null;
  return findUser(currentUser.email);
}

function authMsg(text, kind="info"){
  const el = document.getElementById("auth-msg");
  if(!el) return;
  if(!text){ el.textContent=""; el.className="text-xs muted"; return; }
  el.textContent = text;
  el.className = "text-xs " + (kind==="err" ? "text-red-700" : kind==="ok" ? "text-emerald-700" : "muted");
}
function openAuth(){
  document.getElementById("auth-overlay").style.display="block";
  authMsg("");
}
function closeAuth(){
  document.getElementById("auth-overlay").style.display="none";
  authMsg("");
}
function setAuthTab(tab){
  const tLogin = document.getElementById("tab-login");
  const tReg = document.getElementById("tab-register");
  const pLogin = document.getElementById("panel-login");
  const pReg = document.getElementById("panel-register");
  const onLogin = tab==="login";
  tLogin.classList.toggle("active", onLogin);
  tReg.classList.toggle("active", !onLogin);
  pLogin.style.display = onLogin ? "" : "none";
  pReg.style.display = onLogin ? "none" : "";
  authMsg("");
}

async function doRegister(){
  loadUsers();

  const email = normalizeEmail(document.getElementById("reg-email").value);
  const pass = String(document.getElementById("reg-pass").value || "");
  const pass2 = String(document.getElementById("reg-pass2").value || "");
  const username = String(document.getElementById("reg-username").value || "").trim();

  if(!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){
    authMsg("メールアドレスが不正です", "err"); return;
  }
  if(pass.length < 4){
    authMsg("パスワードは4文字以上にしてください", "err"); return;
  }
  if(pass !== pass2){
    authMsg("パスワード確認が一致しません", "err"); return;
  }
  if(username.length > 20){
    authMsg("ユーザー名は20文字以内です", "err"); return;
  }
  if(findUser(email)){
    authMsg("このメールアドレスは既に登録済みです", "err"); return;
  }

  let passHash;
  try{
    passHash = await sha256(pass);
  }catch(e){
    passHash = simpleHash(pass);
  }

  users.push({
    email,
    passHash,
    profile:{
      username: username || email.split("@")[0] || "User",
      bio:"",
      avatar: (getSelectedWebIcon() && getSelectedWebIcon().dataUrl) ? getSelectedWebIcon().dataUrl : "https://via.placeholder.com/100"
    },
    following:[],
    blocked:[],
    muted:[],
    createdAt: Date.now()
  });
  saveUsers();

  setCurrentUser(email);
  saveSession(email, true);

  authMsg("登録しました。ログイン完了!", "ok");

  document.getElementById("reg-email").value="";
  document.getElementById("reg-pass").value="";
  document.getElementById("reg-pass2").value="";
  document.getElementById("reg-username").value="";

  closeAuth();
  afterAuthBootstrap();
}

async function doLogin(){
  loadUsers();

  const email = normalizeEmail(document.getElementById("login-email").value);
  const pass = String(document.getElementById("login-pass").value || "");
  const remember = !!document.getElementById("login-remember").checked;

  if(!email){
    authMsg("メールアドレスを入力してください", "err"); return;
  }
  const u = findUser(email);
  if(!u || !u.passHash){
    authMsg("ユーザーが見つかりません(会員登録してください)", "err"); return;
  }

  const passHash = await sha256(pass);
  if(passHash !== u.passHash){
    authMsg("パスワードが違います", "err"); return;
  }

  setCurrentUser(email);
  if(remember) saveSession(email, true);
  else clearSession();

  authMsg("ログインしました", "ok");
  closeAuth();
  afterAuthBootstrap();
}

function doLogout(){
  if(!confirm("ログアウトしますか?")) return;
  stopRss(true);
  clearCurrentUser();
  clearSession();
  openAuth();
  setAuthTab("login");
  lockAppForAuth(true);
  syncProfileUI();
  renderTimeline();
  rebuildVRWall();
}

function lockAppForAuth(locked){
  const main = document.getElementById("main-content");
  const header = document.getElementById("app-header");
  const footer = document.querySelector("footer");
  const disable = (el)=>{
    if(!el) return;
    el.style.filter = locked ? "blur(2px)" : "";
    el.style.opacity = locked ? "0.65" : "";
    el.style.pointerEvents = locked ? "none" : "";
    el.style.userSelect = locked ? "none" : "";
  };
  disable(main); disable(header); disable(footer);
}

function restoreAuthState(){
  const s = loadSession();
  if(s && s.email && findUser(s.email)){
    setCurrentUser(s.email);
    return true;
  }
  const cur = JSON.parse(LS.get(KEY.current, "null") || "null");
  if(cur && cur.email && findUser(cur.email)){
    setCurrentUser(cur.email);
    return true;
  }
  clearCurrentUser();
  return false;
}

/* ---------- Posts ---------- */
function loadPosts(){ posts = JSON.parse(LS.get(KEY.posts, "[]") || "[]"); if(!Array.isArray(posts)) posts=[]; }
function savePosts(){ LS.set(KEY.posts, JSON.stringify(posts)); }
function countMyPosts(){
  const me=getMe(); if(!me) return 0;
  return posts.filter(p=>p.authorEmail===me.email && p.type==="user").length;
}

/* ---------- Appearance ---------- */
function applyHeaderBg(dataUrl){ const h=document.getElementById("app-header"); if(h) h.style.backgroundImage=`url('${dataUrl}')`; LS.set(KEY.headerBg, dataUrl); }
function resetHeaderBg(){ const h=document.getElementById("app-header"); if(h) h.style.backgroundImage=""; LS.del(KEY.headerBg); }
function loadAppearance(){
  const defaultFeed = (WEB_ICONS.find(x=>x.id==="eye_squ") || WEB_ICONS[0] || null);
  const DEFAULT_ICONS = { feed: defaultFeed ? defaultFeed.dataUrl : "https://via.placeholder.com/40" };

  const ico = JSON.parse(LS.get(KEY.icons, "null") || "null");
  if(ico && typeof ico==="object") icons = { ...DEFAULT_ICONS, ...ico };
  else icons = { ...DEFAULT_ICONS };

  const hb = LS.get(KEY.headerBg, null); if(hb) applyHeaderBg(hb);
  const fav = LS.get(KEY.favicon, null); if(fav) applyFavicon(fav); else applyFavicon(document.getElementById("favicon-svg")?.href);

  const me = getMe();
  document.getElementById("header-avatar").src = (me && me.profile && me.profile.avatar) ? me.profile.avatar : (getSelectedWebIcon()?.dataUrl || "https://via.placeholder.com/80");
}

/* ---------- Profile UI ---------- */
function syncProfileUI(){
  const me = getMe();
  const isLoggedIn = !!me;

  document.getElementById("header-email").textContent = me ? me.email : "";
  document.getElementById("header-username").textContent = me ? (me.profile.username||"未設定") : "未ログイン";
  document.getElementById("header-avatar").src = me ? (me.profile.avatar||"https://via.placeholder.com/80") : (getSelectedWebIcon()?.dataUrl || "https://via.placeholder.com/80");

  document.getElementById("profile-avatar").src = me ? (me.profile.avatar||"https://via.placeholder.com/100") : (getSelectedWebIcon()?.dataUrl || "https://via.placeholder.com/100");
  document.getElementById("inp-username").value = me ? (me.profile.username||"") : "";
  document.getElementById("inp-bio").value = me ? (me.profile.bio||"") : "";
  document.getElementById("preview-username").textContent = me ? (me.profile.username||"未設定") : "未ログイン";
  document.getElementById("preview-bio").textContent = me ? (me.profile.bio||"まだ自己紹介がありません") : "ログインしてください";

  document.getElementById("btn-logout").style.display = isLoggedIn ? "" : "none";
}
function saveProfile(){
  const me=getMe();
  if(!me) return alert("ログインしてください");
  const name=(document.getElementById("inp-username").value||"").trim();
  const bio=(document.getElementById("inp-bio").value||"").trim();
  if(name.length>20) return alert("ユーザー名は20文字以内です");
  me.profile.username=name||me.profile.username||"User";
  me.profile.bio=bio;
  saveUsers();
  syncProfileUI();
  syncStats();
  renderTimeline();
  rebuildVRWall();
  alert("保存しました");
}
function syncStats(){
  const me=getMe();
  if(!me){
    document.getElementById("stat-followers").textContent="0";
    document.getElementById("stat-following").textContent="0";
    document.getElementById("stat-posts").textContent="0";
    return;
  }
  const followers=users.filter(u=>(u.following||[]).includes(me.email)).length;
  document.getElementById("stat-followers").textContent=String(followers);
  document.getElementById("stat-following").textContent=String((me.following||[]).length);
  document.getElementById("stat-posts").textContent=String(countMyPosts());
}

/* ---------- Post URL ---------- */
function postUrl(postId){
  const base = location.href.split("#")[0];
  return `${base}#post=${encodeURIComponent(postId)}`;
}
function setLatestPostUrl(postId){
  const u = postUrl(postId);
  LS.set(KEY.latest, u);
  document.getElementById("latest-post-url").textContent = u;
}
function loadLatestPostUrl(){
  const u = LS.get(KEY.latest, "");
  document.getElementById("latest-post-url").textContent = u || "(まだ投稿していません)";
}
function highlightFromHash(scroll=true){
  const h = location.hash || "";
  const m = h.match(/post=([^&]+)/);
  if(!m) return;
  const pid = decodeURIComponent(m[1]);
  const el = document.getElementById("post-"+pid);
  if(!el) return;
  el.style.outline="3px solid rgba(91,95,230,.35)";
  el.style.boxShadow="0 0 0 6px rgba(91,95,230,.10)";
  if(scroll) el.scrollIntoView({behavior:"smooth", block:"center"});
  setTimeout(()=>{ el.style.outline=""; el.style.boxShadow=""; }, 2200);
}

/* ---------- Media attach ---------- */
function refreshMediaPreview(){
  const wrap=document.getElementById("media-preview");
  wrap.innerHTML="";
  if(pendingMedia.length===0) return;
  pendingMedia.forEach((m,idx)=>{
    const card=document.createElement("div");
    card.className="glass p-3 border border-slate-200";
    const top=document.createElement("div");
    top.className="flex items-center justify-between mb-2";
    top.innerHTML=`<div class="text-xs font-semibold">${escapeHtml(m.name)} <span class="muted">(${bytesToMB(m.size)}MB)</span></div>
                   <button class="pill px-3 py-1 text-xs hover:bg-slate-50">削除</button>`;
    top.querySelector("button").onclick=()=>{ pendingMedia.splice(idx,1); refreshMediaPreview(); };
    const body=document.createElement("div");
    if((m.type||"").startsWith("image/")) body.innerHTML=`<img src="${m.dataUrl}" class="w-full rounded-xl border border-slate-200 object-cover" style="max-height:180px">`;
    else body.innerHTML=`<video src="${m.dataUrl}" controls class="w-full rounded-xl border border-slate-200" style="max-height:180px"></video>`;
    card.appendChild(top); card.appendChild(body); wrap.appendChild(card);
  });
}
function clearMedia(){ pendingMedia=[]; refreshMediaPreview(); }

/* =========================================================
   RSS(ON/OFF)
========================================================= */
function loadRss(){
  let rss = JSON.parse(LS.get(KEY.rss, "null") || "null");
  if(!rss){
    rss = { feeds: PRESET_RSS.slice(), intervalSec: 120, enabled: false, lastRun: null };
    LS.set(KEY.rss, JSON.stringify(rss));
  }
  if(!Array.isArray(rss.feeds)) rss.feeds = PRESET_RSS.slice();
  rss.intervalSec = Math.max(10, Number(rss.intervalSec)||120);
  rss.enabled = !!rss.enabled;
  return rss;
}
function saveRss(rss){ LS.set(KEY.rss, JSON.stringify(rss)); }
function loadSeen(){ let seen=JSON.parse(LS.get(KEY.rssSeen,"{}")||"{}"); if(!seen||typeof seen!=="object") seen={}; return seen; }
function saveSeen(seen){ LS.set(KEY.rssSeen, JSON.stringify(seen)); }

function setRssIndicator(running){
  document.getElementById("rss-dot").className = "status-dot " + (running ? "on":"off");
  document.getElementById("rss-status-text").textContent = running ? "動作中":"停止中";
}
function setToggleUI(enabled){
  const t = document.getElementById("rss-toggle");
  t.classList.toggle("on", !!enabled);
  t.setAttribute("aria-checked", enabled ? "true":"false");
}
function rssUI(){
  const rss=loadRss();
  document.getElementById("rss-interval").value=rss.intervalSec||120;
  const wrap=document.getElementById("rss-list");
  wrap.innerHTML="";
  (rss.feeds||[]).forEach((u,i)=>{
    const row=document.createElement("div");
    row.className="glass p-3 border border-slate-200 flex items-center gap-2";
    row.innerHTML=`<div class="text-[11px] flex-1 truncate" title="${escapeHtml(u)}">${escapeHtml(u)}</div>
                   <button class="pill px-3 py-2 text-xs hover:bg-slate-50" title="削除"><i class="fas fa-trash"></i></button>`;
    row.querySelector("button").onclick=()=>{
      if(!confirm("このRSSを削除しますか?")) return;
      const r=loadRss(); r.feeds.splice(i,1); saveRss(r); rssUI();
      addLog("削除: "+u,"info");
    };
    wrap.appendChild(row);
  });
  setToggleUI(rss.enabled);
  setRssIndicator(!!rssTimer);
}
function setRssEnabled(enabled){
  const rss = loadRss();
  rss.enabled = !!enabled;
  saveRss(rss);
  setToggleUI(rss.enabled);
  if(rss.enabled) startRss();
  else stopRss(true);
}
function addRssFeed(){
  const inp=document.getElementById("rss-url");
  const url=(inp.value||"").trim();
  if(!/^https?:\/\/.+/i.test(url)) return addLog("正しいRSS URLを入れてください","err");
  const rss=loadRss();
  if(rss.feeds.includes(url)) return addLog("すでに登録済みです","err");
  rss.feeds.push(url); saveRss(rss); inp.value="";
  rssUI();
  addLog("追加: "+url,"ok");
}
function setRssInterval(){
  const v=Number(document.getElementById("rss-interval").value)||120;
  if(v<10) return alert("10秒以上にしてください");
  const rss=loadRss(); rss.intervalSec=v; saveRss(rss);
  addLog("間隔設定: "+v+"秒","ok");
  if(rssTimer){ startRss(); }
}
function clearSeen(){
  LS.del(KEY.rssSeen);
  addLog("既読リセット: ts_rss_seen を削除","ok");
}

/* RSS fetch */
async function fetchTextDirect(url){
  const res = await fetch(url, { cache:"no-store" });
  if(!res.ok) throw new Error("Direct HTTP "+res.status);
  return await res.text();
}
async function fetchTextViaAllOriginsRaw(url){
  const endpoint = "https://api.allorigins.win/raw?url="+encodeURIComponent(url);
  const res=await fetch(endpoint,{cache:"no-store"});
  if(!res.ok) throw new Error("AllOrigins HTTP "+res.status);
  return await res.text();
}
async function fetchTextViaThingProxy(url){
  const endpoint = "https://thingproxy.freeboard.io/fetch/"+url;
  const res=await fetch(endpoint,{cache:"no-store"});
  if(!res.ok) throw new Error("ThingProxy HTTP "+res.status);
  return await res.text();
}
async function fetchJsonViaRss2Json(url){
  const endpoint="https://api.rss2json.com/v1/api.json?rss_url="+encodeURIComponent(url);
  const res=await fetch(endpoint,{cache:"no-store"});
  if(!res.ok) throw new Error("rss2json HTTP "+res.status);
  const j=await res.json();
  if(j && j.status && j.status!=="ok") throw new Error("rss2json status="+j.status+" msg="+(j.message||""));
  return j;
}
function parseXmlToItems(xmlText){
  const dp=new DOMParser();
  const doc=dp.parseFromString(xmlText,"text/xml");
  if(doc.querySelector("parsererror")) throw new Error("XML parse error");
  const nodes = Array.from(doc.querySelectorAll("item, entry")).slice(0, 20);
  const pickText = (n, sels) => {
    for(const s of sels){
      const el = n.querySelector(s);
      if(el && el.textContent) return el.textContent.trim();
    }
    return "";
  };
  return nodes.map(n=>{
    const title = pickText(n, ["title"]);
    let link = "";
    const linkEl = n.querySelector("link");
    if(linkEl){
      link = (linkEl.getAttribute("href")||"").trim() || (linkEl.textContent||"").trim();
    }
    if(!link){ link = pickText(n, ["id"]); }
    const pub = pickText(n, ["pubDate","published","updated","dc\\:date","date"]);
    return { title: title || "(no title)", link, pubDate: pub };
  }).filter(x=>x.title || x.link);
}
function normalizeItems(items){
  return (items||[]).map(it=>{
    const title=(it.title||"").trim();
    const link=(it.link||"").trim();
    const pubDate=(it.pubDate||it.pub||"").trim();
    return { title:title||"(no title)", link, pubDate };
  }).filter(it=>it.title || it.link);
}
async function fetchFeedItems(feedUrl, debug=false){
  const url = feedUrl.trim();
  let lastErr = null;
  try{
    const xml = await fetchTextDirect(url);
    const items = normalizeItems(parseXmlToItems(xml));
    if(items.length){ if(debug) addLog(`取得成功(Direct): ${items.length}件`,"ok"); return items; }
  }catch(e){ lastErr=e; if(debug) addLog("Direct失敗: "+(e.message||e),"err"); }
  try{
    const xml = await fetchTextViaAllOriginsRaw(url);
    const items = normalizeItems(parseXmlToItems(xml));
    if(debug) addLog(`取得成功(AllOrigins): ${items.length}件`,"ok");
    return items;
  }catch(e){ lastErr=e; if(debug) addLog("AllOrigins失敗: "+(e.message||e),"err"); }
  try{
    const xml = await fetchTextViaThingProxy(url);
    const items = normalizeItems(parseXmlToItems(xml));
    if(debug) addLog(`取得成功(ThingProxy): ${items.length}件`,"ok");
    return items;
  }catch(e){ lastErr=e; if(debug) addLog("ThingProxy失敗: "+(e.message||e),"err"); }
  try{
    const j = await fetchJsonViaRss2Json(url);
    const items = normalizeItems((j.items||[]).map(x=>({ title:x.title, link:x.link, pubDate:x.pubDate })));
    if(debug) addLog(`取得成功(rss2json): ${items.length}件`,"ok");
    return items;
  }catch(e){ lastErr=e; if(debug) addLog("rss2json失敗: "+(e.message||e),"err"); }
  throw lastErr || new Error("RSS fetch failed");
}
function startRss(){
  stopRss(false);
  const rss = loadRss();
  if(!rss.enabled){
    rss.enabled = true; saveRss(rss); setToggleUI(true);
  }
  setRssIndicator(true);
  addLog("RSS自動投稿: ON(定期取得開始)","ok");
  fetchAllFeedsOnce(true);
  rssTimer = setInterval(()=>{ fetchAllFeedsOnce(false); }, (rss.intervalSec||120)*1000);
}
function stopRss(writeState=true){
  if(rssTimer){ clearInterval(rssTimer); rssTimer=null; }
  setRssIndicator(false);
  if(writeState){
    const rss = loadRss();
    rss.enabled = false;
    saveRss(rss);
    setToggleUI(false);
    addLog("RSS自動投稿: OFF(停止)","info");
  }
}
function createPost({text, type="user", link="", media=[], authorEmail=null}){
  const me=getMe();
  if(type==="user" && !me){
    alert("ログインしてください");
    openAuth();
    return null;
  }
  const post={
    id: uid(),
    text:(text||"").trim(),
    type,
    link,
    media,
    time: nowJp(),
    authorEmail: authorEmail || (me ? me.email : "anonymous@local")
  };
  posts.unshift(post);
  savePosts();
  setLatestPostUrl(post.id);
  syncStats();
  renderTimeline();
  return post;
}
async function fetchAllFeedsOnce(verbose=false){
  const rss = loadRss();
  if(!rss.feeds || rss.feeds.length===0){
    addLog("RSSが未登録です","err");
    return;
  }
  if(verbose) addLog(`取得開始: feeds=${rss.feeds.length}`,"info");
  const seen = loadSeen();
  let newCount = 0;
  for(const feedUrl of rss.feeds){
    try{
      const items = await fetchFeedItems(feedUrl, verbose);
      for(const it of items.slice(0, 10)){
        const link = safeUrl(it.link) || "";
        const key = link || (feedUrl + "::" + (it.title||""));
        if(!key) continue;
        if(seen[key]) continue;
        seen[key]=true;
        createPost({
          text: it.title || "(no title)",
          type: "feed",
          link: link || "",
          media: [],
          authorEmail: "feed@local"
        });
        newCount++;
        if(newCount >= 20) break;
      }
    }catch(e){
      addLog(`失敗: ${feedUrl} / ${(e.message||e)}`,"err");
    }
    if(newCount >= 20) break;
  }
  saveSeen(seen);
  if(verbose || newCount>0){
    addLog(`取得完了: 新規 ${newCount}件` , newCount ? "ok" : "info");
  }
  const rr = loadRss();
  rr.lastRun = Date.now();
  saveRss(rr);
}
async function rssTestOne(){
  const url=(document.getElementById("rss-url").value||"").trim();
  const target=url || (loadRss().feeds[0]||"");
  if(!target) return alert("テストするURLがありません");
  addLog("URL検査: "+target,"info");
  try{
    const items = await fetchFeedItems(target, true);
    addLog(`検査OK: items=${items.length}(先頭: ${items[0]?items[0].title:"なし"})`, items.length? "ok":"info");
  }catch(e){
    addLog("検査失敗: "+(e.message||e),"err");
    alert("RSS検査で失敗しました。ログを見てください。");
  }
}

/* =========================================================
   Timeline rendering
========================================================= */
function badgeForType(p){
  if(p.type==="feed") return `<span class="badge"><i class="fas fa-rss"></i>FEED</span>`;
  return `<span class="badge"><i class="fas fa-user"></i>ユーザー</span>`;
}
function iconForPost(p){
  if(p.type==="feed") return icons.feed;
  const u=users.find(x=>x.email===p.authorEmail);
  return (u && u.profile && u.profile.avatar) ? u.profile.avatar : (getSelectedWebIcon()?.dataUrl || "https://via.placeholder.com/40");
}
function usernameForPost(p){
  if(p.type==="feed") return "Feed";
  const u=users.find(x=>x.email===p.authorEmail);
  return (u && u.profile && u.profile.username) ? u.profile.username : "Unknown";
}
function visibleInHome(p){
  if(homeMode!=="home") return true;
  const me=getMe();
  if(!me){ return true; }
  if((me.blocked||[]).includes(p.authorEmail)) return false;
  if((me.muted||[]).includes(p.authorEmail)) return false;
  if(p.type==="feed") return true;
  if(p.authorEmail===me.email) return true;
  return (me.following||[]).includes(p.authorEmail);
}
function filterPosts(){
  let arr=posts.slice();
  arr=arr.filter(p=>visibleInHome(p));
  arr=arr.filter(p=>{
    if(currentFilter==="all") return true;
    if(currentFilter==="user") return p.type==="user";
    if(currentFilter==="feed") return p.type==="feed";
    return true;
  });
  if(searchQuery.trim()){
    const q=searchQuery.trim().toLowerCase();
    arr=arr.filter(p=>((p.text||"")+" "+usernameForPost(p)).toLowerCase().includes(q));
  }
  return arr;
}
async function copyArticleUrl(pid){
  const p=posts.find(x=>String(x.id)===String(pid));
  const u=p?safeUrl(p.link):"";
  if(!u) return alert("記事URLがありません");
  await copyText(u);
  alert("記事URLをコピーしました");
}
function openArticleUrl(pid){
  const p=posts.find(x=>String(x.id)===String(pid));
  const u=p?safeUrl(p.link):"";
  if(!u) return alert("記事URLがありません");
  openPopup(u);
}
function renderTimeline(){
  const tl=document.getElementById("timeline");
  const emp=document.getElementById("empty");
  const cnt=document.getElementById("post-count");
  const arr=filterPosts();
  cnt.textContent=`(${arr.length}件)`;
  if(arr.length===0){ tl.innerHTML=""; emp.style.display=""; return; }
  emp.style.display="none";
  tl.innerHTML = arr.map(p=>{
    const pid=String(p.id);
    const myPostUrl=postUrl(pid);
    const articleUrl=safeUrl(p.link);
    const mainText = articleUrl
      ? `<a href="${escapeHtml(articleUrl)}" target="_blank" rel="noopener" class="text-sky-600 underline">${escapeHtml(p.text)}</a>`
      : escapeHtml(p.text);
    const articleRow = articleUrl ? `
      <div class="mt-3 glass p-3 border border-slate-200">
        <div class="flex items-center justify-between gap-2">
          <div class="text-xs font-semibold flex items-center gap-2">
            <i class="fas fa-link text-amber-600"></i> 記事URL
          </div>
          <div class="flex items-center gap-2">
            <button class="pill px-3 py-2 text-xs hover:bg-slate-50" onclick="copyArticleUrl('${escapeHtml(pid)}')">
              <i class="fas fa-copy mr-2"></i>コピー
            </button>
            <button class="pill px-3 py-2 text-xs hover:bg-slate-50" onclick="openArticleUrl('${escapeHtml(pid)}')">
              <i class="fas fa-arrow-up-right-from-square mr-2"></i>開く
            </button>
          </div>
        </div>
        <div class="mt-2 text-xs muted break-all">${escapeHtml(articleUrl)}</div>
      </div>
    ` : "";
    const mediaHtml=(p.media && p.media.length) ? `
      <div class="grid grid-cols-2 md:grid-cols-3 gap-3 mt-3">
        ${p.media.map(m=>{
          if((m.type||"").startsWith("image/")) return `<img src="${m.dataUrl}" class="w-full rounded-xl border border-slate-200 object-cover" style="max-height:220px">`;
          return `<video src="${m.dataUrl}" controls class="w-full rounded-xl border border-slate-200" style="max-height:220px"></video>`;
        }).join("")}
      </div>` : "";
    return `
      <article class="timeline-post p-6" id="post-${escapeHtml(pid)}" data-postid="${escapeHtml(pid)}">
        <div class="flex items-start justify-between gap-3">
          <div class="flex items-center gap-3">
            <img src="${escapeHtml(iconForPost(p))}" class="mini-avatar" onerror="this.src='https://via.placeholder.com/40'">
            <div>
              <div class="flex items-center gap-2">
                <div class="font-semibold">${escapeHtml(usernameForPost(p))}</div>
                ${badgeForType(p)}
              </div>
              <div class="text-xs muted">${escapeHtml(p.time)}</div>
            </div>
          </div>
        </div>
        <div class="mt-3 text-sm leading-relaxed">${mainText}</div>
        ${articleRow}
        ${mediaHtml}
        <div class="mt-3 text-xs muted break-all">
          <i class="fas fa-link mr-2 text-indigo-600"></i>投稿URL: ${escapeHtml(myPostUrl)}
        </div>
      </article>
    `;
  }).join("");
  highlightFromHash(false);
}

/* ---------- Export/Import/Clear ---------- */
function deleteAllPosts(){
  if(!confirm("全投稿を削除しますか?")) return;
  posts=[]; savePosts(); syncStats(); renderTimeline(); rebuildVRWall();
}
function exportData(){
  const payload = {
    users, posts, icons,
    headerBg: LS.get(KEY.headerBg, null),
    favicon: LS.get(KEY.favicon, null),
    rss: JSON.parse(LS.get(KEY.rss, "null") || "null"),
    session: JSON.parse(LS.get(KEY.session, "null") || "null"),
    current: JSON.parse(LS.get(KEY.current, "null") || "null"),
    webIconSelected: JSON.parse(LS.get(KEY.webIconSelected, "null") || "null")
  };
  const blob=new Blob([JSON.stringify(payload,null,2)],{type:"application/json"});
  const url=URL.createObjectURL(blob);
  const a=document.createElement("a");
  a.href=url; a.download="tsumugi_backup.json"; a.click();
  URL.revokeObjectURL(url);
}
function importData(){
  const input=document.createElement("input");
  input.type="file"; input.accept="application/json";
  input.onchange=()=>{
    const f=input.files && input.files[0]; if(!f) return;
    const r=new FileReader();
    r.onload=()=>{
      try{
        const obj=JSON.parse(String(r.result||"{}"));
        if(obj.users){ users=obj.users; saveUsers(); }
        if(obj.posts){ posts=obj.posts; savePosts(); }
        if(obj.icons){ icons=obj.icons; LS.set(KEY.icons, JSON.stringify(icons)); }
        if(obj.headerBg){ applyHeaderBg(obj.headerBg); }
        if(obj.favicon){ applyFavicon(obj.favicon); }
        if(obj.rss){ LS.set(KEY.rss, JSON.stringify(obj.rss)); }
        if(obj.session){ LS.set(KEY.session, JSON.stringify(obj.session)); }
        if(obj.current){ LS.set(KEY.current, JSON.stringify(obj.current)); }
        if(obj.webIconSelected){ LS.set(KEY.webIconSelected, JSON.stringify(obj.webIconSelected)); }
        alert("読み込み完了");
        init();
      }catch(e){
        alert("読み込み失敗: "+(e.message||e));
      }
    };
    r.readAsText(f);
  };
  input.click();
}
function clearAllCache(){
  if(!confirm("Tsumugiのローカルデータを全消去します。よろしいですか?")) return;
  Object.keys(localStorage).forEach(k=>{ if(k.startsWith("ts_")) localStorage.removeItem(k); });
  alert("全消去しました。再読み込みします。");
  location.reload();
}

/* =========================================================
   VR試験
========================================================= */
function openVR(){
  document.getElementById("vr-overlay").style.display="block";
  rebuildVRWall();
}
function closeVR(){
  document.getElementById("vr-overlay").style.display="none";
}
function rebuildVRWall(){
  const wall = document.getElementById("vr-wall");
  if(!wall) return;
  wall.innerHTML="";
  const arr = filterPosts().slice(0, 10);
  const startX = -3.6, startY = 2.2, gapX = 2.5, gapY = 1.35, cols = 3;
  arr.forEach((p, idx)=>{
    const c = idx % cols;
    const r = Math.floor(idx / cols);
    const x = startX + c*gapX;
    const y = startY - r*gapY;
    const z = -4.0;
    const title = (p.text||"").slice(0, 46);
    const link = safeUrl(p.link);
    const line2 = link ? "URL: "+link.slice(0, 40) : "";
    const text = `${usernameForPost(p)}\n${title}${line2?("\n"+line2):""}`;
    const card = document.createElement("a-entity");
    card.setAttribute("position", `${x} ${y} ${z}`);
    const plane = document.createElement("a-plane");
    plane.setAttribute("width","2.3");
    plane.setAttribute("height","1.2");
    plane.setAttribute("color","#111827");
    plane.setAttribute("opacity","0.88");
    plane.setAttribute("material","side:double");
    card.appendChild(plane);
    const txt = document.createElement("a-text");
    txt.setAttribute("value", text);
    txt.setAttribute("color","#e5e7eb");
    txt.setAttribute("width","2.2");
    txt.setAttribute("position","-1.05 0.42 0.01");
    txt.setAttribute("baseline","top");
    txt.setAttribute("wrap-count","28");
    card.appendChild(txt);
    if(link){
      card.addEventListener("click", ()=>{
        closeVR();
        openPopup(link);
      });
    }
    wall.appendChild(card);
  });
}

/* =========================================================
   Events / Init
========================================================= */
function wireEvents(){
  document.getElementById("btn-vr").onclick = ()=>openVR();
  document.getElementById("btn-clear-all").onclick = ()=>clearAllCache();

  document.getElementById("btn-auth-open").onclick = ()=>openAuth();
  document.getElementById("btn-logout").onclick = ()=>doLogout();

  // Auth overlay
  document.getElementById("btn-auth-close").onclick = ()=>closeAuth();
  document.getElementById("tab-login").onclick = ()=>setAuthTab("login");
  document.getElementById("tab-register").onclick = ()=>setAuthTab("register");
  document.getElementById("btn-register").onclick = ()=>doRegister();
  document.getElementById("btn-login").onclick = ()=>doLogin();
  document.getElementById("btn-guest").onclick = ()=>{
    loadUsers();
    ensureGuest();
    saveSession("guest@local", true);
    closeAuth();
    afterAuthBootstrap();
  };

  // VR
  document.getElementById("btn-vr-exit").onclick = ()=>closeVR();
  document.getElementById("btn-vr-enter").onclick = ()=>{
    rebuildVRWall();
    alert("VR試験: マウスで視点、WASD移動(記事カードはクリックで記事を開きます)");
  };

  // profile
  document.getElementById("btn-save-profile").onclick = ()=>saveProfile();
  document.getElementById("btn-change-profile").onclick = ()=>document.getElementById("file-profile").click();
  document.getElementById("file-profile").onchange = (e)=>{
    const me=getMe();
    if(!me) return alert("ログインしてください");
    const f=e.target.files && e.target.files[0]; if(!f) return;
    const r=new FileReader();
    r.onload=()=>{
      me.profile.avatar=String(r.result||"");
      saveUsers();
      syncProfileUI();
      renderTimeline();
      rebuildVRWall();
    };
    r.readAsDataURL(f);
  };

  // headerbg / favicon / feed icon(ファイル)
  document.getElementById("btn-change-headerbg").onclick = ()=>document.getElementById("file-headerbg").click();
  document.getElementById("file-headerbg").onchange = (e)=>{
    const f=e.target.files && e.target.files[0]; if(!f) return;
    const r=new FileReader();
    r.onload=()=>applyHeaderBg(String(r.result||""));
    r.readAsDataURL(f);
  };
  document.getElementById("btn-reset-headerbg").onclick = ()=>resetHeaderBg();

  document.getElementById("btn-change-appicon").onclick = ()=>document.getElementById("file-appicon").click();
  document.getElementById("file-appicon").onchange = (e)=>{
    const f=e.target.files && e.target.files[0]; if(!f) return;
    const r=new FileReader();
    r.onload=()=>applyFavicon(String(r.result||""));
    r.readAsDataURL(f);
  };
  document.getElementById("btn-reset-appicon").onclick = ()=>resetFavicon();

  document.getElementById("btn-change-feedicon").onclick = ()=>document.getElementById("file-feedicon").click();
  document.getElementById("file-feedicon").onchange = (e)=>{
    const f=e.target.files && e.target.files[0]; if(!f) return;
    const r=new FileReader();
    r.onload=()=>{
      icons.feed=String(r.result||icons.feed);
      LS.set(KEY.icons, JSON.stringify(icons));
      renderTimeline();
      rebuildVRWall();
    };
    r.readAsDataURL(f);
  };

  // WEBアイコン適用
  document.getElementById("btn-apply-favicon").onclick = ()=>applySelectedToFavicon();
  document.getElementById("btn-apply-feed").onclick = ()=>applySelectedToFeed();
  document.getElementById("btn-apply-profile").onclick = ()=>applySelectedToProfile();
  document.getElementById("btn-icon-random").onclick = ()=>randomPickIcon();

  // RSS toggle
  const toggle = document.getElementById("rss-toggle");
  const toggleFlip = ()=>{
    const rss = loadRss();
    setRssEnabled(!rss.enabled);
    rssUI();
  };
  toggle.onclick = toggleFlip;
  toggle.onkeydown = (e)=>{ if(e.key==="Enter" || e.key===" "){ e.preventDefault(); toggleFlip(); } };

  // RSS buttons
  document.getElementById("btn-add-rss").onclick = ()=>addRssFeed();
  document.getElementById("btn-fetch-rss").onclick = ()=>fetchAllFeedsOnce(true);
  document.getElementById("btn-set-rss-interval").onclick = ()=>setRssInterval();
  document.getElementById("btn-rss-clear-seen").onclick = ()=>clearSeen();
  document.getElementById("btn-rss-test-one").onclick = ()=>rssTestOne();

  // posting
  const postText=document.getElementById("post-text");
  postText.addEventListener("input", ()=>{ document.getElementById("char-count").textContent=`(${postText.value.length}/600)`; });
  postText.addEventListener("keydown", (e)=>{ if(e.ctrlKey && e.key==="Enter"){ document.getElementById("btn-post").click(); } });

  document.getElementById("btn-post").onclick = ()=>{
    const t=(postText.value||"").trim();
    if(!t && pendingMedia.length===0) return alert("本文か添付が必要です");
    const p = createPost({ text:t||"(添付)", type:"user", media: pendingMedia.slice() });
    if(!p) return;
    postText.value=""; document.getElementById("char-count").textContent="(0/600)";
    clearMedia();
  };
  document.getElementById("btn-delete-all-posts").onclick = ()=>deleteAllPosts();

  // media
  document.getElementById("btn-add-media").onclick = ()=>document.getElementById("file-media").click();
  document.getElementById("file-media").onchange = async (e)=>{
    const me=getMe();
    if(!me){ alert("ログインしてください"); openAuth(); e.target.value=""; return; }
    const files = Array.from(e.target.files||[]);
    if(!files.length) return;
    for(const f of files){
      const dataUrl = await new Promise((res,rej)=>{
        const r=new FileReader();
        r.onload=()=>res(String(r.result||""));
        r.onerror=()=>rej(new Error("file read error"));
        r.readAsDataURL(f);
      });
      pendingMedia.push({ name:f.name, type:f.type||"application/octet-stream", size:f.size||0, dataUrl });
      if(pendingMedia.length>9){ pendingMedia=pendingMedia.slice(0,9); break; }
    }
    refreshMediaPreview();
    e.target.value="";
  };
  document.getElementById("btn-clear-media").onclick = ()=>clearMedia();

  // latest post url
  document.getElementById("btn-copy-post-url").onclick = async ()=>{
    const u=document.getElementById("latest-post-url").textContent.trim();
    if(!u || u.startsWith("(")) return alert("まだ投稿URLがありません");
    await copyText(u); alert("投稿URLをコピーしました");
  };
  document.getElementById("btn-open-post-url").onclick = ()=>{
    const u=document.getElementById("latest-post-url").textContent.trim();
    if(!u || u.startsWith("(")) return alert("まだ投稿URLがありません");
    location.href = u;
  };

  // timeline filter/search
  document.querySelectorAll("[data-filter]").forEach(btn=>{
    btn.onclick = ()=>{
      currentFilter = btn.getAttribute("data-filter") || "all";
      document.querySelectorAll("[data-filter]").forEach(b=>{
        b.classList.remove("btn-primary"); b.classList.add("pill","hover:bg-slate-50");
      });
      btn.classList.remove("pill","hover:bg-slate-50");
      btn.classList.add("btn-primary");
      renderTimeline();
      rebuildVRWall();
    };
  });
  document.getElementById("search").addEventListener("input",(e)=>{
    searchQuery = e.target.value || "";
    renderTimeline();
    rebuildVRWall();
  });
  document.getElementById("btn-refresh").onclick = ()=>{ renderTimeline(); rebuildVRWall(); };
  document.getElementById("btn-export").onclick = ()=>exportData();
  document.getElementById("btn-import").onclick = ()=>importData();

  // home/explore
  document.getElementById("btn-home").onclick = ()=>{ homeMode="home"; renderTimeline(); rebuildVRWall(); };
  document.getElementById("btn-explore").onclick = ()=>{ homeMode="explore"; renderTimeline(); rebuildVRWall(); };
}

/* ログイン後にUIを再同期する */
function afterAuthBootstrap(){
  lockAppForAuth(false);
  loadAppearance();
  syncProfileUI();
  syncStats();
  renderTimeline();
  loadLatestPostUrl();
  rssUI();
  rebuildVRWall();

  renderWebIconGrid();

  const rss = loadRss();
  if(rss.enabled){
    startRss();
  }else{
    stopRss(false);
    setToggleUI(false);
    setRssIndicator(false);
  }
}

/* 起動 */
function init(){
  buildWebIconPack();
  loadSelectedWebIcon();

  loadUsers();
  loadPosts();

  const loggedIn = restoreAuthState();

  loadAppearance();
  renderWebIconGrid();

  syncProfileUI();
  syncStats();
  renderTimeline();
  loadLatestPostUrl();
  rssUI();
  highlightFromHash(false);
  rebuildVRWall();

  if(!loggedIn){
    lockAppForAuth(true);
    openAuth();
    setAuthTab("login");
  }else{
    lockAppForAuth(false);
    const rss = loadRss();
    if(rss.enabled) startRss();
    else{
      stopRss(false);
      setToggleUI(false);
      setRssIndicator(false);
    }
  }
}

/* boot */
wireEvents();
init();
window.addEventListener("hashchange", ()=>highlightFromHash(true));
</script>

</body>
</html>

「俺タワー」製作レイドイベント「お花見お弁当対決!」が開催!個人報酬に新建姫「針式壁裏探知機」が登場 OnlineGamer

「俺タワー」製作レイドイベント「お花見お弁当対決!」が開催!個人報酬に新建姫「針式壁裏探知機」が登場 OnlineGamer
http://www.onlinegamer.jp/news/201604070001/