PageSourceSearch

https://miruku.zeabur.app/js/main.js

js miruku.zeabur.app collected 2026-10-03 16:35:32 UTC 82,827 bytes, 1,772 lines download raw bytes

1/* ============================================================
2   米露谷心理治療所 — 主要 JS
3   ============================================================ */
4
5(function () {
6  'use strict';
7
8  /* ── 1. Header scroll 行為 ── */
9  const header = document.getElementById('site-header');
10  let lastScrollY = window.scrollY;
11  let ticking = false;
12
13  const anchorNav = document.querySelector('.anchor-nav');
14
15  function handleScroll() {
16    const currentScrollY = window.scrollY;
17    if (currentScrollY <= 0) {
18      header.classList.remove('hide');
19      if (anchorNav) anchorNav.classList.remove('header-hidden');
20    } else if (currentScrollY > lastScrollY && currentScrollY > 80) {
21      header.classList.add('hide');
22      if (anchorNav) anchorNav.classList.add('header-hidden');
23    } else if (currentScrollY < lastScrollY) {
24      header.classList.remove('hide');
25      if (anchorNav) anchorNav.classList.remove('header-hidden');
26    }
27    lastScrollY = currentScrollY;
28    ticking = false;
29  }
30
31  window.addEventListener('scroll', function () {
32    if (!ticking) {
33      window.requestAnimationFrame(handleScroll);
34      ticking = true;
35    }
36  }, { passive: true });
37
38
39  /* ── 2. Hamburger menu ── */
40  const hamburger = document.getElementById('hamburger');
41  const mainNav = document.getElementById('main-nav');
42
43  if (hamburger && mainNav) {
44    hamburger.addEventListener('click', function () {
45      const isOpen = mainNav.classList.toggle('open');
46      hamburger.classList.toggle('open', isOpen);
47      hamburger.setAttribute('aria-expanded', isOpen);
48    });
49
50    // 點擊 nav 連結後自動收合
51    mainNav.querySelectorAll('a').forEach(function (link) {
52      link.addEventListener('click', function () {
53        mainNav.classList.remove('open');
54        hamburger.classList.remove('open');
55        hamburger.setAttribute('aria-expanded', 'false');
56      });
57    });
58
59    // 點擊外部關閉
60    document.addEventListener('click', function (e) {
61      if (!header.contains(e.target)) {
62        mainNav.classList.remove('open');
63        hamburger.classList.remove('open');
64        hamburger.setAttribute('aria-expanded', 'false');
65      }
66    });
67  }
68
69
70  /* ── 3. LINE Modal ── */
71  const lineBtn = document.getElementById('lineBtn');
72  const lineModal = document.getElementById('lineModal');
73  const modalClose = document.getElementById('modalClose');
74
75  function openModal() {
76    if (lineModal) lineModal.classList.add('open');
77    document.body.style.overflow = 'hidden';
78  }
79
80  function closeModal() {
81    if (lineModal) lineModal.classList.remove('open');
82    document.body.style.overflow = '';
83  }
84
85  if (lineBtn) lineBtn.addEventListener('click', openModal);
86  if (modalClose) modalClose.addEventListener('click', closeModal);
87
88  if (lineModal) {
89    lineModal.addEventListener('click', function (e) {
90      if (e.target === lineModal) closeModal();
91    });
92  }
93
94  document.addEventListener('keydown', function (e) {
95    if (e.key === 'Escape') closeModal();
96  });
97
98
99  /* ── 4. Active nav link ── */
100  const currentPath = window.location.pathname.split('/').pop() || 'index.html';
101  document.querySelectorAll('.main-nav a').forEach(function (link) {
102    const linkPath = link.getAttribute('href').split('/').pop();
103    if (linkPath === currentPath || (currentPath === '' && linkPath === 'index.html')) {
104      link.classList.add('active');
105    }
106  });
107
108
109  /* ── 5. Tab 切換(首頁多館) ── */
110  const tabBtns = document.querySelectorAll('.tab-btn');
111  const tabPanels = document.querySelectorAll('.tab-panel');
112
113  tabBtns.forEach(function (btn) {
114    btn.addEventListener('click', function () {
115      const target = btn.dataset.tab;
116
117      tabBtns.forEach(function (b) { b.classList.remove('active'); });
118      tabPanels.forEach(function (p) { p.classList.remove('active'); });
119
120      btn.classList.add('active');
121      const panel = document.getElementById('tab-' + target);
122      if (panel) panel.classList.add('active');
123    });
124  });
125
126
127  /* ── 6. 最新消息渲染(首頁,資料來自 news.json) ── */
128  const newsContainer = document.getElementById('news-container');
129  if (newsContainer) {
130    fetch('news.json', { cache: 'no-cache' })
131      .then(function (res) {
132        if (!res.ok) throw new Error('news fetch failed: ' + res.status);
133        return res.json();
134      })
135      .then(function (newsData) {
136        const active = newsData.filter(function (item) { return item.status === 'active'; });
137        const sorted = active.sort(function (a, b) {
138          if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
139          if ((a.order || 0) !== (b.order || 0)) return (a.order || 0) - (b.order || 0);
140          return new Date(b.date) - new Date(a.date);
141        });
142        const latest = sorted.slice(0, 6); // 首頁最多顯示幾篇,可調整
143        function escapeHtml(s) {
144          return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
145            return { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c];
146          });
147        }
148        function safeHref(h) {
149          if (h == null) return null;
150          const s = String(h).trim();
151          if (s === '') return null;
152          const scheme = s.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
153          if (scheme && !/^(https?|mailto|tel)$/i.test(scheme[1])) return null; // 擋 javascript:/data: 等
154          return s;
155        }
156        newsContainer.innerHTML = latest.map(function (item) {
157          const href = safeHref(item.linkHref);
158          const link = href
159            ? `<a href="${escapeHtml(href)}" class="news-link">${escapeHtml(item.linkText)} →</a>`
160            : '';
161          return `
162            <article class="news-card${item.pinned ? ' is-pinned' : ''}">
163              <span class="news-category">${escapeHtml(item.category)}</span>
164              <h3 class="news-title">${escapeHtml(item.title)}</h3>
165              <p class="news-date">${escapeHtml(item.dateDisplay)}</p>
166              <p class="news-summary">${escapeHtml(item.summary)}</p>
167              ${link}
168            </article>
169          `;
170        }).join('');
171        anchorNewsReady = true; anchorTryScroll();
172      })
173      .catch(function (err) {
174        console.error(err);
175        newsContainer.innerHTML = '<p class="news-empty">最新消息暫時無法載入,請稍後再試。</p>';
176        anchorNewsReady = true; anchorTryScroll();
177      });
178  }
179
180
181  /* ── 跨頁錨點(首頁):延後到內容載入後一次捲到位 ── */
182  // 首頁最新消息為非同步 fetch,會在原生跳轉「之後」推移版面,造成從 contact 等頁
183  // 連到 index.html#locations-heading 時「先跳錯、再跳對」。解法:先移除 hash 阻止
184  // 原生跳轉,待最新消息渲染完 + 圖片載入完,再一次 instant 捲到目標(僅首頁需要,
185  // 以 #news-container 是否存在判斷;yucai 等頁不受影響)。
186  var anchorPendingId = null;
187  var anchorNewsReady = false;
188  var anchorLoadReady = false;
189  function anchorTryScroll() {
190    if (!anchorPendingId || !anchorNewsReady || !anchorLoadReady) return;
191    var el = document.getElementById(anchorPendingId);
192    if (el) {
193      el.scrollIntoView({ block: 'start', behavior: 'instant' });
194      if (window.history && history.replaceState) {
195        history.replaceState(null, '', window.location.pathname + window.location.search + '#' + anchorPendingId);
196      }
197    }
198    anchorPendingId = null;
199  }
200  if (document.getElementById('news-container') && window.location.hash.length > 1) {
201    try { anchorPendingId = decodeURIComponent(window.location.hash.slice(1)); } catch (e) { anchorPendingId = null; }
202    if (anchorPendingId && window.history && history.replaceState) {
203      history.replaceState(null, '', window.location.pathname + window.location.search);
204    }
205  }
206  window.addEventListener('load', function () { anchorLoadReady = true; anchorTryScroll(); });
207
208  /* ── 各館服務時間 + 即時狀態(index.html) ── */
209  // 格式:星期幾(0=日 .. 6=六): [開始時, 結束時],null=休息;或 { byAppt: true } 表預約制。
210  const LOCATION_HOURS = {
211    // 中和:一三五 10–21;二四 13–21;六 9–17
212    'zhonghe':      { 1: [10, 21], 2: [13, 21], 3: [10, 21], 4: [13, 21], 5: [10, 21], 6: [9, 17], 0: null },
213    // 板橋成人部:一二 13–21;三四五 10–21;六 9–18
214    'banqiao':      { 1: [13, 21], 2: [13, 21], 3: [10, 21], 4: [10, 21], 5: [10, 21], 6: [9, 18], 0: null },
215    // 板橋兒青部:一二四五 13–21;三 10–21;六 9–17
216    'banqiao-kids': { 1: [13, 21], 2: [13, 21], 3: [10, 21], 4: [13, 21], 5: [13, 21], 6: [9, 17], 0: null },
217    // 新店:一至五 13–21;六 9–18
218    'xindian':      { 1: [13, 21], 2: [13, 21], 3: [13, 21], 4: [13, 21], 5: [13, 21], 6: [9, 18], 0: null },
219    'online':       { byAppt: true },
220  };
221  const locationCards = document.querySelectorAll('.tab-panel[id^="tab-"] .location-card');
222  if (locationCards.length) {
223    const LH_DOW = { 1: '週一', 2: '週二', 3: '週三', 4: '週四', 5: '週五', 6: '週六', 0: '週日' };
224    const LH_ORDER = [1, 2, 3, 4, 5, 6, 0];
225    const lhNow = new Date();
226    const lhToday = lhNow.getDay();
227    const lhHour = lhNow.getHours() + lhNow.getMinutes() / 60;
228    function lhFmt(r) {
229      return r ? (String(r[0]).padStart(2, '0') + ':00 – ' + String(r[1]).padStart(2, '0') + ':00') : '休息';
230    }
231    locationCards.forEach(function (card) {
232      const panel = card.closest('.tab-panel');
233      const data = panel ? LOCATION_HOURS[panel.id.replace(/^tab-/, '')] : null;
234      if (!data) return;
235      const name = card.querySelector('.location-name');
236      if (name && !name.querySelector('.loc-status')) {
237        let badge;
238        if (data.byAppt) {
239          badge = '<span class="loc-status appt"><span class="dot"></span>預約制</span>';
240        } else {
241          const r = data[lhToday];
242          const open = !!(r && lhHour >= r[0] && lhHour < r[1]);
243          badge = open
244            ? '<span class="loc-status open"><span class="dot"></span>服務中</span>'
245            : '<span class="loc-status closed"><span class="dot"></span>已打烊</span>';
246        }
247        name.insertAdjacentHTML('beforeend', badge);
248      }
249      const dd = card.querySelector('.loc-hours-dd');
250      if (dd) {
251        if (data.byAppt) {
252          dd.innerHTML = '<div class="loc-hours"><div class="loc-hours-row"><span class="d">預約制</span>' +
253            '<span class="t" style="color:var(--color-text-muted)">依各心理師時段,請來電或線上預約</span></div></div>';
254        } else {
255          dd.innerHTML = '<div class="loc-hours">' + LH_ORDER.map(function (d) {
256            const r = data[d];
257            const isToday = d === lhToday;
258            const tag = isToday ? '<span class="loc-hours-today-tag">今天</span>' : '';
259            const t = r ? '<span class="t">' + lhFmt(r) + '</span>
259' : '<span class="t closed">休息</span>';
260            return '<div class="loc-hours-row' + (isToday ? ' is-today' : '') + '"><span class="d">' + LH_DOW[d] + '</span>' + t + tag + '</div>';
261          }).join('') + '</div>';
262        }
263      }
264    });
265  }
266
267  /* ── 各館空間照片相簿(index.html・縮圖 + lightbox) ── */
268  // 填入真實照片後,該館卡片即會出現「空間環境」相簿;空陣列=不顯示。
269  // 格式:每館一個陣列,每張 { num: 編號或 null, label: 顯示名稱, src: '照片路徑' }。
270  // 照片放 assets/images/spaces/,命名如 zhonghe-1.jpg。範例:
271  //   'zhonghe': [
272  //     { num: 1, label: '治療室 1', src: 'assets/images/spaces/zhonghe/1.jpg' },
273  //     { num: null, label: '候診區',  src: 'assets/images/spaces/zhonghe/5.jpg' },
274  //   ],
275  // ⚠️ 暫定照片(2026-07):四館先放既有空間照占位,label 統一「空間環境」,
276  //    待真實照片與正式名稱到位後,直接替換 src/label 即可(空陣列=不顯示)。
277  const LOCATION_SPACES = {
278    'zhonghe': [
279      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/1.jpg' },
280      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/2.jpg' },
281      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/3.jpg' },
282      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/4.jpg' },
283      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/5.jpg' },
284      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/6.jpg' },
285      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/7.jpg' },
286      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/8.jpg' },
287      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/9.jpg' },
288      { num: null, label: '空間環境', src: 'assets/images/spaces/zhonghe/10.jpg' },
289    ],
290    'banqiao': [
291      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao/1.jpg' },
292      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao/2.webp' },
293      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao/3.webp' },
294    ],
295    'banqiao-kids': [
296      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/1.jpg' },
297      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/2.jpg' },
298      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/3.jpg' },
299      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/4.jpg' },
300      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/5.jpg' },
301      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/6.jpg' },
302      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/7.jpg' },
303      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/8.jpg' },
304      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/9.jpg' },
305      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/10.jpg' },
306      { num: null, label: '空間環境', src: 'assets/images/spaces/banqiao-kids/11.jpg' },
307    ],
308    'xindian': [
309      { num: null, label: '空間環境', src: 'assets/images/spaces/xindian/1.jpg' },
310      { num: null, label: '空間環境', src: 'assets/images/spaces/xindian/2.jpg' },
311      { num: null, label: '空間環境', src: 'assets/images/spaces/xindian/3.jpg' },
312      { num: null, label: '空間環境', src: 'assets/images/spaces/xindian/4.jpg' },
313      { num: null, label: '空間環境', src: 'assets/images/spaces/xindian/5.jpg' },
314      { num: null, label: '空間環境', src: 'assets/images/spaces/xindian/6.jpg' },
315    ],
316  };
317  const spaceCards = document.querySelectorAll('.tab-panel[id^="tab-"] .location-card');
318  if (spaceCards.length) {
319    let lbList = [];
320    let lbIndex = 0;
321    let lbEl = null;
322    function showLb(i) {
323      if (!lbList.length) return;
324      lbIndex = (i + lbList.length) % lbList.length;
325      const p = lbList[lbIndex];
326      const img = lbEl.querySelector('.space-lb-stage img');
327      img.src = p.src; img.alt = p.label;
328      lbEl.querySelector('.space-lb-cap').innerHTML = (p.num ? '<span class="space-num">' + p.num + '</span>' : '') + p.label;
329    }
330    function closeLb() {
331      if (!lbEl) return;
332      lbEl.classList.remove('open');
333      document.body.classList.remove('space-lb-open');
334    }
335    function buildLb() {
336      lbEl = document.createElement('div');
337      lbEl.className = 'space-lightbox';
338      lbEl.setAttribute('role', 'dialog');
339      lbEl.setAttribute('aria-modal', 'true');
340      lbEl.innerHTML =
341        '<button type="button" class="space-lb-close" aria-label="關閉">×</button>' +
342        '<button type="button" class="space-lb-nav space-lb-prev" aria-label="上一張">‹</button>' +
343        '<figure class="space-lb-stage"><img alt="" /><figcaption class="space-lb-cap"></figcaption></figure>' +
344        '<button type="button" class="space-lb-nav space-lb-next" aria-label="下一張">›</button>';
345      document.body.appendChild(lbEl);
346      lbEl.querySelector('.space-lb-close').addEventListener('click', closeLb);
347      lbEl.querySelector('.space-lb-prev').addEventListener('click', function () { showLb(lbIndex - 1); });
348      lbEl.querySelector('.space-lb-next').addEventListener('click', function () { showLb(lbIndex + 1); });
349      lbEl.addEventListener('click', function (e) { if (e.target === lbEl) closeLb(); });
350      document.addEventListener('keydown', function (e) {
351        if (!lbEl || !lbEl.classList.contains('open')) return;
352        if (e.key === 'Escape') closeLb();
353        else if (e.key === 'ArrowLeft') showLb(lbIndex - 1);
354        else if (e.key === 'ArrowRight') showLb(lbIndex + 1);
355      });
356    }
357    function openLb(list, i) {
358      if (!lbEl) buildLb();
359      lbList = list;
360      lbEl.classList.add('open');
361      document.body.classList.add('space-lb-open');
362      showLb(i);
363    }
364    spaceCards.forEach(function (card) {
365      const panel = card.closest('.tab-panel');
366      const photos = panel ? LOCATION_SPACES[panel.id.replace(/^tab-/, '')] : null;
367      if (!photos || !photos.length) return;
368      const gallery = document.createElement('div');
369      gallery.className = 'location-spaces';
370      gallery.innerHTML = '<p class="location-spaces-title">空間環境</p>' +
371        '<div class="location-spaces-viewport">' +
372          '<button type="button" class="space-arrow space-arrow-prev is-hidden" aria-label="上一批照片">‹</button>' +
373          '<div class="location-spaces-grid">' + photos.map(function (p, i) {
374            return '<button type="button" class="space-thumb" data-i="' + i + '">' +
375              '<img src="' + p.src + '" alt="' + p.label + '" loading="lazy" />' +
376              '<span class="space-cap">' + (p.num ? '<span class="space-num">' + p.num + '</span>' : '') + p.label + '</span>' +
377            '</button>';
378          }).join('') + '</div>' +
379          '<button type="button" class="space-arrow space-arrow-next" aria-label="下一批照片">›</button>' +
380        '</div>';
381      card.appendChild(gallery);
382      const grid = gallery.querySelector('.location-spaces-grid');
383      const arrowPrev = gallery.querySelector('.space-arrow-prev');
384      const arrowNext = gallery.querySelector('.space-arrow-next');
385      function updateArrows() {
386        const max = grid.scrollWidth - grid.clientWidth;
387        arrowPrev.classList.toggle('is-hidden', grid.scrollLeft <= 1);
388        arrowNext.classList.toggle('is-hidden', grid.scrollLeft >= max - 1);
389      }
390      arrowPrev.addEventListener('click', function () { grid.scrollBy({ left: -grid.clientWidth * 0.8, behavior: 'smooth' }); });
391      arrowNext.addEventListener('click', function () { grid.scrollBy({ left: grid.clientWidth * 0.8, behavior: 'smooth' }); });
392      grid.addEventListener('scroll', updateArrows, { passive: true });
393      window.addEventListener('resize', updateArrows);
394      updateArrows();
395      gallery.querySelectorAll('.space-thumb').forEach(function (btn) {
396        btn.addEventListener('click', function () { openLb(photos, parseInt(btn.dataset.i, 10)); });
397      });
398    });
399  }
400
401  /* ── 7. 團隊篩選(team.html) ── */
402  const teamGrid = document.getElementById('team-grid');
403  if (teamGrid && typeof teamData !== 'undefined') {
404    // 心理師排序:所長 / 副所長 → 主任 → 其他(同層級依資料原本順序)
405    function roleRank(role) {
406      if (!role) return 2;
407      if (role.indexOf('所長') !== -1) return 0;
408      if (role.indexOf('主任') !== -1) return 1;
409      return 2;
410    }
411
412    // 渲染心理師
413    const therapists = teamData
414      .filter(function (m) { return m.type === 'therapist'; })
415      .map(function (m, i) { return { m: m, i: i }; })
416      .sort(function (a, b) {
417        const r = roleRank(a.m.role) - roleRank(b.m.role);
418        return r !== 0 ? r : a.i - b.i;
419      })
420      .map(function (x) { return x.m; });
421    const socialWorkers = teamData.filter(function (m) { return m.type === 'social-worker'; });
422    const admins = teamData.filter(function (m) { return m.type === 'admin'; });
423
424    therapists.forEach(function (member) {
425      const card = createTeamCard(member);
426      teamGrid.appendChild(card);
427    });
428
429    // 渲染社工師
430    const socialWorkerGrid = document.getElementById('social-worker-grid');
431    if (socialWorkerGrid) {
432      socialWorkers.forEach(function (member) {
433        const card = createTeamCard(member);
434        socialWorkerGrid.appendChild(card);
435      });
436    }
437
438    // 渲染行政
439    const adminGrid = document.getElementById('admin-grid');
440    if (adminGrid) {
441      admins.forEach(function (member) {
442        const card = createTeamCard(member);
443        adminGrid.appendChild(card);
444      });
445    }
446
447    // 篩選邏輯
448    // 館別為多選(OR 邏輯):activeLocations 為空陣列代表「全部」
449    let activeLocations = [];
450    let activeTarget = 'all';
451    let activeScheme = 'all';
452    // 專長為多選(OR 邏輯):activeSpecialties 為空陣列代表「全部」
453    let activeSpecialties = [];
454
455    // 常見服務項目(與服務頁/問卷一致的 12 項)
456    const COMMON_SPECIALTIES = [
457      '人際議題', '壓力調適', '睡眠困擾', '生活適應',
458      '自我探索', '情緒調適', '親職協助', '青少年適應',
459      '婚姻/伴侶議題', '生涯規劃', '兒童情緒/行為', '兒童人際互動',
460    ];
461
462    // 由 teamData 收集所有「個人特殊專長」:specialties + generalSpecialties 中不屬於 12 項常見項目的值都納入
463    const specialSet = new Set();
464    therapists.forEach(function (m) {
465      [].concat(m.specialties || [], m.generalSpecialties || []).forEach(function (s) {
466        if (!COMMON_SPECIALTIES.includes(s)) specialSet.add(s);
467      });
468    });
469    const SPECIAL_SPECIALTIES = Array.from(specialSet).sort(function (a, b) {
470      return a.localeCompare(b, 'zh-Hant');
471    });
472
473    // 為每張卡片寫入合併後的 specialties data-attr(generalSpecialties + specialties)
474    teamGrid.querySelectorAll('.team-card').forEach(function (card, i) {
475      const m = therapists[i];
476      if (!m) return;
477      const combined = [].concat(m.generalSpecialties || [], m.specialties || []);
478      card.dataset.specialties = combined.join('|');
479    });
480
481    // 渲染 modal 內容
482    const specialtyTrigger = document.getElementById('specialtyTrigger');
483    const specialtyModal = document.getElementById('specialtyModal');
484    if (specialtyTrigger && specialtyModal) {
485      const modalBody = document.getElementById('specialtyModalBody');
486      const searchInput = document.getElementById('specialtySearch');
487      const applyBtn = document.getElementById('specialtyApply');
488      const cancelBtn = document.getElementById('specialtyCancel');
489      const clearBtn = document.getElementById('specialtyClear');
490      const closeBtn = document.getElementById('specialtyModalClose');
491      const chipsEl = document.getElementById('specialtyChips');
492
493      function renderGroup(title, items) {
494        if (!items.length) return '';
495        const checks = items.map(function (v) {
496          const safe = v.replace(/"/g, '&quot;');
497          return '<label class="specialty-check"><input type="checkbox" value="' + safe + '">' + v + '</label>';
498        }).join('');
499        return '<div class="specialty-group"><p class="specialty-group-title">' + title + '</p>' +
500               '<div class="specialty-check-grid">' + checks + '</div></div>';
501      }
502
503      modalBody.innerHTML =
504        renderGroup('常見服務項目', COMMON_SPECIALTIES) +
505        renderGroup('個人特殊專長', SPECIAL_SPECIALTIES);
506
507      const checkboxes = modalBody.querySelectorAll('input[type="checkbox"]');
508
509      function updateApplyLabel() {
510        const n = Array.from(checkboxes).filter(function (c) { return c.checked; }).length;
511        applyBtn.textContent = n > 0 ? '套用 (' + n + ')' : '套用';
512      }
513      checkboxes.forEach(function (c) { c.addEventListener('change', updateApplyLabel); });
514
515      function openModal() {
516        // 進入時讓 modal 內勾選狀態反映目前已套用的條件
517        checkboxes.forEach(function (c) { c.checked = activeSpecialties.indexOf(c.value) !== -1; });
518        searchInput.value = '';
519        modalBody.querySelectorAll('.specialty-check').forEach(function (lbl) { lbl.style.display = ''; });
520        modalBody.querySelectorAll('.specialty-group').forEach(function (g) { g.style.display = ''; });
521        specialtyModal.classList.add('open');
522        updateApplyLabel();
523        setTimeout(function () { searchInput.focus(); }, 50);
524      }
525      function closeModal() { specialtyModal.classList.remove('open'); }
526
527      specialtyTrigger.addEventListener('click', openModal);
528      closeBtn.addEventListener('click', closeModal);
529      cancelBtn.addEventListener('click', closeModal);
530      specialtyModal.addEventListener('click', function (e) {
531        if (e.target === specialtyModal) closeModal();
532      });
533      document.addEventListener('keydown', function (e) {
534        if (e.key === 'Escape' && specialtyModal.classList.contains('open')) closeModal();
535      });
536
537      clearBtn.addEventListener('click', function () {
538        checkboxes.forEach(function (c) { c.checked = false; });
539        updateApplyLabel();
540      });
541
542      searchInput.addEventListener('input', function () {
543        const q = searchInput.value.trim();
544        modalBody.querySelectorAll('.specialty-group').forEach(function (group) {
545          let anyVisible = false;
546          group.querySelectorAll('.specialty-check').forEach(function (lbl) {
547            const visible = !q || lbl.textContent.indexOf(q) !== -1;
548            lbl.style.display = visible ? '' : 'none';
549            if (visible) anyVisible = true;
550          });
551          group.style.display = anyVisible ? '' : 'none';
552        });
553      });
554
555      applyBtn.addEventListener('click', function () {
556        activeSpecialties = Array.from(checkboxes).filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
557        renderSpecialtyChips();
558        applyFilter();
559        closeModal();
560      });
561
562      function renderSpecialtyChips() {
563        chipsEl.innerHTML = '';
564        activeSpecialties.forEach(function (v) {
565          const chip = document.createElement('span');
566          chip.className = 'specialty-chip';
567          chip.innerHTML = v.replace(/[<>&]/g, function (ch) {
568            return ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' })[ch];
569          }) + ' <button type="button" aria-label="移除 ' + v + '">×</button>';
570          chip.querySelector('button').addEventListener('click', function () {
571            activeSpecialties = activeSpecialties.filter(function (s) { return s !== v; });
572            renderSpecialtyChips();
573            applyFilter();
574          });
575          chipsEl.appendChild(chip);
576        });
577      }
578    }
579
580    document.querySelectorAll('[data-filter-location]').forEach(function (btn) {
581      btn.addEventListener('click', function () {
582        const value = btn.dataset.filterLocation;
583        if (value === 'all') {
584          activeLocations = [];
585        } else {
586          const idx = activeLocations.indexOf(value);
587          if (idx === -1) {
588            activeLocations.push(value);
589          } else {
590            activeLocations.splice(idx, 1);
591          }
592        }
593        document.querySelectorAll('[data-filter-location]').forEach(function (b) {
594          const v = b.dataset.filterLocation;
595          b.classList.toggle('active',
596            v === 'all' ? activeLocations.length === 0 : activeLocations.includes(v));
597        });
598        applyFilter();
599      });
600    });
601
602    document.querySelectorAll('[data-filter-target]').forEach(function (btn) {
603      btn.addEventListener('click', function () {
604        activeTarget = btn.dataset.filterTarget;
605        document.querySelectorAll('[data-filter-target]').forEach(function (b) {
606          b.classList.toggle('active', b.dataset.filterTarget === activeTarget);
607        });
608        applyFilter();
609      });
610    });
611
612    document.querySelectorAll('[data-filter-scheme]').forEach(function (btn) {
613      btn.addEventListener('click', function () {
614        activeScheme = btn.dataset.filterScheme;
615        document.querySelectorAll('[data-filter-scheme]').forEach(function (b) {
616          b.classList.toggle('active', b.dataset.filterScheme === activeScheme);
617        });
618        applyFilter();
619      });
620    });
621
622    // 社工師團隊為固定名單、不吃心理師篩選器;為避免使用者誤以為篩選對社工師
623    // 也生效(篩了方案/館別/對象,社工師卻原封不動留在版面上),只要任一篩選
624    // 條件不是「全部」,就整段隱藏社工師團隊區塊,全部回到「全部」才顯示。
625    const socialWorkersSection = document.getElementById('social-workers-section');
626
627    function applyFilter() {
628      teamGrid.querySelectorAll('.team-card').forEach(function (card) {
629        const locations = (card.dataset.locations || '').split(',');
630        const targets = (card.dataset.targets || '').split(',');
631        const schemes = (card.dataset.schemes || '').split(',');
632        const specialties = (card.dataset.specialties || '').split('|').filter(
632Boolean);
633        const matchLoc = activeLocations.length === 0 || activeLocations.some(function (l) {
634          return locations.includes(l);
635        });
636        const matchTarget = activeTarget === 'all' || targets.includes(activeTarget);
637        const matchScheme = activeScheme === 'all' || schemes.includes(activeScheme);
638        const matchSpec = activeSpecialties.length === 0 || activeSpecialties.some(function (s) {
639          return specialties.includes(s);
640        });
641        card.classList.toggle('hidden', !(matchLoc && matchTarget && matchScheme && matchSpec));
642      });
643
644      if (socialWorkersSection) {
645        const anyFilterActive = activeLocations.length > 0 || activeTarget !== 'all' ||
646          activeScheme !== 'all' || activeSpecialties.length > 0;
647        socialWorkersSection.style.display = anyFilterActive ? 'none' : '';
648      }
649    }
650  }
651
652
653  /* ── 8. Team Member 頁 ── */
654  const memberContainer = document.getElementById('member-detail');
655  if (memberContainer && typeof teamData !== 'undefined') {
656    const params = new URLSearchParams(window.location.search);
657    const memberId = params.get('id');
658    const member = teamData.find(function (m) { return m.id === memberId; });
659
660    if (member) {
661      document.title = member.name + ' — 米露谷心理治療所';
662
663      function makeList(arr) {
664        if (!arr || !arr.length) return '';
665        return `<ul class="member-list">${arr.map(function (e) { return `<li>${e}</li>`; }).join('')}</ul>`;
666      }
667
668      function makeExperienceList(arr) {
669        if (!arr || !arr.length) return '';
670        if (typeof arr[0] === 'string') return makeList(arr);
671        return `<ul class="member-list">${arr.map(function (e) {
672          const badge = e.current
673            ? `<span class="exp-badge exp-badge--current">現職</span>`
674            : '';
675          return `<li>${badge}${e.text}</li>`;
676        }).join('')}</ul>`;
677      }
678
679      function makePublicationList(arr) {
680        if (!arr || !arr.length) return '';
681        return `<ul class="member-list">${arr.map(function (p) {
682          if (typeof p === 'string') return `<li>${p}</li>`;
683          const titleText = p.type === 'book' ? `《${p.title}》` : p.title;
684          const metaBits = [];
685          if (p.role) metaBits.push(p.role);
686          if (p.publisher) metaBits.push(p.publisher);
687          if (p.year) metaBits.push(p.year);
688          if (!metaBits.length && p.note) metaBits.push(p.note);
689          const meta = metaBits.length ? `<span class="pub-meta">(${metaBits.join('・')})</span>` : '';
690          const inner = `${titleText}${meta}`;
691          return p.url
692            ? `<li><a href="${p.url}" target="_blank" rel="noopener">${inner}</a></li>`
693            : `<li>${inner}</li>`;
694        }).join('')}</ul>`;
695      }
696
697      function makeSection(title, content) {
698        if (!content) return '';
699        return `<div class="member-section"><p class="member-section-title">${title}</p>${content}</div>`;
700      }
701
702      // 督導情境:?view=supervision 且該心理師有督導資料時,下方顯示「督導相關資料」區塊
703      const isSupervisionView = params.get('view') === 'supervision' && !!member.supervision;
704      function escSup(v) {
705        return String(v == null ? '' : v)
706          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
707      }
708      function supRow(label, val) {
709        return val ? `<div class="sup-row"><dt>${label}</dt><dd>${escSup(val)}</dd></div>` : '';
710      }
711      const supervisionSection = (function () {
712        if (isSupervisionView) {
713          const s = member.supervision;
714          const st = s.style || {};
715          const styleItems = [
716            st.focus ? `<li><span>關注焦點</span>${escSup(st.focus)}</li>` : '',
717            st.atmosphere ? `<li><span>互動氛圍</span>${escSup(st.atmosphere)}</li>` : '',
718            st.transmission ? `<li><span>專業傳承</span>${escSup(st.transmission)}</li>` : '',
719            st.gift ? `<li><span>受督生收穫</span>${escSup(st.gift)}</li>` : '',
720          ].join('');
721          return `
722            <div class="member-supervision-detail">
723              <p class="member-section-title">督導相關資料</p>
724              ${s.tagline ? `<p class="sup-tagline">「${escSup(s.tagline)}」</p>` : ''}
725              <dl class="sup-list">
726                ${supRow('督導取向', s.orientation)}
727                ${supRow('專長服務對象', s.clientGroups)}
728                ${supRow('擅長領域', s.challengeAreas)}
729                ${supRow('督導形式', s.formats)}
730                ${supRow('線上督導', s.online)}
731                ${supRow('實習督導資格', s.internQualified)}
732                ${supRow('學會督導證號', s.assocCertNo)}
733                ${supRow('督導費用', s.fee)}
734                ${supRow('學生優惠', s.studentDiscount)}
735                ${supRow('跨體系實務專長', s.crossSystem)}
736              </dl>
737              ${styleItems ? `<p class="sup-style-title">督導風格</p><ul class="sup-style">${styleItems}</ul>` : ''}
738            </div>`;
739        }
740        // 民眾版(無 view=supervision)不再顯示督導媒合反向小卡
741        return '';
742      })();
743
744      const photoEl = member.photo
745        ? (member.photoCrop
746            // 個人頁=總覽卡的中央直條:內層用「卡片視圖(16:10)」滿高置中、外層 3:4 裁掉左右
747            ? `<div class="member-photo-strip"><img src="${member.photo}" alt="${member.name}" class="member-photo-cardview" style="${window.photoCropStyle(member.photoCrop)}" /></div>`
748            : `<img src="${member.photo}" alt="${member.name}" class="member-photo-img" />`)
749        : `<div class="member-photo-placeholder"></div>`;
750
751      const locationChips = member.locations.map(function (l) {
752        return `<span class="chip">${l}</span>`;
753      }).join('');
754
755      const targetChips = member.targets.map(function (t) {
756        return `<span class="chip chip-primary">${t}</span>`;
757      }).join('');
758
759      const generalSpecialtyChips = (member.generalSpecialties || []).map(function (s) {
760        return `<span class="chip chip-primary">${s}</span>`;
761      }).join('');
762
763      const specialtyChips = member.specialties.map(function (s) {
764        return `<span class="chip">${s}</span>`;
765      }).join('');
766
767      const linksHtml = (function () {
768        if (!member.links) return '';
769        const parts = [];
770        if (member.links.website) parts.push(`<a href="${member.links.website}" class="member-link" target="_blank" rel="noopener">個人網站 →</a>`);
771        if (member.links.instagram) parts.push(`<a href="https://www.instagram.com/${member.links.instagram.replace('@', '')}" class="member-link" target="_blank" rel="noopener">Instagram →</a>`);
772        if (member.links.facebook) parts.push(`<a href="${member.links.facebook}" class="member-link" target="_blank" rel="noopener">Facebook →</a>`);
773        return parts.join('');
774      })();
775
776      memberContainer.innerHTML = `
777        <div class="member-layout">
778          <div class="member-photo">${photoEl}</div>
779          <div class="member-info">
780            <h1>${member.name}</h1>
781            <p class="member-title">${member.title}</p>
782            ${member.licenseNo ? `<p class="member-license">${member.licenseNo}</p>` : ''}
783            ${member.role ? `<p class="member-role">${member.role}</p>` : ''}
784            <div class="member-chips">
785              ${locationChips}
786              ${targetChips}
787            </div>
788
789            ${(member.generalSpecialties && member.generalSpecialties.length) || (member.specialties && member.specialties.length) ? `
790            <div class="member-section">
791              <p class="member-section-title">專長領域</p>
792              ${member.generalSpecialties && member.generalSpecialties.length ? `
793              <p class="member-specialty-label">常見服務項目</p><!-- 舊名稱:一般性專長 -->
794              <div class="member-specialties" style="margin-bottom:14px">${generalSpecialtyChips}</div>` : ''}
795              ${member.specialties && member.specialties.length ? `
796              ${member.generalSpecialties && member.generalSpecialties.length ? '<p class="member-specialty-label">特殊專長</p>' : ''}
797              <div class="member-specialties">${specialtyChips}</div>` : ''}
798            </div>` : ''}
799
800            ${member.schedule && member.schedule.length ? `
801            <div class="member-section">
802              <p class="member-section-title">固定班表</p>
803              <div class="member-schedule">
804                ${member.schedule.map(function (s) {
805                  return `<span class="chip ${locationChipClass(s.location)}">${s.day}・${s.location}</span>`;
806                }).join('')}
807              </div>
808              <p class="member-schedule-note">※ 實際班表以電話確認為準</p>
809            </div>` : ''}
810
811            ${makeSection('學歷', makeList(member.education))}
812            ${makeSection('經歷', makeExperienceList(member.experience))}
813            ${makeSection('認證與訓練', makeList(member.certifications))}
814            ${makeSection('著作 / 媒體', makePublicationList(member.publications))}
815            ${linksHtml ? `<div class="member-section member-links">${linksHtml}</div>` : ''}
816            ${supervisionSection}
817          </div>
818        </div>
819      `;
820
821      // 督導情境:把頁尾 CTA 改為督導洽詢
822      if (isSupervisionView) {
823        const ctaBox = document.getElementById('member-cta');
824        if (ctaBox) {
825          ctaBox.innerHTML =
826            '<h3 style="margin-bottom:12px; font-size:1.15rem;">想預約這位督導嗎?</h3>' +
827            '<p style="color:var(--color-text-muted); font-size:0.92rem; margin:0 auto 28px; max-width:560px; line-height:1.8;">填寫督導申請表,我們會依您的需求與這位督導的時段協助媒合。</p>' +
828            '<div style="display:flex; gap:14px; justify-content:center; flex-wrap:wrap;">' +
829              '<a href="contact.html?view=supervision&designated=yes&name=' + encodeURIComponent(member.name) + '#supervision-heading" class="btn btn-primary">洽詢督導媒合</a>' +
830              '<a href="yucai.html#sv-' + member.id + '" class="btn btn-outline">回到督導陣容</a>' +
831            '</div>';
832        }
833      }
834    } else {
835      memberContainer.innerHTML = `
836        <div style="text-align:center; padding: 80px 0;">
837          <p style="color:var(--color-text-muted);">找不到此心理師的資料。</p>
838          <a href="team.html" class="btn btn-outline" style="margin-top:24px">返回團隊介紹</a>
839        </div>
840      `;
841    }
842  }
843
844
845  /* ── Helper: 館別對應 chip class ── */
846  function locationChipClass(location) {
847    var map = { '中和': 'chip-zhonghe', '板橋': 'chip-banqiao', '新店': 'chip-xindian' };
848    return map[location] || '';
849  }
850
851  /* ── Helper: 建立 Team Card DOM ── */
852  function createTeamCard(member) {
853    const isLinked = member.type === 'therapist' || member.type === 'social-worker';
854    const card = document.createElement(isLinked ? 'a' : 'div');
855    card.className = 'team-card';
856    if (isLinked) card.href = `team-member.html?id=${member.id}`;
857    card.dataset.locations = member.locations.join(',');
858    card.dataset.targets = member.targets.join(',');
859    card.dataset.schemes = (member.schemes || []).join(',');
860
861    const photoEl = member.photo
862      ? `<img src="${member.photo}" alt="${member.name}" loading="lazy"${member.photoCrop ? ` style="${window.photoCropStyle(member.photoCrop)}"` : ''} />`
863      : `<div class="team-card-photo-placeholder"></div>`;
864
865    const locationTags = member.locations.map(function (l) {
866      return `<span class="chip">${l}</span>`;
867    }).join('');
868
869    const targetTags = member.targets.map(function (t) {
870      return `<span class="chip chip-primary">${t}</span>`;
871    }).join('');
872
873    const specialtyTags = (member.specialties || []).slice(0, 4).map(function (s) {
874      return `<span class="chip chip-muted">${s}</span>`;
875    }).join('');
876
877    const educationHtml = (member.type === 'admin' && member.education && member.education.length)
878      ? `<div class="team-card-education">${member.education.map(function (e) { return `<p>${e}</p>`; }).join('')}</div>`
879      : '';
880
881    card.innerHTML = `
882      <div class="team-card-photo">${photoEl}</div>
883      <div class="team-card-body">
884        ${member.role ? `<p class="team-card-role">${member.role}</p>` : ''}
885        <p class="team-card-name">${member.name}</p>
886        <p class="team-card-title">${member.title}</p>
887        <div class="team-card-tags">
888          ${locationTags}
889          ${targetTags}
890        </div>
891        ${specialtyTags ? `<div class="team-card-specialties">${specialtyTags}</div>` : ''}
892        ${educationHtml}
893      </div>
894    `;
895    return card;
896  }
897
898
899  /* ── 9. 館別資訊 / 地圖 切換 ── */
900  /* ── 10. Map Lightbox ── */
901  var mapLightbox = document.getElementById('mapLightbox');
902  var mapLightboxImg = document.getElementById('mapLightboxImg');
903  var mapLightboxClose = document.getElementById('mapLightboxClose');
904
905  function openMapLightbox(src, alt) {
906    mapLightboxImg.src = src;
907    mapLightboxImg.alt = alt;
908    mapLightbox.classList.add('open');
909    document.body.style.overflow = 'hidden';
910  }
911
912  function closeMapLightbox() {
913    mapLightbox.classList.remove('open');
914    document.body.style.overflow = '';
915  }
916
917  document.querySelectorAll('.location-view-panel img').forEach(function (img) {
918    img.addEventListener('click', function () {
919      openMapLightbox(img.src, img.alt);
920    });
921  });
922
923  if (mapLightboxClose) mapLightboxClose.addEventListener('click', closeMapLightbox);
924  if (mapLightbox) mapLightbox.addEventListener('click', function (e) {
925    if (e.target === mapLightbox) closeMapLightbox();
926  });
927  document.addEventListener('keydown', function (e) {
928    if (e.key === 'Escape' && mapLightbox && mapLightbox.classList.contains('open')) closeMapLightbox();
929  });
930
931
932
933  document.querySelectorAll('.location-view-tab').forEach(function (tab) {
934    tab.addEventListener('click', function () {
935      var right = tab.closest('.location-right');
936      var slider = right.querySelector('.location-view-slider');
937      var tabs = right.querySelectorAll('.location-view-tab');
938
939      tabs.forEach(function (t) { t.classList.remove('active'); });
940      tab.classList.add('active');
941
942      if (tab.dataset.view === 'map') {
943        slider.classList.add('show-map');
944      } else {
945        slider.classList.remove('show-map');
946      }
947    });
948  });
949
950
951  /* ── Anchor nav scroll spy + scroll-padding-top 偏移修正 ── */
952  const anchorLinks = document.querySelectorAll('.anchor-nav-list a');
953  if (anchorLinks.length) {
954    const targets = Array.from(anchorLinks).map(function (a) {
955      return document.getElementById(a.getAttribute('href').slice(1));
956    }).filter(Boolean);
957
958    /* 攔截點擊,捲到所屬 section 起點並補 sticky nav 高度 */
959    anchorLinks.forEach(function (a) {
960      a.addEventListener('click', function (e) {
961        const href = a.getAttribute('href');
962        if (!href || !href.startsWith('#')) return;
963        const targetEl = document.getElementById(href.slice(1));
964        if (!targetEl) return;
965        e.preventDefault();
966        const section = targetEl.closest('section') || targetEl;
967        const nav = document.querySelector('.anchor-nav');
968        /* 用 offsetHeight 取 nav 的渲染高度,不受是否 sticky、是否轉場中影響。
969           滾動完成後 nav 一定 sticky 在 top:0(往下滾會自動隱藏 header),
970           所以 section 對齊 nav 底部 = sectionAbsTop - navHeight。 */
971        const navHeight = nav ? nav.offsetHeight : 0;
972        const top = section.getBoundingClientRect().top + window.scrollY - navHeight;
973        window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
974        history.pushState(null, '', href);
975        /* 立即更新 active 狀態,不等 scroll spy */
976        anchorLinks.forEach(function (l) { l.classList.remove('active'); });
977        a.classList.add('active');
978      });
979    });
980
981    function updateAnchorNav() {
982      /* 以各 target「所屬 section 的頂端」判定 active,和點擊捲動(section 對齊
983         nav 底部)一致;閾值只需 nav 高度 + 小緩衝,避免 heading 在 section 內
984         偏下造成剛捲到就仍高亮前一區。 */
985      const nav = document.querySelector('.anchor-nav');
986      const navHeight = nav ? nav.offsetHeight : 0;
987      const threshold = window.scrollY + navHeight + 12;
988      let active = targets[0];
989      targets.forEach(function (t) {
990        const section = t.closest('section') || t;
991        if (section.getBoundingClientRect().top + window.scrollY <= threshold) active = t;
992      });
993      anchorLinks.forEach(function (a) {
994        a.classList.toggle('active', a.getAttribute('href') === '#' + active.id);
995      });
996    }
997
998    window.addEventListener('scroll', updateAnchorNav, { passive: true });
999    updateAnchorNav();
1000  }
1001
1002  /* ── 育才中心:督導清單渲染 ── */
1003  const supervisorList = document.getElementById('supervisor-list');
1004  if (supervisorList && typeof teamData !== 'undefined') {
1005    const supervisors = teamData.filter(function (m) { return m.supervision; });
1006
1007    if (!supervisors.length) {
1008      supervisorList.innerHTML =
1009        '<p class="supervisor-empty">督導陣容更新中,歡迎來信洽詢。</p>';
1010    } else {
1011      function esc(v) {
1012        return String(v == null ? '' : v)
1013          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1014          .replace(/"/g, '&quot;');
1015      }
1016      function field(label, value) {
1017        if (!value) return '';
1018        return '<div class="supervisor-field">' +
1019          '<p class="supervisor-field-label">' + label + '</p>' +
1020          '<p class="supervisor-field-value">' + esc(value) + '</p>' +
1021          '</div>';
1022      }
1023      // 督導風格:只取 style 四項「冒號前」的標題(如「梳理個案架構」)
1024      function styleTitles(style) {
1025        if (!style) return '';
1026        const items = ['focus', 'atmosphere', 'transmission', 'gift']
1027          .map(function (k) {
1028            const v = style[k];
1029            return v ? String(v).split(/[::]/)[0].trim() : '';
1030          })
1031          .filter(Boolean);
1032        if (!items.length) return '';
1033        return '<div class="supervisor-field">' +
1034          '<p class="supervisor-field-label">督導風格</p>' +
1035          '<ul class="supervisor-style-list">' +
1036            items.map(function (t) { return '<li>' + esc(t) + '</li>'; }).join('') +
1037          '</ul>' +
1038          '</div>';
1039      }
1040      supervisorList.innerHTML = supervisors.map(function (m) {
1041        const s = m.supervision;
1042        // 頭像沿用後台設定的對焦點;未設定時 photoCropStyle 的預設(置中貼頂)
1043        // 與 CSS 原本的 object-position 一致,故無 crop 者外觀不變。
1044        const cropStyle = typeof window.photoCropStyle === 'function'
1045          ? window.photoCropStyle(m.photoCrop) : '';
1046        const avatar = m.photo
1047          ? '<img class="supervisor-avatar" src="' + esc(m.photo) + '" alt="' + esc(m.name) +
1048            '" loading="lazy" style="' + esc(cropStyle) + '" />'
1049          : '<span class="supervisor-avatar supervisor-avatar--placeholder" aria-hidden="true"></span>';
1050        const tags = Array.isArray(s.tags) ? s.tags : [];
1051        const groupPlans = Array.isArray(s.groupPlans) ? s.groupPlans : [];
1052        return '' +
1053          '<article class="supervisor-card" id="sv-' + m.id + '" data-tags="' + esc(tags.join('|')) +
1054            '" data-group-plans="' + esc(groupPlans.join('|')) + '">' +
1055            '<div class="supervisor-head">' +
1056              avatar +
1057              '<div class="supervisor-headtext">' +
1058                '<h4 class="supervisor-name">' + esc(m.name) + '</h4>' +
1059                '<p class="supervisor-title">' + esc(m.title) + '</p>' +
1060              '</div>' +
1061            '</div>' +
1062            (s.tagline
1063              ? '<div class="supervisor-field">' +
1064                  '<p class="supervisor-field-label">給受督生的一句話</p>' +
1065                  '<p class="supervisor-tagline">「' + esc(s.tagline) + '」</p>' +
1066                '</div>'
1067              : '') +
1068            field('督導取向', s.orientation) +
1069            field('專長服務對象', s.clientGroups) +
1070            styleTitles(s.style) +
1071            '<a class="supervisor-more" href="team-member.html?id=' + m.id + '&view=supervision">看完整介紹 →</a>' +
1072          '</article>';
1073      }).join('');
1074
1075      // ── 快捷篩選標籤(顯示全部+三分類,單選切換)──
1076      const filterHost = document.getElementById('supervisor-filters');
1077      if (filterHost) {
1078        const CATS = ['全職/兼職實習', '線上遠距', '機構/團體督導'];
1079        let activeTag = null; // null = 顯示全部
1080        // 機構/團體督導的三方案導引卡:點方案可再篩選下方名單。
1081        // 方案1/方案2=維持顯示全部機構/團體督導的督導;方案3(機構/學校外部
1082        // 督導)=只顯示 supervision.groupPlans 有標記「方案3」的督導。
1083        let activeGroupPlan = null; // null = 不限方案
1084        const groupGuideBadges = document.querySelectorAll('.group-guide-badge[data-group-plan]');
1085        function renderGroupGuideBadges() {
1086          groupGuideBadges.forEach(function (btn) {
1087            const on = activeGroupPlan === btn.getAttribute('data-group-plan');
1088            btn.setAttribute('aria-pressed', on ? 'true' : 'false');
1089          });
1090        }
1091        function renderChips() {
1092          const chips = [{ v: null, label: '顯示全部' }]
1093            .concat(CATS.map(function (c) { return { v: c, label: c }; }));
1094          filterHost.innerHTML = chips.map(function (c) {
1095            const on = activeTag === c.v;
1096            return '<button type="button" class="supervisor-chip' + (on ? ' is-active' : '') +
1097              '" data-tag="' + (c.v == null ? '' : esc(c.v)) + '"' +
1098              (on ? ' aria-pressed="true"' : ' aria-pressed="false"') + '>' +
1099              esc(c.label) + '</button>';
1100          }).join('');
1101        }
1102        function applyFilter() {
1103          // 機構/團體督導:於名單上方顯示三欄式導引卡
1104          const guide = document.getElementById('group-supervision-guide');
1105          if (guide) guide.hidden = activeTag !== '機構/團體督導';
1106          renderGroupGuideBadges();
1107          const cards = supervisorList.querySelectorAll('.supervisor-card');
1108          let shown = 0;
1109          cards.forEach(function (card) {
1110            const tags = (card.getAttribute('data-tags') || '').split('|').filter(
1110Boolean);
1111            const groupPlans = (card.getAttribute('data-group-plans') || '').split('|').filter(Boolean);
1112            const matchTag = !activeTag || tags.indexOf(activeTag) !== -1;
1113            const matchGroupPlan = !activeGroupPlan || activeGroupPlan !== '方案3' ||
1114              groupPlans.indexOf('方案3') !== -1;
1115            const match = matchTag && matchGroupPlan;
1116            card.hidden = !match;
1117            if (match) shown++;
1118          });
1119          let empty = supervisorList.querySelector('.supervisor-filter-empty');
1120          if (!shown) {
1121            if (!empty) {
1122              empty = document.createElement('p');
1123              empty.className = 'supervisor-empty supervisor-filter-empty';
1124              supervisorList.appendChild(empty);
1125            }
1126            empty.textContent = '目前沒有符合此分類的督導,試著切換其他標籤。';
1127            empty.hidden = false;
1128          } else if (empty) {
1129            empty.hidden = true;
1130          }
1131        }
1132        filterHost.addEventListener('click', function (e) {
1133          const chip = e.target.closest('.supervisor-chip');
1134          if (!chip) return;
1135          const v = chip.getAttribute('data-tag');
1136          activeTag = v ? v : null;
1137          activeGroupPlan = null; // 切換主分類時,方案子篩選重置
1138          renderChips();
1139          applyFilter();
1140        });
1141        groupGuideBadges.forEach(function (btn) {
1142          btn.addEventListener('click', function () {
1143            const plan = btn.getAttribute('data-group-plan');
1144            activeGroupPlan = activeGroupPlan === plan ? null : plan;
1145            applyFilter();
1146            supervisorList.scrollIntoView({ behavior: 'smooth', block: 'start' });
1147          });
1148        });
1149        renderChips();
1150      }
1151    }
1152
1153    // 動態渲染後才處理錨點:原生 hash 會在卡片產生前失效,故手動捲動
1154    if (window.location.hash) {
1155      const target = document.getElementById(window.location.hash.slice(1));
1156      if (target && target.classList.contains('supervisor-card')) {
1157        target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1158        target.classList.add('supervisor-card--highlight');
1159        setTimeout(function () {
1160          target.classList.remove('supervisor-card--highlight');
1161        }, 1800);
1162      }
1163    }
1164  }
1165
1166  // ── 開場按鈕 A:平滑捲動到陣容篩選區(扣掉 sticky nav 高度)──
1167  const routeBrowse = document.getElementById('supervision-route-browse');
1168  if (routeBrowse) {
1169    routeBrowse.addEventListener('click', function (e) {
1170      const target = document.getElementById('supervisor-filters') ||
1171        document.getElementById('supervisor-list');
1172      if (!target) return; // 找不到就退回原生錨點
1173      e.preventDefault();
1174      const nav = document.querySelector('.anchor-nav');
1175      const navH = nav ? nav.offsetHeight : 0;
1176      const top = target.getBoundingClientRect().top + window.scrollY - navH - 16;
1177      window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
1178    });
1179  }
1180
1181})();
1182
1183/* ============================================================
1184   育才中心:課程區(本季主題置頂 + 可篩選課程列表)
1185   ============================================================ */
1186(function () {
1187  var listEl = document.getElementById('course-list');
1188  if (!listEl || typeof courseData === 'undefined') return;
1189
1190  function esc(v) {
1191    return String(v == null ? '' : v)
1192      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1193      .replace(/"/g, '&quot;');
1194  }
1195  function asArray(v) { return Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]); }
1196  function displayAudience(v) { return v === '家長/深度學習者' ? '家長・深度學習者' : v; }
1197
1198  // 狀態 → CSS 修飾字
1199  var STATUS_MOD = { '現正招生': 'open', '即將推出': 'soon', '典藏課程': 'archive' };
1200  // 形式(配送方式)標籤
1201  function formatTags(course) {
1202    return asArray(course.format).map(function (f) {
1203      return '<span class="course-tag course-tag--format">' + esc(f) + '</span>';
1204    }).join('');
1205  }
1206  function statusTags(course) {
1207    var out = '';
1208    if (course.hot) out += '<span class="course-tag course-tag--hot">
1208熱門課程</span>';
1209    if (course.status) {
1210      out += '<span class="course-tag course-tag--status is-' +
1211        (STATUS_MOD[course.status] || 'open') + '">' + esc(course.status) + '</span>';
1212    }
1213    return out;
1214  }
1215
1216  function courseCover(course) {
1217    if (course.cover) {
1218      return '<div class="course-cover" style="background-image:url(' + esc(course.cover) + ')"></div>';
1219    }
1220    // 佔位:季度/系列文字
1221    return '<div class="course-cover course-cover--placeholder">' +
1222      '<span class="course-cover-series">' + esc(course.series || '課程') + '</span>' +
1223      (course.season ? '<span class="course-cover-season">' + esc(course.season) + '</span>' : '') +
1224      '</div>';
1225  }
1226
1227  // 由 instructorMemberId 對應團隊成員照片
1228  function instructorPhoto(course) {
1229    if (!course.instructorMemberId || typeof teamData === 'undefined') return '';
1230    var m = teamData.filter(function (t) { return t.id === course.instructorMemberId; })[0];
1231    return m && m.photo ? m.photo : '';
1232  }
1233
1234  function courseInstructor(course) {
1235    if (!course.instructor) return '';
1236    var photo = instructorPhoto(course);
1237    var avatar = photo
1238      ? '<img class="course-instructor-avatar" src="' + esc(photo) + '" alt="' + esc(course.instructor) + '" loading="lazy" />'
1239      : '<span class="course-instructor-avatar course-instructor-avatar--placeholder" aria-hidden="true"></span>';
1240    var text = esc(course.instructor) + (course.hours ? '<span class="course-instructor-hours">|' + esc(course.hours) + '</span>' : '');
1241    return '<p class="course-card-instructor">' + avatar +
1242      '<span class="course-instructor-text">' + text + '</span></p>';
1243  }
1244
1245  function courseCard(course) {
1246    return '' +
1247      '<a class="course-card" href="course.html?id=' + esc(course.id) + '">' +
1248        courseCover(course) +
1249        '<div class="course-card-body">' +
1250          (course.series ? '<span class="course-card-series">' + esc(course.series) + '</span>' : '') +
1251          '<h4 class="course-card-title">' + esc(course.title) + '</h4>' +
1252          courseInstructor(course) +
1253          '<div class="course-tags">' + formatTags(course) + statusTags(course) + '</div>' +
1254          (course.enrollDeadline ? '<p class="course-card-deadline">報名截止 ' + esc(course.enrollDeadline) + '</p>' : '') +
1255        '</div>' +
1256      '</a>';
1257  }
1258
1259  /* ── 本季主題置頂 ── */
1260  var hasSeason = typeof seasonMeta !== 'undefined';
1261
1262  (function renderSeason() {
1263    var host = document.getElementById('season-feature');
1264    if (!host || !hasSeason) return;
1265    var sm = seasonMeta;
1266    var coverAlt = esc(sm.name) + ' 主視覺';
1267    host.innerHTML =
1268      (sm.cover ? '<button type="button" class="season-feature-cover" data-poster-src="' + esc(sm.cover) + '" data-poster-alt="' + coverAlt + '" aria-label="放大檢視 ' + coverAlt + '">' +
1269        '<img src="' + esc(sm.cover) + '" alt="' + coverAlt + '" loading="lazy" />' +
1270        '<span class="season-feature-zoom" aria-hidden="true">點擊看大圖</span>' +
1271      '</button>' : '') +
1272      '<div class="season-feature-head">' +
1273        '<span class="season-badge">本季主題</span>' +
1274        '<h3 class="season-title">' + esc(sm.season) + '|' + esc(sm.name) + '</h3>' +
1275        (sm.theme ? '<p class="season-theme">' + esc(sm.theme) + '</p>' : '') +
1276        (sm.tagline ? '<p class="season-intro">' + esc(sm.tagline) + '</p>' : '') +
1277      '</div>' +
1278      '<button type="button" class="season-cta" id="season-view-courses">查看本季課程 ↓</button>';
1279
1280    var coverBtn = host.querySelector('.season-feature-cover');
1281    if (coverBtn) {
1282      coverBtn.addEventListener('click', function () {
1283        openPosterLightbox(coverBtn.getAttribute('data-poster-src'), coverBtn.getAttribute('data-poster-alt'));
1284      });
1285    }
1286  })();
1287
1288  /* ── 海報燈箱(點擊看完整大圖)── */
1289  function openPosterLightbox(src, alt) {
1290    if (!src) return;
1291    var lb = document.getElementById('poster-lightbox');
1292    if (!lb) {
1293      lb = document.createElement('div');
1294      lb.id = 'poster-lightbox';
1295      lb.className = 'poster-lightbox';
1296      lb.setAttribute('role', 'dialog');
1297      lb.setAttribute('aria-modal', 'true');
1298      lb.setAttribute('aria-label', '課程海報');
1299      lb.innerHTML =
1300        '<button type="button" class="poster-lightbox-close" aria-label="關閉">×</button>' +
1301        '<img class="poster-lightbox-img" alt="" />';
1302      document.body.appendChild(lb);
1303      var close = function () {
1304        lb.classList.remove('open');
1305        document.body.classList.remove('poster-lightbox-open');
1306      };
1307      lb.querySelector('.poster-lightbox-close').addEventListener('click', close);
1308      lb.addEventListener('click', function (e) { if (e.target === lb) close(); });
1309      document.addEventListener('keydown', function (e) {
1310        if (e.key === 'Escape' && lb.classList.contains('open')) close();
1311      });
1312    }
1313    var img = lb.querySelector('.poster-lightbox-img');
1314    img.src = src;
1315    img.alt = alt || '課程海報';
1316    lb.classList.add('open');
1317    document.body.classList.add('poster-lightbox-open');
1318    lb.querySelector('.poster-lightbox-close').focus();
1319  }
1320
1321  /* ── 篩選 ── */
1322  var FILTERS = [
1323    { key: 'audience', label: '對象', options: ['專業人員', '家長/深度學習者'] },
1324    { key: 'topics', label: '主題', options: ['兒童發展', '親職教養', '創傷', '情緒', '關係'] },
1325    { key: 'series', label: '系列', options: ['開季講座', '大眾/家長系列', '專業進修系列', '深度互動系列'] },
1326  ];
1327  var active = { audience: [], topics: [], series: [], season: null };
1328
1329  function anyActive() {
1330    return FILTERS.some(function (g) { return active[g.key].length; }) || !!active.season;
1331  }
1332
1333  function renderFilters() {
1334    var host = document.getElementById('course-filters');
1335    if (!host) return;
1336    var groupsHtml = FILTERS.map(function (g) {
1337      var chips = g.options.map(function (opt) {
1338        var on = active[g.key].indexOf(opt) !== -1;
1339        return '<button type="button" class="course-chip' + (on ? ' is-active' : '') +
1340          '" data-group="' + g.key + '" data-value="' + esc(opt) + '"' +
1341          (on ? ' aria-pressed="true"' : ' aria-pressed="false"') + '>' +
1342          esc(displayAudience(opt)) + '</button>';
1343      }).join('');
1344      return '<div class="course-filter-group">' +
1345        '<span class="course-filter-label">' + esc(g.label) + '</span>' +
1346        '<div class="course-filter-chips">' + chips + '</div>' +
1347        '</div>';
1348    }).join('');
1349    // 本季主題作用中:可移除標籤
1350    var seasonHtml = active.season
1351      ? '<div class="course-filter-active"><button type="button" class="active-season-chip" id="course-season-clear">本季主題:' +
1352          esc(hasSeason ? seasonMeta.name : active.season) + ' ✕</button></div>'
1353      : '';
1354    var clearHtml = anyActive()
1355      ? '<button type="button" class="course-filter-clear" id="course-filter-clear">清除篩選 ✕</button>'
1356      : '';
1357    host.innerHTML = seasonHtml + groupsHtml + clearHtml;
1358  }
1359
1360  function matches(course) {
1361    if (active.season && course.season !== active.season) return false;
1362    return FILTERS.every(function (g) {
1363      var sel = active[g.key];
1364      if (!sel.length) return true;
1365      var vals = asArray(course[g.key]);
1366      return sel.some(function (s) { return vals.indexOf(s) !== -1; });
1367    });
1368  }
1369
1370  function renderList() {
1371    var shown = courseData.filter(matches);
1372    var countEl = document.getElementById('course-count');
1373    if (countEl) countEl.textContent = '共 ' + shown.length + ' 堂';
1374    if (!shown.length) {
1375      listEl.innerHTML = '<p class="course-empty">目前沒有符合條件的課程,試著調整篩選條件。</p>';
1376      return;
1377    }
1378    listEl.innerHTML = shown.map(courseCard).join('');
1379  }
1380
1381  function scrollToCatalog() {
1382    var target = document.getElementById('course-catalog') || listEl;
1383    var nav = document.querySelector('.anchor-nav');
1384    var navH = nav ? nav.offsetHeight : 0;
1385    var top = target.getBoundingClientRect().top + window.scrollY - navH - 12;
1386    window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
1387  }
1388
1389  document.getElementById('course-filters').addEventListener('click', function (e) {
1390    var chip = e.target.closest('.course-chip');
1391    if (chip) {
1392      var group = chip.getAttribute('data-group');
1393      var value = chip.getAttribute('data-value');
1394      var arr = active[group];
1395      var i = arr.indexOf(value);
1396      if (i === -1) arr.push(value); else arr.splice(i, 1);
1397      renderFilters();
1398      renderList();
1399      return;
1400    }
1401    if (e.target.closest('#course-season-clear')) {
1402      active.season = null;
1403      renderFilters();
1404      renderList();
1405      return;
1406    }
1407    if (e.target.closest('#course-filter-clear')) {
1408      active = { audience: [], topics: [], series: [], season: null };
1409      renderFilters();
1410      renderList();
1411    }
1412  });
1413
1414  // 「查看本季課程」:套用本季篩選並捲動到課程列表
1415  var seasonBtn = document.getElementById('season-view-courses');
1416  if (seasonBtn && hasSeason) {
1417    seasonBtn.addEventListener('click', function () {
1418      active.season = seasonMeta.currentSeason;
1419      renderFilters();
1420      renderList();
1421      scrollToCatalog();
1422    });
1423  }
1424
1425  renderFilters();
1426  renderList();
1427})();
1428
1429/* ============================================================
1430   育才中心:課程詳情頁(course.html?id=)
1431   ============================================================ */
1432(function () {
1433  var host = document.getElementById('course-detail');
1434  if (!host || typeof courseData === 'undefined') return;
1435
1436  function esc(v) {
1437    return String(v == null ? '' : v)
1438      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1439      .replace(/"/g, '&quot;');
1440  }
1441  function asArray(v) { return Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]); }
1442  function displayAudience(v) { return v === '家長/深度學習者' ? '家長・深度學習者' : v; }
1443
1444  var params = new URLSearchParams(window.location.search);
1445  var id = params.get('id');
1446  var course = courseData.filter(function (c) { return c.id === id; })[0];
1447
1448  if (!course) {
1449    host.innerHTML =
1450      '<div class="course-notfound">' +
1451        '<h1>找不到這門課程</h1>' +
1452        '<p>課程可能已下架或連結有誤。</p>' +
1453        '<a href="yucai.html#courses-heading" class="btn btn-primary">回到育才中心課程</a>' +
1454      '</div>';
1455    return;
1456  }
1457
1458  document.title = course.title + ' — 米露谷育才中心';
1459
1460  // 分享網址:優先用分享子網域 /c/:id(有專屬 OG 預覽),未設定則退回本頁網址
1461  var shareBase = (window.MIRUKU_SHARE_BASE || '').replace(/\/+$/, '');
1462  var shareUrl = shareBase
1463    ? shareBase + '/s/' + encodeURIComponent(id)
1464    : window.location.origin + '/course.html?id=' + encodeURIComponent(id);
1465  function shareBar() {
1466    var u = encodeURIComponent(shareUrl);
1467    var line = 'https://social-plugins.line.me/lineit/share?url=' + u;
1468    var fb = 'https://www.facebook.com/sharer/sharer.php?u=' + u;
1469    var ICON_LINE = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12.007 2.07c-5.458 0-9.93 3.952-9.93 8.814 0 4.358 3.86 8.007 9.073 8.698.353.076.833.233.954.534.11.274.072.703.035.98l-.155.925c-.047.274-.218 1.073.94.585 1.157-.488 6.246-3.678 8.522-6.296 1.572-1.724 2.327-3.47 2.327-5.426.001-4.862-4.47-8.814-9.766-8.814zm-2.922 11.4H7.216a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h.583c.322 0 .583.261.583.583v3.76h1.703c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583zm2.339-.583a.583.583 0 0 1-.583.583h-.583a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h.583c.322 0 .583.261.583.583v4.343zm5.133 0a.583.583 0 0 1-.583.583h-.583a.584.584 0 0 1-.476-.244l-1.896-2.56v2.22a.583.583 0 0 1-.583.584h-.583a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h.583c.187 0 .35.085.46.217l1.912 2.587V8.544c0-.322.261-.583.583-.583h.583c.322 0 .583.261.583.583v4.343zm2.922.583h-1.869a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h1.869c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583h-1.286v.876h1.286c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583h-1.286v.877h1.28
14696c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583z"/></svg>';
1470    var ICON_FB = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07c0 6.02 4.39 11.01 10.13 11.93v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8v8.44C19.61 23.08 24 18.09 24 12.07z"/></svg>';
1471    var ICON_COPY = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg>';
1472    return '<div class="course-share">' +
1473      '<span class="course-share-label">分享</span>' +
1474      '<div class="course-share-btns">' +
1475        '<a class="course-share-btn is-line" href="' + line + '" target="_blank" rel="noopener" aria-label="分享到 LINE" title="分享到 LINE">' + ICON_LINE + '</a>' +
1476        '<a class="course-share-btn is-fb" href="' + fb + '" target="_blank" rel="noopener" aria-label="分享到 Facebook" title="分享到 Facebook">' + ICON_FB + '</a>' +
1477        '<button type="button" class="course-share-btn is-copy" data-share-url="' + esc(shareUrl) + '" aria-label="複製課程連結" title="複製課程連結">' + ICON_COPY + '</button>' +
1478      '</div>' +
1479      '</div>';
1480  }
1481
1482  var STATUS_MOD = { '現正招生': 'open', '即將推出': 'soon', '典藏課程': 'archive' };
1483  function tags() {
1484    var out = asArray(course.format).map(function (f) {
1485      return '<span class="course-tag course-tag--format">' + esc(f) + '</span>';
1486    }).join('');
1487    if (course.hot) out += '<span class="course-tag course-tag--hot">熱門課程</span>';
1488    if (course.status) {
1489      out += '<span class="course-tag course-tag--status is-' +
1490        (STATUS_MOD[course.status] || 'open') + '">' + esc(course.status) + '</span>';
1491    }
1492    return out;
1493  }
1494
1495  // 區塊:標題 + 內容;內容空則回傳 ''(自動隱藏)
1496  function block(title, inner) {
1497    if (!inner) return '';
1498    return '<section class="course-block">' +
1499      '<h2 class="course-block-title">' + esc(title) + '</h2>' + inner + '</section>';
1500  }
1501  function infoRow(label, value) {
1502    if (!value) return '';
1503    return '<div class="course-info-row">' +
1504      '<span class="course-info-label">' + esc(label) + '</span>' +
1505      '<span class="course-info-value">' + esc(value) + '</span></div>';
1506  }
1507  function bullets(arr, cls) {
1508    var items = asArray(arr);
1509    if (!items.length) return '';
1510    return '<ul class="' + cls + '">' + items.map(function (t) {
1511      return '<li>' + esc(t) + '</li>';
1512    }).join('') + '</ul>';
1513  }
1514
1515  // 1. 封面
1516  var cover = course.cover
1517    ? '<div class="course-hero-cover"><img src="' + esc(course.cover) + '" alt="' + esc(course.title) + ' 課程海報" /></div>'
1518    : '<div class="course-hero-cover course-hero-cover--placeholder">' +
1519        '<span>' + esc(course.series || '課程') + '</span></div>';
1520
1521  var heroHtml =
1522    '<header class="course-hero">' +
1523      cover +
1524      '<div class="course-hero-text">' +
1525        (course.season ? '<span class="course-hero-season">' + esc(course.season) + '</span>' : '') +
1526        '<div class="course-hero-head">' +
1527          '<h1 class="course-hero-title">' + esc(course.title) + '</h1>' +
1528          shareBar() +
1529        '</div>' +
1530        '<div class="course-tags course-hero-tags">' + tags() + '</div>' +
1531      '</div>' +
1532    '</header>';
1533
1534  // 2. 課程資訊
1535  // 講師若為本所團隊成員(instructorMemberId),姓名連到其個人頁;並嘗試取回其團隊資料(頭像/職稱)
1536  var memberId = course.instructorMemberId;
1537  var member = (memberId && typeof teamData !== 'undefined')
1538    ? teamData.filter(function (m) { return m.id === memberId; })[0] : null;
1539  var memberHref = memberId ? 'team-member.html?id=' + esc(memberId) : '';
1540  var instructorRow = memberId && course.instructor
1541    ? '<div class="course-info-row"><span class="course-info-label">講師</span>' +
1542        '<span class="course-info-value"><a class="course-instructor-link" href="' + memberHref + '">' + esc(course.instructor) + '</a></span></div>'
1543    : infoRow('講師', course.instructor);
1544  var infoHtml =
1545    instructorRow +
1546    infoRow('課程對象', asArray(course.audience).map(displayAudience).join('、')) +
1547    infoRow('課程形式', asArray(course.format).join('、')) +
1548    infoRow('上課時段', course.schedule || course.startDate) +
1549    infoRow('上課地點', course.location) +
1550    infoRow('人數', course.capacity) +
1551    infoRow('費用', course.price) +
1552    infoRow('報名截止', course.enrollDeadline) +
1553    infoRow('研習證明', course.certificate);
1554  var infoBlock = infoHtml
1555    ? '<section class="course-block"><h2 class="course-block-title">課程資訊</h2>' +
1556        '<div class="course-info">' + infoHtml + '</div></section>'
1557    : '';
1558
1559  // 3. 簡介
1560  var introBlock = block('課程簡介',
1561    course.intro ? '<p class="course-intro">' + esc(course.intro) + '</p>' : '');
1562
1563  // 4. 特色
1564  var featureBlock = block('課程特色', bullets(course.features, 'course-features'));
1565
1566  // 5. 講師資訊
1567  var bioInner = '';
1568  if (memberId) {
1569    // 講師為本所團隊成員:整張卡片可點,連到個人頁(頭像/職稱取自團隊資料)
1570    var cardName = esc(member && member.name ? member.name : course.instructor);
1571    var cardTitle = member
1572      ? [member.title, member.role].filter(Boolean).map(esc).join('|')
1573      : '米露谷團隊成員';
1574    var avatar = (member && member.photo)
1575      ? '<span class="course-bio-avatar" style="background-image:url(' + esc(member.photo) + ')"></span>'
1576      : '<span class="course-bio-avatar course-bio-avatar--empty"></span>';
1577    bioInner =
1578      '<a class="course-bio-card" href="' + memberHref + '">' +
1579        avatar +
1580        '<span class="course-bio-cardinfo">' +
1581          '<span class="course-bio-cardname">' + cardName + '</span>' +
1582          (cardTitle ? '<span class="course-bio-cardtitle">' + cardTitle + '</span>' : '') +
1583        '</span>' +
1584        '<span class="course-bio-cardbtn">查看完整介紹 →</span>' +
1585      '</a>';
1586  } else {
1587    var bio = course.instructorBio || {};
1588    var bioRows =
1589      infoRow('學經歷', bio.background) +
1590      infoRow('專長', bio.expertise) +
1591      infoRow('擅長主題', bio.topics) +
1592      infoRow('曾開課程', bio.pastCourses);
1593    if (bioRows || bio.philosophy) {
1594      bioInner = '<div class="course-bio">' +
1595        (course.instructor ? '<p class="course-bio-name">' + esc(course.instructor) + '</p>' : '') +
1596        (bioRows ? '<div class="course-info">' + bioRows + '</div>' : '') +
1597        (bio.philosophy ? '<blockquote class="course-bio-quote">「' + esc(bio.philosophy) + '」</blockquote>' : '') +
1598        '</div>';
1599    }
1600  }
1601  var bioBlock = block('講師資訊', bioInner);
1602
1603  // 6. 適合對象
1604  var suitableBlock = block('適合對象', bullets(course.suitableFor, 'course-suitable'));
1605
1606  // 7. 課程成果
1607  var outcomesBlock = block('課程成果',
1608    course.outcomes ? '<p class="course-intro">' + esc(course.outcomes) + '</p>' : '');
1609
1610  // 8. 學員回饋
1611  var testis = asArray(course.testimonials);
1612  var testiInner = testis.length
1613    ? '<div class="course-testimonials">' + testis.map(function (t) {
1614        var text = typeof t === 'string' ? t : (t.text || '');
1615        var from = typeof t === 'object' && t.from ? t.from : '';
1616        if (!text) return '';
1617        return '<blockquote class="course-testimonial">' +
1618          '<p>「' + esc(text) + '」</p>' +
1619          (from ? '<cite>— ' + esc(from) + '</cite>' : '') +
1620          '</blockquote>';
1621      }).join('') + '</div>'
1622    : '';
1623  var testiBlock = block('學員回饋', testiInner);
1624
1625  // 9. 相關課程
1626  var relIds = asArray(course.related);
1627  var relCourses = relIds.map(function (rid) {
1628    return courseData.filter(function (c) { return c.id === rid; })[0];
1629  }).filter(Boolean);
1630  var relInner = relCourses.length
1631    ? '<div class="course-related">' + relCourses.map(function (c) {
1632        return '<a class="course-related-card" href="course.html?id=' + esc(c.id) + '">' +
1633          (c.series ? '<span class="course-card-series">' + esc(c.series) + '</span>' : '') +
1634          '<span class="course-related-title">' + esc(c.title) + '</span>' +
1635          '<span class="course-related-arrow" aria-hidden="true">→</span>' +
1636          '</a>';
1637      }).join('') + '</div>'
1638    : '';
1639  var relBlock = block('相關課程', relInner);
1640
1641  // 報名與退費規範(refund 全文,換行保留:空行分段、單一換行 → <br>)
1642  function multiline(text) {
1643    return String(text).split(/\n{2,}/).map(function (p) {
1644      return '<p>' + esc(p).replace(/\n/g, '<br>') + '</p>';
1645    }).join('');
1646  }
1647  var refundBlock = block('報名與退費規範',
1648    course.refund ? '<div class="course-refund">' + multiline(course.refund) + '</div>' : '');
1649
1650  // CTA(依狀態調整文案)
1651  var ctaText, ctaNote;
1652  if (course.status === '典藏課程') {
1653    ctaText = '洽詢未來場次'; ctaNote = '本梯次已結束,歡迎來信詢問未來開課資訊。';
1654  } else if (course.status === '即將推出') {
1655    ctaText = '我要洽詢'; ctaNote = '課程準備中,歡迎先來信洽詢,開放報名時將優先通知。';
1656  } else {
1657    ctaText = '我要報名 / 洽詢'; ctaNote = '報名與課程相關問題,歡迎透過聯絡頁面或 LINE 與我們聯繫。';
1658  }
1659  var enrollIsUrl = /^https?:\/\//i.test(course.enroll || '');
1660  var primaryBtn = (enrollIsUrl && course.status === '現正招生')
1661    ? '<a href="' + esc(course.enroll) + '" target="_blank" rel="noopener" class="btn btn-primary">前往報名 ↗</a>'
1662    : '<a href="contact.html" class="btn btn-primary">' + esc(ctaText) + '</a>';
1663  var enrollNote = (!enrollIsUrl && course.enroll)
1664    ? '<p class="course-cta-enroll">' + esc(course.enroll) + '</p>' : '';
1665  var ctaHtml =
1666    '<div class="course-cta">' +
1667      '<p class="course-cta-note">' + esc(ctaNote) + '</p>' +
1668      enrollNote +
1669      '<div class="course-cta-btns">' +
1670        primaryBtn +
1671        '<a href="yucai.html#courses-heading" class="btn btn-outline">回到課程列表</a>' +
1672      '</div>' +
1673    '</div>';
1674
1675  host.innerHTML =
1676    heroHtml + infoBlock + introBlock + featureBlock + bioBlock +
1677    suitableBlock + outcomesBlock + testiBlock + relBlock + refundBlock + ctaHtml;
1678
1679  // 複製連結
1680  var copyBtn = host.querySelector('.course-share-btn.is-copy');
1681  if (copyBtn) {
1682    var copyOrig = copyBtn.innerHTML;
1683    var ICON_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>';
1684    copyBtn.addEventListener('click', function () {
1685      var t = copyBtn.getAttribute('data-share-url');
1686      if (navigator.clipboard && navigator.clipboard.writeText) {
1687        navigator.clipboard.writeText(t).then(function () {
1688          copyBtn.innerHTML = ICON_CHECK;
1689          copyBtn.classList.add('is-copied');
1690          copyBtn.setAttribute('title', '已複製 ✓');
1691          setTimeout(function () {
1692            copyBtn.innerHTML = copyOrig;
1693            copyBtn.classList.remove('is-copied');
1694            copyBtn.setAttribute('title', '複製課程連結');
1695          }, 1600);
1696        });
1697      } else {
1698        window.prompt('複製此連結:', t);
1699      }
1700    });
1701  }
1702})();
1703
1704/* ============================================================
1705   Hero blob — SVG path morph + entrance animation
1706   ============================================================ */
1707(function () {
1708  var svgs = document.querySelectorAll('.hero [data-blob]');
1709  if (!svgs.length) return;
1710
1711  var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1712
1713  function blobPath(cx, cy, r, n, amp, t, phase) {
1714    var pts = [];
1715    for (var i = 0; i < n; i++) {
1716      var a = (i / n) * Math.PI * 2;
1717      var radius = r * (1 + amp * Math.sin(t + phase + i * 1.7));
1718      pts.push({ x: cx + Math.cos(a) * radius, y: cy + Math.sin(a) * radius });
1719    }
1720    var d = 'M ' + pts[0].x.toFixed(2) + ',' + pts[0].y.toFixed(2);
1721    for (var j = 0; j < n; j++) {
1722      var cur = pts[j];
1723      var next = pts[(j + 1) % n];
1724      var prev = pts[(j - 1 + n) % n];
1725      var nn = pts[(j + 2) % n];
1726      var c1x = cur.x + (next.x - prev.x) / 6;
1727      var c1y = cur.y + (next.y - prev.y) / 6;
1728      var c2x = next.x - (nn.x - cur.x) / 6;
1729      var c2y = next.y - (nn.y - cur.y) / 6;
1730      d += ' C ' + c1x.toFixed(2) + ',' + c1y.toFixed(2)
1731         + ' ' + c2x.toFixed(2) + ',' + c2y.toFixed(2)
1732         + ' ' + next.x.toFixed(2) + ',' + next.y.toFixed(2);
1733    }
1734    return d + ' Z';
1735  }
1736
1737  var blobs = Array.prototype.map.call(svgs, function (svg) {
1738    return {
1739      path: svg.querySelector('path'),
1740      cx: +svg.dataset.cx, cy: +svg.dataset.cy,
1741      r: +svg.dataset.r, n: +svg.dataset.n,
1742      amp: +svg.dataset.amp,
1743      speed: +svg.dataset.speed,
1744      phase: +svg.dataset.phase
1745    };
1746  });
1747
1748  /* Seed an initial path so blob isn't blank during fade-in */
1749  blobs.forEach(function (b) {
1750    b.path.setAttribute('d', blobPath(b.cx, b.cy, b.r, b.n, b.amp, 0, b.phase));
1751  });
1752
1753  if (!reduceMotion) {
1754    var start = performance.now();
1755    var tick = function (now) {
1756      var elapsed = (now - start) / 1000;
1757      blobs.forEach(function (b) {
1758        b.path.setAttribute('d', blobPath(b.cx, b.cy, b.r, b.n, b.amp, elapsed * b.speed, b.phase));
1759      });
1760      requestAnimationFrame(tick);
1761    };
1762    requestAnimationFrame(tick);
1763  }
1764
1765  /* Entrance — toggle on next frame so CSS transition fires */
1766  requestAnimationFrame(function () {
1767    requestAnimationFrame(function () {
1768      document.querySelectorAll('.hero .hero-blob-layer, .hero .hero-card')
1769        .forEach(function (el) { el.classList.add('is-in'); });
1770    });
1771  });
1772})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.