CSSの基礎

CSSって?

  • HTMLの見た目(色・余白・レイアウト・アニメ)を指定するスタイル言語。
  • 重要キーワード:Cascade(優先順位の連なり)、Specificity(詳細度)、Inheritance(継承)。

1) CSSの書き方・読み込み方

<!-- 外部ファイル(推奨) -->
<link rel="stylesheet" href="styles.css">

<!-- ページ内(学習用) -->
<style>
  p { color: #333; }
</style>

<!-- インライン(基本非推奨) -->
<p style="color:#333;">テキスト</p>

2) セレクタの基本

/* タイプ(要素) */      h1 { ... }
/* クラス */             .btn { ... }
/* ID */                 #header { ... }   /* 乱用しない */
 /* 子孫・子・隣接 */    nav a { ... }  nav > a { ... }  h2 + p { ... }
/* 属性 */               input[type="email"] { ... }
/* 擬似クラス */         a:hover, li:nth-child(2) { ... }
/* 擬似要素 */           p::first-line, a::after { content:"→"; }

3) カスケード&優先度(Specificity)

  • 計算イメージ:ID(100) > class/属性/擬似クラス(10) > 要素/擬似要素(1)
  • 競合したら:後勝ち(後から書いた方が有効)
  • !importantは最終手段(設計悪化のもと)

4) ボックスモデル(超重要)

margin ─ 外側の余白
border ─ 枠線
padding ─ 内側の余白
content ─ 中身
* { box-sizing: border-box; } /* 幅計算が直感的になる定番 */

5) 単位&色

  • 単位:px(固定)/ %(相対)/ em(親のfont-size基準)/ rem(ルート基準)/ vw,vh(ビューポート)
    → レスポンシブは rem% を多用。
  • 色:#222 / rgb(34 34 34) / hsl(210 10% 20%)(HSLは調整しやすい)
  • 変数:--brand: #5865f2;color: var(--brand);

6) 文字・余白の基本

html { font-size: 16px; }       /* remの基準 */
body { line-height: 1.7; }
h1 { font-size: clamp(1.5rem, 3vw, 2.5rem); } /* 可変サイズ */
p  { margin: 0 0 1rem; }

7) レイアウト:display / Flex / Grid

display

.block { display: block; }        /* 幅いっぱい */
.inline { display: inline; }      /* 行内 */
.inline-block { display:inline-block; }

Flex(1次元レイアウト:横並び・縦中央寄せが得意)

.container {
  display: flex;
  gap: 1rem;
  align-items: center;        /* 交差軸整列(縦) */
  justify-content: space-between; /* 主軸整列(横) */
}
.center {
  display:flex; align-items:center; justify-content:center;
}

Grid(2次元レイアウト:段組が得意)

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 3列 */
  gap: 1rem;
}

/* レスポンシブな自動詰め */
.auto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
}

8) 位置指定

.box { position: relative; }
.badge {
  position: absolute; top: .5rem; right: .5rem;
}
header { position: sticky; top: 0; } /* スクロール追従 */

9) レスポンシブ(メディアクエリ・コンテナクエリ)

/* 画面幅が768px以上で適用(モバイル優先) */
@media (min-width: 768px) {
  .nav { display: flex; }
}

/* コンテナクエリ(対応ブラウザ増) */
.card { container-type: inline-size; }
@container (min-width: 500px) {
  .card__side-by-side { display:flex; }
}

10) トランジション&トランスフォーム

.btn {
  transition: transform .2s ease, background-color .2s;
}
.btn:hover {
  transform: translateY(-2px) scale(1.02);
  background: #111;
  color: #fff;
}

11) リセットとベース

