シンプルメモアプリ(javascript)のコード

memo.html

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>メモツール</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="style.css">
</head>
<body>

<!-- 浮遊タブ -->
<div class="floating-tabs">

  <!-- タブエリア -->
  <div class="tab-list" id="tabContainer">
    <button class="tab-btn" data-id="1" draggable="true">1</button>
    <button class="tab-btn" data-id="2" draggable="true">2</button>
    <button class="tab-btn" data-id="3" draggable="true">3</button>
    <button class="tab-btn" data-id="4" draggable="true">4</button>
    <button class="tab-btn" data-id="5" draggable="true">5</button>
    <button class="tab-btn" data-id="6" draggable="true">6</button>
    <button class="tab-btn" data-id="7" draggable="true">7</button>
    <button class="tab-btn" data-id="8" draggable="true">8</button>
    <button class="tab-btn" data-id="9" draggable="true">9</button>
    <button class="tab-btn" data-id="10" draggable="true">10</button>
  </div>

  <!-- 固定ボタン -->
  <div class="tab-tools">
    <button class="tab-btn width-btn" id="toggleWidthBtn" title="幅切り替え">📐</button>
    <button class="tab-btn save-btn" id="saveJsonBtn">json</button>
    <button class="tab-btn save-btn" id="saveHtmlBtn">html</button>
    <button class="tab-btn reset-btn" id="resetAllBtn" title="すべてリセット">
      🗑️
    </button>
    <button class="tab-btn import-btn" id="importJsonBtn" title="JSONから復元(インポート)">
      📥
    </button>
    <button class="tab-btn" id="open-current-folder" title="このページのフォルダを開く">
      📁
    </button>
  </div>

</div>

<!-- メインメモ -->
<textarea id="memo" placeholder="ここにメモを書いてください"></textarea>

<!-- ★ 外部JS -->
<script src="app.js"></script>
<script src="reset.js"></script>
<script src="search-all-tabs.js"></script>
<script src="import-json.js"></script>
<script src="export-html.js"></script>
<script src="open-folder.js"></script>
</body>
</html>

 favicon.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <rect x="4" y="4" width="22" height="24" rx="2" fill="#FCEEA7"/>
  <line x1="8" y1="12" x2="22" y2="12" stroke="#DCCB8C" stroke-width="2"/>
  <line x1="8" y1="18" x2="22" y2="18" stroke="#DCCB8C" stroke-width="2"/>
  <line x1="8" y1="24" x2="16" y2="24" stroke="#DCCB8C" stroke-width="2"/>
  <path d="M22 2 L30 10 L16 24 L8 26 L10 18 Z" fill="#FF9800"/>
  <path d="M8 26 L10 24 L12 26 Z" fill="#5D4037"/> </svg>

search-all-tabs.js

document.addEventListener("DOMContentLoaded", () => {

  /* =========================
     Ctrl + F フック
  ========================= */
  document.addEventListener("keydown", (e) => {

    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "f") {
      e.preventDefault();
      openSearchModal();
    }

  });

});

function openSearchModal() {

  if (document.getElementById("searchModal")) return;

  const modal = document.createElement("div");
  modal.id = "searchModal";
  modal.innerHTML = `
<div class="search-overlay">
  <div class="search-box">
    <input type="text" id="searchInput" placeholder="全タブ検索">
    <div id="searchResults"></div>
    <button id="closeSearch">閉じる</button>
  </div>
</div>
`;

  document.body.appendChild(modal);

  const input = document.getElementById("searchInput");
  const results = document.getElementById("searchResults");

  input.focus();

  input.addEventListener("input", () => {
    searchAllTabs(input.value, results);
  });

  document.getElementById("closeSearch").addEventListener("click", closeSearchModal);

  document.addEventListener("keydown", escCloseHandler);
}

