PageSourceSearch

https://fohok.xin/

html fohok.xin collected 2026-09-28 07:03:42 UTC 43,612 bytes, 871 lines download raw bytes

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="关闭">&times;</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.