/*******************************************************************************
CUSTOM STYLES

Add your custom CSS styles here to override or extend the default styles.
*******************************************************************************/

/*
 * 线稿插画（文件名以 -ink.webp 结尾）
 *
 * 尺寸只能在这里控制：tufted.css 里 `img { width: auto }` 会盖掉 HTML 上的
 * width 属性，所以 .typ 里写多宽都不算数，实际宽度由下面这条 max-width 决定。
 * 居中不用管——桌面端靠 `section > img` 的 left/transform，移动端靠 margin auto，
 * 两种方式都跟具体宽度无关。
 */
img[src$="-ink.webp"] {
	max-width: min(320px, 100%);
}

/*
 * 这类图是「透明底 + 墨色线条」，浅色模式下直接就对。暗色模式下线条几乎看不见，
 * 所以反相一次让线条变浅；底色是透明的，反相不会带出白框。
 * 下面的选择器条件跟 theme.css 里判断暗色的条件完全一致，别单独改一边。
 */
html[data-theme="dark"] img[src$="-ink.webp"],
html:not([data-theme]) img[src$="-ink.webp"] {
	filter: invert(1);
}

@media (prefers-color-scheme: light) {
	html:not([data-theme]) img[src$="-ink.webp"] {
		filter: none;
	}
}

/*
 * 线稿下面的一行配文。
 * 注意插图并没有在正文栏里居中：Typst 会把 `#image` 包进 <p>，
 * tufted.css 里那条居中的 `section > img` 只认 section 的直接子元素，命不中，
 * 所以图是贴着正文栏左边、宽度由上面那条 max-width 决定的。
 * 配文要跟图对齐，就得用同一个宽度再在里面居中——别改成整栏 text-align: center，
 * 那样文字会跑到图的右边去。两处宽度必须保持一致。
 */
.ink-caption {
	max-width: min(320px, 100%);
	text-align: center;
	font-style: italic;
	opacity: 0.75;
}


/*******************************************************************************
动效

原则：只给结构层级（标题、插图、博客条目）加入场动效，正文段落保持不动——
读到一半的段落跟着动会干扰阅读，这跟 Tufte 那套「不打扰读者」是冲突的。

初始隐藏状态全部挂在 html.js-anim 下面，这个类由 reveal.js 在首次绘制前同步加上。
没有 JS、JS 出错、或者用户开了「减少动态效果」时类都不存在，页面就是普通静态页。
*******************************************************************************/

