# bio-header2.html 変更点まとめ

`bio-header.html` をベースに `bio-header2.html` を作成し、  
`css/bio.css` と `js/bio.js` を更新しました。

---

## 1. HTML：ヘッダー／フッターの位置を明示するコメント追加

bio-header.html のコメントを `★★★` 付きに変更し、後から差し替えやすくしました。

### ヘッダー

```html
<!-- ★★★ ヘッダーエリア（後から変更予定） ★★★ ここから -->
<div class="container_general-_nosp">
  ...（ナビゲーション・検索モーダル）
</div>
<!-- ★★★ ヘッダーエリア（後から変更予定） ★★★ ここまで -->
```

### フッター

```html
<!-- ★★★ フッターエリア（後から変更予定） ★★★ ここから -->
<section class="footer-light site_footer" aria-label="サイトフッター">
  ...
</section>
<!-- ★★★ フッターエリア（後から変更予定） ★★★ ここまで -->
```

---

## 2. CSS：遠距離ジャンプ時の Loading overlay

`css/bio.css` 末尾に追加。年代を超えるジャンプ時に半透明オーバーレイ＋スピナーを表示します。

```css
.bio-timeline-loading {
  position: fixed;
  inset: 0;
  z-index: 9998;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(247, 248, 250, 0.85);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease, visibility 0.15s ease;
  pointer-events: none;
}

.bio-timeline-loading.is_active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.bio-timeline-loading__spinner {
  width: 36px;
  height: 36px;
  border: 3px solid var(--soft-border);
  border-top-color: var(--ink);
  border-radius: 50%;
  animation: bio-timeline-spin 0.6s linear infinite;
}

@keyframes bio-timeline-spin {
  to { transform: rotate(360deg); }
}
```

---

## 3. JS：Loading overlay の表示・非表示ヘルパー

`js/bio.js` に追加。DOM に overlay 要素を生成し、クラスで表示を切り替えます。

```js
let _loadingOverlayEl = null;

function ensureLoadingOverlay() {
  if (_loadingOverlayEl && _loadingOverlayEl.parentNode) return _loadingOverlayEl;
  const el = document.createElement('div');
  el.className = 'bio-timeline-loading';
  el.setAttribute('aria-hidden', 'true');
  el.setAttribute('aria-busy', 'false');
  const spinner = document.createElement('div');
  spinner.className = 'bio-timeline-loading__spinner';
  el.appendChild(spinner);
  document.body.appendChild(el);
  _loadingOverlayEl = el;
  return el;
}

function showLoadingOverlay() {
  const el = ensureLoadingOverlay();
  el.classList.add('is_active');
  el.setAttribute('aria-busy', 'true');
}

function hideLoadingOverlay() {
  if (!_loadingOverlayEl) return;
  _loadingOverlayEl.classList.remove('is_active');
  _loadingOverlayEl.setAttribute('aria-busy', 'false');
}
```

---

## 4. JS：スクロール共通ヘルパー

スクロールコンテナの取得と、要素へのスクロールを共通化しました。  
`navigateToHash` や年クリック処理で同じロジックを使えるようにしています。

```js
function getScrollContainer() {
  if (document.body.classList.contains('has_site_header')) {
    return document.getElementById('mainScrollColumn')
        || document.getElementById('diaryList');
  }
  return null;
}

function scrollToElement(anchorEl, behavior = 'smooth') {
  const scrollContainer = getScrollContainer();
  if (!scrollContainer || scrollContainer === document.documentElement) {
    anchorEl.scrollIntoView({ behavior, block: 'start' });
    return;
  }
  const ar = anchorEl.getBoundingClientRect();
  const cr = scrollContainer.getBoundingClientRect();
  const targetScrollTop = scrollContainer.scrollTop + (ar.top - cr.top) - 4;
  const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
  const clamped = Math.max(0, Math.min(targetScrollTop, maxScroll));
  scrollContainer.scrollTo({ top: clamped, behavior });
}
```

---

## 5. JS：年号クリック時のフロー改善（遠距離ジャンプ対応）

年代を超えるジャンプか同一年代内かを判定し、スクロール方式を切り替えます。

### 判定ロジック

```js
const focusDecade = Math.floor(
  Number(getEventBoxFocusYear() || activeYear) / 10
) * 10;
const crossDecade = Number.isFinite(focusDecade) && focusDecade !== decadeNum;
```

### 遠距離ジャンプ（年代を超える）

1. Loading 表示（未ロード時）
2. `ensureLoaded` → `waitForLayoutStable(80ms)`
3. Loading 非表示
4. `scrollToElement(anchor, 'auto')` で即座に着地
5. 150〜3000ms の 6 回リトライで画像読み込みによるズレを補正

### 同一年代内

1. `scrollToElement(anchor)` で smooth スクロール
2. 同じ 6 回リトライで補正

### リトライ処理