function searchAllTabs(keyword, resultBox) {

  resultBox.innerHTML = "";
  if (!keyword.trim()) return;

  const {
    KEY_PREFIX,
    NAME_PREFIX,
    getTabButtons,
    switchTab
  } = window.MemoApp;

  const lower = keyword.toLowerCase();

  getTabButtons().forEach(btn => {
    const id = btn.dataset.id;
    const name = localStorage.getItem(NAME_PREFIX + id) || btn.textContent;
    const text = localStorage.getItem(KEY_PREFIX + id) || "";

    if (
      name.toLowerCase().includes(lower) ||
      text.toLowerCase().includes(lower)
    ) {
      const item = document.createElement("div");
      item.className = "search-item";
      item.textContent = `[${name}] ${text.slice(0, 40)}`;

      item.addEventListener("click", () => {
        closeSearchModal();
        switchTab(id);

        // ★ ハイライト
        highlightInTextarea(window.MemoApp.textarea, keyword);
      });

      resultBox.appendChild(item);
    }
  });
}

function closeSearchModal() {
  const modal = document.getElementById("searchModal");
  if (modal) modal.remove();

  removeHighlightOverlay();

  document.removeEventListener("keydown", escCloseHandler);
}

function escCloseHandler(e) {
  if (e.key === "Escape") {
    closeSearchModal();
  }
}

function highlightInTextarea(textarea, keyword) {

  removeHighlightOverlay();

  if (!keyword) return;

  const value = textarea.value;
  if (!value) return;

  // 正規表現用にエスケープ
  const escaped = keyword.replace(/[.*+?^${}()|[\]\]/g, "$&");
  const regex = new RegExp(escaped, "gi");

  if (!regex.test(value)) return;

  const overlay = document.createElement("div");
  overlay.id = "highlightOverlay";

  const style = getComputedStyle(textarea);
  overlay.style.position = "fixed";
  overlay.style.inset = "0";
  overlay.style.pointerEvents = "none";
  overlay.style.whiteSpace = "pre-wrap";
  overlay.style.font = style.font;
  overlay.style.padding = style.padding;
  overlay.style.lineHeight = style.lineHeight;
  overlay.style.background = "transparent";
  overlay.style.color = "transparent";
  overlay.style.zIndex = "2";

  // ★ 全一致をハイライト
  overlay.innerHTML = escapeHtml(value).replace(regex, match => {
    return `<span class="search-highlight">${escapeHtml(match)}</span>`;
  });

  document.body.appendChild(overlay);
}


function removeHighlightOverlay() {
  const overlay = document.getElementById("highlightOverlay");
  if (overlay) overlay.remove();
}

function escapeHtml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

 reset.js

document.addEventListener("DOMContentLoaded", () => {

  const resetAllBtn = document.getElementById("resetAllBtn");
  if (!resetAllBtn) return;

  resetAllBtn.addEventListener("click", () => {

    const ok = confirm(
      "すべてのタブをリセットします。\n\n" +
      "・タブ名\n" +
      "・並び順\n" +
      "・メモ内容\n\n" +
      "すべて削除され、元に戻せません。\n\n" +
      "本当に実行しますか?"
    );

    if (!ok) return;

    const {
      KEY_PREFIX,
      NAME_PREFIX,
      ORDER_KEY,
      CURRENT_TAB_KEY,
      getTabButtons,
      tabContainer,
      textarea,
      switchTab
    } = window.MemoApp;

    /* =========
       localStorage 削除
    ========= */
    getTabButtons().forEach(btn => {
      const id = btn.dataset.id;
      localStorage.removeItem(KEY_PREFIX + id);
      localStorage.removeItem(NAME_PREFIX + id);
    });

    localStorage.removeItem(ORDER_KEY);
    localStorage.removeItem(CURRENT_TAB_KEY);

    /* =========
       UI 初期化
    ========= */

    // タブ順をID順に戻す
    const buttons = Array.from(getTabButtons());
    buttons
      .sort((a, b) => Number(a.dataset.id) - Number(b.dataset.id))
      .forEach(btn => tabContainer.appendChild(btn));

    // タブ名・active解除
    getTabButtons().forEach(btn => {
      btn.textContent = btn.dataset.id;
      btn.classList.remove("active");
    });

    // textarea 初期化
    textarea.value = "";

    // タブ1を初期化
    window.MemoApp.currentTab = "1";
    localStorage.setItem(CURRENT_TAB_KEY, "1");
    switchTab("1", true);
  });

});

 open-folder.js