@keyframes rise-in {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* --- 载入时：页眉、大标题、署名依次浮上来 --- */

/* 标题层级说明：Typst 导出的 HTML 没有 <h1>，`= 标题` 变成 <h2>，
   `== 小节` 变成 <h3>。所以每页第一个 <h2> 才是页面大标题。 */

html.js-anim header {
	animation: rise-in 520ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

html.js-anim article section > h2:first-of-type {
	animation: rise-in 520ms cubic-bezier(0.22, 0.61, 0.36, 1) 70ms both;
}

html.js-anim .article-byline {
	animation: rise-in 520ms cubic-bezier(0.22, 0.61, 0.36, 1) 140ms both;
}

/* --- 滚动显现 --- */
/* ⚠️ 下面这组选择器必须和 reveal.js 里的 REVEAL_SELECTOR 完全一致 */

html.js-anim article section > h2:not(:first-of-type),
html.js-anim article section > h3,
html.js-anim article section > h4,
html.js-anim article section > figure,
html.js-anim article section > img,
html.js-anim .blog-entry {
	opacity: 0;
	transform: translateY(14px);
	transition:
		opacity 600ms cubic-bezier(0.22, 0.61, 0.36, 1),
		transform 600ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

html.js-anim .is-revealed {
	opacity: 1;
	transform: none;
}

/* --- 主题切换：颜色渐变而不是硬切 --- */
/* theme-anim-ready 要等首帧之后才加，否则一进页面就会先渐变一次 */

html.theme-anim-ready body,
html.theme-anim-ready pre,
html.theme-anim-ready table,
html.theme-anim-ready th,
html.theme-anim-ready td,
html.theme-anim-ready .blog-entry {
	transition:
		background-color 240ms ease,
		color 240ms ease,
		border-color 240ms ease;
}

/* 线稿图在两个主题之间是靠反相切换的，跟着一起渐变，别硬跳 */
html.theme-anim-ready img[src$="-ink.webp"] {
	transition: filter 240ms ease;
}

/* --- 博客列表：悬停时标题轻微右移 --- */
/* 动内部的 a 而不是 .blog-entry 本身，因为 .blog-entry 的 transform 被显现动效占着了 */

.blog-entry-content a:any-link {
	display: inline-block;
	transition: transform 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.blog-entry:hover .blog-entry-content a:any-link {
	transform: translateX(5px);
}

/* --- 用户要求减少动效时，全部关掉 --- */
/* reveal.js 已经会直接不启用，这里再兜一层，防止用户中途改系统设置 */

@media (prefers-reduced-motion: reduce) {
	html.js-anim header,
	html.js-anim article section > h2:first-of-type,
	html.js-anim .article-byline {
		animation: none;
	}

	html.js-anim article section > h2:not(:first-of-type),
	html.js-anim article section > h3,
	html.js-anim article section > h4,
	html.js-anim article section > figure,
	html.js-anim article section > img,
	html.js-anim .blog-entry {
		opacity: 1;
		transform: none;
	}

	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}


/*******************************************************************************
双语模块（bilingual）

配合 tufted-lib/bilingual.typ 与 assets/lang-toggle.js 使用。
显示状态完全由 CSS 驱动：JS 只维护 <html data-bilingual-mode="zh|en">
和导航栏按钮的文案；切换是页面级全局的，所有双语块与行内片段一起变。

无 JS 的降级：导航按钮隐藏，按 .typ 里 default: 指定的语言静态展示——
此时走 html:not([data-bilingual-mode]) 这组选择器，读每个块自己的
data-lang-mode，与 JS 控制的全局状态互不冲突。
*******************************************************************************/

/* --- 块本体：对齐正文栏宽，上下间距与段落一致 --- */

.bilingual {
	width: 55%;
	margin: 1.4rem 0;
}

@media (max-width: 760px) {
	.bilingual {
		width: 100%;
	}
}

/* --- 导航栏全局切换按钮：在深色模式按钮左边 --- */
/* 默认隐藏；页面含双语内容时由 lang-toggle.js 加 html.has-bilingual 显示。
   按钮文案（中 / EN）也由 JS 按当前语言填。 */

.lang-toggle-btn {
	display: none;
	align-items: center;
	justify-content: center;
	height: 2.4rem;
	padding: 0 0.55em;
	background: transparent;
	border: none;
	border-radius: 0.375rem;
	cursor: pointer;
	margin-left: auto; /* 把自己和主题按钮一起推到导航栏右侧 */
	margin-right: 0.2em;
	font-family: inherit;
	font-size: 1.05rem;
	letter-spacing: 0.04em;
	line-height: 1;
	color: #374151;
	transition: background-color 0.15s ease;
}

html.has-bilingual .lang-toggle-btn {
	display: flex;
}

/* 自动右移交给语言按钮，主题按钮跟在它左边即可 */
.theme-toggle-btn {
	margin-left: 0;
}

.lang-toggle-btn:hover {
	background: var(--highlight-weak, rgba(128, 128, 128, 0.2));
}

.lang-toggle-btn:focus-visible {
	outline: 2px solid var(--highlight-strong, rgba(128, 128, 128, 0.4));
	outline-offset: 2px;
}

html[data-theme="dark"] .lang-toggle-btn {
	color: #d1d5db;
}

/* --- 只显示当前语言的那一份 --- */
/* 每组两条：html[data-bilingual-mode] 是 JS 维护的全局状态；
   html:not(...) 下退回各块自己的 data-lang-mode（Typst 的 default 参数） */

/* 行内双语片段（bilingual-inline）：规则与块级一致，
   无 JS 时默认只显示中文（与站点默认语言一致）。 */

html[data-bilingual-mode="zh"] .bilingual-inline .bi-en,
html:not([data-bilingual-mode]) .bilingual-inline .bi-en {
	display: none;
}

html[data-bilingual-mode="en"] .bilingual-inline .bi-zh {
	display: none;
}

html[data-bilingual-mode="zh"] .bilingual-en,
html:not([data-bilingual-mode]) .bilingual[data-lang-mode="zh"] .bilingual-en {
	display: none;
}

html[data-bilingual-mode="en"] .bilingual-zh,
html:not([data-bilingual-mode]) .bilingual[data-lang-mode="en"] .bilingual-zh {
	display: none;
}

/* --- 面板本身要长得像一个段落 --- */
/*
 * 只有一段文字时，Typst 不会给它包 <p>，文字是直接躺在 .bilingual-panel 里的裸文本，
 * 拿不到 tufte 给 p 的字号和行高，会比周围正文明显小一号（14px vs 19.6px）。
 * 所以这里把 p 的排版参数抄一份到面板上。
 * 面板里如果是 <ul>、<pre> 这些块元素，它们有自己的规则，不受影响。
 */

.bilingual-panel {
	font-size: 1.4rem;
	line-height: 2.2rem;
	text-align: justify;
	text-justify: inter-ideograph;
}

/* --- 面板内部：去掉首末元素的外边距，让块间距和普通段落一致 --- */

.bilingual-panel > :first-child {
	margin-top: 0;
}

.bilingual-panel > :last-child {
	margin-bottom: 0;
}

/* --- 主题切换时颜色平滑过渡（与上方 theme-anim-ready 规则同速） --- */

html.theme-anim-ready .lang-toggle-btn,
html.theme-anim-ready .bilingual-en {
	transition:
		background-color 240ms ease,
		color 240ms ease,
		border-color 240ms ease;
}

/* --- GitHub 贡献热力图 --- */

#github-graph {
	width: 55%;
	margin: 1.5rem 0;
	overflow-x: auto;
	-webkit-overflow-scrolling: touch;
}

#github-graph .gh-loading {
	font-size: 0.9rem;
	opacity: 0.5;
}

#github-graph .gh-chart {
	width: 100%;
	max-width: 720px;
	height: auto;
}

/* 摆放徽章的边栏目前会遮盖右半边贡献图（J 之后的月份），
	用 55% 限宽让图留在正文栏，小屏边栏下沉后再放开全宽。 */
@media (max-width: 760px) {
	#github-graph {
		width: 100%;
	}
}

