{{- /* Homepage DayPage Redesign — Bear / 熊
     Mirrors the design prototype: Hero + AI Chat + Posts + Books + Projects + Travel + Subscribe + Footer
     Lang: en (default) / zh — detected from .Lang
*/ -}}

{{- $isZh := eq .Lang "zh" -}}
{{- $lang  := .Lang -}}
{{- $d     := site.Data.homepage -}}

<div class="homepage-root" id="hp-root" data-lang="{{ $lang }}">

  <main class="hp-page">

    {{/* ── Hero ─────────────────────────────────────────── */}}
    <section class="hp-hero" id="hp-about">
      {{/* Ambient 3D field — sits behind both columns, purely decorative.
           Stays empty (transparent) on phones / reduced-motion / no-WebGL. */}}
      <div class="hp-hero-field" id="hp-hero-field" aria-hidden="true"></div>
      <div>
        <div class="hp-hero-meta-row">
          <span class="hp-status-pill" id="hp-status-pill">
            <span class="hp-live-dot"></span>
            <span id="hp-status-text">{{ if $isZh }}在线 · 漫游中{{ else }}Online · Roaming{{ end }}</span>
          </span>
          <span class="hp-entry-label">ENTRY · <span id="hp-entry-date">{{ now.Format "2006/01/02" }}</span></span>
        </div>

        <h1>
          <span class="hp-hero-name-row">
            <span class="hp-hero-name-text">
              {{ $d.hero.nameEn }} <span class="hp-accent">{{ $d.hero.nameLast }}</span>.
            </span>
            <span class="hp-hero-chibi" aria-hidden="true">
              <svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M22 95 Q22 70 50 70 Q78 70 78 95 Z" fill="#FAF8F6"/>
                <ellipse cx="50" cy="68" rx="9" ry="3" fill="#E8DCCA" opacity="0.6"/>
                <circle cx="50" cy="44" r="22" fill="#FFE4C4"/>
                <path d="M28 42 Q28 22 50 20 Q72 22 72 42 Q72 36 64 32 Q56 28 50 30 Q44 28 36 32 Q28 36 28 42 Z" fill="#2B1810"/>
                <circle cx="32" cy="26" r="5" fill="#2B1810"/>
                <circle cx="68" cy="26" r="5" fill="#2B1810"/>
                <circle cx="32" cy="26" r="2.5" fill="#8B5A3C"/>
                <circle cx="68" cy="26" r="2.5" fill="#8B5A3C"/>
                <circle cx="42" cy="46" r="2.4" fill="#2B1810"/>
                <circle cx="58" cy="46" r="2.4" fill="#2B1810"/>
                <circle cx="42.7" cy="45.4" r="0.8" fill="#FFFFFF"/>
                <circle cx="58.7" cy="45.4" r="0.8" fill="#FFFFFF"/>
                <circle cx="38" cy="52" r="2.2" fill="#FFB3A1" opacity="0.6"/>
                <circle cx="62" cy="52" r="2.2" fill="#FFB3A1" opacity="0.6"/>
                <path d="M46 54 Q50 58 54 54" stroke="#2B1810" stroke-width="1.4" stroke-linecap="round" fill="none"/>
              </svg>
            </span>
          </span>
          <span class="hp-hero-name-zh">{{ if $isZh }}{{ $d.hero.nameZh }} · {{ $d.hero.nickZh }}{{ else }}熊 · {{ $d.hero.nickZh }}{{ end }}</span>
        </h1>

        <div class="hp-hero-tag">
          {{- range $i, $tag := $d.hero.tagline }}{{ if $i }}<span class="hp-sep">/</span>{{ end }}<span>{{ $tag }}</span>{{ end }}
        </div>

        <p class="hp-hero-bio">
          {{ if $isZh }}{{ $d.hero.bioZh | safeHTML }}{{ else }}{{ $d.hero.bioEn | safeHTML }}{{ end }}
        </p>

        {{- if site.Params.socialIcons }}
        <div class="hp-hero-socials">
          {{- range site.Params.socialIcons }}
          {{- if eq .name "wechat" }}
          {{- $qrSrc := .qr | relURL -}}
          {{- with resources.Get "wechat.jpg" -}}
            {{- $prod := (hugo.IsProduction | or (eq site.Params.env "production")) -}}
            {{- if and hugo.IsExtended $prod -}}
              {{- $qrSrc = (.Resize "440x webp q88").RelPermalink -}}
            {{- else -}}
              {{- $qrSrc = (.Resize "440x q88").RelPermalink -}}
            {{- end -}}
          {{- end -}}
          <button type="button" class="hp-social-icon hp-social-icon--wechat"
            title="{{ .title | default "微信 / WeChat" }}"
            aria-label="{{ .title | default "微信 / WeChat" }}"
            aria-haspopup="dialog"
            data-wechat-id="{{ .wechatID }}"
            data-wechat-qr="{{ $qrSrc }}"
            onclick="openWechatContact(this)">
            {{ partial "svg.html" . }}
          </button>
          {{- else }}
          <a class="hp-social-icon" href="{{ trim .url " " | safeURL }}" target="_blank" rel="noopener noreferrer" title="{{ .name | title }}">
            {{ partial "svg.html" . }}
          </a>
          {{- end }}
          {{- end }}
        </div>
        {{- end }}
      </div>

      {{- $bubblesENData := slice -}}
      {{- $angles := slice -90 -30 30 90 150 210 -}}
      {{- $dists  := slice 1.0 1.05 0.95 1.0 1.05 0.95 -}}
      {{- range $i, $text := $d.ai.bubblesEn -}}
        {{- $bubblesENData = $bubblesENData | append (dict "text" $text "angle" (index $angles $i) "dist" (index $dists $i)) -}}
      {{- end -}}
      {{- $bubblesZHData := slice -}}
      {{- range $i, $text := $d.ai.bubblesZh -}}
        {{- $bubblesZHData = $bubblesZHData | append (dict "text" $text "angle" (index $angles $i) "dist" (index $dists $i)) -}}
      {{- end -}}
      <div id="hp-bear-ai" class="hp-bear-orbit-wrap" data-lang="{{ $lang }}"
        data-bubbles-en='{{ $bubblesENData | jsonify }}'
        data-bubbles-zh='{{ $bubblesZHData | jsonify }}'
        data-sys-en="{{ $d.ai.sysPromptEn }}"
        data-sys-zh="{{ $d.ai.sysPromptZh }}">
        {{/* Persistent 3D stage: the Three.js canvas lives here and is NEVER
             torn down on orbit↔chat swaps. orbit and chat are DOM layers
             rendered into #hp-bear-layer on top of it, so the same AI orb
             flows continuously between states (recedes behind the chat
             glass rather than disappearing). aria-hidden: purely ambient. */}}
        <div id="hp-bear-stage" class="hp-bear-stage" aria-hidden="true"></div>
        <div id="hp-bear-layer" class="hp-bear-layer"></div>
      </div>
    </section>

    <div class="hp-divider-fade"></div>

    {{/* ── Posts ───────────────────────────────────────── */}}
    <section class="hp-section" id="hp-posts">
      <div class="hp-section-head">
        <span class="hp-section-num">01</span>
        <span class="hp-section-label">{{ if $isZh }}最新信号{{ else }}Latest Signals{{ end }}</span>
        <h2 class="hp-section-title">
          {{ if $isZh }}正在写的{{ else }}Writing{{ end }}<span class="hp-zh">{{ if $isZh }}{{ else }}文章{{ end }}</span>
        </h2>
        <a class="hp-section-action" href="{{ "/articles/" | relLangURL }}">
          {{ if $isZh }}所有文章{{ else }}All Posts{{ end }}
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
        </a>
      </div>

      {{- $pages    := (where (where site.RegularPages "Lang" $lang) "Section" "in" site.Params.mainSections).ByDate.Reverse }}
      {{- $featured := index $pages 0 }}
      {{- $recent   := after 1 $pages | first 2 }}
      {{- $archive  := after 3 $pages | first 4 }}

      <div class="hp-posts-grid">
        {{- with $featured }}
        <a class="hp-post-card hp-featured" href="{{ .RelPermalink }}">
          <div class="hp-post-meta">
            <span class="hp-cat">{{ with .Params.categories }}{{ index . 0 }}{{ end }}</span>
            <span>{{ .Date.Format "Jan 02" | upper }}</span>
            <span>·</span><span>{{ .ReadingTime }} min</span>
          </div>
          <h3 class="hp-post-title">{{ .Title }}</h3>
          <p class="hp-post-excerpt">{{ .Summary | plainify | truncate 160 }}</p>
          <div class="hp-post-foot"><span>READ ESSAY</span><span class="hp-post-arrow">↗</span></div>
        </a>
        {{- end }}
        {{- range $recent }}
        <a class="hp-post-card" href="{{ .RelPermalink }}">
          <div class="hp-post-meta">
            <span class="hp-cat">{{ with .Params.categories }}{{ index . 0 }}{{ end }}</span>
            <span>{{ .Date.Format "Jan 02" | upper }}</span>
          </div>
          <h3 class="hp-post-title">{{ .Title }}</h3>
          <p class="hp-post-excerpt">{{ .Summary | plainify | truncate 120 }}</p>
          <div class="hp-post-foot"><span>{{ .ReadingTime }} min</span><span class="hp-post-arrow">↗</span></div>
        </a>
        {{- end }}
      </div>

      <div class="hp-posts-list">
        {{- range $archive }}
        <a class="hp-posts-list-row" href="{{ .RelPermalink }}">
          <span class="hp-posts-list-date">{{ .Date.Format "Jan 02" | upper }}</span>
          <span class="hp-posts-list-cat">{{ with .Params.categories }}{{ index . 0 }}{{ end }}</span>
          <span class="hp-posts-list-title">{{ .Title }}</span>
          <span class="hp-posts-list-arrow">→</span>
        </a>
        {{- end }}
      </div>

      <div class="hp-cat-row">
        <a class="hp-cat-tile" href="{{ "/ai-technology/" | relLangURL }}">
          <div class="hp-cat-tile-icon">⚡</div>
          <div class="hp-cat-tile-name">AI / Tech</div>
          <div class="hp-cat-tile-zh">技术</div>
          <div class="hp-cat-tile-count"><span>ENTRIES</span><strong>{{ len (where site.RegularPages "Section" "ai-technology") }}</strong></div>
        </a>
        <a class="hp-cat-tile" href="{{ "/growth/" | relLangURL }}">
          <div class="hp-cat-tile-icon">🌱</div>
          <div class="hp-cat-tile-name">Growth</div>
          <div class="hp-cat-tile-zh">成长</div>
          <div class="hp-cat-tile-count"><span>ENTRIES</span><strong>{{ len (where site.RegularPages "Section" "growth") }}</strong></div>
        </a>
        <a class="hp-cat-tile" href="{{ "/travel/" | relLangURL }}">
          <div class="hp-cat-tile-icon">🌐</div>
          <div class="hp-cat-tile-name">Travel</div>
          <div class="hp-cat-tile-zh">旅行</div>
          <div class="hp-cat-tile-count"><span>ENTRIES</span><strong>{{ len site.Taxonomies.tags.travel }}</strong></div>
        </a>
        <a class="hp-cat-tile" href="{{ "/projects/" | relLangURL }}">
          <div class="hp-cat-tile-icon">📦</div>
          <div class="hp-cat-tile-name">Projects</div>
          <div class="hp-cat-tile-zh">项目</div>
          <div class="hp-cat-tile-count"><span>ENTRIES</span><strong>{{ len (where site.RegularPages "Section" "projects") }}</strong></div>
        </a>
      </div>
    </section>

    <div class="hp-divider-fade"></div>

    {{/* ── Books ──────────────────────────────────────── */}}
    <section class="hp-section" id="hp-books">
      <div class="hp-section-head">
        <span class="hp-section-num">02</span>
        <span class="hp-section-label">{{ if $isZh }}我的书{{ else }}Books · Travelogues{{ end }}</span>
        <h2 class="hp-section-title">{{ if $isZh }}我的旅行书{{ else }}Field Notebooks{{ end }}<span class="hp-zh">{{ if $isZh }}{{ else }}游记{{ end }}</span></h2>
      </div>
      <div class="hp-books-grid">
        {{- range $d.books }}
        <a class="hp-book" href="{{ .url }}" target="_blank" rel="noopener">
          <div class="hp-book-cover {{ .coverClass }}">
            <div class="hp-book-cover-label">A FIELD NOTEBOOK</div>
            <div>
              <div class="hp-book-cover-title">{{ if $isZh }}{{ .titleZh }}{{ else }}{{ .titleEn }}{{ end }}</div>
              <div class="hp-book-cover-sub">{{ if $isZh }}{{ .subtitleZh }}{{ else }}{{ .subtitleEn }}{{ end }}</div>
            </div>
            <div class="hp-book-cover-foot"><span>VOL.{{ .volume }}</span><span>BEAR · {{ .year }}</span></div>
          </div>
          <div class="hp-book-info">
            <div class="hp-book-stamp">{{ .stamp }}</div>
            <div class="hp-book-meta"><strong>FIELD NOTES</strong><span>·</span><span>{{ .coords }}</span></div>
            <h3 class="hp-book-title">{{ if $isZh }}{{ .titleZh }}{{ else }}{{ .titleEn }}{{ end }}</h3>
            <div class="hp-book-zh">{{ .titleZh }} · {{ .subtitleEn }}</div>
            <p class="hp-book-excerpt">"{{ if $isZh }}{{ .excerptZh }}{{ else }}{{ .excerptEn }}{{ end }}"</p>
            <div class="hp-book-stats">
              <div class="hp-book-stat"><span class="hp-book-stat-num">{{ printf "%02d" .chapters }}</span><span class="hp-book-stat-label">Chapters</span></div>
              <div class="hp-book-stat"><span class="hp-book-stat-num">{{ .pages }}</span><span class="hp-book-stat-label">Pages</span></div>
              <div class="hp-book-stat"><span class="hp-book-stat-num">{{ printf "%02d" .cities }}</span><span class="hp-book-stat-label">Cities</span></div>
              <span class="hp-book-cta">READ ↗</span>
            </div>
          </div>
        </a>
        {{- end }}
      </div>
    </section>

    <div class="hp-divider-fade"></div>

    {{/* ── Projects ────────────────────────────────────── */}}
    <section class="hp-section" id="hp-projects">
      <div class="hp-section-head">
        <span class="hp-section-num">03</span>
        <span class="hp-section-label">{{ if $isZh }}产品 / 开源{{ else }}Products &amp; Open Source{{ end }}</span>
        <h2 class="hp-section-title">{{ if $isZh }}正在构建{{ else }}Building{{ end }}<span class="hp-zh">{{ if $isZh }}{{ else }}项目{{ end }}</span></h2>
        <a class="hp-section-action" href="https://github.com/telepace" target="_blank" rel="noopener">
          GITHUB
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3"/></svg>
        </a>
      </div>
      <div class="hp-projects-featured">
        {{- range $d.projects.featured }}
        <a class="hp-project-featured {{ .style }}" href="{{ .url }}" target="_blank" rel="noopener">
          <div class="hp-project-tag">{{ if $isZh }}{{ .tagZh }}{{ else }}{{ .tagEn }}{{ end }}</div>
          <h3 class="hp-project-name">{{ .name }}</h3>
          <p class="hp-project-desc">{{ if $isZh }}{{ .descZh }}{{ else }}{{ .descEn }}{{ end }}</p>
          <div class="hp-project-foot">
            <span class="hp-project-lang">{{ .lang }}</span>
            <span class="hp-project-cta">{{ if $isZh }}{{ .visitZh }}{{ else }}{{ .visitEn }}{{ end }} ↗</span>
          </div>
        </a>
        {{- end }}
      </div>
      <div class="hp-repos-grid">
        {{- range $d.projects.repos }}
        <a class="hp-repo" href="https://github.com/{{ .org }}/{{ .name }}" target="_blank" rel="noopener">
          <div class="hp-repo-head">
            <div class="hp-repo-icon">📦</div>
            <div class="hp-repo-name"><span class="hp-org">{{ .org }}/</span>{{ .name }}</div>
          </div>
          <p class="hp-repo-desc">{{ .desc }}</p>
          <div class="hp-repo-foot">
            <span><span class="hp-repo-lang-dot hp-lang-{{ .lc }}"></span>{{ .lang }}</span>
          </div>
        </a>
        {{- end }}
      </div>
    </section>

    <div class="hp-divider-fade"></div>

    {{/* ── Writing Activity ─────────────────────────────── */}}
    {{- $allPages := (where (where site.RegularPages "Lang" $lang) "Date" "gt" (time "1970-01-01")).ByDate.Reverse -}}
    {{- $thisYear := now.Year -}}
    {{- $thisYearPages := where $allPages "Date.Year" $thisYear -}}
    {{- $firstYear := $thisYear -}}
    {{- range $allPages -}}{{- if lt .Date.Year $firstYear -}}{{- $firstYear = .Date.Year -}}{{- end -}}{{- end -}}
    {{- $yearsActive := add (sub $thisYear $firstYear) 1 -}}
    {{- $allPosts := slice -}}
    {{- range $allPages -}}
      {{- $allPosts = $allPosts | append (dict "date" (.Date.Format "2006-01-02") "title" .Title "url" .RelPermalink) -}}
    {{- end -}}
    {{- $recentPosts := $allPages | first 8 -}}
    <section class="hp-section" id="hp-travel" data-post-dates='{{ $allPosts | jsonify }}'>
      <div class="hp-section-head">
        <span class="hp-section-num">04</span>
        <span class="hp-section-label">{{ if $isZh }}写作{{ else }}Writing{{ end }}</span>
        <h2 class="hp-section-title">{{ if $isZh }}发布记录{{ else }}Post Activity{{ end }}<span class="hp-zh">{{ if $isZh }}{{ else }}写作{{ end }}</span></h2>
        <a class="hp-section-action" href="{{ "/archives/" | relLangURL }}">
          {{ if $isZh }}完整归档{{ else }}All Archives{{ end }}
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
        </a>
      </div>
      <div class="hp-travel-block">
        <div>
          <div class="hp-travel-stats">
            <div><div class="hp-travel-stat-num">{{ len $allPages }}</div><div class="hp-travel-stat-label">{{ if $isZh }}总文章{{ else }}Posts{{ end }}</div></div>
            <div><div class="hp-travel-stat-num">{{ len $thisYearPages }}</div><div class="hp-travel-stat-label">{{ if $isZh }}今年{{ else }}This Year{{ end }}</div></div>
            <div><div class="hp-travel-stat-num">{{ $yearsActive }}</div><div class="hp-travel-stat-label">{{ if $isZh }}写作年数{{ else }}Years{{ end }}</div></div>
            <div><div class="hp-travel-stat-num">{{ len (site.Taxonomies.categories) }}</div><div class="hp-travel-stat-label">{{ if $isZh }}分类{{ else }}Topics{{ end }}</div></div>
          </div>
          <div class="hp-travel-cities">
            {{- range $i, $p := $recentPosts -}}
              {{- if $i }}<span class="hp-city-sep">·</span>{{ end -}}
              <a href="{{ $p.RelPermalink }}" class="hp-recent-post-link">{{ $p.Title }}</a>
            {{- end -}}
          </div>
        </div>
        <div class="hp-heatmap" id="hp-heatmap-wrap" data-archive-url="{{ "/archives/" | relLangURL }}" data-first-year="{{ $firstYear }}" data-this-year="{{ $thisYear }}">
          <div class="hp-heatmap-year-tabs" id="hp-heatmap-tabs">
            {{- range seq $thisYear -1 $firstYear -}}
              <button class="hp-heatmap-year-tab{{ if eq . $thisYear }} active{{ end }}" data-year="{{ . }}" aria-pressed="{{ if eq . $thisYear }}true{{ else }}false{{ end }}">{{ . }}</button>
            {{- end -}}
          </div>
          <div class="hp-heatmap-viewport">
            <div class="hp-heatmap-canvas" id="hp-heatmap-canvas">
              <div class="hp-heatmap-months" id="hp-heatmap-months" aria-hidden="true"></div>
              <div class="hp-heatmap-body">
                <div class="hp-heatmap-days" aria-hidden="true">
                  <span></span><span>MON</span><span></span><span>WED</span><span></span><span>FRI</span><span></span>
                </div>
                <div class="hp-heatmap-grid" id="hp-heatmap-grid"></div>
              </div>
            </div>
          </div>
          <div class="hp-heatmap-legend">
            <span>LESS</span>
            <span class="hp-heatmap-legend-cells">
              <span style="background:var(--heatmap-empty)"></span>
              <span style="background:var(--heatmap-low)"></span>
              <span style="background:var(--heatmap-mid)"></span>
              <span style="background:var(--heatmap-high)"></span>
            </span>
            <span>MORE</span>
            <span class="hp-heatmap-legend-year" id="hp-heatmap-legend-year">{{ $thisYear }} ACTIVITY</span>
          </div>
        </div>
      </div>
    </section>

    {{/* ── Subscribe ────────────────────────────────────── */}}
    <div class="hp-subscribe" id="hp-subscribe">
      <div>
        <div class="hp-subscribe-tag">SIGNAL · 信号</div>
        <h3>{{ if $isZh }}别错过任何一个信号。{{ else }}Never miss a signal.{{ end }}</h3>
        <p>{{ if $isZh }}为构建者写作。AI、开源与漫游笔记，直接送到你的邮箱。已有 {{ $d.subscribe.count }}+ 读者。{{ else }}Built for builders. New posts on AI, open source &amp; nomad life — direct to your inbox. Joined by {{ $d.subscribe.count }}+ readers.{{ end }}</p>
      </div>
      <form class="hp-subscribe-form" onsubmit="return hpSubscribeSubmit(event,'{{ $lang }}')">
        <div class="hp-subscribe-input-row">
          <input class="hp-subscribe-input" type="email" id="hp-subscribe-email"
            placeholder="{{ if $isZh }}你的邮箱{{ else }}your@email.com{{ end }}" required>
          <button class="hp-subscribe-btn" type="submit" id="hp-subscribe-btn">
            {{ if $isZh }}订阅{{ else }}SUBSCRIBE{{ end }}
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
          </button>
        </div>
        <div class="hp-subscribe-meta">
          {{ if $isZh }}不发广告，随时退订{{ else }}No spam, unsubscribe anytime{{ end }}
          <span class="hp-subscribe-sep">·</span>
          <a class="hp-subscribe-rss" href="{{ "/index.xml" | relLangURL }}" target="_blank" rel="noopener" title="RSS Feed">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19.01 7.38 20 6.18 20C4.98 20 4 19.01 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1z"/></svg>
            RSS
          </a>
          {{- with (site.GetPage "/").OutputFormats.Get "JSONFeed" }}
          <a class="hp-subscribe-rss" href="{{ .RelPermalink }}" target="_blank" rel="noopener" title="JSON Feed 1.1">JSON</a>
          {{- end }}
          {{- with (site.GetPage "/").OutputFormats.Get "OPML" }}
          <a class="hp-subscribe-rss" href="{{ .RelPermalink }}" download title="{{ if $isZh }}OPML 订阅集 — 导入到任意 RSS 阅读器{{ else }}OPML — import all channels into any reader{{ end }}">OPML</a>
          {{- end }}
        </div>
      </form>

      {{/* ── By-channel feeds (compact, magazine-style) ── */}}
      <div class="hp-subscribe-channels">
        <span class="hp-subscribe-channels-label">{{ if $isZh }}按频道订阅{{ else }}By channel{{ end }}</span>
        <div class="hp-subscribe-channels-list">
          {{- $colors := slice "#3b82f6" "#10b981" "#a855f7" "#ec4899" "#06b6d4" "#eab308" -}}
          {{- range $i, $section := site.Sections -}}
            {{- $rss := $section.OutputFormats.Get "RSS" -}}
            {{- if $rss -}}
              {{- $title := $section.Title | default $section.Section -}}
              {{- $color := index $colors (mod $i (len $colors)) }}
          <a class="hp-channel-pill" href="{{ $rss.RelPermalink }}" target="_blank" rel="noopener" title="RSS · {{ $title }}" style="--ch-color: {{ $color }}">
            <span class="hp-channel-pill-dot" aria-hidden="true"></span>
            <span class="hp-channel-pill-name">{{ $title }}</span>
            <span class="hp-channel-pill-fmt">RSS</span>
          </a>
            {{- end -}}
          {{- end }}
        </div>
      </div>
    </div>

  </main>

  {{/* ── Footer ──────────────────────────────────────── */}}
  <footer class="hp-footer hp-page">
    <div class="hp-footer-grid">
      <div>
        <div class="hp-footer-brand">BEAR · 熊</div>
        <p class="hp-footer-bio">{{ if $isZh }}{{ $d.hero.footerBioZh }}{{ else }}{{ $d.hero.footerBioEn }}{{ end }}</p>
      </div>
      <div class="hp-footer-col">
        <h5>{{ if $isZh }}导航{{ else }}Navigate{{ end }}</h5>
        <ul>
          <li><a href="{{ "/start-here/" | relLangURL }}">Start Here</a></li>
          <li><a href="{{ "/about/" | relLangURL }}">About</a></li>
          <li><a href="{{ "/articles/" | relLangURL }}">All Posts</a></li>
          <li><a href="{{ "/archives/" | relLangURL }}">Archives</a></li>
        </ul>
      </div>
      <div class="hp-footer-col">
        <h5>{{ if $isZh }}书与项目{{ else }}Books &amp; Builds{{ end }}</h5>
        <ul>
          {{- range $d.books }}
          <li><a href="{{ .url }}" target="_blank" rel="noopener">{{ .titleEn }} · {{ .titleZh }}</a></li>
          {{- end }}
          {{- range $d.projects.featured }}
          <li><a href="{{ .url }}" target="_blank" rel="noopener">{{ .name }}</a></li>
          {{- end }}
        </ul>
      </div>
      <div class="hp-footer-col">
        <h5>{{ if $isZh }}联系{{ else }}Connect{{ end }}</h5>
        <ul>
          {{- range site.Params.socialIcons }}
          {{- if eq .name "wechat" }}
          {{- $qrSrc := .qr | relURL -}}
          {{- with resources.Get "wechat.jpg" -}}
            {{- $prod := (hugo.IsProduction | or (eq site.Params.env "production")) -}}
            {{- if and hugo.IsExtended $prod -}}
              {{- $qrSrc = (.Resize "440x webp q88").RelPermalink -}}
            {{- else -}}
              {{- $qrSrc = (.Resize "440x q88").RelPermalink -}}
            {{- end -}}
          {{- end -}}
          <li><button type="button" class="hp-footer-wechat" aria-haspopup="dialog"
            data-wechat-id="{{ .wechatID }}" data-wechat-qr="{{ $qrSrc }}"
            onclick="openWechatContact(this)">{{ .title | default (.name | title) }}</button></li>
          {{- else }}
          <li><a href="{{ trim .url " " | safeURL }}" target="_blank" rel="noopener noreferrer">{{ .title | default (.name | title) }}</a></li>
          {{- end }}
          {{- end }}
          <li><a href="{{ "/index.xml" | relLangURL }}">RSS</a></li>
          {{- with (site.GetPage "/").OutputFormats.Get "JSONFeed" }}
          <li><a href="{{ .RelPermalink }}">JSON Feed</a></li>
          {{- end }}
          {{- with (site.GetPage "/").OutputFormats.Get "OPML" }}
          <li><a href="{{ .RelPermalink }}" download>OPML (all)</a></li>
          {{- end }}
        </ul>
      </div>
    </div>
    <div class="hp-footer-bottom">
      <span>© {{ now.Format "2006" }} XINWEI XIONG · THE QUIET COLLECTOR</span>
      <div class="hp-footer-bottom-right">
        <span>BUILT WITH SIGNAL</span>
        <span>{{ $d.hero.siteVersion }} · {{ now.Format "2006" }}</span>
      </div>
    </div>
  </footer>


