Files
blog/design/mockup.html
T
wait4xandGLM-5.3 0fbb023375 docs: 落库博客重设计调研报告 v1.8 与视觉样板 v2
调研收官产物入库:主报告(架构 C''=Astro SSG+gitea 双写作流、
Artalk 评论、分片 webfont、双语预译管线、多端适配规格、
Phase 0-5 实施规划)+ 五份侦察原稿 + 可交互单文件样板
(三路线×明暗×五页型,命令面板/终端/热力图/masonry)。

Co-authored-by: GLM-5.3
2026-10-24 21:48:11 +08:00

1051 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN" data-route="c" data-page="home" data-scheme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>wait4x · 视觉样板</title>
<script>if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.dataset.scheme='dark';</script>
<style>
/* ============ 基底与常量 ============ */
*{box-sizing:border-box;margin:0}
:root{--tbh:54px;--h0:#ebedf0;--h1:#9be9a8;--h2:#40c463;--h3:#30a14e;--h4:#216e39}
html{
color-scheme:light;
--serif:Georgia,"Iowan Old Style","Songti SC",STSong,"Noto Serif CJK SC","Source Han Serif SC",SimSun,serif;
--sans:system-ui,-apple-system,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;
--mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,"Liberation Mono",monospace;
--ease:cubic-bezier(.19,1,.22,1);
--dur:.35s;
--stage:#f4f4f2;
scroll-padding-top:calc(var(--tbh) + 20px);
}
html[data-scheme="dark"]{color-scheme:dark;--stage:#0b0d10;--h0:#22272e;--h1:#0e4429;--h2:#006d32;--h3:#26a641;--h4:#39d353}
::selection{background:color-mix(in srgb,var(--accent) 26%,transparent)}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
body{
background:var(--bg);color:var(--text);
font-family:var(--font-ui);font-size:16px;line-height:1.7;
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
padding-top:var(--tbh);
}
a{color:inherit;text-decoration:none}
button{font:inherit}
img,svg{display:block}
/* ============ 路线甲 · 纸墨书卷 ============ */
html[data-route="a"]{
--font-ui:var(--serif);--font-essay:var(--serif);--font-tech:var(--serif);
--bg:#f6f5f3;--surface:#fff;--text:#2c2620;--title:#1d1912;
--muted:#7d7466;--faint:#a89e8e;
--border:#e6e1d6;--border-strong:#d4ccbc;
--accent:#009900;
--measure:648px;
--sheet:#fff;--sheet-line:var(--border);--sheet-pad:36px;--sheet-radius:8px;--sheet-ink:#2c2620;
--inline-bg:#efede7;--quote-line:#c9c0ad;
}
html[data-route="a"][data-scheme="dark"]{
--bg:#1f1c17;--surface:#262119;--text:#d8d1c0;--title:#e8e2d5;
--muted:#9c927e;--faint:#6f6656;
--border:#332d22;--border-strong:#453d2d;
--accent:#7fb069;
--sheet:#262119;--sheet-line:#3a3325;--sheet-ink:#d8d1c0;
--inline-bg:#2d2820;--quote-line:#5a5140;
}
/* ============ 路线乙 · 极简灰阶 ============ */
html[data-route="b"]{
--font-ui:var(--sans);--font-essay:var(--sans);--font-tech:var(--sans);
--bg:#fff;--surface:#fff;--text:#333;--title:#111;
--muted:#666;--faint:#999;
--border:#e8e8e8;--border-strong:#d4d4d4;
--accent:#3454d1;
--measure:680px;
--sheet:transparent;--sheet-line:transparent;--sheet-pad:0;--sheet-radius:0;--sheet-ink:inherit;
--inline-bg:#f2f2f2;--quote-line:#ddd;
}
html[data-route="b"][data-scheme="dark"]{
--bg:#282c35;--surface:#282c35;--text:#c9ccd4;--title:#f5f5f7;
--muted:#8b8f98;--faint:#5c6066;
--border:#363b46;--border-strong:#454b58;
--accent:#8ea2ff;
--inline-bg:#31353f;--quote-line:#4a505c;
}
/* ============ 路线丙 · 灰阶骨架 × 分轨正文 ============ */
html[data-route="c"]{
--font-ui:var(--sans);--font-essay:var(--serif);--font-tech:var(--sans);
--bg:#fff;--surface:#fff;--text:#333;--title:#111;
--muted:#666;--faint:#999;
--border:#e8e8e8;--border-strong:#d4d4d4;
--accent:#3454d1;
--measure:704px;
--sheet:#f6f5f3;--sheet-line:#e6e1d6;--sheet-pad:44px;--sheet-radius:14px;--sheet-ink:#2c2620;
--inline-bg:#f1f2f4;--quote-line:#d0d0d0;
}
html[data-route="c"][data-scheme="dark"]{
--bg:#191d23;--surface:#191d23;--text:#c6cbd4;--title:#eef1f5;
--muted:#8b8f98;--faint:#565b66;
--border:#2b303a;--border-strong:#3d434f;
--accent:#8ea2ff;
--sheet:#201d18;--sheet-line:#332e24;--sheet-ink:#d8d1c0;
--inline-bg:#2a2e36;--quote-line:#4a4438;
}
/* ============ 预览工具条(非博客 UI) ============ */
.tb{
position:fixed;top:0;left:0;right:0;z-index:100;
display:flex;align-items:center;gap:10px;flex-wrap:nowrap;overflow-x:auto;
padding:9px 16px;height:var(--tbh);
background:#1a1d23f2;backdrop-filter:blur(8px);
border-bottom:1px solid #ffffff14;color:#a7aeb9;
scrollbar-width:none;
}
.tb::-webkit-scrollbar{display:none}
.tb-brand{flex:none;font-size:12px;color:#8a93a3;font-family:var(--sans);white-space:nowrap}
.tb-brand b{color:#e6e9ee;font-weight:600}
.tb-sep{flex:none;width:1px;height:16px;background:#ffffff22}
.tb-group{display:flex;gap:2px;flex:none}
.tb-seg{position:relative}
.tb-seg input{position:absolute;opacity:0;pointer-events:none}
.tb-seg span{
display:inline-block;padding:4px 10px;border-radius:7px;
font-size:12px;font-family:var(--sans);white-space:nowrap;cursor:pointer;
color:#a7aeb9;user-select:none;
}
.tb-seg span:hover{color:#e6e9ee}
.tb-seg input:checked + span{background:#2c313b;color:#fff}
.tb-seg input:focus-visible + span{outline:1px solid #9db4ff}
.tb-seg .badge{
font-style:normal;font-size:10px;color:#f5c97b;border:1px solid #f5c97b55;
border-radius:5px;padding:0 5px;margin-left:6px;vertical-align:1px;
}
.tb-btn{
flex:none;display:inline-flex;align-items:center;justify-content:center;
width:28px;height:28px;border-radius:7px;border:none;cursor:pointer;
background:transparent;color:#a7aeb9;
}
.tb-btn:hover{background:#2c313b;color:#fff}
.tb-right{flex:none;display:flex;gap:4px;margin-left:auto}
.tb-pal{width:auto;gap:5px;padding:0 9px}
.tb-pal span{font-family:var(--mono);font-size:10.5px;letter-spacing:.04em}
.ic-sun{display:none}
html[data-scheme="dark"] .ic-sun{display:block}
html[data-scheme="dark"] .ic-moon{display:none}
/* ============ 阅读进度条(仅技术文章页) ============ */
#pbar{
position:fixed;top:0;left:0;height:2px;width:0;z-index:200;
background:var(--accent);display:none;
}
html[data-page="tech"] #pbar{display:block}
/* ============ 站点骨架 ============ */
.container{max-width:var(--measure);margin:0 auto;padding:0 20px}
.site-head{border-bottom:1px solid var(--border)}
.site-head-row{display:flex;align-items:center;justify-content:space-between;height:58px}
.site-name{font-size:17px;font-weight:700;color:var(--title)}
html[data-route="b"] .site-name{font-weight:700}
.nav{display:flex;align-items:center;gap:18px}
.nav-item{
display:inline-flex;align-items:center;gap:6px;
font-size:14px;color:var(--muted);cursor:pointer;background:none;border:none;padding:0;
}
.nav-item:hover{color:var(--title)}
html[data-route="b"] .nav-t{display:none}
html:not([data-route="b"]) .nav-item:not(.nav-icon) svg{display:none}
html:has(input#p-home:checked) .nav-item[for="p-home"],
html:has(input#p-gallery:checked) .nav-item[for="p-gallery"]{color:var(--title)}
.site-foot{margin-top:80px;border-top:1px solid var(--border)}
.site-foot .container{
padding-top:22px;padding-bottom:40px;
font-size:12.5px;color:var(--faint);
}
.site-foot a{color:var(--faint)}
.site-foot a:hover{color:var(--accent)}
/* 页脚构建透明:mono 微文案,hash 暗示链到 gitea commit */
.build{margin-top:8px;font-family:var(--mono);font-size:11px;letter-spacing:.02em}
.build a{text-decoration:underline;text-decoration-style:dashed;text-underline-offset:3px}
.build a:hover{color:var(--accent)}
.foot-term{
margin-left:10px;display:inline-flex;align-items:center;justify-content:center;
background:none;border:none;color:var(--faint);cursor:pointer;
padding:2px;border-radius:5px;vertical-align:-2px;
}
.foot-term:hover{color:var(--accent)}
/* ============ 页面切换 ============ */
.view{display:none;padding-bottom:24px}
html[data-page="home"] .view-home,
html[data-page="essay"] .view-essay,
html[data-page="tech"] .view-tech,
html[data-page="gallery"] .view-gallery,
html[data-page="moments"] .view-moments{display:block}
html[data-page="tech"] .view-tech .container{max-width:calc(var(--measure) + 200px)}
html[data-page="gallery"] .view-gallery .container{max-width:calc(var(--measure) + 176px)}
/* ============ 首页 ============ */
.hero{display:flex;align-items:center;gap:18px;margin:44px 0 40px}
.hero-avatar{flex:none;border-radius:14px;box-shadow:0 1px 2px #0000001a}
.hero-name{font-size:21px;font-weight:700;color:var(--title)}
.hero-bio{font-size:15px;color:var(--muted);margin:4px 0 12px}
.socials{display:flex;align-items:center;gap:14px}
.socials a{color:var(--muted)}
.socials a:hover{color:var(--accent)}
.year{font-size:15px;font-weight:600;color:var(--muted);margin:6px 0 4px}
.track{
display:flex;align-items:center;gap:8px;
margin:22px 0 4px;font-size:12.5px;color:var(--faint);
}
.track::before{content:"";width:7px;height:7px;border-radius:2px;background:var(--border-strong)}
.row{
display:flex;align-items:baseline;gap:12px;
padding:9px 0;cursor:pointer;
}
.row-title{flex:1;min-width:0;font-size:16px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row:hover .row-title{color:var(--accent)}
.row-date{flex:none;color:var(--faint);font-size:13px;font-variant-numeric:tabular-nums;white-space:nowrap}
.emo{
display:none;width:10px;height:10px;border-radius:3px;
background:var(--emo,#999);flex:none;align-self:center;
}
html[data-route="c"] .emo{display:inline-block}
.tag{
flex:none;font-family:var(--mono);font-size:12px;color:var(--muted);
background:var(--inline-bg);border-radius:5px;padding:1px 7px;white-space:nowrap;
}
/* 最新影像入口:仅乙 / 丙 */
.latest{display:none;margin-top:44px}
html[data-route="b"] .latest,
html[data-route="c"] .latest{display:block}
.latest-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.latest-head .lab{font-size:12.5px;color:var(--faint)}
.more{font-size:13px;color:var(--accent);cursor:pointer}
.more:hover{color:var(--title)}
.thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:12px}
.thumbs figure{aspect-ratio:1/1;border-radius:8px;overflow:hidden;cursor:pointer}
.thumbs svg{width:100%;height:100%}
/* ============ 首页 · 开源活动(贡献热力图 + 项目卡) ============ */
.oss{margin-top:44px}
#heat{
display:grid;grid-template-rows:repeat(7,10px);grid-auto-flow:column;grid-auto-columns:10px;gap:3px;
margin-top:12px;width:max-content;max-width:100%;overflow-x:auto;padding-bottom:2px;
}
#heat i,.heat-legend i{border-radius:2.5px}
.h0{background:var(--h0)}.h1{background:var(--h1)}.h2{background:var(--h2)}.h3{background:var(--h3)}.h4{background:var(--h4)}
.heat-meta{
display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;
margin-top:10px;font-size:12px;color:var(--faint);
}
.heat-legend{display:inline-flex;align-items:center;gap:3px;font-family:var(--mono)}
.heat-legend i{width:9px;height:9px;margin:0 1px}
.repos{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}
.repo{
display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;
border:1px solid var(--border);border-radius:10px;padding:12px 14px;
font-size:13px;color:var(--muted);cursor:pointer;
}
.repo code{font-family:var(--mono);font-size:13px;color:var(--title)}
.repo-meta{width:100%;display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:12px;margin-top:6px}
.repo-meta i{width:8px;height:8px;border-radius:50%;background:var(--lc,#999);flex:none}
.repo-meta b{margin-left:auto;font-weight:400;font-variant-numeric:tabular-nums}
/* ============ 正文排版(三路线共用参数) ============ */
.prose{max-width:min(720px,var(--measure));overflow-wrap:break-word}
.prose p{margin:0 0 .9em;text-indent:0}
.essay .prose{font-family:var(--font-essay);font-size:var(--prose-size,20px);line-height:1.75}
.tech .prose{font-family:var(--font-tech);font-size:19px;line-height:1.7}
html[data-route="a"] .prose{text-align:justify}
.post-title{
font-size:27px;line-height:1.3;letter-spacing:0;text-wrap:balance;
color:var(--title);font-weight:700;
}
.prose h2,.prose h3{letter-spacing:0;text-wrap:balance;color:var(--title);line-height:1.3}
.prose h2{font-size:22px;margin:1.8em 0 .6em;scroll-margin-top:calc(var(--tbh) + 24px)}
.prose h3{font-size:19px;margin:1.5em 0 .5em;scroll-margin-top:calc(var(--tbh) + 24px)}
html[data-route="b"] .post-title{font-weight:900}
html[data-route="b"] .prose h2,
html[data-route="b"] .prose h3{font-weight:700}
/* 引文 A:长书摘(1px 墨线左边框 + muted) */
.prose blockquote.pull{
margin:1.6em 0;padding:.1em 0 .1em 1.5em;
border-left:1px solid var(--quote-line);color:var(--muted);
font-size:inherit;line-height:inherit;
}
.prose blockquote.pull p{margin:0}
/* 引文 D:金句(62px 巨型左引号水印 + 缩进) */
.quote-hero{position:relative;margin:1.9em 0 .4em;padding-left:2.1em}
.quote-hero::before{
content:"“";position:absolute;left:0;top:-26px;
font-family:var(--serif);font-size:62px;line-height:1;
color:var(--text);opacity:.12;pointer-events:none;
}
.quote-hero p{margin:0;font-size:inherit;line-height:inherit;color:var(--title)}
.quote-src{font-size:.875rem;color:var(--faint);margin:-.3em 0 1.8em}
/* 内联代码 */
.prose code:not(pre code){
font-family:var(--mono);font-size:.875em;
background:var(--inline-bg);border-radius:4px;padding:.15em .4em;
}
/* 终端风代码块:黑底、圆点、$ 提示符、手写语法色 */
.term{margin:1.5em 0;border-radius:10px;overflow:hidden;background:#0d1117;border:1px solid #ffffff12}
.term-bar{
display:flex;align-items:center;gap:6px;
padding:9px 12px;border-bottom:1px solid #ffffff0d;
}
.term-bar .dot{width:10px;height:10px;border-radius:50%}
.term-bar .r{background:#ff5f56}.term-bar .y{background:#ffbd2e}.term-bar .g{background:#27c93f}
.term-title{margin-left:6px;font-family:var(--mono);font-size:11px;color:#8b949e}
.term pre{
margin:0;padding:14px 18px;overflow-x:auto;
font-family:var(--mono);font-size:16px;line-height:1.6;color:#c9d1d9;
white-space:pre;
}
.term .cm{color:#5c6370}
.term .str{color:#98c379}
.term .cmd{color:#61afef}
.term .kw{color:#c678dd}
.term .pr{color:#61afef;font-weight:700;user-select:none}
/* ============ 文章页 · 书评 ============ */
.post{margin-top:36px}
.sheet{
background:var(--sheet);color:var(--sheet-ink,var(--text));
border:1px solid var(--sheet-line);border-radius:var(--sheet-radius);
padding:var(--sheet-pad);
}
.post-cat{font-size:.75rem;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.post-head .post-title{margin-top:10px}
.post-meta{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:12px;font-size:14px;color:var(--muted)}
.post-head + .prose{margin-top:28px}
/* 上一篇 / 下一篇 */
.pn{display:flex;justify-content:space-between;gap:16px;margin-top:36px}
.pn-item{display:flex;flex-direction:column;gap:2px;min-width:0}
.pn-item.right{text-align:right;align-items:flex-end}
.pn-item span{font-size:12px;color:var(--faint)}
.pn-item strong{
font-size:15px;font-weight:600;color:var(--title);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;cursor:pointer;
}
.pn-item:hover strong{color:var(--accent)}
/* 评论区占位 */
.comments{
margin-top:28px;border:1px dashed var(--border-strong);border-radius:10px;
padding:30px 20px;text-align:center;color:var(--faint);font-size:13px;
}
.comments b{display:block;font-weight:400;color:var(--muted);margin-bottom:4px}
/* 字号 dock:仅书评页出现 */
.fsdock{
position:fixed;right:18px;top:calc(var(--tbh) + 14px);z-index:60;
display:none;flex-direction:column;gap:6px;
}
html[data-page="essay"] .fsdock{display:flex}
.fsdock button{
width:38px;height:30px;border:1px solid var(--border);border-radius:8px;
background:var(--surface);color:var(--muted);font-family:var(--mono);
font-size:11px;cursor:pointer;
}
.fsdock button:hover{color:var(--title);border-color:var(--border-strong)}
/* ============ 文章页 · 技术 ============ */
.tech-grid{display:grid;grid-template-columns:minmax(0,1fr) 172px;gap:56px;align-items:start;margin-top:36px}
.toc{
position:sticky;top:calc(var(--tbh) + 24px);
font-size:13px;padding-left:16px;border-left:1px solid var(--border);
}
.toc-h{font-weight:600;color:var(--title);font-size:13px;margin-bottom:6px}
.toc a{display:block;padding:4px 0;color:var(--muted);cursor:pointer}
.toc a:hover{color:var(--accent)}
.toc a.on{color:var(--accent);font-weight:600;box-shadow:inset 2px 0 0 var(--accent);padding-left:10px;margin-left:-11px}
.toc .toc-sub{padding-left:14px}
.toc a.on.toc-sub{padding-left:24px}
.tech .post-meta{margin-top:12px}
/* ============ 图库(masonry:格高由照片宽高比驱动) ============ */
.g-title{font-size:20px;font-weight:700;color:var(--title);margin-top:40px}
.g-filters{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0 20px}
.chip{
padding:5px 13px;border-radius:999px;border:1px solid var(--border);
background:transparent;color:var(--muted);font-size:13px;cursor:pointer;
}
.chip:hover{border-color:var(--border-strong);color:var(--title)}
.chip.on{color:var(--title);border-color:var(--border-strong);font-weight:600}
.stage{background:var(--stage);padding:14px;border-radius:14px}
.wall{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:8px;grid-auto-flow:dense;gap:14px}
.shot{position:relative;aspect-ratio:var(--ar,4/3);border-radius:10px;overflow:hidden;cursor:zoom-in}
.shot svg{width:100%;height:100%}
.exif{
position:absolute;left:0;right:0;bottom:0;
display:flex;flex-direction:column;gap:2px;
padding:28px 12px 10px;
background:linear-gradient(180deg,transparent,#080a0e9e);
color:#fff;opacity:0;transform:translateY(6px);pointer-events:none;
}
.exif strong{font-size:13px;font-weight:600}
.exif span{font-size:11.5px;opacity:.85;font-variant-numeric:tabular-nums}
.shot:hover .exif,.shot:focus-within .exif{opacity:1;transform:none}
.g-note{margin-top:14px;font-size:12.5px;color:var(--faint)}
/* ============ 瞬间 · 时间线 ============ */
.tl{list-style:none;margin:26px 0 0;padding:0}
.tl-item{
position:relative;margin:0 0 20px 56px;padding-left:24px;
border-left:1px solid var(--border-strong);
}
.tl-item:last-child{margin-bottom:0}
.tl-date{
position:absolute;left:-60px;top:3px;width:44px;text-align:right;
font-family:var(--mono);font-size:12.5px;color:var(--faint);font-variant-numeric:tabular-nums;
}
.tl-item::after{
content:"";position:absolute;left:-4.5px;top:7px;width:8px;height:8px;border-radius:50%;
background:var(--accent);box-shadow:0 0 0 3px var(--bg);
}
.mom-card{border:1px solid var(--border);border-radius:12px;padding:14px 16px;background:var(--surface)}
.mom-card p{font-size:15px;color:var(--text);margin:0 0 8px}
.mom-card p:last-of-type{margin-bottom:0}
.mom-fig{border-radius:8px;overflow:hidden;margin:0 0 10px;max-width:420px}
.mom-fig.vt{max-width:230px}
.mom-fig svg{width:100%;height:auto}
.mom-tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
/* ============ 浮层公共 + 命令面板 ============ */
dialog{border:none;background:var(--surface);color:var(--text);padding:0}
dialog::backdrop{background:#0b0d1066;backdrop-filter:blur(2px)}
#palette{
width:min(560px,calc(100vw - 40px));margin:13vh auto auto;
border:1px solid var(--border-strong);border-radius:14px;
box-shadow:0 24px 60px #00000040;overflow:hidden;
}
#pq{
width:100%;border:none;outline:none;background:transparent;
padding:15px 18px;font:inherit;font-size:15px;color:var(--title);
border-bottom:1px solid var(--border);
}
#pr{max-height:320px;overflow:auto;padding:6px}
#pr button{
display:flex;align-items:center;gap:10px;width:100%;text-align:left;
border:none;background:none;font:inherit;font-size:14px;color:var(--text);
padding:9px 12px;border-radius:8px;cursor:pointer;
}
#pr button em{margin-left:auto;font-style:normal;font-family:var(--mono);font-size:11px;color:var(--faint)}
#pr button.on{background:var(--inline-bg);color:var(--title);box-shadow:inset 2px 0 0 var(--accent)}
.pal-none{padding:18px;text-align:center;color:var(--faint);font-size:13px}
.pal-hint{
display:flex;gap:16px;padding:10px 16px;border-top:1px solid var(--border);
font-size:11.5px;color:var(--faint);font-family:var(--mono);
}
/* ============ 快捷键帮助 ============ */
#keys{width:min(430px,calc(100vw - 40px));margin:auto;border:1px solid var(--border-strong);border-radius:14px;padding:22px 24px}
.kh{font-size:15px;font-weight:700;color:var(--title);margin-bottom:14px}
.klist{display:grid;grid-template-columns:auto 1fr;gap:9px 18px;align-items:center;margin:0}
.klist dt{white-space:nowrap}
.klist dd{margin:0;font-size:13px;color:var(--muted)}
kbd{
font-family:var(--mono);font-size:11.5px;color:var(--title);
background:var(--inline-bg);border:1px solid var(--border-strong);border-bottom-width:2px;
border-radius:5px;padding:1px 6px;
}
.kn{margin-top:14px;font-size:12px;color:var(--faint)}
/* ============ 终端彩蛋 ============ */
#term{inset:0;width:100vw;height:100vh;max-width:none;max-height:none;margin:0;border-radius:0;background:#0d1117;color:#7ee787}
#term::backdrop{background:#0d1117}
.tbar{display:flex;align-items:center;gap:6px;padding:10px 14px;border-bottom:1px solid #ffffff14}
.tbar .dot{width:10px;height:10px;border-radius:50%}
.tbar .r{background:#ff5f56}.tbar .y{background:#ffbd2e}.tbar .g{background:#27c93f}
.tbar .ttitle{margin-left:6px;font-family:var(--mono);font-size:11px;color:#8b949e}
#tclose{
margin-left:auto;width:26px;height:26px;border-radius:6px;border:none;cursor:pointer;
background:none;color:#8b949e;display:grid;place-items:center;
}
#tclose:hover{background:#ffffff14;color:#fff}
#tout{
padding:16px 18px 8px;font-family:var(--mono);font-size:14px;line-height:1.65;color:#7ee787;
max-height:calc(100vh - 130px);overflow:auto;white-space:pre-wrap;
}
#tout .tc{color:#d29922}
#tout .te{color:#f85149}
#tform{display:flex;gap:8px;padding:10px 18px 18px;font-family:var(--mono);font-size:14px}
.tprompt{color:#d29922;white-space:nowrap;user-select:none}
#tin{flex:1;min-width:0;background:transparent;border:none;outline:none;color:#7ee787;font:inherit;caret-color:#7ee787}
#tin:focus-visible{outline:none}
/* ============ 灯箱 ============ */
#lb{inset:0;width:100vw;height:100vh;max-width:none;max-height:none;margin:0;border-radius:0;background:transparent;display:flex;flex-direction:column}
#lb::backdrop{background:#000000f2}
#lbimg{flex:1;min-height:0;display:grid;place-items:center;padding:38px 20px 8px}
#lbimg svg{height:auto;max-height:100%;border-radius:6px}
.lbbottom{
display:flex;align-items:center;gap:16px;padding:12px 22px 20px;
color:#c9d1d9;font-family:var(--mono);font-size:12.5px;
}
.lbbottom strong{color:#fff;font-size:13px;font-weight:600;margin-right:10px;font-family:var(--sans)}
.lbbottom span{opacity:.82;font-variant-numeric:tabular-nums}
#lbexif{flex:1;min-width:0;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lbnav{
flex:none;width:38px;height:38px;border-radius:10px;cursor:pointer;
border:1px solid #ffffff2e;background:#ffffff0f;color:#e6e9ee;
display:grid;place-items:center;
}
.lbnav:hover{background:#ffffff1f}
#lbclose{position:absolute;top:16px;right:18px}
/* ============ 动效纪律(唯一缓动 × 0.35s,全部包进 no-preference) ============ */
@media (prefers-reduced-motion: no-preference){
html{scroll-behavior:smooth}
.hero,.feed > *,.oss,.latest{animation:rise var(--dur) var(--ease) both;animation-delay:calc(var(--i,0) * 45ms)}
.view{animation:viewin var(--dur) var(--ease) both}
dialog[open]{animation:pop .25s var(--ease)}
.row-title,.row-date,.chip,.toc a,.nav-item,.socials a,.more,.pn-item strong,
.repo,#pr button,.fsdock button,.lbnav,.foot-term,
.tb-seg span,.tb-btn,.shot svg,.exif,.site-foot a{
transition:
color var(--dur) var(--ease),
background-color var(--dur) var(--ease),
border-color var(--dur) var(--ease),
opacity var(--dur) var(--ease),
transform var(--dur) var(--ease);
}
}
@keyframes rise{from{opacity:0;transform:translateY(12px)}}
@keyframes viewin{from{opacity:0;transform:translateY(10px)}}
@keyframes pop{from{opacity:0;transform:translateY(8px)}}
/* ============ 响应式 ============ */
@media (max-width:1120px){
.toc{display:none}
.tech-grid{display:block}
}
@media (max-width:820px){
.wall{grid-template-columns:repeat(2,1fr)}
.repos{grid-template-columns:1fr}
}
@media (max-width:760px){
.tb{position:static}
body{padding-top:0}
.fsdock{top:12px}
}
@media (max-width:560px){
.sheet{padding:22px}
.hero{margin:32px 0 30px}
.pn{flex-direction:column;gap:14px}
.pn-item.right{align-items:flex-start;text-align:left}
.tl-item{margin-left:40px}
.tl-date{left:-46px;width:34px}
#tout{font-size:13px}
}
</style>
</head>
<body>
<!-- 内联渐变定义(全部占位图共用,零外部资源) -->
<svg aria-hidden="true" focusable="false" style="position:absolute;width:0;height:0">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5b7cfa"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient>
<linearGradient id="lg1" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7b267"/><stop offset="1" stop-color="#9a3d0f"/></linearGradient>
<linearGradient id="lg2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#67e0cf"/><stop offset="1" stop-color="#0d5c56"/></linearGradient>
<linearGradient id="lg3" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#c4b5fd"/><stop offset="1" stop-color="#4c1d95"/></linearGradient>
<linearGradient id="lg4" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fda4af"/><stop offset="1" stop-color="#831843"/></linearGradient>
<linearGradient id="lg5" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#9fb3c8"/><stop offset="1" stop-color="#2f3e52"/></linearGradient>
<linearGradient id="lg6" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#86efac"/><stop offset="1" stop-color="#14532d"/></linearGradient>
<linearGradient id="lg7" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fcd34d"/><stop offset="1" stop-color="#92400e"/></linearGradient>
<linearGradient id="lg8" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#93c5fd"/><stop offset="1" stop-color="#1e3a8a"/></linearGradient>
<linearGradient id="lg9" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#d4d4d4"/><stop offset="1" stop-color="#404040"/></linearGradient>
</defs>
</svg>
<!-- 阅读进度条(技术文章页) -->
<div id="pbar" aria-hidden="true"></div>
<!-- 预览工具条:不属于博客 UI -->
<header class="tb" aria-label="视觉样板预览控制">
<span class="tb-brand"><b>wait4x</b> · 视觉样板</span>
<span class="tb-sep"></span>
<div class="tb-group" role="radiogroup" aria-label="视觉路线">
<label class="tb-seg"><input type="radio" name="route" value="a"><span>甲·纸墨书卷</span></label>
<label class="tb-seg"><input type="radio" name="route" value="b"><span>乙·极简灰阶</span></label>
<label class="tb-seg"><input type="radio" name="route" value="c" checked><span>丙·灰阶骨架×分轨正文<em class="badge">推荐</em></span></label>
</div>
<button class="tb-btn" id="schemeBtn" type="button" aria-label="切换明暗模式" title="明暗切换(d)">
<svg class="ic-moon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M20.6 13.2A8.2 8.2 0 1 1 10.8 3.4a6.5 6.5 0 0 0 9.8 9.8Z"/></svg>
<svg class="ic-sun" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5 5l1.4 1.4M17.6 17.6 19 19M19 5l-1.4 1.4M6.4 17.6 5 19"/></svg>
</button>
<span class="tb-sep"></span>
<div class="tb-group" role="radiogroup" aria-label="页面类型">
<label class="tb-seg"><input type="radio" name="page" value="home" id="p-home" checked><span>首页</span></label>
<label class="tb-seg"><input type="radio" name="page" value="essay" id="p-essay"><span>文章页·书评</span></label>
<label class="tb-seg"><input type="radio" name="page" value="tech" id="p-tech"><span>文章页·技术</span></label>
<label class="tb-seg"><input type="radio" name="page" value="gallery" id="p-gallery"><span>图库</span></label>
<label class="tb-seg"><input type="radio" name="page" value="moments" id="p-moments"><span>瞬间</span></label>
</div>
<div class="tb-right">
<button class="tb-btn" id="keysBtn" type="button" aria-label="快捷键帮助(?)" title="快捷键(?)">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="3" y="6.5" width="18" height="11" rx="2"/><path d="M6.5 10h.01M10 10h.01M13.5 10h.01M17 10h.01M9 14h6M6.5 14h.01M17 14h.01"/></svg>
</button>
<button class="tb-btn tb-pal" id="paletteBtn" type="button" aria-label="命令面板(Ctrl K 或 /)" title="命令面板(Ctrl K 或 /)">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20.5 20.5-4.6-4.6"/></svg>
<span>Ctrl K</span>
</button>
</div>
</header>
<!-- 站点导航 -->
<header class="site-head">
<div class="container site-head-row">
<a class="site-name" href="#top">wait4x</a>
<nav class="nav" aria-label="站点导航">
<label class="nav-item" for="p-home" title="文章">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="4.5" y="3" width="15" height="18" rx="2.5"/><path d="M9 8h6M9 12h6M9 16h3.5"/></svg>
<span class="nav-t">文章</span>
</label>
<label class="nav-item" for="p-gallery" title="图库">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.7"/><path d="m5 17.5 4.5-4.5 3 3 3.5-3.5 3 3"/></svg>
<span class="nav-t">图库</span>
</label>
<a class="nav-item" href="#about" title="关于">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="8.2" r="3.8"/><path d="M4.5 20c1.6-3.4 4.3-5 7.5-5s5.9 1.6 7.5 5"/></svg>
<span class="nav-t">关于</span>
</a>
<button class="nav-item nav-icon" id="navSearch" type="button" aria-label="搜索(Ctrl K)" title="搜索(Ctrl K)">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20.5 20.5-4.6-4.6"/></svg>
</button>
</nav>
</div>
</header>
<main>
<!-- ============ 首页 ============ -->
<section class="view view-home">
<div class="container">
<div class="hero" style="--i:0">
<svg class="hero-avatar" viewBox="0 0 60 60" width="60" height="60" role="img" aria-label="头像占位">
<rect width="60" height="60" rx="14" fill="url(#lgA)"/>
<circle cx="44" cy="15" r="10" fill="#fff" opacity=".25"/>
<path d="M0 46 20 28 34 40 46 32 60 46 60 60 0 60Z" fill="#000" opacity=".14"/>
<text x="11" y="40" font-size="20" font-weight="700" fill="#fff" opacity=".92" font-family="system-ui,sans-serif">w4</text>
</svg>
<div>
<div class="hero-name">wait4x</div>
<p class="hero-bio">写作者 · 读者 · 快门按压者,拥抱开源。</p>
<div class="socials">
<a href="#github" aria-label="GitHub" title="GitHub">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.57 2.34 1.12 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.7 0 0 .84-.28 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.4.2 2.44.1 2.7.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.6.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z"/></svg>
</a>
<a href="#rss" aria-label="RSS 订阅" title="RSS">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 11.5a8.5 8.5 0 0 1 8.5 8.5M4 4.5a15.5 15.5 0 0 1 15.5 15.5"/><circle cx="5.4" cy="18.6" r="1.5" fill="currentColor" stroke="none"/></svg>
</a>
<a href="#mail" aria-label="邮箱" title="邮箱">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><path d="m4 7.5 8 5.5 8-5.5"/></svg>
</a>
</div>
</div>
</div>
<div class="feed">
<h2 class="year" style="--i:0">2026</h2>
<div class="track" style="--i:1">阅读</div>
<label class="row" for="p-essay" style="--i:2"><i class="emo" style="--emo:#7c3aed" title="情绪色"></i><span class="row-title">等待戈多</span><span class="row-date">03-31</span></label>
<label class="row" for="p-essay" style="--i:3"><i class="emo" style="--emo:#0f766e" title="情绪色"></i><span class="row-title">海风中失落的血色馈赠</span><span class="row-date">03-25</span></label>
<label class="row" for="p-essay" style="--i:4"><i class="emo" style="--emo:#b45309" title="情绪色"></i><span class="row-title">面纱</span><span class="row-date">03-09</span></label>
<label class="row" for="p-essay" style="--i:5"><i class="emo" style="--emo:#9f1239" title="情绪色"></i><span class="row-title">赎罪</span><span class="row-date">02-26</span></label>
<label class="row" for="p-essay" style="--i:6"><i class="emo" style="--emo:#4d7c0f" title="情绪色"></i><span class="row-title">泥潭</span><span class="row-date">02-09</span></label>
<div class="track" style="--i:7">技术</div>
<label class="row" for="p-tech" style="--i:8"><span class="row-title">CS各类配置</span><code class="tag">#linux</code><span class="row-date">04-28</span></label>
</div>
<!-- 开源活动:热力图 + 项目卡 -->
<div class="oss" style="--i:10">
<div class="track">开源活动</div>
<div id="heat" role="img" aria-label="近一年开源贡献热力图(示例数据,构建时由 gitea 生成)"></div>
<div class="heat-meta">
<span>开源活动 · 数据来自 gitea,构建时生成</span>
<span class="heat-legend">少<i class="h0"></i><i class="h1"></i><i class="h2"></i><i class="h3"></i><i class="h4"></i>多</span>
</div>
<div class="repos">
<a class="repo" href="https://git.wait4x.online/wait4x/blog" target="_blank" rel="noopener noreferrer">
<code>wait4x/blog</code>
<span>本站源码:内容即代码,构建产物零运行时 JS。</span>
<span class="repo-meta"><i style="--lc:#ff5a03"></i>Astro<b>★ 86</b></span>
</a>
<a class="repo" href="https://git.wait4x.online/wait4x/hydro-problems" target="_blank" rel="noopener noreferrer">
<code>wait4x/hydro-problems</code>
<span>OJ 题库生成脚本:从题面到判题数据一条流水线。</span>
<span class="repo-meta"><i style="--lc:#3572a5"></i>Python<b>★ 47</b></span>
</a>
</div>
</div>
<div class="latest" style="--i:11">
<div class="latest-head">
<span class="lab">最新影像</span>
<label class="more" for="p-gallery">进入图库 →</label>
</div>
<div class="thumbs">
<figure title="暮色山脊"><svg viewBox="0 0 80 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位"><rect width="80" height="80" fill="url(#lg4)"/><circle cx="56" cy="22" r="9" fill="#fff" opacity=".35"/><path d="M0 80V56l26-18 22 18 16-10 16 20v14Z" fill="#000" opacity=".18"/></svg></figure>
<figure title="防波堤"><svg viewBox="0 0 80 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位"><rect width="80" height="80" fill="url(#lg2)"/><circle cx="24" cy="30" r="8" fill="#fff" opacity=".3"/><path d="M0 52h80" stroke="#fff" stroke-width="1" opacity=".25"/><path d="M0 80V64l30-8 50 12v12Z" fill="#000" opacity=".18"/></svg></figure>
<figure title="雪线"><svg viewBox="0 0 80 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位"><rect width="80" height="80" fill="url(#lg8)"/><path d="M0 80 30 34l16 24 12-16 22 38Z" fill="#000" opacity=".2"/><circle cx="62" cy="16" r="6" fill="#fff" opacity=".4"/></svg></figure>
<figure title="窗台的光"><svg viewBox="0 0 80 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位"><rect width="80" height="80" fill="url(#lg7)"/><rect x="18" y="14" width="44" height="52" rx="3" fill="#fff" opacity=".22"/><rect x="24" y="20" width="32" height="40" rx="2" fill="#fff" opacity=".18"/><path d="M0 80V66l80-10v24Z" fill="#000" opacity=".16"/></svg></figure>
</div>
</div>
</div>
</section>
<!-- ============ 文章页 · 书评《等待戈多》 ============ -->
<section class="view view-essay">
<div class="container">
<article class="post essay">
<div class="sheet">
<header class="post-head">
<div class="post-cat">Reading</div>
<h1 class="post-title">等待戈多</h1>
<div class="post-meta">
<i class="emo" style="--emo:#7c3aed" title="情绪色"></i>
<span>2026-03-31</span><span>·</span><span>阅读时长 8 分钟</span>
</div>
</header>
<div class="prose">
<p>戈多是全书贯穿始终的人物,可并没有任何一个人见过戈多。爱斯特拉冈和弗拉季米尔在乡间小路上等他,等到靴子脱了又穿,等到胡萝卜啃完了又换饼干,等到波卓和幸运儿来了又走,戈多始终没有来。第二天他们接着等,仿佛头一天的一切都可以原封不动地再来一遍。</p>
<p>「谁是戈多?为何等待?」贝克特对这两个问题一向敬谢不敏,据说他只回过一句:我要是知道,早在戏里说清楚了。于是戈多成了一个空位——有人读出上帝,有人读出希望,有人读出永远缺席的意义本身。空位越空,人们越急着往里填东西;这大概正是这出戏首演七十年后仍然刺人的原因。</p>
<blockquote class="quote-hero"><p>希望迟迟不来,苦死了等的人。</p></blockquote>
<p class="quote-src">——萨缪尔·贝克特《等待戈多》</p>
<p>就戏剧动作而言,《等待戈多》几乎什么都没有发生,而这正是它最残忍的地方:它不安排绝望的事件,只安排绝望的节奏。对话膨胀又塌缩,约定立起又被推翻,戏在「咱们走吧」与「他们不动」之间原地打转。等待本身成了全部的情节,也成了全部的刑罚。</p>
<blockquote class="pull">
<p>爱斯特拉冈:咱们走吧。<br>弗拉季米尔:咱们不能。<br>爱斯特拉冈:为什么不能?<br>弗拉季米尔:咱们在等待戈多。</p>
</blockquote>
<p>可我并不觉得这是全然的黑暗。两个流浪汉在无望之待里,仍然留住了几样东西:他们拌嘴,也互相系鞋带;他们想上吊,又舍不得用裤腰带;他们每天遇见,每天确认「你又来了」。无望之待没有被拔高成救赎,但陪伴让囚笼有了呼吸的缝隙——自我救赎未必是走出困境,有时只是在困境里替对方把靴子脱下来。</p>
<p>合上书那天傍晚,我在站台等一班迟迟不来的车,忽然想起幸运儿的思想独白:原来我们都站在各自的乡间小路上,书包里装着胡萝卜,等一个不会来的戈多。贝克特没有给我们答案,只留下一句温柔的嘲讽——他们许诺明天一切都会好起来。信不信,由等的人自己决定。</p>
</div>
</div>
<nav class="pn" aria-label="上下篇">
<a class="pn-item" href="#prev">
<span>← 上一篇</span><strong>海风中失落的血色馈赠</strong>
</a>
<label class="pn-item right" for="p-tech">
<span>下一篇 →</span><strong>CS各类配置</strong>
</label>
</nav>
<div class="comments"><b>评论系统占位</b>此处将接入 Artalk(Go 单二进制自托管,sqlite 存储)</div>
</article>
</div>
</section>
<!-- ============ 文章页 · 技术《CS各类配置》 ============ -->
<section class="view view-tech">
<div class="container">
<div class="tech-grid">
<article class="post tech">
<header class="post-head">
<h1 class="post-title">CS各类配置</h1>
<div class="post-meta">
<code class="tag">#docker</code><code class="tag">#debian</code>
<span>·</span><span>2026-04-28</span>
</div>
</header>
<div class="prose">
<p>上周把一台 2 核 2G 的入门云服务器重装成 Debian 12,顺手把从裸系统到能跑服务的最小流程整理成清单。目标很简单:换一块干净的地基——初始化用户、换源、装 Docker,全程不超过二十分钟,之后所有服务一律进容器。</p>
<h2 id="t-sys">系统初始化</h2>
<p>第一件事是建普通用户、配好密钥,再把密码登录关掉。用 <code>adduser deploy</code> 创建用户并加入 <code>sudo</code> 组,随后在 <code>/etc/ssh/sshd_config</code> 里设置 <code>PasswordAuthentication no</code> 与 <code>PermitRootLogin no</code>,重载 sshd 即可。UFW 只放行 22 与 80/443,云控制台的安全组记得同步。</p>
<h2 id="t-src">更换软件源</h2>
<p>官方源对国内机器不算友好,换到清华 TUNA 镜像几乎是必选项。改动集中在 <code>/etc/apt/sources.list</code>,把 <code>deb.debian.org</code> 整体替换即可,bookworm 与 bookworm/updates 两段都要覆盖。</p>
<figure class="term">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">bash · deploy@vps</span></div>
<pre><code><span class="cm"># 换源:备份后替换为清华 TUNA 镜像(Debian 12 / bookworm)</span>
<span class="pr">$</span> <span class="kw">sudo</span> <span class="cmd">cp</span> /etc/apt/sources.list{,.bak}
<span class="pr">$</span> <span class="kw">sudo</span> <span class="cmd">sed</span> -i <span class="str">'s|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g'</span> /etc/apt/sources.list
<span class="pr">$</span> <span class="kw">sudo</span> <span class="cmd">apt</span> update &amp;&amp; <span class="kw">sudo</span> <span class="cmd">apt</span> upgrade -y
<span class="cm"># 安装 Docker Engine(官方 APT 仓库)</span>
<span class="pr">$</span> <span class="cmd">curl</span> -fsSL <span class="str">https://download.docker.com/linux/debian/gpg</span> | <span class="kw">sudo</span> <span class="cmd">gpg</span> --dearmor -o /usr/share/keyrings/docker.gpg
<span class="pr">$</span> <span class="cmd">echo</span> <span class="str">"deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable"</span> | <span class="kw">sudo</span> <span class="cmd">tee</span> /etc/apt/sources.list.d/docker.list
<span class="pr">$</span> <span class="kw">sudo</span> <span class="cmd">apt</span> update &amp;&amp; <span class="kw">sudo</span> <span class="cmd">apt</span> install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
<span class="pr">$</span> <span class="kw">sudo</span> <span class="cmd">systemctl</span> enable --now docker &amp;&amp; <span class="kw">sudo</span> <span class="cmd">docker</span> run --rm hello-world</code></pre>
</figure>
<h2 id="t-dock">安装 Docker</h2>
<p>官方的 <code>get.docker.com</code> 脚本虽然省事,但会替你决定源的写入方式;按上面的官方 APT 仓库手动配置更可控,以后升级也只走同一条路。装完用 <code>docker version</code> 确认 Engine 与 compose 插件都在。</p>
<h3 id="t-mir">国内镜像加速</h3>
<p>拉取镜像前,在 <code>/etc/docker/daemon.json</code> 里注册镜像加速地址,<code>systemctl restart docker</code> 生效;<code>docker info</code> 的末尾能看到 Registry Mirrors 是否挂载成功。</p>
</div>
</article>
<aside class="toc" aria-label="目录">
<div class="toc-h">目录</div>
<a href="#t-sys">系统初始化</a>
<a href="#t-src">更换软件源</a>
<a href="#t-dock">安装 Docker</a>
<a class="toc-sub" href="#t-mir">国内镜像加速</a>
</aside>
</div>
</div>
</section>
<!-- ============ 图库(masonry) ============ -->
<section class="view view-gallery">
<div class="container">
<h1 class="g-title">图库</h1>
<div class="g-filters" role="group" aria-label="分组筛选(静态示意)">
<button class="chip on" type="button">全部</button>
<button class="chip" type="button">2026</button>
<button class="chip" type="button">街拍</button>
<button class="chip" type="button">风景</button>
</div>
<div class="stage">
<div class="wall" id="wall">
<figure class="shot" tabindex="0" style="--ar:3/2">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:晨光街口"><rect width="400" height="300" fill="url(#lg1)"/><circle cx="286" cy="80" r="36" fill="#fff" opacity=".32"/><path d="M0 300V190h44v-42h30v62h36v-92h34v72h42v-46h40v92h58v-60h40v124Z" fill="#000" opacity=".2"/><path d="M0 300v-30l400-40v70Z" fill="#000" opacity=".22"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>晨光街口 · 街拍</strong><span>X-T5 · 23mm f/1.4 · f/2 · 1/500s · ISO 160</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:16/9">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:防波堤"><rect width="400" height="300" fill="url(#lg2)"/><circle cx="120" cy="96" r="30" fill="#fff" opacity=".3"/><path d="M0 168h400" stroke="#fff" stroke-width="2" opacity=".25"/><path d="M0 300V210l120-26 140 30 140-44v130Z" fill="#000" opacity=".18"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>防波堤 · 风景</strong><span>X-T5 · 35mm f/1.4 · f/8 · 1/1000s · ISO 160</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:2/3">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:夜班地铁"><rect width="400" height="300" fill="url(#lg3)"/><circle cx="320" cy="62" r="20" fill="#fff" opacity=".4"/><path d="M0 300V170h50v-40h36v56h44v-96h40v78h48v-52h44v88h56v-64h42v160Z" fill="#000" opacity=".26"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>夜班地铁 · 街拍</strong><span>X-T5 · 35mm f/1.4 · f/1.8 · 1/125s · ISO 3200</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:3/2">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:暮色山脊"><rect width="400" height="300" fill="url(#lg4)"/><circle cx="96" cy="88" r="34" fill="#fff" opacity=".32"/><path d="M0 300 130 130l90 96 70-58 110 132Z" fill="#000" opacity=".2"/><path d="M0 300v-56l400-36v92Z" fill="#000" opacity=".24"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>暮色山脊 · 风景</strong><span>X-T5 · 56mm f/1.2 · f/5.6 · 1/250s · ISO 400</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:1/1">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:雨后屋檐"><rect width="400" height="300" fill="url(#lg5)"/><path d="M40 96 200 40l160 56Z" fill="#000" opacity=".28"/><path d="M70 110v130M120 118v150M170 108v160M220 112v150M270 104v160M320 112v140" stroke="#fff" stroke-width="2" opacity=".22"/><path d="M0 300v-40l400-24v64Z" fill="#000" opacity=".2"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>雨后屋檐 · 街拍</strong><span>X-T5 · 23mm f/2 · f/2.8 · 1/250s · ISO 1250</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:4/5">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:林间小径"><rect width="400" height="300" fill="url(#lg6)"/><path d="M52 0v300M148 0v260M252 0v280M348 0v300" stroke="#000" stroke-width="14" opacity=".18"/><path d="M170 300c30-70 90-120 60-190 70 40 30 130 60 190Z" fill="#fff" opacity=".14"/><path d="M0 300v-30l400-28v58Z" fill="#000" opacity=".2"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>林间小径 · 风景</strong><span>X-T5 · 18-55mm @35mm · f/5.6 · 1/60s · ISO 640</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:3/2">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:窗台的光"><rect width="400" height="300" fill="url(#lg7)"/><rect x="90" y="52" width="220" height="180" rx="6" fill="#fff" opacity=".22"/><rect x="112" y="74" width="176" height="136" rx="4" fill="#fff" opacity=".16"/><path d="M60 300 200 150l20 150Z" fill="#000" opacity=".16"/><path d="M0 300v-26l400-30v56Z" fill="#000" opacity=".18"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>窗台的光 · 街拍</strong><span>X-T5 · 35mm f/1.4 · f/2 · 1/250s · ISO 640</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:16/9">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:雪线"><rect width="400" height="300" fill="url(#lg8)"/><circle cx="316" cy="60" r="24" fill="#fff" opacity=".45"/><path d="M0 300 120 96l70 116 56-72L400 300Z" fill="#000" opacity=".2"/><path d="M120 96l44 72-44 20-40-28Z" fill="#fff" opacity=".28"/><path d="M0 300v-24l400-26v50Z" fill="#000" opacity=".22"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>雪线 · 风景</strong><span>X-T5 · 56mm · f/8 · 1/1500s · ISO 200</span></figcaption>
</figure>
<figure class="shot" tabindex="0" style="--ar:4/5">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="照片占位:站台"><rect width="400" height="300" fill="url(#lg9)"/><path d="M60 40v180M330 40v180" stroke="#000" stroke-width="10" opacity=".18"/><path d="M40 220h320" stroke="#000" stroke-width="4" opacity=".22"/><path d="M0 300v-44l400-30v74Z" fill="#000" opacity=".22"/><circle cx="200" cy="70" r="26" fill="#fff" opacity=".3"/><text x="12" y="286" font-size="12" fill="#fff" opacity=".55" font-family="system-ui,sans-serif">照片占位</text></svg>
<figcaption class="exif"><strong>站台 · 街拍</strong><span>X-T5 · 23mm · f/2 · 1/60s · ISO 1600</span></figcaption>
</figure>
</div>
</div>
<p class="g-note">共 21 张 · EXIF 自动提取 · 格高由照片实际宽高比驱动——真实构建时计算,零 JS</p>
</div>
</section>
<!-- ============ 瞬间 · 时间线 ============ -->
<section class="view view-moments">
<div class="container">
<h1 class="g-title">瞬间</h1>
<p class="g-note">零碎的记录:随手拍、一句话感想、开源动态。</p>
<ol class="tl">
<li class="tl-item">
<time class="tl-date" datetime="2026-10-19">10-19</time>
<div class="mom-card">
<figure class="mom-fig"><svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid slice" role="img" aria-label="随手拍占位:傍晚的云"><rect width="300" height="200" fill="url(#lg7)"/><ellipse cx="90" cy="70" rx="46" ry="14" fill="#fff" opacity=".28"/><ellipse cx="180" cy="50" rx="60" ry="12" fill="#fff" opacity=".22"/><path d="M0 200v-46l60-12 40 8 50-16 60 14 90-10v62Z" fill="#000" opacity=".22"/></svg></figure>
<p>傍晚的云压得很低,路灯提前亮了。</p>
<div class="mom-tags"><code class="tag">#随手拍</code><code class="tag">#手机</code></div>
</div>
</li>
<li class="tl-item">
<time class="tl-date" datetime="2026-10-12">10-12</time>
<div class="mom-card">
<p>《人鼠之间》——梦越简单,碎的时候越响。</p>
<div class="mom-tags"><code class="tag">#阅读</code><code class="tag">#一句话书评</code></div>
</div>
</li>
<li class="tl-item">
<time class="tl-date" datetime="2026-10-02">10-02</time>
<div class="mom-card">
<p>给上游提的 doctor 误报 issue 当天被采纳,第三天随新版本发布——开源最好的正反馈。</p>
<div class="mom-tags"><code class="tag">#开源</code></div>
</div>
</li>
<li class="tl-item">
<time class="tl-date" datetime="2026-09-21">09-21</time>
<div class="mom-card">
<figure class="mom-fig vt"><svg viewBox="0 0 200 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="随手拍占位:长椅上的猫"><rect width="200" height="300" fill="url(#lg6)"/><path d="M78 118 92 94l14 24Z" fill="#000" opacity=".55"/><path d="M108 118 122 94l14 24Z" fill="#000" opacity=".55"/><circle cx="100" cy="150" r="42" fill="#000" opacity=".55"/><rect x="30" y="208" width="140" height="14" rx="6" fill="#000" opacity=".3"/><path d="M0 300v-60l200-30v90Z" fill="#000" opacity=".25"/></svg></figure>
<p>楼下的猫接管了整条长椅。</p>
<div class="mom-tags"><code class="tag">#随手拍</code><code class="tag">#猫</code></div>
</div>
</li>
</ol>
</div>
</section>
</main>
<!-- 页脚 -->
<footer class="site-foot">
<div class="container">
<div>© 2026 wait4x · <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">陕ICP备2026014860号-1</a><button class="foot-term" id="footTerm" type="button" aria-label="打开终端(快捷键 ~)" title="终端(~)"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m4 5.5 6.5 6.5L4 18.5"/><path d="M12.5 18.5H20"/></svg></button></div>
<div class="build">built from <a href="https://git.wait4x.online/wait4x/blog/commit/e3f1a9c" target="_blank" rel="noopener noreferrer">e3f1a9c</a> · 2026-10-24T13:42Z · Astro</div>
</div>
</footer>
<!-- 字号 dock(书评页) -->
<div class="fsdock" role="group" aria-label="正文字号">
<button type="button" data-fs="-2" aria-label="减小字号">A-</button>
<button type="button" data-fs="r" aria-label="重置字号">A</button>
<button type="button" data-fs="2" aria-label="增大字号">A+</button>
</div>
<!-- 命令面板 -->
<dialog id="palette" aria-label="命令面板">
<input id="pq" type="text" placeholder="搜索文章、页面,或执行动作…" autocomplete="off" spellcheck="false" aria-label="搜索">
<div id="pr" role="listbox" aria-label="结果列表"></div>
<div class="pal-hint"><span>↑↓ 选择</span><span>Enter 执行</span><span>Esc 关闭</span></div>
</dialog>
<!-- 快捷键帮助 -->
<dialog id="keys" aria-label="快捷键帮助">
<div class="kh">快捷键</div>
<dl class="klist">
<dt><kbd>Ctrl</kbd> <kbd>K</kbd> 或 <kbd>/</kbd></dt><dd>命令面板</dd>
<dt><kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd></dt><dd>切换路线 甲 / 乙 / 丙</dd>
<dt><kbd>d</kbd></dt><dd>切换明暗</dd>
<dt><kbd>g</kbd> <kbd>h</kbd></dt><dd>首页</dd>
<dt><kbd>g</kbd> <kbd>e</kbd></dt><dd>书评页</dd>
<dt><kbd>g</kbd> <kbd>t</kbd></dt><dd>技术页</dd>
<dt><kbd>g</kbd> <kbd>g</kbd></dt><dd>图库</dd>
<dt><kbd>g</kbd> <kbd>m</kbd></dt><dd>瞬间</dd>
<dt><kbd>~</kbd></dt><dd>终端</dd>
<dt><kbd>?</kbd></dt><dd>本帮助</dd>
</dl>
<p class="kn">输入框聚焦时快捷键暂停;Esc 关闭浮层。</p>
</dialog>
<!-- 终端彩蛋 -->
<dialog id="term" aria-label="终端">
<div class="tbar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="ttitle">wait4x@blog — bash</span><button id="tclose" type="button" aria-label="关闭终端"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m5 5 14 14M19 5 5 19"/></svg></button></div>
<div id="tout" aria-live="polite"></div>
<form id="tform"><label class="tprompt" for="tin">wait4x@blog:~$</label><input id="tin" autocomplete="off" spellcheck="false" aria-label="终端输入"></form>
</dialog>
<!-- 灯箱 -->
<dialog id="lb" aria-label="照片查看">
<button id="lbclose" type="button" class="lbnav" aria-label="关闭"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m5 5 14 14M19 5 5 19"/></svg></button>
<div id="lbimg"></div>
<div class="lbbottom">
<button id="lbprev" type="button" class="lbnav" aria-label="上一张(←)"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14.5 5.5-6.5 6.5 6.5 6.5"/></svg></button>
<div id="lbexif"></div>
<button id="lbnext" type="button" class="lbnav" aria-label="下一张(→)"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 5.5 6.5 6.5-6.5 6.5"/></svg></button>
</div>
</dialog>
<script>
const root=document.documentElement,pb=document.getElementById('pbar');
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
function go(n,v){const r=$(`.tb input[name=${n}][value="${v}"]`);if(r){r.checked=true;r.dispatchEvent(new Event('change'))}}
for(const i of $$('.tb input'))i.addEventListener('change',()=>{root.dataset[i.name]=i.value;if(i.name==='page')requestAnimationFrame(mason)});
const flipScheme=()=>{root.dataset.scheme=root.dataset.scheme==='dark'?'light':'dark'};
$('#schemeBtn').addEventListener('click',flipScheme);
addEventListener('scroll',()=>{const h=root.scrollHeight-innerHeight;pb.style.width=(h>0?root.scrollTop/h*100:0)+'%'},{passive:true});
// 命令面板与终端共用数据(样板内写死;真实站点构建时从内容集合生成)
const POSTS=[
{t:'等待戈多',c:'阅读 · 书评',k:'贝克特 戏剧 荒诞 等待',go:()=>go('page','essay')},
{t:'海风中失落的血色馈赠',c:'阅读 · 书评',k:'短篇 小说',go:()=>go('page','essay')},
{t:'面纱',c:'阅读 · 书评',k:'毛姆 小说',go:()=>go('page','essay')},
{t:'赎罪',c:'阅读 · 书评',k:'麦克尤恩 小说',go:()=>go('page','essay')},
{t:'泥潭',c:'阅读 · 书评',k:'小说',go:()=>go('page','essay')},
{t:'CS各类配置',c:'技术',k:'#docker #debian linux 服务器',go:()=>go('page','tech')},
];
const PAGES=[
{t:'首页 · 文章列表',k:'home blog',go:()=>go('page','home')},
{t:'文章页 · 书评《等待戈多》',k:'essay reading',go:()=>go('page','essay')},
{t:'文章页 · 技术《CS各类配置》',k:'tech docker',go:()=>go('page','tech')},
{t:'图库',k:'gallery photo 照片 影像',go:()=>go('page','gallery')},
{t:'瞬间 · 时间线',k:'moments timeline 动态',go:()=>go('page','moments')},
];
const ACTIONS=[
{t:'切换明暗模式',k:'theme dark light 明暗',go:flipScheme},
{t:'路线 甲 · 纸墨书卷',k:'route a 甲',go:()=>go('route','a')},
{t:'路线 乙 · 极简灰阶',k:'route b 乙',go:()=>go('route','b')},
{t:'路线 丙 · 灰阶骨架×分轨正文',k:'route c 丙',go:()=>go('route','c')},
{t:'打开终端',k:'terminal shell 终端',go:openTerm},
{t:'快捷键帮助',k:'keyboard help 快捷键',go:()=>$('#keys').showModal()},
];
const ITEMS=[...POSTS.map(x=>(x.p='文章',x)),...PAGES.map(x=>(x.p='页面',x)),...ACTIONS.map(x=>(x.p='动作',x))];
// 命令面板:纯前端 include 过滤 + ↑↓/Enter
const pal=$('#palette'),pq=$('#pq'),pr=$('#pr');let hits=[],ai=0;
const paint=()=>$$('#pr button').forEach((b,n)=>{b.classList.toggle('on',n===ai);b.setAttribute('aria-selected',n===ai)});
const render=()=>{const q=pq.value.trim().toLowerCase();hits=ITEMS.filter(x=>!q||(x.t+x.c+x.k+x.p).toLowerCase().includes(q));ai=0;pr.innerHTML=hits.map((h,n)=>`<button type="button" role="option" data-n="${n}" aria-selected="${!n}"><span>${h.t}</span><em>${h.p}</em></button>`).join('')||'<div class="pal-none">没有匹配的结果</div>'};
const runHit=()=>{const h=hits[ai];if(h){pal.close();h.go()}};
function openPal(){pq.value='';render();pal.showModal();pq.focus()}
pq.addEventListener('input',render);
pq.addEventListener('keydown',e=>{if(e.key==='ArrowDown'){e.preventDefault();ai=Math.min(ai+1,hits.length-1);paint()}else if(e.key==='ArrowUp'){e.preventDefault();ai=Math.max(ai-1,0);paint()}else if(e.key==='Enter'){e.preventDefault();runHit()}});
pr.addEventListener('click',e=>{const b=e.target.closest('button');if(b){ai=+b.dataset.n;runHit()}});
pr.addEventListener('mousemove',e=>{const b=e.target.closest('button');if(b&&+b.dataset.n!==ai){ai=+b.dataset.n;paint()}});
$('#paletteBtn').addEventListener('click',openPal);
$('#navSearch').addEventListener('click',openPal);
// 终端彩蛋:黑底绿字琥珀提示符,switch 分发
const term=$('#term'),tin=$('#tin'),tout=$('#tout');
const tprint=(s,c)=>{const d=document.createElement('div');if(c)d.className=c;d.textContent=s;tout.appendChild(d);tout.scrollTop=tout.scrollHeight};
function openTerm(){term.showModal();tin.focus()}
$('#tform').addEventListener('submit',e=>{e.preventDefault();const raw=tin.value.trim();tin.value='';tprint('wait4x@blog:~$ '+raw,'tc');const a=raw.split(/\s+/),c=a[0];
switch(c){
case'':break;
case'help':['help 列出命令','whoami 站长是谁','ls 列出文章','theme dark|light 切换主题','exit 关闭终端'].forEach(s=>tprint(' '+s));break;
case'whoami':tprint('wait4x —— 写作者 · 读者 · 快门按压者,拥抱开源。');break;
case'ls':POSTS.forEach(p=>tprint(` ${p.t}(${p.c})`));break;
case'theme':a[1]==='dark'||a[1]==='light'?(root.dataset.scheme=a[1],tprint(` theme → ${a[1]}`)):tprint(' 用法:theme dark|light','te');break;
case'exit':term.close();break;
default:tprint(` bash: ${c}: command not found`,'te');
}});
$('#tclose').addEventListener('click',()=>term.close());
$('#footTerm').addEventListener('click',openTerm);
// 贡献热力图:固定种子(样例每次渲染一致,等价构建时生成),周末稀疏 + 三段深色聚集
let seed=20261024;const rnd=()=>(seed=(seed*1103515245+12345)%2147483648)/2147483648;
{const BURST=[[7,10],[21,25],[40,44]],end=new Date(Date.UTC(2026,9,24)),f=[];
for(let w=0;w<53;w++)for(let d=0;d<7;d++){
const r=rnd();let lv=r<.34?0:r<.6?1:r<.82?2:3;
if(d>=5)lv=Math.min(lv,1);
if(BURST.some(([a,b])=>w>=a&&w<=b))lv=Math.min(4,lv+2);
const dt=new Date(end);dt.setUTCDate(dt.getUTCDate()+(d-5)+(w-52)*7);
f.push(`<i class="h${lv}" title="${dt.toISOString().slice(0,10)} · ${['无活动','1 次提交','3 次提交','6 次提交','10+ 次提交'][lv]}"></i>`);
}
$('#heat').innerHTML=f.join('');}
// 图库 masonry:span 由占位块渲染高度推算(真实构建时由照片宽高比计算,零 JS)
const wall=$('#wall');
function mason(){if(!wall||!wall.offsetHeight)return;for(const s of wall.children)s.style.gridRowEnd=`span ${Math.ceil((s.offsetHeight+14)/22)}`}
addEventListener('resize',mason);
// 灯箱:原生 dialog,←→ 切换,Esc 关闭
const lb=$('#lb'),lbimg=$('#lbimg'),lbexif=$('#lbexif'),shots=$$('.shot');let si=0;
function showLb(i){si=(i+shots.length)%shots.length;const s=shots[si];const [aw,ah]=s.style.getPropertyValue('--ar').split('/').map(Number);
const w=Math.min(innerWidth*.92,1100,innerHeight*.72*aw/ah);
lbimg.innerHTML=s.querySelector('svg').outerHTML;lbimg.firstElementChild.style.width=w+'px';
lbexif.innerHTML=s.querySelector('figcaption').innerHTML;lb.open||lb.showModal()}
shots.forEach((s,i)=>{s.addEventListener('click',()=>showLb(i));s.addEventListener('keydown',e=>{if(e.key==='Enter')showLb(i)})});
lb.addEventListener('keydown',e=>{if(e.key==='ArrowLeft'){e.preventDefault();showLb(si-1)}if(e.key==='ArrowRight'){e.preventDefault();showLb(si+1)}});
$('#lbprev').addEventListener('click',()=>showLb(si-1));
$('#lbnext').addEventListener('click',()=>showLb(si+1));
$('#lbclose').addEventListener('click',()=>lb.close());
// 书评页字号 dock:--prose-size 三档 18 / 20 / 22
let pfs=20;const setFs=v=>{pfs=Math.max(18,Math.min(22,v));$('.essay .prose').style.setProperty('--prose-size',pfs+'px')};
$$('.fsdock button').forEach(b=>b.addEventListener('click',()=>setFs(b.dataset.fs==='r'?20:pfs+ +b.dataset.fs)));
// 技术页 TOC 当前节高亮
const tocLinks=$$('.toc a'),tocMap=Object.fromEntries(tocLinks.map(a=>[a.hash.slice(1),a]));
const io=new IntersectionObserver(es=>{for(const e of es)if(e.isIntersecting){tocLinks.forEach(a=>a.classList.remove('on'));tocMap[e.target.id]?.classList.add('on')}},{rootMargin:'-15% 0px -70% 0px'});
$$('.tech .prose h2,.tech .prose h3').forEach(h=>io.observe(h));
$('#keysBtn').addEventListener('click',()=>$('#keys').showModal());
// 键盘:Ctrl+K / / 面板,1/2/3 路线,d 明暗,g 序列键(300ms 超时),? 帮助,~ 终端;输入聚焦时禁用
let gk='',gt;
addEventListener('keydown',e=>{
if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='k'){e.preventDefault();pal.open?pal.close():openPal();return}
if(e.ctrlKey||e.metaKey||e.altKey)return;
const t=e.target;
if(document.querySelector('dialog[open]')||t.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))return;
if(e.key==='/'){e.preventDefault();openPal();return}
if(e.key==='?'){$('#keys').showModal();return}
if(e.key==='`'||e.key==='~'){openTerm();return}
if(gk==='g'){clearTimeout(gt);gk='';const m={h:'home',e:'essay',t:'tech',g:'gallery',m:'moments'}[e.key];if(m)go('page',m);return}
if(e.key==='d')return flipScheme();
if(e.key==='1')return go('route','a');
if(e.key==='2')return go('route','b');
if(e.key==='3')return go('route','c');
if(e.key==='g'){gk='g';gt=setTimeout(()=>gk='',300)}
});
</script>
</body>
</html>