```js
[150, 350, 600, 1000, 1800, 3000].forEach((delayMs) => {
  setTimeout(() => {
    const anchorAgain = document.getElementById(`year-${String(y)}`);
    if (!anchorAgain) return;
    const ar = anchorAgain.getBoundingClientRect();
    const cr = scrollContainer.getBoundingClientRect();
    const off = ar.top - cr.top;
    if (Math.abs(off) > 2) {
      const targetScrollTop = scrollContainer.scrollTop + off - 4;
      const clamped = Math.max(0, Math.min(
        targetScrollTop,
        scrollContainer.scrollHeight - scrollContainer.clientHeight
      ));
      scrollContainer.scrollTo({ top: clamped, behavior: 'auto' });
    }
  }, delayMs);
});
```

---

## 6. JS：年代クリック時の Loading 対応

未ロード decade の場合に loading を表示します。

```js
const needsLoad = ensureLoaded
  && !state?.loadedDecades?.has(d)
  && (decadeCountFromIndex.get(d) || 0) > 0;
if (needsLoad) showLoadingOverlay();
try {
  if (ensureLoaded && decadeCountFromIndex.get(d) > 0) {
    await ensureLoaded(d);
    await waitForLayoutStable(80);
  }
  // ... 年代リスト展開処理 ...
} finally {
  if (needsLoad) hideLoadingOverlay();
}
```

---

## 7. JS：`navigateToHash` を mainScrollColumn 対応に統一

ハッシュ遷移（`#year-2008` など）でも、年クリックと同じスクロールロジックを使用。

```js
async function navigateToHash(state) {
  const hash = String(location.hash || '').replace(/^#/, '').trim();
  if (!hash) return;
  const decade = state.index ? pickDecadeFromHash(state.index) : NaN;
  const needsLoad = Number.isFinite(decade)
    && state.loadedDecades
    && !state.loadedDecades.has(decade);
  if (needsLoad) {
    showLoadingOverlay();
    try {
      await ensureDecadeLoaded(state, decade);
      await waitForLayoutStable(80);
    } finally {
      hideLoadingOverlay();
    }
  } else if (Number.isFinite(decade)) {
    await ensureDecadeLoaded(state, decade);
  }
  const target = document.getElementById(hash);
  if (target) { scrollToElement(target); return; }
  const mYear = hash.match(/^year-(\d{4})$/);
  if (mYear) {
    const a = document.getElementById(`year-${mYear[1]}`);
    if (a) scrollToElement(a);
  }
}
```

---

## 8. JS：スクロールスパイの改善

### 年が変わったときだけログ出力

```js
const yearChanged = y !== activeYear;
// ... setActiveYear, setActiveDecade ...
if (yearChanged) debugLogTimelineSync('スクロールスパイで更新', null, null);
```

### ログのスロットル（400ms 間隔）

```js
let _lastScrollSpyLogAt = 0;
function debugLogTimelineSync(source, ...) {
  if (source === 'スクロールスパイで更新') {
    const now = performance.now();
    if (now - _lastScrollSpyLogAt < 400) return;
    _lastScrollSpyLogAt = now;
  }
  // ...
}
```

### 初期ロード直後のスパイ抑制（2.5秒）

画像読み込みでスクロール位置が変動するため、ロードから 2.5 秒以内で先頭付近にいる場合はスパイを無視。

```js
const initAt = window.BIO_INITIAL_LOAD_AT;
if (Number.isFinite(initAt) && performance.now() - initAt < 2500) {
  const sc = getScrollContainer();
  if (sc && sc.scrollTop < 20) return;
}
```

### `scrollSpyIgnoreUntil` の延長

年クリック後の無視時間を 1.5秒 → **3.5秒** に延長。画像読み込みによる変動でスパイが上書きしないようにしています。

---

## 9. JS：デバッグログの拡充

### 最上部カードの年月日を表示

```js
function getTopVisibleCardDate() {
  const cards = Array.from(
    document.querySelectorAll('#diaryList [data-bind="diaryCard"]')
  ).filter((c) => !c.hidden);
  if (!cards.length) return null;
  const withTop = cards.map((c) => ({ card: c, top: c.getBoundingClientRect().top }));
  withTop.sort((a, b) => a.top - b.top);
  const topCard = withTop[0].card;
  const start = topCard.querySelector('[data-bind="diaryDate"]')?.textContent?.trim() || '';
  const end   = topCard.querySelector('[data-bind="diaryDateEnd"]')?.textContent?.trim() || '';
  const year  = String(topCard.dataset.diaryYear || '').trim();
  return { 年月日: start, 終了日: end || '—', 年: year };
}
```

### ログ出力項目の追加

```js
console.log('[Bio Timeline]', source, {
  クリックした年代: clickedDecade ?? '—',
  クリックした年号: clickedYear ?? '—',
  'タイムライン アクティブ 年代': activeDecade,
  'タイムライン アクティブ 年号': activeYear,
  'イベントボックス フォーカス年': focusYear ?? '—',
  '表示されているイベントカード（最上部）': topCardDate ?? '—',
  ...extra
});
```

---

## 変更ファイル一覧

| ファイル | 変更内容 |
|---------|---------|
| `bio-header2.html` | bio-header.html コピー＋ヘッダー／フッターコメント変更 |
| `css/bio.css` | Loading overlay のスタイル追加 |
| `js/bio.js` | Loading 制御、スクロール共通化、遠距離ジャンプ対応、スパイ改善、デバッグログ拡充 |