</div><!-- .homepage-root -->

<script>
(function() {
  var root = document.getElementById('hp-bear-ai');
  if (!root) return;
  // orbit/chat DOM render into #hp-bear-layer; the Three.js canvas in
  // #hp-bear-stage underneath is left untouched across swaps.
  var layer = document.getElementById('hp-bear-layer') || root;
  var isZh = root.dataset.lang === 'zh';
  var suggestions = JSON.parse(isZh ? root.dataset.bubblesZh : root.dataset.bubblesEn);
  var chatOpen = false, messages = [], loading = false, streaming = false;

  // Imperative handle to the 3D orb, registered by the module loader below
  // once (and if) it mounts. Guarded everywhere so the widget degrades to
  // the static/no-WebGL case without errors.
  var bearOrb = null;
  window.__hpBearOrbReady = function(handle) { bearOrb = handle; };
  function orb(method) {
    if (bearOrb && typeof bearOrb[method] === 'function') bearOrb[method]();
  }
  function orbSet(method, val) {
    if (bearOrb && typeof bearOrb[method] === 'function') bearOrb[method](val);
  }

  var CHIBI = '<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">' +
    '<path d="M22 95 Q22 70 50 70 Q78 70 78 95 Z" fill="#FAF8F6"/>' +
    '<ellipse cx="50" cy="68" rx="9" ry="3" fill="#E8DCCA" opacity="0.6"/>' +
    '<circle cx="50" cy="44" r="22" fill="#FFE4C4"/>' +
    '<path d="M28 42 Q28 22 50 20 Q72 22 72 42 Q72 36 64 32 Q56 28 50 30 Q44 28 36 32 Q28 36 28 42 Z" fill="#2B1810"/>' +
    '<circle cx="32" cy="26" r="5" fill="#2B1810"/><circle cx="68" cy="26" r="5" fill="#2B1810"/>' +
    '<circle cx="32" cy="26" r="2.5" fill="#8B5A3C"/><circle cx="68" cy="26" r="2.5" fill="#8B5A3C"/>' +
    '<circle cx="42" cy="46" r="2.4" fill="#2B1810"/><circle cx="58" cy="46" r="2.4" fill="#2B1810"/>' +
    '<circle cx="42.7" cy="45.4" r="0.8" fill="#FFFFFF"/><circle cx="58.7" cy="45.4" r="0.8" fill="#FFFFFF"/>' +
    '<circle cx="38" cy="52" r="2.2" fill="#FFB3A1" opacity="0.6"/><circle cx="62" cy="52" r="2.2" fill="#FFB3A1" opacity="0.6"/>' +
    '<path d="M46 54 Q50 58 54 54" stroke="#2B1810" stroke-width="1.4" stroke-linecap="round" fill="none"/></svg>';

  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

  // Render bear AI message with rich formatting
  function renderMsg(s) {
    var origin = window.location.origin;
    // Split into lines for paragraph-aware processing
    var lines = String(s).split('\n');
    var out = [];
    var i = 0;
    while (i < lines.length) {
      var line = lines[i];
      // Markdown link on its own line → article card
      var cardMatch = line.match(/^\s*\[([^\]]{1,120})\]\(((?:https?:\/\/|\/)[^)]{1,400})\)\s*$/);
      if (cardMatch) {
        var cardTitle = esc(cardMatch[1]);
        var cardUrl   = cardMatch[2].startsWith('/') ? origin + cardMatch[2] : cardMatch[2];
        var cardPath  = cardMatch[2].replace(/^https?:\/\/[^\/]+/, '').replace(/\/$/, '') || '/';
        out.push(
          '<a href="' + cardUrl + '" target="_blank" rel="noopener noreferrer" class="hp-ai-card">' +
            '<span class="hp-ai-card-icon" aria-hidden="true">' +
              '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
            '</span>' +
            '<span class="hp-ai-card-body">' +
              '<span class="hp-ai-card-title">' + cardTitle + '</span>' +
              '<span class="hp-ai-card-path">' + esc(cardPath) + '</span>' +
            '</span>' +
            '<span class="hp-ai-card-arrow" aria-hidden="true">↗</span>' +
          '</a>'
        );
        i++; continue;
      }
      // Empty line → paragraph break
      if (line.trim() === '') {
        if (out.length && out[out.length-1] !== '<p class="hp-ai-para">') out.push('</p><p class="hp-ai-para">');
        i++; continue;
      }
      // Inline Markdown links within text
      var inlined = esc(line)
        .replace(/\[([^\]]{1,120})\]\(((?:https?:\/\/|\/)[^)]{1,400})\)/g, function(_, t, u) {
          var href = u.startsWith('/') ? origin + u : u;
          return '<a href="' + href + '" target="_blank" rel="noopener noreferrer" class="hp-ai-link">' + t + ' ↗</a>';
        })
        .replace(/\*\*([^*]{1,200})\*\*/g, '<strong>$1</strong>');
      out.push(inlined + ' ');
      i++;
    }
    var body = out.join('');
    // Wrap plain text in a paragraph
    if (!body.startsWith('<a') && !body.startsWith('</p>')) body = '<p class="hp-ai-para">' + body.trimEnd() + '</p>';
    return body;
  }

  function renderOrbit() {
    var r = 155;
    var bubbles = suggestions.map(function(s,i) {
      var rad = s.angle * Math.PI / 180;
      var x = (Math.cos(rad) * r * s.dist).toFixed(1);
      var y = (Math.sin(rad) * r * s.dist).toFixed(1);
      return '<button class="hp-bear-bubble" style="left:calc(50% + '+x+'px);top:calc(50% + '+y+'px);animation-delay:'+(i*0.12)+'s" data-msg="'+esc(s.text)+'">'+s.text+'</button>';
    }).join('');

    // Orbit state: bring the orb forward (un-recess) — it's the hero again.
    orbSet('setRecessed', false);
    orbSet('setThinking', false);
    layer.innerHTML =
      '<div class="hp-bear-orbit" id="hp-orbit">'+
        '<svg class="hp-bear-orbit-rings" viewBox="-200 -200 400 400" aria-hidden="true">'+
          '<defs><radialGradient id="bGlow" cx="50%" cy="50%" r="50%">'+
            '<stop offset="0%" stop-color="rgba(93,48,0,0.18)"/>'+
            '<stop offset="60%" stop-color="rgba(93,48,0,0.04)"/>'+
            '<stop offset="100%" stop-color="rgba(93,48,0,0)"/>'+
          '</radialGradient></defs>'+
          '<circle cx="0" cy="0" r="190" fill="url(#bGlow)"/>'+
          '<circle cx="0" cy="0" r="180" fill="none" stroke="var(--border-subtle)" stroke-width="1" stroke-dasharray="2 6"/>'+
          '<circle cx="0" cy="0" r="140" fill="none" stroke="var(--border-subtle)" stroke-width="1"/>'+
          '<circle cx="0" cy="0" r="100" fill="none" stroke="var(--accent-border)" stroke-width="1" stroke-dasharray="1 3"/>'+
          '<line x1="0" y1="-180" x2="0" y2="-170" stroke="var(--fg-subtle)" stroke-width="1"/>'+
          '<line x1="0" y1="170" x2="0" y2="180" stroke="var(--fg-subtle)" stroke-width="1"/>'+
          '<line x1="-180" y1="0" x2="-170" y2="0" stroke="var(--fg-subtle)" stroke-width="1"/>'+
          '<line x1="170" y1="0" x2="180" y2="0" stroke="var(--fg-subtle)" stroke-width="1"/>'+
          '<circle class="orb-dot" cx="140" cy="-30" r="2" fill="var(--accent)"/>'+
          '<circle class="orb-dot orb-dot-2" cx="-130" cy="60" r="2" fill="var(--accent)"/>'+
          '<circle class="orb-dot orb-dot-3" cx="40" cy="160" r="1.5" fill="var(--fg-muted)"/>'+
        '</svg>'+
        bubbles+
        '<button class="hp-bear-avatar-btn" id="hp-avatar-btn" aria-label="'+(isZh?'与 Bear 对话':'Talk to Bear')+'">'+
          '<span class="hp-bear-avatar-pulse"></span>'+
          '<span class="hp-bear-avatar-pulse hp-bear-avatar-pulse-2"></span>'+
          '<span class="hp-bear-avatar-core"><span class="hp-bear-chibi">'+CHIBI+'</span></span>'+
          '<span class="hp-bear-avatar-status"><span class="hp-bear-avatar-status-dot"></span>'+(isZh?'在线':'ONLINE')+'</span>'+
        '</button>'+
        '<div class="hp-bear-hud hp-bear-hud-tl"><div class="hp-bear-hud-label">DIGITAL TWIN</div><div class="hp-bear-hud-val">{{ $d.ai.hudTwinVal }}</div></div>'+
        '<div class="hp-bear-hud hp-bear-hud-tr"><div class="hp-bear-hud-label">'+(isZh?'协议':'PROTOCOL')+'</div><div class="hp-bear-hud-val">{{ $d.ai.hudProtocolVal }}</div></div>'+
        '<div class="hp-bear-hud hp-bear-hud-bl"><div class="hp-bear-hud-label">'+(isZh?'语气':'VOICE')+'</div><div class="hp-bear-hud-val">{{ $d.ai.hudVoiceVal }}</div></div>'+
        '<div class="hp-bear-hud hp-bear-hud-br"><div class="hp-bear-hud-label">'+(isZh?'点击开始':'TAP · BEGIN')+'</div><div class="hp-bear-hud-val">↗ '+(isZh?'聊天':'CHAT')+'</div></div>'+
        '<div class="hp-bear-cta-line">'+(isZh?'点击头像 · 与 Bear 的数字分身对话':"Tap the orb · Talk to Bear's digital twin")+'</div>'+
      '</div>';

    document.getElementById('hp-avatar-btn').addEventListener('click', function() { orb('pulse'); openChat(null); });
    var orbit = document.getElementById('hp-orbit');
    orbit.addEventListener('mouseenter', function() { this.classList.add('is-hover'); });
    orbit.addEventListener('mouseleave', function() { this.classList.remove('is-hover'); });
    layer.querySelectorAll('.hp-bear-bubble').forEach(function(btn) {
      // Plain `click`, same as the avatar orb and the chat quick-buttons.
      // (An earlier pointerdown/pointerup+armed scheme swallowed taps: any
      // pointercancel — easily fired while the bubbles float / on a slight
      // drag — reset the flag so pointerup bailed, and `click` was never
      // listened for at all.) The bubbles are <button>s, so click already
      // covers mouse, touch and keyboard (Enter/Space) for free. The
      // :hover/:focus rule freezes the float so the target is steady.
      btn.addEventListener('click', function() { orb('pulse'); openChat(this.dataset.msg); });
    });
  }

  function renderChat() {
    var bodyHtml = '';
    if (messages.length === 0) {
      bodyHtml = '<div class="hp-bear-chat-msg hp-bear-chat-msg-bear"><div class="hp-bear-chat-bubble">'+(isZh?'嗨。在路上，但有空聊。问点什么？':"Hey. On the road, but happy to talk. What's on your mind?")+'</div><div class="hp-bear-chat-time">'+(isZh?'刚刚':'just now')+'</div></div>';
    } else {
      bodyHtml = messages.map(function(m, idx) {
        var cls = m.role==='user' ? 'hp-bear-chat-msg-user' : 'hp-bear-chat-msg-bear';
        var t   = m.role==='user' ? (isZh?'已送达':'delivered') : (isZh?'刚刚':'just now');
        var content = m.role==='user' ? esc(m.content) : renderMsg(m.content);
        // The last bear bubble, while a reply is still streaming, gets a
        // blinking caret so partial tokens read as "being typed".
        var streamCls = (streaming && m.role==='bear' && idx===messages.length-1) ? ' is-streaming' : '';
        return '<div class="hp-bear-chat-msg '+cls+'"><div class="hp-bear-chat-bubble'+streamCls+'">'+content+'</div><div class="hp-bear-chat-time">'+t+'</div></div>';
      }).join('');
    }
    if (loading) bodyHtml += '<div class="hp-bear-chat-msg hp-bear-chat-msg-bear"><div class="hp-bear-chat-bubble hp-bear-chat-bubble-typing"><span><i></i><i></i><i></i></span></div></div>';

    var quickHtml = suggestions.slice(0,3).map(function(s) {
      return '<button class="hp-bear-chat-quick-btn"'+(loading?' disabled':'')+" data-msg='"+esc(s.text)+"'>"+s.text+'</button>';
    }).join('');

    // Chat state: sink the orb behind the glass panel (it keeps flowing
    // through the backdrop blur), and mirror the loading flag onto the orb's
    // "thinking" ramp so it visibly computes while a reply streams in.
    orbSet('setRecessed', true);
    orbSet('setThinking', loading);
    layer.innerHTML =
      '<div class="hp-bear-chat">'+
        '<div class="hp-bear-chat-head">'+
          '<div class="hp-bear-chat-head-left">'+
            '<span class="hp-bear-chat-avatar">'+CHIBI+'<span class="hp-bear-chat-avatar-dot"></span></span>'+
            '<div>'+
              '<div class="hp-bear-chat-name">Bear <span class="hp-bear-chat-name-zh">· 熊</span></div>'+
              '<div class="hp-bear-chat-meta"><span class="hp-bear-chat-pulse-dot"></span>'+(loading?(isZh?'正在输入…':'typing…'):(isZh?'在线 · 通常几秒回复':'online · usually replies in seconds'))+'</div>'+
            '</div>'+
          '</div>'+
          '<div class="hp-bear-chat-actions">'+
            (messages.length?'<button class="hp-bear-chat-icon-btn" id="hp-chat-clear" title="Clear"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>':'')+
            '<button class="hp-bear-chat-icon-btn" id="hp-chat-close" title="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>'+
          '</div>'+
        '</div>'+
        '<div class="hp-bear-chat-body" id="hp-chat-body">'+bodyHtml+'</div>'+
        '<div class="hp-bear-chat-quick">'+quickHtml+'</div>'+
        '<div class="hp-bear-chat-input-row">'+
          '<input class="hp-bear-chat-input" id="hp-chat-input" placeholder="'+(isZh?'发条消息…':'Message Bear…')+'"'+(loading?' disabled':'')+'>'+
          '<button class="hp-bear-chat-send" id="hp-chat-send" disabled><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>'+
        '</div>'+
        '<div class="hp-bear-chat-foot"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.5 4.5L6 9l4.5 1.5L12 15l1.5-4.5L18 9l-4.5-1.5z"/></svg><span>'+(isZh?"AI 模拟 Bear 的语气 · 不是本人":"AI mimicking Bear's voice · not the human")+'</span></div>'+
      '</div>';

    var body = document.getElementById('hp-chat-body');
    if (body) body.scrollTop = body.scrollHeight;

    var inp  = document.getElementById('hp-chat-input');
    var send = document.getElementById('hp-chat-send');
    if (inp) {
      inp.addEventListener('input', function() { send.disabled = !this.value.trim() || loading; });
      inp.addEventListener('keydown', function(e) {
        if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); if (!loading && this.value.trim()) sendMsg(this.value); }
      });
      if (!loading) setTimeout(function(){ inp && inp.focus(); }, 100);
    }
    if (send) send.addEventListener('click', function() { var v=inp&&inp.value; if(v) sendMsg(v); });
    var clr = document.getElementById('hp-chat-clear');
    if (clr) clr.addEventListener('click', function() { messages=[]; renderChat(); });
    var cls = document.getElementById('hp-chat-close');
    if (cls) cls.addEventListener('click', function() { chatOpen=false; renderOrbit(); });
    layer.querySelectorAll('.hp-bear-chat-quick-btn').forEach(function(btn) {
      btn.addEventListener('click', function() { if(!loading) { orb('pulse'); sendMsg(this.dataset.msg); } });
    });
  }

  // Patch only the growing reply bubble in place. Calling renderChat() on
  // every streamed token rebuilt the whole chat via innerHTML, so the bear
  // message was a brand-new node each time and replayed the hpMsgIn enter
  // animation — the panel strobed. Here we mutate the existing last bear
  // bubble's HTML only: no node churn, no re-animation, smooth typewriter.
  function updateStreamingBubble(contentHtml) {
    var body = document.getElementById('hp-chat-body');
    if (!body) { renderChat(); return; }
    var bubbles = body.querySelectorAll('.hp-bear-chat-msg-bear .hp-bear-chat-bubble');
    var last = bubbles[bubbles.length - 1];
    if (!last) { renderChat(); return; }
    last.classList.add('is-streaming');
    last.innerHTML = contentHtml;
    // Keep the newest text in view without yanking if the user scrolled up.
    var nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 80;
    if (nearBottom) body.scrollTop = body.scrollHeight;
  }

  function openChat(initMsg) {
    chatOpen = true; renderChat();
    if (initMsg) sendMsg(initMsg);
  }

  function sendMsg(text) {
    text = String(text||'').trim();
    if (!text || loading) return;
    chatOpen = true;
    messages.push({ role:'user', content:text });
    loading = true;
    orbSet('setThinking', true);   // orb visibly "computes" while we wait
    renderChat();
    var sys = isZh ? root.dataset.sysZh : root.dataset.sysEn;
    var history = messages.slice(0,-1).map(function(m){ return { role: m.role==='user'?'user':'assistant', content: m.content }; });
    fetch('/.netlify/functions/blog-ai', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        question: text,
        language: isZh ? 'zh' : 'en',
        context: history,
        searchContext: sys
      })
    })
    .then(function(res) {
      if (!res.ok) throw new Error('api error');
      var ct = res.headers.get('content-type') || '';
      if (ct.includes('text/event-stream')) {
        var reader = res.body.getReader();
        var decoder = new TextDecoder();
        var reply = '';
        messages.push({ role:'bear', content:'' });
        loading = false;
        streaming = true;          // caret on the growing bubble; orb keeps thinking
        // Build the chat skeleton ONCE (this lays down the empty bear bubble);
        // every subsequent token patches that bubble in place — no re-render,
        // no flicker.
        renderChat();
        function pump() {
          return reader.read().then(function(chunk) {
            if (chunk.done) {
              streaming = false;
              orbSet('setThinking', false);
              renderChat();         // final pass: drops the caret, settles state
              return;
            }
            var lines = decoder.decode(chunk.value).split('\n');
            lines.forEach(function(line) {
              if (line.startsWith('data: ')) {
                var d = line.slice(6).trim();
                if (d && d !== '[DONE]') {
                  try {
                    var j = JSON.parse(d);
                    if (j.delta) {
                      reply += j.delta;
                      messages[messages.length-1].content = reply;
                      updateStreamingBubble(renderMsg(reply));  // in-place, no flicker
                    }
                  } catch(e) {}
                }
              }
            });
            return pump();
          });
        }
        return pump();
      }
      return res.json().then(function(d) {
        messages.push({ role:'bear', content: d.answer || d.content || (isZh?'嗯，走神了，再问一次？':"Lost my train of thought. Try again?") });
        loading = false; streaming = false; orbSet('setThinking', false); renderChat();
      });
    })
    .catch(function() {
      messages.push({ role:'bear', content: isZh?'网络有点问题，再试一次？':"Connection hiccup. Try again?" });
      loading = false; streaming = false; orbSet('setThinking', false); renderChat();
    });
  }

  renderOrbit();

  /* ── Build-time post dates → heatmap; RSS → status pill only ── */
  (function() {
    // 1. Heatmap — GitHub-style column-per-week grid, per-year view, rich
    //    interactive tooltip (hover / focus / click-to-pin), year switcher.
    (function() {
      var section  = document.getElementById('hp-travel');
      var wrap     = document.getElementById('hp-heatmap-wrap');
      var hg       = document.getElementById('hp-heatmap-grid');
      var months   = document.getElementById('hp-heatmap-months');
      var canvas   = document.getElementById('hp-heatmap-canvas');
      var tabs     = document.getElementById('hp-heatmap-tabs');
      var legendYr = document.getElementById('hp-heatmap-legend-year');
      if (!hg || !section || !wrap) return;

      var archiveBase = wrap.dataset.archiveUrl || '/archives/';
      var thisYear    = parseInt(wrap.dataset.thisYear, 10) || new Date().getFullYear();
      var MONTHS      = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];

      var rawPosts = [];
      try { rawPosts = JSON.parse(section.dataset.postDates || '[]'); } catch(e) {}

      // countMap: YYYY-MM-DD → count, postsMap: YYYY-MM-DD → [{title,url}]
      var countMap = {};
      var postsMap = {};
      rawPosts.forEach(function(p) {
        var d = typeof p === 'string' ? p : p.date;
        if (!d || d <= '1970-01-01') return;
        countMap[d] = (countMap[d] || 0) + 1;
        if (p.title) {
          postsMap[d] = postsMap[d] || [];
          postsMap[d].push({ title: p.title, url: p.url });
        }
      });

      // Local-timezone date key. (toISOString() is UTC: east of Greenwich it
      // shifts every key back one day — Jan 1 posts fell out of their year.)
      function pad2(n) { return n < 10 ? '0' + n : '' + n; }
      function keyOf(d) { return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); }
      var todayKey = keyOf(new Date());

      /* ── Tooltip: single element, interactive (links are clickable),
            delayed hide so the pointer can travel into it, click-to-pin
            for multi-post days. Appended inside the card so the DayPage
            CSS tokens resolve. ── */
      var tooltip = document.createElement('div');
      tooltip.className = 'hp-heat-tooltip';
      tooltip.setAttribute('role', 'tooltip');
      wrap.appendChild(tooltip);

      var hideTimer = null;
      var pinned = false;
      var anchorCell = null;

      function buildTip(key, cnt, posts) {
        tooltip.textContent = '';
        var d = new Date(key + 'T12:00:00');
        var dateEl = document.createElement('div');
        dateEl.className = 'hp-heat-tip-date';
        dateEl.textContent = d.toLocaleDateString(isZh ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'short', day: 'numeric' });
        tooltip.appendChild(dateEl);

        var countEl = document.createElement('div');
        if (cnt === 0) {
          countEl.className = 'hp-heat-tip-count is-empty';
          countEl.textContent = isZh ? '无发布' : 'No posts';
        } else {
          countEl.className = 'hp-heat-tip-count';
          countEl.textContent = isZh ? cnt + ' 篇文章' : cnt + (cnt > 1 ? ' posts' : ' post');
        }
        tooltip.appendChild(countEl);

        posts.forEach(function(p) {
          if (!p.url) return;
          var a = document.createElement('a');
          a.className = 'hp-heat-tip-link';
          a.href = p.url;
          a.textContent = p.title;
          tooltip.appendChild(a);
        });
      }

      function positionTip(cell) {
        var rect = cell.getBoundingClientRect();
        var tw = tooltip.offsetWidth  || 200;
        var th = tooltip.offsetHeight || 60;
        var left = rect.left + rect.width / 2 - tw / 2;
        var top  = rect.top - th - 10;
        if (left < 8) left = 8;
        if (left + tw > window.innerWidth - 8) left = window.innerWidth - tw - 8;
        if (top < 8) top = rect.bottom + 10;
        tooltip.style.left = left + 'px';
        tooltip.style.top  = top  + 'px';
      }

      function showTip(cell) {
        clearTimeout(hideTimer);
        var key = cell.dataset.date;
        buildTip(key, parseInt(cell.dataset.count, 10) || 0, postsMap[key] || []);
        tooltip.classList.add('is-visible');
        positionTip(cell);
        anchorCell = cell;
      }

      function hideTip() {
        tooltip.classList.remove('is-visible', 'is-pinned');
        pinned = false;
        anchorCell = null;
      }

      function scheduleHide() {
        if (pinned) return;
        clearTimeout(hideTimer);
        hideTimer = setTimeout(hideTip, 150);
      }

      tooltip.addEventListener('mouseenter', function() { clearTimeout(hideTimer); });
      tooltip.addEventListener('mouseleave', scheduleHide);
      document.addEventListener('click', function(e) {
        if (pinned && !tooltip.contains(e.target) && !(anchorCell && anchorCell === e.target)) hideTip();
      });
      document.addEventListener('keydown', function(e) { if (e.key === 'Escape') hideTip(); });
      // fixed-position tooltip goes stale the moment anything scrolls
      document.addEventListener('scroll', function() {
        if (tooltip.classList.contains('is-visible')) hideTip();
      }, true);
      window.addEventListener('resize', hideTip);

      /* ── Cell interaction (event delegation — one listener set for the
            ~370 cells instead of ~740 bound closures per render) ── */
      function cellOf(e) {
        var c = e.target.closest ? e.target.closest('.hp-heat-cell') : null;
        return (c && c.dataset.date) ? c : null;
      }

      hg.addEventListener('mouseover', function(e) {
        var c = cellOf(e);
        if (c && !pinned) showTip(c);
      });
      hg.addEventListener('mouseout', function(e) {
        if (cellOf(e)) scheduleHide();
      });
      hg.addEventListener('focusin', function(e) {
        var c = cellOf(e);
        if (c) showTip(c);
      });
      hg.addEventListener('focusout', function(e) {
        if (cellOf(e)) scheduleHide();
      });

      function activateCell(c) {
        var key = c.dataset.date;
        var posts = postsMap[key] || [];
        if (!posts.length) return;
        if (posts.length === 1 && posts[0].url) {
          window.location.href = posts[0].url;
          return;
        }
        // multi-post day: pin the tooltip so its links can be picked
        showTip(c);
        pinned = true;
        tooltip.classList.add('is-pinned');
      }

      hg.addEventListener('click', function(e) {
        var c = cellOf(e);
        if (c) activateCell(c);
      });
      hg.addEventListener('keydown', function(e) {
        if (e.key !== 'Enter' && e.key !== ' ') return;
        var c = cellOf(e);
        if (c) { e.preventDefault(); activateCell(c); }
      });

      /* ── Year render: column-per-week (grid-auto-flow: column in CSS),
            month labels positioned at their actual week columns ── */
      function renderYear(year) {
        hg.textContent = '';
        if (months) months.textContent = '';

        var jan1  = new Date(year, 0, 1);
        var start = new Date(year, 0, 1 - jan1.getDay());          // back to Sunday
        var dec31 = new Date(year, 11, 31);
        var end   = new Date(year, 11, 31 + (6 - dec31.getDay())); // forward to Saturday
        var totalDays = Math.round((end - start) / 86400000) + 1;  // always a multiple of 7

        var yearTotal = 0;
        var frag = document.createDocumentFragment();
        var monthMarks = [];

        for (var i = 0; i < totalDays; i++) {
          var cur = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
          var key = keyOf(cur);
          var inYear = cur.getFullYear() === year;
          var cnt = inYear ? (countMap[key] || 0) : 0;
          yearTotal += cnt;
          // absolute scale: 1 / 2 / 3+ posts a day — relative scaling washed
          // every active day into the same top shade in low-volume years
          var lvl = cnt >= 3 ? 3 : cnt;

          var cell = document.createElement('div');
          var cls = 'hp-heat-cell';
          if (lvl) cls += ' hp-heat-' + lvl;
          if (!inYear) cls += ' hp-heat-out';
          if (inYear && key > todayKey) cls += ' hp-heat-future';
          if (key === todayKey) cls += ' hp-heat-today';

          if (inYear && key <= todayKey) {
            cell.dataset.date = key;
            cell.dataset.count = cnt;
            if (cnt > 0) {
              cell.className = cls + ' has-posts';
              cell.tabIndex = 0;
              cell.setAttribute('role', 'button');
              var d = new Date(key + 'T12:00:00');
              cell.setAttribute('aria-label',
                d.toLocaleDateString(isZh ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric' }) +
                (isZh ? '，' + cnt + ' 篇文章' : ', ' + cnt + (cnt > 1 ? ' posts' : ' post')));
            } else {
              cell.className = cls;
            }
          } else {
            cell.className = cls;
          }

          if (inYear && cur.getDate() === 1) {
            monthMarks.push({ m: cur.getMonth(), week: Math.floor(i / 7) });
          }
          frag.appendChild(cell);
        }
        hg.appendChild(frag);

        // month labels aligned to their real week columns
        var cs = getComputedStyle(hg);
        var pitch = (parseFloat(cs.gridAutoColumns) || 10) + (parseFloat(cs.columnGap) || 3);
        if (months) {
          monthMarks.forEach(function(mk) {
            var s = document.createElement('span');
            s.textContent = MONTHS[mk.m];
            s.style.left = (mk.week * pitch) + 'px';
            months.appendChild(s);
          });
        }

        if (legendYr) {
          legendYr.textContent = year + ' · ' + yearTotal + (yearTotal === 1 ? ' POST' : ' POSTS');
        }

        // narrow viewports open on JAN — pan the current year to "today"
        // so the recent activity is what greets the visitor
        if (year === thisYear) {
          var vp = wrap.querySelector('.hp-heatmap-viewport');
          if (vp && vp.scrollWidth > vp.clientWidth) {
            var todayWeek = Math.floor(Math.round((new Date() - start) / 86400000) / 7);
            var railW = canvas ? (hg.getBoundingClientRect().left - canvas.getBoundingClientRect().left) : 32;
            vp.scrollLeft = Math.max(0, railW + (todayWeek + 2) * pitch - vp.clientWidth);
          }
        }
      }

      // Tab switching with a soft cross-fade
      if (tabs) {
        tabs.addEventListener('click', function(e) {
          var btn = e.target.closest('.hp-heatmap-year-tab');
          if (!btn) return;
          var yr = parseInt(btn.dataset.year, 10);
          tabs.querySelectorAll('.hp-heatmap-year-tab').forEach(function(b) {
            b.classList.toggle('active', b === btn);
            b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
          });
          hideTip();
          if (canvas) {
            canvas.classList.add('is-swapping');
            setTimeout(function() {
              renderYear(yr);
              canvas.classList.remove('is-swapping');
            }, 120);
          } else {
            renderYear(yr);
          }
        });
      }

      renderYear(thisYear);
    })();

    // 2. RSS fetch only for latest post title → status pill
    (function() {
      var rssUrl = isZh ? '/zh/index.xml' : '/index.xml';
      var CACHE_KEY = 'hp_rss_pill_' + (isZh ? 'zh' : 'en');
      var CACHE_TTL = 30 * 60 * 1000;

      function applyLatest(item) {
        var statusEl = document.getElementById('hp-status-text');
        var dateEl   = document.getElementById('hp-entry-date');
        if (statusEl && item.title) {
          var short = item.title.length > 28 ? item.title.slice(0,26) + '…' : item.title;
          statusEl.textContent = (isZh ? '最新 · ' : 'Latest · ') + short;
          var pill = document.getElementById('hp-status-pill');
          if (pill && item.href && item.href !== '#') {
            pill.style.cursor = 'pointer';
            pill.onclick = function() { window.open(item.href, '_blank'); };
            pill.title = item.title;
          }
        }
        if (dateEl && item.date) {
          var d = item.date;
          dateEl.textContent = d.getFullYear() + '/' +
            String(d.getMonth()+1).padStart(2,'0') + '/' +
            String(d.getDate()).padStart(2,'0');
        }
      }

      function parseFirst(xml) {
        var m = xml.match(/<item[\s\S]*?<\/item>/);
        if (!m) return null;
        var item = m[0];
        var pubM  = item.match(/<pubDate>([^<]+)<\/pubDate>/);
        var titleM = item.match(/<title>([^<]+)<\/title>/);
        var linkM  = item.match(/<link>([^<]+)<\/link>/);
        if (!pubM) return null;
        return {
          date:  new Date(pubM[1]),
          title: titleM ? titleM[1].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>') : '',
          href:  linkM ? linkM[1].trim() : '#'
        };
      }

      try {
        var cached = sessionStorage.getItem(CACHE_KEY);
        if (cached) {
          var obj = JSON.parse(cached);
          if (Date.now() - obj.ts < CACHE_TTL) { applyLatest(obj.item); return; }
        }
      } catch(e) {}

      fetch(rssUrl)
        .then(function(r) { return r.text(); })
        .then(function(xml) {
          var item = parseFirst(xml);
          if (!item) return;
          try { sessionStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), item: item })); } catch(e) {}
          applyLatest(item);
        })
        .catch(function() {});
    })();
  })();


  /* Subscribe */
  window.hpSubscribeSubmit = function(e, lang) {
    e.preventDefault();
    var btn = document.getElementById('hp-subscribe-btn');
    if (btn) { btn.textContent = lang==='zh'?'已订阅 ✓':'SUBSCRIBED ✓'; btn.disabled=true; }
    return false;
  };
})();
</script>