@media (max-width: 768px) {
	#github-graph .gh-chart {
		min-width: 660px;
	}
}

/* --- Margin note 层叠修复 --- */
/* reveal.js 给同级 h3/img 等加 transform，形成新层叠上下文，
   会盖住浮动到右侧的 marginnote，导致其中链接无法点击。 */
.marginnote {
	z-index: 1;
}

/* --- 旅行地图：线稿地球 --- */
/*
 * 配合 assets/travel-globe.js。颜色全部走 currentColor + opacity，
 * 这样明暗两个主题都不用单独配色（正文色由 theme.css 的 --theme-text 决定）。
 * 强调色只有标记点用到，那个绿色在两种背景上都够清楚，所以直接写死。
 */

/*
 * 容器要显式限成 55%，跟正文栏对齐。不写的话 div 会占满 article 的整个宽度
 * （含右边的 marginnote 栏），里面 margin: auto 的球就跑到页面正中去了。
 * 这个 55% 与 tufted.css 里正文块的宽度一致。
 */
#travel-globe {
	width: 55%;
	margin: 1.5rem 0;
}

@media (max-width: 760px) {
	#travel-globe {
		width: 100%;
	}
}

.globe-svg {
	display: block;
	width: 100%;
	max-width: 460px;
	height: auto;
	margin: 0 auto;
	cursor: grab;
	touch-action: none; /* 触屏上拖球时不要把页面一起滚走 */
	-webkit-user-select: none;
	user-select: none;
}

.globe-svg.is-dragging {
	cursor: grabbing;
}

.globe-ocean {
	fill: currentColor;
	opacity: 0.035;
}

.globe-graticule {
	fill: none;
	stroke: currentColor;
	stroke-width: 0.6;
	opacity: 0.2;
}

.globe-land {
	fill: none;
	stroke: currentColor;
	stroke-width: 1.1;
	stroke-linejoin: round;
	opacity: 0.75;
}

.globe-rim {
	fill: none;
	stroke: currentColor;
	stroke-width: 1;
	opacity: 0.35;
}

.globe-marker.is-hidden {
	display: none;
}

.globe-marker-halo {
	fill: #2f9e44;
	opacity: 0.22;
}

.globe-marker-dot {
	fill: #2f9e44;
}