/* まずはこれでOKな最小ベース */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
img, video { max-width: 100%; height: auto; display:block; }
button, input, select, textarea { font: inherit; }
:root {
  --bg: #fff; --fg:#222; --muted:#6b7280; --brand:#3b82f6;
}
@media (prefers-color-scheme: dark) {
  :root { --bg:#0b0b0f; --fg:#e5e7eb; --muted:#9ca3af; }
}
body { background: var(--bg); color: var(--fg); line-height:1.7; }
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

12) すぐ試せるミニページ(HTML+CSS)

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS基礎デモ</title>
<style>
  * { box-sizing: border-box; }
  body { margin:0; font-family: system-ui, sans-serif; line-height:1.7; }
  header, footer { padding:16px; background:#f5f5f5; }
  .wrap { max-width:960px; margin:24px auto; padding:0 16px; }
  .hero {
    padding: 48px 16px; text-align:center;
    background: linear-gradient(120deg, #e0f2fe, #fde68a);
    border-radius:16px;
  }
  .btn {
    display:inline-block; padding:.75rem 1rem; border-radius:9999px;
    background:#111; color:#fff; transition: transform .2s ease;
  }
  .btn:hover { transform: translateY(-2px); }
  .grid {
    display:grid; gap:1rem;
    grid-template-columns: repeat(auto-fit, minmax(220px,1fr));
    margin-top:24px;
  }
  .card { border:1px solid #e5e7eb; border-radius:12px; padding:16px; }
  .card h3 { margin:.25rem 0 .5rem; }
</style>
</head>
<body>
  <header><div class="wrap"><strong>CSS基礎デモ</strong></div></header>
  <main class="wrap">
    <section class="hero">
      <h1>CSSの基本を掴もう</h1>
      <p>セレクタ / ボックスモデル / Flex / Grid / レスポンシブ</p>
      <a class="btn" href="#cards">カードを見る</a>
    </section>

    <section id="cards" class="grid">
      <article class="card">
        <h3>セレクタ</h3>
        <p>`.class` / `#id` / `a:hover` / `input[type="text"]` …</p>
      </article>
      <article class="card">
        <h3>Flex</h3>
        <p>横並び・中央寄せが簡単。`display:flex; gap:1rem;`</p>
      </article>
      <article class="card">
        <h3>Grid</h3>
        <p>2次元レイアウト。`auto-fit`×`minmax()`が実用的。</p>
      </article>
    </section>
  </main>
  <footer><div class="wrap"><small>&copy; 2025 CSS Demo</small></div></footer>
</body>
</html>

13) つまずきポイント

  • 高さが合わない:親にalign-items: stretchheight:auto、画像にはdisplay:block
  • 中央寄せできない:インラインはtext-align:center、ブロックはmargin: 0 auto、Flexならcenter
  • 崩れるbox-sizing:border-boxにして、余白はgap優先、width指定は最小限。
  • 優先順位に勝てない:セレクタを少しだけ強くする(親クラスを1段増やすなど)。!importantは避ける。

次に進むなら

  • レイアウト設計:BEM・Utility First(Tailwind的考え方)
  • モダン機能:subgridcontainer querieslogical propertiesmargin-inlineなど)
  • パフォーマンス:未使用CSSの削減、content-visibilitywill-changeの慎重な活用

HTMLの基礎

HTMLってなに?

  • Webページの骨組みを作る言語(見出し・段落・画像・リンクなどの構造)。
  • 見た目はCSS、動きはJSが担当。HTMLは“意味と構造”。

まずは雛形(コピペOK)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>はじめてのHTML</title>
  <meta name="description" content="このページの説明文" />
</head>
<body>
  <h1>こんにちは!</h1>
  <p>これは最小構成のHTML5ページです。</p>
</body>
</html>

よく使う要素(超基本)

  • 見出し:<h1>~<h6>(ページに基本はh1は1つ
  • 段落:<p>
  • リンク:<a href="https://example.com">リンク</a>
  • 画像:<img src="img.png" alt="画像の説明">alt必須
  • リスト:<ul><li>…</li></ul> / <ol>…</ol>
  • 強調:<strong>(重要) / <em>(強調)
  • 区切り:<br>(改行は最小限)、<hr>(区切り線)
  • まとまり:<div>(汎用ブロック)、<span>(汎用インライン)

セマンティック要素(構造をわかりやすく)

  • header(ヘッダー)
  • nav(ナビ)
  • main(主内容は1ページ1つ
  • section(章)
  • article(単体で完結する記事)
  • aside(補足)
  • footer(フッター)

属性のキホン

  • id(一意な識別子)/class(グループ化)
  • href(リンク先)/src(画像・スクリプト元)
  • alt(画像代替文)/title(補足ヒント)
  • target="_blank"rel="noopener noreferrer"とセットで

フォーム最小例

<form action="/search" method="get">
  <label for="q">検索:</label>
  <input id="q" name="q" type="search" required>
  <button type="submit">送信</button>
</form>

テーブル最小例(表)

<table>
  <thead><tr><th>商品</th><th>価格</th></tr></thead>
  <tbody>
    <tr><td>りんご</td><td>120</td></tr>
    <tr><td>みかん</td><td>100</td></tr>
  </tbody>
</table>

CSS / JS の読み込み

<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
  • deferはHTML解析後に実行(推奨)。

ちょっとだけ“実践的”なサンプル

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>ミニサイト</title>
  <style>
    body { font-family: system-ui, sans-serif; line-height: 1.7; margin: 0; }
    header, footer { padding: 16px; background: #f5f5f5; }
    nav a { margin-right: 12px; }
    main { max-width: 920px; margin: 24px auto; padding: 0 16px; }
    img { max-width: 100%; height: auto; }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
  </style>
</head>
<body>
  <header>
    <h1>ミニサイト</h1>
    <nav>
      <a href="#about">概要</a>
      <a href="#gallery">ギャラリー</a>
      <a href="#contact">お問い合わせ</a>
    </nav>
  </header>

  <main id="content">
    <section id="about">
      <h2>概要</h2>
      <p>これはHTMLの基本で作ったミニページです。</p>
    </section>

    <section id="gallery">
      <h2>ギャラリー</h2>
      <div class="card">
        <img src="sample.jpg" alt="サンプル画像">
        <p>レスポンシブに画像が縮みます。</p>
      </div>
      <ul>
        <li>箇条書き1</li>
        <li>箇条書き2</li>
      </ul>
    </section>

    <section id="contact">
      <h2>お問い合わせ</h2>
      <form>
        <label for="name">お名前</label><br>
        <input id="name" name="name" required><br><br>
        <label for="msg">メッセージ</label><br>
        <textarea id="msg" name="msg" rows="4"></textarea><br><br>
        <button type="submit">送信</button>
      </form>
    </section>
  </main>

  <footer>
    <small>&copy; 2025 MiniSite</small>
  </footer>

  <script>
    // ごく簡単なJS:ナビをクリックしたらスムーズスクロール
    document.querySelectorAll('nav a').forEach(a => {
      a.addEventListener('click', e => {
        const id = a.getAttribute('href');
        if (id.startsWith('#')) {
          e.preventDefault();
          document.querySelector(id)?.scrollIntoView({ behavior: 'smooth' });
        }
      });
    });
  </script>
</body>
</html>

初心者がつまずきやすいポイント

  • 文字化け→<meta charset="utf-8">を必ず入れる。
  • スマホで拡大縮小が変→<meta name="viewport" …>を入れる。
  • 画像が大きすぎる→CSSでimg { max-width: 100%; height: auto; }
  • 見出し乱用→h1はページの主題に1回、階層は順序を守る。
  • altなし→スクリーンリーダー/SEO的にマイナス。必ず書く。

もっと深掘り(フォームのバリデーション、SEO、アクセシビリティ、Flex/Gridレイアウト、コンポーネント化など)もまとめられます。どこから強化したい?(例:フォームをしっかり、レイアウトを学ぶ、CSS設計、JS連携 など)

X風サイト.html

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>X-like UI – Polished (Left-aligned)</title>
<style>
/* ===== Design Tokens ===== */
:root{
  /* colors */
  --bg: #0b0d10;
  --surface: #0e1116;
  --panel: rgba(16,18,24,.75);
  --card: #0f1319;
  --line: #1c2230;          /* single-pixel separators */
  --text: #e7ecf3;
  --muted: #9aa7ba;
  --accent: #1da1f2;
  --accent-2: #7dd3fc;
  --accent-3: #60a5fa;

  /* radius & shadow */
  --r: 14px;
  --shadow-sm: 0 1px 0 rgba(255,255,255,.02) inset, 0 8px 24px rgba(0,0,0,.35);
  --shadow-card: 0 10px 30px rgba(0,0,0,.25);

  /* layout */
  --col-left: 84px;
  --col-center-min: 360px;
  --col-center-max: 720px;
  --col-right-min: 280px;
  --col-right-max: 420px;

  /* NEW: tighter left gutter for center column */
  --gutter-x: 10px;

  /* motion */
  --e1: cubic-bezier(.2,.8,.2,1);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;
  background:
    radial-gradient(1200px 600px at 10% -10%, rgba(29,161,242,.15), transparent 40%),
    radial-gradient(800px 500px at 110% -10%, rgba(96,165,250,.12), transparent 40%),
    var(--bg);
  color:var(--text);
  font:14px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", "Hiragino Kaku Gothic ProN", "Helvetica Neue", Arial;
}

/* focus ring */
:where(a,button,[role="button"],input,textarea,.sb-btn,.ic-btn,.plus,.btn,.btn-ghost,.btn-primary){outline:none}
:where(a,button,[role="button"],input,textarea,.sb-btn,.ic-btn,.plus,.btn,.btn-ghost,.btn-primary):focus-visible{
  box-shadow:0 0 0 3px rgba(29,161,242,.35);
  border-radius:10px;
}

/* ===== Layout ===== */
.app{
  display:grid; gap:0;
  grid-template-columns: var(--col-left) minmax(var(--col-center-min),var(--col-center-max)) minmax(var(--col-right-min),var(--col-right-max));
  height:100%;
  max-width:1280px;
  margin:0 auto;
}
.sidebar{
  position:sticky; top:0; height:100vh;
  backdrop-filter: blur(10px);
  background:linear-gradient(180deg, rgba(16,18,24,.7), rgba(16,18,24,.3));
  /* NOTE: borders are drawn via ::after to avoid subpixel drift */
  padding:10px 8px; display:flex; flex-direction:column; gap:6px;
}
.sb-btn{
  border-radius:999px; padding:12px 14px; cursor:pointer;
  display:flex; align-items:center; gap:14px; transition:background .2s var(--e1), transform .12s var(--e1);
}
.sb-btn:hover{background:rgba(255,255,255,.04)}
.sb-btn:active{transform:translateY(1px)}
.sb-icon{width:28px;height:28px;display:grid;place-items:center;border-radius:999px}
.sb-btn .label{font-weight:700;letter-spacing:.02em}

.compose-fab{
  position:fixed; left:18px; bottom:18px; z-index:50; cursor:pointer;
  border:none; color:#fff; font-weight:800; letter-spacing:.02em;
  border-radius:999px; padding:12px 18px;
  background:linear-gradient(135deg, var(--accent), var(--accent-3));
  box-shadow:0 10px 30px rgba(29,161,242,.35);
  transition: transform .12s var(--e1), filter .2s var(--e1);
}
.compose-fab:hover{filter:brightness(1.05)}
.compose-fab:active{transform:translateY(1px)}
.you-chip{
  position:fixed; left:18px; bottom:84px; width:48px;height:48px;border-radius:999px;
  display:grid;place-items:center;font-weight:900; background:conic-gradient(from 180deg at 50% 50%, #0f629e, #0c3c68 70%, #0f629e);
  color:#fff; border:1px solid rgba(255,255,255,.08);
}

.main{
  position:relative; /* for ::after separator */
  min-height:100vh; background:rgba(0,0,0,.25);
  backdrop-filter: blur(6px);
}
.rightcol{
  padding:14px; position:sticky; top:0; height:100vh; overflow:auto;
}
.searchbar{
  display:flex; align-items:center; gap:10px; padding:10px 14px;
  background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.06);
  border-radius:999px;
}

/* ===== Single-pixel separators (no drift) ===== */
.sidebar::after,
.main::after{
  content:"";
  position:absolute; top:0; bottom:0; width:1px; background:var(--line);
  pointer-events:none; z-index:10;
}
.sidebar::after{ right:0; }  /* Sidebar ↔ Main */
.main::after{ right:0; }     /* Main ↔ Right column */

/* ===== Header Tabs ===== */
.header-title{padding:10px var(--gutter-x); font-size:20px; font-weight:900; border-bottom:1px solid var(--line)}
.tabs{
  position:sticky; top:0; z-index:6;
  display:flex; gap:24px; padding:0 var(--gutter-x);        /* <<< tightened left padding */
  background:linear-gradient(180deg, rgba(14,17,22,.8), rgba(14,17,22,.55));
  backdrop-filter: saturate(120%) blur(8px);
  border-bottom:1px solid var(--line);
}
.tab{
  padding:16px 6px; cursor:pointer; color:var(--muted); font-weight:800; border-bottom:3px solid transparent;
  transition:color .2s var(--e1), border-color .2s var(--e1);
}
.tab.active{color:var(--text); border-color:var(--accent)}

/* ===== Composer ===== */
.composer{
  padding:14px var(--gutter-x);                               /* <<< tightened */
  border-bottom:1px solid var(--line); display:flex; gap:12px;
  background:linear-gradient(180deg, rgba(17,22,29,.8), rgba(17,22,29,.4));
}
.avatar{
  width:42px; height:42px; border-radius:999px; display:grid; place-items:center; font-weight:900; letter-spacing:.02em;
  color:#fff; border:1px solid rgba(255,255,255,.08);
  background:radial-gradient(120% 120% at 20% 15%, #1e81c5, #0f2a43 70%);
}
.composer-box{flex:1}
.composer textarea{
  width:100%; min-height:76px; resize:vertical; background:transparent; border:none; color:var(--text);
  outline:none; font-size:18px; caret-color:var(--accent);
}
.reply-scope{color:var(--accent);font-weight:700;font-size:13px}
.row{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:6px}
.icons{display:flex; gap:8px}
.ic-btn{width:32px;height:32px;display:grid;place-items:center;border-radius:10px;cursor:pointer;transition:background .2s}
.ic-btn:hover{background:rgba(255,255,255,.06)}
.post-btn{
  border:none; color:#0b1220; font-weight:900; letter-spacing:.02em;
  padding:9px 18px; border-radius:999px; cursor:not-allowed;
  background:linear-gradient(135deg, #6b7280 0%, #9aa7ba 100%);
  filter:saturate(.7); opacity:.7; transition:filter .2s, transform .12s;
}
.post-btn.enabled{
  cursor:pointer; opacity:1; color:#fff; filter:none;
  background:linear-gradient(135deg, var(--accent), var(--accent-3));
  box-shadow:0 12px 30px rgba(29,161,242,.3);
}
.post-btn.enabled:hover{filter:brightness(1.05)}
.post-btn.enabled:active{transform:translateY(1px)}

/* ===== Cards ===== */
.card{
  border-bottom:1px solid var(--line); display:flex; gap:12px;
  padding:14px var(--gutter-x);                                 /* <<< tightened */
  background:linear-gradient(180deg, rgba(16,20,27,.5), rgba(16,20,27,.25));
}
.meta{display:flex; gap:6px; color:var(--muted)}
.name{font-weight:900}
.handle{color:var(--muted)}
.hash a{color:var(--accent)}
.post-img{
  border-radius:18px; border:1px solid rgba(255,255,255,.06); width:100%; margin-top:10px;
  box-shadow:var(--shadow-card)
}
.actions{display:flex; gap:26px; margin-top:8px; color:var(--muted)}
.action{display:flex; gap:6px; align-items:center; cursor:pointer; transition:color .15s}
.action:hover{color:var(--accent)}

/* ===== Right column ===== */
.rightcol .rc-card{
  background:linear-gradient(180deg, rgba(17,22,29,.65), rgba(17,22,29,.35));
  border:1px solid rgba(255,255,255,.08); border-radius:18px; overflow:hidden; margin-top:12px; box-shadow:var(--shadow-sm)
}
.rc-title{font-weight:900; padding:12px 16px; border-bottom:1px solid rgba(255,255,255,.06)}
.rc-item{padding:12px 16px; border-top:1px solid rgba(255,255,255,.04)}
.chip{background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); border:1px solid rgba(255,255,255,.08);
  padding:4px 10px; border-radius:999px; color:var(--muted); font-size:12px}

/* ===== Subtabs / Explore ===== */
.subtabs{display:flex; gap:18px; padding:10px var(--gutter-x); border-bottom:1px solid var(--line); background:rgba(255,255,255,.02)}  /* <<< tightened */
.subtab{padding:10px 2px; color:var(--muted); font-weight:800; cursor:pointer; border-bottom:3px solid transparent}
.subtab.active{color:var(--text); border-color:var(--accent)}

/* ===== Empties / Profile / Lists ===== */
.empty{display:grid; place-items:center; padding:72px var(--gutter-x); color:var(--muted); text-align:center}
.empty h2{margin:0 0 6px; color:var(--text)}
.cover{height:170px; background:
  radial-gradient(70% 120% at 10% 0%, rgba(29,161,242,.25), transparent 40%),
  radial-gradient(60% 120% at 100% 0%, rgba(96,165,250,.2), transparent 40%),
  linear-gradient(180deg,#121722,#0b0f17)}
.prof-wrap{padding:0 var(--gutter-x) 16px}
.prof-row{display:flex; justify-content:space-between; align-items:end; margin-top:-36px}
.pfp{width:96px;height:96px;border-radius:999px;border:4px solid var(--surface);background:radial-gradient(120% 120% at 20% 15%, #1e81c5, #0f2a43 70%);display:grid;place-items:center;color:#fff;font-weight:900}
.btn{background:transparent; border:1px solid rgba(255,255,255,.18); color:#fff; border-radius:999px; padding:8px 14px; cursor:pointer; transition:background .2s}
.btn:hover{background:rgba(255,255,255,.05)}
.alert{background:rgba(18,42,24,.65); border:1px solid #165c36; color:#a9f2b7; border-radius:14px; padding:12px 14px; margin:12px var(--gutter-x); display:flex; gap:10px; align-items:center}
.check{width:18px;height:18px;border-radius:4px;border:2px solid #a9f2b7;display:grid;place-items:center}

.list-row{display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 16px}
.list-pill{width:44px;height:44px;border-radius:12px; background:linear-gradient(135deg,#374151,#1f2937)}
.plus{width:28px;height:28px;border-radius:999px;border:1px solid rgba(255,255,255,.18);display:grid;place-items:center}

/* ===== Grok ===== */
.grok{display:grid; place-items:center; padding:44px var(--gutter-x)}
.grok .logo{font-size:28px; font-weight:1000; display:flex; align-items:center; gap:10px}
.grok .input{margin-top:18px; display:flex; gap:8px; width:min(680px,90vw)}
.grok .input input{flex:1; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.18); background:#0c1118; color:#fff; transition:border .2s}
.grok .input input:focus{border-color:rgba(125,211,252,.6)}
.grok .input button{padding:12px 16px; border-radius:12px; border:1px solid rgba(255,255,255,.18); background:linear-gradient(135deg,#10151f,#0e131b); color:#fff}
.grok .tools{display:flex; gap:8px; margin-top:12px}

/* ===== E2EE Page ===== */
.e2ee{padding:56px var(--gutter-x); text-align:center}
.e2ee h1{font-size:26px; margin:0 0 12px}
.bullet{display:flex; gap:10px; align-items:flex-start; justify-content:center; color:#cfd9ea}
.e2ee .cta{display:flex; gap:12px; justify-content:center; margin-top:18px}
.btn-ghost{border:1px solid rgba(255,255,255,.18); background:transparent; color:#fff; padding:10px 14px; border-radius:999px}
.btn-primary{border:1px solid rgba(29,161,242,.55); background:linear-gradient(135deg, var(--accent), var(--accent-2)); color:#001e33; font-weight:900; padding:10px 16px; border-radius:999px}

/* ===== Modal ===== */
.modal-backdrop{
  position:fixed; inset:0; background:rgba(4,8,12,.6); display:none; align-items:center; justify-content:center; z-index:100;
  animation:fadeIn .2s var(--e1);
}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.modal{
  width:min(640px,92vw); background:linear-gradient(180deg, rgba(14,18,24,.95), rgba(14,18,24,.85));
  border:1px solid rgba(255,255,255,.12); border-radius:20px; overflow:hidden; box-shadow:var(--shadow-card)
}
.modal .top{display:flex; align-items:center; justify-content:space-between; padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.06)}
.close-x{width:32px;height:32px;border-radius:999px;display:grid;place-items:center;cursor:pointer;transition:background .2s}
.close-x:hover{background:rgba(255,255,255,.06)}
.small{font-size:12px;color:var(--muted)}

/* ===== Toast ===== */
.toast{
  position:fixed; left:50%; transform:translateX(-50%) translateY(20px);
  bottom:20px; background:rgba(18,22,28,.92); border:1px solid rgba(255,255,255,.12);
  padding:10px 14px; border-radius:999px; display:none; gap:10px; align-items:center; z-index:120;
  box-shadow:var(--shadow-card); animation:slideUp .25s var(--e1);
}
.toast .view{background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12); padding:6px 10px; border-radius:999px}
@keyframes slideUp{from{opacity:0; transform:translateX(-50%) translateY(40px)} to{opacity:1; transform:translateX(-50%) translateY(0)}}

/* ===== Utilities ===== */
.hide{display:none !important}

/* ===== Responsive ===== */
@media (max-width:1100px){
  .app{grid-template-columns: 72px 1fr}
  .rightcol{display:none}
}
@media (max-width:520px){
  .sb-btn .label{display:none}
  .sidebar{align-items:center}
}
</style>
</head>
<body>
<div class="app">
  <!-- Sidebar -->
  <aside class="sidebar" aria-label="Sidebar">
    <div class="sb-btn" data-nav="home"><div class="sb-icon">🏠</div><div class="label">Home</div></div>
    <div class="sb-btn" data-nav="explore"><div class="sb-icon">🔎</div><div class="label">Explore</div></div>
    <div class="sb-btn" data-nav="notifications"><div class="sb-icon">🔔</div><div class="label">Notifications</div></div>
    <div class="sb-btn" data-nav="messages"><div class="sb-icon">✉️</div><div class="label">Messages</div></div>
    <div class="sb-btn" data-nav="grok"><div class="sb-icon">⚡</div><div class="label">Grok</div></div>
    <div class="sb-btn" data-nav="lists"><div class="sb-icon">🗂️</div><div class="label">Lists</div></div>
    <div class="sb-btn" data-nav="profile"><div class="sb-icon">👤</div><div class="label">Profile</div></div>
    <div class="sb-btn" data-nav="encrypted"><div class="sb-icon">🔒</div><div class="label">Chat</div></div>
  </aside>

  <!-- Main -->
  <main class="main" id="main">
    <!-- Home -->
    <section id="view-home" role="region" aria-label="Home timeline">
      <div class="tabs">
        <div class="tab active" data-home-tab="foryou">For you</div>
        <div class="tab" data-home-tab="following">Following</div>
      </div>

      <div class="composer">
        <div class="avatar">裕平</div>
        <div class="composer-box">
          <div class="reply-scope">Everyone can reply</div>
          <textarea id="compose-input" placeholder="What’s happening?"></textarea>
          <div class="row">
            <div class="icons" aria-label="Composer actions">
              <div class="ic-btn" title="Media">🖼️</div>
              <div class="ic-btn" title="GIF">🌀</div>
              <div class="ic-btn" title="Poll">📊</div>
              <div class="ic-btn" title="Emoji">😊</div>
              <div class="ic-btn" title="Schedule">🗓️</div>
              <div class="ic-btn" title="Location">📍</div>
            </div>
            <button id="post-btn" class="post-btn" disabled>Post</button>
          </div>
        </div>
      </div>

      <article class="card">
        <div class="avatar">H</div>
        <div style="flex:1">
          <div class="meta"><span class="name">HANA</span><span class="handle">@HANA__BRAVE · 9h</span></div>
          <div>🎂 <span class="hash"><a href="#">#HAPPYJISOODAY</a></span><br/>Happy Birthday JISOO 🤍<br/><span class="hash"><a href="#">#HANA</a> <a href="#">#JISOO</a></span></div>
          <img class="post-img" alt="sample" src="https://picsum.photos/seed/jisoo/720/380" />
          <div class="actions">
            <div class="action">💬 <span>24</span></div>
            <div class="action">🔁 <span>10</span></div>
            <div class="action">❤️ <span>128</span></div>
            <div class="action">↗️</div>
          </div>
        </div>
      </article>

      <article class="card">
        <div class="avatar">X</div>
        <div style="flex:1">
          <div class="meta"><span class="name">MTV VMA · LIVE</span><span class="handle"> · now</span></div>
          <div class="hash"><a href="#">#VMAs</a></div>
          <img class="post-img" alt="vmas" src="https://picsum.photos/seed/vma/720/300" />
          <div class="actions">
            <div class="action">💬 <span>8</span></div>
            <div class="action">🔁 <span>3</span></div>
            <div class="action">❤️ <span>42</span></div>
            <div class="action">↗️</div>
          </div>
        </div>
      </article>

      <div id="feed-anchor"></div>
    </section>

    <!-- Explore -->
    <section id="view-explore" class="hide" role="region" aria-label="Explore">
      <div class="header-title">
        <div class="searchbar"><span>🔎</span><input style="flex:1;background:transparent;border:none;color:#fff;outline:none" placeholder="Search" /></div>
      </div>
      <div class="subtabs">
        <div class="subtab active">For You</div>
        <div class="subtab">Trending</div>
        <div class="subtab">News</div>
        <div class="subtab">Sports</div>
        <div class="subtab">Entertainment</div>
      </div>
      <article class="card">
        <div class="avatar">T</div>
        <div style="flex:1">
          <div class="meta"><span class="name">Tokyo 2025</span><span class="handle"> · promoted</span></div>
          <img class="post-img" alt="tokyo" src="https://picsum.photos/seed/tokyo2025/720/260" />
        </div>
      </article>
    </section>

    <!-- Notifications -->
    <section id="view-notifications" class="hide" role="region" aria-label="Notifications">
      <div class="tabs">
        <div class="tab active">All</div>
        <div class="tab">Verified</div>
        <div class="tab">Mentions</div>
      </div>
      <div class="empty">
        <div>
          <h2>Nothing to see here — yet</h2>
          <div>From likes to reposts and a whole lot more, this is where all the action happens.</div>
        </div>
      </div>
    </section>

    <!-- Messages -->
    <section id="view-messages" class="hide" role="region" aria-label="Messages">
      <div class="inbox" style="min-height:60vh">
        <div class="dm-left" style="border-right:1px solid var(--line)">
          <div style="padding:16px">
            <h3 style="margin:4px 0">Welcome to your inbox!</h3>
            <div class="muted">Drop a line, share posts and more with private conversations between you and others on X.</div>
            <div style="height:10px"></div>
            <button class="btn">Write a message</button>
          </div>
        </div>
        <div>
          <div class="empty">
            <div>
              <h2>Select a message</h2>
              <div class="muted">Choose from your existing conversations, start a new one, or just keep swimming.</div>
              <div style="height:10px"></div>
              <button class="btn">New message</button>
            </div>
          </div>
        </div>
      </div>
    </section>

    <!-- Lists -->
    <section id="view-lists" class="hide" role="region" aria-label="Lists">
      <div class="header-title">Lists</div>
      <div style="padding:16px">
        <div class="muted" style="padding:8px 16px">Discover new Lists</div>
        <div class="rc-card" role="list">
          <div class="list-row">
            <div style="display:flex;gap:12px;align-items:center">
              <div class="list-pill"></div>
              <div>
                <div>J.League · <span class="muted">60 members</span></div>
                <div class="muted">2K followers including @sascha348</div>
              </div>
            </div>
            <div class="plus">+</div>
          </div>
          <div class="list-row">
            <div style="display:flex;gap:12px;align-items:center">
              <div class="list-pill"></div>
              <div>
                <div>Official Accounts · <span class="muted">83 members</span></div>
                <div class="muted">263 followers including @dencetuno</div>
              </div>
            </div>
            <div class="plus">+</div>
          </div>
          <div class="list-row">
            <div style="display:flex;gap:12px;align-items:center">
              <div class="list-pill"></div>
              <div>
                <div>kitchen · <span class="muted">52 members</span></div>
                <div class="muted">181 followers including @Carolina_3254</div>
              </div>
            </div>
            <div class="plus">+</div>
          </div>
        </div>

        <div class="muted" style="padding:22px 16px 8px">Your Lists</div>
        <div class="empty" style="opacity:.75"><div>You haven't created or followed any Lists. When you do, they'll show up here.</div></div>
      </div>
    </section>

    <!-- Profile -->
    <section id="view-profile" class="hide" role="region" aria-label="Profile">
      <div class="cover"></div>
      <div class="prof-wrap">
        <div class="prof-row">
          <div class="pfp">裕平</div>
          <button class="btn">Edit profile</button>
        </div>
        <h2 style="margin:10px 0 0">長留裕平</h2>
        <div class="muted">@PingZhang89719 · Joined September 2025</div>
        <div style="height:8px"></div>
        <div class="muted">0 Following · 0 Followers</div>
      </div>
      <div class="alert">
        <div class="check">✔</div>
        <div>
          <div class="name" style="font-weight:900">You aren’t verified yet</div>
          <div class="muted">Get verified for boosted replies, analytics, ad-free browsing, and more.</div>
        </div>
      </div>
      <article class="card">
        <div class="avatar">裕平</div>
        <div style="flex:1">
          <div class="meta"><span class="name">長留裕平</span><span class="handle"> · 1m</span></div>
          <div>はじめました。</div>
        </div>
      </article>
    </section>

    <!-- Grok -->
    <section id="view-grok" class="hide" role="region" aria-label="Grok">
      <div class="grok">
        <div class="logo">⚡ <span>Grok</span></div>
        <div class="muted">Ask anything</div>
        <div class="input">
          <input placeholder="Ask anything" />
          <button>➤</button>
        </div>
        <div class="tools">
          <button class="btn">Create Images</button>
          <button class="btn">Edit Image</button>
        </div>
        <div class="muted" style="margin-top:16px">Fast ▾ &nbsp; · &nbsp; History</div>
      </div>
    </section>

    <!-- Encrypted Chat -->
    <section id="view-encrypted" class="hide" role="region" aria-label="Encrypted Chat">
      <div class="e2ee">
        <h1>Meet new Chat, now fully encrypted.</h1>
        <div class="muted">X Chat are now protected with end-to-end encryption on all your devices.</div>
        <div style="height:12px"></div>
        <div class="bullet">🔒 <div><b>End-to-End Encryption</b><br/>Your messages are protected across devices.</div></div>
        <div class="bullet">🛡️ <div><b>Uncompromising Privacy</b><br/>No one — not even X — can access or read your messages.</div></div>
        <div class="cta">
          <button class="btn-ghost">Maybe later</button>
          <button class="btn-primary">Set up now</button>
        </div>
      </div>
    </section>
  </main>

  <!-- Right -->
  <aside class="rightcol" aria-label="Right column">
    <div class="rc-card">
      <div class="rc-title">What’s happening</div>
      <div class="rc-item"><b>MTV Video Music Awards 2025</b><div class="muted">LIVE</div></div>
      <div class="rc-item"><b>東京2025 世界陸上</b><div class="muted">Trending · 8,724 posts</div></div>
      <div class="rc-item"><b>JISOO</b><div class="chip">K-POP · Trending</div></div>
    </div>
    <div class="rc-card">
      <div class="rc-title">Who to follow</div>
      <div class="rc-item">🅿️ <b>Product Dev</b> · <span class="muted">@buildhub</span> <button class="btn" style="float:right">Follow</button></div>
      <div class="rc-item">🧠 <b>AI Lab</b> · <span class="muted">@ailab</span> <button class="btn" style="float:right">Follow</button></div>
    </div>
  </aside>
</div>

<!-- Floating -->
<button class="compose-fab" id="open-compose">Post</button>
<div class="you-chip">裕平</div>

<!-- Modal -->
<div class="modal-backdrop" id="composer-modal" aria-hidden="true">
  <div class="modal" role="dialog" aria-modal="true" aria-label="New post">
    <div class="top">
      <div class="close-x" id="close-compose">✕</div>
      <a class="small" href="#" id="drafts-link">Drafts</a>
    </div>
    <div class="composer" style="border:none">
      <div class="avatar">裕平</div>
      <div class="composer-box">
        <div class="reply-scope">Everyone can reply</div>
        <textarea id="modal-input" placeholder="What’s happening?"></textarea>
        <div class="row">
          <div class="icons">
            <div class="ic-btn">🖼️</div><div class="ic-btn">🌀</div><div class="ic-btn">📊</div><div class="ic-btn">😊</div><div class="ic-btn">🗓️</div><div class="ic-btn">📍</div>
          </div>
          <button id="modal-post" class="post-btn" disabled>Post</button>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Toast -->
<div class="toast" id="toast">
  <div>✅ Your post was sent.</div>
  <a class="view" href="#feed-anchor">View</a>
</div>

<script>
/* ---------- Router ---------- */
const views = {
  home: document.getElementById('view-home'),
  explore: document.getElementById('view-explore'),
  notifications: document.getElementById('view-notifications'),
  messages: document.getElementById('view-messages'),
  lists: document.getElementById('view-lists'),
  profile: document.getElementById('view-profile'),
  grok: document.getElementById('view-grok'),
  encrypted: document.getElementById('view-encrypted'),
};
function show(view){
  for(const k in views){ views[k].classList.add('hide'); }
  (views[view]||views.home).classList.remove('hide');
  window.location.hash = view;
}
document.querySelectorAll('.sb-btn').forEach(btn=>{
  btn.addEventListener('click', ()=>show(btn.dataset.nav));
});
window.addEventListener('load', ()=>{
  const v = location.hash.replace('#','');
  if(v && views[v]) show(v);
});

/* ---------- Composer (inline) ---------- */
const composeInput = document.getElementById('compose-input');
const postBtn = document.getElementById('post-btn');
composeInput.addEventListener('input',()=>{
  const on = composeInput.value.trim().length>0;
  postBtn.disabled = !on; postBtn.classList.toggle('enabled', on);
});
postBtn.addEventListener('click', ()=>{
  addPost(composeInput.value.trim());
  composeInput.value=''; postBtn.disabled=true; postBtn.classList.remove('enabled');
  showToast();
});

/* ---------- Composer (modal) ---------- */
const modal = document.getElementById('composer-modal');
const openCompose = document.getElementById('open-compose');
const closeCompose = document.getElementById('close-compose');
const modalInput = document.getElementById('modal-input');
const modalPost = document.getElementById('modal-post');

openCompose.addEventListener('click', ()=>{ modal.style.display='flex'; modalInput.focus(); });
closeCompose.addEventListener('click', ()=>{ modal.style.display='none'; });

modalInput.addEventListener('input', ()=>{
  const on = modalInput.value.trim().length>0;
  modalPost.disabled = !on; modalPost.classList.toggle('enabled', on);
});
modalPost.addEventListener('click', ()=>{
  addPost(modalInput.value.trim());
  modalInput.value=''; modalPost.disabled=true; modalPost.classList.remove('enabled');
  modal.style.display='none'; showToast();
});
modal.addEventListener('click', (e)=>{ if(e.target===modal) modal.style.display='none'; });

/* ---------- Add Post ---------- */
function addPost(text){
  if(!text) return;
  const card = document.createElement('article');
  card.className='card';
  card.innerHTML = `
    <div class="avatar">裕平</div>
    <div style="flex:1">
      <div class="meta"><span class="name">長留裕平</span><span class="handle"> · now</span></div>
      <div>${escapeHTML(text)}</div>
      <div class="actions">
        <div class="action">💬 <span>0</span></div>
        <div class="action">🔁 <span>0</span></div>
        <div class="action">❤️ <span>0</span></div>
        <div class="action">↗️</div>
      </div>
    </div>`;
  const anchor = document.getElementById('feed-anchor');
  anchor.parentNode.insertBefore(card, anchor);
}
function escapeHTML(s){return s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')}

/* ---------- Toast ---------- */
const toast = document.getElementById('toast');
function showToast(){
  toast.style.display='flex';
  clearTimeout(showToast._t);
  showToast._t = setTimeout(()=> toast.style.display='none', 2600);
}

/* ---------- Home tab visual only ---------- */
document.querySelectorAll('[data-home-tab]').forEach(t=>{
  t.addEventListener('click', ()=>{
    document.querySelectorAll('[data-home-tab]').forEach(x=>x.classList.remove('active'));
    t.classList.add('active');
  });
});
</script>
</body>
</html>

MailLite — シンプルWebメール

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>MailLite — シンプルWebメール</title>
<style>
  :root{
    --bg:#f6f7fb;
    --panel:#ffffff;
    --text:#1f2937;
    --muted:#6b7280;
    --primary:#4f46e5;
    --primary-weak:#eef2ff;
    --border:#e5e7eb;
    --danger:#ef4444;
    --success:#10b981;
    --warning:#f59e0b;
  }
  .dark{
    --bg:#0b0e15;
    --panel:#0f1623;
    --text:#e5e7eb;
    --muted:#9ca3af;
    --primary:#8b5cf6;
    --primary-weak:#221a36;
    --border:#1f2937;
    --danger:#f87171;
    --success:#34d399;
    --warning:#fbbf24;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;background:var(--bg);color:var(--text);
    font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,"Noto Sans JP",sans-serif;
  }
  .app{
    display:grid;grid-template-rows:56px 1fr;height:100%;
  }
  /* Topbar */
  .topbar{
    display:flex;align-items:center;gap:12px;
    padding:0 16px;border-bottom:1px solid var(--border);background:var(--panel);
    position:sticky;top:0;z-index:5;
  }
  .logo{
    display:flex;align-items:center;gap:10px;font-weight:700;
    letter-spacing:.2px;
  }
  .badge{font-size:10px;padding:2px 6px;border-radius:999px;background:var(--primary-weak);color:var(--primary)}
  .search{
    margin-left:auto;display:flex;align-items:center;gap:8px;background:var(--bg);
    padding:6px 10px;border-radius:10px;border:1px solid var(--border);min-width:220px;max-width:460px;flex:1;
  }
  .search input{border:none;background:transparent;outline:none;color:var(--text);width:100%}
  .icon{width:18px;height:18px;display:inline-block;flex:0 0 18px}
  .btn{
    display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:10px;
    border:1px solid var(--border);background:var(--panel);cursor:pointer;color:var(--text);
  }
  .btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
  .btn.ghost{background:transparent}
  .btn:disabled{opacity:.6;cursor:not-allowed}

  /* Layout */
  .layout{
    display:grid;grid-template-columns:260px 360px 1fr;gap:12px;padding:12px;height:calc(100vh - 56px);
  }
  .panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;overflow:hidden;display:flex;flex-direction:column;min-height:0}
  .sidebar{padding:12px}
  .compose-block{padding:12px}
  .compose-button{width:100%;justify-content:center}
  .nav-group{margin-top:8px}
  .nav-item{
    display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:10px;color:var(--text);text-decoration:none;cursor:pointer;
  }
  .nav-item:hover{background:var(--primary-weak)}
  .nav-item.active{background:var(--primary);color:#fff}
  .nav-item .count{margin-left:auto;opacity:.8}

  /* List */
  .list-toolbar{display:flex;align-items:center;gap:8px;padding:8px;border-bottom:1px solid var(--border)}
  .list{overflow:auto}
  .msg{
    display:grid;grid-template-columns:24px 1fr auto;gap:10px;padding:12px;border-bottom:1px solid var(--border);cursor:pointer;
  }
  .msg:hover{background:var(--primary-weak)}
  .msg.unread{background:linear-gradient(0deg,transparent,transparent), var(--panel);font-weight:600}
  .msg .from{color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .msg .subject{color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .msg .snippet{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .msg .meta{display:flex;flex-direction:column;align-items:end;gap:4px;color:var(--muted)}
  .chip{display:inline-flex;align-items:center;gap:6px;padding:2px 8px;border-radius:999px;border:1px solid var(--border);font-size:11px}
  .star{cursor:pointer;opacity:.7}
  .star.active{opacity:1}
  .avatar{
    width:24px;height:24px;border-radius:50%;background:linear-gradient(135deg,var(--primary),#22c1c3);
    display:grid;place-items:center;color:#fff;font-size:12px;font-weight:700;
  }

  /* Reader */
  .reader-head{
    padding:12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  }
  .reader-title{font-size:18px;font-weight:700}
  .reader-meta{color:var(--muted);font-size:12px}
  .reader-actions{margin-left:auto;display:flex;gap:8px}
  .reader-body{padding:16px;overflow:auto}
  .empty{display:grid;place-items:center;height:100%;color:var(--muted)}

  /* Modal */
  .modal{
    position:fixed;inset:0;background:rgba(0,0,0,.4);display:none;align-items:center;justify-content:center;z-index:20;
  }
  .modal.open{display:flex}
  .modal-card{
    width:min(920px,94vw);max-height:88vh;overflow:auto;background:var(--panel);border:1px solid var(--border);
    border-radius:16px;
  }
  .modal-head{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border)}
  .modal-body{padding:16px;display:grid;gap:10px}
  .field{display:grid;gap:6px}
  .input, textarea{
    width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);
    background:transparent;color:var(--text);outline:none;
  }
  textarea{min-height:220px;resize:vertical}
  .row{display:flex;gap:10px;flex-wrap:wrap}
  .grow{flex:1}

  /* Responsive */
  @media (max-width: 1100px){
    .layout{grid-template-columns:220px 1fr}
    .reader{display:none}
    .layout.show-reader .list{display:none}
    .layout.show-reader .reader{display:flex}
  }
  @media (max-width: 640px){
    .layout{grid-template-columns:1fr}
    .sidebar{display:none}
  }
</style>
</head>
<body>
<div class="app">
  <!-- Topbar -->
  <div class="topbar">
    <div class="logo">
      <svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <path d="M3 7.5 12 13l9-5.5v9A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5v-9Z" stroke="currentColor" stroke-width="1.5"/>
        <path d="m3 7.5 9-5 9 5" stroke="currentColor" stroke-width="1.5"/>
      </svg>
      MailLite <span class="badge">beta</span>
    </div>

    <div class="search">
      <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="m21 21-4.2-4.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="1.5"/></svg>
      <input id="search" placeholder="メールを検索(差出人・件名・本文・ラベル)" />
      <button class="btn ghost" id="clearSearch" title="検索クリア">クリア</button>
    </div>

    <button class="btn" id="toggleDark" title="ダークモード">
      <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9Z" stroke="currentColor" stroke-width="1.5"/></svg>
      主题
    </button>
    <button class="btn primary" id="composeBtn">
      <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M4 20h16M6 14l9.5-9.5a2.1 2.1 0 1 1 3 3L9 17l-5 1 2-4Z" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>
      新規作成
    </button>
  </div>

  <!-- Main layout -->
  <div class="layout" id="layout">
    <!-- Sidebar -->
    <aside class="panel sidebar">
      <div class="compose-block">
        <button class="btn primary compose-button" id="composeBtn2">
          <svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M4 20h16M6 14l9.5-9.5a2.1 2.1 0 1 1 3 3L9 17l-5 1 2-4Z" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>
          新規メール
        </button>
      </div>
      <nav class="nav-group" id="folders"></nav>
    </aside>

    <!-- List -->
    <section class="panel">
      <div class="list-toolbar">
        <button class="btn" id="markReadBtn" title="既読にする">既読</button>
        <button class="btn" id="markUnreadBtn" title="未読にする">未読</button>
        <button class="btn" id="archiveBtn" title="アーカイブ">アーカイブ</button>
        <button class="btn" id="deleteBtn" title="削除">削除</button>
        <div style="margin-left:auto;display:flex;align-items:center;gap:6px;">
          <span class="chip"><span class="dot" style="width:8px;height:8px;border-radius:50%;background:var(--primary)"></span> ラベル</span>
        </div>
      </div>
      <div class="list" id="list"></div>
    </section>

    <!-- Reader -->
    <section class="panel reader" id="reader">
      <div class="empty" id="emptyState">メールを選択してください</div>
      <div style="display:none;flex-direction:column;height:100%;" id="readerWrap">
        <div class="reader-head">
          <div style="display:flex;align-items:center;gap:10px;min-width:0">
            <div class="avatar" id="readerAvatar">Y</div>
            <div style="min-width:0">
              <div class="reader-title" id="readerSubject">件名</div>
              <div class="reader-meta" id="readerMeta">From – To ・ 日付</div>
            </div>
          </div>
          <div class="reader-actions">
            <button class="btn" id="replyBtn" title="返信">返信</button>
            <button class="btn" id="starBtn" title="スター">
              <span id="starIcon">☆</span> スター
            </button>
            <button class="btn" id="archBtn" title="アーカイブ">アーカイブ</button>
            <button class="btn" id="trashBtn" title="削除" style="color:var(--danger)">削除</button>
          </div>
        </div>
        <div class="reader-body">
          <div id="readerBody"></div>
        </div>
      </div>
    </section>
  </div>
</div>

<!-- Compose Modal -->
<div class="modal" id="composeModal" aria-hidden="true">
  <div class="modal-card">
    <div class="modal-head">
      <strong>新規メッセージ</strong>
      <div class="row">
        <button class="btn" id="saveDraftBtn">下書き保存</button>
        <button class="btn primary" id="sendBtn">送信</button>
        <button class="btn" id="closeModalBtn">閉じる</button>
      </div>
    </div>
    <div class="modal-body">
      <div class="row">
        <div class="field grow">
          <label for="to">宛先(カンマ区切り)</label>
          <input class="input" id="to" placeholder="example@example.com, someone@domain.jp" />
        </div>
        <div class="field" style="min-width:160px;">
          <label for="label">ラベル</label>
          <input class="input" id="label" placeholder="work, personal など" />
        </div>
      </div>
      <div class="field">
        <label for="subject">件名</label>
        <input class="input" id="subject" placeholder="件名を入力" />
      </div>
      <div class="field">
        <label for="body">本文</label>
        <textarea id="body" placeholder="本文を入力"></textarea>
      </div>
    </div>
  </div>
</div>

<script>
/** ======= Simple Mail App (no backend) ======= */
const DB_KEY = "maillite-db-v1";
const QS = sel => document.querySelector(sel);
const QSA = sel => [...document.querySelectorAll(sel)];
const state = {
  currentFolder: "inbox",
  query: "",
  selectedIds: new Set(),
  currentId: null,
  db: { messages: [] }
};
const FOLDERS = [
  {id:"inbox",   name:"受信箱",    icon:"📥"},
  {id:"starred", name:"スター",    icon:"⭐"},
  {id:"sent",    name:"送信済み",  icon:"📤"},
  {id:"drafts",  name:"下書き",    icon:"📝"},
  {id:"archive", name:"アーカイブ",icon:"🗄️"},
  {id:"spam",    name:"迷惑",      icon:"🚫"},
  {id:"trash",   name:"ゴミ箱",    icon:"🗑️"},
];

function initDB(){
  const saved = localStorage.getItem(DB_KEY);
  if(saved){
    state.db = JSON.parse(saved);
    return;
  }
  // Seed sample messages
  const now = Date.now();
  const demo = [
    mkMsg("suzuki@example.com","ようこそ MailLite へ","MailLite をお試しいただきありがとうございます!\n\nこのメールはデモです。",["welcome"], now-3600_000),
    mkMsg("shop@ec.example.com","【お知らせ】サマーセール開催!","最大 50%OFF。今すぐチェック!",["promo"], now-7200_000),
    mkMsg("boss@company.jp","明日の打合せ議題","・リリース計画\n・障害対応\n・コスト見直し",["work"], now-86400_000, true),
    mkMsg("friend@chat.jp","週末の予定どう?","映画かカラオケ行かない?",["personal"], now-5400_000),
    mkMsg("security@service.jp","ログイン通知","新しい端末からログインがありました。",["security"], now-9600_000),
  ];
  demo[2].unread = false;
  state.db.messages = demo;
  persist();
}
function mkMsg(from, subject, body, labels=[], time=Date.now(), starred=false){
  const id = crypto.randomUUID();
  const initials = (from.split("@")[0][0]||"U").toUpperCase();
  return {
    id, box:"inbox", from, to:[], subject, body, labels, starred, unread:true,
    date: time, initials
  };
}
function persist(){ localStorage.setItem(DB_KEY, JSON.stringify(state.db)); }

function formatDate(ts){
  const d = new Date(ts);
  const pad = n => String(n).padStart(2,"0");
  return `${d.getFullYear()}/${pad(d.getMonth()+1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

// Render folders
function renderFolders(){
  const el = QS("#folders");
  el.innerHTML = "";
  for(const f of FOLDERS){
    const count = countFolder(f.id);
    const a = document.createElement("a");
    a.className = "nav-item" + (state.currentFolder===f.id?" active":"");
    a.dataset.id = f.id;
    a.innerHTML = `<span>${f.icon}</span><span>${f.name}</span><span class="count">${count||""}</span>`;
    a.addEventListener("click", ()=>{
      state.currentFolder = f.id;
      state.selectedIds.clear();
      state.currentId = null;
      render();
    });
    el.appendChild(a);
  }
}
function countFolder(folder){
  return filterByFolder(state.db.messages, folder).length;
}
function filterByFolder(list, folder){
  switch(folder){
    case "inbox":   return list.filter(m=>m.box==="inbox");
    case "starred": return list.filter(m=>m.starred && m.box!=="trash");
    case "sent":    return list.filter(m=>m.box==="sent");
    case "drafts":  return list.filter(m=>m.box==="drafts");
    case "archive": return list.filter(m=>m.box==="archive");
    case "spam":    return list.filter(m=>m.box==="spam");
    case "trash":   return list.filter(m=>m.box==="trash");
    default:        return list;
  }
}

// Search
function matchesQuery(m,q){
  if(!q) return true;
  const s = q.toLowerCase();
  const hay = [
    m.from, (m.to||[]).join(","), m.subject, m.body, (m.labels||[]).join(",")
  ].join("\n").toLowerCase();
  return hay.includes(s);
}

// Render list
function renderList(){
  const wrap = QS("#list");
  const items = filterByFolder(state.db.messages, state.currentFolder)
    .filter(m=>matchesQuery(m, state.query))
    .sort((a,b)=>b.date-a.date);
  wrap.innerHTML = "";
  if(items.length===0){
    wrap.innerHTML = `<div class="empty" style="height:100%;">${state.query? "検索結果がありません":"このフォルダは空です"}</div>`;
    return;
  }
  for(const m of items){
    const row = document.createElement("div");
    row.className = "msg" + (m.unread?" unread":"");
    row.dataset.id = m.id;
    const starClass = m.starred? "active":"";
    row.innerHTML = `
      <div class="avatar" title="${m.from}">${m.initials}</div>
      <div style="min-width:0">
        <div class="from">${m.from}</div>
        <div class="subject">${m.subject}</div>
        <div class="snippet">${(m.labels?.length? m.labels.map(l=>"#"+l).join(" ")+" · ":"")}${m.body.replace(/\n/g," ").slice(0,120)}</div>
      </div>
      <div class="meta">
        <div>${formatDate(m.date)}</div>
        <div class="star ${starClass}" data-star-id="${m.id}" title="スター">${m.starred?"★":"☆"}</div>
      </div>
    `;
    row.addEventListener("click", (e)=>{
      // If star clicked, don't open
      if(e.target && e.target.dataset.starId){ return; }
      openMessage(m.id);
    });
    row.querySelector(".star").addEventListener("click",(e)=>{
      e.stopPropagation();
      toggleStar(m.id);
    });
    wrap.appendChild(row);
  }
}

// Reader
function openMessage(id){
  const m = state.db.messages.find(x=>x.id===id);
  if(!m) return;
  state.currentId = id;
  m.unread = false;
  persist();
  QS("#emptyState").style.display = "none";
  QS("#readerWrap").style.display = "flex";
  QS("#readerSubject").textContent = m.subject || "(件名なし)";
  QS("#readerMeta").textContent    = `From: ${m.from}  /  To: ${(m.to||[]).join(", ")||"(なし)"} ・ ${formatDate(m.date)}`; 
  QS("#readerBody").innerHTML      = safeHtml(m.body).replace(/\n/g,"<br>");
  QS("#readerAvatar").textContent  = m.initials || "U";
  QS("#starIcon").textContent      = m.starred ? "★" : "☆";
  // Mobile: show reader
  QS("#layout").classList.add("show-reader");
  render();
}

function safeHtml(s=""){
  return s.replace(/[&<>"']/g, ch => ({
    "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"
  }[ch]));
}

// Actions
function getSelectedOrCurrentIds(){
  if(state.selectedIds.size>0) return [...state.selectedIds];
  if(state.currentId) return [state.currentId];
  return [];
}
function markRead(read=true){
  for(const id of getSelectedOrCurrentIds()){
    const m = state.db.messages.find(x=>x.id===id);
    if(m) m.unread = !read;
  }
  persist(); render();
}
function moveTo(box){
  for(const id of getSelectedOrCurrentIds()){
    const m = state.db.messages.find(x=>x.id===id);
    if(m) m.box = box;
  }
  // If current message was moved out of view, clear reader
  if(state.currentId){
    const cm = state.db.messages.find(x=>x.id===state.currentId);
    const visible = filterByFolder([cm], state.currentFolder).length>0;
    if(!visible){ state.currentId = null; QS("#readerWrap").style.display="none"; QS("#emptyState").style.display="grid"; }
  }
  persist(); render();
}
function toggleStar(id){
  const m = state.db.messages.find(x=>x.id===id);
  if(m){ m.starred = !m.starred; persist(); render(); if(state.currentId===id) QS("#starIcon").textContent=m.starred?"★":"☆"; }
}

// Compose
function openCompose(prefill={}){
  QS("#composeModal").classList.add("open");
  QS("#to").value      = prefill.to?.join(", ") || "";
  QS("#subject").value = prefill.subject || "";
  QS("#body").value    = prefill.body || "";
  QS("#label").value   = (prefill.labels||[]).join(", ");
}
function closeCompose(){ QS("#composeModal").classList.remove("open"); }

function saveDraft(){
  const draft = collectForm();
  draft.box = "drafts";
  draft.unread = false;
  draft.from = "me@local";
  draft.id = crypto.randomUUID();
  state.db.messages.push(draft);
  persist(); closeCompose(); render();
  alert("下書きを保存しました。");
}
function sendMail(){
  const msg = collectForm();
  if(!msg.to.length){ alert("宛先を入力してください"); return; }
  msg.box = "sent";
  msg.unread = false;
  msg.from = "me@local";
  msg.id = crypto.randomUUID();
  state.db.messages.push(msg);
  persist(); closeCompose(); render();
  alert("送信しました(デモ動作:実送信なし)");
}
function collectForm(){
  const to = QS("#to").value.split(",").map(s=>s.trim()).filter(Boolean);
  const subject = QS("#subject").value.trim() || "(件名なし)";
  const body = QS("#body").value;
  const labels = QS("#label").value.split(",").map(s=>s.trim()).filter(Boolean);
  return { to, subject, body, labels, date: Date.now(), starred:false, unread:true, initials:"M" };
}

// Selection (click+Ctrl/Shift optional simplified)
QS("#list").addEventListener("click",(e)=>{
  const row = e.target.closest(".msg");
  if(!row) return;
  if(e.ctrlKey || e.metaKey){
    const id = row.dataset.id;
    if(state.selectedIds.has(id)) state.selectedIds.delete(id); else state.selectedIds.add(id);
    row.classList.toggle("selected");
  }
});

// Topbar controls
QS("#composeBtn").onclick = ()=>openCompose();
QS("#composeBtn2").onclick = ()=>openCompose();
QS("#closeModalBtn").onclick = closeCompose;
QS("#saveDraftBtn").onclick = saveDraft;
QS("#sendBtn").onclick = sendMail;

QS("#search").addEventListener("input",(e)=>{ state.query = e.target.value.trim(); renderList(); });
QS("#clearSearch").onclick = ()=>{ QS("#search").value=""; state.query=""; renderList(); };

QS("#toggleDark").onclick = ()=>{
  document.body.classList.toggle("dark");
  localStorage.setItem("maillite-theme", document.body.classList.contains("dark")? "dark":"light");
};

// List toolbar actions
QS("#markReadBtn").onclick = ()=>markRead(true);
QS("#markUnreadBtn").onclick = ()=>markRead(false);
QS("#archiveBtn").onclick = ()=>moveTo("archive");
QS("#deleteBtn").onclick = ()=>moveTo("trash");

// Reader actions
QS("#replyBtn").onclick = ()=>{
  const m = state.db.messages.find(x=>x.id===state.currentId);
  if(!m) return;
  openCompose({ to:[m.from], subject:"Re: "+m.subject, body:`\n\n--- ${m.from} さんのメッセージ ---\n${m.body}`, labels:["reply"] });
};
QS("#starBtn").onclick = ()=>{ if(state.currentId) toggleStar(state.currentId); };
QS("#archBtn").onclick = ()=>moveTo("archive");
QS("#trashBtn").onclick = ()=>moveTo("trash");

// Clicking outside modal to close
QS("#composeModal").addEventListener("click",(e)=>{ if(e.target===QS("#composeModal")) closeCompose(); });

// Mobile back from reader by clicking empty area (or press Escape)
document.addEventListener("keydown",(e)=>{
  if(e.key==="Escape"){
    if(QS("#composeModal").classList.contains("open")) closeCompose();
    else QS("#layout").classList.remove("show-reader");
  }
});

// Initial render
function render(){
  renderFolders();
  renderList();
}
(function boot(){
  initDB();
  render();
  // Theme
  if(localStorage.getItem("maillite-theme")==="dark"){ document.body.classList.add("dark"); }
})();
</script>
</body>
</html>

ミニ百科.html

<!DOCTYPE html>
<html lang="ja" class="scroll-smooth">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>ミニ百科 – シングルファイル版</title>
  <meta name="description" content="検索・カテゴリ・タグ・ブックマーク対応のシングルファイル百科事典。" />
  <link rel="preconnect" href="https://cdn.jsdelivr.net" />
  <!-- TailwindCSS (CDN) -->
  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Font Awesome (icons) -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-5I0VnK5tQhJ0eZ5Ck1gC3b6h9fJ3k6l9FeI3K6J0q9JtO1Yw1l2Y7N5M6d2xQf8Q2F6mZ8l2s3A=" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <!-- Favicon (inline SVG) -->
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%234f46e5' d='M32 56c0-13.3 10.7-24 24-24h144c13.3 0 24 10.7 24 24v144c0 13.3-10.7 24-24 24H56c-13.3 0-24-10.7-24-24z'/%3E%3Cpath fill='white' d='M72 80h112v16H72zM72 112h80v16H72zM72 144h112v16H72zM72 176h96v16H72z'/%3E%3C/svg%3E" />
  <style>
    /* 追加の細かなスタイル */
    .line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
    .prose h2{scroll-margin-top:6rem}
    .toc a{display:block;padding:.25rem .5rem;border-radius:.5rem}
    .toc a.active{background:rgba(99,102,241,.12)}
  </style>
  <script>
    // ダークモード初期化
    (function(){
      const theme=localStorage.getItem('theme');
      if(theme==='dark'||(!theme&&window.matchMedia('(prefers-color-scheme: dark)').matches)){
        document.documentElement.classList.add('dark');
      }
    })();
  </script>
</head>
<body class="bg-slate-50 text-slate-800 dark:bg-slate-900 dark:text-slate-100 min-h-screen">
  <!-- Skip link -->
  <a href="#main" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:bg-indigo-600 focus:text-white focus:px-3 focus:py-2 focus:rounded">本文へスキップ</a>

  <!-- Header -->
  <header class="sticky top-0 z-40 backdrop-blur border-b border-slate-200/60 dark:border-slate-700/60 bg-white/70 dark:bg-slate-900/70">
    <div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-3 flex items-center gap-3">
      <button id="btnHome" class="shrink-0 px-2 py-1 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-900/40" title="ホーム">
        <i class="fa-solid fa-book-open text-indigo-600"></i>
      </button>
      <h1 class="text-lg sm:text-2xl font-bold tracking-tight">ミニ百科 <span class="text-indigo-600">Mini Encyclopedia</span></h1>
      <div class="ms-auto flex items-center gap-2">
        <button id="btnRandom" class="px-3 py-2 rounded-xl bg-indigo-600 text-white text-sm hover:opacity-90"><i class="fa-solid fa-shuffle me-1"></i>ランダム</button>
        <button id="btnBookmarks" class="px-3 py-2 rounded-xl bg-amber-500 text-white text-sm hover:opacity-90"><i class="fa-solid fa-star me-1"></i>ブックマーク</button>
        <button id="btnDark" class="px-3 py-2 rounded-xl bg-slate-800 text-white text-sm dark:bg-slate-700 hover:opacity-90" title="ダーク/ライト切替"><i class="fa-solid fa-moon"></i></button>
      </div>
    </div>
  </header>

  <!-- Toolbar -->
  <section class="border-b border-slate-200/60 dark:border-slate-700/60 bg-white/60 dark:bg-slate-900/60">
    <div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-4 grid gap-3 sm:grid-cols-12 items-end">
      <div class="sm:col-span-6">
        <label for="search" class="block text-sm text-slate-600 dark:text-slate-300 mb-1">記事検索</label>
        <div class="relative">
          <input id="search" type="search" placeholder="キーワード(例: 富士山 / 恐竜 / インターネット)" class="w-full rounded-2xl border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 px-4 py-2 pe-10 outline-none focus:ring-2 focus:ring-indigo-500" />
          <i class="fa-solid fa-magnifying-glass absolute right-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
        </div>
      </div>
      <div class="sm:col-span-3">
        <label for="category" class="block text-sm text-slate-600 dark:text-slate-300 mb-1">カテゴリ</label>
        <select id="category" class="w-full rounded-2xl border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 px-4 py-2 outline-none focus:ring-2 focus:ring-indigo-500">
          <option value="">すべて</option>
        </select>
      </div>
      <div class="sm:col-span-3">
        <label for="sort" class="block text-sm text-slate-600 dark:text-slate-300 mb-1">並び替え</label>
        <select id="sort" class="w-full rounded-2xl border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 px-4 py-2 outline-none focus:ring-2 focus:ring-indigo-500">
          <option value="recent">更新が新しい順</option>
          <option value="title">タイトル順</option>
        </select>
      </div>
      <div class="sm:col-span-12" id="tagBar" aria-label="タグフィルタ" class="flex flex-wrap gap-2"></div>
    </div>
  </section>

  <main id="main" class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-6">
    <!-- Home / List View -->
    <section id="view-home" class="grid gap-6">
      <div class="flex items-center justify-between">
        <h2 class="text-xl sm:text-2xl font-semibold">記事一覧</h2>
        <div class="text-sm text-slate-500"><span id="resultCount">0</span> 件</div>
      </div>
      <div id="cards" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
        <!-- cards injected -->
      </div>
      <div class="flex items-center justify-center gap-2 pt-2" id="pager"></div>
    </section>

    <!-- Article View -->
    <section id="view-article" class="hidden lg:grid lg:grid-cols-12 gap-8">
      <aside class="lg:col-span-3 order-last lg:order-first">
        <div class="sticky top-[6.5rem] border border-slate-200 dark:border-slate-700 rounded-2xl p-4">
          <h3 class="font-semibold mb-2">目次</h3>
          <nav id="toc" class="toc text-sm space-y-1"></nav>
        </div>
      </aside>
      <article class="lg:col-span-9">
        <nav class="text-sm text-slate-500 mb-3" id="breadcrumb"></nav>
        <header class="mb-4">
          <h1 id="articleTitle" class="text-2xl sm:text-3xl font-bold tracking-tight"></h1>
          <div class="mt-2 flex flex-wrap items-center gap-2 text-sm text-slate-500" id="articleMeta"></div>
          <div class="mt-3 flex items-center gap-2">
            <button id="btnCopyLink" class="px-3 py-2 rounded-xl border border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800"><i class="fa-solid fa-link me-1"></i>リンクをコピー</button>
            <button id="btnToggleBookmark" class="px-3 py-2 rounded-xl border border-amber-400 text-amber-600 hover:bg-amber-50"><i class="fa-regular fa-star me-1"></i>ブックマーク</button>
          </div>
        </header>
        <div id="articleContent" class="prose prose-slate dark:prose-invert max-w-none"></div>
        <section class="mt-8">
          <h3 class="font-semibold mb-2">関連タグ</h3>
          <div id="articleTags" class="flex flex-wrap gap-2"></div>
        </section>
      </article>
    </section>
  </main>

  <footer class="border-t border-slate-200 dark:border-slate-700 py-8">
    <div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 text-sm text-slate-500 flex flex-wrap items-center gap-2">
      <span>© <span id="year"></span> ミニ百科</span>
      <span class="mx-1">•</span>
      <button id="btnExport" class="underline underline-offset-4">データを書き出す(JSON)</button>
      <span class="mx-1">•</span>
      <label class="cursor-pointer">JSON読み込み <input id="fileImport" type="file" accept="application/json" class="hidden" /></label>
    </div>
  </footer>

  <!-- Structured data placeholder (updated on article view) -->
  <script id="ldjson" type="application/ld+json">{}</script>

  <script>
  // ======================
  //  サンプル記事データ
  // ======================
  /**
   * 記事スキーマ:
   * id, slug, title, category, tags[], summary, updated(ISO), author, content(HTML)
   */
  const ARTICLES = [
    {
      id: 1,
      slug: 'fuji-san',
      title: '富士山',
      category: '地理',
      tags: ['日本','山','世界文化遺産'],
      summary: '日本の象徴ともいわれる成層火山。標高3,776mで日本最高峰。',
      updated: '2025-08-15',
      author: 'ミニ百科編集部',
      content: `
        <p>富士山は本州中部に位置する<span>成層火山</span>で、標高は3,776m。2013年に世界文化遺産に登録されました。古来より信仰の対象であり、芸術や文学にも多く登場します。</p>
        <h2 id="geo">地形と地質</h2>
        <p>富士山は何度もの噴火活動を経て現在の美しい円錐形を形成しました。火口は山頂部にあり、外輪としてお鉢巡りが知られています。</p>
        <h2 id="climb">登山と保全</h2>
        <p>一般的な登山シーズンは夏。登山道の混雑やゴミ問題、低温・高山病などのリスク対策が重要です。</p>
        <h2 id="culture">文化的意義</h2>
        <p>葛飾北斎の『富嶽三十六景』をはじめ、絵画や和歌に頻繁に詠まれ、日本の象徴として国際的にも広く知られています。</p>
      `
    },
    {
      id: 2,
      slug: 'internet-basics',
      title: 'インターネットの基礎',
      category: 'テクノロジー',
      tags: ['ネットワーク','Web','通信'],
      summary: '世界中のコンピュータを相互接続する情報ネットワークの総称。',
      updated: '2025-07-01',
      author: 'ミニ百科編集部',
      content: `
        <p>インターネットは標準化された<span>TCP/IP</span>により機器同士が通信する巨大なネットワークです。Web、メール、動画配信など多様なサービスの土台になっています。</p>
        <h2 id="protocols">主要プロトコル</h2>
        <p>HTTP/HTTPS、DNS、SMTP、FTPなどが代表的。セキュリティ確保には暗号化や認証が重要です。</p>
        <h2 id="web">Webの仕組み</h2>
        <p>ブラウザがURLを解決し、サーバからHTML/CSS/JS等のリソースを取得・表示します。</p>
        <h2 id="safety">安全な利用</h2>
        <p>二要素認証、ソフトウェア更新、フィッシング対策、強力なパスワード管理が基本です。</p>
      `
    },
    {
      id: 3,
      slug: 'dinosaurs',
      title: '恐竜',
      category: '生物',
      tags: ['古生物学','白亜紀','化石'],
      summary: '中生代に栄えた爬虫類のグループ。鳥類は恐竜の系統に含まれると考えられている。',
      updated: '2025-05-28',
      author: 'ミニ百科編集部',
      content: `
        <p>恐竜は約2億3000万年前に出現し、中生代に多様化しました。<span>鳥類</span>は恐竜の一系統とみなされます。</p>
        <h2 id="era">時代区分</h2>
        <p>三畳紀・ジュラ紀・白亜紀に区分され、各時代で特徴的な種が繁栄しました。</p>
        <h2 id="extinction">大量絶滅</h2>
        <p>約6600万年前の大量絶滅で多くが消滅。隕石衝突や火山活動などが要因と考えられています。</p>
      `
    },
    {
      id: 4,
      slug: 'ww2-overview',
      title: '第二次世界大戦(概説)',
      category: '歴史',
      tags: ['20世紀','戦争','国際関係'],
      summary: '1939年から1945年にかけて行われた世界規模の戦争。',
      updated: '2025-03-10',
      author: 'ミニ百科編集部',
      content: `
        <p>第二次世界大戦は多数の国が参戦した世界規模の戦争で、政治・経済・科学技術・社会に長期の影響を与えました。</p>
        <h2 id="fronts">主要戦線</h2>
        <p>ヨーロッパ、太平洋、北アフリカ、東部戦線など多くの戦域に分かれました。</p>
        <h2 id="aftermath">戦後の世界</h2>
        <p>国際連合の設立、冷戦構造の形成、国際秩序の再編などにつながりました。</p>
      `
    },
    {
      id: 5,
      slug: 'ai-basics',
      title: '人工知能の基礎',
      category: 'テクノロジー',
      tags: ['AI','機械学習','深層学習'],
      summary: '知的な処理をコンピュータで実現する研究分野と技術群。',
      updated: '2025-06-12',
      author: 'ミニ百科編集部',
      content: `
        <p>人工知能は探索・推論から機械学習・深層学習まで多様な手法を含みます。現代では大量データと計算資源により実世界応用が拡大。</p>
        <h2 id="ml">機械学習</h2>
        <p>教師あり・教師なし・強化学習などの枠組みがあり、予測や分類に用いられます。</p>
        <h2 id="dl">深層学習</h2>
        <p>多層ニューラルネットワークにより画像・音声・自然言語処理で高精度を実現。</p>
      `
    },
    {
      id: 6,
      slug: 'sakura',
      title: 'サクラ(桜)',
      category: '文化',
      tags: ['日本文化','植物','季節'],
      summary: '日本の春を象徴する花。花見は古くからの季節行事。',
      updated: '2025-04-02',
      author: 'ミニ百科編集部',
      content: `
        <p>桜はバラ科サクラ属の総称。品種が多く、花期は短いものの観賞価値が高いことで知られます。</p>
        <h2 id="hanami">花見の歴史</h2>
        <p>貴族文化から庶民に広がり、現在では地域の祭りや観光資源にもなっています。</p>
      `
    },
    {
      id: 7,
      slug: 'japan-history-outline',
      title: '日本史(概説)',
      category: '歴史',
      tags: ['古代','中世','近代'],
      summary: '古代から現代までの日本の歴史を大まかに概観する。',
      updated: '2025-01-20',
      author: 'ミニ百科編集部',
      content: `
        <p>日本史は縄文・弥生・古墳などの古代から、中世・近世、明治以降の近代・現代に至るまで連続する多様な変化の歴史です。</p>
        <h2 id="ancient">古代</h2>
        <p>稲作の普及、古代国家の形成、律令制の確立など。</p>
        <h2 id="modern">近代・現代</h2>
        <p>近代化、戦後復興、高度経済成長、少子高齢化と新たな課題。</p>
      `
    },
    {
      id: 8,
      slug: 'programming-intro',
      title: 'プログラミング入門',
      category: 'テクノロジー',
      tags: ['コード','アルゴリズム','学習'],
      summary: 'コンピュータに手順を伝えるための技術と考え方の総称。',
      updated: '2025-07-22',
      author: 'ミニ百科編集部',
      content: `
        <p>プログラミングは問題を分解し、再利用可能な手順として表現する作業です。変数、条件分岐、反復、関数などの基本を学ぶと応用が広がります。</p>
        <h2 id="lang">主な言語</h2>
        <p>Python、JavaScript、C#、C++ など用途に応じて選択されます。</p>
      `
    },
    {
      id: 9,
      slug: 'tea-ceremony',
      title: '茶道',
      category: '文化',
      tags: ['日本文化','礼法','芸道'],
      summary: '湯を沸かし茶を点て、客をもてなす総合芸術。',
      updated: '2025-05-03',
      author: 'ミニ百科編集部',
      content: `
        <p>茶道は道具、作法、空間、季節感などが一体となる総合芸術です。<span>和敬清寂</span>の精神が重視されます。</p>
        <h2 id="tools">道具</h2>
        <p>茶碗、茶筅、茶杓、釜、柄杓など。取り扱いには所作と配慮が求められます。</p>
      `
    },
    {
      id: 10,
      slug: 'solar-system',
      title: '太陽系',
      category: '天文学',
      tags: ['惑星','衛星','宇宙'],
      summary: '太陽とその周囲を公転する天体の集まり。',
      updated: '2025-06-30',
      author: 'ミニ百科編集部',
      content: `
        <p>太陽系は太陽を中心に、8つの惑星、準惑星、小惑星、彗星、塵やガスが重力で結びつくシステムです。</p>
        <h2 id="planets">惑星</h2>
        <p>水星・金星・地球・火星・木星・土星・天王星・海王星。各惑星は固有の特徴を持ちます。</p>
      `
    }
  ];

  // ================
  // ユーティリティ
  // ================
  const $ = (sel, root=document) => root.querySelector(sel);
  const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
  const fmtDate = iso => new Date(iso).toLocaleDateString('ja-JP', {year:'numeric', month:'short', day:'numeric'});
  const unique = arr => [...new Set(arr)];
  const slugToArticle = slug => ARTICLES.find(a=>a.slug===slug);
  const STORAGE = {
    bookmarks: 'mini_ency_bookmarks',
    history: 'mini_ency_history'
  };

  function getBookmarks(){
    try{return JSON.parse(localStorage.getItem(STORAGE.bookmarks)||'[]');}catch{ return []; }
  }
  function setBookmarks(list){ localStorage.setItem(STORAGE.bookmarks, JSON.stringify(unique(list))); }
  function isBookmarked(slug){ return getBookmarks().includes(slug); }
  function toggleBookmark(slug){
    const list=getBookmarks();
    if(list.includes(slug)) setBookmarks(list.filter(s=>s!==slug));
    else setBookmarks([...list, slug]);
  }
  function pushHistory(slug){
    try{
      const now = Date.now();
      const hist = JSON.parse(localStorage.getItem(STORAGE.history)||'[]');
      const filtered = hist.filter(h=>h.slug!==slug);
      filtered.unshift({slug, t: now});
      localStorage.setItem(STORAGE.history, JSON.stringify(filtered.slice(0,50)));
    }catch{}
  }

  // ================
  // 検索・フィルタ
  // ================
  let state = {
    q: '',
    category: '',
    tag: '',
    sort: 'recent',
    page: 1,
    perPage: 9
  };

  function normalize(str){ return (str||'').toString().toLowerCase(); }

  function filterArticles(){
    let list = ARTICLES.slice();
    if(state.q){
      const q = normalize(state.q);
      list = list.filter(a => normalize(a.title+" "+a.summary+" "+a.tags.join(' ')+" "+a.content.replace(/<[^>]+>/g,'')).includes(q));
    }
    if(state.category){ list = list.filter(a => a.category===state.category); }
    if(state.tag){ list = list.filter(a => a.tags.includes(state.tag)); }
    if(state.sort==='recent'){ list.sort((a,b)=> new Date(b.updated)-new Date(a.updated)); }
    if(state.sort==='title'){ list.sort((a,b)=> a.title.localeCompare(b.title,'ja')); }
    return list;
  }

  // =============
  //  一覧描画
  // =============
  function renderCategories(){
    const select = $('#category');
    const cats = unique(ARTICLES.map(a=>a.category)).sort((a,b)=>a.localeCompare(b,'ja'));
    select.innerHTML = '<option value="">すべて</option>' + cats.map(c=>`<option value="${c}">${c}</option>`).join('');
  }

  function renderTagsBar(){
    const bar = $('#tagBar');
    const tags = unique(ARTICLES.flatMap(a=>a.tags)).sort((a,b)=>a.localeCompare(b,'ja'));
    bar.innerHTML = '<div class="flex flex-wrap gap-2">' + tags.map(t=>
      `<button data-tag="${t}" class="tag-btn px-3 py-1 rounded-full border border-slate-300 dark:border-slate-700 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 ${state.tag===t?'bg-indigo-600 text-white border-indigo-600':''}">#${t}</button>`
    ).join('') + '</div>';
    $$('.tag-btn').forEach(b=> b.addEventListener('click',()=>{ state.tag = (state.tag===b.dataset.tag? '' : b.dataset.tag); state.page=1; syncList(); }));
  }

  function createCard(a){
    const bookmarked = isBookmarked(a.slug);
    return `
      <article class="border border-slate-200 dark:border-slate-700 rounded-2xl p-4 bg-white/70 dark:bg-slate-800/70 hover:shadow transition">
        <header class="flex items-start justify-between gap-3">
          <h3 class="text-lg font-semibold leading-tight">${a.title}</h3>
          <button class="bookmark inline-flex items-center justify-center w-9 h-9 rounded-full ${bookmarked?'text-amber-500':'text-slate-400'}" title="ブックマーク" data-slug="${a.slug}">
            <i class="${bookmarked?'fa-solid':'fa-regular'} fa-star"></i>
          </button>
        </header>
        <div class="mt-1 text-sm text-slate-500">${a.category}・更新 ${fmtDate(a.updated)}</div>
        <p class="mt-2 text-sm line-clamp-3">${a.summary}</p>
        <div class="mt-3 flex flex-wrap gap-2 text-xs">${a.tags.map(t=>`<span class='px-2 py-1 rounded-full bg-slate-100 dark:bg-slate-700'>#${t}</span>`).join('')}</div>
        <div class="mt-4 flex gap-2">
          <a href="#/a/${a.slug}" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl bg-indigo-600 text-white text-sm hover:opacity-90"><i class="fa-solid fa-circle-info"></i> 詳細</a>
          <button class="copy-link inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-slate-300 dark:border-slate-700 text-sm hover:bg-slate-100 dark:hover:bg-slate-800" data-link="${location.origin+location.pathname}#/a/${a.slug}"><i class="fa-solid fa-link"></i>リンク</button>
        </div>
      </article>
    `;
  }

  function renderPager(total){
    const pager = $('#pager');
    const pages = Math.max(1, Math.ceil(total/state.perPage));
    state.page = Math.min(state.page, pages);
    let html='';
    for(let i=1;i<=pages;i++){
      html += `<button class="px-3 py-1 rounded-lg border ${i===state.page?'bg-indigo-600 text-white border-indigo-600':'border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800'}" data-page="${i}">${i}</button>`;
    }
    pager.innerHTML = html;
    $$('#pager button').forEach(b=> b.addEventListener('click',()=>{ state.page=Number(b.dataset.page); syncList(false); }));
  }

  function syncList(scrollTop=true){
    const list = filterArticles();
    $('#resultCount').textContent = list.length;
    renderPager(list.length);

    const start=(state.page-1)*state.perPage;
    const pageItems=list.slice(start, start+state.perPage);
    $('#cards').innerHTML = pageItems.map(createCard).join('');

    // events
    $$('.bookmark').forEach(b=> b.addEventListener('click',()=>{ toggleBookmark(b.dataset.slug); syncList(false); }));
    $$('.copy-link').forEach(b=> b.addEventListener('click',()=> copyText(b.dataset.link)));

    if(scrollTop) window.scrollTo({top:0, behavior:'smooth'});
  }

  // =================
  // 記事ページ描画
  // =================
  function renderArticle(slug){
    const a = slugToArticle(slug);
    if(!a){ location.hash = ''; return; }

    // breadcrumb
    $('#breadcrumb').innerHTML = `<a class="underline" href="#">ホーム</a> / <span class="text-slate-600">${a.category}</span>`;

    // title & meta
    $('#articleTitle').textContent = a.title;
    $('#articleMeta').innerHTML = `
      <span><i class="fa-regular fa-calendar"></i> 更新 ${fmtDate(a.updated)}</span>
      <span class="mx-1">•</span>
      <span><i class="fa-regular fa-user"></i> ${a.author}</span>
      <span class="mx-1">•</span>
      <span><i class="fa-solid fa-folder"></i> ${a.category}</span>
    `;

    // content
    const container = $('#articleContent');
    container.innerHTML = a.content;

    // tags
    $('#articleTags').innerHTML = a.tags.map(t=>`<a href="#" data-tag="${t}" class="px-3 py-1 rounded-full border border-slate-300 dark:border-slate-700 text-sm hover:bg-slate-100 dark:hover:bg-slate-800">#${t}</a>`).join('');
    $$('#articleTags a').forEach(el=> el.addEventListener('click',(e)=>{ e.preventDefault(); state.tag=el.dataset.tag; location.hash=''; }));

    // bookmark button
    const btnBM = $('#btnToggleBookmark');
    const setBM = ()=>{
      const marked = isBookmarked(a.slug);
      btnBM.innerHTML = `<i class="${marked?'fa-solid':'fa-regular'} fa-star me-1"></i>${marked?'保存済み':'ブックマーク'}`;
    };
    btnBM.onclick = ()=>{ toggleBookmark(a.slug); setBM(); };
    setBM();

    // copy link
    $('#btnCopyLink').onclick = ()=> copyText(location.href);

    // TOC
    buildTOC();

    // JSON-LD
    updateLDJSON(a);

    // history
    pushHistory(a.slug);
  }

  function buildTOC(){
    const toc = $('#toc');
    const headings = $$('#articleContent h2, #articleContent h3');
    if(headings.length===0){ toc.innerHTML = '<div class="text-slate-500 text-sm">見出しがありません</div>'; return; }
    let html='';
    headings.forEach(h=>{
      if(!h.id) h.id = h.textContent.trim().toLowerCase().replace(/[^a-z0-9一-龥ぁ-んァ-ヶー]+/g,'-');
      const indent = h.tagName==='H3' ? 'ms-4' : '';
      html += `<a href="#${h.id}" class="${indent} hover:text-indigo-600">${h.textContent}</a>`;
    });
    toc.innerHTML = html;

    const observer = new IntersectionObserver((entries)=>{
      entries.forEach(e=>{
        if(e.isIntersecting){
          $$('#toc a').forEach(a=>a.classList.remove('active'));
          const a = $(`#toc a[href="#${e.target.id}"]`);
          if(a) a.classList.add('active');
        }
      });
    }, {rootMargin: '0px 0px -70% 0px'});
    headings.forEach(h=> observer.observe(h));
  }

  function updateLDJSON(a){
    const obj = {
      '@context':'https://schema.org',
      '@type':'Article',
      headline: a.title,
      dateModified: a.updated,
      author: { '@type':'Organization', name: a.author },
      keywords: a.tags.join(','),
      articleSection: a.category,
      url: location.href
    };
    $('#ldjson').textContent = JSON.stringify(obj);
  }

  // ============
  //  ルーター
  // ============
  function route(){
    const hash = location.hash.slice(1);
    if(hash.startsWith('/a/')){
      const slug = hash.split('/')[2];
      $('#view-home').classList.add('hidden');
      $('#view-article').classList.remove('hidden');
      renderArticle(slug);
      window.scrollTo({top:0, behavior:'instant'});
    }else{
      $('#view-article').classList.add('hidden');
      $('#view-home').classList.remove('hidden');
      syncList();
    }
  }

  window.addEventListener('hashchange', route);

  // ============
  //  便利機能
  // ============
  function copyText(text){
    navigator.clipboard.writeText(text).then(()=>{
      toast('リンクをコピーしました');
    }, ()=>{
      prompt('コピーできない場合は手動で選択してコピーしてください:', text);
    });
  }

  function toast(msg){
    const t = document.createElement('div');
    t.textContent = msg;
    t.className = 'fixed left-1/2 -translate-x-1/2 bottom-6 z-50 bg-black/80 text-white px-4 py-2 rounded-xl text-sm';
    document.body.appendChild(t);
    setTimeout(()=>{ t.remove(); }, 1600);
  }

  function randomArticle(){
    const a = ARTICLES[Math.floor(Math.random()*ARTICLES.length)];
    location.hash = `#/a/${a.slug}`;
  }

  // ==============
  //  I/O (JSON)
  // ==============
  function exportJSON(){
    const blob = new Blob([JSON.stringify(ARTICLES, null, 2)], {type:'application/json'});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'mini-encyclopedia.json'; a.click();
    URL.revokeObjectURL(url);
  }

  function importJSON(file){
    const reader = new FileReader();
    reader.onload = (e)=>{
      try{
        const data = JSON.parse(e.target.result);
        if(Array.isArray(data)){
          // 形式が正しければ差し替え
          if(data.every(x=>x.slug && x.title && x.content)){
            ARTICLES.length = 0; // 破壊的更新
            data.forEach(x=> ARTICLES.push(x));
            init();
            toast('JSONを読み込みました');
          }else{
            alert('スキーマが不正です。slug/title/content は必須です。');
          }
        }else{
          alert('配列形式のJSONが必要です');
        }
      }catch(err){
        alert('JSONの解析に失敗しました: '+err.message);
      }
    };
    reader.readAsText(file);
  }

  // ============
  //  初期化
  // ============
  function init(){
    // 年
    $('#year').textContent = new Date().getFullYear();

    // カテゴリ・タグバー
    renderCategories();
    renderTagsBar();

    // イベント
    $('#search').addEventListener('input', (e)=>{ state.q = e.target.value.trim(); state.page=1; syncList(); });
    $('#category').addEventListener('change', (e)=>{ state.category = e.target.value; state.page=1; syncList(); });
    $('#sort').addEventListener('change', (e)=>{ state.sort = e.target.value; state.page=1; syncList(); });
    $('#btnRandom').addEventListener('click', randomArticle);
    $('#btnBookmarks').addEventListener('click', ()=>{
      const bms = getBookmarks();
      if(bms.length===0){ toast('ブックマークはまだありません'); return; }
      const first = slugToArticle(bms[0]);
      if(first) location.hash = `#/a/${first.slug}`;
    });
    $('#btnHome').addEventListener('click', ()=>{ location.hash=''; });

    // ダークモード切替
    $('#btnDark').addEventListener('click', ()=>{
      const root = document.documentElement;
      const isDark = root.classList.toggle('dark');
      localStorage.setItem('theme', isDark? 'dark':'light');
    });

    // JSON I/O
    $('#btnExport').addEventListener('click', exportJSON);
    $('#fileImport').addEventListener('change', (e)=>{ const f=e.target.files?.[0]; if(f) importJSON(f); e.target.value=''; });

    // 初回描画
    route();
  }

  document.addEventListener('DOMContentLoaded', init);
  </script>
</body>
</html>

XLogpro.html(ツイートまとめサイト)

<!DOCTYPE html>
<html lang="ja" data-theme="light" style="--cols:3; --card-h:640px; --accent:#2563eb">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Xlog Pro — HTMLだけで動く自動ツイートまとめ</title>
  <link rel="preconnect" href="https://platform.twitter.com" crossorigin>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/>
  <meta name="description" content="HTMLだけ/APIキー不要のX(Twitter)まとめボード。プロフィール・ハッシュタグ・検索・リストを好きな列で配置し、JSON/HTML書き出しや手動ランキング、ボード切替に対応。">
  <style>
    :root{
      --bg: #0b0e14; --panel:#111827; --muted:#9aa4b2; --text:#e5e7eb; --border:#1f2937; --chip:#141a23; --card:#0f172a; --btn:#1f2937; --btn-text:#e5e7eb; --link:#60a5fa;
    }
    [data-theme="light"]{ --bg:#f8fafc; --panel:#ffffff; --muted:#64748b; --text:#0f172a; --border:#e2e8f0; --chip:#f1f5f9; --card:#ffffff; --btn:#0f172a; --btn-text:#ffffff; --link:#1d9bf0; }
    *{box-sizing:border-box}
    body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"}
    header{position:sticky;top:0;z-index:10;background:var(--panel);border-bottom:1px solid var(--border)}
    .wrap{max-width:1280px;margin:0 auto;padding:12px 16px}
    .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
    .brand{display:flex;gap:10px;align-items:center;font-weight:800}
    .brand i{color:var(--accent)}
    .muted{color:var(--muted)}
    .pill{display:inline-flex;gap:8px;align-items:center;background:var(--chip);border:1px solid var(--border);border-radius:999px;padding:6px 10px}
    .input, select, textarea{background:transparent;border:1px solid var(--border);border-radius:10px;padding:8px 10px;color:var(--text)}
    textarea{min-height:88px;width:100%;}
    input[type="text"].input{min-width:220px}
    button{cursor:pointer;border:none}
    .btn{background:var(--btn);color:var(--btn-text);padding:9px 12px;border-radius:12px}
    .btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)}
    .btn.ghost{background:transparent;color:var(--text)}
    .btn.badge{padding:6px 10px;border-radius:999px}
    .grid{display:grid;grid-template-columns:320px 1fr;gap:16px}
    @media (max-width:1080px){.grid{grid-template-columns:1fr}}
    aside{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:14px;position:sticky;top:72px;height:max-content}
    h2{margin:6px 0 12px 0;font-size:18px}
    .list{display:flex;flex-direction:column;gap:12px}
    .card{background:var(--card);border:1px solid var(--border);border-radius:16px;overflow:hidden}
    .card .head{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--border)}
    .card .head .title{display:flex;gap:8px;align-items:center;font-weight:700}
    .card .body{padding:0;min-height:var(--card-h)}
    .sources{display:flex;flex-wrap:wrap;gap:8px}
    .chip{background:var(--chip);border:1px solid var(--border);border-radius:999px;padding:6px 10px;display:flex;gap:8px;align-items:center}
    .chip b{color:var(--accent)}
    .columns{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
    @media (max-width:1200px){:root{--cols:2}}
    @media (max-width:860px){:root{--cols:1}}
    .drag{cursor:grab}
    .toolbar{display:flex;gap:8px;flex-wrap:wrap}
    .footer{padding:24px 16px;color:var(--muted);text-align:center}
    .kbd{font-family:ui-monospace, Menlo, Monaco, Consolas; background:var(--chip); border:1px solid var(--border); padding:2px 6px; border-radius:6px}
    .danger{color:#ef4444}
    .accent{color:var(--accent)}
    .section{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:14px}
    .help{font-size:13px;color:var(--muted)}
    .label{font-size:12px;color:var(--muted)}
    .tiny{font-size:12px}
    .row-wrap{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
    .w-100{width:100%}
    .space{height:8px}
  </style>
</head>
<body>
  <header>
    <div class="wrap row">
      <div class="brand"><i class="fa-solid fa-wave-square"></i> Xlog <span class="muted">Pro</span></div>
      <div class="pill">
        <i class="fa-solid fa-diagram-project"></i>
        <select id="boardSelect" title="ボード切替"></select>
        <button id="boardNew" class="btn badge secondary" title="新規ボード"><i class="fa-solid fa-plus"></i></button>
        <button id="boardRename" class="btn badge secondary" title="名前変更"><i class="fa-solid fa-pen"></i></button>
        <button id="boardDelete" class="btn badge secondary danger" title="削除"><i class="fa-regular fa-trash-can"></i></button>
      </div>
      <div class="pill" title="テーマ切替"><i class="fa-solid fa-circle-half-stroke"></i>
        <label class="row-wrap"><input id="themeToggle" type="checkbox" /> ダーク</label>
      </div>
      <div class="pill" title="アクセントカラー">
        <i class="fa-solid fa-palette"></i>
        <input id="accentPicker" type="color" value="#2563eb" />
      </div>
      <div class="pill" title="列数"><i class="fa-solid fa-table-columns"></i>
        <input id="colsRange" type="range" min="1" max="4" step="1" value="3"/>
        <span id="colsVal" class="tiny"></span>
      </div>
      <div class="pill" title="カード高さ"><i class="fa-solid fa-up-down"></i>
        <input id="heightRange" type="range" min="360" max="1200" step="40" value="640"/>
        <span id="heightVal" class="tiny"></span>
      </div>
      <div class="pill" title="自動再読み込み">
        <label class="row-wrap"><input id="autoRefreshToggle" type="checkbox"/> 自動</label>
        <select id="refreshMinutes">
          <option value="3">3分</option>
          <option value="5" selected>5分</option>
          <option value="10">10分</option>
          <option value="30">30分</option>
        </select>
      </div>
      <div class="pill help tiny">ショートカット: <span class="kbd">N</span> 追加 / <span class="kbd">R</span> 再描画 / <span class="kbd">G</span> グリッド- / <span class="kbd">H</span> 高さ-</div>
    </div>
  </header>

  <main class="wrap grid">
    <aside>
      <div class="list">
        <div class="section">
          <h2>ソースを追加</h2>
          <div class="toolbar row-wrap">
            <select id="sourceType">
              <option value="profile">プロフィール</option>
              <option value="hashtag">ハッシュタグ</option>
              <option value="search">検索キーワード</option>
              <option value="list">リストURL</option>
            </select>
            <input id="sourceValue" class="input" type="text" placeholder="@username / #tag / キーワード / リストURL" />
            <input id="sourceLabel" class="input" type="text" placeholder="表示名(任意)" />
            <button id="addBtn" class="btn"><i class="fa-solid fa-plus"></i> 追加</button>
          </div>
          <div class="space"></div>
          <label class="label">まとめて追加(改行/カンマ区切りOK)</label>
          <textarea id="bulkArea" placeholder="@OpenAI, #UnrealEngine, Unity URP, https://twitter.com/i/lists/123...\n@EpicGames"></textarea>
          <div class="row-wrap">
            <button id="bulkAdd" class="btn secondary"><i class="fa-solid fa-download"></i> 取り込み</button>
            <button id="bulkClear" class="btn ghost"><i class="fa-solid fa-eraser"></i> クリア</button>
          </div>
          <p class="help" style="margin-top:8px">形式は自動判定:<span class="kbd">@id</span> → プロフィール、<span class="kbd">#tag</span> → ハッシュタグ、<span class="kbd">twitter.com/i/lists</span> → リスト、それ以外は検索。</p>
          <div class="space"></div>
          <div class="row-wrap help tiny">クイック追加:</div>
          <div class="row-wrap">
            <button class="btn badge secondary quick" data-type="hashtag" data-val="#UnrealEngine">#UnrealEngine</button>
            <button class="btn badge secondary quick" data-type="hashtag" data-val="#Unity3D">#Unity3D</button>
            <button class="btn badge secondary quick" data-type="search" data-val="VRM OR \"Meta Quest\"">VR/Quest</button>
            <button class="btn badge secondary quick" data-type="profile" data-val="@OpenAI">@OpenAI</button>
          </div>
        </div>

        <div class="section">
          <h2>保存・書き出し</h2>
          <div class="toolbar row-wrap">
            <button id="exportBtn" class="btn secondary"><i class="fa-solid fa-file-export"></i> JSON</button>
            <label class="btn secondary" for="importFile"><i class="fa-solid fa-file-import"></i> JSON読込</label>
            <input id="importFile" type="file" accept="application/json" hidden />
            <button id="exportHtmlBtn" class="btn"><i class="fa-regular fa-file-code"></i> 単一HTML</button>
            <button id="clearBtn" class="btn ghost danger"><i class="fa-regular fa-trash-can"></i> すべて削除</button>
          </div>
          <p class="help">単一HTML: いまのレイアウトと設定を埋め込んだ自立HTMLを生成します。</p>
        </div>

        <div class="section">
          <h2>手動ランキング</h2>
          <div class="toolbar row-wrap">
            <input id="tweetUrl" class="input" type="text" placeholder="ツイートURLを貼り付け" />
            <button id="addTweetBtn" class="btn"><i class="fa-brands fa-x-twitter"></i> 追加</button>
          </div>
          <label class="label">メモ(任意・次回以降も保持)</label>
          <textarea id="tweetNote" placeholder="このツイートの要点やタグ(例: #UE5 #VRM)"></textarea>
          <p class="help">※HTMLのみの制約で自動集計は不可。URLをカード化して手動で順序を決められます。</p>
        </div>

        <div class="section">
          <h2>RSS生成(ランキング→RSS)</h2>
          <div class="toolbar row-wrap">
            <input id="rssTitle" class="input" type="text" placeholder="RSSタイトル(例: Xlogランキング)"/>
            <button id="rssExport" class="btn secondary"><i class="fa-solid fa-rss"></i> RSSを書き出し</button>
          </div>
          <p class="help">ランキングに登録したツイートURLから簡易RSS(XML)を生成し、ファイルとして保存します。</p>
        </div>

        <div class="section">
          <h2>ヘルプ</h2>
          <div class="help">
            ・列の並べ替えはカードの <span class="kbd">⋯</span> アイコンをドラッグ。<br>
            ・<span class="kbd">R</span> で全カラムを再描画。<br>
            ・URLハッシュ <span class="kbd">#data=</span> に設定をBase64で埋め込んで共有可能(メニューから自動生成予定)。
          </div>
        </div>
      </div>
    </aside>

    <section>
      <div class="card" style="margin-bottom:16px">
        <div class="head">
          <div class="title"><i class="fa-solid fa-layer-group drag"></i> マイまとめ <span class="muted tiny" id="boardInfo"></span></div>
          <div class="sources" id="activeChips"></div>
        </div>
        <div class="body" style="padding:14px">
          <div id="columns" class="columns"></div>
        </div>
      </div>

      <div class="card">
        <div class="head">
          <div class="title"><i class="fa-regular fa-star"></i> 手動ランキング</div>
          <div class="help">ドラッグで順序変更/🗑で削除/✎でメモ編集</div>
        </div>
        <div class="body" style="padding:14px">
          <div id="ranking" class="columns"></div>
        </div>
      </div>

      <div class="footer">Xlog Pro v2 — HTML Only / Embedded Timelines. No API keys. <span class="muted">Made for you.</span></div>
    </section>
  </main>

  <script async src="https://platform.twitter.com/widgets.js"></script>
  <script>
    // ========== 基本ユーティリティ ==========
    const $ = (s, d=document)=>d.querySelector(s);
    const $$ = (s, d=document)=>Array.from(d.querySelectorAll(s));

    const defaultBoard = ()=>({sources:[], tweets:[]});
    const defaultState = ()=>({
      version:2,
      dark:false,
      accent:'#2563eb',
      autoRefresh:false,
      minutes:5,
      columns:3,
      cardHeight:640,
      boards:{'Default': defaultBoard()},
      activeBoard:'Default'
    });

    const store = {
      key: 'xlog-pro-v2',
      load(){
        try{ return JSON.parse(localStorage.getItem(this.key)) || defaultState(); }
        catch(e){ return defaultState(); }
      },
      save(v){ localStorage.setItem(this.key, JSON.stringify(v)); }
    };

    function migrate(s){
      const base = defaultState();
      if (!s || typeof s !== 'object') return base;
      // v1互換(sources/tweets直下 → boards.Default)
      if (s.sources || s.tweets){
        base.boards.Default.sources = s.sources||[];
        base.boards.Default.tweets = s.tweets||[];
      }
      // 既存キー上書き
      for (const k of ['dark','accent','autoRefresh','minutes','columns','cardHeight','boards','activeBoard']){
        if (k in s) base[k]=s[k];
      }
      return base;
    }

    // ハッシュ (#data=BASE64) から読み込み
    function loadFromHash(){
      const h = location.hash || '';
      if (!h.startsWith('#data=')) return null;
      try{
        const b64 = decodeURIComponent(h.slice(6));
        const json = atob(b64);
        return JSON.parse(json);
      }catch(e){ return null; }
    }

    let embedded = (typeof window.__XLOG_INITIAL_STATE__!== 'undefined') ? window.__XLOG_INITIAL_STATE__ : null;
    if (!embedded){
      const el = document.getElementById('xlog-init');
      if (el) { try{ embedded = JSON.parse(el.textContent); }catch(_e){} }
    }

    let state = migrate( embedded || loadFromHash() || store.load() );

    // ========== テーマ/アクセント/レイアウト適用 ==========
    function applySkin(){
      document.documentElement.setAttribute('data-theme', state.dark ? 'dark' : 'light');
      document.documentElement.style.setProperty('--cols', state.columns);
      document.documentElement.style.setProperty('--card-h', state.cardHeight+'px');
      document.documentElement.style.setProperty('--accent', state.accent || '#2563eb');
      $('#themeToggle').checked = !!state.dark;
      $('#colsRange').value = String(state.columns);
      $('#colsVal').textContent = state.columns+'列';
      $('#heightRange').value = String(state.cardHeight);
      $('#heightVal').textContent = state.cardHeight+'px';
      $('#accentPicker').value = state.accent || '#2563eb';
    }

    // ========== X埋め込み ==========
    function waitTwttr(){
      return new Promise(res=>{
        if (window.twttr && twttr.widgets) return res();
        const timer = setInterval(()=>{ if(window.twttr && twttr.widgets){ clearInterval(timer); res(); } }, 200);
      });
    }

    function timelineOptions(){
      return {
        height: state.cardHeight,
        theme: state.dark ? 'dark' : 'light',
        chrome: 'nofooter noborders transparent',
        linkColor: getComputedStyle(document.documentElement).getPropertyValue('--link').trim() || '#1d9bf0'
      };
    }

    async function createTimeline(el, src){
      await waitTwttr();
      const opts = timelineOptions();
      const t = (src.type||'profile');
      if (t==='profile'){
        const screenName = src.value.replace(/^@/,'');
        return twttr.widgets.createTimeline({ sourceType:'profile', screenName }, el, opts);
      }
      if (t==='list'){
        return twttr.widgets.createTimeline({ sourceType:'url', url: src.value }, el, opts);
      }
      if (t==='hashtag'){
        const tag = src.value.replace(/^#/,'');
        const url = `https://twitter.com/hashtag/${encodeURIComponent(tag)}?f=live`;
        return twttr.widgets.createTimeline({ sourceType:'url', url }, el, opts);
      }
      if (t==='search'){
        const url = `https://twitter.com/search?q=${encodeURIComponent(src.value)}&f=live`;
        return twttr.widgets.createTimeline({ sourceType:'url', url }, el, opts);
      }
    }

    // ========== 現在ボードの参照 ==========
    function board(){ return state.boards[state.activeBoard] || (state.boards[state.activeBoard]=defaultBoard()); }

    // ========== 描画 ==========
    function chipNode(src, idx){
      const chip = document.createElement('span');
      chip.className='chip';
      const kind = {profile:'@',hashtag:'#',search:'検索:',list:'リスト'}[src.type] || '';
      chip.innerHTML = `<b>${kind}</b> ${src.label || src.value} <a class="muted" href="${openUrl(src)}" target="_blank" title="Xで開く"><i class="fa-solid fa-arrow-up-right-from-square"></i></a> <button title="削除" data-del="${idx}" class="muted"><i class="fa-solid fa-xmark"></i></button>`;
      chip.querySelector('button').onclick = ()=>{ board().sources.splice(idx,1); store.save(state); renderAll(); };
      return chip;
    }

    function openUrl(src){
      if (src.type==='profile') return `https://twitter.com/${src.value.replace(/^@/,'')}`;
      if (src.type==='hashtag') return `https://twitter.com/hashtag/${src.value.replace(/^#/,'')}`;
      if (src.type==='list') return src.value;
      return `https://twitter.com/search?q=${encodeURIComponent(src.value)}&f=live`;
    }

    function columnCard(src, idx){
      const card = document.createElement('div');
      card.className='card';
      card.draggable=true; card.dataset.idx=idx;
      card.innerHTML = `
        <div class="head">
          <div class="title"><i class="fa-solid fa-grip-vertical drag"></i> ${src.label || prettyLabel(src)}</div>
          <div class="toolbar">
            <a class="btn ghost" href="${openUrl(src)}" target="_blank" title="Xで開く"><i class="fa-solid fa-arrow-up-right-from-square"></i></a>
            <button class="btn ghost" title="再読み込み" data-refresh="${idx}"><i class="fa-solid fa-rotate"></i></button>
            <button class="btn ghost danger" title="削除" data-remove="${idx}"><i class="fa-regular fa-trash-can"></i></button>
          </div>
        </div>
        <div class="body"><div class="embed" style="min-height:120px"></div></div>`;

      // DnD 並べ替え
      card.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', idx); card.style.opacity='0.6'; });
      card.addEventListener('dragend', ()=>{ card.style.opacity='1'; });
      card.addEventListener('dragover', e=>{ e.preventDefault(); card.style.outline='2px dashed var(--accent)'; });
      card.addEventListener('dragleave', ()=>{ card.style.outline='none'; });
      card.addEventListener('drop', e=>{
        e.preventDefault(); card.style.outline='none';
        const from = +e.dataTransfer.getData('text/plain');
        const to = +card.dataset.idx;
        if (from===to) return;
        const arr = board().sources;
        const [moved] = arr.splice(from,1);
        arr.splice(to,0,moved);
        store.save(state); renderAll();
      });

      // 操作
      card.querySelector('[data-remove]')?.addEventListener('click', ()=>{ board().sources.splice(idx,1); store.save(state); renderAll(); });
      card.querySelector('[data-refresh]')?.addEventListener('click', ()=>{ mountTimeline(card, src); });
      // 初回描画
      mountTimeline(card, src);
      return card;
    }

    function mountTimeline(card, src){
      const holder = card.querySelector('.embed');
      holder.innerHTML = '<div style="padding:14px" class="muted">読み込み中…</div>';
      createTimeline(holder, src).catch(()=>{
        holder.innerHTML = '<div style="padding:14px" class="danger">読み込みに失敗しました。値を確認してください。</div>';
      });
    }

    function prettyLabel(src){
      if (src.type==='profile') return '@'+src.value.replace(/^@/,'');
      if (src.type==='hashtag') return '#'+src.value.replace(/^#/,'');
      if (src.type==='search') return '検索: '+src.value;
      if (src.type==='list') return 'リスト';
      return src.value;
    }

    function renderAll(){
      // ボード情報
      const info = $('#boardInfo');
      const b = board();
      info.textContent = `(${state.activeBoard}|${b.sources.length}列 / ${b.tweets.length}件)`;

      // チップ
      const chips = $('#activeChips'); chips.innerHTML='';
      b.sources.forEach((s,i)=> chips.appendChild(chipNode(s,i)) );
      // カラム
      const col = $('#columns'); col.innerHTML='';
      b.sources.forEach((s,i)=> col.appendChild(columnCard(s,i)) );
      // ランキング
      renderRanking();
    }

    // ========== ランキング ==========
    function parseTweetId(url){ const m = (url||'').match(/status\/(\d{5,})/); return m? m[1] : null; }
    function tweetUrlFromId(id){ return `https://twitter.com/i/web/status/${id}`; }

    async function addTweet(url, note){
      const id = parseTweetId(url);
      if (!id) return alert('ツイートURLが正しくありません');
      board().tweets.push({id, note: (note||'')});
      store.save(state); renderRanking();
    }

    async function renderRanking(){
      await waitTwttr();
      const root = $('#ranking'); root.innerHTML='';
      const arr = board().tweets;
      arr.forEach((t, idx)=>{
        const card = document.createElement('div'); card.className='card'; card.draggable=true; card.dataset.idx=idx;
        card.innerHTML = `
          <div class="head">
            <div class="title"><i class="fa-solid fa-grip-vertical drag"></i> エントリ #${idx+1}</div>
            <div class="toolbar">
              <button class="btn ghost" data-edit="${idx}" title="メモ編集"><i class="fa-regular fa-pen-to-square"></i></button>
              <a class="btn ghost" href="${tweetUrlFromId(t.id)}" target="_blank" title="Xで開く"><i class="fa-solid fa-arrow-up-right-from-square"></i></a>
              <button class="btn ghost danger" title="削除" data-del-rank="${idx}"><i class="fa-regular fa-trash-can"></i></button>
            </div>
          </div>
          <div class="body">
            <div class="embed"></div>
            <div style="padding:10px 14px;border-top:1px solid var(--border)" class="tiny"><span class="muted">メモ:</span> <span class="note">${escapeHtml(t.note||'')}</span></div>
          </div>`;

        // イベント
        card.querySelector('[data-del-rank]')?.addEventListener('click', ()=>{ arr.splice(idx,1); store.save(state); renderRanking(); });
        card.querySelector('[data-edit]')?.addEventListener('click', ()=>{
          const newNote = prompt('メモを編集', t.note||'');
          if (newNote!==null){ t.note = newNote; store.save(state); renderRanking(); }
        });

        // DnD 並べ替え
        card.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', 'rank:'+idx); card.style.opacity='0.6'; });
        card.addEventListener('dragend', ()=>{ card.style.opacity='1'; });
        card.addEventListener('dragover', e=>{ e.preventDefault(); card.style.outline='2px dashed var(--accent)'; });
        card.addEventListener('dragleave', ()=>{ card.style.outline='none'; });
        card.addEventListener('drop', e=>{
          e.preventDefault(); card.style.outline='none';
          const data = e.dataTransfer.getData('text/plain'); if (!data.startsWith('rank:')) return;
          const from = +data.split(':')[1]; const to = +card.dataset.idx;
          const [moved] = arr.splice(from,1); arr.splice(to,0,moved);
          store.save(state); renderRanking();
        });

        const holder = card.querySelector('.embed');
        twttr.widgets.createTweet(t.id, holder, { theme: state.dark ? 'dark' : 'light' });
        root.appendChild(card);
      });
    }

    function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, m=> ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[m])); }

    // ========== 自動再描画 ==========
    let refreshTimer = null;
    function applyAutoRefresh(){
      if (refreshTimer) { clearInterval(refreshTimer); refreshTimer=null; }
      if (state.autoRefresh){
        const ms = Math.max(1, +state.minutes) * 60 * 1000;
        refreshTimer = setInterval(()=>{
          $$('#columns .card').forEach((card, i)=>{
            const src = board().sources[i]; if (src) mountTimeline(card, src);
          });
        }, ms);
      }
    }

    // ========== ボード管理 ==========
    function refreshBoardSelect(){
      const sel = $('#boardSelect'); sel.innerHTML='';
      Object.keys(state.boards).forEach(name=>{
        const opt = document.createElement('option'); opt.value=name; opt.textContent=name; sel.appendChild(opt);
      });
      sel.value = state.activeBoard;
    }

    function addBoard(name){
      if (!name) return;
      if (state.boards[name]) return alert('同名のボードが存在します');
      state.boards[name] = defaultBoard(); state.activeBoard = name; store.save(state);
      refreshBoardSelect(); renderAll();
    }

    function renameBoard(newName){
      if (!newName) return;
      if (state.boards[newName]) return alert('同名のボードが存在します');
      const old = state.activeBoard;
      state.boards[newName] = state.boards[old];
      delete state.boards[old];
      state.activeBoard = newName; store.save(state);
      refreshBoardSelect(); renderAll();
    }

    function deleteBoard(){
      const names = Object.keys(state.boards);
      if (names.length<=1) return alert('最後のボードは削除できません');
      if (!confirm(`ボード「${state.activeBoard}」を削除しますか?`)) return;
      delete state.boards[state.activeBoard];
      state.activeBoard = Object.keys(state.boards)[0];
      store.save(state); refreshBoardSelect(); renderAll();
    }

    // ========== 共有(URLハッシュ生成) ==========
    function exportHashUrl(){
      const cloned = JSON.parse(JSON.stringify(state));
      const json = JSON.stringify(cloned);
      const b64 = btoa(json);
      const url = location.origin + location.pathname + '#data=' + encodeURIComponent(b64);
      return url;
    }

    // ========== 単一HTML出力 ==========
    function download(filename, text){
      const blob = new Blob([text], {type:'text/html'});
      const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click(); URL.revokeObjectURL(a.href);
    }

    function exportSingleHtml(){
      // 現在のHTMLに初期状態スクリプトを差し込む
      let html = document.documentElement.outerHTML;
      const idx = html.indexOf('<head>');
      const inject = `<head>\n  <script>window.__XLOG_INITIAL_STATE__=${JSON.stringify(state)}<\/script>`;
      if (idx>=0){ html = html.replace('<head>', inject); }
      download('xlog-pro.html', '<!DOCTYPE html>\n' + html);
    }

    // ========== RSS生成 ==========
    function exportRss(){
      const title = $('#rssTitle').value.trim() || 'Xlog Ranking';
      const items = board().tweets.map(t=>({
        title: (t.note||'Tweet '+t.id).replace(/[\r\n]+/g,' ').slice(0,120),
        link: tweetUrlFromId(t.id),
        guid: t.id,
        description: escapeHtml(t.note||''),
        pubDate: new Date().toUTCString()
      }));
      const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0"><channel>\n<title>${escapeXml(title)}</title>\n<link>${escapeXml(location.href)}</link>\n<description>Generated by Xlog Pro</description>\n${items.map(i=>`<item><title>${escapeXml(i.title)}</title><link>${escapeXml(i.link)}</link><guid isPermaLink=\"false\">${escapeXml(i.guid)}</guid><description>${escapeXml(i.description)}</description><pubDate>${i.pubDate}</pubDate></item>`).join('')}\n</channel></rss>`;
      const blob = new Blob([xml], {type:'application/rss+xml'});
      const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='xlog-ranking.xml'; a.click(); URL.revokeObjectURL(a.href);
    }

    function escapeXml(s){ return (s||'').replace(/[<>&\"']/g, m=> ({'<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;','\'':'&apos;'}[m])); }

    // ========== 入力ヘルパ ==========
    function detectType(v){
      if (/^@/.test(v)) return 'profile';
      if (/^#/.test(v)) return 'hashtag';
      if (/twitter\.com\/i\/lists\//.test(v)) return 'list';
      return 'search';
    }

    function addSource(type, value, label){
      const src = {type, value:value.trim(), label:(label||'').trim()};
      board().sources.push(src); store.save(state); renderAll();
    }

    function bulkAddFromText(txt){
      const parts = txt.split(/[\n,]+/).map(s=>s.trim()).filter(Boolean);
      let count = 0;
      for (const p of parts){ addSource(detectType(p), p, ''); count++; }
      return count;
    }

    // ========== キーイベント ==========
    function setupShortcuts(){
      window.addEventListener('keydown', (e)=>{
        if (['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) return;
        if (e.key==='n' || e.key==='N'){ $('#sourceValue').focus(); }
        if (e.key==='r' || e.key==='R'){ renderAll(); }
        if (e.key==='g' || e.key==='G'){ state.columns=Math.max(1,state.columns-1); applySkin(); store.save(state); }
        if (e.key==='h' || e.key==='H'){ state.cardHeight=Math.max(360,state.cardHeight-40); applySkin(); store.save(state); renderAll(); }
      });
    }

    // ========== 設定と起動 ==========
    window.addEventListener('DOMContentLoaded', ()=>{
      // スキン
      applySkin();

      // ボード選択
      refreshBoardSelect();
      $('#boardSelect').addEventListener('change', (e)=>{ state.activeBoard = e.target.value; store.save(state); renderAll(); });
      $('#boardNew').addEventListener('click', ()=>{ const name = prompt('新しいボード名','Board '+(Object.keys(state.boards).length+1)); addBoard(name); });
      $('#boardRename').addEventListener('click', ()=>{ const name = prompt('新しい名前', state.activeBoard); if (name) renameBoard(name); });
      $('#boardDelete').addEventListener('click', deleteBoard);

      // テーマ/アクセント/レイアウト
      $('#themeToggle').addEventListener('change', e=>{ state.dark = e.target.checked; store.save(state); applySkin(); renderAll(); });
      $('#accentPicker').addEventListener('input', e=>{ state.accent = e.target.value; store.save(state); applySkin(); });
      $('#colsRange').addEventListener('input', e=>{ state.columns = +e.target.value; store.save(state); applySkin(); });
      $('#heightRange').addEventListener('input', e=>{ state.cardHeight = +e.target.value; store.save(state); applySkin(); renderAll(); });

      // ソース追加
      $('#addBtn').addEventListener('click', ()=>{
        const type = $('#sourceType').value;
        const val = $('#sourceValue').value.trim();
        const label = $('#sourceLabel').value.trim();
        if (!val) return alert('値を入力してください');
        addSource(type, val, label);
        $('#sourceValue').value=''; $('#sourceLabel').value='';
      });
      $$('.quick').forEach(btn=> btn.addEventListener('click', ()=> addSource(btn.dataset.type, btn.dataset.val, '')) );

      // まとめて追加
      $('#bulkAdd').addEventListener('click', ()=>{ const n = bulkAddFromText($('#bulkArea').value); alert(n+'件追加しました'); $('#bulkArea').value=''; });
      $('#bulkClear').addEventListener('click', ()=> $('#bulkArea').value='' );

      // ランキング
      $('#addTweetBtn').addEventListener('click', ()=>{
        const url = $('#tweetUrl').value.trim();
        const note = $('#tweetNote').value.trim();
        if (!url) return;
        addTweet(url, note); $('#tweetUrl').value=''; $('#tweetNote').value='';
      });

      // 設定
      $('#autoRefreshToggle').checked = !!state.autoRefresh;
      $('#refreshMinutes').value = String(state.minutes||5);
      $('#autoRefreshToggle').addEventListener('change', e=>{ state.autoRefresh = e.target.checked; store.save(state); applyAutoRefresh(); });
      $('#refreshMinutes').addEventListener('change', e=>{ state.minutes = +e.target.value; store.save(state); applyAutoRefresh(); });

      // 書き出し/読み込み
      $('#exportBtn').addEventListener('click', ()=>{
        const blob = new Blob([JSON.stringify(state,null,2)], {type:'application/json'});
        const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='xlog-pro-config.json'; a.click(); URL.revokeObjectURL(a.href);
      });
      $('#importFile').addEventListener('change', e=>{
        const file = e.target.files?.[0]; if (!file) return;
        const fr = new FileReader();
        fr.onload = () => {
          try{ const obj = JSON.parse(fr.result); state = migrate(obj); store.save(state); applySkin(); refreshBoardSelect(); renderAll(); applyAutoRefresh(); }
          catch(err){ alert('JSONの読み込みに失敗しました'); }
        };
        fr.readAsText(file);
      });
      $('#exportHtmlBtn').addEventListener('click', exportSingleHtml);
      $('#clearBtn').addEventListener('click', ()=>{
        if (!confirm('現在のボードのソースとランキングを削除しますか?')) return;
        const b = board(); b.sources = []; b.tweets=[]; store.save(state); renderAll();
      });

      // RSS
      $('#rssExport').addEventListener('click', exportRss);

      // 初期描画
      renderAll();
      applyAutoRefresh();
      setupShortcuts();
    });
  </script>
</body>
</html>

BranchBoard.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>GBranchBoard</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- Bootstrap & FontAwesome -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
  <style>
    body {
      background: #f6f8fa;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    }
    .repo-header { background: #fff; padding: 20px; border: 1px solid #ddd; margin-bottom: 10px; }
    .nav-tabs .nav-link.active { border-color: #ddd #ddd #fff; background: #fff; }
    .file-list li { border-bottom: 1px solid #eee; padding: 8px 0; display: flex; align-items: center; }
    .file-list i { margin-right: 10px; }
    .readme-box, .issues-box, .commits-box { background: #fff; padding: 20px; border: 1px solid #ddd; margin-top: 10px; }
    #editor { height: 400px; width: 100%; border: 1px solid #ccc; }
  </style>
</head>
<body>

  <!-- ヘッダー -->
  <nav class="navbar navbar-dark bg-dark px-3">
    <span class="text-white"><i class="fas fa-user-circle"></i> owner</span>
  </nav>

  <div class="container mt-3">
    <!-- リポジトリヘッダー -->
    <div class="repo-header">
      <h3><i class="fas fa-book"></i> owner / <strong>SampleRepo</strong></h3>
      <p class="text-muted">最終更新: 2025年5月26日</p>
      <button class="btn btn-sm btn-outline-secondary"><i class="fas fa-star"></i> Star</button>
      <button class="btn btn-sm btn-outline-secondary"><i class="fas fa-code-branch"></i> Fork</button>
    </div>

    <!-- タブ -->
    <ul class="nav nav-tabs" id="repoTabs">
      <li class="nav-item"><a class="nav-link active" href="#" onclick="switchTab('code')"><i class="fas fa-code"></i> Code</a></li>
      <li class="nav-item"><a class="nav-link" href="#" onclick="switchTab('issues')"><i class="fas fa-exclamation-circle"></i> Issues</a></li>
      <li class="nav-item"><a class="nav-link" href="#" onclick="switchTab('commits')"><i class="fas fa-history"></i> Commits</a></li>
    </ul>

    <!-- Codeタブ -->
    <div id="tab-code" class="tab-content">
      <ul class="file-list list-unstyled bg-white p-3 border">
        <li><i class="fas fa-folder"></i> src/</li>
        <li><i class="fas fa-file-code"></i> index.js</li>
        <li><i class="fas fa-file-alt"></i> README.md</li>
        <li><i class="fas fa-file-alt"></i> LICENSE</li>
      </ul>

      <div class="readme-box">
        <h4><i class="fas fa-book-open"></i> README.md</h4>
        <hr>
        <div id="readme-content"></div>
      </div>

      <div class="mt-4">
        <h5><i class="fas fa-code"></i> コード編集 (Monaco Editor)</h5>
        <div id="editor"></div>
        <button id="saveCode" class="btn btn-success mt-2"><i class="fas fa-save"></i> 保存</button>
        <button id="themeToggle" class="btn btn-secondary mt-2"><i class="fas fa-adjust"></i> テーマ切替</button>
      </div>
    </div>

    <!-- Issuesタブ -->
    <div id="tab-issues" class="tab-content" style="display:none;">
      <div class="issues-box">
        <h4><i class="fas fa-exclamation-circle"></i> Open Issues</h4>
        <ul class="list-group">
          <li class="list-group-item">
            <strong>#1</strong> READMEの翻訳が必要です<br>
            <small class="text-muted">posted by owner - 1日前</small>
          </li>
          <li class="list-group-item">
            <strong>#2</strong> index.jsにテストコードを追加してください<br>
            <small class="text-muted">posted by owner - 2日前</small>
          </li>
        </ul>
      </div>
    </div>

    <!-- Commitsタブ -->
    <div id="tab-commits" class="tab-content" style="display:none;">
      <div class="commits-box">
        <h4><i class="fas fa-history"></i> Commits</h4>
        <ul class="list-group">
          <li class="list-group-item">
            <strong>Initial commit</strong> - 2025-05-25<br>
            <small class="text-muted">by owner</small>
          </li>
          <li class="list-group-item">
            <strong>README updated</strong> - 2025-05-26<br>
            <small class="text-muted">by owner</small>
          </li>
        </ul>
      </div>
    </div>
  </div>

  <!-- ライブラリ -->
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>

  <!-- タブ切替・Markdown表示・Monaco起動 -->
  <script>
    const markdown = `
# SampleRepo

## 特徴
- Monaco Editorでコード編集
- Markdown表示(README)
- ダークモード対応
- Issue・コミットのUI

## 技術
- HTML/CSS/JS
- Bootstrap5
- Monaco Editor
- Marked.js
    `;
    document.getElementById('readme-content').innerHTML = marked.parse(markdown);

    function switchTab(tab) {
      ['code', 'issues', 'commits'].forEach(id => {
        document.getElementById('tab-' + id).style.display = (id === tab) ? 'block' : 'none';
      });
      document.querySelectorAll('#repoTabs .nav-link').forEach(link => link.classList.remove('active'));
      document.querySelector(`#repoTabs .nav-link[onclick*="${tab}"]`).classList.add('active');
    }

    // Monaco起動
    require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
    require(['vs/editor/editor.main'], function () {
      window.editor = monaco.editor.create(document.getElementById('editor'), {
        value: localStorage.getItem('savedCode') || `function hello() {\n  console.log("Hello from Monaco Editor!");\n}`,
        language: 'javascript',
        theme: 'vs-light',
        automaticLayout: true
      });

      document.getElementById('saveCode').onclick = function () {
        const code = editor.getValue();
        localStorage.setItem('savedCode', code);
        alert('保存しました!');
      };

      document.getElementById('themeToggle').onclick = function () {
        const theme = monaco.editor.getTheme() === 'vs-dark' ? 'vs-light' : 'vs-dark';
        monaco.editor.setTheme(theme);
      };
    });
  </script>
</body>
</html>

Tsumugi.html(SNS)

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

  <!-- Favicon -->
  <link rel="shortcut icon"
        href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23667eea' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/></svg>"/>

  <!-- 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"/>

  <style>
    :root {
      --grad-a: #667eea;
      --grad-b: #764ba2;
    }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: linear-gradient(135deg, var(--grad-a) 0%, var(--grad-b) 100%);
      min-height: 100vh;
    }
    .glass-effect {
      background: rgba(255,255,255,0.25);
      backdrop-filter: blur(10px);
      border-radius: 16px;
      border: 1px solid rgba(255,255,255,0.18);
    }
    .card-hover { transition: all 0.25s ease; }
    .card-hover:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.25); }

    .gradient-text {
      background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
      -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
    }

    .timeline-post {
      background: white;
      border-radius: 16px;
      box-shadow: 0 10px 20px -12px rgba(0,0,0,0.2);
      transition: all 0.25s ease;
      border-left: 4px solid var(--grad-a);
    }
    .timeline-post:hover { transform: translateX(4px); }

    .profile-avatar {
      width: 100px; height: 100px; border-radius: 50%;
      object-fit: cover; border: 4px solid white;
      box-shadow: 0 10px 24px rgba(0,0,0,0.15);
    }
    .btn-primary {
      background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
      border: none; transition: all 0.2s ease;
    }
    .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(0,0,0,0.18); }

    .section-divider {
      height: 3px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
      border-radius: 2px; margin: 2rem 0;
    }
    .username-badge {
      background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
      color: white; padding: 0.2rem 0.5rem; border-radius: 1rem;
      font-size: 0.75rem; font-weight: 600; display: inline-block; margin-left: 0.5rem;
    }
    .share-menu { position: absolute; z-index: 50; min-width: 180px; right: 0; top: 110%; background: white; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.13); }
    .share-menu button { width: 100%; text-align: left; padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 0.95rem; }
    .share-menu button:hover { background: #f0f4ff; }

    .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
    .status-active { background-color: #10b981; animation: pulse 2s infinite; }
    .status-inactive { background-color: #6b7280; }

    .log-container {
      max-height: 180px; overflow-y: auto; background: rgba(255,255,255,0.12);
      border-radius: 10px; padding: 10px; margin-top: 10px; font-size: 0.8rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    }
    .error-message { color: #ef4444; background: rgba(239, 68, 68, 0.08); padding: 8px; border-radius: 8px; margin: 5px 0; }
    .success-message { color: #10b981; background: rgba(16, 185, 129, 0.08); padding: 8px; border-radius: 8px; margin: 5px 0; }

    @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }

    .dark .glass-effect { background: rgba(0,0,0,0.30); border: 1px solid rgba(255,255,255,0.12); }
    .dark .timeline-post { background: #1f2937; color: #f9fafb; border-left-color: #4f46e5; }
    .dark .share-menu { background: #111827; color: #e5e7eb; }
    .dark .success-message { background: rgba(16,185,129,0.12); }
    .dark .error-message { background: rgba(239,68,68,0.12); }

    @media print {
      body { background: white !important; -webkit-print-color-adjust: exact; }
      .glass-effect { background: white !important; backdrop-filter: none !important; border: 1px solid #e5e7eb !important; }
    }
  </style>
</head>

<body>
  <!-- ログイン/登録モーダル -->
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" style="display:none">
    <div class="bg-white rounded-2xl shadow-2xl w-full max-w-sm p-6">
      <h2 class="text-2xl font-bold mb-4 text-center" id="auth-title">ログイン</h2>
      <div id="auth-error" class="error-message mb-2" style="display:none"></div>
      <form id="auth-form" autocomplete="off">
        <div class="mb-3">
          <label class="block mb-1 text-sm font-semibold">メールアドレス</label>
          <input type="email" id="auth-email" class="w-full border rounded px-3 py-2" required>
        </div>
        <div class="mb-3">
          <label class="block mb-1 text-sm font-semibold">パスワード</label>
          <input type="password" id="auth-password" class="w-full border rounded px-3 py-2" required>
        </div>
        <button type="submit" class="btn-primary w-full py-2 rounded-lg text-white font-semibold">ログイン</button>
      </form>
      <div class="mt-4 text-center">
        <button id="toggle-auth-mode" class="text-blue-600 underline text-sm">新規登録はこちら</button>
      </div>
    </div>
  </div>

  <!-- ヘッダー -->
  <header class="glass-effect mx-4 mt-4 p-6">
    <div class="text-center">
      <h1 class="text-4xl font-bold text-white mb-2">
        <i class="fas fa-comments mr-3"></i>Tsumugi
        <span class="text-sm opacity-80 ml-2"></span>
      </h1>
      <p class="text-white text-lg opacity-90">次世代ソーシャルネットワーク</p>
      <div class="mt-4 flex justify-center items-center space-x-4">
        <div class="flex items-center">
          <img id="header-profile-icon" class="profile-avatar" src="https://via.placeholder.com/100" alt="プロフィール">
          <div class="ml-3 text-white text-left">
            <div class="font-semibold text-lg" id="header-username">未設定</div>
            <div class="text-sm opacity-75" id="header-user-email"></div>
          </div>
        </div>
        <button onclick="toggleDarkMode()" class="btn-primary px-4 py-2 rounded-full text-white">
          <i class="fas fa-moon mr-2"></i>ダークモード
        </button>
        <button onclick="showSystemStatus()" class="btn-primary px-4 py-2 rounded-full text-white">
          <i class="fas fa-info-circle mr-2"></i>ステータス
        </button>
        <button id="logout-btn" onclick="logout()" class="btn-primary px-4 py-2 rounded-full text-white" style="display:none">
          <i class="fas fa-sign-out-alt mr-2"></i>ログアウト
        </button>
      </div>
    </div>
  </header>

  <!-- メイン -->
  <div class="max-w-6xl mx-auto px-4 py-6" id="main-content" style="display:none">
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
      <!-- 左カラム:プロフィール/BOT/RSS -->
      <div class="lg:col-span-1 space-y-6">
        <!-- プロフィール -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-2xl font-bold gradient-text mb-4"><i class="fas fa-user-circle mr-2"></i>プロフィール</h3>
          <div class="text-center mb-6">
            <img id="profile-icon" class="profile-avatar mx-auto mb-4" src="https://via.placeholder.com/100" alt="プロフィール">
            <input type="file" id="profile-upload" accept="image/*" onchange="uploadProfileIcon(event)" class="hidden">
            <button onclick="document.getElementById('profile-upload').click()" class="btn-primary px-4 py-2 rounded-full text-white">
              <i class="fas fa-camera mr-2"></i>画像変更
            </button>
          </div>
          <div class="space-y-4">
            <div>
              <label class="block text-white font-semibold mb-2">ユーザー名</label>
              <input type="text" id="username" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" placeholder="ユーザー名を入力してください" maxlength="20">
            </div>
            <div>
              <label class="block text-white font-semibold mb-2">自己紹介</label>
              <textarea id="self-intro" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" rows="4" placeholder="自己紹介を入力してください"></textarea>
            </div>
            <button onclick="saveProfile()" class="btn-primary w-full py-2 rounded-lg text-white">
              <i class="fas fa-save mr-2"></i>プロフィール保存
            </button>
            <div class="p-3 bg-white bg-opacity-80 rounded-lg">
              <h5 class="font-semibold text-gray-800 mb-2">プレビュー:</h5>
              <div class="text-gray-700">
                <div class="font-semibold mb-1" id="username-preview">未設定</div>
                <div id="self-intro-preview" class="text-sm whitespace-pre-line min-h-8">まだ自己紹介がありません</div>
              </div>
            </div>
          </div>
        </div>

        <!-- RSS自動投稿機能(全体共有 + 個別ON/OFF) -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-xl font-bold text-white mb-4">
            <i class="fas fa-rss mr-2"></i>RSS自動投稿(全体共有)
            <span class="status-indicator" id="rss-status"></span>
            <span id="rss-status-text" class="text-xs opacity-75">停止中</span>
          </h3>
          <div>
            <input id="rss-url" type="text" class="w-full p-2 border rounded-lg bg-white bg-opacity-90 mb-2" placeholder="RSSフィードURLを入力">
            <button onclick="addRssFeed()" class="btn-primary w-full py-2 rounded-lg text-white mb-2">
              <i class="fas fa-plus mr-2"></i>追加
            </button>

            <div id="rss-list" class="mb-3"></div>

            <div class="flex items-center space-x-2 mb-2">
              <input type="number" id="rss-interval" class="w-1/2 p-2 border rounded-lg bg-white bg-opacity-90" min="10" max="3600" value="300" placeholder="間隔(秒)">
              <button onclick="setRssInterval()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                <i class="fas fa-clock mr-2"></i>間隔設定
              </button>
            </div>

            <div class="flex items-center space-x-2 mb-2">
              <button onclick="fetchRssNow()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                <i class="fas fa-sync mr-2"></i>今すぐ取得
              </button>
              <button onclick="stopRssAuto()" class="bg-red-500 hover:bg-red-600 flex-1 py-2 rounded-lg text-white">
                <i class="fas fa-stop mr-2"></i>自動停止
              </button>
            </div>

            <div class="flex items-center space-x-2">
              <button onclick="setAllRssEnabled(true)" class="btn-primary flex-1 py-2 rounded-lg text-white text-sm">
                <i class="fas fa-toggle-on mr-1"></i>すべてON
              </button>
              <button onclick="setAllRssEnabled(false)" class="bg-gray-500 hover:bg-gray-600 flex-1 py-2 rounded-lg text-white text-sm">
                <i class="fas fa-toggle-off mr-1"></i>すべてOFF
              </button>
            </div>

            <div id="rss-log" class="log-container text-white text-xs mt-3"></div>
          </div>
        </div>

        <!-- BOT機能 -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-xl font-bold text-white mb-4">
            <i class="fas fa-robot mr-2"></i>BOT機能
            <span class="status-indicator" id="bot-status"></span>
            <span id="bot-status-text" class="text-xs opacity-75">停止中</span>
          </h3>
          <div class="space-y-4">
            <div>
              <textarea id="botContent" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" rows="3" placeholder="BOT投稿内容"></textarea>
              <button onclick="postBotMessage()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white">
                <i class="fas fa-robot mr-2"></i>BOT投稿
              </button>
            </div>
            <div>
              <input type="number" id="botIntervalSec" class="w-full p-2 border rounded-lg bg-white bg-opacity-90" placeholder="マルコフ自動投稿間隔(秒)" min="10" max="3600" value="60">
              <div class="flex space-x-2 mt-2">
                <button onclick="postMarkovBot()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                  <i class="fas fa-dice mr-2"></i>マルコフ生成
                </button>
                <button onclick="startBotAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                  <i class="fas fa-play mr-2"></i>自動開始
                </button>
                <button onclick="stopBotAutoPost()" class="bg-red-500 hover:bg-red-600 flex-1 py-2 rounded-lg text-white">
                  <i class="fas fa-stop mr-2"></i>停止
                </button>
              </div>
            </div>
            <div class="text-white text-xs opacity-75">
              <i class="fas fa-info-circle mr-1"></i>
              マルコフ連鎖では過去の投稿からランダムな文章を生成します(FEED本文は省略し、タイトルのみを学習対象にしません)。
            </div>
            <div id="bot-log" class="log-container text-white text-xs"></div>
          </div>
        </div>
      </div>

      <!-- 右カラム:投稿&タイムライン -->
      <div class="lg:col-span-2 space-y-6">
        <!-- 新規投稿 -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-2xl font-bold gradient-text mb-4"><i class="fas fa-edit mr-2"></i>新規投稿</h3>
          <div>
            <textarea id="postContent" class="w-full p-4 border rounded-lg bg-white bg-opacity-90" rows="4" placeholder="今何を考えていますか?" maxlength="500"></textarea>
            <div class="mt-4 flex justify-between items-center">
              <div class="text-white text-sm opacity-90">
                <i class="fas fa-info-circle mr-1"></i>
                あなたの思いを共有しましょう
                <span id="char-count" class="ml-2">(0/500)</span>
              </div>
              <button onclick="createUserPost()" class="btn-primary px-6 py-3 rounded-lg text-white font-semibold">
                <i class="fas fa-paper-plane mr-2"></i>投稿する
              </button>
            </div>
          </div>
        </div>

        <div class="section-divider"></div>

        <!-- タイムライン -->
        <div class="glass-effect p-6">
          <div class="flex justify-between items-center mb-6">
            <h3 class="text-2xl font-bold text-white">
              <i class="fas fa-stream mr-2"></i>タイムライン
              <span id="post-count" class="text-sm font-normal opacity-75 ml-2">(0件の投稿)</span>
            </h3>
            <div class="flex space-x-2">
              <button onclick="clearAllPosts()" class="bg-red-500 hover:bg-red-600 px-3 py-1 rounded text-white text-sm">
                <i class="fas fa-trash mr-1"></i>全削除
              </button>
              <button onclick="exportData()" class="bg-green-500 hover:bg-green-600 px-3 py-1 rounded text-white text-sm">
                <i class="fas fa-download mr-1"></i>エクスポート
              </button>
            </div>
          </div>
          <div id="timeline" class="space-y-4"></div>
          <div id="empty-timeline" class="text-center py-12 text-white opacity-80">
            <i class="fas fa-comments text-4xl mb-4"></i>
            <p class="text-lg">まだ投稿がありません</p>
            <p class="text-sm">最初の投稿をして、タイムラインを始めましょう!</p>
          </div>
        </div>
      </div>
    </div>
  </div>

  <footer class="glass-effect mx-4 mb-4 p-4 text-center">
    <p class="text-white opacity-80">
      <i class="fas fa-copyright mr-2"></i>2025 Verse – 次世代ソーシャルネットワーク v2.3
      <span class="ml-4"><i class="fas fa-rss mr-1"></i>共有RSS / 個別ON/OFF対応(安全なフィード例)</span>
    </p>
  </footer>

  <script>
    // ==== 初期RSS(安全な例のみ。成人向け/不適切なサイトは除外) ====
    const PRESET_RSS = [
               "http://2ch-2.net/rss/all.xml",
    "http://2ch-ranking.net/rss/livemarket1.rdf",
    "http://2ch-ranking.net/rss/livemarket2.rdf",
    "http://kabumatome.doorblog.jp/index.rdf",
    "http://momoniji.com/feed",
    "http://oekakigakusyuu.blog97.fc2.com/?xml",
    "http://otanews.livedoor.biz/atom.xml",
    "http://otanews.livedoor.biz/index.rdf",
    "http://news4vip.livedoor.biz/index.rdf",
    "http://news.kakaku.com/prdnews/rss.asp",
    "http://www.jma-net.go.jp/rss/jma.rss",
    "http://rss.asahi.com/rss/asahi/newsheadlines.rdf",
    "https://uploadvr.com/feed/",
    "http://www.atmarkit.co.jp/rss/rss2dc.xml",
    "http://liginc.co.jp/feed",
    "http://liginc.co.jp/feed/",
    "http://blog.livedoor.jp/shachiani/index.rdf",
    "http://manga.lemon-s.com/atom.xml",
    "http://b.hatena.ne.jp/search/text?safe=on&q=%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3&users=500&mode=rss",
    "http://creator-life.net/feed/",
    "http://feedblog.ameba.jp/rss/ameblo/ca-1pixel/rss20.xml",
    "http://rssblog.ameba.jp/ca-1pixel/rss20.xml",
    "http://weekly.ascii.jp/cate/1/rss.xml",
    "http://blog.livedoor.jp/coleblog/atom.xml",
    "http://2chantena.antenam.biz/rss1s.rss",
    "http://www.4gamer.net/rss/index.xml",
    "http://www.4gamer.net/rss/news_topics.xml",
    "http://nyan.eggtree.net/feed.xml",
    "http://nunnnunn.hatenablog.com/rss",
    "http://www.nikkansports.com/general/atom.xml",
    "http://feeds.afpbb.com/rss/afpbb/access_ranking",
    "http://akiba-pc.watch.impress.co.jp/cda/rss/akiba-pc.rdf",
    "https://area.autodesk.jp/rss.xml",
    "http://av.watch.impress.co.jp/sublink/av.rdf",
    "http://rss.allabout.co.jp/aa/latest/ch/netpc/",
    "http://www.ar-ch.org/atom.xml",
    "http://feeds.arstechnica.com/arstechnica/BAaf",
    "https://feeds.feedburner.com/awwwards-sites-of-the-day",
    "http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss091.xml",
    "http://www.criteo.com/blog/rss/",
    "https://blueskyweb.xyz/rss.xml",
    "http://boingboing.net/rss.xml",
    "http://www.cc2.co.jp/blog/?feed=rss2",
    "http://cgarena.com/cgarena.xml",
    "http://cgtracking.net/feed",
    "http://japan.cnet.com/rss/index.rdf",
    "http://newclassic.jp/feed",
    "https://www.cssmania.com/feed/",
    "http://ceron.jp/top/?type=rss",
    "http://blog.btrax.com/jp/comments/feed/",
    "http://2ch-ranking.net/rss/wildplus.rdf",
    "http://design-spice.com/feed/",
    "http://dev.classmethod.jp/feed/",
    "http://ishida-a-coicoi.blog.so-net.ne.jp/atom.xml",
    "http://feeds.feedburner.com/ura-akiba?format=xml",
    "http://game.watch.impress.co.jp/sublink/game.rdf",
    "http://gigazine.co.jp/feed/",
    "http://labs.gree.jp/blog/comments/feed/",
    "http://www.gamespark.jp/rss/index.rdf",
    "http://www.gamebusiness.jp/rss/index.rdf",
    "http://www.gamebusiness.jp/rss/rss.php",
    "http://hackread.com/feed/",
    "https://io3000.com/feed/",
    "http://www.inside-games.jp/rss/index.rdf",
    "http://blog.livedoor.jp/itsoku/index.rdf",
    "http://rss.itmedia.co.jp/rss/1.0/news_bursts.xml",
    "http://octoba.net/feed",
    "http://www.ota-suke.jp/index.xml",
    "http://blog.livedoor.jp/kaigai_no/index.rdf",
    "https://land-book.com/feed/",
    "http://2ch.logpo.jp/1hour.xml",
    "http://menthas.com/javascript/rss",
    "http://www.nhk.or.jp/rss/news/cat0.xml",
    "http://ozpa-h4.com/feed/",
    "https://www.youtube.com/feeds/videos.xml?channel_id=UC1DCedRgGHBdm81E1llLhOQ",
    "http://rass.blog43.fc2.com/?xml",
    "http://stackoverflow.com/feeds",
    "http://www.slideshare.net/rss/latest",
    "http://www.jp.square-enix.com/whatsnew2/whatsnew.rdf",
    "http://www.ituore.com/feed",
    "http://synodos.jp/comments/feed",
    "http://www.shinkigensha.co.jp/feed/",
    "http://e-shuushuu.net/index.rss",
    "http://slashdot.org/index.rss",
    "http://feeds.feedburner.com/TheHackersNews?format=xml",
    "http://googleblog.blogspot.com/atom.xml",
    "http://www.theregister.co.uk/tonys/slashdot.rdf",
    "http://thinkit.co.jp/rss.xml",
    "http://blog.livedoor.jp/news23vip/atom.xml",
    "http://blog.livedoor.jp/news23vip/index.rdf",
    "http://www.webcreatorbox.com/feed/",
    "http://web-d.navigater.info/atom.xml",
    "http://2ch-c.net/?xml_all",
    "http://smhn.info/feed",
    "http://feeds.japan.zdnet.com/rss/zdnet/all.rdf",
    "http://20kaido.com/index.rdf",
    "http://2chnode.com/rss/feed/all",
    "http://akiba-souken.com/feed/all/",
    "http://amaebi.net/index.rdf",
    "http://amakakeru.blog59.fc2.com/?xml",
    "http://artskype.com/rss/feed.xml",
    "http://asitagamienai.blog118.fc2.com/?xml",
    "http://beta.egmnow.com/feed/",
    "http://blog.livedoor.jp/ogenre/index.rdf",
    "http://blog.nicovideo.jp/atom.xml",
    "http://blog.tsubuani.com/feed",
    "http://blogs.adobe.com/flex/atom.xml",
    "http://blogs.adobe.com/index.xml",
    "http://bm.s5-style.com/feed",
    "http://business.nikkeibp.co.jp/rss/all_nbo.rdf",
    "http://createlier.sitemix.jp/feed/",
    "http://crocro.com/news/nc.cgi?action=search&skin=rdf_srch_xml",
    "http://d.hatena.ne.jp/thk/rss",
    "http://damage0.blomaga.jp/index.rdf",
    "http://danbooru.donmai.us/posts.atom",
    "http://danbooru.donmai.us/posts.atom?tags=rss",
    "http://dengekionline.com/cate/11/rss.xml",
    "http://dictionary.reference.com/wordoftheday/wotd.rss",
    "http://doujin-games88.net/feed",
    "http://doujin.sekurosu.com/rss",
    "http://dousyoko.blog.fc2.com/?xml",
    "http://eroaniblog.blog.fc2.com/?xml",
    "http://eroanimedougakan.blog.fc2.com/?xml",
    "http://erogetrailers.com/api?md=latest",
    "http://eronizimage.blog.fc2.com/?xml",
    "http://erosanime.blog121.fc2.com/?xml",
    "http://erotaganime.blog.fc2.com/?xml",
    "http://feed.nikkeibp.co.jp/rss/nikkeibp/index.rdf",
    "http://feed.rssad.jp/rss/gigazine/rss_2.0",
    "http://feed.rssad.jp/rss/jcast/index.xml",
    "http://feed.rssad.jp/rss/klug/fxnews/rss5.xml",
    "http://feedblog.ameba.jp/rss/ameblo/yusayusa0211/rss20.xml",
    "http://feeds.adobe.com/xml/rss.cfm?query=byMostRecent&languages=1",
    "http://feeds.builder.japan.zdnet.com/rss/builder/all.rdf",
    "http://feeds.fc2.com/fc2/xml?host=anrism.blog&format=xml",
    "http://feeds.fc2.com/fc2/xml?host=kahouha2jigen.blog&format=xml",
    "http://feeds.feedburner.com/gekiura",
    "http://feeds.journal.mycom.co.jp/rss/mycom/index",
    "http://feeds.reuters.com/reuters/JPTopNews?format=xml",
    "http://galten705.blog.fc2.com/?xml",
    "http://gamanjiru.net/feed",
    "http://gamanjiru.net/feed/atom",
    "http://gamebiz.jp/?feed=rss",
    "http://gamenode.jp/rss/feed/all",
    "http://ggsoku.com/feed/atom/",
    "http://girlcelly.blog.fc2.com/?xml&trackback",
    "http://hairana.blog.fc2.com/?xml",
    "http://haruka-yumenoato.net/static/rss/index.rss",
    "http://headline.harikonotora.net/rss2.xml",
    "http://hentaidoujinanime.com/?xml",
    "http://homepage1.nifty.com/maname/index.rdf",
    "http://horiemon.com/feed/",
    "http://ideahacker.net/feed/",
    "http://itpro.nikkeibp.co.jp/rss/develop.rdf",
    "http://itpro.nikkeibp.co.jp/rss/news.rss",
    "http://itpro.nikkeibp.co.jp/rss/oss.rdf",
    "http://itpro.nikkeibp.co.jp/rss/win.rdf",
    "http://japan.internet.com/rss/rdf/index.rdf",
    "http://jp.leopard-raws.org/rss.php",
    "http://jp.techcrunch.com/feed/",
    "http://kakaku.com/trendnews/rss.xml",
    "http://kamisoku.blog47.fc2.com/?xml",
    "http://kanesoku.com/index.rdf",
    "http://kibougamotenai.blog.fc2.com/?xml",
    "http://kiisu.jpn.org/rss/now.xml",
    "http://konachan.com/post/piclens?page=1&tags=loli",
    "http://labo.tv/2chnews/index.xml",
    "http://lineblog.me/yamamotoichiro/atom.xml",
    "http://majimougen.blog.fc2.com/?xml",
    "http://mantan-web.jp/rss/mantan.xml",
    "http://matome.naver.jp/feed/hot",
    "http://matome.naver.jp/feed/tech",
    "http://matome.sekurosu.com/rss",
    "http://mizuhonokuni2ch.com/?xml",
    "http://momoiroanime.blog.fc2.com/?xml",
    "http://moroahedoujin.com/?xml",
    "http://nesingazou.blog.fc2.com/?xml",
    "http://newnews-moe.com/index.rdf",
    "http://news.ameba.jp/index.xml",
    "http://news.com.com/2547-1_3-0-5.xml",
    "http://news.nicovideo.jp/?rss=2.0",
    "http://news.nicovideo.jp/ranking/hot?rss=2.0",
    "http://newsbiz.yahoo.co.jp/topnews.rss",
    "http://nijitora.blog.fc2.com/?xml",
    "http://nodvd21ver2.blog.fc2.com/?xml",
    "http://orebibou.com/feed/",
    "http://osu.ppy.sh/feed/ranked/",
    "http://otakomu.jp/feed",
    "http://pcgameconquest.blog.fc2.com/?xml",
    "http://picks.dir.yahoo.co.jp/dailypicks/rss/",
    "http://piknik2ch.blog76.fc2.com/?xml",
    "http://plus.appgiga.jp/feed/user",
    "http://purisoku.com/index.rdf",
    "http://rdsig.yahoo.co.jp/RV=1/RU=aHR0cDovL3NlYXJjaHJhbmtpbmcueWFob28uY28uanAvcnNzL2J1cnN0X3JhbmtpbmctcnNzLnhtbA--;_ylt=A2RhjFhfAi9XEi0A6Glhdu57",
    "http://read2ch.net/rss/",
    "http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml",
    "http://rss.rssad.jp/rss/akibapc/akiba-pc.rdf",
    "http://rss.rssad.jp/rss/ascii/biz/rss.xml",
    "http://rss.rssad.jp/rss/ascii/hobby/rss.xml",
    "http://rss.rssad.jp/rss/ascii/it/rss.xml",
    "http://rss.rssad.jp/rss/ascii/mac/rss.xml",
    "http://rss.rssad.jp/rss/ascii/pc/rss.xml",
    "http://rss.rssad.jp/rss/ascii/rss.xml",
    "http://rss.rssad.jp/rss/codezine/new/20/index.xml",
    "http://rss.rssad.jp/rss/forest/rss.xml",
    "http://rss.rssad.jp/rss/gihyo/feed/atom",
    "http://rss.rssad.jp/rss/headline/headline.rdf",
    "http://rss.rssad.jp/rss/impresswatch/pcwatch.rdf",
    "http://rss.rssad.jp/rss/itm/1.0/makoto.xml",
    "http://rss.rssad.jp/rss/itm/1.0/netlab.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_akiba.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_android_appli.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_apple.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_facebook.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_google.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_ipad.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_iphone.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_iphone_appli.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_mixi.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_smartphone.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_twitter.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_ustream.xml",
    "http://rss.rssad.jp/rss/itmbizid/1.0/bizid.xml",
    "http://rss.rssad.jp/rss/itmnews/2.0/news_bursts.xml",
    "http://rss.rssad.jp/rss/japaninternetcom/index.rdf",
    "http://rss.rssad.jp/rss/oshietekun/atom.xml",
    "http://rss.rssad.jp/rss/slashdot/slashdot.rss",
    "http://rss.rssad.jp/rss/zaikeishimbun/main.xml",
    "http://rssc.dokoda.jp/r/8a1dd8f128047929ba4390dab3c8065e/http/searchranking.yahoo.co.jp/realtime_buzz/",
    "http://sakurabaryo.com/feed/",
    "http://sankei.jp.msn.com/rss/news/points.xml",
    "http://sankei.jp.msn.com/rss/news/west_points.xml",
    "http://search.goo.ne.jp/rss/newkw.rdf",
    "http://sekurosu.com/rss",
    "http://streaming.yahoo.co.jp/rss/newly/anime/",
    "http://sub0000528116.hmk-temp.com/wordpress/?feed=rss2",
    "http://sukebei.nyaa.se/?page=rss&sort=2",
    "http://tenshoku.mynavi.jp/knowhow/rss.xml",
    "http://tensinyakimeshi.blog98.fc2.com/?xml",
    "http://thefreedom12.blog41.fc2.com/?xml",
    "http://togetter.com/rss/hot/culture/62",
    "http://togetter.com/rss/hot/culture/63",
    "http://torimatome.main.jp/blogs/comments/feed",
    "http://torimatome.main.jp/blogs/feed",
    "http://toshinokyouko.com/rss.php",
    "http://tvanimedouga.blog93.fc2.com/?xml",
    "http://uranourainformation.blog21.fc2.com/?xml",
    "http://video.fc2.com/a/feed_popular.php?m=week",
    "http://weather.livedoor.com/forecast/rss/area/400010.xml",
    "http://wotopi.jp/feed",
    "http://www.100shiki.com/feed",
    "http://www.alistapart.com/rss.xml",
    "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=36",
    "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=38",
    "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=47",
    "http://www.blosxom.com/?feed=rss2",
    "http://www.britannica.com/eb/dailycontent/rss",
    "http://www.csmonitor.com/rss/top.rss",
    "http://www.ehackingnews.com/feeds/posts/default",
    "http://www.falcom.co.jp/new.xml",
    "http://www.famitsu.com/rss/category/fcom_game.rdf",
    "http://www.famitsu.com/rss/fcom_all.rdf",
    "http://www.ganganonline.com/rss/index.xml",
    "http://www.ideaxidea.com/feed",
    "http://www.itnews711.com/index.rdf",
    "http://www.jp.playstation.com/whatsnew/whatsnew.rdf",
    "http://www.keyman.or.jp/rss/v1/?rss_type=all",
    "http://www.koubo.co.jp/rss.xml",
    "http://www.nyaa.se/?page=rss&sort=2",
    "http://www.nyaa.se/?page=rss&user=118009",
    "http://www.nytimes.com/services/xml/rss/userland/HomePage.xml",
    "http://www.phianime.tv/feed/",
    "http://www.rebootdevelop.hr/feed/",
    "http://www.rictus.com/muchado/feed/",
    "http://www.sbcr.jp/atom.xml",
    "http://www.slashgear.com/comments/feed/",
    "http://www.torrent-anime.com/feed",
    "http://www.torrent-anime.com/feed/",
    "http://www.webimemo.com/feed/",
    "http://www.wired.com/news_drop/netcenter/netcenter.rdf",
    "http://www.xvideos.com/rss/rss.xml",
    "http://www.youtube.com/rss/user/KADOKAWAanime/videos.rss",
    "http://www.youtube.com/rss/user/demosouko/videos.rss",
    "http://www.yukawanet.com/index.rdf",
    "http://www.zou3.net/php/rss/nikkei2rss.php?head=main",
    "http://xml.ehgt.org/ehtracker.xml",
    "http://xml.metafilter.com/rss.xml",
    "http://xvideos.2jiero.info/feed",
    "http://yaraon.blog109.fc2.com/?xml",
    "http://yusaani.com/home/feed/",
    "http://zipdeyaruo.blog42.fc2.com/?xml",
    "http://www.portalgraphics.net/rss/latest_image_list.xml",
    "http://api.syosetu.com/writernovel/430380.Atom",
    "http://creive.me/feed/",
    "http://gihyo.jp/dev/feed/atom",
    "http://gihyo.jp/feed/rss1",
    "http://hakase255.blog135.fc2.com/?xml",
    "http://2ch-ranking.net/rss/zenban.rdf",
    "http://www.isus.jp/feed/",
    "http://www.jiji.com/rss/ranking.rdf",
    "http://jp.gamesindustry.biz/rss/index.xml",
    "https://www.youtube.com/feeds/videos.xml?channel_id=UCx1nAvtVDIsaGmCMSe8ofsQ",
    "http://zakuzaku911.com/index.rdf",
    "http://ke-tai.org/blog/feed/",
    "http://data.newantenna.net/ero/rss/all.xml",
    "http://developer.mixi.co.jp/feed/atom",
    "http://neoneetch.blog.fc2.com/?xml",
    "http://rss.itmedia.co.jp/rss/1.0/netlab.xml",
    "http://netgeek.biz/feed",
    "http://blog.esuteru.com/index.rdf",
    "http://b.hatena.ne.jp/hotentry/game.rss",
    "http://b.hatena.ne.jp/hotentry.rss",
    "http://mobile.seisyun.net/rss/hot.rdf",
    "http://yomi.mobi/rss/hot.rdf",
    "http://saymygame.com/feed/",
    "http://blog.webcreativepark.net/atom.xml",
    "http://buhidoh.net/?xml",
    "http://www.webcyou.com/?feed=rss2",
    "http://withnews.jp/rss/consumer/new.rdf",
    "https://yande.re/post/atom?tags=loli",
    "http://blog.livedoor.jp/nizigami/atom.xml",
    "http://nvmzaq.blog.fc2.com/?xml",
    "http://keieimanga.net/index.rdf",
    "http://megumi.ldblog.jp/atom.xml",
    "http://kirik.tea-nifty.com/diary/index.rdf",
    "http://sinri.net/comments/feed",
    "http://himasoku.com/atom.xml",
    "http://himasoku.com/index.rdf",
    "http://20kaido.com/index.rdf",
    "http://h723.blog.fc2.com/?xml",
    "http://onecall2ch.com/index.rdf",
    "http://www.forest.impress.co.jp/rss.xml",
    "http://www.zaikei.co.jp/rss/sections/it.xml",
    "http://akiba.keizai.biz/rss.xml",
    "http://agag.tw/feed/2d-popular.rss",
    "http://adult-vr.jp/feed/",
    "http://www.anige-sokuhouvip.com/?xml",
    "http://animeanime.jp/rss/index.rdf",
    "http://alfalfalfa.com/index.rdf",
    "http://feeds.feedburner.com/fc2/GhfA",
    "http://erogetaiken072.blog.fc2.com/?xml",
    "http://otanew.jp/atom.xml",
    "http://jin115.com/index.rdf",
    "http://www.onlinegamer.jp/rss/news.rdf",
    "http://karapaia.livedoor.biz/index.rdf",
    "http://getnews.jp/feed/ext/orig",
    "http://www.gungho.co.jp/news/xml/rss.xml",
    "http://blog.livedoor.jp/kinisoku/index.rdf",
    "http://feeds.gizmodo.jp/rss/gizmodo/index.xml",
    "http://himado.in/?sort=movie_id&rss=1",
    "http://k-tai.impress.co.jp/cda/rss/ktai.rdf",
    "http://gehasoku.com/atom.xml",
    "http://feedblog.ameba.jp/rss/ameblo/principia-ca/rss20.xml",
    "http://zai.diamond.jp/list/feed/rssfxnews",
    "http://capacitor.blog.fc2.com/?xml",
    "http://blog.livedoor.jp/vipsister23/index.rdf",
    "http://vipsister23.com/atom.xml",
    "http://b.hatena.ne.jp/search/tag?safe=on&q=2ch&users=500&mode=rss",
    "http://b.hatena.ne.jp/search/tag?safe=on&q=%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF&users=500&mode=rss",
    "http://b.hatena.ne.jp/search/tag?safe=off&q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&users=500&mode=rss",
    "http://shikaku2ch.doorblog.jp/atom.xml",
    "http://dotinstall.com/lessons.rss",
    "http://2ch-ranking.net/rss/news4vip.rdf",
    "http://blog.livedoor.jp/insidears/index.rdf",
    "http://2ch-ranking.net/rss/newsplus.rdf",
    "http://2ch-ranking.net/rss/news.rdf",
    "http://nullpoantenna.com/game.rdf",
    "http://workingnews.blog117.fc2.com/?xml",
    "http://bm.s5-style.com/feed",
    "http://2ch-ranking.net/rss/ghard.rdf",
    "http://www.724685.com/blog/rss.xml",
    "http://www.yukawanet.com/index.rdf",
    "http://2ch-ranking.net/rss/bizplus.rdf",
    "http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0&lang=ja-jp",
    "http://www.tarikin.net/rss0.rdf",
    "http://blog.livedoor.jp/dqnplus/index.rdf",
    "http://www.seojapan.com/blog/feed",
    "http://2ch-ranking.net/rss/morningcoffee.rdf",
    "http://2ch-ranking.net/mt50k.rdf",
    "http://rssblog.ameba.jp/yandereotto/rss20.xml",
    "https://business.nikkei.com/rss/sns/nb.rdf",
    "http://daredemopc.blog51.fc2.com/?xml",
    "http://erogetaikenban.blog65.fc2.com/?xml",
    "http://news.goo.ne.jp/rss/topstories/gootop/index.rdf",
    "http://lanovelien.blog121.fc2.com/?xml",
    "http://news.livedoor.com/topics/rss/eco.xml",
    "http://ragnarokonline.gungho.jp/index.rdf",
    "http://rocketnews24.com/feed/",
    "https://news.denfaminicogamer.jp/feed",
    "http://www.igda.jp/?feed=rss2",
    "http://feeds.cnn.co.jp/cnn/rss"
    ];

    // ==== 既存データのロード ====
    if (!localStorage.getItem('verse_shared_rssFeeds')) {
      localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(PRESET_RSS));
    }

    // アプリ状態
    let users = JSON.parse(localStorage.getItem('verse_users') || '[]');
    let currentUser = JSON.parse(localStorage.getItem('verse_currentUser') || 'null');
    let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
    let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
    let isInitialized = false;

    // RSS/BOT状態
    let botInterval = null;
    let rssInterval = null;

    // 共有RSS設定
    let sharedRssFeeds = JSON.parse(localStorage.getItem('verse_shared_rssFeeds') || '[]');
    let sharedRssInterval = Number(localStorage.getItem('verse_shared_rssInterval')) || 300;
    let sharedRssLastIds = JSON.parse(localStorage.getItem('verse_shared_rssLastIds') || '{}');
    // 各フィードのON/OFF(true=ON)。未設定はデフォルトON。
    let sharedRssEnabled = JSON.parse(localStorage.getItem('verse_shared_rssEnabled') || '{}');

    // ===== 認証UI =====
    function showAuthModal(mode = 'login', errorMsg = '') {
      document.getElementById('auth-title').textContent = (mode === 'register') ? '新規登録' : 'ログイン';
      document.getElementById('auth-form').authMode = mode;
      document.getElementById('auth-email').value = '';
      document.getElementById('auth-password').value = '';
      document.getElementById('auth-modal').style.display = '';
      document.getElementById('main-content').style.display = 'none';
      document.getElementById('auth-error').textContent = errorMsg || '';
      document.getElementById('auth-error').style.display = errorMsg ? '' : 'none';
      document.getElementById('toggle-auth-mode').textContent = (mode === 'register') ? 'ログインはこちら' : '新規登録はこちら';
    }
    function hideAuthModal() {
      document.getElementById('auth-modal').style.display = 'none';
      document.getElementById('main-content').style.display = '';
    }

    document.addEventListener('DOMContentLoaded', () => {
      document.getElementById('auth-form').onsubmit = function(e) {
        e.preventDefault();
        const email = document.getElementById('auth-email').value.trim().toLowerCase();
        const password = document.getElementById('auth-password').value;
        if (!email || !password) {
          showAuthModal(this.authMode, 'メールアドレスとパスワードを入力してください');
          return;
        }
        if (this.authMode === 'register') {
          if (users.find(u => u.email === email)) {
            showAuthModal('register', 'このメールアドレスは既に登録されています');
            return;
          }
          const newUser = {
            email,
            password,
            profile: { icon: 'https://via.placeholder.com/100', username: email.split('@')[0], selfIntro: '' }
          };
          users.push(newUser);
          localStorage.setItem('verse_users', JSON.stringify(users));
          currentUser = { email };
          localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
          showAuthModal('login', '登録完了!ログインしてください');
        } else {
          const user = users.find(u => u.email === email && u.password === password);
          if (!user) { showAuthModal('login', 'メールアドレスまたはパスワードが違います'); return; }
          currentUser = { email };
          localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
          hideAuthModal();
          initializeApp();
        }
      };

      document.getElementById('toggle-auth-mode').onclick = function() {
        const mode = (document.getElementById('auth-title').textContent === '新規登録') ? 'login' : 'register';
        showAuthModal(mode);
      };

      if (!currentUser) showAuthModal('login'); else { hideAuthModal(); initializeApp(); }
    });

    function logout() {
      localStorage.removeItem('verse_currentUser');
      currentUser = null;
      stopRssAuto();
      showAuthModal('login');
    }

    // ===== 初期化 =====
    function initializeApp() {
      if (isInitialized) return;
      if (!currentUser) { showAuthModal('login'); return; }

      users = JSON.parse(localStorage.getItem('verse_users') || '[]');
      posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
      isDarkMode = localStorage.getItem('verse_darkMode') === 'true';

      const user = users.find(u => u.email === currentUser.email);
      window.profile = user ? user.profile : { icon: 'https://via.placeholder.com/100', username: 'ゲストユーザー', selfIntro: '' };

      updateAllUI();
      updateStatusIndicators();
      updateRssUI();

      if (isDarkMode) {
        document.body.classList.add('dark');
        document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
      } else {
        document.body.classList.remove('dark');
        document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
      }

      document.getElementById('main-content').style.display = '';
      document.getElementById('logout-btn').style.display = '';
      isInitialized = true;

      startRssAuto();
      addLog('bot-log', 'BOT機能初期化完了', 'success');
      addLog('rss-log', 'RSS自動投稿(全体共有)初期化完了', 'success');
    }

    // ===== 投稿(ユーザー/BOT) =====
    function createUserPost() {
      const ta = document.getElementById('postContent');
      const txt = ta.value.trim();
      if (!txt) return alert('投稿内容を入力してください。');
      if (!currentUser) return alert('ログインが必要です。');
      createPost(txt, 'user', profile.username, profile.icon);
      ta.value = '';
      updateCharCount();
    }

    function createPost(content, type = 'user', username = null, icon = null, extra = {}) {
      if (!content || !content.trim()) return false;
      const post = {
        id: Date.now() + Math.random(),
        content: content.trim(),
        likes: 0,
        timestamp: new Date().toLocaleString('ja-JP'),
        type,
        username: username || profile.username,
        icon: icon || profile.icon,
        userEmail: currentUser ? currentUser.email : '',
        ...extra
      };
      posts.unshift(post);
      saveData();
      renderTimeline();
      return true;
    }

    function likePost(id) {
      const idx = posts.findIndex(p => p.id === id);
      if (idx >= 0) {
        posts[idx].likes++;
        saveData();
        renderTimeline();
      }
    }

    function deletePost(id) {
      if (!confirm('この投稿を削除しますか?')) return;
      posts = posts.filter(p => p.id !== id);
      saveData();
      renderTimeline();
    }

    function clearAllPosts() {
      if (!confirm('全ての投稿を削除しますか?')) return;
      posts = [];
      saveData();
      renderTimeline();
    }

    function exportData() {
      const blob = new Blob([JSON.stringify(posts, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'verse_posts.json';
      a.click();
      URL.revokeObjectURL(url);
    }

    // ===== タイムライン描画(JSX混入の修正・無害化) =====
    function escapeHtml(s) {
      return (s || '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[m]));
    }

    function renderTimeline() {
      const tl = document.getElementById('timeline');
      const emp = document.getElementById('empty-timeline');
      const cnt = document.getElementById('post-count');
      if (!tl || !emp || !cnt) return;

      const allPosts = posts;
      if (allPosts.length === 0) {
        tl.innerHTML = '';
        emp.style.display = 'block';
        cnt.textContent = '(0件の投稿)';
        return;
      }
      emp.style.display = 'none';
      cnt.textContent = `(${allPosts.length}件の投稿)`;

      tl.innerHTML = allPosts.map(p => {
        const info = { bot: '🤖 BOT', markov: '🎲 MarkovBOT', user: '👤 ユーザー', feed: '📰 FEEDBOT' }[p.type] || '👤';
        const main = p.link
          ? `<a href="${p.link}" target="_blank" class="text-blue-600 underline">${escapeHtml(p.content)}</a>`
          : `${escapeHtml(p.content)}`;
        return `
          <div class="timeline-post p-6">
            <div class="flex justify-between items-start mb-4">
              <div class="flex items-center space-x-3">
                <img src="${p.icon}" class="w-10 h-10 rounded-full object-cover" onerror="this.src='https://via.placeholder.com/40'">
                <div>
                  <div class="flex items-center">
                    <span class="font-semibold text-gray-800 dark:text-white">${escapeHtml(p.username)}</span>
                    <span class="username-badge">${info}</span>
                  </div>
                  <div class="text-sm text-gray-500 dark:text-gray-400">${p.timestamp}</div>
                </div>
              </div>
            </div>
            <div class="text-gray-800 dark:text-gray-200 mb-4 leading-relaxed">${main}</div>
            <div class="flex items-center space-x-4 pt-4 border-t border-gray-100 dark:border-gray-600">
              <button onclick="likePost(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500">
                <i class="fas fa-heart"></i><span>${p.likes}</span>
              </button>
              <div class="relative">
                <button onclick="toggleShareMenu(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-blue-500">
                  <i class="fas fa-share"></i><span>シェア</span>
                </button>
                <div id="share-menu-${p.id}" class="share-menu hidden">
                  <button onclick="shareToX(${p.id})"><i class="fab fa-x-twitter text-blue-400 mr-2"></i>Xでシェア</button>
                  <button onclick="shareToLine(${p.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
                  <button onclick="copyPost(${p.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
                </div>
              </div>
              <button onclick="deletePost(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500 ml-auto">
                <i class="fas fa-trash"></i><span>削除</span>
              </button>
            </div>
          </div>
        `;
      }).join('');
    }

    function getPostContentText(id) {
      const p = posts.find(x => x.id === id);
      if (!p) return '';
      const tmp = document.createElement('div');
      tmp.innerHTML = p.content;
      return tmp.textContent || tmp.innerText || '';
    }
    function toggleShareMenu(id) {
      document.querySelectorAll('[id^="share-menu-"]')
        .forEach(el => el.classList.add('hidden'));
      const m = document.getElementById('share-menu-' + id);
      if (m) m.classList.toggle('hidden');
    }
    function shareToX(id) {
      const t = encodeURIComponent(getPostContentText(id));
      const u = encodeURIComponent(location.href);
      window.open(`https://twitter.com/intent/tweet?text=${t}&url=${u}`, '_blank');
    }
    function shareToLine(id) {
      const u = encodeURIComponent(location.href);
      window.open(`https://social-plugins.line.me/lineit/share?url=${u}`, '_blank');
    }
    function copyPost(id) {
      const t = getPostContentText(id);
      if (navigator.clipboard) {
        navigator.clipboard.writeText(t).then(() => alert('コピーしました')).catch(() => fallbackCopy(t));
      } else fallbackCopy(t);
    }
    function fallbackCopy(t) {
      const ta = document.createElement('textarea');
      ta.value = t; document.body.appendChild(ta);
      ta.select(); document.execCommand('copy');
      document.body.removeChild(ta);
      alert('コピーしました');
    }

    // ===== RSS UI(個別ON/OFF + 一括ON/OFF) =====
    function updateRssUI() {
      const listDiv = document.getElementById('rss-list');
      if (!listDiv) return;

      if (!sharedRssFeeds || sharedRssFeeds.length === 0) {
        listDiv.innerHTML = '<div class="text-white text-xs opacity-80">RSSフィード未登録</div>';
      } else {
        listDiv.innerHTML = sharedRssFeeds.map((url, i) => {
          const enabled = sharedRssEnabled[url] !== false; // 既定ON
          const enc = encodeURIComponent(url);
          return `
            <div class="flex items-center space-x-2 bg-white bg-opacity-90 rounded px-2 py-2 mb-1">
              <input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleRssEnabled('${enc}', this.checked)" title="ON/OFF">
              <div class="truncate flex-1 text-xs" title="${escapeHtml(url)}">${escapeHtml(url)}</div>
              <button onclick="delRssFeed(${i})" class="text-red-500 hover:text-red-700" title="削除"><i class="fas fa-trash"></i></button>
            </div>
          `;
        }).join('');
      }
      const iv = document.getElementById('rss-interval');
      if (iv) iv.value = sharedRssInterval || 300;
    }

    function toggleRssEnabled(encUrl, on) {
      const url = decodeURIComponent(encUrl);
      sharedRssEnabled[url] = !!on;
      saveSharedRss();
      addLog('rss-log', `FEED ${on ? 'ON' : 'OFF'}: ${url}`, 'info');
    }
    function setAllRssEnabled(on) {
      (sharedRssFeeds || []).forEach(u => sharedRssEnabled[u] = !!on);
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', `全フィードを${on ? 'ON' : 'OFF'}にしました`, 'success');
    }

    function saveSharedRss() {
      localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(sharedRssFeeds));
      localStorage.setItem('verse_shared_rssInterval', String(sharedRssInterval));
      localStorage.setItem('verse_shared_rssLastIds', JSON.stringify(sharedRssLastIds));
      localStorage.setItem('verse_shared_rssEnabled', JSON.stringify(sharedRssEnabled));
    }

    function addRssFeed() {
      const url = document.getElementById('rss-url').value.trim();
      if (!/^https?:\/\/.+/.test(url)) { addLog('rss-log', '正しいRSSフィードURLを入力してください', 'error'); return; }
      if (!sharedRssFeeds) sharedRssFeeds = [];
      if (sharedRssFeeds.includes(url)) { addLog('rss-log', 'すでに登録済みです', 'error'); return; }
      sharedRssFeeds.push(url);
      sharedRssEnabled[url] = true; // 既定ON
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', `RSS追加: ${url}`, 'success');
      document.getElementById('rss-url').value = '';
    }

    function delRssFeed(i) {
      if (!sharedRssFeeds[i]) return;
      if (!confirm('このフィードを削除しますか?')) return;
      const url = sharedRssFeeds[i];
      sharedRssFeeds.splice(i, 1);
      delete sharedRssEnabled[url];
      delete sharedRssLastIds[url];
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', 'RSS削除', 'info');
    }

    function setRssInterval() {
      const iv = document.getElementById('rss-interval').valueAsNumber || 300;
      if (iv < 10) return alert('間隔は10秒以上で設定してください。');
      sharedRssInterval = iv;
      saveSharedRss();
      updateRssUI();
      startRssAuto();
      addLog('rss-log', `自動投稿間隔を${iv}秒に設定`, 'success');
    }

    function fetchRssNow() { fetchRssFeeds(); }

    // ===== RSS取得(OFFのフィードはスキップ) =====
    function fetchRssFeeds() {
      if (!sharedRssFeeds || sharedRssFeeds.length === 0) return;
      sharedRssFeeds.forEach(feedUrl => {
        if (sharedRssEnabled[feedUrl] === false) {
          addLog('rss-log', `OFFのため取得スキップ: ${feedUrl}`, 'info');
          return;
        }
        fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feedUrl))
          .then(resp => resp.json())
          .then(data => {
            if (!data.items || !data.items.length) return;
            let lastId = sharedRssLastIds[feedUrl] || '';
            let newItems = [];
            for (const item of data.items) {
              const guid = item.guid || item.link || item.pubDate || item.title;
              if (!lastId || String(guid) > String(lastId)) newItems.push(item);
            }
            if (newItems.length === 0) return;

            newItems.reverse().forEach(item => {
              const guid = item.guid || item.link || item.pubDate || item.title;
              // FEED本文は省略(タイトル+リンクのみポスト)
              if (!posts.some(p => p.type === 'feed' && p.link === item.link)) {
                createPost(item.title, 'feed', 'FEEDBOT', 'https://cdn-icons-png.flaticon.com/512/3416/3416046.png', { link: item.link });
                addLog('rss-log', `新しい記事: ${item.title}`, 'success');
              }
              sharedRssLastIds[feedUrl] = guid;
            });
            saveSharedRss();
          })
          .catch(() => addLog('rss-log', 'RSS取得エラー: ' + feedUrl, 'error'));
      });
    }

    function startRssAuto() {
      stopRssAuto();
      fetchRssFeeds();
      rssInterval = setInterval(fetchRssFeeds, (sharedRssInterval || 300) * 1000);
      updateStatusIndicators();
      addLog('rss-log', `RSS自動投稿を開始 (${sharedRssInterval}秒間隔)`, 'success');
    }

    function stopRssAuto() {
      if (rssInterval) clearInterval(rssInterval);
      rssInterval = null;
      updateStatusIndicators();
      addLog('rss-log', 'RSS自動投稿を停止しました', 'info');
    }

    // ===== BOT =====
    function postBotMessage() {
      const ta = document.getElementById('botContent');
      const txt = ta.value.trim();
      if (!txt) return alert('BOT投稿内容を入力してください。');
      if (!currentUser) return alert('ログインが必要です。');
      if (createPost(txt, 'bot', 'BOT', profile.icon)) {
        ta.value = '';
        addLog('bot-log', `BOT投稿: "${txt.substring(0, 30)}..."`, 'success');
      }
    }

    function generateMarkovText() {
      // FEED(外部記事)は学習対象から除外し、ユーザーとBOT投稿のみ学習
      let text = posts
        .filter(p => ['user','bot'].includes(p.type))
        .map(p => {
          const d = document.createElement('div');
          d.innerHTML = p.content;
          return (d.textContent || d.innerText || '')
            .replace(/\s+/g, ' ').replace(/https?:\/\/\S+/g, '').trim();
        }).join(' ');

      if (text.length < 20) {
        const fallbacks = ["今日はいい天気ですね!","最近面白いニュースありましたか?","新しいアイデアが浮かんできました。","皆さんはどう思いますか?"];
        return fallbacks[Math.floor(Math.random() * fallbacks.length)];
      }

      const tokens = text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\w]+|[。、!?\r\n]/g) || [];
      if (tokens.length < 2) return tokens.join('');

      const markov = {};
      for (let i = 0; i < tokens.length - 2; i++) {
        const key = tokens[i] + '|' + tokens[i+1];
        if (!markov[key]) markov[key] = [];
        markov[key].push(tokens[i+2]);
      }

      let idx = Math.floor(Math.random() * (tokens.length - 2));
      let key = tokens[idx] + '|' + tokens[idx+1];
      let result = [tokens[idx], tokens[idx+1]];
      let maxLen = 60 + Math.floor(Math.random() * 40);

      for (let i = 0; i < maxLen; i++) {
        const nexts = markov[key];
        if (!nexts || nexts.length === 0) break;
        const next = nexts[Math.floor(Math.random() * nexts.length)];
        result.push(next);
        if (/[。!?\n]/.test(next)) break;
        key = result[result.length - 2] + '|' + result[result.length - 1];
      }
      return result.join('').replace(/\n/g, '');
    }

    function postMarkovBot() {
      const txt = generateMarkovText();
      if (createPost(txt, 'markov', 'MarkovBOT', profile.icon)) {
        addLog('bot-log', `マルコフ投稿: "${txt.substring(0, 40)}..."`, 'success');
      }
    }

    function startBotAutoPost() {
      const iv = document.getElementById('botIntervalSec').valueAsNumber || 60;
      if (iv < 10) { alert('間隔は10秒以上で設定してください。'); return; }
      stopBotAutoPost();
      setTimeout(postMarkovBot, 3000);
      botInterval = setInterval(postMarkovBot, iv * 1000);
      updateStatusIndicators();
      addLog('bot-log', `マルコフBOT自動投稿開始 (${iv}秒間隔)`, 'success');
    }

    function stopBotAutoPost() {
      if (botInterval) {
        clearInterval(botInterval);
        botInterval = null;
        updateStatusIndicators();
        addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
      }
    }

    // ===== 共通UI/保存 =====
    function addLog(id, msg, type = 'info') {
      const el = document.getElementById(id);
      const ts = new Date().toLocaleTimeString('ja-JP');
      const div = document.createElement('div');
      const cls = { error: 'error-message', success: 'success-message', info: 'text-white opacity-90' }[type] || 'text-white opacity-90';
      div.className = cls;
      div.innerHTML = `<span class="opacity-75">[${ts}]</span> ${escapeHtml(msg)}`;
      if (el) {
        el.appendChild(div);
        el.scrollTop = el.scrollHeight;
        while (el.children.length > 100) el.removeChild(el.firstChild);
      }
      try { console.log(`[${ts}] ${msg}`); } catch(_) {}
    }

    function updateStatusIndicators() {
      const botI = document.getElementById('bot-status');
      const botT = document.getElementById('bot-status-text');
      if (botI && botT) {
        const active = botInterval !== null;
        botI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
        botT.textContent = active ? '動作中' : '停止中';
      }
      const rssI = document.getElementById('rss-status');
      const rssT = document.getElementById('rss-status-text');
      if (rssI && rssT) {
        const active = rssInterval !== null;
        rssI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
        rssT.textContent = active ? '動作中' : '停止中';
      }
    }

    function showSystemStatus() {
      alert(
`=== Verse システムステータス ===\n全体投稿数: ${posts.length}\nRSS登録数: ${sharedRssFeeds.length}\nBOT投稿数: ${posts.filter(p => ['bot', 'markov'].includes(p.type)).length}\n\nBOT自動投稿: ${botInterval ? '動作中' : '停止中'}\nRSS自動投稿: ${rssInterval ? '動作中' : '停止中'}`
      );
    }

    function uploadProfileIcon(e) {
      const f = e.target.files[0];
      if (!f) return;
      if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
      const r = new FileReader();
      r.onload = () => {
        profile.icon = r.result;
        saveProfileNoAlert();
        updateAllUI();
        alert('プロフィール画像更新!');
      };
      r.readAsDataURL(f);
    }

    function saveProfile() {
      const un = document.getElementById('username').value.trim();
      const si = document.getElementById('self-intro').value.trim();
      if (un.length > 20) { alert('ユーザー名は20文字以内で。'); return; }
      profile.username = un || 'ゲストユーザー';
      profile.selfIntro = si;
      saveProfileNoAlert();
      updateAllUI();
      alert('プロフィール保存!');
    }

    function saveProfileNoAlert() {
      users = JSON.parse(localStorage.getItem('verse_users') || '[]');
      const idx = users.findIndex(u => u.email === (currentUser && currentUser.email));
      if (idx >= 0) {
        users[idx].profile = profile;
        localStorage.setItem('verse_users', JSON.stringify(users));
      }
    }

    function updateAllUI() {
      const pi = document.getElementById('profile-icon');
      const hi = document.getElementById('header-profile-icon');
      if (pi) pi.src = profile.icon;
      if (hi) hi.src = profile.icon;
      ['username-preview','header-username'].forEach(id => {
        const el = document.getElementById(id);
        if (el) el.textContent = profile.username;
      });
      const sip = document.getElementById('self-intro-preview');
      if (sip) sip.textContent = profile.selfIntro || 'まだ自己紹介がありません';
      const emailEl = document.getElementById('header-user-email');
      if (emailEl && currentUser) emailEl.textContent = currentUser.email;
      renderTimeline();
    }

    function updateCharCount() {
      const pc = document.getElementById('postContent');
      const cc = document.getElementById('char-count');
      if (pc && cc) {
        const l = pc.value.length;
        cc.textContent = `(${l}/500)`;
        cc.style.color = l > 450 ? '#ef4444' : '';
      }
    }

    function toggleDarkMode() {
      isDarkMode = !isDarkMode;
      if (isDarkMode) {
        document.body.classList.add('dark');
        document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
      } else {
        document.body.classList.remove('dark');
        document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
      }
      localStorage.setItem('verse_darkMode', isDarkMode.toString());
    }

    function saveData() { localStorage.setItem('verse_posts', JSON.stringify(posts)); }

    // 入力UIフック
    document.addEventListener('DOMContentLoaded', () => {
      const pc = document.getElementById('postContent');
      if (pc) {
        pc.addEventListener('input', updateCharCount);
        pc.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); createUserPost(); } });
      }
      const ui = document.getElementById('username');
      if (ui) ui.addEventListener('input', () => {
        const v = ui.value.trim() || 'ゲストユーザー';
        ['username-preview','header-username'].forEach(id => {
          const el = document.getElementById(id);
          if (el) el.textContent = v;
        });
      });
      const si = document.getElementById('self-intro');
      if (si) si.addEventListener('input', () => {
        const v = si.value.trim() || 'まだ自己紹介がありません';
        const el = document.getElementById('self-intro-preview');
        if (el) el.textContent = v;
      });
    });

    // 終了処理・エラー
    window.addEventListener('beforeunload', () => { stopBotAutoPost(); stopRssAuto(); saveData(); });
    window.addEventListener('error', e => { addLog('bot-log', `システムエラー: ${e.message}`, 'error'); });
  </script>
</body>
</html>

GPT-5

GPT‑5(正式には次世代ChatGPTとしてリリース予定)は、無料プランでも利用可能になる予定で、ユーザーはいわゆる標準的なインテリジェンス設定で無制限にチャット利用できるとOpenAI CEOのサム・アルトマン氏が公式に発表しています。 Plus や Pro の有料プランでは、さらに高度な知能レベルや追加機能にアクセスできるようになります note(ノート)+7note(ノート)+7MiraLab.inc+7


🎯 どこで無料で使えるか?

  • ChatGPT(OpenAI公式ウェブ版/アプリ)
    無料アカウントでも GPT‑5(標準モード)を利用可能。乱用防止のための使用制限(例:1日の回数上限など)は設けられる可能性があるが、無制限チャットが基本です Reddit
  • Microsoft Copilot
    Windows PCに統合された Copilot は GPT‑5 に対応する「Smart モード」を導入予定で、プラットフォームそのもので GPT‑5 を無料で利用できるようになると見られます(ただし、精度やレスポンスの仕様は流動的)note(ノート)+4note(ノート)+4accelc.co.jp+4

📋 まとめ

サービスGPT‑5 利用状況(無料プラン)
ChatGPT(OpenAI公式)✅ 無料で標準インテリジェンス設定において無制限利用可能(ただし乱用対策あり)
Microsoft Copilot(Windows統合)✅ Smart モードで GPT‑5 を採用、無料での使用が期待される
その他のAIサービス現時点では GPT‑5 の対応情報は限定的

ℹ️ 注意点と補足

  • GPT‑5 は現在、2025年8月ごろの正式リリースが予定されていますThe Verge。したがって、ChatGPT の無料版でも GPT‑5 を使い始めるのはリリース後になります。
  • アルトマン氏は「地球上のすべての人へのGPT‑5の無料提供」を理想として語っていますが、これはまだ公式に実現しているわけではなく、長期的な構想です chatgpt5free.com+15note(ノート)+15shift-ai.co.jp+15
  • 無料プランでは乱用防止や公平性維持のために、トークン制限・アクセス制限などの利用制約が設けられる可能性があります。また、Plus/Pro プランでは優先アクセスや高度な機能が得られます MiraLab.inc+1note(ノート)+1

✅ ご参考まで

  • 使い始めるには?
    OpenAI公式サイトまたはモバイルアプリから ChatGPT にサインアップ・ログインすれば、無料プランで GPT‑5(標準モード)が利用可能になります(リリース後)。
  • Microsoft Copilot を使うなら?
    Windows PC に標準搭載されている Copilot を通じて、GPT‑5 Smart モードで利用できるようになる予定です。

まとめると、GPT‑5 はリリース後に ChatGPT の無料プランで使えます(標準モードで無制限利用、ただし制限あり)。さらに、Microsoft Copilot ユーザーも同時に利用できる可能性があるとの見通しです。

推しログ – 推し活情報共有サービス

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>推しログ - 推し活情報共有サービス</title>
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-gradient-to-r from-purple-200 to-indigo-200">

  <!-- ヘッダー -->
  <header class="bg-purple-600 text-white p-4 shadow-lg">
    <div class="container mx-auto flex justify-between items-center">
      <h1 class="text-3xl font-bold"><i class="fa-solid fa-heart mr-2"></i>推しログ</h1>
      <nav class="space-x-4">
        <a href="#" class="hover:text-gray-200">ホーム</a>
        <a href="#" class="hover:text-gray-200">スケジュール</a>
        <a href="#" class="hover:text-gray-200">ニュース</a>
        <a href="#" class="hover:text-gray-200">ログイン</a>
      </nav>
    </div>
  </header>

  <!-- メインコンテンツ -->
  <main class="container mx-auto my-8 px-4">

    <!-- 推しスケジュール -->
    <section class="bg-white p-8 rounded-xl shadow-xl mb-8">
      <h2 class="text-2xl font-bold border-b pb-2 mb-4"><i class="fa-regular fa-calendar mr-2"></i>今月の推しスケジュール</h2>
      <ul class="space-y-3">
        <li class="flex justify-between items-center p-4 bg-gray-50 rounded-lg shadow">
          <span class="font-semibold">ライブ配信「推しの部屋」</span>
          <span class="text-gray-500">8/10(木) 20:00〜</span>
        </li>
        <li class="flex justify-between items-center p-4 bg-gray-50 rounded-lg shadow">
          <span class="font-semibold">ニューシングル発売日!</span>
          <span class="text-gray-500">8/15(火)</span>
        </li>
        <li class="flex justify-between items-center p-4 bg-gray-50 rounded-lg shadow">
          <span class="font-semibold">ファンクラブ限定イベント</span>
          <span class="text-gray-500">8/25(金) 18:30〜</span>
        </li>
      </ul>
      <button class="mt-4 bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded">もっと見る</button>
    </section>

    <!-- 最新推しニュース -->
    <section class="bg-white p-8 rounded-xl shadow-xl mb-8">
      <h2 class="text-2xl font-bold border-b pb-2 mb-4"><i class="fa-solid fa-newspaper mr-2"></i>最新推しニュース</h2>
      <article class="mb-6 border-b pb-4">
        <h3 class="font-semibold text-purple-700 mb-1">推しの新曲MVが公開!</h3>
        <p class="text-gray-600">待望の新曲MVが公式YouTubeチャンネルにて公開されました!視聴数も急上昇中!</p>
        <a href="#" class="text-blue-500 hover:underline">詳しく見る <i class="fa-solid fa-arrow-right ml-1"></i></a>
      </article>

      <article>
        <h3 class="font-semibold text-purple-700 mb-1">推し、テレビ番組に出演決定!</h3>
        <p class="text-gray-600">8/12(土)の「音楽バズ」にゲスト出演予定です。特別トークもお楽しみに!</p>
        <a href="#" class="text-blue-500 hover:underline">詳しく見る <i class="fa-solid fa-arrow-right ml-1"></i></a>
      </article>
      <button class="mt-4 bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded">もっと見る</button>
    </section>

    <!-- 推しメンバー紹介 -->
    <section class="bg-white p-8 rounded-xl shadow-xl">
      <h2 class="text-2xl font-bold border-b pb-2 mb-4"><i class="fa-solid fa-user-group mr-2"></i>推しメンバー紹介</h2>
      <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
        <div class="text-center">
          <img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し1">
          <h3 class="font-semibold">推しメン1</h3>
        </div>
        <div class="text-center">
          <img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し2">
          <h3 class="font-semibold">推しメン2</h3>
        </div>
        <div class="text-center">
          <img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し3">
          <h3 class="font-semibold">推しメン3</h3>
        </div>
        <div class="text-center">
          <img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し4">
          <h3 class="font-semibold">推しメン4</h3>
        </div>
      </div>
    </section>

  </main>

  <!-- フッター -->
  <footer class="bg-gray-900 text-white text-center py-4 mt-8">
    <p class="text-sm">© 2024 推しログ. All rights reserved.</p>
  </footer>

</body>
</html>