document.addEventListener("DOMContentLoaded", () => {

  const openFolderBtn = document.getElementById("open-current-folder");
  if (!openFolderBtn) return;

  openFolderBtn.addEventListener("click", () => {

    // file:// 以外では無効
    if (location.protocol !== "file:") {
      alert("この機能はローカル環境専用です");
      return;
    }

    // file:///D:/path/to/file.html → D:/path/to/
    let folderPath = decodeURIComponent(location.pathname);

    // Windows 用に先頭の / を除去
    if (folderPath.startsWith("/")) {
      folderPath = folderPath.slice(1);
    }

    // ファイル名を削除
    folderPath = folderPath.replace(/\/[^\/]+$/, "");

    // Explorer で開く
    location.href = "explorer:" + folderPath;
  });

});

 import-json.js

document.addEventListener("DOMContentLoaded", () => {

  const importBtn = document.getElementById("importJsonBtn");
  if (!importBtn) return;

  importBtn.addEventListener("click", () => {

    const ok = confirm(
      "JSONファイルからメモを復元します。\n\n" +
      "現在の内容はすべて上書きされます。\n\n" +
      "続行しますか?"
    );

    if (!ok) return;

    const input = document.createElement("input");
    input.type = "file";
    input.accept = "application/json";

    input.addEventListener("change", () => {
      const file = input.files[0];
      if (!file) return;

      const reader = new FileReader();

      reader.onload = () => {
        try {
          const data = JSON.parse(reader.result);
          restoreFromJson(data);
        } catch (e) {
          alert("JSONファイルの形式が正しくありません");
        }
      };

      reader.readAsText(file, "utf-8");
    });

    input.click();
  });

});

/* =========================
   JSON復元処理
========================= */
function restoreFromJson(data) {

  const {
    KEY_PREFIX,
    NAME_PREFIX,
    ORDER_KEY,
    CURRENT_TAB_KEY,
    getTabButtons,
    tabContainer,
    textarea,
    switchTab
  } = window.MemoApp;

  /* =========
     既存データ削除
  ========= */
  getTabButtons().forEach(btn => {
    const id = btn.dataset.id;
    localStorage.removeItem(KEY_PREFIX + id);
    localStorage.removeItem(NAME_PREFIX + id);
  });

  localStorage.removeItem(ORDER_KEY);
  localStorage.removeItem(CURRENT_TAB_KEY);

  /* =========
     データ復元
  ========= */
  const ids = Object.keys(data);

  ids.forEach(id => {
    const item = data[id];
    if (item.text) {
      localStorage.setItem(KEY_PREFIX + id, item.text);
    }
    if (item.name) {
      localStorage.setItem(NAME_PREFIX + id, item.name);
    }
  });

  /* =========
     タブ並び順復元
  ========= */
  ids.forEach(id => {
    const btn = tabContainer.querySelector(`.tab-btn[data-id="${id}"]`);
    if (btn) tabContainer.appendChild(btn);
  });

  localStorage.setItem(ORDER_KEY, JSON.stringify(ids));

  /* =========
     UI反映
  ========= */
  getTabButtons().forEach(btn => {
    btn.textContent =
      localStorage.getItem(NAME_PREFIX + btn.dataset.id) ||
      btn.dataset.id;
    btn.classList.remove("active");
  });

  textarea.value = "";

  const firstTab = ids[0] || "1";
  window.MemoApp.currentTab = firstTab;
  localStorage.setItem(CURRENT_TAB_KEY, firstTab);
  switchTab(firstTab, true);
}

 export-html.js

