1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4<meta charset="UTF-8"> 5<meta itemprop="name" content="æè°¶é©¿ç« - å¼åè 弿¾ä»åº"/> 6<meta itemprop="image" content="https://blog.fohok.xin/wp-content/uploads/2026/04/cropped-a91fabf8a49526b0ab7941fb2659297e1755339178171.jpg"/> 7<meta name="description" itemprop="description" content="ä¸ä¸ªä¸ºç¬ç«å¼åè æé ç弿ºé¡¹ç®å±ç¤ºä»åºãå¨è¿éï¼åå¸ä½ çä½åï¼æ¢ç´¢ä»äººçåé ï¼è¿æ¥å¿åéåçå¼åè ã"/> 8<meta name="viewport" content="width=device-width, initial-scale=1.0">
9<script> 10/* Apply theme before first paint to avoid flash */ 11(function(){var t=localStorage.getItem('fohok_theme'); 12if(t==='dark'||((!t)&&window.matchMedia('(prefers-color-scheme: dark)').matches)){ 13 document.documentElement.setAttribute('data-theme','dark'); 14}else{document.documentElement.setAttribute('data-theme','light');} 15})(); 16</script>
16 17<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)"> 18<meta name="theme-color" content="#050505" media="(prefers-color-scheme: dark)"> 19<meta property="og:title" content="æè°¶é©¿ç« · å¼åè 弿¾ä»åº"> 20<meta property="og:description" content="ä¸ä¸ªä¸ºç¬ç«å¼åè æé ç弿ºé¡¹ç®å±ç¤ºç©ºé´ãåå¸ä½ çä½åï¼æ¢ç´¢ä»äººçåé ï¼è¿æ¥å¿åéåç人ã"> 21<meta property="og:image" content="https://fohok.xin/img/fohok-2.png"> 22<meta property="og:url" content="https://fohok.xin/"> 23<meta property="og:type" content="website"> 24<meta property="og:site_name" content="æè°¶é©¿ç« · å¼åè 弿¾ä»åº"> 25<link rel="icon" href="/img/fohok-2.png" sizes="any"> 26<link rel="alternate" type="application/rss+xml" title="æè°¶é©¿ç« RSS" href="/fohok-rss.php"> 27<title>æè°¶é©¿ç« · å¼åè 弿¾ä»åº</title> 28 29<link rel="preconnect" href="https://fonts.font.im"> 30<link href="https://fonts.font.im/css2?family=JetBrains+Mono:wght@400;500;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet"> 31 32<style> 33*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 34 35/* ââ TOKENS âââââââââââââââââââââââââââââââââââââââââââââââ */ 36:root { 37 color-scheme: light; 38 /* æ¥é´ */ 39 --bg-primary: #fafafa; 40 --bg-card: #ffffff; 41 --bg-secondary: #f5f5f5; 42 --bg-input: #f5f5f5; 43 --bg-hover: #f0f0f0; 44 --text-primary: #171717; 45 --text-secondary: #404040; 46 --text-dim: #737373; 47 --text-muted: #a3a3a3; 48 --border-primary: #e5e5e5; 49 --border-secondary: #d4d4d4; 50 --border-input: #d4d4d4; 51 --border-hover: #a3a3a3; 52 --accent-color: #171717; 53 --accent-inverse: #f5f5f5; 54 --danger-color: #dc2626; 55 --grid-line: rgba(23,23,23,.05); 56 57 /* ç¶æè²æ¿ï¼æ¥é´ï¼ */ 58 --state-ok-bg: #f0fdf4; --state-ok-bd: #bbf7d0; --state-ok-tx: #166534; 59 --state-info-bg: #eff6ff;--state-info-bd:#bfdbfe; --state-info-tx:#1e40af; 60 --state-warn-bg:#fefce8; --state-warn-bd:#fde68a; --state-warn-tx:#854d0e; 61 --state-notice-bg:#f0f9ff;--state-notice-bd:#bae6fd;--state-notice-tx:#075985; 62 --state-danger-bg:#fef2f2;--state-danger-bd:#fecaca;--state-danger-tx:#dc2626; 63 64 --scrollbar-track: #f5f5f5; 65 --scrollbar-thumb: #d4d4d4; 66 67 --f-sans: 'Noto Sans SC', system-ui, sans-serif; 68 --f-mono: 'JetBrains Mono', ui-monospace, monospace; 69 70 --r-2: 4px; --r-3: 6px; --r-4: 8px; --r-6: 12px; 71 --ease: cubic-bezier(0.16, 1, 0.3, 1); 72} 73 74[data-theme="dark"] { 75 color-scheme: dark; 76 --bg-primary: #050505; 77 --bg-card: #0a0a0a; 78 --bg-secondary: #111111; 79 --bg-input: #111111; 80 --bg-hover: #1a1a1a; 81 --text-primary: #f5f5f5; 82 --text-secondary: #d4d4d4; 83 --text-dim: #a3a3a3; 84 --text-muted: #737373; 85 --border-primary: #1a1a1a; 86 --border-secondary: #222222; 87 --border-input: #222222; 88 --border-hover: #333333; 89 --accent-color: #f5f5f5; 90 --accent-inverse: #171717; 91 --danger-color: #f87171; 92 --grid-line: rgba(255,255,255,.045); 93 94 --state-ok-bg:#052e16; --state-ok-bd:#14532d; --state-ok-tx:#4ade80; 95 --state-info-bg:#0f172a;--state-info-bd:#1e3a5f;--state-info-tx:#60a5fa; 96 --state-warn-bg:#1a1a00;--state-warn-bd:#3a3000;--state-warn-tx:#fbbf24; 97 --state-notice-bg:#0d1117;--state-notice-bd:#1e3a5f;--state-notice-tx:#7dd3fc; 98 --state-danger-bg:#1a0808;--state-danger-bd:#3a1515;--state-danger-tx:#f87171; 99 100 --scrollbar-track: #0a0a0a; 101 --scrollbar-thumb: #333333; 102} 103 104/* ââ BASE âââââââââââââââââââââââââââââââââââââââââââââââ */ 105html { font-size: 16px; scroll-behavior: smooth; } 106body { 107 background: var(--bg-primary); 108 color: var(--text-primary); 109 font-family: var(--f-sans); 110 min-height: 100vh; 111 overflow-x: hidden; 112 -webkit-font-smoothing: antialiased; 113 transition: background .3s, color .3s; 114} 115 116::-webkit-scrollbar { width: 4px; height: 4px; } 117::-webkit-scrollbar-track { background: var(--scrollbar-track); } 118::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 2px; } 119 120a { color: inherit; text-decoration: none; } 121::selection { background: color-mix(in srgb, var(--accent-color) 16%, transparent); color: var(--text-primary); } 122 123/* Utility */ 124.hl { background: var(--bg-secondary); padding: 0 .12em; border-radius: 2px; } 125 126/* ââ TOPBAR âââââââââââââââââââââââââââââââââââââââââââââ */ 127#topbar { 128 position: fixed; top: 0; left: 0; right: 0; height: 64px; 129 background: color-mix(in srgb, var(--bg-primary) 80%, transparent); 130 backdrop-filter: blur(14px); 131 -webkit-backdrop-filter: blur(14px); 132 border-bottom: 1px solid var(--border-primary); 133 z-index: 100; 134 display: flex; align-items: center; 135 padding: 0 28px; gap: 12px; 136} 137 138.logo-mark { display: flex; align-items: center; gap: 12px; color: var(--text-primary); } 139.logo-block { width: 34px; height: 34px; object-fit: cover; display: block; border-radius: 6px; border: 1px solid var(--border-secondary); } 140.logo-text { display: flex; flex-direction: column; line-height: 1.1; } 141.logo-char { font-family: var(--f-sans); font-size: 17px; font-weight: 700; letter-spacing: .01em; color: var(--text-primary); } 142.logo-sub { font-family: var(--f-mono); font-size: 9.5px; color: var(--text-dim); letter-spacing: .18em; text-transform: uppercase; margin-top: 2px; font-weight: 500; } 143 144/* ââ TOPBAR ACTION ICONS âââââââââââââââââââââââââââââââ */ 145.topbar-app-icon, .theme-toggle { 146 position: relative; 147 display: inline-flex; align-items: center; justify-content: center; 148 width: 40px; height: 40px; 149 color: var(--text-primary); 150 background: var(--bg-secondary); 151 border: 1px solid var(--border-primary); 152 border-radius: var(--r-3); 153 transition: background .15s, border-color .15s, transform .15s, box-shadow .15s; 154 text-decoration: none; cursor: pointer; font-family: inherit; 155} 156.topbar-app-icon:hover, .theme-toggle:hover { background: var(--bg-hover); border-color: var(--border-hover); transform: translateY(-1px); } 157.app-icon-dot { position: absolute; top: -3px; right: -3px; width: 10px; height: 10px; background: var(--danger-color); border: 2px solid var(--bg-card); border-radius: 50%; pointer-events: none; }
158.theme-toggle .ico-sun { display: none; } 159.theme-toggle .ico-moon { display: block; } 160[data-theme="dark"] .theme-toggle .ico-sun { display: block; } 161[data-theme="dark"] .theme-toggle .ico-moon { display: none; } 162 163/* ââ DROPDOWN ââââââââââââââââââââââââââââââââââââââââââ */ 164.dropdown { position: relative; } 165.dropdown-trigger { background: none; border: none; cursor: pointer; display: flex; align-items: center; padding: 3px; } 166.dropdown-trigger img { width: 36px; height: 36px; border-radius: 50%; border: 2px solid var(--border-primary); object-fit: cover; } 167.dropdown-menu { 168 position: absolute; right: 0; top: calc(100% + 10px); 169 background: var(--bg-card); color: var(--text-primary); 170 border: 1px solid var(--border-secondary); border-radius: var(--r-6); 171 min-width: 210px; z-index: 200; opacity: 0; pointer-events: none; 172 transform: translateY(-6px); 173 transition: opacity .18s var(--ease), transform .18s var(--ease); 174 box-shadow: 0 12px 32px -12px rgba(0,0,0,.18); 175 padding: 6px; 176} 177.dropdown-menu.show { opacity: 1; pointer-events: auto; transform: none; } 178.dropdown-item { 179 display: flex; align-items: center; gap: 11px; 180 padding: 10px 12px; font-size: 14px; font-weight: 500; 181 color: var(--text-primary); cursor: pointer; 182 border: none; background: none; width: 100%; text-align: left; 183 font-family: var(--f-sans); border-radius: var(--r-2); 184 transition: background .12s; 185} 186.dropdown-item:hover { background: var(--bg-hover); } 187.dropdown-item.danger { color: var(--danger-color); } 188.dropdown-item.danger:hover { background: color-mix(in srgb, var(--danger-color) 10%, transparent); } 189.dropdown-sep { height: 1px; background: var(--border-primary); margin: 6px 4px; } 190 191/* ââ BUTTONS âââââââââââââââââââââââââââââââââââââââââââ */ 192.btn { 193 display: inline-flex; align-items: center; justify-content: center; 194 gap: 8px; font-family: var(--f-sans); font-size: 14px; font-weight: 500; 195 cursor: pointer; border-radius: var(--r-3); 196 transition: background .18s, border-color .18s, color .18s, transform .18s var(--ease), box-shadow .18s; 197 text-decoration: none; letter-spacing: .01em; border: 1px solid transparent; 198} 199.btn svg { stroke-width: 2; flex-shrink: 0; } 200.btn-primary { background: var(--accent-color); color: var(--accent-inverse); padding: 12px 22px; } 201.btn-primary:hover { filter: brightness(1.12); transform: translateY(-1px); box-shadow: 0 8px 20px -10px rgba(0,0,0,.4); } 202.btn-ghost { background: transparent; color: var(--text-dim); border-color: var(--border-secondary); padding: 11px 21px; } 203.btn-ghost:hover { border-color: var(--border-hover); color: var(--text-primary); background: var(--bg-hover); transform: translateY(-1px); } 204.btn-sm { padding: 8px 14px; font-size: 13px; border-radius: var(--r-2); } 205.btn-sm.btn-primary:hover { box-shadow: 0 6px 14px -8px rgba(0,0,0,.35); } 206 207/* ââ ANNOUNCEMENT BUBBLE âââââââââââââââââââââââââââââââ */ 208.logo-wrap { position: relative; } 209.ann-bubble { 210 position: absolute; 211 top: calc(100% + 14px); left: 0; 212 width: 340px; max-width: calc(100vw - 52px); 213 background: var(--bg-card); color: var(--text-primary); 214 border: 1px solid var(--border-secondary); border-radius: var(--r-6); 215 box-shadow: 0 20px 44px -18px rgba(0,0,0,.22); 216 z-index: 300;
216 opacity: 0; pointer-events: none; 217 transform: translateY(-8px) scale(.98); 218 transform-origin: top left; 219 transition: opacity .2s var(--ease), transform .2s var(--ease); 220 overflow: hidden; 221} 222.ann-bubble.show { opacity: 1; pointer-events: auto; transform: none; } 223.ann-bubble::before { 224 content: ''; position: absolute; top: -7px; left: 16px; 225 width: 12px; height: 12px; 226 background: var(--bg-card); 227 border-left: 1px solid var(--border-secondary); 228 border-top: 1px solid var(--border-secondary); 229 transform: rotate(45deg); z-index: 1; 230} 231.ann-head { 232 display: flex; align-items: center; justify-content: space-between; 233 padding: 12px 18px; 234 font-family: var(--f-mono); font-size: 11px; font-weight: 500; 235 letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim); 236 border-bottom: 1px solid var(--border-primary); 237} 238.ann-close-x { background: none; border: none; cursor: pointer; color: var(--text-dim); font-size: 16px; font-weight: 500; line-height: 1; padding: 2px 6px; border-radius: var(--r-2); font-family: var(--f-sans); } 239.ann-close-x:hover { color: var(--text-primary); background: var(--bg-hover); } 240.ann-title { font-family: var(--f-sans); font-size: 15px; font-weight: 700; color: var(--text-primary); line-height: 1.3; padding: 14px 18px 6px; } 241.ann-content { font-size: 13.5px; line-height: 1.7; color: var(--text-secondary); padding: 0 18px 16px; } 242.ann-content p { margin: 0; } 243.ann-content p + p { margin-top: 8px; } 244.ann-content a { color: var(--accent-color); font-weight: 500; text-decoration: underline; text-underline-offset: 2px; } 245.ann-content a:hover { opacity: .75; } 246 247/* severity ç¶æï¼èªå¨éé æ¥/å¤ï¼ */ 248.ann-bubble.ann-notice { background: var(--state-notice-bg); border-color: var(--state-notice-bd); } 249.ann-bubble.ann-notice::before { background: var(--state-notice-bg); } 250.ann-bubble.ann-info { background: var(--state-info-bg); border-color: var(--state-info-bd); } 251.ann-bubble.ann-info::before { background: var(--state-info-bg); } 252.ann-bubble.ann-warning { background: var(--state-warn-bg); border-color: var(--state-warn-bd); } 253.ann-bubble.ann-warning::before { background: var(--state-warn-bg); } 254.ann-bubble.ann-important{ background: var(--accent-color); color: var(--accent-inverse); border-color: var(--accent-color); } 255.ann-bubble.ann-important::before{ background: var(--accent-color); } 256.ann-bubble.ann-important .ann-head{ color: color-mix(in srgb, currentColor 72%, transparent); border-bottom-color: color-mix(in srgb, currentColor 18%, transparent); } 257.ann-bubble.ann-important .ann-title{ color: inherit; } 258.ann-bubble.ann-important .ann-content{ color: color-mix(in srgb, currentColor 86%, transparent); } 259.ann-bubble.ann-important .ann-content a{ color: inherit; text-decoration-color: currentColor; } 260.ann-bubble.ann-important .ann-close-x{ color: inherit; } 261.ann-bubble.ann-important .ann-close-x:hover{ background: color-mix(in srgb, #fff 12%, transparent); } 262 263/* Badge dot on logo */ 264.logo-badge { 265 position: absolute; top: -2px; right: -2px; 266 width: 10px; height: 10px; 267 background: var(--danger-color); 268 border: 2px solid var(--bg-primary); 269 border-radius: 50%; display: none; 270 animation: badgePulse 1.8s ease infinite; 271} 272.logo-badge.show { display: block; } 273@keyframes badgePulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.3); } } 274 275/* ââ HERO âââââââââââââââââââââââââââââââââââââââââââââââ */ 276.hero { 277 min-height: 100svh; padding-top: 64px; 278 position: relative; 279 display: flex; align-items: center; 280 border-bottom: 1px solid var(--border-primary); 281 overflow: hidden; 282} 283/* ç»ç½æ ¼çº¹çï¼å¼åºå¼åè æ°è´¨ */ 284.hero::before { 285 content: ''; position: absolute; inset: 0; pointer-events: none; 286 background-image: 287 linear-gradient(to right, var(--grid-line) 1px, transparent 1px), 288 linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px); 289 background-size: 44px 44px; 290 -webkit-mask-image: radial-gradient(82% 68% at 50% 34%, #000 0%, transparent 80%); 291 mask-image: radial-gradient(82% 68% at 50% 34%, #000 0%, transparent 80%); 292} 293.hero-inner { 294 position: relative; z-index: 1; 295 width: 100%; max-width: 1280px; margin: 0 auto; 296 padding: 90px 28px 110px; 297} 298.hero-eyebrow { 299 display: inline-flex; align-items: center; gap: 10px; 300 font-family: var(--f-mono); font-size: 12px; font-weight: 500; 301 letter-spacing: .14em; text-transform: uppercase; color: var(--text-dim); 302 background: var(--bg-card); border: 1px solid var(--border-secondary); 303 border-radius: var(--r-3); padding: 7px 13px; margin-bottom: 30px; 304} 305.hero-eyebrow .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--state-ok-tx); box-shadow: 0 0 0 3px color-mix(in srgb, var(--state-ok-tx) 16%, transparent); } 306.hero-title { 307 font-family: var(--f-sans); font-weight: 700; 308 font-size: clamp(40px, 7vw, 84px); 309 line-height: 1.05; letter-spacing: -.02em; color: var(--text-primary); 310 margin-bottom: 22px; text-transform: none; 311} 312.hero-title .l2 { display: block; } 313.hero-title .marker, .hero-title .marker-pink { 314 color: var(--accent-color); 315 background: var(--bg-secondary); 316 border-radius: var(--r-2); 317 padding: 0 .08em; 318 box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--accent-color) 22%, transparent); 319} 320.hero-sub { 321 display: flex; align-items: center; gap: 12px; 322 font-family: var(--f-mono); font-size: clamp(14px, 2vw, 17px); font-weight: 500; 323 color: var(--text-dim); letter-spacing: .06em; margin-bottom: 26px; 324} 325.hero-sub::before { content: ''; width: 38px; height: 1px; background: var(--border-hover); } 326.hero-desc { 327 font-size: 16px; line-height: 1.8; color: var(--text-secondary); 328 max-width: 560px; font-weight: 400; margin-bottom: 38px; 329} 330.hero-desc em { 331 font-style: normal; font-weight: 500; color: var(--text-primary); 332 background: linear-gradient(transparent 62%, color-mix(in srgb, var(--accent-color) 16%, transparent) 0); 333 padding: 0 .08em; border-radius: 2px; 334} 335.hero-cta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } 336 337/* ââ SECTION HEAD ââââââââââââââââââââââââââââââââââââââ */ 338.section { border-bottom: 1px solid var(--border-primary); padding: 84px 28px; position: relative; } 339.section-inner { max-width: 1280px; margin: 0 auto; } 340.section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 40px; } 341.section-title { font-family: var(--f-sans); font-weight: 700; font-size: clamp(26px, 4vw, 40px); letter-spacing: -.02em; color: var(--text-primary); line-height: 1.1; } 342.section-title .mk { color: var(--accent-color); border-bottom: 2px solid var(--border-hover); padding-bottom: 2px; } 343.section-tag { font-family: var(--f-mono); font-size: 12px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim); border: 1px solid var(--border-primary); background: var(--bg-card); padding: 6px 12px; border-radius: var(--r-3); } 344 345/* ââ STATS (å¡çå¼) ââââââââââââââââââââââââââââââââââââ */ 346.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } 347.stat-cell { 348 background: var(--bg-card); 349 border: 1px solid var(--border-primary); 350 border-radius: var(--r-4); 351 padding: 30px 24px; position: relative; overflow: hidden; 352 transition: transform .18s var(--ease), border-color .18s, box-shadow .18s; 353} 354.stat-cell::after {
355 content: ''; position: absolute; top: 0; left: -120%; width: 55%; height: 100%; 356 background: linear-gradient(105deg, transparent, rgba(255,255,255,.22), transparent); 357 transform: skewX(-18deg); transition: left .6s var(--ease); opacity: 0; 358} 359.stat-cell:hover::after { left: 150%; opacity: 1; } 360.stat-cell:hover { transform: translateY(-2px); border-color: var(--border-hover); box-shadow: 0 10px 24px -16px rgba(0,0,0,.22); } 361.stat-num { font-family: var(--f-mono); font-weight: 700; font-size: clamp(30px, 5vw, 52px); line-height: 1; color: var(--text-primary); margin-bottom: 12px; display: block; letter-spacing: -.02em; } 362.stat-label { font-family: var(--f-mono); font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); font-weight: 500; } 363 364/* ââ FEATURES (å¡çå¼) âââââââââââââââââââââââââââââââââ */ 365.feat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } 366.feat-card { 367 background: var(--bg-card); 368 border: 1px solid var(--border-primary); 369 border-radius: var(--r-4); 370 padding: 24px; position: relative; overflow: hidden; 371 transition: transform .18s var(--ease), border-color .18s, box-shadow .18s; 372} 373.feat-card::after { 374 content: ''; position: absolute; top: 0; left: -120%; width: 55%; height: 100%; 375 background: linear-gradient(105deg, transparent, rgba(255,255,255,.20), transparent); 376 transform: skewX(-18deg); transition: left .6s var(--ease); opacity: 0; 377} 378.feat-card:hover::after { left: 150%; opacity: 1; } 379.feat-card:hover { transform: translateY(-2px); border-color: var(--border-hover); box-shadow: 0 12px 26px -16px rgba(0,0,0,.24); } 380.feat-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } 381.feat-tag { font-family: var(--f-mono); font-size: 11px; font-weight: 500; letter-spacing: .04em; padding: 3px 10px; border-radius: 99px; border: 1px solid var(--border-secondary); color: var(--text-dim); transition: border-color .15s, color .15s; } 382.feat-card:hover .feat-tag { border-color: var(--accent-color); color: var(--accent-color); } 383.feat-ico { width: 40px; height: 40px; color: var(--accent-color); background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--r-3); display: grid; place-items: center; transition: background .15s, transform .15s; } 384.feat-card:hover .feat-ico { background: var(--bg-hover); } 385.feat-title { font-family: var(--f-sans); font-size: 16px; font-weight: 700; color: var(--text-primary); letter-spacing: -.01em; line-height: 1.25; margin-bottom: 8px; } 386.feat-desc { font-size: 13.5px; line-height: 1.75; color: var(--text-secondary); font-weight: 400; } 387 388/* ââ CTA âââââââââââââââââââââââââââââââââââââââââââââââ */ 389.cta-section { padding: 0; border-bottom: 1px solid var(--border-primary); } 390.cta-card { background: var(--bg-card); border-top: 1px solid var(--border-primary); padding: 90px 28px 100px; position: relative; overflow: hidden; } 391.cta-card::before { 392 content: ''; position: absolute; top: -90px; right: -90px; 393 width: 260px; height: 260px; border-radius: 50%; 394 border: 1px solid var(--border-primary); 395 background: radial-gradient(circle at center, var(--bg-secondary), transparent 70%); 396 z-index: 0; 397} 398.cta-inner { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; } 399.cta-label { display: inline-flex; align-items: center; gap: 10px; font-family: var(--f-mono); font-size: 12px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim); border: 1px solid var(--border-secondary); background: var(--bg-primary); border-radius: var(--r-3); padding: 7px 12px; margin-bottom: 22px; } 400.cta-title { font-family: var(--f-sans); font-weight: 700; font-size: clamp(34px, 7vw, 64px); line-height: 1.04; letter-spacing: -.02em; color: var(--text-primary); margin-bottom: 22px; max-width: 860px; } 401.cta-title .mk { color: var(--accent-color); border-bottom: 3px solid var(--border-hover); padding-bottom: 2px; } 402.cta-sub { font-size: 16px; color: var(--text-secondary); font-weight: 400; line-height: 1.75; max-width: 480px; margin-bottom: 34px; } 403.cta-actions { display: flex; gap: 12px; flex-wrap: wrap; } 404 405/* ââ FOOTER ââââââââââââââââââââââââââââââââââââââââââââ */ 406footer { background: var(--bg-card); color: var(--text-primary); padding: 26px 28px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; max-width: 1280px; margin: 0 auto; border-bottom: 1px solid var(--border-primary); } 407.footer-brand { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 10px; letter-spacing: .01em; } 408.footer-brand .sq { width: 12px; height: 12px; border-radius: 3px; background: var(--accent-color); border: 1px solid var(--border-hover); opacity: .85; } 409.footer-copy { font-family: var(--f-mono); font-size: 11.5px; color: var(--text-dim); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } 410.footer-copy a { color: var(--text-dim); transition: color .12s; display: inline-flex; align-items: center; gap: 4px; } 411.footer-copy a:hover { color: var(--accent-color); } 412.footer-copy img { opacity: .7; border-radius: 2px; } 413 414/* ââ TOAST âââââââââââââââââââââââââââââââââââââââââââââ */ 415#toast { position: fixed; bottom: 24px; right: 24px; z-index: 999; display: flex; flex-direction: column; gap: 10px; } 416.toast-item { background: var(--bg-card); color: var(--text-primary); padding: 12px 18px; font-size: 14px; font-family: var(--f-sans); font-weight: 500; border: 1px solid var(--border-secondary); border-radius: var(--r-4); max-width: 320px; box-shadow: 0 12px 28px -14px rgba(0,0,0,.25); animation: toastIn .28s var(--ease) forwards; } 417.toast-item::before { content: '> '; color: var(--accent-color); font-family: var(--f-mono); } 418.toast-item.out { animation: toastOut .22s var(--ease) forwards; } 419@keyframes toastIn { from { opacity: 0;
419 transform: translate(20px, 0); } to { opacity: 1; transform: none; } } 420@keyframes toastOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translate(20px, 0); } } 421 422/* ââ ENTER ANIMATIONS ââââââââââââââââââââââââââââââââââ */ 423.reveal { opacity: 0; transform: translateY(18px); transition: opacity .6s var(--ease), transform .6s var(--ease); } 424.reveal.visible { opacity: 1; transform: none; } 425 426/* ââ RESPONSIVE ââââââââââââââââââââââââââââââââââââââââ */ 427@media (max-width: 960px) { 428 .feat-grid { grid-template-columns: repeat(2, 1fr); } 429 .stats { grid-template-columns: repeat(2, 1fr); } 430} 431@media (max-width: 640px) { 432 #topbar { padding: 0 16px; } 433 .logo-sub { display: none; } 434 .feat-grid { grid-template-columns: 1fr; } 435 .stats { grid-template-columns: 1fr; } 436 .section { padding: 56px 18px; } 437 .hero-inner { padding: 70px 20px 90px; } 438 .cta-card { padding: 64px 18px 76px; } 439 footer { flex-direction: column; align-items: flex-start; gap: 12px; padding: 22px 18px; } 440 .hero-desc { font-size: 15px; } 441 .ann-bubble { width: 288px; left: -8px; } 442 .ann-bubble::before { left: 8px; } 443} 444</style> 445</head> 446<body> 447 448<!-- ââ TOPBAR ââââââââââââââââââââââââââââââââââââââââââââ --> 449<header id="topbar"> 450 <div class="logo-wrap"> 451 <a href="/" class="logo-mark" onclick="event.preventDefault(); toggleAnnBubble()"> 452 <img class="logo-block" src="https://fohok.xin/img/fohok-2.png" alt="æè°¶é©¿ç«"> 453 <span class="logo-text"> 454 <span class="logo-char">æè°¶é©¿ç«</span> 455 <span class="logo-sub">Open-Fohok</span> 456 </span> 457 </a> 458 <span class="logo-badge" id="logo-badge" aria-hidden="true"></span> 459 460 <!-- ââ ANNOUNCEMENT BUBBLE ââââââââââââââââââââââââ --> 461 <div id="ann-bubble" class="ann-bubble"> 462 <div class="ann-head"> 463 <span>å ¬å / Announcement</span> 464 <span style="display:flex;align-items:center;gap:8px;"> 465 <button class="ann-close-x" onclick="dismissAnnBubble(event)" aria-label="å ³é">×</button> 466 </span> 467 </div> 468 <div class="ann-title" id="ann-title">Please be patient...</div> 469 <div class="ann-content" id="ann-content"></div> 470 </div> 471 </div> 472 473 <div style="flex:1"></div> 474 475 <button class="theme-toggle" id="themeToggle" title="忢䏻é¢" aria-label="忢æ¥é´/å¤é´ä¸»é¢"> 476 <svg class="ico-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> 477 <svg class="ico-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></svg> 478 </button> 479 480 <a href="/download.php" class="topbar-app-icon" title="ä¸è½½ App"> 481 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"> 482 <rect x="5" y="2" width="14" height="20" rx="1"/> 483 <line x1="12" y1="17" x2="12.01" y2="17" stroke-linecap="round"/> 484 </svg> 485 <span class="app-icon-dot"></span> 486 </a> 487 488 <a href="/login.php" class="btn btn-primary btn-sm"> 489 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg> 490 ç»å½ 491 </a> 492 </header> 493 494<!-- ââ HERO ââââââââââââââââââââââââââââââââââââââââââââââ --> 495<section class="hero"> 496 <div class="hero-inner"> 497 498 <span class="hero-eyebrow reveal"> 499 <span class="dot"></span> 500 Open Source Platform · Est. 2025 501 </span> 502
503 <h1 class="hero-title reveal" style="transition-delay:.06s"> 504 <span class="l2"><span class="marker">æè°¶</span>é©¿ç«</span> 505 <span class="l2">å¼åè <span class="marker-pink">弿¾ä»åº</span></span> 506 </h1> 507 508 <p class="hero-sub reveal" style="transition-delay:.12s"> 509 Developer Open Repository 510 </p> 511 512 <p class="hero-desc reveal" style="transition-delay:.18s"> 513 ä¸ä¸ªä¸º<em>ç¬ç«å¼åè </em>æé ç弿ºé¡¹ç®å±ç¤ºç©ºé´ãåå¸ä½ çä½åï¼æ¢ç´¢ä»äººçåé ï¼<em>è¿æ¥</em>å¿åéåç人ã 514 </p> 515 516 <div class="hero-cta reveal" style="transition-delay:.24s"> 517 <a href="/home.php" class="btn btn-primary"> 518 è¿å ¥å¹¿åº 519 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18"/></svg> 520 </a> 521 <a href="/login.php" class="btn btn-ghost"> 522 ç»å½ / 注å 523 </a> 524 </div> 525 </div> 526</section> 527 528<!-- ââ STATS ââââââââââââââââââââââââââââââââââââââââââââââ --> 529<section class="section"> 530 <div class="section-inner"> 531 <div class="section-head reveal"> 532 <h2 class="section-title">ç«å <span class="mk">ç»è®¡</span></h2> 533 <span class="section-tag">statistics</span> 534 </div> 535
536 <div class="stats reveal" style="transition-delay:.08s"> 537 <div class="stat-cell"> 538 <span class="stat-num" id="stat-projects">â</span> 539 <span class="stat-label">Projects / 项ç®</span> 540 </div> 541 <div class="stat-cell"> 542 <span class="stat-num" id="stat-developers">â</span> 543 <span class="stat-label">Developers / å¼åè </span> 544 </div> 545 <div class="stat-cell"> 546 <span class="stat-num" id="stat-approved">â</span> 547 <span class="stat-label">Approved / å·²æ¶å½</span> 548 </div> 549 <div class="stat-cell"> 550 <span class="stat-num">â</span> 551 <span class="stat-label">Possibilities / å¯è½</span> 552 </div> 553 </div> 554 </div> 555</section> 556 557<!-- ââ FEATURES ââââââââââââââââââââââââââââââââââââââââââ --> 558<section class="section"> 559 <div class="section-inner"> 560 <div class="section-head reveal"> 561 <h2 class="section-title">æ¬ç«<span class="mk">ç¹æ§</span></h2> 562 <span class="section-tag">Index · ç®å½</span> 563 </div> 564
565 <div class="feat-grid reveal" style="transition-delay:.08s"> 566 <div class="feat-card"> 567 <div class="feat-head"> 568 <span class="feat-tag">No. 01</span> 569 <div class="feat-ico"> 570 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="14"/><line x1="8" y1="9" x2="16" y2="9"/><line x1="8" y1="13" x2="13" y2="13"/></svg> 571 </div> 572 </div> 573 <h3 class="feat-title">项ç®å±ç¤º</h3> 574 <p class="feat-desc">ä¼é å°åç°ä½ ç弿ºé¡¹ç®ï¼çæ¬ãç®ä»ã龿¥ãèç³»æ¹å¼ï¼ä¸ç®äºç¶ã</p> 575 </div> 576 577 <div class="feat-card"> 578 <div class="feat-head"> 579 <span class="feat-tag">No. 02</span> 580 <div class="feat-ico"> 581 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5L12 15.8l-4.9 2.6.9-5.5-4-3.9 5.5-.8z"/></svg> 582 </div> 583 </div> 584 <h3 class="feat-title">äººå·¥å®¡æ ¸</h3> 585 <p class="feat-desc">ææé¡¹ç®ç»è¿ä¸¥æ ¼å®¡æ ¸ï¼ä¿è¯å 容质éï¼æç»åå¾ä¿¡æ¯å¹²æ°ã</p> 586 </div> 587 588 <div class="feat-card"> 589 <div class="feat-head"> 590 <span class="feat-tag">No. 03</span> 591 <div class="feat-ico"> 592 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="10"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg> 593 </div> 594 </div> 595 <h3 class="feat-title">Accountå®å ¨</h3> 596 <p class="feat-desc">æ¯æX-GORB Oauth2.0ç»å½/æ«ç ç»å½/éè¡å¯é¥/龿¥éªè¯ç»å½ï¼ä¾¿æ·æ å¿§</p> 597 </div> 598 599 <div class="feat-card"> 600 <div class="feat-head"> 601 <span class="feat-tag">No. 04</span> 602 <div class="feat-ico"> 603 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 14 8 9 13 15 17 10 21 14"/><line x1="3" y1="19" x2="21" y2="19"/></svg> 604 </div> 605 </div> 606 <h3 class="feat-title">宿¶æ´æ°</h3> 607 <p class="feat-desc">éæ¶æäº¤åä¿®æ¹é¡¹ç®ä¿¡æ¯ï¼ä¿æä½åçæ¬ä¸æè¿°å§ç»å¤äºææ°ç¶æã</p> 608 </div> 609 610 <div class="feat-card"> 611 <div class="feat-head"> 612 <span class="feat-tag">No. 05</span> 613 <div class="feat-ico"> 614 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="12" height="16"/><rect x="15" y="11" width="6" height="9"/></svg> 615 </div> 616 </div> 617 <h3 class="feat-title">å¼åè å好</h3> 618 <p class="feat-desc">ç®æ´æ å¹²æ°ççé¢è®¾è®¡ï¼è®©ä½ 䏿³¨äºä½åæ¬èº«ï¼èéç¹ççæä½æµç¨ã</p> 619 </div> 620 621 <div class="feat-card"> 622 <div class="feat-head"> 623 <span class="feat-tag">No. 06</span> 624 <div class="feat-ico"> 625 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="12" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="18" cy="18" r="3"/><line x1="8.5" y1="10.5" x2="15.5" y2="7.5"/><line x1="8.5" y1="13.5" x2="15.5" y2="16.5"/></svg> 626 </div> 627 </div> 628 <h3 class="feat-title">è¿æ¥åä½è </h3> 629 <p class="feat-desc">éè¿é¡¹ç®å±ç¤ºé¡µé¢ï¼ç»è¯å¿åéåçç¬ç«å¼åè ï¼å ±å»ºå¼æºç¤¾åºã</p> 630 </div> 631 </div> 632 </div> 633</section> 634 635<!-- ââ CTA ââââââââââââââââââââââââââââââââââââââââââââââââ --> 636<section class="cta-section"> 637 <div class="cta-card"> 638 <div class="cta-inner"> 639 <div class="reveal"> 640 <span class="cta-label">å å ¥æä»¬ / Join</span> 641 <h2 class="cta-title"> 642 让æ´å¤äºº<br><span class="mk">çè§</span>ä½ çä½å 643 </h2> 644 <p class="cta-sub"> 645 æ¯ä¸ä¸ªå¼æºé¡¹ç®é½å¼å¾è¢«åç°ãå¨æè°¶é©¿ç«åå¸ä½ çä½åï¼ä»å¤©å°±å¼å§ã 646 </p> 647 <div class="cta-actions"> 648 <a href="/login.php" class="btn btn-primary"> 649 ç«å³ç»å½
650 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18"/></svg> 651 </a> 652 <a href="/home.php" class="btn btn-ghost">æµè§å¹¿åº</a> 653 </div> 654 </div> 655 </div> 656 </div> 657</section> 658 659<!-- ââ FOOTER ââââââââââââââââââââââââââââââââââââââââââââ --> 660<footer> 661 <span class="footer-brand"><span class="sq"></span>æè°¶é©¿ç«</span> 662 <p class="footer-copy"> 663 © 2026 æè°¶é©¿ç« · å¼åè 弿¾ä»åº â A <a href="https://www.xgorb.cn">X-GORB Studio</a> Brand 664 <span>|</span> 665 <a href="https://beian.miit.gov.cn" target="_blank"> 666 <img src="https://fohok.xin/img/icp.png" alt="ICP夿¡å¾æ " style="width:18px;height:auto;"> 667 é½ICPå¤2026024151å· 668 </a> 669 <span>|</span> 670 <a href="https://beian.mps.gov.cn/#/query/webSearch?code=35058302351237" target="_blank"> 671 <img src="https://fohok.xin/img/mps.png" alt="å ¬å®å¤æ¡å¾æ " style="width:18px;height:auto;"> 672 é½å ¬ç½å®å¤35058302351237å· 673 </a> 674 </p> 675</footer> 676 677<!-- ââ TOAST ââââââââââââââââââââââââââââââââââââââââââââââ --> 678<div id="toast"></div> 679
680<script> 681/* Theme toggle */ 682(function(){ 683 var btn = document.getElementById('themeToggle'); 684 if (btn) btn.addEventListener('click', function(){ 685 var doc = document.documentElement; 686 var next = doc.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; 687 doc.setAttribute('data-theme', next); 688 localStorage.setItem('fohok_theme', next); 689 }); 690})(); 691 692/* Dropdown */ 693function toggleDropdown(id) { 694 document.getElementById(id).classList.toggle('show'); 695} 696document.addEventListener('click', e => { 697 if (!e.target.closest('.dropdown')) 698 document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('show')); 699}); 700 701/* Reveal on scroll */ 702const revealEls = document.querySelectorAll('.reveal'); 703const revealIO = new IntersectionObserver((entries) => { 704 entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); revealIO.unobserve(e.target); }}); 705}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' }); 706revealEls.forEach(el => revealIO.observe(el)); 707 708/* Trigger hero reveals immediately */ 709window.addEventListener('DOMContentLoaded', () => { 710 document.querySelectorAll('.hero .reveal').forEach((el, i) => { 711 setTimeout(() => el.classList.add('visible'), i * 90); 712 }); 713 loadStats(); 714 preloadAnnouncement(); 715}); 716 717/* Stats counter */ 718function animateCount(el, target, duration = 1100) { 719 const start = performance.now(); 720 const step = (now) => { 721 const p = Math.min((now - start) / duration, 1); 722 const ease = 1 - Math.pow(1 - p, 3); 723 el.textContent = Math.round(ease * target); 724 if (p < 1) requestAnimationFrame(step); 725 else el.textContent = target; 726 }; 727 requestAnimationFrame(step); 728} 729 730async function loadStats() { 731 try { 732 const res = await fetch('/api/project.php?action=stats'); 733 const data = await res.json(); 734 if (data.ok && data.stats) { 735 animateCount(document.getElementById('stat-projects'), data.stats.total_projects || 0); 736 animateCount(document.getElementById('stat-developers'), data.stats.total_developers || 0); 737 animateCount(document.getElementById('stat-approved'), data.stats.approved_projects || 0); 738 } 739 } catch(e) { console.warn('stats:', e); } 740} 741 742/* ââ ANNOUNCEMENT BUBBLE ââââââââââââââââââââââââââââââââ */ 743const ANN_SEVERITY = { 744 notice: { label: 'éç¥', cls: 'ann-notice' }, 745 info: { label: 'æç¤º', cls: 'ann-info' }, 746 important: { label: 'éè¦', cls: 'ann-important' }, 747 warning: { label: 'è¦å', cls: 'ann-warning' } 748}; 749let annData = null; /* cached announcement data */ 750let annLoaded = false; /* whether fetch completed (success or fail) */ 751let annLoading = false; /* whether fetch is currently in-flight */ 752 753async function preloadAnnouncement() { 754 /* Guard: don't fetch again if already loaded or currently loading */ 755 if (annLoaded || annLoading) return; 756 annLoading = true; 757 758 try { 759 const res = await fetch('https://fohok.xin/api/announcement.php?id=2'); 760 const data = await res.json(); 761 annLoaded = true; 762 annLoading = false; 763 764 if (!data.ok || !data.title) return; 765 annData = data; 766 767 /* Show badge only if NOT dismissed this session */ 768 if (!sessionStorage.getItem('ann_dismissed_1')) { 769 document.getElementById('logo-badge').classList.add('show'); 770 } 771 772 /* Auto-show bubble 1s after page load, if not dismissed */ 773 setTimeout(() => { 774 if (!sessionStorage.getItem('ann_dismissed_1') && !openBubble && annData) { 775 toggleAnnBubble(); 776 } 777 }, 1000); 778 779 /* If the bubble is already open (user clicked before load finished), fill it now */ 780 const bubble = document.getElementById('ann-bubble'); 781 if (bubble.classList.contains('show')) renderAnnBubble(); 782 } catch(e) { 783 annLoaded = true; 784 annLoading = false; 785 console.warn('announcement:', e); 786 } 787} 788 789function renderAnnBubble() { 790 if (!annData) return; 791 const sev = ANN_SEVERITY[annData.severity] || ANN_SEVERITY.info; 792 793 const bubble = document.getElementById('ann-bubble'); 794 const sevTag = document.getElementById('ann-sev-tag'); /* may be null if removed from HTML */ 795 const titleEl = document.getElementById('ann-title'); 796 const contentEl = document.getElementById('ann-content'); 797 798 /* Apply severity class (color), preserve show state */ 799 bubble.className = 'ann-bubble ' + sev.cls; 800 if (openBubble) bubble.classList.add('show'); 801 802 /* sev-tag element was removed from HTML â guard against null */ 803 if (sevTag) sevTag.textContent = sev.label; 804 805 titleEl.textContent = annData.title; 806 contentEl.innerHTML = annData.content || ''; 807} 808 809let openBubble = false;
810function toggleAnnBubble() { 811 const bubble = document.getElementById('ann-bubble'); 812 openBubble = !bubble.classList.contains('show'); 813 814 if (openBubble) { 815 /* If data not yet loaded, show placeholder and trigger fetch */ 816 if (!annLoaded && !annData) { 817 document.getElementById('ann-title').textContent = 'Please be patient...'; 818 document.getElementById('ann-content').textContent = ''; 819 preloadAnnouncement(); /* safe: guarded by annLoading flag */ 820 } 821 /* If data is ready, render it */ 822 if (annData) renderAnnBubble(); 823 bubble.classList.add('show'); 824 /* Hide the badge once opened */ 825 document.getElementById('logo-badge').classList.remove('show'); 826 } else { 827 bubble.classList.remove('show'); 828 } 829} 830 831function dismissAnnBubble(e) { 832 if (e) e.stopPropagation(); 833 const bubble = document.getElementById('ann-bubble'); 834 bubble.classList.remove('show'); 835 openBubble = false; 836 /* Remember dismissal so badge doesn't reappear on reload, 837 but data stays cached â user can still click logo to view */ 838 sessionStorage.setItem('ann_dismissed_1', '1'); 839 document.getElementById('logo-badge').classList.remove('show'); 840} 841 842/* Click outside bubble to close */ 843document.addEventListener('click', e => { 844 if (!e.target.closest('.logo-wrap')) { 845 const bubble = document.getElementById('ann-bubble'); 846 if (bubble.classList.contains('show')) { 847 bubble.classList.remove('show'); 848 openBubble = false; 849 } 850 } 851}); 852 853/* Toast */ 854function showToast(msg) { 855 const wrap = document.getElementById('toast'); 856 const el = document.createElement('div'); 857 el.className = 'toast-item'; 858 el.textContent = msg; 859 wrap.appendChild(el); 860 setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 240); }, 3500); 861} 862 863/* Read ?error= from URL (PHP fallback for non-PHP environments) */ 864(function() { 865 const p = new URLSearchParams(location.search); 866 const err = p.get('error'); 867 if (err) showToast(err); 868})(); 869</script>
869 870</body> 871</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.