/* 现居地（上海）：点大一圈，再加一道描边区分开 */
.globe-marker.is-home .globe-marker-dot {
	stroke: var(--theme-bg, #fffff8);
	stroke-width: 1.5;
}

.globe-marker.is-home .globe-marker-label {
	font-weight: 600;
}

/* 引线：标签被避让推离原点时才显示，见 travel-globe.js 里的引线阈值 */
.globe-marker-leader {
	stroke: currentColor;
	stroke-width: 0.6;
	opacity: 0.35;
	pointer-events: none; /* 别挡住标记的点击 */
}

/* --- 写过游记的地方：多一圈，可点开 --- */

.globe-marker.has-article {
	cursor: pointer;
}

.globe-marker-ring {
	fill: none;
	stroke: #2f9e44;
	stroke-width: 1.2;
	opacity: 0.55;
	transition:
		opacity 160ms ease,
		stroke-width 160ms ease;
}

/* 透明的大圆，只为好点中；真正的点半径才 3.5px */
.globe-marker-hit {
	fill: transparent;
	stroke: none;
}

/* 鼠标移到点上、或移到边栏对应标题上时的高亮 */
.globe-marker.is-active .globe-marker-ring {
	opacity: 1;
	stroke-width: 2;
}

.globe-marker.is-active .globe-marker-halo {
	opacity: 0.4;
}

.globe-marker.is-active .globe-marker-label {
	font-weight: 600;
}

.globe-marker-label {
	fill: currentColor;
	font-family: inherit;
	font-size: 14px;
	opacity: 0.9;
	/* 标签压在陆地轮廓上时，靠一圈同底色描边把字衬出来 */
	paint-order: stroke;
	stroke: var(--theme-bg, #fffff8);
	stroke-width: 3px;
	stroke-linejoin: round;
}

/*
 * ⚠️ 这条必须放在上面那条之后。手机上 SVG 被缩到约 315px，而 viewBox 是 520，
 * 里面的字会跟着缩到实际 8px 左右，太小了，这里按缩放比放大补回去。
 * 写在前面 #travel-globe 那个 @media 里是不生效的——同优先级下后面的规则会盖掉它。
 *
 * travel-globe.js 的标签避让用 getComputedTextLength() 量实际宽度，
 * 所以字号一变排布会自动重算，不用改 JS。
 */
@media (max-width: 760px) {
	.globe-marker-label {
		font-size: 21px;
	}
}

/* --- 地名按钮：点一下把那个地方转到正面 --- */

.globe-legend {
	display: flex;
	flex-wrap: wrap;
	gap: 0.4rem;
	justify-content: center;
	margin-top: 0.9rem;
}

.globe-legend-item {
	padding: 0.2rem 0.7rem;
	border: 1px solid var(--theme-table-border, #ccc);
	border-radius: 2em;
	background: transparent;
	color: inherit;
	font-family: inherit;
	font-size: 1.2rem;
	line-height: 1.6;
	cursor: pointer;
	transition:
		background-color 0.15s ease,
		border-color 0.15s ease;
}

.globe-legend-item:hover {
	background: var(--highlight-weak, rgba(128, 128, 128, 0.15));
	border-color: var(--theme-text, #111);
}

/* 现居地在列表里也标出来 */
.globe-legend-item.is-home {
	border-color: #2f9e44;
	font-weight: 600;
}

/* --- 边栏的游记列表 --- */
/*
 * 这些 <a> 由 content/Travel/index.typ 生成，带 data-globe-place；
 * travel-globe.js 靠它跟地球上的点对应起来，鼠标移上来地球会转过去。
 * 注意它们在 marginnote 这个 <span> 里，所以只能是行内元素 + display: block，
 * 不能写成 <div>/<li>（会把边注整个顶空，见 tufted-lib/layout.typ 的说明）。
 */

/* padding-left 和 text-indent 取相反数做悬挂缩进：标题折行时，
   第二行会跟第一行的文字对齐，而不是顶到圆点下面。
   这个值要和 ::before 的 width 一致。 */
.trip-link {
	display: block;
	margin-top: 0.35em;
	padding-left: 1.25em;
	text-indent: -1.25em;
	transition: color 160ms ease;
}

/* 行首那个小圆点，跟地球上的标记同色，暗示两边是一回事 */
.trip-link::before {
	content: "○";
	display: inline-block;
	width: 1.25em; /* 圆点本身约 0.85em，余下的就是和标题之间的空隙 */
	text-indent: 0;
	color: #2f9e44;
	font-size: 0.85em;
	text-decoration: none; /* 别跟着链接一起加下划线 */
}

.trip-link.is-active::before,
.trip-link:hover::before {
	content: "●";
}

.globe-legend-item:focus-visible {
	outline: 2px solid var(--highlight-strong, rgba(128, 128, 128, 0.4));
	outline-offset: 2px;
}

/* --- GitHub 组织徽章 --- */

.gh-orgs {
	display: flex;
	gap: 0.5rem;
	flex-wrap: wrap;
	margin: 0.75rem 0 1.5rem;
}

.gh-org-badge {
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
	padding: 0.25rem 0.65rem;
	border: 1px solid var(--border-color, #d0d7de);
	border-radius: 2em;
	font-size: 0.85rem;
	line-height: 1.4;
	color: var(--theme-text, #1f2328);
	text-decoration: none;
	background: var(--bg-weak, #f6f8fa);
	transition: background-color 0.15s ease, border-color 0.15s ease;
}

.gh-org-badge:hover {
	background: var(--highlight-weak, rgba(128, 128, 128, 0.15));
	border-color: var(--border-strong, #8c959f);
}

.gh-org-avatar {
	width: 20px;
	height: 20px;
	border-radius: 50%;
	flex-shrink: 0;
}

html[data-theme="dark"] .gh-org-badge {
	border-color: #30363d;
	background: #161b22;
	color: #e6edf3;
}

html[data-theme="dark"] .gh-org-badge:hover {
	background: #21262d;
	border-color: #8b949e;
}

/* ============================================================
   正文衬线字体：Times New Roman（中文回退到系统中文字体）
   ============================================================ */
body {
	font-family: "Times New Roman", Times, "Songti SC", "SimSun", serif;
}

code, pre, kbd, samp {
	font-family: "SF Mono", Menlo, Consolas, "Noto Sans Mono CJK SC", monospace;
}