document.addEventListener("DOMContentLoaded", () => {

  const saveHtmlBtn = document.getElementById("saveHtmlBtn");
  if (!saveHtmlBtn) return;

  saveHtmlBtn.addEventListener("click", () => {

    const {
      KEY_PREFIX,
      NAME_PREFIX,
      getTabButtons,
      textarea,
      currentTab
    } = window.MemoApp;

    // 念のため現在タブ保存
    localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);

    let htmlBody = "";

    getTabButtons().forEach(btn => {
      const id = btn.dataset.id;
      const title = localStorage.getItem(NAME_PREFIX + id) || `タブ ${id}`;
      const text  = localStorage.getItem(KEY_PREFIX + id) || "";

      htmlBody += `
<section class="memo-section">
  <h2>${escapeHtml(title)}</h2>
  <pre>${escapeHtml(text)}</pre>
</section>
`;
    });

    const html = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>メモ</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  body {
    font-family: system-ui, -apple-system, "Segoe UI",
                 "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
    padding: 24px;
    line-height: 1.7;
    background: #ffffff;
    color: #111;
  }
  .memo-section {
    margin-bottom: 32px;
  }
  h2 {
    font-size: 18px;
    border-left: 4px solid #3b82f6;
    padding-left: 10px;
  }
  pre {
    white-space: pre-wrap;
    word-break: break-word;
    background: #f5f5f5;
    padding: 16px;
    border-radius: 6px;
  }
</style>
</head>
<body>
<h1>メモ</h1>
${htmlBody}
</body>
</html>`;

    const blob = new Blob([html], { type: "text/html" });
    const url = URL.createObjectURL(blob);

    const a = document.createElement("a");
    a.href = url;

    const ts = makeTimestamp();
    a.download = `simple-memo_${ts}.html`;

    a.click();

    URL.revokeObjectURL(url);
  });

});

/* =========================
   HTMLエスケープ
========================= */
function escapeHtml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

function makeTimestamp() {
  const d = new Date();
  const pad = n => String(n).padStart(2, "0");

  return (
    d.getFullYear() +
    pad(d.getMonth() + 1) +
    pad(d.getDate()) + "_" +
    pad(d.getHours()) +
    pad(d.getMinutes()) +
    pad(d.getSeconds())
  );
}

 app.js

document.addEventListener("DOMContentLoaded", () => {

  const textarea = document.getElementById("memo");
  const tabContainer = document.getElementById("tabContainer");
  const saveJsonBtn = document.getElementById("saveJsonBtn");

  const KEY_PREFIX = "simple_memo_tab_";
  const NAME_PREFIX = "simple_memo_tab_name_";
  const ORDER_KEY = "simple_memo_tab_order";
  const CURRENT_TAB_KEY = "simple_memo_current_tab";

  let currentTab = localStorage.getItem(CURRENT_TAB_KEY) || "1";

  // ===== export 用に公開 =====
  window.MemoApp = {};

  window.MemoApp.KEY_PREFIX  = KEY_PREFIX;
  window.MemoApp.NAME_PREFIX = NAME_PREFIX;
  window.MemoApp.ORDER_KEY  = ORDER_KEY;
  window.MemoApp.CURRENT_TAB_KEY = CURRENT_TAB_KEY;

  window.MemoApp.getTabButtons = getTabButtons;
  window.MemoApp.tabContainer = tabContainer;
  window.MemoApp.textarea = textarea;
  window.MemoApp.switchTab = switchTab;

  Object.defineProperty(window.MemoApp, "currentTab", {
    get() {
      return currentTab;
    },
    set(val) {
      currentTab = val;
    }
  });

  /* =========================
     タイムスタンプ作成
  ========================= */

function makeTimestamp() {
  const d = new Date();
  const pad = n => String(n).padStart(2, "0");

  const Y = d.getFullYear();
  const M = pad(d.getMonth() + 1);
  const D = pad(d.getDate());
  const h = pad(d.getHours());
  const m = pad(d.getMinutes());
  const s = pad(d.getSeconds());

  return `${Y}${M}${D}_${h}${m}${s}`;
}

  /* =========================
     タブ切り替え
  ========================= */
  function switchTab(id, skipSave = false) {
    if (!skipSave) {
      localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);
    }

    currentTab = id;
    localStorage.setItem(CURRENT_TAB_KEY, currentTab);

    textarea.value = localStorage.getItem(KEY_PREFIX + currentTab) || "";

    getTabButtons().forEach(btn => {
      btn.classList.toggle("active", btn.dataset.id === currentTab);
    });

    updateDocumentTitle(); // ★ 追加
  }

  function updateDocumentTitle() {
    const {
      NAME_PREFIX,
      currentTab
    } = window.MemoApp;

    const name = localStorage.getItem(NAME_PREFIX + currentTab) || `メモ ${currentTab}`;
    document.title = name + " - シンプルメモ";
  }

  /* =========================
     タブ名の復元
  ========================= */
  function loadTabNames() {
    getTabButtons().forEach(btn => {
      const name = localStorage.getItem(NAME_PREFIX + btn.dataset.id);
      if (name) btn.textContent = name;
    });
  }

  /* =========================
     タブ順序の復元
  ========================= */
  function loadTabOrder() {
    const order = JSON.parse(localStorage.getItem(ORDER_KEY));
    if (!order) return;

    order.forEach(id => {
      const btn = tabContainer.querySelector(`.tab-btn[data-id="${id}"]`);
      if (btn) tabContainer.appendChild(btn);
    });
  }

    function getTabButtons() {
    return tabContainer.querySelectorAll(".tab-btn[data-id]");
  }

    function renameTab(btn) {
      const currentName = btn.textContent;
      const name = prompt("タブ名を変更", currentName);

      if (!name) return;

      btn.textContent = name;
      localStorage.setItem(NAME_PREFIX + btn.dataset.id, name);

      updateDocumentTitle(); // ★ 追加
    }

  /* =========================
     タブイベント
  ========================= */
  getTabButtons().forEach(btn => {

    // 通常クリック:タブ切り替え
    btn.addEventListener("click", () => {
      switchTab(btn.dataset.id);
    });

    // 右クリック:名前変更
    btn.addEventListener("contextmenu", e => {
      e.preventDefault();
      renameTab(btn);
    });

    // ダブルクリック:名前変更
    btn.addEventListener("dblclick", e => {
      e.preventDefault();
      renameTab(btn);
    });

  });


  /* =========================
     入力時保存
  ========================= */
  textarea.addEventListener("input", () => {
    localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);
  });

  /* =========================
     JSON保存
  ========================= */
  saveJsonBtn.addEventListener("click", () => {
    localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);

    const data = {};
      getTabButtons().forEach(btn => {
      const id = btn.dataset.id;
      data[id] = {
        name: localStorage.getItem(NAME_PREFIX + id) || id,
        text: localStorage.getItem(KEY_PREFIX + id) || ""
      };
    });

    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    const ts = makeTimestamp();
    a.download = `simple-memo-backup_${ts}.json`;
    a.click();
  });

  /* =========================
     ドラッグ&ドロップ
  ========================= */
  let dragged = null;

  tabContainer.addEventListener("dragstart", e => {
    if (!e.target.dataset.id) return;
    dragged = e.target;
    e.target.classList.add("dragging");
  });

  tabContainer.addEventListener("dragend", e => {
    e.target.classList.remove("dragging");
    saveTabOrder();
  });

  tabContainer.addEventListener("dragover", e => {
    e.preventDefault();
    const target = e.target.closest(".tab-btn[data-id]");
    if (!target || target === dragged) return;

    const rect = target.getBoundingClientRect();
    const next = (e.clientX - rect.left) > rect.width / 2;
    tabContainer.insertBefore(dragged, next ? target.nextSibling : target);
  });

  function saveTabOrder() {
    const order = [...tabContainer.querySelectorAll(".tab-btn[data-id]")]
      .map(btn => btn.dataset.id);
    localStorage.setItem(ORDER_KEY, JSON.stringify(order));
  }

  /* =========================
     初期化
  ========================= */
  loadTabOrder();
  loadTabNames();
  switchTab(currentTab, true); 

});

/* =========================
   Ctrl + S 無効化
========================= */
document.addEventListener("keydown", (e) => {

  // Ctrl + S / Cmd + S(Mac対応)
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
    e.preventDefault();

    // 任意:自分の保存処理を呼ぶならここ
    // document.getElementById("saveJsonBtn")?.click();

    console.log("Ctrl+S blocked");
  }

});

// ===== 幅切り替え =====
const toggleWidthBtn = document.getElementById("toggleWidthBtn");
const WIDTH_MODE_KEY = "simple_memo_width_mode";

// 復元
if (localStorage.getItem(WIDTH_MODE_KEY) === "narrow") {
  document.body.classList.add("narrow");
}

// クリックで切り替え
toggleWidthBtn.addEventListener("click", () => {
  document.body.classList.toggle("narrow");

  if (document.body.classList.contains("narrow")) {
    localStorage.setItem(WIDTH_MODE_KEY, "narrow");
  } else {
    localStorage.setItem(WIDTH_MODE_KEY, "full");
  }
});

 style.css

/* =========================
   リセット
========================= */
* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

body {
  font-family: system-ui, -apple-system, "Segoe UI",
               "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
  background: #ffffff;
}

/* =========================
   浮遊タブ全体(背景なし・右寄せ)
========================= */
.floating-tabs {
  position: fixed;
  top: 10px;
  right: 10px;
  z-index: 1000;

  display: flex;
  align-items: center;
  justify-content: flex-end; /* ★ 全部右寄せ */
  gap: 6px;

  padding: 0;               /* ★ 背景がないので余白も不要 */
  background: none;         /* ★ 薄い背景を削除 */
  backdrop-filter: none;    /* ★ ぼかしも不要 */
}

/* タブリスト(右側に並ぶだけ) */
.tab-list {
  display: flex;
  gap: 6px;
}

/* 右端固定ツール(そのまま) */
.tab-tools {
  display: flex;
  gap: 6px;
  margin-left: 6px; /* タブとの区切り */
}

/* =========================
   ボタン共通
========================= */
.tab-btn {
  min-width: 34px;
  height: 26px;
  padding: 0 8px;

  border-radius: 6px;
  border: none;
  cursor: pointer;

  font-size: 12px;
  line-height: 1;
  white-space: nowrap;

  background: #4b5563;
  color: #ffffff;
}

/* アクティブタブ */
.tab-btn.active {
  background: #3b82f6;
  font-weight: 600;
}

/* 保存・フォルダは少し目立たせる */
.tab-tools .tab-btn {
  background: #111827;
}

.tab-tools .tab-btn:hover {
  background: #1f2937;
}

/* ドラッグ中 */
.tab-btn.dragging {
  opacity: 0.5;
}

/* =========================
   メモエリア
========================= */
textarea {
  position: fixed;
  inset: 0;

  width: 100%;
  height: 100%;

  border: none;
  resize: none;
  outline: none;

  /* ★ 上部にタブ分の余白を確保 */
  padding: 56px 38px 20px 25px;

  font-size: 16px;
  line-height: 1.7;
  background: #ffffff;
}

/* =========================
   全タブ検索
========================= */
.search-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.4);
  z-index: 9999;
  display: flex;
  justify-content: center;
  align-items: center;
}

.search-box {
  background: #fff;
  width: 480px;
  max-width: 90%;
  border-radius: 10px;
  padding: 16px;
}

#searchInput {
  width: 100%;
  padding: 10px;
  font-size: 16px;
  margin-bottom: 10px;
}

#searchResults {
  max-height: 260px;
  overflow-y: auto;
}

.search-item {
  padding: 6px 8px;
  border-bottom: 1px solid #eee;
  cursor: pointer;
  font-size: 14px;
}

.search-item:hover {
  background: #f3f4f6;
}

/* =========================
   検索ハイライト
========================= */
.search-highlight {
  background: #ffeb3b;
  color: #000;
  padding: 0 2px;
  border-radius: 2px;
}

/* ===== 幅制限モード ===== */
body.narrow textarea {
  max-width: 1000px;
  margin: 0 auto;
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  box-shadow: 0 0 0 1px #e5e7eb;
  padding:55px 20px 20px;
}

/* ボタンアイコン用(任意) */
.width-btn {
  background: #6366f1;
  font-weight: bold;
}