PageSourceSearch

https://openmuseum.tw/validUser?from=/

html openmuseum.tw collected 2026-10-02 10:30:38 UTC 124,681 bytes, 1,039 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4	<meta charset="UTF-8">
5	<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6	<title>數位文化中心</title>
7	<link rel="icon" href="/auth-assets-7q2k9x4m/favicon.png">
8	<link rel="stylesheet" href="/auth-assets-7q2k9x4m/auth.css">
9	<style nonce="OEJNNdEDZn0oGoyYA3Jbug">.captcha-zone{display:flex;flex-direction:column;align-items:center;gap:.55rem}.tsr-hidden{display:none}.cap-fallback-note{font-size:12px;line-height:1.4;opacity:.75;margin:0;text-align:left}.cap-toggle{font-size:12.5px}.cap-back{font-size:12.5px}.cap-help{font-size:13px;text-align:center}</style>
10	<style nonce="OEJNNdEDZn0oGoyYA3Jbug">.cap-bar{display:flex;align-items:center;justify-content:center;gap:13px;flex-wrap:wrap;margin-top:4px}
11.cap-switch{display:flex;flex-direction:column;gap:3px;align-items:flex-start;text-align:left;min-width:0}
12.cap-switch:empty{display:none}
13.cd-row{display:flex;flex-direction:column;align-items:center;gap:1px;margin:0;position:relative}
14.cd-row[hidden]{display:none}
15.cd-ring-wrap{position:relative;width:42px;height:42px;flex:none;cursor:help}
16.cd-ring-wrap svg{display:block}
17.cd-arc{transition:stroke-dashoffset .5s linear,stroke .3s}
18.cd-num{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:500;font-variant-numeric:tabular-nums}
19.cd-state{font-size:11px;opacity:.6;text-align:center;white-space:nowrap}
20.cd-tip{position:absolute;left:0;bottom:calc(100% + 8px);width:240px;max-width:78vw;background:rgba(18,20,26,.96);color:#fff;font-size:12px;line-height:1.5;padding:8px 10px;border-radius:8px;opacity:0;visibility:hidden;transition:opacity .15s;pointer-events:none;z-index:30}
21.cd-ring-wrap:hover ~ .cd-tip,.cd-ring-wrap:focus ~ .cd-tip{opacity:1;visibility:visible}
22.cd-expired{display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;padding:10px 0 4px}
23.cd-expired.tsr-hidden{display:none}
24.cd-expired .cd-ring-wrap{width:50px;height:50px}
25.cd-expired .cd-num{color:#E24B4A}
26.cd-exp-title{font-size:1.05rem;margin:.2rem 0 0}
27.cd-exp-body{margin:0}
28.cd-locked > form{display:none}
29@media (prefers-reduced-motion:reduce){.cd-arc{transition:none}}</style>
vendor: 84 bytes, lines 29-30
29
30	<script src="https://www.google.com/recaptcha/api.js?hl=en" async defer></script>
31</head>
32<body data-layout="split">
33	<div class="bg"></div>
34
35	<header class="topbar">
36		<a class="brand" data-link="official" href="https://ascdc.sinica.edu.tw/" target="_blank" rel="noopener noreferrer">
37			<img class="brand-logo" src="/auth-assets-7q2k9x4m/logo-ascdc.png" alt="">
38			<span class="brand-name" data-i18n="brand">Academia Sinica Center for Digital Cultures</span>
39		</a>
40		<div class="controls">
41			<div class="lang-switch"><select id="langSelect" aria-label="Language"></select></div>
42			<button class="theme-toggle" id="themeToggle" type="button" aria-label="Toggle theme">
43				<svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1111.2 3 7 7 0 0021 12.8z"/></svg>
44				<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19"/></svg>
45			</button>
46		</div>
47	</header>
48
49	<main class="scene">
50		<aside class="scene-brand" aria-hidden="true">
51			<img class="scene-logo" src="/auth-assets-7q2k9x4m/logo-ascdc.png" alt="">
52			<div class="scene-id"><span class="scene-name" data-i18n="brand">Academia Sinica Center for Digital Cultures</span><span class="scene-tag" data-i18n="org">Academia Sinica</span></div>
53			<div class="scene-art" aria-hidden="true"></div>
54		</aside>
55		<div class="scene-main">
56		<section class="card">
57			<img class="card-logo" src="/auth-assets-7q2k9x4m/logo-ascdc.png" alt="">
58			<h1 class="card-title" data-i18n="login_title">Identity Verification</h1>
59			<p class="card-desc" data-i18n="login_desc">Please sign in to continue to the page you requested.</p>
60
61			<form action="" method="post" autocomplete="off" novalidate>
62				<input type="hidden" name="token" value="17406a004ba69562a15d21907ae8f6e2708d6b18443722848215d2706d663c19">
63
64				<div class="alert" role="alert">
65					<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v6M12 16.5v.5"/></svg>
66					<span class="alert-msg"></span>
67				</div>
68				<div class="captcha-zone">
69					<div class="g-recaptcha" data-sitekey="6Ld8ibYUAAAAAHM8poK5jPvY897LD_aSja2xrtz-" data-callback="__cdRcSolved" data-expired-callback="__cdRcExpired"></div>
70					<!-- Tessera:$tesseraPrimary 時直接顯示;否則預設隱藏,boot.js 在 reCAPTCHA 載入失敗(auto)/使用者手動(manual)時切換。proof 隨表單 POST。 -->
71					<tessera-widget class="tsr-hidden" lang="en" challenge="{&quot;ticket&quot;:&quot;eyJkaWZmaWN1bHR5IjoxOCwiZXhwaXJlc19hdCI6MTc5MDkzNzIxNiwiaXBfcHJlZml4X2hpbnQiOiI4MS4xNi4zLjAvMjQiLCJpc3N1ZWRfYXQiOjE3OTA5MzcwMzYsInJhbmRvbV9zZWVkIjoiYzk4NDUwNTMzODg5ZmZkNTgwMjNkZjI4Y2M3YTNmZWYiLCJzY29wZSI6Ii92YWxpZFVzZXIiLCJ0aWNrZXRfaWQiOiJLNDdyRXZTdENBWFJxbTlZdkNYbWx3IiwidWFfaGFzaCI6Im5vbmUiLCJ2ZXJzaW9uIjoxfQ.Zo3RuYbenBjY98xRC8VqIuTfZyce20-atktYmp1dp8Q&quot;,&quot;payload&quot;:{&quot;version&quot;:1,&quot;ticket_id&quot;:&quot;K47rEvStCAXRqm9YvCXmlw&quot;,&quot;issued_at&quot;:1790937036,&quot;expires_at&quot;:1790937216,&quot;scope&quot;:&quot;/validUser&quot;,&quot;difficulty&quot;:18,&quot;random_seed&quot;:&quot;c98450533889ffd58023df28cc7a3fef&quot;,&quot;ip_prefix_hint&quot;:&quot;81.16.3.0/24&quot;,&quot;ua_hash&quot;:&quot;none&quot;}}" challenge-url="/validUser?from=/" field-prefix="tessera"></tessera-widget>
72					<!-- 倒數環(左)與切換提示/切回鈕(右)併排同一列,省垂直空間。cd-row 任何 captcha 頁都在;cap-switch 僅 Tessera 可用時。 -->
73					<div class="cap-bar">
74						<div class="cd-row" hidden>
75							<div class="cd-ring-wrap" tabindex="0" role="timer" aria-live="off" aria-label="" aria-describedby="cd-tip">
76								<svg viewBox="0 0 42 42" width="42" height="42" aria-hidden="true">
77									<circle cx="21" cy="21" r="18" fill="none" stroke="rgba(128,128,128,.25)" stroke-width="3.5"></circle>
78									<circle class="cd-arc" cx="21" cy="21" r="18" fill="none" stroke="#1D9E75" stroke-width="3.5" stroke-linecap="round" transform="rotate(-90 21 21)" stroke-dasharray="113.1" stroke-dashoffset="0"></circle>
79								</svg>
80								<span class="cd-num">--:--</span>
81							</div>
82							<span class="cd-state"></span>
83							<span class="cd-tip" id="cd-tip" role="tooltip">How long the current verification stays valid. If it expires, refresh the page to verify again — your account name is kept.</span>
84						</div>
85						<div class="cap-switch">
86							<!-- primary 不顯示;fallback 預設隱藏,切換時 boot.js 依 auto/manual 取 data-* 文字後顯示。 -->
87							<p class="cap-fallback-note tsr-hidden" data-auto="The CAPTCHA couldn’t load, so we switched to built-in verification." data-manual="Switched to built-in verification.">The CAPTCHA couldn’t load, so we switched to built-in verification.</p>
88							<a href="#" class="cap-toggle tsr-hidden">Use built-in verification</a>
89							<!-- 切回 Google reCAPTCHA:僅手動切到 Tessera 後顯示(auto 失敗不提供)。 -->
90							<a href="#" class="cap-back tsr-hidden">Switch back to Google reCAPTCHA</a>
91						</div>
92					</div>
93					<!-- 連續驗證失敗(>=2 次)才顯示的求助連結;導向聯絡我們。 -->
94					<a class="cap-help tsr-hidden" data-link="contact" href="https://ascdc.sinica.edu.tw/contact" target="_blank" rel="noopener noreferrer">Still can’t verify? Contact us</a>
95				</div>
96
97				<button class="btn btn-primary" type="submit">
98					<span class="label" data-i18n="btn_continue">Continue</span>
99				</button>
100			</form>
101			<div class="cd-expired tsr-hidden" role="status" aria-live="polite">
102				<div class="cd-ring-wrap">
103					<svg viewBox="0 0 42 42" width="50" height="50" aria-hidden="true">
104						<circle cx="21" cy="21" r="18" fill="none" stroke="rgba(128,128,128,.25)" stroke-width="3.5"></circle>
105						<circle cx="21" cy="21" r="18" fill="none" stroke="#E24B4A" stroke-width="3.5" stroke-linecap="round" transform="rotate(-90 21 21)" stroke-dasharray="113.1" stroke-dashoffset="113.1"></circle>
106					</svg>
107					<span class="cd-num">
108						<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#E24B4A" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"></circle><path d="M9 9l6 6M15 9l-6 6"></path></svg>
109					</span>
110				</div>
111				<p class="card-title cd-exp-title">Verification has expired</p>
112				<p class="card-desc cd-exp-body">For your security, refresh the page to get a new verification. Your account name is kept; you’ll need to re-enter your password.</p>
113				<button type="button" class="btn btn-primary cd-reload">
114					<span class="label cd-reload-label">Refresh</span>
115				</button>
116			</div>
117		</section>
118
119		<footer class="footer">
120			<div class="links">
121				<a data-link="about" href="https://ascdc.sinica.edu.tw/about" target="_blank" rel="noopener noreferrer" data-i18n="nav_about">About</a>
122				<a data-link="contact" href="https://ascdc.sinica.edu.tw/contact" target="_blank" rel="noopener noreferrer" data-i18n="nav_contact">Contact</a>
123			</div>
124			<!-- 英文 brand 本身即全稱(含 Academia Sinica),用 brand 避免「機構 · 」把名稱切半;其餘語言機構與中心是分開片語,用 tagline 保留「機構 · 中心」。語言切換會重載,故依 $uiLang 分流正確。 -->
125			<div class="copyright">© 2026 <span data-i18n="brand">Academia Sinica Center for Digital Cultures</span>. <span data-i18n="footer_rights">All rights reserved.</span></div>
126		</footer>
127		</div>
128	</main>
129
130	
130<script src="/auth-assets-7q2k9x4m/showcase.js"></script>
130
131	
131<script src="/auth-assets-7q2k9x4m/fonts.js"></script>
131
132	
132<script src="/auth-assets-7q2k9x4m/auth.js"></script>
132
133	<!-- Tessera fallback widget + boot:固定走 /next.php?tessera=…(同源,CSP script-src 'self' 放行)。
134	     用 /next.php 絕對路徑 → 不論本頁 URL 是 /validUser 或 /next.php 都載得到
135	     (/validUser?tessera=… 不被 HAProxy 的 ?from= rewrite 處理,會 404)。 -->
136	
136<script nonce="OEJNNdEDZn0oGoyYA3Jbug">/* Tessera widget — 自含 drop-in 版(由 widget/build.mjs 組裝,請勿手改)。
137   ASCDC 原創。「我不是機器人」PoW 人機驗證,WCAG 2.2 AA + i18n。
138   注意:預設用 Blob Worker 求解,CSP 需允許 worker-src 'self' blob:;
139   若 CSP 僅 'self',請以 <tessera-widget worker-src="/path/tessera-worker.js"> 指定外部 worker。 */
140/* Tessera — 純 JS SHA-256(同步)。演算法為標準 SHA-256(NIST FIPS 180-4,公有領域);此為 ASCDC 自有實作(非宣稱演算法原創)。
141   用途:當瀏覽器 WebCrypto `crypto.subtle` 不可用時(非安全內容,例如純 HTTP、非 localhost)
142   仍能算 PoW 雜湊 → widget 在任何情境都能驗證,不會因「非安全內容」而失敗。
143   標準 SHA-256:輸出與 WebCrypto / PHP hash('sha256',…,true) / 後端逐位元組一致(已對拍驗證)。
144   接受字串(UTF-8 編碼)或 Uint8Array,回傳 32-byte Uint8Array。 */
145(function (global) {
146  'use strict';
147  var K = new Int32Array([
148    0x428a2f98|0,0x71374491|0,0xb5c0fbcf|0,0xe9b5dba5|0,0x3956c25b|0,0x59f111f1|0,0x923f82a4|0,0xab1c5ed5|0,
149    0xd807aa98|0,0x12835b01|0,0x243185be|0,0x550c7dc3|0,0x72be5d74|0,0x80deb1fe|0,0x9bdc06a7|0,0xc19bf174|0,
150    0xe49b69c1|0,0xefbe4786|0,0x0fc19dc6|0,0x240ca1cc|0,0x2de92c6f|0,0x4a7484aa|0,0x5cb0a9dc|0,0x76f988da|0,
151    0x983e5152|0,0xa831c66d|0,0xb00327c8|0,0xbf597fc7|0,0xc6e00bf3|0,0xd5a79147|0,0x06ca6351|0,0x14292967|0,
152    0x27b70a85|0,0x2e1b2138|0,0x4d2c6dfc|0,0x53380d13|0,0x650a7354|0,0x766a0abb|0,0x81c2c92e|0,0x92722c85|0,
153    0xa2bfe8a1|0,0xa81a664b|0,0xc24b8b70|0,0xc76c51a3|0,0xd192e819|0,0xd6990624|0,0xf40e3585|0,0x106aa070|0,
154    0x19a4c116|0,0x1e376c08|0,0x2748774c|0,0x34b0bcb5|0,0x391c0cb3|0,0x4ed8aa4a|0,0x5b9cca4f|0,0x682e6ff3|0,
155    0x748f82ee|0,0x78a5636f|0,0x84c87814|0,0x8cc70208|0,0x90befffa|0,0xa4506ceb|0,0xbef9a3f7|0,0xc67178f2|0
156  ]);
157  var _enc = (typeof TextEncoder !== 'undefined') ? new TextEncoder() : null;
158  function utf8(str) {
159    if (_enc) return _enc.encode(str);
160    // 後援(無 TextEncoder 的舊環境):手動 UTF-8
161    var out = [], i, c;
162    for (i = 0; i < str.length; i++) {
163      c = str.charCodeAt(i);
164      if (c < 0x80) out.push(c);
165      else if (c < 0x800) { out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); }
166      else if (c < 0xd800 || c >= 0xe000) { out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }
167      else { i++; c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }
168    }
169    return new Uint8Array(out);
170  }
171  function ROTR(n, x) { return (x >>> n) | (x << (32 - n)); }
172
173  function sha256(data) {
174    var msg = (typeof data === 'string') ? utf8(data) : data;
175    var len = msg.length;
176    var bitLenHi = Math.floor(len / 0x20000000);      // (len*8) 高 32 bit
177    var bitLenLo = (len << 3) >>> 0;                  // (len*8) 低 32 bit
178    var withOne = len + 1;
179    var pad = (56 - (withOne % 64) + 64) % 64;
180    var total = withOne + pad + 8;
181    var m = new Uint8Array(total);
182    m.set(msg);
183    m[len] = 0x80;
184    m[total - 8] = (bitLenHi >>> 24) & 0xff; m[total - 7] = (bitLenHi >>> 16) & 0xff;
185    m[total - 6] = (bitLenHi >>> 8) & 0xff;  m[total - 5] = bitLenHi & 0xff;
186    m[total - 4] = (bitLenLo >>> 24) & 0xff; m[total - 3] = (bitLenLo >>> 16) & 0xff;
187    m[total - 2] = (bitLenLo >>> 8) & 0xff;  m[total - 1] = bitLenLo & 0xff;
188
189    var H0 = 0x6a09e667|0, H1 = 0xbb67ae85|0, H2 = 0x3c6ef372|0, H3 = 0xa54ff53a|0,
190        H4 = 0x510e527f|0, H5 = 0x9b05688c|0, H6 = 0x1f83d9ab|0, H7 = 0x5be0cd19|0;
191    var w = new Int32Array(64), off, i;
192    for (off = 0; off < total; off += 64) {
193      for (i = 0; i < 16; i++) {
194        w[i] = (m[off + i*4] << 24) | (m[off + i*4 + 1] << 16) | (m[off + i*4 + 2] << 8) | (m[off + i*4 + 3]);
195      }
196      for (i = 16; i < 64; i++) {
197        var x15 = w[i-15], x2 = w[i-2];
198        var s0 = ROTR(7, x15) ^ ROTR(18, x15) ^ (x15 >>> 3);
199        var s1 = ROTR(17, x2) ^ ROTR(19, x2) ^ (x2 >>> 10);
200        w[i] = (w[i-16] + s0 + w[i-7] + s1) | 0;
201      }
202      var a=H0,b=H1,c=H2,d=H3,e=H4,f=H5,g=H6,h=H7;
203      for (i = 0; i < 64; i++) {
204        var S1 = ROTR(6, e) ^ ROTR(11, e) ^ ROTR(25, e);
205        var ch = (e & f) ^ (~e & g);
206        var t1 = (h + S1 + ch + K[i] + w[i]) | 0;
207        var S0 = ROTR(2, a) ^ ROTR(13, a) ^ ROTR(22, a);
208        var maj = (a & b) ^ (a & c) ^ (b & c);
209        var t2 = (S0 + maj) | 0;
210        h=g; g=f; f=e; e=(d + t1)|0; d=c; c=b; b=a; a=(t1 + t2)|0;
211      }
212      H0=(H0+a)|0; H1=(H1+b)|0; H2=(H2+c)|0; H3=(H3+d)|0;
212 H4=(H4+e)|0; H5=(H5+f)|0; H6=(H6+g)|0; H7=(H7+h)|0;
213    }
214    var Hs = [H0,H1,H2,H3,H4,H5,H6,H7], out = new Uint8Array(32);
215    for (i = 0; i < 8; i++) {
216      out[i*4] = (Hs[i] >>> 24) & 0xff; out[i*4+1] = (Hs[i] >>> 16) & 0xff;
217      out[i*4+2] = (Hs[i] >>> 8) & 0xff; out[i*4+3] = Hs[i] & 0xff;
218    }
219    return out;
220  }
221
222  global.tesseraSha256 = sha256;
223  if (typeof module !== 'undefined' && module.exports) module.exports = sha256;
224})(typeof self !== 'undefined' ? self : typeof globalThis !== 'undefined' ? globalThis : this);
225
226/* =============================================================================
227 * Tessera widget — <tessera-widget> 自訂元件(Web Component)
228 * ASCDC 原創。「我不是機器人」人機驗證 UI;比 reCAPTCHA/ALTCHA 更精緻,WCAG 2.2 AA + i18n。
229 *
230 * 此檔是「元件邏輯原始碼」。build.mjs 會把下列佔位符換成內嵌資產,
231 * 產出自含、零額外請求、可單檔 drop-in 的 widget/tessera.js:
232 *   '__TESSERA_CSS__'        → tessera.css 全文(字串)
233 *   __TESSERA_I18N__         → i18n.json 物件
234 *   '__TESSERA_WORKER_SRC__' → vendored miner + worker 合併原始碼(字串,供 Blob Worker)
235 *
236 * 設計要點:
237 *   - Shadow DOM 封裝樣式;hidden 欄位寫在 light DOM(才會隨表單送出)。
238 *   - role="checkbox" + 鍵盤操作 + aria-live 狀態(WCAG 4.1.2 / 2.1.1 / 4.1.3)。
239 *   - 求解優先用 Worker(不卡 UI);Worker 不可用時 fallback 主執行緒分塊求解。
240 *   - 後端唯一可信:widget 只負責求 nonce + 帶回 ticket/nonce,通關由後端數學驗證。
241 * ===========================================================================*/
242(function () {
243  'use strict';
244
245  if (typeof window === 'undefined' || !window.customElements) return;
246  if (customElements.get('tessera-widget')) return;
247
248  var CSS = "/* =============================================================================\n * Tessera widget 樣式 — 「我不是機器人」人機驗證\n * ASCDC 原創。設計目標:比 reCAPTCHA / ALTCHA 更精緻,且符合 WCAG 2.2 AA。\n *\n * 無障礙重點:\n *   - 對比:文字/邊框 ≥ 4.5:1、≥ 3:1(非文字),深淺色都達標。\n *   - 焦點::focus-visible 有 2px 高對比外框(1.4.11 / 2.4.7 / 2.4.13)。\n *   - 目標大小:可點區 ≥ 24×24 CSS px(2.5.8 Target Size Minimum)。\n *   - 動態偏好:prefers-reduced-motion 關閉旋轉/位移動畫(2.3.3)。\n *   - 主題:prefers-color-scheme + [data-theme] 覆寫;RTL 由 :host([dir=rtl])。\n *\n * 這份 CSS 是「樣式唯一真實來源」;tessera.js 內嵌同一份字串以便零額外請求 drop-in。\n * 改樣式請改這裡,再用 build 同步(或直接覆寫 CSS 變數客製主題)。\n * ===========================================================================*/\n\n:host,\n.tessera-scope {\n  /* ---- 可客製主題變數(覆寫這些即可改色,不必動結構)---- */\n  --tsr-font: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans TC\",\n    \"Microsoft JhengHei\", Roboto, Helvetica, Arial, sans-serif;\n  --tsr-radius: 14px;\n  --tsr-bg: #ffffff;\n  --tsr-bg-2: #f7f8fa;\n  --tsr-fg: #1d2433;          /* 主文字,對白底 > 12:1 */\n  --tsr-fg-muted: #5b6472;    /* 次文字,對白底 ≥ 4.6:1 */\n  --tsr-border: #c7cdd6;      /* 邊框,對白底 ≥ 3:1 */\n  --tsr-accent: #4f46e5;      /* 主色(靛)*/\n  --tsr-accent-2: #06b6d4;    /* 漸層副色(青)*/\n  --tsr-accent-fg: #ffffff;\n  --tsr-success: #0f9d6b;     /* 成功綠,對白底 ≥ 3.3:1 */\n  --tsr-danger: #d23b3b;      /* 失敗紅,對白底 ≥ 4.5:1 */\n  --tsr-focus: #2563eb;\n  --tsr-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 6px 20px rgba(16, 24, 40, .08);\n  --tsr-shadow-hover: 0 2px 4px rgba(16, 24, 40, .08), 0 12px 32px rgba(16, 24, 40, .14);\n\n  display: inline-block;\n  font-family: var(--tsr-font);\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n  line-height: 1.4;\n}\n\n/* ---- 深色:跟隨系統,或由 [data-theme=\"dark\"] 強制 ---- */\n@media (prefers-color-scheme: dark) {\n  :host(:not([theme=\"light\"])),\n  .tessera-scope:not([data-theme=\"light\"]) {\n    --tsr-bg: #181b22;\n    --tsr-bg-2: #20242d;\n    --tsr-fg: #f1f3f7;\n    --tsr-fg-muted: #aab2c0;     /* 對深底 ≥ 4.7:1 */\n    --tsr-border: #3a4150;       /* 對深底 ≥ 3:1 */\n    --tsr-accent: #8b8bff;\n    --tsr-accent-2: #2dd4bf;\n    --tsr-success: #34d399;\n    --tsr-danger: #f87171;\n    --tsr-focus: #93c5fd;\n    --tsr-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 6px 20px rgba(0, 0, 0, .5);\n    --tsr-shadow-hover: 0 2px 6px rgba(0, 0, 0, .5), 0 14px 36px rgba(0, 0, 0, .6);\n  }\n}\n:host([theme=\"dark\"]),\n.tessera-scope[data-theme=\"dark\"] {\n  --tsr-bg: #181b22;\n  --tsr-bg-2: #20242d;\n  --tsr-fg: #f1f3f7;\n  --tsr-fg-muted: #aab2c0;\n  --tsr-border: #3a4150;\n  --tsr-accent: #8b8bff;\n  --tsr-accent-2: #2dd4bf;\n  --tsr-success: #34d399;\n  --tsr-danger: #f87171;\n  --tsr-focus: #93c5fd;\n  --tsr-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 6px 20px rgba(0, 0, 0, .5);\n  --tsr-shadow-hover: 0 2px 6px rgba(0, 0, 0, .5), 0 14px 36px rgba(0, 0, 0, .6);\n}\n\n* { box-sizing: border-box; }\n\n/* ---- 外框卡片 ---- */\n.tsr {\n  position: relative;\n  display: flex;\n  align-items: center;\n  gap: 13px;\n  width: 300px;\n  max-width: 100%;\n  min-height: 74px;\n  padding: 14px 16px;\n  background: var(--tsr-bg);\n  border: 1px solid var(--tsr-border);\n  border-radius: var(--tsr-radius);\n  box-shadow: var(--tsr-shadow);\n  color: var(--tsr-fg);\n  cursor: pointer;\n  overflow: hidden;\n  transition: box-shadow .25s ease, border-color .25s ease, transform .15s ease;\n  -webkit-tap-highlight-color: transparent;\n}\n.tsr:hover { box-shadow: var(--tsr-shadow-hover); }\n.tsr:active { transform: translateY(1px); }\n.tsr[aria-disabled=\"true\"] { cursor: default; opacity: .8; }\n\n/* 頂緣漸層細條(品味細節,非資訊載體) */\n.tsr::before {\n  content: \"\";\n  position: absolute; inset: 0 0 auto 0;\n  height: 3px;\n  background: linear-gradient(90deg, var(--tsr-accent), var(--tsr-accent-2));\n  opacity: .9;\n  transform: scaleX(0);\n  transform-origin: left;\n  transition: transform .5s ease;\n}\n.tsr[data-state=\"solving\"]::before,\n.tsr[data-state=\"verifying\"]::before { transform: scaleX(1); }\n.tsr[data-state=\"success\"]::before {\n  transform: scaleX(1);\n  background: var(--tsr-success);\n}\n\n/* ---- 焦點外框(WCAG 2.4.7 / 2.4.13)---- */\n.tsr:focus-visible {\n  outline: 3px solid var(--tsr-focus);\n  outline-offset: 2px;\n}\n/* 舊瀏覽器後援 */\n.tsr.tsr-focus { outline: 3px solid var(--tsr-focus); outline-offset: 2px; }\n\n/* ---- 左側勾選方塊(同時是 spinner / 勾 / 叉 的容器)---- */\n.tsr-box {\n  position: relative;\n  flex: 0 0 auto;\n  width: 28px; height: 28px;   /* 目標 ≥ 24px(2.5.8)*/\n  border: 2px solid var(--tsr-border);\n  border-radius: 7px;\n  background: var(--tsr-bg-2);\n  transition: border-color .2s ease, background .2s ease;\n}\n.tsr:hover .tsr-box { border-color: var(--tsr-accent); }\n\n/* 勾(SVG path 用 stroke-dashoffset 畫出來)*/\n.tsr-check {\n  position: absolute; inset: 0;\n  width: 100%; height: 100%;\n  stroke: var(--tsr-accent-fg);\n  stroke-width: 3.5;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  fill: none;\n  opacity: 0;\n  transform: scale(.5);\n  transition: opacity .2s ease, transform .2s ease;\n}\n.tsr-check path {\n  stroke-dasharray: 24;\n  stroke-dashoffset: 24;\n}\n\n/* spinner */\n.tsr-spinner {\n  position: absolute; inset: 50% auto auto 50%;\n  width: 18px; height: 18px;\n  margin: -9px 0 0 -9px;\n  border: 2.5px solid color-mix(in srgb, var(--tsr-accent) 25%, transparent);\n  border-top-color: var(--tsr-accent);\n  border-radius: 50%;\n  opacity: 0;\n  animation: tsr-spin .8s linear infinite;\n}\n/* 不支援 color-mix 的後援 */\n@supports not (color: color-mix(in srgb, red, blue)) {\n  .tsr-spinner { border-color: rgba(120,120,160,.3); border-top-color: var(--tsr-accent); }\n}\n\n/* 叉(失敗)*/\n.tsr-cross {\n  position: absolute; inset: 0;\n  width: 100%; height: 100%;\n  stroke: var(--tsr-accent-fg);\n  stroke-width: 3.5; stroke-linecap: round;\n  fill: none; opacity: 0; transform: scale(.5);\n  transition: opacity .2s ease, transform .2s ease;\n}\n\n/* ---- 各狀態下方塊的呈現 ---- */\n.tsr[data-state=\"solving\"]   .tsr-box,\n.tsr[data-state=\"verifying\"] .tsr-box { border-color: var(--tsr-accent); background: transparent; }\n.tsr[data-state=\"solving\"]   .tsr-spinner,\n.tsr[data-state=\"verifying\"] .tsr-spinner { opacity: 1; }\n\n.tsr[data-state=\"success\"] .tsr-box {\n  border-color: var(--tsr-success);\n  background: var(--tsr-success);\n}\n.tsr[data-state=\"success\"] .tsr-check {\n  opacity: 1; transform: scale(1);\n}\n/* 動畫掛在 path(stroke-dashoffset 在 path 上);掛在 .tsr-check(svg)無效 → 勾畫不出來 */\n.tsr[data-state=\"success\"] .tsr-check path {\n  animation: tsr-draw .45s ease forwards .05s;\n}\n\n.tsr[data-state=\"error\"] .tsr-box,\n.tsr[data-state=\"expired\"] .tsr-box {\n  border-color: var(--tsr-danger);\n  background: var(--tsr-danger);\n}\n.tsr[data-state=\"error\"] .tsr-cross,\n.tsr[data-state=\"expired\"] .tsr-cross { opacity: 1; transform: scale(1); }\n\n/* ---- 中間文字 ---- */\n.tsr-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }\n.tsr-label {\n  font-size: 15px;\n  font-weight: 600;\n  color: var(--tsr-fg);\n  letter-spacing: .01em;\n}\n.tsr-status {\n  margin-top: 2px;\n  font-size: 12.5px;\n  color: var(--tsr-fg-muted);\n  min-height: 1.2em;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.tsr[data-state=\"success\"] .tsr-status { color: var(--tsr-success); font-weight: 600; }\n.tsr[data-state=\"error\"] .tsr-status,\n.tsr[data-state=\"expired\"] .tsr-status { color: var(--tsr-danger); font-weight: 600; }\n\n/* ---- 右側品牌標記 ---- */\n.tsr-brand {\n  flex: 0 0 auto;\n  display: flex; flex-direction: column; align-items: center; gap: 2px;\n  opacity: .85;\n  user-select: none;\n}\n.tsr-mark {\n  width: 26px;
248 height: 26px;\n  display: block;\n}\n.tsr-mark .tsr-tile { transition: opacity .3s ease, transform .3s ease; transform-origin: center; }\n.tsr[data-state=\"solving\"]   .tsr-mark .tsr-tile,\n.tsr[data-state=\"verifying\"] .tsr-mark .tsr-tile { animation: tsr-pulse 1.4s ease-in-out infinite; }\n.tsr-mark .tsr-tile:nth-child(2) { animation-delay: .15s; }\n.tsr-mark .tsr-tile:nth-child(3) { animation-delay: .3s; }\n.tsr-mark .tsr-tile:nth-child(4) { animation-delay: .45s; }\n.tsr-brand-text {\n  font-size: 9px; font-weight: 700; letter-spacing: .06em;\n  color: var(--tsr-fg-muted); text-transform: uppercase;\n}\n\n/* ---- 底部進度條(只在求解時出現)---- */\n.tsr-progress {\n  position: absolute; left: 0; right: 0; bottom: 0;\n  height: 3px;\n  background: transparent;\n  overflow: hidden;\n  opacity: 0;\n  transition: opacity .2s ease;\n}\n.tsr[data-state=\"solving\"] .tsr-progress,\n.tsr[data-state=\"verifying\"] .tsr-progress { opacity: 1; }\n.tsr-bar {\n  height: 100%;\n  width: 30%;\n  background: linear-gradient(90deg, var(--tsr-accent), var(--tsr-accent-2));\n  border-radius: 3px;\n  /* 求解進度未知時走 indeterminate 來回掃動;有 % 時切成 determinate(JS 設 width)*/\n}\n.tsr-progress[data-mode=\"indeterminate\"] .tsr-bar {\n  width: 35%;\n  animation: tsr-sweep 1.3s ease-in-out infinite;\n}\n.tsr-progress[data-mode=\"determinate\"] .tsr-bar {\n  transition: width .25s ease;\n}\n\n/* RTL:阿拉伯語等,水平鏡像 */\n:host([dir=\"rtl\"]) .tsr,\n.tessera-scope[dir=\"rtl\"] .tsr { direction: rtl; }\n\n/* ---- 動畫 ---- */\n@keyframes tsr-spin { to { transform: rotate(360deg); } }\n@keyframes tsr-draw { to { stroke-dashoffset: 0; } }\n@keyframes tsr-sweep {\n  0%   { transform: translateX(-120%); }\n  100% { transform: translateX(420%); }\n}\n@keyframes tsr-pulse {\n  0%, 100% { opacity: .35; transform: scale(.9); }\n  50%      { opacity: 1;   transform: scale(1.05); }\n}\n\n/* WCAG 2.3.3:尊重「減少動態效果」偏好 —— 關掉所有非必要動畫 */\n@media (prefers-reduced-motion: reduce) {\n  .tsr,\n  .tsr *,\n  .tsr-check,\n  .tsr-spinner,\n  .tsr-bar,\n  .tsr-mark .tsr-tile {\n    animation: none !important;\n    transition: none !important;\n  }\n  .tsr[data-state=\"success\"] .tsr-check path { stroke-dashoffset: 0; }\n  /* 求解時用靜態脈動替代掃動,仍提供「進行中」回饋但不位移 */\n  .tsr-progress[data-mode=\"indeterminate\"] .tsr-bar { width: 100%; opacity: .6; }\n}\n\n/* 高對比模式:強制可見邊界 */\n@media (forced-colors: active) {\n  .tsr { border: 1px solid CanvasText; }\n  .tsr-box { border: 2px solid CanvasText; }\n  .tsr:focus-visible { outline: 3px solid Highlight; }\n}\n";
249  var I18N = {"_rtl":["ar","he","fa","ur"],"_fallback":"en","en":{"label":"I'm not a robot","aria_checkbox":"Verify that you are human","status_idle":"Click to verify","status_start":"Preparing verification…","status_solving":"Verifying, please wait…","status_verifying":"Confirming…","status_success":"Verified","status_error":"Verification failed — try again","status_expired":"Verification timed out — try again","status_offline":"Connection problem — try again","retry":"Try again","brand_title":"Fallback verification mode by ASCDC","progress_label":"Verification progress"},"zh-Hant":{"label":"我不是機器人","aria_checkbox":"請確認您不是機器人","status_idle":"點選以開始驗證","status_start":"正在準備驗證…","status_solving":"驗證中,請稍候…","status_verifying":"正在確認…","status_success":"驗證通過","status_error":"驗證失敗,請重試","status_expired":"驗證已逾時,請重試","status_offline":"連線發生問題,請重試","retry":"重試","brand_title":"由 ASCDC 提供的備用驗證模式","progress_label":"驗證進度"},"zh-Hans":{"label":"我不是机器人","aria_checkbox":"请确认您不是机器人","status_idle":"点击以开始验证","status_start":"正在准备验证…","status_solving":"正在验证,请稍候…","status_verifying":"正在确认…","status_success":"验证通过","status_error":"验证失败,请重试","status_expired":"验证已超时,请重试","status_offline":"连接出现问题,请重试","retry":"重试","brand_title":"由 ASCDC 提供的备用验证模式","progress_label":"验证进度"}
249,"ja":{"label":"私はロボットではありません","aria_checkbox":"あなたが人間であることを確認します","status_idle":"クリックして確認を開始","status_start":"確認を準備しています…","status_solving":"確認中です。お待ちください…","status_verifying":"確認しています…","status_success":"確認できました","status_error":"確認に失敗しました。もう一度お試しください","status_expired":"確認がタイムアウトしました。もう一度お試しください","status_offline":"接続に問題があります。もう一度お試しください","retry":"再試行","brand_title":"ASCDC 提供の予備認証モード","progress_label":"確認の進行状況"},"ko":{"label":"저는 로봇이 아닙니다","aria_checkbox":"사람인지 확인합니다","status_idle":"클릭하여 확인 시작","status_start":"확인을 준비하는 중…","status_solving":"확인 중입니다. 잠시만 기다려 주세요…","status_verifying":"확인하는 중…","status_success":"확인됨","status_error":"확인에 실패했습니다. 다시 시도하세요","status_expired":"확인 시간이 초과되었습니다. 다시 시도하세요","status_offline":"연결에 문제가 있습니다. 다시 시도하세요","retry":"다시 시도","brand_title":"ASCDC 제공 예비 인증 모드","progress_label":"확인 진행 상황"},"es":{"label":"No soy un robot","aria_checkbox":"Verifica que eres humano","status_idle":"Haz clic para verificar","status_start":"Preparando la verificación…","status_solving":"Verificando, espera un momento…","status_verifying":"Confirmando…","status_success":"Verificado","status_error":"La verificación falló: inténtalo de nuevo","status_expired":"La verificación caducó: inténtalo de nuevo","status_offline":"Problema de conexión: inténtalo de nuevo","retry":"Reintentar","brand_title":"Modo de verificación alternativo de ASCDC","progress_label":"Progreso de la verificación"},"fr":{"label":"Je ne suis pas un robot","aria_checkbox":"Vérifiez que vous êtes humain","status_idle":"Cliquez pour vérifier","status_start":"Préparation de la vérification…","status_solving":"Vérification en cours, veuillez patienter…","status_verifying":"Confirmation…","status_success":"Vérifié","status_error":"Échec de la vérification — réessayez","status_expired":"Vérification expirée — réessayez","status_offline":"Problème de connexion — réessayez","retry":"Réessayer","brand_title":"Mode de vérification de secours par ASCDC","progress_label":"Progression de la vérification"},"de":{"label":"Ich bin kein Roboter","aria_checkbox":"Bestätigen Sie, dass Sie ein Mensch sind","status_idle":"Zum Verifizieren klicken","status_start":"Überprüfung wird vorbereitet…","status_solving":"Überprüfung läuft, bitte warten…","status_verifying":"Wird bestätigt…","status_success":"Bestätigt","status_error":"Überprüfung fehlgeschlagen – erneut versuchen","status_expired":"Überprüfung abgelaufen – erneut versuchen","status_offline":"Verbindungsproblem – erneut versuchen","retry":"Erneut versuchen","brand_title":"Ersatz-Verifizierungsmodus von ASCDC","progress_label":"Fortschritt der Überprüfung"},"pt-BR":{"label":"Não sou um robô","aria_checkbox":"Confirme que você é humano","status_idle":"Clique para verificar","status_start":"Preparando a verificação…","status_solving":"Verificando, aguarde…","status_verifying":"Confirmando…","status_success":"Verificado","status_error":"Falha na verificação — tente novamente","status_expired":"A verificação expirou — tente novamente","status_offline":"Problema de conexão — tente novamente","retry":"Tentar novamente","brand_title":"Modo de verificação alternativo da ASCDC","progress_label":"Progresso da verificação"},"ru":{"label":"Я не робот","aria_checkbox":"Подтвердите, что вы человек","status_idle":"Нажмите для проверки","status_start":"Подготовка проверки…","status_solving":"Идёт проверка, подождите…","status_verifying":"Подтверждение…","status_success":"Проверено","status_error":"Проверка не удалась — повторите","status_expired":"Время проверки истекло — повторите","status_offline":"Проблема с подключением — повторите","retry":"Повторить","brand_title":"Резервный режим проверки от ASCDC","progress_label":"Ход проверки"},"it":{"label":"Non sono un robot","aria_checkbox":"Verifica di essere umano","status_idle":"Fai clic per verificare","status_start":"Preparazione della verifica…","status_solving":"Verifica in corso, attendere…","status_verifying":"Conferma in corso…","status_success":"Verificato","status_error":"Verifica non riuscita — riprova","status_expired":"Verifica scaduta — riprova","status_offline":"Problema di connessione — riprova","retry":"Riprova","brand_title":"Modalità di verifica alternativa di ASCDC","progress_label":"Avanzamento della verifica"},"nl":{"label":"Ik ben geen robot","aria_checkbox":"Bevestig dat je een mens bent","status_idle":"Klik om te verifiëren","status_start":"Verificatie voorbereiden…","status_solving":"Bezig met verifiëren, even geduld…","status_verifying":"Bevestigen…","status_success":"Geverifieerd","status_error":"Verificatie mislukt — probeer opnieuw","status_expired":"Verificatie verlopen — probeer opnieuw","status_offline":"Verbindingsprobleem — probeer opnieuw","retry":"Opnieuw proberen","brand_title":"Reserve­verificatiemodus van ASCDC","progress_label":"Voortgang van de verificatie"},"vi":{"label":"Tôi không phải là người máy","aria_checkbox":"Xác minh bạn là con người","status_idle":"Nhấp để xác minh","status_start":"Đang chuẩn bị xác minh…","status_solving":"Đang xác minh, vui lòng đợi…","status_verifying":"Đang xác nhận…","status_success":"Đã xác minh","status_error":"Xác minh thất bại — thử lại","status_expired":"Xác minh đã hết hạn — thử lại","status_offline":"Sự cố kết nối — thử lại","retry":"Thử lại","brand_title":"Chế độ xác minh dự phòng bởi ASCDC","progress_label":"Tiến trình xác minh"},"th":{"label":"ฉันไม่ใช่โปรแกรมอัตโนมัติ","aria_checkbox":"ยืนยันว่าคุณเป็นมนุษย์","status_idle":"คลิกเพื่อยืนยัน","status_start":"กำลังเตรียมการยืนยัน…","status_solving":"กำลังยืนยัน โปรดรอสักครู่…","status_verifying":"กำลังตรวจสอบ…","status_success":"ยืนยันแล้ว","status_error":"การยืนยันล้มเหลว — ลองอีกครั้ง","status_expired":"การยืนยันหมดเวลา — ลองอีกครั้ง","status_offline":"การเชื่อมต่อมีปัญหา — ลองอีกครั้ง","retry":"ลองอีกครั้ง","brand_title":"โหมดการยืนยันสำรองโดย ASCDC","progress_label":"ความคืบหน้าการยืนยัน"},"id":{"label":"Saya bukan robot","aria_checkbox":"Verifikasi bahwa Anda manusia","status_idle":"Klik untuk memverifikasi","status_start":"Menyiapkan verifikasi…","status_solving":"Memverifikasi, mohon tunggu…","status_verifying":"Mengonfirmasi…","status_success":"Terverifikasi","status_error":"Verifikasi gagal — coba lagi","status_expired":"Verifikasi kedaluwarsa — coba lagi","status_offline":"Masalah koneksi — coba lagi","retry":"Coba lagi","brand_title":"Mode verifikasi cadangan oleh ASCDC","progress_label":"Kemajuan verifikasi"},"ar":{"label":"لست روبوتًا","aria_checkbox":"تأكَّد من أنك إنسان","status_idle":"انقر للتحقق","status_start":"جارٍ التحضير للتحقق…","status_solving":"جارٍ التحقق، يُرجى الانتظار…","status_verifying":"جارٍ التأكيد…","status_success":"تم التحقق","status_error":"فشل التحقق — أعد المحاولة","status_expired":"انتهت مهلة التحقق — أعد المحاولة","status_offline":"مشكلة في الاتصال — أعد المحاولة","retry":"إعادة المحاولة","brand_title":"وضع التحقق الاحتياطي من ASCDC","progress_label":"تقدّم التحقق"},"pt":{"label":"Não sou um robô","aria_checkbox":"Confirme que é humano","status_idle":"Clique para verificar","status_start":"A preparar a verificação…","status_solving":"A verificar, aguarde…","status_verifying":"A confirmar…","status_success":"Verificado","status_error":"A verificação falhou — tente novamente","status_expired":"A verificação expirou — tente novamente","status_offline":"Problema de ligação — tente novamente","retry":"Tentar novamente","brand_title":"Modo de verificação alternativo da ASCDC","progress_label":"Progresso da verificação"},"ms":{"label":"Saya bukan robot","aria_checkbox":"Sahkan bahawa anda manusia","status_idle":"Klik untuk mengesahkan","status_start":"Menyediakan pengesahan…","status_solving":"Mengesahkan, sila tunggu…","status_verifying":"Mengesahkan dengan pelayan…","status_success":"Disahkan","status_error":"Pengesahan gagal — cuba lagi","status_expired":"Pengesahan tamat masa — cuba lagi","status_offline":"Masalah sambungan — cuba lagi","retry":"Cuba lagi","brand_title":"Mod pengesahan sandaran oleh ASCDC","progress_label":"Kemajuan pengesahan"},"fil":{"label":"Hindi ako robot","aria_checkbox":"Patunayang ikaw ay tao","status_idle":"I-click para mag-verify","status_start":"Inihahanda ang pag-verify…","status_solving":"Nagve-verify, mangyaring maghintay…","status_verifying":"Kinukumpirma…","status_success":"Na-verify","status_error":"Nabigo ang pag-verify — subukang muli","status_expired":"Nag-expire ang pag-verify — subukang muli","status_offline":"Problema sa koneksyon — subukang muli","retry":"Subukang muli","brand_title":"Backup na mode ng pag-verify ng ASCDC","progress_label":"Progreso ng pag-verify"}};
250  var WORKER_SRC = "/* Tessera — 純 JS SHA-256(同步)。演算法為標準 SHA-256(NIST FIPS 180-4,公有領域);此為 ASCDC 自有實作(非宣稱演算法原創)。\r\n   用途:當瀏覽器 WebCrypto `crypto.subtle` 不可用時(非安全內容,例如純 HTTP、非 localhost)\r\n   仍能算 PoW 雜湊 → widget 在任何情境都能驗證,不會因「非安全內容」而失敗。\r\n   標準 SHA-256:輸出與 WebCrypto / PHP hash('sha256',…,true) / 後端逐位元組一致(已對拍驗證)。\r\n   接受字串(UTF-8 編碼)或 Uint8Array,回傳 32-byte Uint8Array。 */\r\n(function (global) {\r\n  'use strict';\r\n  var K = new Int32Array([\r\n    0x428a2f98|0,0x71374491|0,0xb5c0fbcf|0,0xe9b5dba5|0,0x3956c25b|0,0x59f111f1|0,0x923f82a4|0,0xab1c5ed5|0,\r\n    0xd807aa98|0,0x12835b01|0,0x243185be|0,0x550c7dc3|0,0x72be5d74|0,0x80deb1fe|0,0x9bdc06a7|0,0xc19bf174|0,\r\n    0xe49b69c1|0,0xefbe4786|0,0x0fc19dc6|0,0x240ca1cc|0,0x2de92c6f|0,0x4a7484aa|0,0x5cb0a9dc|0,0x76f988da|0,\r\n    0x983e5152|0,0xa831c66d|0,0xb00327c8|0,0xbf597fc7|0,0xc6e00bf3|0,0xd5a79147|0,0x06ca6351|0,0x14292967|0,\r\n    0x27b70a85|0,0x2e1b2138|0,0x4d2c6dfc|0,0x53380d13|0,0x650a7354|0,0x766a0abb|0,0x81c2c92e|0,0x92722c85|0,\r\n    0xa2bfe8a1|0,0xa81a664b|0,0xc24b8b70|0,0xc76c51a3|0,0xd192e819|0,0xd6990624|0,0xf40e3585|0,0x106aa070|0,\r\n    0x19a4c116|0,0x1e376c08|0,0x2748774c|0,0x34b0bcb5|0,0x391c0cb3|0,0x4ed8aa4a|0,0x5b9cca4f|0,0x682e6ff3|0,\r\n    0x748f82ee|0,0x78a5636f|0,0x84c87814|0,0x8cc70208|0,0x90befffa|0,0xa4506ceb|0,0xbef9a3f7|0,0xc67178f2|0\r\n  ]);\r\n  var _enc = (typeof TextEncoder !== 'undefined') ? new TextEncoder() : null;\r\n  function utf8(str) {\r\n    if (_enc) return _enc.encode(str);\r\n    // 後援(無 TextEncoder 的舊環境):手動 UTF-8\r\n    var out = [], i, c;\r\n    for (i = 0; i < str.length; i++) {\r\n      c = str.charCodeAt(i);\r\n      if (c < 0x80) out.push(c);\r\n      else if (c < 0x800) { out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); }\r\n      else if (c < 0xd800 || c >= 0xe000) { out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }\r\n      else { i++; c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }\r\n    }\r\n    return new Uint8Array(out);\r\n  }\r\n  function ROTR(n, x) { return (x >>> n) | (x << (32 - n)); }\r\n\r\n  function sha256(data) {\r\n    var msg = (typeof data === 'string') ? utf8(data) : data;\r\n    var len = msg.length;\r\n    var bitLenHi = Math.floor(len / 0x20000000);      // (len*8) 高 32 bit\r\n    var bitLenLo = (len << 3) >>> 0;                  // (len*8) 低 32 bit\r\n    var withOne = len + 1;\r\n    var pad = (56 - (withOne % 64) + 64) % 64;\r\n    var total = withOne + pad + 8;\r\n    var m = new Uint8Array(total);\r\n    m.set(msg);\r\n    m[len] = 0x80;\r\n    m[total - 8] = (bitLenHi >>> 24) & 0xff; m[total - 7] = (bitLenHi >>> 16) & 0xff;\r\n    m[total - 6] = (bitLenHi >>> 8) & 0xff;  m[total - 5] = bitLenHi & 0xff;\r\n    m[total - 4] = (bitLenLo >>> 24) & 0xff; m[total - 3] = (bitLenLo >>> 16) & 0xff;\r\n    m[total - 2] = (bitLenLo >>> 8) & 0xff;  m[total - 1] = bitLenLo & 0xff;
250\r\n\r\n    var H0 = 0x6a09e667|0, H1 = 0xbb67ae85|0, H2 = 0x3c6ef372|0, H3 = 0xa54ff53a|0,\r\n        H4 = 0x510e527f|0, H5 = 0x9b05688c|0, H6 = 0x1f83d9ab|0, H7 = 0x5be0cd19|0;\r\n    var w = new Int32Array(64), off, i;\r\n    for (off = 0; off < total; off += 64) {\r\n      for (i = 0; i < 16; i++) {\r\n        w[i] = (m[off + i*4] << 24) | (m[off + i*4 + 1] << 16) | (m[off + i*4 + 2] << 8) | (m[off + i*4 + 3]);\r\n      }\r\n      for (i = 16; i < 64; i++) {\r\n        var x15 = w[i-15], x2 = w[i-2];\r\n        var s0 = ROTR(7, x15) ^ ROTR(18, x15) ^ (x15 >>> 3);\r\n        var s1 = ROTR(17, x2) ^ ROTR(19, x2) ^ (x2 >>> 10);\r\n        w[i] = (w[i-16] + s0 + w[i-7] + s1) | 0;\r\n      }\r\n      var a=H0,b=H1,c=H2,d=H3,e=H4,f=H5,g=H6,h=H7;\r\n      for (i = 0; i < 64; i++) {\r\n        var S1 = ROTR(6, e) ^ ROTR(11, e) ^ ROTR(25, e);\r\n        var ch = (e & f) ^ (~e & g);\r\n        var t1 = (h + S1 + ch + K[i] + w[i]) | 0;\r\n        var S0 = ROTR(2, a) ^ ROTR(13, a) ^ ROTR(22, a);\r\n        var maj = (a & b) ^ (a & c) ^ (b & c);\r\n        var t2 = (S0 + maj) | 0;\r\n        h=g; g=f; f=e; e=(d + t1)|0; d=c; c=b; b=a; a=(t1 + t2)|0;\r\n      }\r\n      H0=(H0+a)|0; H1=(H1+b)|0; H2=(H2+c)|0; H3=(H3+d)|0; H4=(H4+e)|0; H5=(H5+f)|0; H6=(H6+g)|0; H7=(H7+h)|0;\r\n    }\r\n    var Hs = [H0,H1,H2,H3,H4,H5,H6,H7], out = new Uint8Array(32);\r\n    for (i = 0; i < 8; i++) {\r\n      out[i*4] = (Hs[i] >>> 24) & 0xff; out[i*4+1] = (Hs[i] >>> 16) & 0xff;\r\n      out[i*4+2] = (Hs[i] >>> 8) & 0xff; out[i*4+3] = Hs[i] & 0xff;\r\n    }\r\n    return out;\r\n  }\r\n\r\n  global.tesseraSha256 = sha256;\r\n  if (typeof module !== 'undefined' && module.exports) module.exports = sha256;\r\n})(typeof self !== 'undefined' ? self : typeof globalThis !== 'undefined' ? globalThis : this);\r\n\n/* Tessera — PoW 人機驗證瀏覽器求解端(UMD)。PoW 方案為 Hashcash 式 leading-zero-bits(概念:Adam Back 1997,公有領域);程式碼為 ASCDC 自有實作(移植自內部 PoW Gate 專案)。\r\n   安全性不依賴此檔保密:它只「找 nonce」,通關由 server 數學驗證。 */\r\n/* eslint-disable */\r\n// =============================================================================\r\n// public/ClientPoWMiner.js\r\n// -----------------------------------------------------------------------------\r\n// 這是瀏覽器端的「挖礦端」求解器。\r\n//\r\n// 設計原則(與比特幣工作證明精神一致):\r\n//   1. 這支 JS 可以被任何人看到、複製、修改,安全性不依賴它保密。\r\n//   2. 它只負責「努力找 nonce」。\r\n//   3. 它不負責判定自己是否通過,也不可以自己簽發 pass token。\r\n//   4. 找到 nonce 後,必須交給 server 用數學方式驗證。\r\n//\r\n// 為什麼看光也沒關係:\r\n//   通關條件是 SHA256(pow-v1|ticket_id|random_seed|scope|difficulty|nonce) 的\r\n//   前 difficulty 個 bit 為 0。攻擊者看懂全部程式也無法跳過——他仍然必須實際\r\n//   找到滿足條件的 nonce,而 server 會自己重算 hash 驗證。\r\n//\r\n// 本檔以 UMD 形式封裝,可在以下環境運作:\r\n//   - 瀏覽器主執行緒(
250<script src=\"/ClientPoWMiner.js\">)\r\n//   - Web Worker(importScripts('/ClientPoWMiner.js'))\r\n//   - Node.js 測試(require('../public/ClientPoWMiner.js'))\r\n// =============================================================================\r\n\r\n(function (global) {\r\n  'use strict';\r\n\r\n  // ---- 取得 WebCrypto subtle(瀏覽器 / Worker / Node 都嘗試)----------------\r\n  var _subtle =\r\n    typeof crypto !== 'undefined' && crypto.subtle\r\n      ? crypto.subtle\r\n      : global.crypto && global.crypto.subtle\r\n      ? global.crypto.subtle\r\n      : null;\r\n  // Node 測試環境的後援(若全域 crypto 不存在)\r\n  if (!_subtle && typeof module !== 'undefined' && module.exports) {\r\n    try {\r\n      _subtle = require('node:crypto').webcrypto.subtle;\r\n    } catch (e) {\r\n      /* ignore */\r\n    }\r\n  }\r\n\r\n  function nowMs() {\r\n    if (typeof performance !== 'undefined' && performance.now) return performance.now();\r\n    return Date.now();\r\n  }\r\n\r\n  class ClientPoWMiner {\r\n    constructor(options = {}) {\r\n      this.baseUrl = options.baseUrl || '';\r\n      this.challengePath = options.challengePath || '/challenge';\r\n      this.verifyPath = options.verifyPath || '/verify';\r\n\r\n      // 若設定 workerUrl 且環境支援 Worker,solveChallenge 會把計算丟到 Worker,\r\n      // 避免卡住主 UI;否則 fallback 為主執行緒 inline 求解(Node 測試走這條)。\r\n      this.workerUrl = options.workerUrl || null;\r\n\r\n      this.onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;\r\n      this.onStatus = typeof options.onStatus === 'function' ? options.onStatus : null;\r\n\r\n      // 每隔多少次嘗試回報一次 progress\r\n      this.progressInterval = options.progressInterval || 20000;\r\n      // nonce 搜尋上限(保險,避免無窮迴圈)\r\n      this.maxNonce = options.maxNonce || Number.MAX_SAFE_INTEGER;\r\n\r\n      this._encoder = new TextEncoder();\r\n    }\r\n\r\n    _status(state, info) {\r\n      if (this.onStatus) this.onStatus(state, info || {});\r\n    }\r\n\r\n    // -------------------------------------------------------------------------\r\n    // 1) 取得 challenge:向 /challenge?scope=... 要 HMAC 簽章過的 ticket\r\n    // -------------------------------------------------------------------------\r\n    async fetchChallenge(scope) {\r\n      const url =\r\n        this.baseUrl + this.challengePath + '?scope=' + encodeURIComponent(scope || '/');\r\n      const res = await fetch(url, { credentials: 'same-origin' });\r\n      const data = await res.json();\r\n      if (!data || !data.ok || !data.challenge) {\r\n        throw new Error('fetchChallenge 失敗:' + (data && data.error ? data.error : res.status));\r\n      }\r\n      // challenge = { ticket, payload }\r\n      return data.challenge;\r\n    }\r\n\r\n    // -------------------------------------------------------------------------\r\n    // 2) 求解 nonce:不斷嘗試直到 hash 前 difficulty 個 bit 為 0\r\n    // -------------------------------------------------------------------------\r\n    async solveChallenge(challenge) {\r\n      const payload = challenge.payload || challenge;\r\n      const difficulty = payload.difficulty;\r\n\r\n      // 若可用 Worker,丟給 Worker 求解(避免卡 UI)\r\n      if (this.workerUrl && typeof Worker !== 'undefined') {\r\n        return this._solveWithWorker(challenge);\r\n      }\r\n\r\n      // ---- 主執行緒 inline 求解(fallback / Node 測試)----\r\n      const start = nowMs();\r\n      let nonce = 0;\r\n      let attempts = 0;\r\n\r\n      while (nonce <= this.maxNonce) {\r\n        const input = this.buildMiningInput(challenge, nonce);\r\n        const hash = await this.sha256Bytes(input);\r\n        attempts++;\r\n\r\n        if (this.hasLeadingZeroBits(hash, difficulty)) {\r\n          return {\r\n            nonce: String(nonce),\r\n            hash: this.bytesToHex(hash),\r\n            attempts,\r\n            elapsed_ms: nowMs() - start,\r\n          };\r\n        }\r\n\r\n        if (this.onProgress && attempts % this.progressInterval === 0) {\r\n          this.onProgress({ attempts, nonce, elapsed_ms: nowMs() - start });\r\n        }\r\n        nonce++;\r\n      }\r\n      throw new Error('nonce 空間已耗盡,未找到解');\r\n    }\r\n\r\n    // 以 Worker 求解(瀏覽器)\r\n    _solveWithWorker(challenge) {\r\n      const self_ = this;\r\n      return new Promise(function (resolve, reject) {\r\n        const worker = new Worker(self_.workerUrl);\r\n        worker.onmessage = function (e) {\r\n          const m = e.data || {};\r\n          if (m.type === 'progress') {\r\n            if (self_.onProgress) self_.onProgress(m);\r\n          } else if (m.type === 'solved') {\r\n            worker.terminate();
250\r\n            resolve({\r\n              nonce: m.nonce,\r\n              hash: m.hash,\r\n              attempts: m.attempts,\r\n              elapsed_ms: m.elapsed_ms,\r\n            });\r\n          } else if (m.type === 'error') {\r\n            worker.terminate();\r\n            reject(new Error(m.message || 'worker error'));\r\n          }\r\n        };\r\n        worker.onerror = function (err) {\r\n          worker.terminate();\r\n          reject(err);\r\n        };\r\n        worker.postMessage({\r\n          type: 'solve',\r\n          challenge: challenge,\r\n          progressInterval: self_.progressInterval,\r\n        });\r\n      });\r\n    }\r\n\r\n    // -------------------------------------------------------------------------\r\n    // 3) 送出 proof 給 server 驗證\r\n    //    注意:client_hash / elapsed_ms / solved 只是「除錯與展示用」,\r\n    //         server 不會採信它們對安全性的影響。\r\n    // -------------------------------------------------------------------------\r\n    async submitProof(challenge, proof) {\r\n      const payload = challenge.payload || challenge;\r\n      const res = await fetch(this.baseUrl + this.verifyPath, {\r\n        method: 'POST',\r\n        headers: { 'Content-Type': 'application/json' },\r\n        credentials: 'same-origin',\r\n        body: JSON.stringify({\r\n          ticket: challenge.ticket,\r\n          ticket_id: payload.ticket_id,\r\n          nonce: proof.nonce,\r\n          client_hash: proof.hash, // debug only:server 會自己重算\r\n          elapsed_ms: proof.elapsed_ms, // debug only:server 忽略\r\n          attempts: proof.attempts, // debug only\r\n          solved: true, // server 忽略此欄位\r\n        }),\r\n      });\r\n      let data = {};\r\n      try {\r\n        data = await res.json();\r\n      } catch (e) {\r\n        /* ignore */\r\n      }\r\n      return { ok: res.ok && !!data.ok, status: res.status, data: data };\r\n    }\r\n\r\n    // -------------------------------------------------------------------------\r\n    // 4) 完整流程:取得 challenge → 求解 → 送驗證 → 顯示通過狀態\r\n    // -------------------------------------------------------------------------\r\n    async run(scope) {\r\n      this._status('fetching', { scope: scope });\r\n      const challenge = await this.fetchChallenge(scope);\r\n      const payload = challenge.payload || challenge;\r\n\r\n      this._status('solving', { difficulty: payload.difficulty, scope: payload.scope });\r\n      const proof = await this.solveChallenge(challenge);\r\n\r\n      this._status('submitting', proof);\r\n      const result = await this.submitProof(challenge, proof);\r\n\r\n      this._status(result.ok ? 'passed' : 'failed', { proof: proof, result: result });\r\n      return { challenge: challenge, proof: proof, result: result };\r\n    }\r\n\r\n    // =========================================================================\r\n    // 演算法原語(必須與 server 端 crypto-utils.js 完全一致)\r\n    // =========================================================================\r\n\r\n    // SHA-256:優先 WebCrypto;非安全內容(純 HTTP、無 crypto.subtle)→ 純 JS 同步後援。\r\n    async sha256Bytes(data) {\r\n      if (!_subtle) {\r\n        var _sha = (typeof tesseraSha256 !== 'undefined') ? tesseraSha256 : null;\r\n        if (!_sha && typeof require !== 'undefined') { try { _sha = require('./tessera-sha256.js'); } catch (e) {} }\r\n        if (!_sha) throw new Error('此環境不支援 WebCrypto subtle 且無純 JS SHA-256 後援');\r\n        return _sha(data);\r\n      }\r\n      const bytes = typeof data === 'string' ? this._encoder.encode(d
250ata) : data;\r\n      const digest = await _subtle.digest('SHA-256', bytes);\r\n      return new Uint8Array(digest);\r\n    }\r\n\r\n    // 檢查 hash 前 difficulty 個 bit 是否為 0(與 server 演算法逐字一致)。\r\n    // 這只是 client 自己用來「停止搜尋」,真正驗證仍在 server。\r\n    hasLeadingZeroBits(hashBytes, difficulty) {\r\n      if (!Number.isInteger(difficulty) || difficulty < 0) return false;\r\n      const fullBytes = difficulty >> 3;\r\n      const remBits = difficulty & 7;\r\n      if (fullBytes + (remBits > 0 ? 1 : 0) > hashBytes.length) return false;\r\n      for (let i = 0; i < fullBytes; i++) {\r\n        if (hashBytes[i] !== 0) return false;\r\n      }\r\n      if (remBits > 0) {\r\n        const mask = (0xff << (8 - remBits)) & 0xff;\r\n        if ((hashBytes[fullBytes] & mask) !== 0) return false;\r\n      }\r\n      return true;\r\n    }\r\n\r\n    // 產生挖礦輸入字串:pow-v1|ticket_id|random_seed|scope|difficulty|nonce\r\n    // 格式必須跟 server verifier 完全一致。\r\n    buildMiningInput(challenge, nonce) {\r\n      const p = challenge.payload || challenge;\r\n      return (\r\n        'pow-v1|' +\r\n        String(p.ticket_id) +\r\n        '|' +\r\n        String(p.random_seed) +\r\n        '|' +\r\n        String(p.scope) +\r\n        '|' +\r\n        String(p.difficulty) +\r\n        '|' +\r\n        String(nonce)\r\n      );\r\n    }\r\n\r\n    // 工具:Uint8Array → hex 字串(debug 顯示用)\r\n    bytesToHex(u8) {\r\n      var s = '';\r\n      for (var i = 0; i < u8.length; i++) {\r\n        s += u8[i].toString(16).padStart(2, '0');\r\n      }\r\n      return s;\r\n    }\r\n  }\r\n\r\n  // ---- UMD 匯出 -------------------------------------------------------------\r\n  global.ClientPoWMiner = ClientPoWMiner;\r\n  if (typeof module !== 'undefined' && module.exports) {\r\n    module.exports = { ClientPoWMiner: ClientPoWMiner };\r\n  }\r\n})(\r\n  typeof self !== 'undefined'\r\n    ? self\r\n    : typeof globalThis !== 'undefined'\r\n    ? globalThis\r\n    : this\r\n);\r\n\n/* Tessera — PoW 人機驗證求解 Web Worker。PoW 方案為 Hashcash 式 leading-zero-bits(概念:Adam Back 1997,公有領域);程式碼為 ASCDC 自有實作(移植自內部 PoW Gate 專案)。\r\n   安全性不依賴此檔保密:它只「找 nonce」,通關由 server 數學驗證。 */\r\n/* eslint-disable */\r\n// =============================================================================\r\n// public/pow-worker.js\r\n// -----------------------------------------------------------------------------\r\n// Web Worker 求解器。\r\n//\r\n// 職責:\r\n//   1. 在 Worker(背景執行緒)內執行 nonce 搜尋,避免卡住主 UI。\r\n//   2. 每隔固定嘗試次數回報 progress。\r\n//   3. 找到 nonce 後回傳 proof(nonce / hash / attempts / elapsed_ms)。\r\n//   4. 不自行判定 server 是否通過——它只回傳「找到的解」,由 server 驗證。\r\n//\r\n// 它直接 importScripts 載入 ClientPoWMiner,重用同一份 buildMiningInput /\r\n// sha256Bytes / hasLeadingZeroBits,確保與 server 演算法逐字一致。\r\n// =============================================================================\r// (importScripts 已由 build 內聯 miner 取代)\nvar miner = new self.ClientPoWMiner();\r\n\r\n// 計算一個 hash 的「前導 0 bit 數」(純展示用,不影響求解條件)。\r\nfunction leadingZeroBits(hashBytes) {\r\n  var bits = 0;\r\n  for (var i = 0; i < hashBytes.length; i++) {\r\n    var b = hashBytes[i];\r\n    if (b === 0) { bits += 8; continue; }\r\n    var m = 0x80;\r\n    while (m && (b & m) === 0) { bits++; m >>= 1; }\r\n    break;\r\n  }\r\n  return bits;\r\n}\r\n\r\nself.onmessage = async function (e) {\r\n  var msg = e.data || {};\r\n  if (msg.type !== 'solve') return;\r\n\r\n  var challenge = msg.challenge;\r\n  var payload = challenge.payload || challenge;\r\n  var difficulty = payload.difficulty;\r\n  var interval = msg.progressInterval || 20000;\r\n\r\n  var start = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();\r\n  var nonce = 0;\r\n  var attempts = 0;\r\n  var bestBits = 0; // 目前看過的最佳前導 0 bit 數(展示「越來越接近目標」)\r\n\r\n  function elapsed() {\r\n    var t = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();\r\n    return t - start;\r\n  }\r\n\r\n  try {\r\n    while (true) {\r\n      var input = miner.buildMiningInput(challenge, nonce);\r\n      // 用純 JS SHA-256(同步):非安全內容(純 HTTP)也能算,且 tight loop 比 WebCrypto 的 async 更快。\r\n      // tesseraSha256 由 build 內聯在本 worker 之前(WORKER_SRC = sha256 + miner + worker)。\r\n      var hash = tesseraSha256(input);\r\n      attempts++;\r\n\r\n      var bits = leadingZeroBits(hash);\r\n      if (bits > bestBits) bestBits = bits;\r\n\r\n      // 真正的通關條件仍然是 ServerPoWGate 與 client 共用的 hasLeadingZeroBits。\r\n      if (miner.hasLeadingZeroBits(hash, difficulty)) {\r\n        self.postMessage({\r\n          type: 'solved',\r\n          nonce: String(nonce),\r\n          hash: miner.bytesToHex(hash),\r\n          leadingZeroBits: bits,\r\n          attempts: attempts,\r\n          elapsed_ms: elapsed(),\r\n        });\r\n        return;\r\n      }\r\n\r\n      if (attempts % interval === 0) {\r\n        self.postMessage({\r\n          type: 'progress',\r\n          attempts: attempts,\r\n          nonce: nonce,\r\n          bestBits: bestBits,\r\n          difficulty: difficulty,\r\n          elapsed_ms: elapsed(),\r\n        });\r\n      }\r\n      nonce++;\r\n    }\r\n  } catch (err) {\r\n    self.postMessage({ type: 'error', message: String((err && err.message) || err) });\r\n  }\r\n};\r\n";
251  var MEMORY_WORKER_SRC = "/* Tessera — 純 JS SHA-256(同步)。演算法為標準 SHA-256(NIST FIPS 180-4,公有領域);此為 ASCDC 自有實作(非宣稱演算法原創)。\r\n   用途:當瀏覽器 WebCrypto `crypto.subtle` 不可用時(非安全內容,例如純 HTTP、非 localhost)\r\n   仍能算 PoW 雜湊 → widget 在任何情境都能驗證,不會因「非安全內容」而失敗。\r\n   標準 SHA-256:輸出與 WebCrypto / PHP hash('sha256',…,true) / 後端逐位元組一致(已對拍驗證)。\r\n   接受字串(UTF-8 編碼)或 Uint8Array,回傳 32-byte Uint8Array。 */\r\n(function (global) {\r\n  'use strict';\r\n  var K = new Int32Array([\r\n    0x428a2f98|0,0x71374491|0,0xb5c0fbcf|0,0xe9b5dba5|0,0x3956c25b|0,0x59f111f1|0,0x923f82a4|0,0xab1c5ed5|0,\r\n    0xd807aa98|0,0x12835b01|0,0x243185be|0,0x550c7dc3|0,0x72be5d74|0,0x80deb1fe|0,0x9bdc06a7|0,0xc19bf174|0,\r\n    0xe49b69c1|0,0xefbe4786|0,0x0fc19dc6|0,0x240ca1cc|0,0x2de92c6f|0,0x4a7484aa|0,0x5cb0a9dc|0,0x76f988da|0,\r\n    0x983e5152|0,0xa831c66d|0,0xb00327c8|0,0xbf597fc7|0,0xc6e00bf3|0,0xd5a79147|0,0x06ca6351|0,0x14292967|0,\r\n    0x27b70a85|0,0x2e1b2138|0,0x4d2c6dfc|0,0x53380d13|0,0x650a7354|0,0x766a0abb|0,0x81c2c92e|0,0x92722c85|0,\r\n    0xa2bfe8a1|0,0xa81a664b|0,0xc24b8b70|0,0xc76c51a3|0,0xd192e819|0,0xd6990624|0,0xf40e3585|0,0x106aa070|0,\r\n    0x19a4c116|0,0x1e376c08|0,0x2748774c|0,0x34b0bcb5|0,0x391c0cb3|0,0x4ed8aa4a|0,0x5b9cca4f|0,0x682e6ff3|0,\r\n    0x748f82ee|0,0x78a5636f|0,0x84c87814|0,0x8cc70208|0,0x90befffa|0,0xa4506ceb|0,0xbef9a3f7|0,0xc67178f2|0\r\n  ]);\r\n  var _enc = (typeof TextEncoder !== 'undefined') ? new TextEncoder() : null;\r\n  function utf8(str) {\r\n    if (_enc) return _enc.encode(str);\r\n    // 後援(無 TextEncoder 的舊環境):手動 UTF-8\r\n    var out = [], i, c;\r\n    for (i = 0; i < str.length; i++) {\r\n      c = str.charCodeAt(i);\r\n      if (c < 0x80) out.push(c);\r\n      else if (c < 0x800) { out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); }\r\n      else if (c < 0xd800 || c >= 0xe000) { out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }\r\n      else { i++; c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }\r\n    }\r\n    return new Uint8Array(out);\r\n  }\r\n  function ROTR(n, x) { return (x >>> n) | (x << (32 - n)); }\r\n\r\n  function sha256(data) {\r\n    var msg = (typeof data === 'string') ? utf8(data) : data;\r\n    var len = msg.length;\r\n    var bitLenHi = Math.floor(len / 0x20000000);      // (len*8) 高 32 bit\r\n    var bitLenLo = (len << 3) >>> 0;                  // (len*8) 低 32 bit\r\n    var withOne = len + 1;\r\n    var pad = (56 - (withOne % 64) + 64) % 64;\r\n    var total = withOne + pad + 8;\r\n    var m = new Uint8Array(total);\r\n    m.set(msg);\r\n    m[len] = 0x80;\r\n    m[total - 8] = (bitLenHi >>> 24) & 0xff; m[total - 7] = (bitLenHi >>> 16) & 0xff;\r\n    m[total - 6] = (bitLenHi >>> 8) & 0xff;  m[total - 5] = bitLenHi & 0xff;\r\n    m[total - 4] = (bitLenLo >>> 24) & 0xff; m[total - 3] = (bitLenLo >>> 16) & 0xff;\r\n    m[total - 2] = (bitLenLo >>> 8) & 0xff;  m[total - 1] = bitLenLo & 0xff;
251\r\n\r\n    var H0 = 0x6a09e667|0, H1 = 0xbb67ae85|0, H2 = 0x3c6ef372|0, H3 = 0xa54ff53a|0,\r\n        H4 = 0x510e527f|0, H5 = 0x9b05688c|0, H6 = 0x1f83d9ab|0, H7 = 0x5be0cd19|0;\r\n    var w = new Int32Array(64), off, i;\r\n    for (off = 0; off < total; off += 64) {\r\n      for (i = 0; i < 16; i++) {\r\n        w[i] = (m[off + i*4] << 24) | (m[off + i*4 + 1] << 16) | (m[off + i*4 + 2] << 8) | (m[off + i*4 + 3]);\r\n      }\r\n      for (i = 16; i < 64; i++) {\r\n        var x15 = w[i-15], x2 = w[i-2];\r\n        var s0 = ROTR(7, x15) ^ ROTR(18, x15) ^ (x15 >>> 3);\r\n        var s1 = ROTR(17, x2) ^ ROTR(19, x2) ^ (x2 >>> 10);\r\n        w[i] = (w[i-16] + s0 + w[i-7] + s1) | 0;\r\n      }\r\n      var a=H0,b=H1,c=H2,d=H3,e=H4,f=H5,g=H6,h=H7;\r\n      for (i = 0; i < 64; i++) {\r\n        var S1 = ROTR(6, e) ^ ROTR(11, e) ^ ROTR(25, e);\r\n        var ch = (e & f) ^ (~e & g);\r\n        var t1 = (h + S1 + ch + K[i] + w[i]) | 0;\r\n        var S0 = ROTR(2, a) ^ ROTR(13, a) ^ ROTR(22, a);\r\n        var maj = (a & b) ^ (a & c) ^ (b & c);\r\n        var t2 = (S0 + maj) | 0;\r\n        h=g; g=f; f=e; e=(d + t1)|0; d=c; c=b; b=a; a=(t1 + t2)|0;\r\n      }\r\n      H0=(H0+a)|0; H1=(H1+b)|0; H2=(H2+c)|0; H3=(H3+d)|0; H4=(H4+e)|0; H5=(H5+f)|0; H6=(H6+g)|0; H7=(H7+h)|0;\r\n    }\r\n    var Hs = [H0,H1,H2,H3,H4,H5,H6,H7], out = new Uint8Array(32);\r\n    for (i = 0; i < 8; i++) {\r\n      out[i*4] = (Hs[i] >>> 24) & 0xff; out[i*4+1] = (Hs[i] >>> 16) & 0xff;\r\n      out[i*4+2] = (Hs[i] >>> 8) & 0xff; out[i*4+3] = Hs[i] & 0xff;\r\n    }\r\n    return out;\r\n  }\r\n\r\n  global.tesseraSha256 = sha256;\r\n  if (typeof module !== 'undefined' && module.exports) module.exports = sha256;\r\n})(typeof self !== 'undefined' ? self : typeof globalThis !== 'undefined' ? globalThis : this);\r\n\n/* Tessera — memory-hard(MTP-lite,概念依 Biryukov–Khovratovich《Egalitarian Computing》2016)瀏覽器求解端。程式碼為 ASCDC 自有實作(移植自內部 PoW Gate ServerMemoryGate/ClientMemoryMiner)。\r\n   只負責建 dataset/Merkle/找 nonce;通關由後端 O(K·logN) 抽樣驗證。安全不依賴此檔保密。 */\r\n/* eslint-disable */\r\n// =============================================================================\r\n// public/ClientMemoryMiner.js\r\n// -----------------------------------------------------------------------------\r\n// Memory-hard 求解端(瀏覽器「挖礦端」)。與 src/ServerMemoryGate.js 的 dataset /\r\n// Merkle / answer 規則「逐字一致」,使用 WebCrypto SHA-256。\r\n//\r\n// 它只負責:配置 dataset → 建 Merkle tree → 找 nonce → 產生抽樣 proof。\r\n// 它不判定自己是否通過;通關由 ServerMemoryGate 數學驗證。\r\n//\r\n// 與既有 ClientPoWMiner 並存,未修改任何 Hashcash 核心。UMD:可在瀏覽器 / Web Worker\r\n// / Node 測試執行。\r\n// =============================================================================\r\n\r\n(function (global) {\r\n  'use strict';\r\n\r\n  var _subtle =\r\n    typeof crypto !== 'undefined' && crypto.subtle\r\n      ? crypto.subtle\r\n      : global.crypto && global.crypto.subtle\r\n      ? global.crypto.subtle\r\n      : null;\r\n  if (!_subtle && typeof module !== 'undefined' && module.exports) {\r\n    try { _subtle = require('node:crypto').webcrypto.subtle; } catch (e) {}\r\n  }\r\n\r\n  var enc = new TextEncoder();\r\n\r\n  function nowMs() {\r\n    if (typeof performance !== 'undefined' && performance.now) return performance.now();\r\n    return Date.now();\r\n  }\r\n  function concatBytes(arrays) {\r\n    var len = 0, i;\r\n    for (i = 0; i < arrays.length; i++) len += arrays[i].length;\r\n    var out = new Uint8Array(len), o = 0;\r\n    for (i = 0; i < arrays.length; i++) { out.set(arrays[i], o); o += arrays[i].length; }\r\n    return out;\r\n  }\r\n  function u32BE(u8, off) {\r\n    return ((u8[off] << 24) | (u8[off + 1] << 16) | (u8[off + 2] << 8) | u8[off + 3]) >>> 0;\r\n  }\r\n  function toHex(u8) {\r\n    var s = '';\r\n    for (var i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0');\r\n    return s;\r\n  }\r\n\r\n  class ClientMemoryMiner {\r\n    constructor(options) {\r\n      options = options || {};\r\n      this.baseUrl = options.baseUrl || '';
251\r\n      this.challengePath = options.challengePath || '/api/memory/challenge';\r\n      this.verifyPath = options.verifyPath || '/api/memory/verify';\r\n    }\r\n\r\n    async sha256Bytes(data) {\r\n      if (!_subtle) {\r\n        // 非安全內容(純 HTTP、無 crypto.subtle)→ 純 JS 同步後援(L3 也能在純 HTTP 運作)\r\n        var _sha = (typeof tesseraSha256 !== 'undefined') ? tesseraSha256 : null;\r\n        if (!_sha && typeof require !== 'undefined') { try { _sha = require('./tessera-sha256.js'); } catch (e) {} }\r\n        if (!_sha) throw new Error('此環境不支援 WebCrypto subtle 且無純 JS SHA-256 後援');\r\n        return _sha(data);\r\n      }\r\n      var bytes = typeof data === 'string' ? enc.encode(data) : data;\r\n      var d = await _subtle.digest('SHA-256', bytes);\r\n      return new Uint8Array(d);\r\n    }\r\n\r\n    hasLeadingZeroBits(hashBytes, difficulty) {\r\n      if (!Number.isInteger(difficulty) || difficulty < 0) return false;\r\n      var fullBytes = difficulty >> 3;\r\n      var remBits = difficulty & 7;\r\n      if (fullBytes + (remBits > 0 ? 1 : 0) > hashBytes.length) return false;\r\n      for (var i = 0; i < fullBytes; i++) if (hashBytes[i] !== 0) return false;\r\n      if (remBits > 0) {\r\n        var mask = (0xff << (8 - remBits)) & 0xff;\r\n        if ((hashBytes[fullBytes] & mask) !== 0) return false;\r\n      }\r\n      return true;\r\n    }\r\n    countLeadingZeroBits(u8) {\r\n      var bits = 0;\r\n      for (var i = 0; i < u8.length; i++) {\r\n        var b = u8[i];\r\n        if (b === 0) { bits += 8; continue; }\r\n        var m = 0x80;\r\n        while (m && (b & m) === 0) { bits++; m >>= 1; }\r\n        break;\r\n      }\r\n      return bits;\r\n    }\r\n\r\n    // ---- dataset / merkle(與 server 逐字一致)----\r\n    async buildDataset(payload, onProgress) {\r\n      var N = payload.block_count, tid = payload.ticket_id, seed = payload.random_seed;\r\n      var blocks = new Array(N);\r\n      blocks[0] = await this.sha256Bytes('mem-v1|blk|' + tid + '|' + seed + '|0');\r\n      for (var i = 1; i < N; i++) {\r\n        var prev = blocks[i - 1];\r\n        var j = u32BE(prev, 0) % i;\r\n        var suffix = enc.encode('|' + tid + '|' + seed + '|' + i);\r\n        blocks[i] = await this.sha256Bytes(concatBytes([prev, blocks[j], suffix]));\r\n        if (onProgress && i % 512 === 0) onProgress(i, N);\r\n      }\r\n      return blocks;\r\n    }\r\n    async _leafHash(block) { return this.sha256Bytes(concatBytes([new Uint8Array([0x00]), block])); }\r\n    async _nodeHash(l, r) { return this.sha256Bytes(concatBytes([new Uint8Array([0x01]), l, r])); }\r\n\r\n    async buildMerkle(blocks, onProgress) {\r\n      var level = [];\r\n      for (var i = 0; i < blocks.length; i++) {\r\n        level.push(await this._leafHash(blocks[i]));\r\n        if (onProgress && i % 512 === 0) onProgress(i, blocks.length);\r\n      }\r\n      var layers = [level];\r\n      while (level.length > 1) {\r\n        var next = [];\r\n        for (var k = 0; k < level.length; k += 2) next.push(await this._nodeHash(level[k], level[k + 1]));\r\n        layers.push(next);\r\n        level = next;\r\n      }\r\n      return { root: level[0], layers: layers };\r\n    }\r\n    merkleProof(layers, index) {\r\n      var path = [], idx = index;\r\n      for (var l = 0; l < layers.length - 1; l++) { path.push(toHex(layers[l][idx ^ 1])); idx >>= 1; }\r\n      return path;\r\n    }\r\n    async deriveIndices(rootBytes, nonce, K, N) {\r\n      var indices = [];\r\n      var pool = await this.sha256Bytes(concatBytes([rootBytes, enc.encode('|' + nonce)]));\r\n      var off = 0;\r\n      for (var k = 0; k < K; k++) {\r\n        if (off + 4 > pool.length) { pool = await this.sha256Bytes(pool); off = 0; }\r\n        var v = u32BE(pool, off); off += 4;\r\n        indices.push(1 + (v % (N - 1)));\r\n      }\r\n      return indices;\r\n    }\r\n    async answerHash(rootBytes, nonce, cellBytes, seed) {\r\n      var parts = [rootBytes, enc.encode('|' + nonce + '|')];\r\n      for (var i = 0; i < cellBytes.length; i++) parts.push(cellBytes[i]);\r\n      parts.push(enc.encode('|' + seed));\r\n      return this.sha256Bytes(concatBytes(parts));\r\n    }\r\n\r\n    // ---- 完整求解(在 Worker 或 Node 內呼叫;hooks 回報 phase / progress)----\r\n    async solveChallenge(challenge, hooks) {\r\n      hooks = hooks || {};\r\n      var payload = challenge.payload || challenge;\r\n      var N = payload.block_count, K = payload.sample_count, diff = payload.difficulty, seed = payload.random_seed;\r\n      var t0 = nowMs();\r\n      var phase = function (p) { if (hooks.onPhase) hooks.onPhase(p); };\r\n      var prog = function (o) { if (hooks.onProgress) hooks.onProgress(o); };\r\n\r\n      phase('dataset');\r\n      var blocks = await this.buildDataset(payload, function (done, total) {\r\n        prog({ phase: 'dataset', done: done, total: total, elapsed_ms: nowMs() - t0 });\r\n      });\r\n\r\n      phase('merkle');\r\n      var m = await this.buildMerkle(blocks, function (done, total) {\r\n        prog({ phase: 'merkle', done: done, total: total, elapsed_ms: nowMs() - t0 });\r\n      });\r\n      var root = m.root, layers = m.layers;\r\n\r\n      phase('nonce');\r\n      var nonce = 0, required = null, Y = null, attempts = 0;\r\n      for (;;) {\r\n        required = await this.deriveIndices(root, String(nonce), K, N);\r\n        var cells = required.map(function (i) { return blocks[i]; });\r\n        Y = await this.answerHash(root, String(nonce), cells, seed);\r\n        attempts++;\r\n        if (this.hasLeadingZeroBits(Y, diff)) break;
251\r\n        if (attempts % 200 === 0) prog({ phase: 'nonce', attempts: attempts, nonce: nonce, elapsed_ms: nowMs() - t0 });\r\n        nonce++;\r\n      }\r\n\r\n      phase('proof');\r\n      var self_ = this;\r\n      var samples = required.map(function (i) {\r\n        var j = u32BE(blocks[i - 1], 0) % i;\r\n        return {\r\n          index: i,\r\n          value: toHex(blocks[i]),\r\n          path: self_.merkleProof(layers, i),\r\n          prevValue: toHex(blocks[i - 1]),\r\n          prevPath: self_.merkleProof(layers, i - 1),\r\n          backIndex: j,\r\n          backValue: toHex(blocks[j]),\r\n          backPath: self_.merkleProof(layers, j),\r\n        };\r\n      });\r\n      var proof = {\r\n        challenge_type: 'memory-v1',\r\n        root: toHex(root),\r\n        nonce: String(nonce),\r\n        mem_bytes: N * payload.block_size,\r\n        sample_indexes: required,\r\n        answer_hash: toHex(Y),\r\n        samples: samples,\r\n      };\r\n      var stats = {\r\n        attempts: attempts,\r\n        nonce: String(nonce),\r\n        root: toHex(root),\r\n        elapsed_ms: nowMs() - t0,\r\n        sample_indexes: required,\r\n        proof_bytes: JSON.stringify(proof).length,\r\n        mem_bytes: N * payload.block_size,\r\n        block_count: N,\r\n        block_size: payload.block_size,\r\n        rounds: payload.rounds,\r\n        sample_count: K,\r\n        leadingZeroBits: this.countLeadingZeroBits(Y),\r\n        answer_hash: toHex(Y),\r\n      };\r\n      phase('done');\r\n      return { proof: proof, stats: stats };\r\n    }\r\n\r\n    async fetchChallenge(scope) {\r\n      var url = this.baseUrl + this.challengePath + '?scope=' + encodeURIComponent(scope || '/memory-demo');\r\n      var res = await fetch(url, { credentials: 'same-origin' });\r\n      var data = await res.json();\r\n      if (!data || !data.ok || !data.challenge) throw new Error('memory challenge 失敗');\r\n      return data.challenge;\r\n    }\r\n    async submitProof(challenge, proof) {\r\n      var res = await fetch(this.baseUrl + this.verifyPath, {\r\n        method: 'POST',\r\n        headers: { 'Content-Type': 'application/json' },\r\n        credentials: 'same-origin',\r\n        body: JSON.stringify({ ticket: challenge.ticket, proof: proof }),\r\n      });\r\n      var data = {};\r\n      try { data = await res.json(); } catch (e) {}\r\n      return { ok: res.ok && !!data.ok, status: res.status, data: data };\r\n    }\r\n    async run(scope) {\r\n      var challenge = await this.fetchChallenge(scope);\r\n      var solved = await this.solveChallenge(challenge);\r\n      var result = await this.submitProof(challenge, solved.proof);\r\n      return { challenge: challenge, proof: solved.proof, stats: solved.stats, result: result };\r\n    }\r\n  }\r\n\r\n  global.ClientMemoryMiner = ClientMemoryMiner;\r\n  if (typeof module !== 'undefined' && module.exports) {\r\n    module.exports = { ClientMemoryMiner: ClientMemoryMiner };\r\n  }\r\n})(\r\n  typeof self !== 'undefined' ? self : typeof globalThis !== 'undefined' ? globalThis : this\r\n);\r\n\n/* Tessera — memory-hard(MTP-lite,概念依 Biryukov–Khovratovich 2016)求解 Web Worker。程式碼為 ASCDC 自有實作(移植自內部 PoW Gate)。\r\n   背景配置 dataset、建 Merkle、找 nonce、產生抽樣 proof,分階段(dataset/merkle/nonce/proof)回報進度。\r\n   注意:本檔由 build 內聯 tessera-memory-miner.js(取代下方 importScripts);獨立使用時請確保 miner 先載入。 */\r// (importScripts 已由 build 內聯 miner 取代)\nvar miner = new self.ClientMemoryMiner();\r\n\r\nself.onmessage = async function (e) {\r\n  var msg = e.data || {};\r\n  if (msg.type !== 'solve') return;\r\n  try {\r\n    var res = await miner.solveChallenge(msg.challenge, {\r\n      onPhase: function (p) { self.postMessage({ type: 'phase', phase: p }); },\r\n      onProgress: function (o) { self.postMessage({ type: 'progress', progress: o }); },\r\n    });\r\n    self.postMessage({ type: 'solved', proof: res.proof, stats: res.stats });\r\n  } catch (err) {\r\n    self.postMessage({ type: 'error', message: String((err && err.message) || err) });\r\n  }\r\n};\r\n";
252
253  var RTL = (I18N && I18N._rtl) || ['ar', 'he', 'fa', 'ur'];
254  var FALLBACK = (I18N && I18N._fallback) || 'en';
255
256  // ---- 語言解析:attr → <html lang> → navigator;正規化 zh 變體 -------------
257  function normLang(raw) {
258    if (!raw) return '';
259    var s = String(raw).trim();
260    var low = s.toLowerCase().replace('_', '-');
261    // 中文變體:簡繁判定
262    if (low.indexOf('zh') === 0) {
263      if (/hant|tw|hk|mo/.test(low)) return 'zh-Hant';
264      if (/hans|cn|sg|my/.test(low)) return 'zh-Hans';
265      return 'zh-Hant'; // 預設繁中(本專案主要語境)
266    }
267    // 葡語:巴西(pt-BR)vs 歐洲(pt);只有帶 br 才判巴西,其餘(含裸 pt / pt-PT)用歐洲葡語
268    if (low.indexOf('pt') === 0) return low.indexOf('br') >= 0 ? 'pt-BR' : 'pt';
269    // 菲律賓語:tl / tl-* / fil* 一律對應 fil
270    if (low === 'tl' || low.indexOf('tl-') === 0 || low.indexOf('fil') 
270=== 0) return 'fil';
271    // 馬來語:ms / ms-*
272    if (low.indexOf('ms') === 0) return 'ms';
273    // 取主語言碼
274    var base = low.split('-')[0];
275    return base;
276  }
277
278  function pickLang(requested) {
279    var order = [];
280    if (requested) order.push(requested);
281    try {
282      var htmlLang = document.documentElement && document.documentElement.getAttribute('lang');
283      if (htmlLang) order.push(htmlLang);
284    } catch (e) {}
285    try {
286      var navs = (navigator.languages && navigator.languages.slice()) || [navigator.language];
287      for (var i = 0; i < navs.length; i++) order.push(navs[i]);
288    } catch (e) {}
289    for (var j = 0; j < order.length; j++) {
290      var n = normLang(order[j]);
291      if (n && I18N[n]) return n;
292      // 主碼 fallback(如 es-MX → es)
293      var base = n.split('-')[0];
294      if (I18N[base]) return base;
295    }
296    return FALLBACK;
297  }
298
299  function t(lang, key) {
300    var pack = I18N[lang] || I18N[FALLBACK] || {};
301    if (pack[key] != null) return pack[key];
302    var fb = I18N[FALLBACK] || {};
303    return fb[key] != null ? fb[key] : key;
304  }
305
306  // ---- 內嵌求解原語(Worker 不可用時的主執行緒 fallback)---------------------
307  var _enc = new TextEncoder();
308  function buildMiningInput(p, nonce) {
309    return 'pow-v1|' + p.ticket_id + '|' + p.random_seed + '|' + p.scope + '|' + p.difficulty + '|' + nonce;
310  }
311  // SHA-256:優先純 JS 同步(非安全內容/純 HTTP 也能用);tesseraSha256 由 build 內聯為全域。
312  // 無內聯時退回 WebCrypto(回 Promise)。_solveInline 用 await,對同步/Promise 皆相容。
313  function sha256(strOrBytes) {
314    if (typeof tesseraSha256 === 'function') return tesseraSha256(strOrBytes);
315    var bytes = typeof strOrBytes === 'string' ? _enc.encode(strOrBytes) : strOrBytes;
316    return crypto.subtle.digest('SHA-256', bytes).then(function (d) { return new Uint8Array(d); });
317  }
318  function hasLeadingZeroBits(h, diff) {
319    var full = diff >> 3, rem = diff & 7;
320    if (full + (rem > 0 ? 1 : 0) > h.length) return false;
321    for (var i = 0; i < full; i++) if (h[i] !== 0) return false;
322    if (rem > 0) { var m = (0xff << (8 - rem)) & 0xff; if ((h[full] & m) !== 0) return false; }
323    return true;
324  }
325  function leadingZeroBits(h) {
326    var bits = 0;
327    for (var i = 0; i < h.length; i++) {
328      var b = h[i];
329      if (b === 0) { bits += 8; continue; }
330      var m = 0x80; while (m && (b & m) === 0) { bits++; m >>= 1; } break;
331    }
332    return bits;
333  }
334
335  var _blobUrl = null, _memBlobUrl = null;
336  function workerBlobUrl() {
337    if (_blobUrl) return _blobUrl;
338    try { _blobUrl = URL.createObjectURL(new Blob([WORKER_SRC], { type: 'text/javascript' })); }
339    catch (e) { _blobUrl = null; }
340    return _blobUrl;
341  }
342  function memoryWorkerBlobUrl() {
343    if (_memBlobUrl) return _memBlobUrl;
344    try { _memBlobUrl = URL.createObjectURL(new Blob([MEMORY_WORKER_SRC], { type: 'text/javascript' })); }
345    catch (e) { _memBlobUrl = null; }
346    return _memBlobUrl;
347  }
348
349  // =====================================================================
350  // <tessera-widget>
351  // =====================================================================
352  class TesseraWidget extends HTMLElement {
353    static get observedAttributes() { return ['lang', 'theme', 'disabled', 'dir']; }
354
355    constructor() {
356      super();
357      this.attachShadow({ mode: 'open' });
358      this._state = 'idle';
359      this._worker = null;
360      this._inlineAbort = false;
361      this._verified = false;
362      this._timer = null;
363      this._lang = FALLBACK;
364      this._maxSolveMs = 30000;     // 求解硬上限 → 逾時
365      this._fieldPrefix = 'tessera';
366      this._hidden = { ticket: null, nonce: null };
367    }
368
369    // ---- 公開屬性:embedded challenge {ticket, payload} ----
370    set challenge(v) { this._challenge = v; }
371    get challenge() { return this._challenge; }
372
373    connectedCallback() {
374      this._fieldPrefix = this.getAttribute('field-prefix') || 'tessera';
375      if (this.hasAttribute('max-solve-ms')) {
376        var m = parseInt(this.getAttribute('max-solve-ms'), 10);
377        if (m > 0) this._maxSolveMs = m;
378      }
379      // attribute 帶入 challenge(JSON)
380      if (!this._challenge && this.hasAttribute('challenge')) {
381        try { this._challenge = JSON.parse(this.getAttribute('challenge')); } catch (e) {}
382      }
383      this._lang = pickLang(this.getAttribute('lang'));
384      this._isRtl = RTL.indexOf(this._lang.split('-')[0]) >= 0;
385      this._render();
386      this._ensureHiddenFields();
387      if (this.hasAttribute('auto')) {
388        // 自動開始(不需點擊)
389        this._start();
390      }
391    }
392
393    disconnectedCallback() { this._cleanup(); }
394
395    attributeChangedCallback(name) {
396      if (!this.shadowRoot || !this._root) return;
397      if (name === 'lang') {
398        this._lang = pickLang(this.getAttribute('lang'));
399        this._isRtl = RTL.indexOf(this._lang.split('-')[0]) >= 0;
400        this._applyLangText();
401      } else if (name === 'theme') {
402        this._applyTheme();
403      } else if (name === 'disabled') {
404        this._applyDisabled();
405      }
406    }
407
408    // ---- 渲染 ----
409    _render() {
410      var lang = this._lang;
411      // CSS 注入:優先用 Constructable Stylesheet(adoptedStyleSheets)→ 在嚴格 CSP
412      // (style-src 'self',無 unsafe-inline)下仍可運作;不支援才退回 inline <style>。
413      var styleTag = '';
414      var adopted = false;
415      try {
416        if (this.shadowRoot.adoptedStyleSheets !== undefined && typeof CSSStyleSheet === 'function') {
417          var sheet = new CSSStyleSheet();
418          sheet.replaceSync(CSS);
419          this.shadowRoot.adoptedStyleSheets = [sheet];
420          adopted = true;
421        }
422      } catch (e) { adopted = false; }
423      if (!adopted) styleTag = '<style>' + CSS + '</style>';
424      this.shadowRoot.innerHTML =
425        styleTag +
426        '<div class="tsr" part="card" role="checkbox" tabindex="0" aria-checked="false" data-state="idle"' +
427        (this._isRtl ? ' dir="rtl"' : '') + '>' +
428          '<span class="tsr-box" aria-hidden="true">' +
429            '<span class="tsr-spinner"></span>' +
430            '<svg class="tsr-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 12.5l4.5 4.5L19.5 7"/></svg>
430' +
431            '<svg class="tsr-cross" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7L7 17"/></svg>' +
432          '</span>' +
433          '<span class="tsr-body">' +
434            '<span class="tsr-label" id="tsr-lbl"></span>' +
435            '<span class="tsr-status" id="tsr-st" role="status" aria-live="polite"></span>' +
436          '</span>' +
437          '<span class="tsr-brand">' +
438            '<svg class="tsr-mark" viewBox="0 0 24 24" aria-hidden="true">' +
439              '<rect class="tsr-tile" x="2"  y="2"  width="9" height="9" rx="2.4" fill="var(--tsr-accent)"/>' +
440              '<rect class="tsr-tile" x="13" y="2"  width="9" height="9" rx="2.4" fill="var(--tsr-accent-2)"/>' +
441              '<rect class="tsr-tile" x="2"  y="13" width="9" height="9" rx="2.4" fill="var(--tsr-accent-2)"/>' +
442              '<rect class="tsr-tile" x="13" y="13" width="9" height="9" rx="2.4" fill="var(--tsr-accent)"/>' +
443            '</svg>' +
444            '<span class="tsr-brand-text">TESSERA</span>' +
445          '</span>' +
446          '<div class="tsr-progress" data-mode="indeterminate"><div class="tsr-bar"></div></div>' +
447        '</div>';
448
449      this._root = this.shadowRoot.querySelector('.tsr');
450      this._elLabel = this.shadowRoot.getElementById('tsr-lbl');
451      this._elStatus = this.shadowRoot.getElementById('tsr-st');
452      this._elProgress = this.shadowRoot.querySelector('.tsr-progress');
453      this._elBar = this.shadowRoot.querySelector('.tsr-bar');
454      this._elBrand = this.shadowRoot.querySelector('.tsr-brand');
455
456      this._applyTheme();
457      this._applyLangText();
458      this._applyDisabled();
459
460      var self_ = this;
461      this._root.addEventListener('click', function () { self_._activate(); });
462      this._root.addEventListener('keydown', function (e) {
463        if (e.key === ' ' || e.key === 'Enter' || e.key === 'Spacebar') {
464          e.preventDefault();
465          self_._activate();
466        }
467      });
468      // focus-visible 後援(舊瀏覽器)
469      this._root.addEventListener('focus', function () { self_._root.classList.add('tsr-focus'); });
470      this._root.addEventListener('blur', function () { self_._root.classList.remove('tsr-focus'); });
471    }
472
473    _applyTheme() {
474      // 主題由 host 的 [theme=light|dark] 屬性直接驅動(CSS :host([theme=...]));
475      // 不設值=auto(跟隨系統 prefers-color-scheme)。此處僅正規化非法值。
476      var theme = this.getAttribute('theme');
477      if (theme && theme !== 'light' && theme !== 'dark') this.removeAttribute('theme');
478    }
479
480    _applyLangText() {
481      if (!this._root) return;
482      var lang = this._lang;
483      this._elLabel.textContent = t(lang, 'label');
484      this._root.setAttribute('aria-label', t(lang, 'aria_checkbox'));
485      this._elBrand.setAttribute('title', t(lang, 'brand_title'));
486      this._elProgress.setAttribute('aria-label', t(lang, 'progress_label'));
487      if (this._isRtl) this._root.setAttribute('dir', 'rtl'); else this._root.removeAttribute('dir');
488      if (this._state === 'idle') this._setStatus('status_idle');
489    }
490
491    _applyDisabled() {
492      var dis = this.hasAttribute('disabled');
493      if (!this._root) return;
494      if (dis) {
495        this._root.setAttribute('aria-disabled', 'true');
496        this._root.removeAttribute('tabindex');
497      } else if (!this._verified && this._state !== 'solving' && this._state !== 'verifying') {
498        this._root.removeAttribute('aria-disabled');
499        this._root.setAttribute('tabindex', '0');
500      }
501    }
502
503    _ensureHiddenFields() {
504      // hidden 欄位放 light DOM,才會隨外層 <form> 送出
505      var mk = function (name) {
506        var el = document.createElement('input');
507        el.type = 'hidden';
508        el.name = name;
509        return el;
510      };
511      this._hidden.ticket = mk(this._fieldPrefix + '_ticket');
512      this._hidden.nonce = mk(this._fieldPrefix + '_nonce');
513      this.appendChild(this._hidden.ticket);
514      this.appendChild(this._hidden.nonce);
515    }
516
517    // memory-hard 才需要的 proof 欄位(JSON);延遲建立
518    _ensureProofField() {
519      if (this._hidden.proof) return;
520      var el = document.createElement('input');
521      el.type = 'hidden';
522      el.name = this._fieldPrefix + '_proof';
523      this._hidden.proof = el;
524      this.appendChild(el);
525    }
526
527    // ---- 狀態機 ----
528    _setState(s) {
529      this._state = s;
530      if (this._root) {
531        this._root.setAttribute('data-state', s);
532        this._root.setAttribute('aria-busy', (s === 'solving' || s === 'verifying' || s === 'start') ? 'true' : 'false');
533      }
534    }
535    _setStatus(key) { if (this._elStatus) this._elStatus.textContent = t(this._lang, key); }
536
537    _activate() {
538      if (this.hasAttribute('disabled')) return;
539      if (this._verified) return;
540      if (this._state === 'solving' || this._state === 'verifying' || this._state === 'start') return;
541      if (this._state === 'error' || this._state === 'expired') { this.reset(); }
542      this._start();
543    }
544
545    async _start() {
546      this._verified = false;
547      this._inlineAbort = false;
548      this._setState('start');
549      this._setStatus('status_start');
550      this._setProgress('indeterminate', 0);
551
552      // 取得 challenge:embedded 優先,否則 challenge-url 抓
553      var challenge = this._challenge;
554      try {
555        if (!challenge && this.hasAttribute('challenge-url')) {
556          challenge = await this._fetchChallenge();
557        }
558      } catch (e) {
559        return this._fail('status_offline', 'fetch_failed');
560      }
561      if (!challenge || !(challenge.payload || challenge.ticket)) {
562        return this._fail('status_error', 'no_challenge');
563      }
564      this._challenge = challenge;
565
566      var payload = challenge.payload || challenge;
567      // 票券過期:嵌入式挑戰會過期(後端 ticketTtl)。若有 challenge-url → 重抓一張新挑戰(難度依
568      //   後端 RiskScore + retry 提升);否則無法 refetch,回過期。這修正「逾時後點擊無反應」(過期卡死)。
569      if (payload.expires_at && (payload.expires_at * 1000) < Date.now()) {
570        if (this.hasAttribute('challenge-url')) {
571          try { challenge = await this._fetchChallenge(); }
572          catch (e) { return this._fail('status_offline', 'fetch_failed'); }
573          if (!challenge || !(challenge.payload || challenge.ticket)) return this._fail('status_error', 'no_challenge');
574          this._challenge = challenge;
575          payload = challenge.payload || challenge;
576        }
577        if (payload.expires_at && (payload.expires_at * 1000) < Date.now()) {
578          return this._fail('status_expired', 'client_expired');
579        }
580      }
581
582      this._setState('solving');
583      this._setStatus('status_solving');
584      this._deadline = Date.now() + this._maxSolveMs;
585      this._startTimer();
586
587      var self_ = this;
588      try {
589        var proof = await this._solve(challenge, payload, function (p) { self_._onProgress(p, payload); });
590        this._clearTimer();
591        await this._onSolved(challenge, proof);
592      } catch (err) {
593        this._clearTimer();
594        if (String(err && err.message) === 'timeout') this._fail('status_expired', 'timeout');
595        else if (this._inlineAbort) { /* reset 主動中止,不報錯 */ }
596        else this._fail('status_error', String((err && err.message) || err));
597      }
598    }
599
600    async _fetchChallenge() {
601      var scope = this.getAttribute('scope') || (location.pathname || '/');
602      var url = this.getAttribute('challenge-url') + (this.getAttribute('challenge-url').indexOf('?') < 0 ? '?' : '&') + 'scope=' + encodeURIComponent(scope);
603      // 標頭內容協商:宿主端點(如 next.php)以 X-Tessera-Challenge 辨識「要新挑戰 JSON」而非整頁 HTML
604      //   (正式前端會把 ?tessera= query 端點 302 擋掉,故用同源頁面 URL + 標頭)。X-Tessera-Retry 供難度提升。
605      var res = await fetch(url, {
606        credentials: 'same-origin',
607        headers: { 'X-Tessera-Challenge': '1', 'X-Tessera-Retry': String(this._retryHint || 0) }
608      });
609      var data = await res.json();
610      if (!data || !data.ok || !data.challenge) throw new Error('challenge_failed');
611      return data.challenge;
612    }
613
614    // 求解:依 challenge_type 路由(memory-v1 走 memory worker);Worker 優先,fallback 主執行緒
615    _solve(challenge, payload, onProgress) {
616      if ((payload.challenge_type || '') === 'memory-v1') {
617        return this._solveMemory(challenge, onProgress);
618      }
619      var url = workerBlobUrl();
620      var external = this.getAttribute('worker-src');
621      if ((external || url) && typeof Worker !== 'undefined') {
622        return this._solveWorker(external || url, challenge, onProgress);
623      }
624      return this._solveInline(challenge, payload, onProgress);
625    }
626
627    // memory-hard 求解(僅 Worker;無 Worker 環境會 reject → 上層顯示錯誤)
628    _solveMemory(challenge, onProgress) {
629      var self_ = this;
630      var url = this.getAttribute('memory-worker-src') || memoryWorkerBlobUrl();
631      if (!url || typeof Worker === 'undefined') {
632        return Promise.reject(new Error('memory_no_worker'));
633      }
634      return new Promise(function (resolve, reject) {
635        var w;
636        try { w = new Worker(url); } catch (e) { return reject(new Error('memory_worker_spawn')); }
637        self_._worker = w;
638        w.onmessage = function (e) {
639          var m = e.data || {};
640          if (m.type === 'phase') {
641            // dataset/merkle/nonce/proof → 仍顯示「驗證中」,進度於 progress 事件更新
642          } else if (m.type === 'progress' && m.progress) {
643            var pr = m.progress;
644            if (pr.total) onProgress({ memFrac: Math.min(pr.done / pr.total, 0.95), phase: pr.phase });
645            else onProgress({ indeterminate: true, phase: pr.phase });
646          } else if (m.type === 'solved') {
647            w.terminate(); self_._worker = null;
648            resolve({ memory: true, proof: m.proof, stats: m.stats || {}, nonce: m.proof && m.proof.nonce });
649          } else if (m.type === 'error') {
650            w.terminate(); self_._worker = null; reject(new Error(m.message || 'memory_worker_error'));
651          }
652        };
653        w.onerror = function () { w.terminate(); self_._worker = null; reject(new Error('memory_worker_error')); };
654        w.postMessage({ type: 'solve', challenge: challenge });
655      });
656    }
657
658    _solveWorker(workerUrl, challenge, onProgress) {
659      var self_ = this;
660      return new Promise(function (resolve, reject) {
661        var w;
662        try { w = new Worker(workerUrl); }
663        catch (e) { return self_._solveInline(challenge, challenge.payload || challenge, onProgress).then(resolve, reject); }
664        self_._worker = w;
665        w.onmessage = function (e) {
666          var m = e.data || {};
667          if (m.type === 'progress') onProgress(m);
668          else if (m.type === 'solved') { w.terminate(); self_._worker = null;
668 resolve({ nonce: m.nonce, hash: m.hash, attempts: m.attempts, elapsed_ms: m.elapsed_ms }); }
669          else if (m.type === 'error') { w.terminate(); self_._worker = null; reject(new Error(m.message || 'worker_error')); }
670        };
671        w.onerror = function () {
672          w.terminate(); self_._worker = null;
673          // Worker 載入/執行失敗 → 退回主執行緒
674          self_._solveInline(challenge, challenge.payload || challenge, onProgress).then(resolve, reject);
675        };
676        w.postMessage({ type: 'solve', challenge: challenge, progressInterval: 15000 });
677      });
678    }
679
680    async _solveInline(challenge, payload, onProgress) {
681      var diff = payload.difficulty;
682      var nonce = 0, attempts = 0, best = 0;
683      var start = (performance && performance.now) ? performance.now() : Date.now();
684      while (true) {
685        if (this._inlineAbort) throw new Error('aborted');
686        if (Date.now() > this._deadline) throw new Error('timeout');
687        var h = await sha256(buildMiningInput(payload, nonce));
688        attempts++;
689        var bits = leadingZeroBits(h); if (bits > best) best = bits;
690        if (hasLeadingZeroBits(h, diff)) {
691          var hex = ''; for (var i = 0; i < h.length; i++) hex += h[i].toString(16).padStart(2, '0');
692          return { nonce: String(nonce), hash: hex, attempts: attempts, elapsed_ms: ((performance && performance.now) ? performance.now() : Date.now()) - start };
693        }
694        if (attempts % 1500 === 0) {
695          onProgress({ attempts: attempts, bestBits: best, difficulty: diff });
696          // 讓出主執行緒,避免凍結 UI
697          await new Promise(function (r) { setTimeout(r, 0); });
698        }
699        nonce++;
700      }
701    }
702
703    _onProgress(p, payload) {
704      if (p && typeof p.memFrac === 'number') { this._setProgress('determinate', p.memFrac); return; }
705      if (p && p.indeterminate) { this._setProgress('indeterminate', 0); return; }
706      var diff = (p && p.difficulty) || (payload && payload.difficulty) || 1;
707      var best = (p && p.bestBits) || 0;
708      // 以「目前最佳前導 0 bit / 目標難度」近似進度,封頂 92% 留給最終命中
709      var frac = Math.min(best / diff, 0.92);
710      this._setProgress('determinate', frac);
711    }
712
713    _setProgress(mode, frac) {
714      if (!this._elProgress) return;
715      this._elProgress.setAttribute('data-mode', mode);
716      this._elProgress.setAttribute('role', 'progressbar');
717      if (mode === 'determinate') {
718        var pct = Math.round(frac * 100);
719        this._elBar.style.width = Math.max(6, pct) + '%';
720        this._elProgress.setAttribute('aria-valuenow', String(pct));
721        this._elProgress.setAttribute('aria-valuemin', '0');
722        this._elProgress.setAttribute('aria-valuemax', '100');
723      } else {
724        this._elBar.style.width = '';
725        this._elProgress.removeAttribute('aria-valuenow');
726      }
727    }
728
729    async _onSolved(challenge, proof) {
730      this._setState('verifying');
731      this._setStatus('status_verifying');
732      this._setProgress('determinate', 1);
733
734      var isMemory = !!(proof && proof.memory);
735      // 寫入 hidden 欄位(隨表單送出由後端驗證)
736      if (this._hidden.ticket) this._hidden.ticket.value = challenge.ticket || '';
737      if (isMemory) {
738        // memory-hard:proof 為物件(含 root/nonce/samples)→ 以 JSON 帶回
739        this._ensureProofField();
740        this._hidden.proof.value = JSON.stringify(proof.proof || {});
741        if (this._hidden.nonce) this._hidden.nonce.value = (proof.proof && proof.proof.nonce) || '';
742      } else {
743        if (this._hidden.nonce) this._hidden.nonce.value = proof.nonce || '';
744      }
745
746      // 若指定 verify-url,主動 POST 給後端即時驗證(否則交給表單送出)
747      var verifyUrl = this.getAttribute('verify-url');
748      if (verifyUrl) {
749        try {
750          var body = isMemory
751            ? { ticket: challenge.ticket, proof: proof.proof }
752            : { ticket: challenge.ticket, nonce: proof.nonce };
753          var res = await fetch(verifyUrl, {
754            method: 'POST', credentials: 'same-origin',
755            headers: { 'Content-Type': 'application/json' },
756            body: JSON.stringify(body)
757          });
758          var data = {};
759          try { data = await res.json(); } catch (e) {}
760          if (!(res.ok && data && data.ok)) {
761            return this._fail('status_error', (data && data.reason) || ('http_' + res.status));
762          }
763          if (data.passToken && this._hidden.ticket) {
764            // 後端回 passToken 時,改帶 passToken(可選欄位)
765            var pt = document.createElement('input');
766            pt.type = 'hidden'; pt.name = this._fieldPrefix + '_pass';
767            pt.value = data.passToken; this.appendChild(pt);
768          }
769        } catch (e) {
770          return this._fail('status_offline', 'verify_network');
771        }
772      }
773      this._succeed(challenge, proof);
774    }
775
776    _succeed(challenge, proof) {
777      this._verified = true;
778      this._setState('success');
779      this._setStatus('status_success');
780      this._root.setAttribute('aria-checked', 'true');
781      this._root.setAttribute('aria-disabled', 'true');
782      this._root.removeAttribute('tabindex');
783      // 統一事件形狀:memory 與 pow 都暴露 {ticket, type, nonce, proof, stats}
784      var isMemory = !!(proof && proof.memory);
785      var detail = isMemory
786        ? { ticket: challenge.ticket, type: 'memory-v1', nonce: proof.nonce, proof: proof.proof, stats: proof.stats || {} }
787        : { ticket: challenge.ticket, type: 'pow-v1', nonce: proof.nonce, proof: proof, stats: proof };
788      this.dispatchEvent(new CustomEvent('tessera-verified', { bubbles: true, composed: true, detail: detail }));
789    }
790
791    _fail(statusKey, reason) {
792      var expired = statusKey === 'status_expired';
793      // 逾時/過期:對齊 Google reCAPTCHA —— 回「未打勾」idle 狀態(空方塊、可重點),
794      //   僅狀態列顯示逾時訊息;不停在 error 叉。其他錯誤維持 error 叉。
795      this._setState(expired ? 'idle' : 'error');
796      this._setStatus(statusKey);
797      this._setProgress('indeterminate', 0);
798      this._verified = false;
799      this._root.setAttribute('aria-checked', 'false');
800      this._root.removeAttribute('aria-disabled');
801      this._root.setAttribute('tabindex', '0');
802      // 逾時累計 → 下次重新驗證時難度提升的依據(帶進事件 detail.retry;求解時再帶給挑戰 gateLevel)。
803      if (expired) this._retryHint = (this._retryHint || 0) + 1;
804      this.dispatchEvent(new CustomEvent(expired ? 'tessera-expired' : 'tessera-error', {
805        bubbles: true, composed: true, detail: { reason: reason, retry: this._retryHint || 0 }
806      }));
807    }
808
809    _startTimer() {
810      var self_ = this;
811      this._clearTimer();
812      this._timer = setInterval(function () {
813        if (Date.now() > self_._deadline) {
814          self_._clearTimer();
815          self_._cleanupWorker();
816          self_._inlineAbort = true;
817          self_._fail('status_expired', 'timeout');
818        }
819      }, 1000);
820    }
821    _clearTimer() { if (this._timer) { clearInterval(this._timer); this._timer = null; } }
822
823    _cleanupWorker() { if (this._worker) { tr
823y { this._worker.terminate(); } catch (e) {} this._worker = null; } }
824    _cleanup() { this._clearTimer(); this._cleanupWorker(); this._inlineAbort = true; }
825
826    // ---- 公開方法 ----
827    reset() {
828      this._cleanup();
829      this._inlineAbort = true;
830      this._verified = false;
831      if (this._hidden.ticket) this._hidden.ticket.value = '';
832      if (this._hidden.nonce) this._hidden.nonce.value = '';
833      if (this._hidden.proof) this._hidden.proof.value = '';
834      this._setState('idle');
835      this._setProgress('indeterminate', 0);
836      this._root.setAttribute('aria-checked', 'false');
837      this._root.removeAttribute('aria-disabled');
838      this._root.setAttribute('tabindex', '0');
839      this._setStatus('status_idle');
840    }
841    start() { this._activate(); }
842    isVerified() { return !!this._verified; }
843
844    // ---- 動態設定語系 / 主題(供宿主套用使用者保存的偏好,且可隨時改)----
845    /** 設定語系(BCP-47,如 'ja' / 'zh-Hant' / 'ar');即時重繪文字與 RTL。等同 setAttribute('lang', …)。 */
846    setLang(lang) { if (lang != null && lang !== '') this.setAttribute('lang', String(lang)); }
847    /** 設定主題:'light' | 'dark' | 'auto'(或空=跟隨系統 prefers-color-scheme);即時套用(CSS 驅動)。 */
848    setTheme(theme) {
849      var v = (theme == null ? '' : String(theme)).toLowerCase();
850      if (v === 'light' || v === 'dark') this.setAttribute('theme', v);
851      else this.removeAttribute('theme'); // auto / 系統
852    }
853  }
854
855  customElements.define('tessera-widget', TesseraWidget);
856})();
857</script>
857
858	
858<script nonce="OEJNNdEDZn0oGoyYA3Jbug">(function(){
859  'use strict';
860  function $(s,r){return (r||document).querySelector(s);}
861  function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded',fn); }
862  ready(function(){
863    var zone=$('.captcha-zone'); if(!zone) return;
864    var rc=$('.g-recaptcha',zone);
865    var tw=$('tessera-widget',zone);
866    var toggle=$('.cap-toggle',zone);
867    var note=$('.cap-fallback-note',zone);
868    var help=$('.cap-help',zone);
869    var back=$('.cap-back',zone);
870    if(!tw) return;
871    // 把 auth 頁面偏好(<html data-theme> / <html lang>,使用者保存或當場切換的設定)同步給 widget,並動態跟隨。
872    function syncPrefs(){
873      var dt=document.documentElement.getAttribute('data-theme');
874      if(dt==='dark'||dt==='light'){ tw.setTheme?tw.setTheme(dt):tw.setAttribute('theme',dt); }
875      else { tw.setTheme?tw.setTheme('auto'):tw.removeAttribute('theme'); }
876      var lg=document.documentElement.getAttribute('lang'); if(lg){ tw.setLang?tw.setLang(lg):tw.setAttribute('lang',lg); }
877    }
878    syncPrefs();
879    try{ new MutationObserver(syncPrefs).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme','lang','dir']}); }catch(e){}
880
881    // 連續驗證失敗 → 顯示「聯繫我們」求助連結(widget 每次失敗會 bubble tessera-error / tessera-expired;
882    // composed:true 可穿出 Shadow DOM 冒泡到 zone)。成功則清除。門檻 2 次,避免單次手滑就出現。
883    var troubles=0;
884    function onTrouble(){ if(++troubles>=2 && help){ help.classList.remove('tsr-hidden'); } }
885    zone.addEventListener('tessera-error', onTrouble);
886    zone.addEventListener('tessera-expired', onTrouble);
887    zone.addEventListener('tessera-verified', function(){ troubles=0; if(help){ help.classList.add('tsr-hidden'); } });
888
889    // primary:伺服器端已直接顯示 Tessera(reCAPTCHA 未設定 / CAPTCHA_PRIMARY=tessera)。
890    //   此模式「本來就用內建驗證」,並無 reCAPTCHA 失敗 / 切換情境 → 不顯示任何切換提示。
891    var primary = !tw.classList.contains('tsr-hidden');
892    if(primary) return;
893
894    // ---- 以下為「reCAPTCHA 為主、Tessera 為備援」的切換邏輯 ----
895    var shown=false;
896    function showTessera(reason){
897      if(shown) return; shown=true;
898      tw.classList.remove('tsr-hidden');
899      if(rc) rc.classList.add('tsr-hidden');
900      if(toggle) toggle.classList.add('tsr-hidden');
901      if(note){
902        // 文案依切換原因:auto=reCAPTCHA 載入失敗(陳述已自動改用);manual=使用者主動切換(中性確認)。
903        var msg=note.getAttribute(reason==='manual' ? 'data-manual' : 'data-auto');
904        if(msg){ note.textContent=msg; }
905        note.classList.remove('tsr-hidden');
906      }
907      // 只有「手動切換」(reCAPTCHA 其實正常)才提供切回 Google;auto(reCAPTCHA 壞掉)切回沒意義。
908      if(reason==='manual' && back){ back.classList.remove('tsr-hidden'); }
909    }
910    // 切回 Google reCAPTCHA(reverse of showTessera):隱藏 Tessera/提示/切回鈕,重新顯示 reCAPTCHA + 切換鈕。
911    function showRecaptcha(){
912      shown=false;
913      tw.classList.add('tsr-hidden');
914      if(rc) rc.classList.remove('tsr-hidden');
915      if(note) note.classList.add('tsr-hidden');
916      if(back) back.classList.add('tsr-hidden');
917      if(toggle) toggle.classList.remove('tsr-hidden');
918    }
919    function recaptchaRendered(){
920      // reCAPTCHA 載入後會在 .g-recaptcha 內插入 iframe
921      return !!(rc && rc.querySelector('iframe')) || (typeof window.grecaptcha!=='undefined' && window.grecaptcha.render);
922    }
923    // 手動切換(reCAPTCHA 正常時提供的選項)+ 切回
924    if(toggle){ toggle.addEventListener('click',function(e){ e.preventDefault(); showTessera('manual'); }); }
925    if(back){ back.addEventListener('click',function(e){ e.preventDefault(); showRecaptcha(); }); }
926    // 自動偵測:若該網域未註冊 reCAPTCHA / 載入被擋 → 一段時間後沒渲染就自動切 Tessera
927    if(!rc){ showTessera('auto'); return; }
928    var waited=0, iv=setInterval(function(){
929      waited+=500;
930      if(recaptchaRendered()){ clearInterval(iv); if(toggle) toggle.classList.remove('tsr-hidden'); return; }
931      if(waited>=5000){ clearInterval(iv); showTessera('auto'); }
932    },500);
933  });
934})();</script>
934
935	
935<script nonce="OEJNNdEDZn0oGoyYA3Jbug">window.__CD__={teExp:1790937216000,teWin:180,now:1790937036742,tePrimary:false,orangeFrac:0.6667,redFrac:0.1667,preempt:0.95,rcWin:110,lang:"en"};window.__CD_I18N__={"zh-Hant":{"label":"驗證有效時間","ok":"時間充足","warn":"請儘快完成","danger":"即將逾時","tip":"這是本次人機驗證的有效時間。逾時需重新整理頁面再驗證一次,您輸入的帳號會保留。","exp_title":"驗證有效時間已過","exp_body":"為了安全,請重新整理頁面取得新的驗證。您輸入的帳號會保留,密碼需重新輸入。","reload":"重新整理"},"zh-Hans":{"label":"验证有效时间","ok":"时间充足","warn":"请尽快完成","danger":"即将超时","tip":"这是本次人机验证的有效时间。超时需刷新页面再验证一次,您输入的账号会保留。","exp_title":"验证有效时间已过","exp_body":"为了安全,请刷新页面获取新的验证。您输入的账号会保留,密码需重新输入。","reload":"刷新页面"},"en":{"label":"Verification time left","ok":"Plenty of time","warn":"Please finish soon","danger":"About to expire","tip":"How long the current verification stays valid. If it expires, refresh the page to verify again — your account name is kept.","exp_title":"Verification has expired","exp_body":"For your security, refresh the page to get a new verification. Your account name is kept; you’ll need to re-enter your password.","reload":"Refresh"},"ja":{"label":"認証の有効時間","ok":"時間に余裕あり","warn":"お早めに完了を","danger":"まもなく期限切れ","tip":"現在の確認が有効な残り時間です。期限切れの場合はページを再読み込みして再度確認してください。入力したアカウントは保持されます。","exp_title":"認証の有効期限が切れました","exp_body":"安全のため、ページを再読み込みして新しい確認を取得してください。アカウントは保持されますが、パスワードは再入力が必要です。","reload":"再読み込み"},"ko":{"label":"인증 유효 시간","ok":"시간 충분","warn":"ê³§ 완료해 주세요","danger":"ê³§ 만료됨","tip":"현재 인증이 유효한 남은 시간입니다. 만료되면 페이지를 새로 고쳐 다시 인증하세요. 입력한 계정은 유지됩니다.","exp_title":"인증 유효 시간이 지났습니다","exp_body":"보안을 위해 페이지를 새로 고쳐 새 인증을 받으세요. 계정은 유지되며 비밀번호는 다시 입력해야 합니다.","reload":"새로 고침"},"es":{"label":"Tiempo de verificación restante","ok":"Tiempo de sobra","warn":"Termina pronto, por favor","danger":"A punto de caducar","tip":"Tiempo durante el cual la verificación actual sigue siendo válida. Si caduca, actualiza la página para verificar de nuevo; se conservará tu cuenta.","exp_title":"La verificación ha caducado","exp_body":"Por seguridad, actualiza la página para obtener una nueva verificación. Se conservará tu cuenta; tendrás que volver a escribir la contraseña.","reload":"Actualizar"},"fr":{"label":"Temps de vérification restant","ok":"Largement le temps","warn":"À terminer rapidement","danger":"Bientôt expiré","tip":"Durée pendant laquelle la vérification actuelle reste valable. En cas d’expiration, actualisez la page pour vérifier à nouveau ; votre identifiant est conservé.","exp_title":"La vérification a expiré","exp_body":"Par sécurité, actualisez la page pour obtenir une nouvelle vérification. Votre identifiant est conservé ; vous devrez ressaisir le mot de passe.","reload":"Actualiser"},"de":{"label":"Verbleibende Prüfzeit","ok":"Genug Zeit","warn":"Bitte bald abschließen","danger":"Läuft gleich ab","tip":"So lange bleibt die aktuelle Prüfung gültig. Nach Ablauf die Seite neu laden und erneut verifizieren – Ihr Konto bleibt erhalten.","exp_title":"Die Prüfung ist abgelaufen","exp_body":"Bitte laden Sie die Seite aus Sicherheitsgründen neu, um eine neue Prüfung zu erhalten. Ihr Konto bleibt erhalten; das Passwort müssen Sie erneut eingeben.","reload":"Neu laden"},"ru":{"label":"Осталось времени на проверку","ok":"Времени достаточно","warn":"Завершите поскорее","danger":"Скоро истечёт","tip":"Сколько ещё действительна текущая проверка. После истечения обновите страницу и пройдите проверку снова — ваш логин сохранится.","exp_title":"Срок проверки истёк","exp_body":"В целях безопасности обновите страницу, чтобы получить новую проверку. ЛогиÐ
935½ сохранится, пароль нужно ввести заново.","reload":"Обновить"},"vi":{"label":"Thời gian xác minh còn lại","ok":"Còn nhiều thời gian","warn":"Vui lòng hoàn tất sớm","danger":"Sắp hết hạn","tip":"Đây là thời gian hiệu lá»±c cá»§a lần xác minh này. Nếu hết hạn, vui lòng tải lại trang để xác minh lại; tài khoản bạn đã nhập sẽ được giữ lại.","exp_title":"Xác minh đã hết hạn","exp_body":"Vì lý do bảo mật, vui lòng tải lại trang để xác minh lại. Tài khoản bạn đã nhập sẽ được giữ lại; bạn cần nhập lại mật khẩu.","reload":"Tải lại"},"th":{"label":"เวลายืนยันที่เหลือ","ok":"มีเวลาเหลือมาก","warn":"โปรดทำให้เสร็จเร็ว ๆ","danger":"ใกล้หมดเวลา","tip":"นี่คือเวลาที่การยืนยันปัจจุบันยังใช้ได้ หากหมดเวลา โปรดรีเฟรชหน้าเพื่อยืนยันอีกครั้ง บัญชีของคุณจะถูกเก็บไว้","exp_title":"การยืนยันหมดเวลาแล้ว","exp_body":"เพื่อความปลอดภัย โปรดรีเฟรชหน้าเพื่อรับการยืนยันใหม่ บัญชีจะถูกเก็บไว้ แต่ต้องกรอกรหัสผ่านอีกครั้ง","reload":"รีเฟรช"},"id":{"label":"Sisa waktu verifikasi","ok":"Waktu masih banyak","warn":"Harap segera selesaikan","danger":"Hampir kedaluwarsa","tip":"Ini adalah masa berlaku verifikasi ini. Jika kedaluwarsa, muat ulang halaman untuk verifikasi ulang; akun yang Anda masukkan akan tetap tersimpan.","exp_title":"Verifikasi telah kedaluwarsa","exp_body":"Demi keamanan, muat ulang halaman untuk mendapatkan verifikasi baru. Akun Anda disimpan; Anda perlu memasukkan kata sandi lagi.","reload":"Muat ulang"},"ms":{"label":"Baki masa pengesahan","ok":"Masa mencukupi","warn":"Sila selesaikan segera","danger":"Hampir tamat tempoh","tip":"Ini tempoh pengesahan semasa masih sah. Jika tamat tempoh, muat semula halaman untuk mengesah semula; akaun anda akan dikekalkan.","exp_title":"Pengesahan telah tamat tempoh","exp_body":"Demi keselamatan, sila muat semula halaman untuk mendapatkan pengesahan baharu. Akaun anda dikekalkan; anda perlu memasukkan kata laluan semula.","reload":"Muat semula"},"fil":{"label":"Natitirang oras ng pag-verify","ok":"Marami pang oras","warn":"Pakitapos na agad","danger":"Malapit nang mag-expire","tip":"Ito ang tagal na balido ang kasalukuyang pag-verify. Kapag nag-expire, i-refresh ang pahina para mag-verify ulit; mananatili ang iyong account.","exp_title":"Nag-expire na ang pag-verify","exp_body":"Para sa iyong seguridad, i-refresh ang pahina para makakuha ng bagong pag-verify. Mananatili ang account; kailangan mong muling ilagay ang password.","reload":"I-refresh"},"nl":{"label":"Resterende verificatietijd","ok":"Tijd genoeg","warn":"Rond het snel af","danger":"Verloopt bijna","tip":"Zo lang blijft de huidige verificatie geldig. Vernieuw bij het verlopen de pagina om opnieuw te verifiëren; je account blijft behouden.","exp_title":"De verificatie is verlopen","exp_body":"Vernieuw voor je veiligheid de pagina voor een nieuwe verificatie. Je account blijft behouden; je moet het wachtwoord opnieuw invoeren.","reload":"Vernieuwen"},"pt":{"label":"Tempo de verificação restante","ok":"Tempo de sobra","warn":"Conclua em breve, por favor","danger":"Prestes a expirar","tip":"Tempo durante o qual a verificação atual continua válida. Se expirar, atualize a página para verificar de novo; a sua conta será mantida.","exp_title":"A verificação expirou","exp_body":"Por segurança, atualize a página para obter uma nova verificação. A sua conta será mantida; terá de introduzir a palavra-passe novamente.","reload":"Atualizar"}};</script>
935
936	
936<script nonce="OEJNNdEDZn0oGoyYA3Jbug">(function(){
937  'use strict';
938  var CFG=window.__CD__; if(!CFG){ return; }
939  var I18N=window.__CD_I18N__||{};
940  function $(s,r){ return (r||document).querySelector(s); }
941  function ready(fn){ if(document.readyState!=='loading'){ fn(); } else { document.addEventListener('DOMContentLoaded',fn); } }
942  function canon(l){
943    if(!l){ return 'en'; }
944    var lo=String(l).toLowerCase();
945    var m={'zh':'zh-Hant','zh-tw':'zh-Hant','zh-hk':'zh-Hant','zh-mo':'zh-Hant','zh-hant':'zh-Hant','zh-cn':'zh-Hans','zh-sg':'zh-Hans','zh-hans':'zh-Hans','tl':'fil'};
946    if(m[lo]){ return m[lo]; }
947    if(I18N[l]){ return l; }
948    for(var k in I18N){ if(k.toLowerCase()===lo){ return k; } }
949    var base=lo.split('-')[0];
950    for(var k2 in I18N){ if(k2.toLowerCase()===base){ return k2; } }
951    return I18N['en']?'en':(Object.keys(I18N)[0]||'en');
952  }
953  var lang=canon(document.documentElement.getAttribute('lang')||CFG.lang||'en');
954  function T(){ return I18N[lang]||I18N['en']||{}; }
955  var CIRC=2*Math.PI*18;
956  ready(function(){
957    var zone=$('.captcha-zone');
958    var row=$('.cd-row'), arc=$('.cd-arc'), num=$('.cd-num'), state=$('.cd-state'),
959        tip=$('.cd-tip'), wrap=$('.cd-ring-wrap'), panel=$('.cd-expired'), card=$('.card');
960    if(!row||!arc||!panel){ return; }
961    var skew=(typeof CFG.now==='number')?(CFG.now-Date.now()):0;
962    function applyI18N(){
963      var t=T();
964      if(tip&&t.tip){ tip.textContent=t.tip; }
965      var et=panel.querySelector('.cd-exp-title'); if(et&&t.exp_title){ et.textContent=t.exp_title; }
966      var eb=panel.querySelector('.cd-exp-body'); if(eb&&t.exp_body){ eb.textContent=t.exp_body; }
967      var rl=panel.querySelector('.cd-reload-label'); if(rl&&t.reload){ rl.textContent=t.reload; }
968    }
969    applyI18N();
970    var sel=$('#langSelect');
971    // 切換語言 → 帶 ?lang 重新載入,讓伺服器把整頁(cap-* 切換提示 / reCAPTCHA hl / 倒數 / 標題)一次渲染成新語言。
972    // 切換前暫存帳號(密碼不存),載入後由上面的還原邏輯帶回;並寫 AUTH_LANG cookie 確保下次記住。
973    if(sel){ sel.addEventListener('change',function(){
974      var v=sel.value||lang;
975      try{ var af=$('#account'); if(af&&af.value){ sessionStorage.setItem('cd_acct',af.value); } }catch(e){}
976      try{ document.cookie='AUTH_LANG='+encodeURIComponent(v)+'; path=/; max-age=31536000; samesite=Lax'; }catch(e){}
977      try{ var u=new URL(location.href); u.searchParams.set('lang', v); location.assign(u.toString()); }catch(e){ location.reload(); }
978    }); }
979    try{ new MutationObserver(function(){ var nl=canon(document.documentElement.getAttribute('lang')||lang); if(nl!==lang){ lang=nl; applyI18N(); tick(); } }).observe(document.documentElement,{attributes:true,attributeFilter:['lang']}); }catch(e){}
980    try{ var acc=sessionStorage.getItem('cd_acct'); if(acc){ var af=$('#account'); if(af&&!af.value){ af.value=acc; } sessionStorage.removeItem('cd_acct'); } }catch(e){}
981    var rcDeadline=null, teShownAt=null, teRevWin=0;
982    window.__cdRcSolved=function(){ rcDeadline=Date.now()+((CFG.rcWin||110)*1000); tick(); };
983    window.__cdRcExpired=function(){ rcDeadline=Date.now()-1; tick(); };
984    function visible(el){ return !!(el && !el.classList.contains('tsr-hidden') && el.offsetParent!==null); }
985    function active(){
986      var tw=zone&&zone.querySelector('tessera-widget');
987      var rc=zone&&zone.querySelector('.g-recaptcha');
988      if(visible(tw) && typeof CFG.teExp==='number'){
989        if(CFG.tePrimary){ return {dl:CFG.teExp,win:(CFG.teWin||180)*1000,srv:true}; }
990        // fallback:widget 載入時隱藏、稍後才顯示 → 從「顯示當下」固定一個窗(min(teWin, 票券剩餘秒)),錨定真實票券到期;避免沿用載入絕對窗造成誤鎖/誤橘
991        if(teShownAt===null){ teShownAt=Date.now(); teRevWin=Math.max(0,Math.min((CFG.teWin||180),(CFG.teExp-(Date.now()+skew))/1000)); }
992        if(teRevWin<=0){ return {dl:(Date.now()+skew)-1,win:(CFG.teWin||180)*1000,srv:true,dead:true}; }
993        return {dl:(teShownAt+skew)+teRevWin*1000,win:teRevWin*1000,srv:true};
994      }
995      if(visible(rc)){ return rcDeadline?{dl:rcDeadline,win:(CFG.rcWin||110)*1000,srv:false}:null; }
996      if(typeof CFG.teExp==='number'){ return {dl:CFG.teExp,win:(CFG.teWin||180)*1000,srv:true}; }
997      return null;
998    }
999    var done=false, timer=null;
1000    function fmt(s){ s=Math.max(0,Math.ceil(s)); var m=Math.floor(s/60); var x=s%60; return m+':'+(x<10?'0'+x:x); }
1001    function expire(){
1002      if(done){ return; } done=true;
1003      if(timer){ clearInterval(timer); timer=null; }
1004      if(card){ card.classList.add('cd-locked'); }
1005      panel.classList.remove('tsr-hidden');
1006      var rb=panel.querySelector('.cd-reload'); if(rb){ try{ rb.focus(); }catch(e){} }
1007    }
1008    function tick(){
1009      if(done){ return; }
1010      var a=active();
1011      if(!a){ row.setAttribute('hidden',''); return; }
1012      row.removeAttribute('hidden');
1013      if(a.dead){ expire(); return; }
1014      var now=a.srv?(Date.now()+skew):Date.now();
1015      var rem=(a.dl-now)/1000, win=a.win/1000;
1016      var frac=Math.max(0,Math.min(1,rem/win));
1017      arc.style.strokeDashoffset=(CIRC*(1-frac)).toFixed(2);
1018      num.textContent=fmt(rem);
1019      var t=T(), c, sk, fr=win>0?rem/win:0;
1020      if(fr<=(CFG.redFrac||0.1667)){ c='#E24B4A'; sk=t.danger; }
1021      else if(fr<=(CFG.orangeFrac||0.6667)){ c='#E6952B'; sk=t.warn; }
1022      else { c='#1D9E75'; sk=t.ok; }
1023      arc.setAttribute('stroke',c);
1024      if(state){ state.textContent=sk||''; }
1025      if(wrap){ wrap.setAttribute('aria-label',(t.label||'')+' '+fmt(rem)); }
1026      if(rem<=0 || rem<=win*(1-(CFG.preempt||0.95))){ expire(); }
1027    }
1028    try{ if(zone){ var mo=new MutationObserver(function(){ tick(); }); ['.g-recaptcha','tessera-widget'].forEach(function(s){ var el=zone.querySelector(s); if(el) mo.observe(el,{attributes:true,attributeFilter:['class']}); }); } }catch(e){}
1029    var rl2=panel.querySelector('.cd-reload');
1030    if(rl2){ rl2.addEventListener('click',function(e){ e.preventDefault();
1031      try{ var af2=$('#account'); if(af2&&af2.value){ sessionStorage.setItem('cd_acct',af2.value); } }catch(e2){}
1032      location.reload();
1033    }); }
1034    tick();
1035    timer=setInterval(tick,500);
1036  });
1037})();</script>
1037
1038</body>
1039</html>

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.