{{/* ── Hero ambient 3D field ─────────────────────────────────────
     three.js + the hero-field shader, both fingerprinted and locally
     hosted (same pattern as knowledge-galaxy). The module self-gates:
     on phones / reduced-motion / no-WebGL it mounts nothing, so this
     block adds zero runtime cost there beyond a tiny deferred import. */}}
{{- $three := resources.Get "js/vendor/three.module.min.js" | resources.Fingerprint "sha384" -}}
{{- /* Bundle hero-field + its ./webgl-guard import into one file via esbuild,
       keeping the bare `three` import external so it resolves via the importmap
       below. This avoids a second network request 404'ing on the relative
       guard import. */ -}}
{{- $heroField := resources.Get "js/three/hero-field.js"
      | js.Build (dict "format" "esm" "minify" hugo.IsProduction "externals" (slice "three")) -}}
{{- /* The Bear AI orb: shader sphere + neural particle field, same bundling
       rules (three external → importmap, webgl-guard inlined). */ -}}
{{- $bearOrb := resources.Get "js/three/bear-orb.js"
      | js.Build (dict "format" "esm" "minify" hugo.IsProduction "externals" (slice "three")) -}}
{{- if not site.Params.assets.disableFingerprinting -}}
  {{- $heroField = $heroField | fingerprint -}}
  {{- $bearOrb = $bearOrb | fingerprint -}}
{{- end -}}
<script type="importmap">
{
  "imports": {
    "three": "{{ $three.RelPermalink }}"
  }
}
</script>
<script type="module">
  import { mountHeroField } from "{{ $heroField.RelPermalink }}";
  const host = document.getElementById('hp-hero-field');
  if (host) {
    // --accent lives on .homepage-root and inherits down to the host, so read
    // it off the host itself (light/dark flips it automatically via CSS).
    mountHeroField(host, host);
  }

  // Bear AI orb — mount onto the persistent stage layer. Self-gates (no
  // WebGL / reduced-motion / phones → returns null and the CSS avatar puck
  // shows through instead). When it does mount, register the handle so the
  // widget IIFE can drive pulse / thinking / recess, and flag the wrap so
  // CSS hides the fallback puck and lets the live orb show.
  import { mountBearOrb } from "{{ $bearOrb.RelPermalink }}";
  const stage = document.getElementById('hp-bear-stage');
  const wrap  = document.getElementById('hp-bear-ai');
  if (stage && wrap) {
    const handle = mountBearOrb(stage, wrap);
    if (handle) {
      wrap.classList.add('has-orb');
      if (typeof window.__hpBearOrbReady === 'function') window.__hpBearOrbReady(handle);
    }
  }
</script>
