修改页面风格

This commit is contained in:
2026-06-18 11:07:23 +08:00 Unverified
parent 7c3679e885
commit 9d6de3158e
15 changed files with 1293 additions and 5617 deletions
+268 -5533
View File
File diff suppressed because it is too large Load Diff
+3 -38
View File
@@ -9,44 +9,9 @@
"preview": "astro preview",
"server": "astro dev"
},
"hexo": {
"version": "8.1.1"
},
"dependencies": {
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.6.0",
"astro": "^5.13.0",
"axios": "^1.15.0",
"cheerio": "^1.2.0",
"hexo": "^8.1.1",
"hexo-abbrlink": "^2.2.1",
"hexo-ai-summary-liushen": "^2.0.0",
"hexo-butterfly-swiper-lyx": "^1.0.2",
"hexo-deployer-git": "^4.0.0",
"hexo-douban": "^2.3.6",
"hexo-filter-mathjax": "^0.9.1",
"hexo-generator-archive": "^2.0.0",
"hexo-generator-category": "^2.0.0",
"hexo-generator-index": "^4.0.0",
"hexo-generator-search": "^2.4.3",
"hexo-generator-searchdb": "^1.5.0",
"hexo-generator-sitemap": "^3.0.1",
"hexo-generator-tag": "^2.0.0",
"hexo-indexnow": "^1.2.0",
"hexo-pretty-feed": "^2.1.0",
"hexo-renderer-ejs": "^2.0.0",
"hexo-renderer-marked": "^7.0.1",
"hexo-renderer-pug": "^3.0.0",
"hexo-renderer-stylus": "^3.0.1",
"hexo-safego": "^2.3.0",
"hexo-server": "^3.0.0",
"hexo-spoiler": "^1.7.4",
"hexo-wordcount": "^6.0.1",
"moment": "^2.30.1",
"node-fetch": "^3.3.2",
"p-limit": "^7.3.0",
"typesense": "^3.0.5",
"vite-plugin-require-transform": "^1.0.21",
"xml2js": "^0.6.2"
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.3",
"astro": "^6.4.8"
}
}
+64
View File
@@ -0,0 +1,64 @@
---
import type { BlogPost } from '@/lib/posts';
type TocItem = {
depth: number;
text: string;
id: string;
};
interface Props {
post: BlogPost;
toc: TocItem[];
relatedPosts: BlogPost[];
}
const { post, toc, relatedPosts } = Astro.props;
const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
---
<aside class="article-sidebar" aria-label="文章侧边栏">
<section class="sidebar-card article-author-card">
<img class="profile-avatar" src={avatar} alt="biss" loading="lazy" />
<div class="profile-name">biss</div>
<div class="article-author-meta">
<span>{post.dateText}</span>
{post.categories[0] && <a href={`/categories/${encodeURIComponent(post.categories[0])}/`}>{post.categories[0]}</a>}
</div>
</section>
{toc.length > 0 && (
<section class="sidebar-card toc-card">
<h2>
<span aria-hidden="true">☰</span>
文章目录
<em>{toc.length}</em>
</h2>
<nav class="toc-list" aria-label="文章目录">
{toc.map((item) => (
<a class={`toc-depth-${item.depth}`} href={`#${item.id}`}>{item.text}</a>
))}
</nav>
</section>
)}
<section class="sidebar-card same-category-card">
<h2>
<span aria-hidden="true">▣</span>
同类文章
<em>{relatedPosts.length}</em>
</h2>
{relatedPosts.length > 0 ? (
<div class="same-category-list">
{relatedPosts.map((item) => (
<a href={item.href}>
<strong>{item.title}</strong>
<time datetime={item.date.toISOString()}>{item.dateText}</time>
</a>
))}
</div>
) : (
<p class="sidebar-empty">这个分类下暂时没有其他文章。</p>
)}
</section>
</aside>
+207
View File
@@ -0,0 +1,207 @@
---
import type { BlogPost } from '@/lib/posts';
interface Props {
posts: BlogPost[];
tags: string[];
categories: string[];
}
const { posts, tags, categories } = Astro.props;
const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
const recentPosts = posts.slice(0, 5);
---
<aside class="home-sidebar" aria-label="首页侧边栏">
<section class="sidebar-card profile-card">
<img class="profile-avatar" src={avatar} alt="biss" loading="lazy" />
<div class="profile-name">biss</div>
<div class="profile-stats">
<a href="/archives/">
<span>文章</span>
<strong>{posts.length}</strong>
</a>
<a href="/tags/">
<span>标签</span>
<strong>{tags.length}</strong>
</a>
<a href="/categories/">
<span>分类</span>
<strong>{categories.length}</strong>
</a>
</div>
<a class="follow-button" href="https://github.com/bishshi" target="_blank" rel="noreferrer">
<span class="github-mark" aria-hidden="true">GitHub</span>
<span>Follow Me</span>
</a>
<div class="profile-links" aria-label="联系方式">
<a href="https://github.com/bishshi" target="_blank" rel="noreferrer" aria-label="GitHub">GitHub</a>
<a href="mailto:bishsh2006@gmail.com" aria-label="Email">Email</a>
</div>
</section>
<section class="sidebar-card notice-card">
<h2><span aria-hidden="true">!</span> 公告</h2>
<p>欢迎来自 <strong id="visitor-location">远方</strong> 的小友。</p>
<p>烧烤三件套,灵魂蘸料。</p>
<p id="visitor-distance">当前位置距离主约 <strong>473</strong> 公里!</p>
<p id="aside-weather">当前天气加载中...</p>
</section>
<div class="sticky-sidebar">
<section class="sidebar-card calendar-card">
<h2>日历</h2>
<div class="calendar-widget">
<div class="calendar-summary">
<strong id="calendar-week">第--周 周--</strong>
<span id="calendar-date">--</span>
<small id="calendar-solar">----年--月 第--天</small>
<small id="calendar-lunar">今天也很好</small>
</div>
<div class="calendar-grid" id="calendar-grid" aria-label="本月日历"></div>
</div>
</section>
<section class="sidebar-card schedule-card">
<h2>剩余时间</h2>
<div class="schedule-widget">
<div class="schedule-left">
<strong>距离除夕</strong>
<span id="new-year-days">--</span>
<small id="new-year-date">---- -- --</small>
</div>
<div class="schedule-bars">
<div class="schedule-row">
<span>本年</span>
<progress id="year-progress" max="100" value="0"></progress>
<em id="year-left">--天</em>
</div>
<div class="schedule-row">
<span>本月</span>
<progress id="month-progress" max="100" value="0"></progress>
<em id="month-left">--天</em>
</div>
<div class="schedule-row">
<span>本周</span>
<progress id="week-progress" max="100" value="0"></progress>
<em id="week-left">--天</em>
</div>
</div>
</div>
</section>
<section class="sidebar-card recent-card">
<h2>最新文章</h2>
<div class="sidebar-posts">
{recentPosts.map((post) => (
<a class="sidebar-post" href={post.href}>
{post.cover ? (
<img src={post.cover} alt="" loading="lazy" />
) : (
<span class="post-thumb-fallback" aria-hidden="true"></span>
)}
<span>
<strong>{post.title}</strong>
<time datetime={post.date.toISOString()}>{post.dateText}</time>
</span>
</a>
))}
</div>
</section>
</div>
</aside>
<script is:inline>
(() => {
const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const pad = (value) => String(value).padStart(2, '0');
const percent = (used, total) => Math.min(100, Math.max(0, (used / total) * 100));
const setText = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
function dayOfYear(date) {
const start = new Date(date.getFullYear(), 0, 1);
return Math.floor((date - start) / 86400000) + 1;
}
function weekOfYear(date) {
const firstDay = new Date(date.getFullYear(), 0, 1);
const pastDays = Math.floor((date - firstDay) / 86400000);
return Math.ceil((pastDays + firstDay.getDay() + 1) / 7);
}
function renderCalendar(now) {
setText('calendar-week', `第${weekOfYear(now)}周 ${dayNames[now.getDay()]}`);
setText('calendar-date', pad(now.getDate()));
setText('calendar-solar', `${now.getFullYear()}年${now.getMonth() + 1}月 第${dayOfYear(now)}天`);
setText('calendar-lunar', '万千风景,五历如一');
const grid = document.getElementById('calendar-grid');
if (!grid) return;
const year = now.getFullYear();
const month = now.getMonth();
const first = new Date(year, month, 1).getDay();
const days = new Date(year, month + 1, 0).getDate();
const previousDays = new Date(year, month, 0).getDate();
const cells = [];
for (let i = 0; i < 42; i += 1) {
const dayNumber = i - first + 1;
const isCurrent = dayNumber >= 1 && dayNumber <= days;
const displayDay = isCurrent
? dayNumber
: dayNumber < 1
? previousDays + dayNumber
: dayNumber - days;
const isToday = isCurrent && displayDay === now.getDate();
cells.push(`<span class="${isCurrent ? '' : 'muted'} ${isToday ? 'today' : ''}">${displayDay}</span>`);
}
grid.innerHTML = cells.join('');
}
function renderSchedule(now) {
const year = now.getFullYear();
const nextEve = new Date(year + 1, 1, 16);
const daysToEve = Math.max(0, Math.ceil((nextEve - now) / 86400000));
const yearStart = new Date(year, 0, 1);
const yearEnd = new Date(year + 1, 0, 1);
const monthStart = new Date(year, now.getMonth(), 1);
const monthEnd = new Date(year, now.getMonth() + 1, 1);
const weekStart = new Date(year, now.getMonth(), now.getDate() - ((now.getDay() + 6) % 7));
const weekEnd = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7);
setText('new-year-days', String(daysToEve));
setText('new-year-date', `${nextEve.getFullYear()}-${pad(nextEve.getMonth() + 1)}-${pad(nextEve.getDate())}`);
const rows = [
['year', yearStart, yearEnd],
['month', monthStart, monthEnd],
['week', weekStart, weekEnd],
];
rows.forEach(([name, start, end]) => {
const total = end - start;
const used = now - start;
const left = Math.max(0, Math.ceil((end - now) / 86400000));
const bar = document.getElementById(`${name}-progress`);
if (bar) bar.value = percent(used, total);
setText(`${name}-left`, `还剩${left}天`);
});
}
function initSidebar() {
const now = new Date();
renderCalendar(now);
renderSchedule(now);
}
initSidebar();
})();
</script>
<script is:inline src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script is:inline src="https://cdn.jsdmirror.com/gh/bishshi/welcomemessage/txmap.js"></script>
<script is:inline src="/js/weather.js"></script>
+14
View File
@@ -0,0 +1,14 @@
---
interface Props {
title: string;
subtitle?: string;
}
const { title, subtitle } = Astro.props;
---
<header class="page-header-card">
<p class="page-header-eyebrow">Bi's Blog</p>
<h1>{title}</h1>
{subtitle && <p class="page-header-subtitle">{subtitle}</p>}
</header>
+2
View File
@@ -13,6 +13,7 @@ export type BlogPost = MarkdownModule & {
tags: string[];
categories: string[];
summary: string;
cover?: string;
href: string;
};
@@ -79,6 +80,7 @@ export function getAllPosts(): BlogPost[] {
tags: asArray(module.frontmatter.tags),
categories: asArray(module.frontmatter.categories),
summary: makeSummary(module),
cover: typeof module.frontmatter.cover === 'string' ? module.frontmatter.cover : undefined,
href: `/posts/${slug}/`,
};
})
+5 -4
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PageHeaderCard from '@/components/PageHeaderCard.astro';
import { getSitePages } from '@/lib/pages';
export function getStaticPaths() {
@@ -14,10 +15,10 @@ const { Content } = page;
---
<BaseLayout title={page.title}>
<article class="article">
<header class="article-header">
<h1>{page.title}</h1>
</header>
<section class="content-wrap narrow page-with-card">
<PageHeaderCard title={page.title} />
</section>
<article class="article page-article">
<div class="prose">
<Content />
</div>
+2 -1
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PageHeaderCard from '@/components/PageHeaderCard.astro';
import { getAllPosts } from '@/lib/posts';
const posts = getAllPosts();
@@ -7,7 +8,7 @@ const posts = getAllPosts();
<BaseLayout title="归档">
<section class="content-wrap narrow">
<h1>归档</h1>
<PageHeaderCard title="归档" subtitle={`共 ${posts.length} 篇文章`} />
<div class="archive-list">
{posts.map((post) => (
<a href={post.href}>
+2 -1
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PageHeaderCard from '@/components/PageHeaderCard.astro';
import PostCard from '@/components/PostCard.astro';
import { getAllCategories, getAllPosts } from '@/lib/posts';
@@ -16,7 +17,7 @@ const posts = getAllPosts().filter((post) => post.categories.includes(category))
<BaseLayout title={category}>
<section class="content-wrap">
<h1>{category}</h1>
<PageHeaderCard title={category} subtitle={`共 ${posts.length} 篇文章`} />
<div class="post-list">
{posts.map((post) => <PostCard post={post} />)}
</div>
+2 -1
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PageHeaderCard from '@/components/PageHeaderCard.astro';
import { getAllCategories, getAllPosts } from '@/lib/posts';
const posts = getAllPosts();
@@ -8,7 +9,7 @@ const categories = getAllCategories();
<BaseLayout title="分类">
<section class="content-wrap narrow">
<h1>分类</h1>
<PageHeaderCard title="分类" subtitle={`共 ${categories.length} 个分类`} />
<div class="term-grid">
{categories.map((category) => (
<a href={`/categories/${encodeURIComponent(category)}/`}>
+14 -8
View File
@@ -1,9 +1,12 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import HomeSidebar from '@/components/HomeSidebar.astro';
import PostCard from '@/components/PostCard.astro';
import { getAllPosts } from '@/lib/posts';
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/posts';
const posts = getAllPosts();
const tags = getAllTags();
const categories = getAllCategories();
---
<BaseLayout>
@@ -14,13 +17,16 @@ const posts = getAllPosts();
<p>把折腾、学习和生活片段安静地收在这里。</p>
</div>
</section>
<section class="content-wrap">
<div class="section-heading">
<h2>最新文章</h2>
<a href="/archives/">查看全部</a>
</div>
<div class="post-list">
{posts.slice(0, 16).map((post) => <PostCard post={post} />)}
<section class="content-wrap home-layout">
<div class="home-main">
<div class="section-heading">
<h2>最新文章</h2>
<a href="/archives/">查看全部</a>
</div>
<div class="post-list">
{posts.slice(0, 16).map((post) => <PostCard post={post} />)}
</div>
</div>
<HomeSidebar posts={posts} tags={tags} categories={categories} />
</section>
</BaseLayout>
+86 -12
View File
@@ -1,4 +1,5 @@
---
import ArticleSidebar from '@/components/ArticleSidebar.astro';
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllPosts } from '@/lib/posts';
@@ -11,20 +12,93 @@ export function getStaticPaths() {
const { post } = Astro.props;
const { Content } = post;
const allPosts = getAllPosts();
function slugifyHeading(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/<[^>]+>/g, '')
.replace(/[`*_~[\]()]/g, '')
.replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function makeToc(raw: string) {
const used = new Map<string, number>();
let inFence = false;
return raw
.split(/\r?\n/)
.flatMap((line) => {
if (/^\s*```/.test(line)) {
inFence = !inFence;
return [];
}
if (inFence) return [];
const match = /^(#{1,3})\s+(.+?)\s*#*$/.exec(line);
if (!match) return [];
const text = match[2].replace(/<[^>]+>/g, '').trim();
if (!text) return [];
const baseId = slugifyHeading(text) || 'section';
const count = used.get(baseId) ?? 0;
used.set(baseId, count + 1);
return [{
depth: match[1].length,
text,
id: count === 0 ? baseId : `${baseId}-${count}`,
}];
});
}
const toc = makeToc(post.rawContent?.() ?? '');
const relatedPosts = allPosts
.filter((item) => item.slug !== post.slug)
.filter((item) => item.categories.some((category) => post.categories.includes(category)))
.slice(0, 8);
---
<BaseLayout title={post.title} description={post.summary}>
<article class="article">
<header class="article-header">
<time datetime={post.date.toISOString()}>{post.dateText}</time>
<h1>{post.title}</h1>
<div class="meta-list">
{post.categories.map((category) => <a href={`/categories/${encodeURIComponent(category)}/`}>{category}</a>)}
{post.tags.map((tag) => <a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>)}
<section class="post-layout">
<article class="article post-article">
<header class="article-header">
<time datetime={post.date.toISOString()}>{post.dateText}</time>
<h1>{post.title}</h1>
<div class="meta-list">
{post.categories.map((category) => <a href={`/categories/${encodeURIComponent(category)}/`}>{category}</a>)}
{post.tags.map((tag) => <a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>)}
</div>
</header>
<div class="prose" data-toc={JSON.stringify(toc)}>
<Content />
</div>
</header>
<div class="prose">
<Content />
</div>
</article>
</article>
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} />
</section>
</BaseLayout>
<script is:inline>
(() => {
const prose = document.querySelector('.prose[data-toc]');
if (!prose) return;
let toc = [];
try {
toc = JSON.parse(prose.dataset.toc || '[]');
} catch {
toc = [];
}
prose.querySelectorAll('h1, h2, h3').forEach((heading, index) => {
const item = toc[index];
if (item?.id && !heading.id) heading.id = item.id;
});
})();
</script>
+2 -1
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PageHeaderCard from '@/components/PageHeaderCard.astro';
import PostCard from '@/components/PostCard.astro';
import { getAllPosts, getAllTags } from '@/lib/posts';
@@ -16,7 +17,7 @@ const posts = getAllPosts().filter((post) => post.tags.includes(tag));
<BaseLayout title={tag}>
<section class="content-wrap">
<h1>#{tag}</h1>
<PageHeaderCard title={`#${tag}`} subtitle={`共 ${posts.length} 篇文章`} />
<div class="post-list">
{posts.map((post) => <PostCard post={post} />)}
</div>
+2 -1
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PageHeaderCard from '@/components/PageHeaderCard.astro';
import { getAllPosts, getAllTags } from '@/lib/posts';
const posts = getAllPosts();
@@ -8,7 +9,7 @@ const tags = getAllTags();
<BaseLayout title="标签">
<section class="content-wrap narrow">
<h1>标签</h1>
<PageHeaderCard title="标签" subtitle={`共 ${tags.length} 个标签`} />
<div class="term-grid">
{tags.map((tag) => (
<a href={`/tags/${encodeURIComponent(tag)}/`}>
+620 -17
View File
@@ -8,6 +8,11 @@
--accent: #0f766e;
--accent-strong: #14532d;
--shadow: 0 14px 45px rgba(58, 51, 38, 0.1);
--page-gutter: clamp(20px, 4vw, 64px);
--wide-width: 1440px;
--content-width: 1280px;
--article-width: 1080px;
--narrow-width: 1000px;
}
* {
@@ -33,45 +38,106 @@ a {
.site-header {
position: sticky;
top: 0;
top: 12px;
z-index: 10;
border-bottom: 1px solid rgba(222, 216, 204, 0.8);
background: rgba(255, 253, 248, 0.88);
backdrop-filter: blur(14px);
pointer-events: none;
}
.nav {
width: min(1120px, calc(100% - 32px));
min-height: 64px;
position: relative;
width: min(var(--wide-width), calc(100% - (var(--page-gutter) * 2)));
min-height: 58px;
margin: 0 auto;
padding: 0 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.56);
border-radius: 999px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0.2)),
rgba(255, 255, 255, 0.28);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.82),
inset 0 -18px 38px rgba(255, 255, 255, 0.16),
0 18px 54px rgba(44, 55, 63, 0.18);
backdrop-filter: blur(22px) saturate(180%);
-webkit-backdrop-filter: blur(22px) saturate(180%);
pointer-events: auto;
}
.nav::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.72), transparent 30%),
linear-gradient(90deg, rgba(255, 255, 255, 0.42), transparent 38%, rgba(255, 255, 255, 0.22));
opacity: 0.74;
pointer-events: none;
}
.nav::after {
content: "";
position: absolute;
top: 7px;
right: 16px;
left: 16px;
height: 1px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.78);
pointer-events: none;
}
.brand {
position: relative;
z-index: 1;
font-size: 1.05rem;
font-weight: 800;
}
.nav-links {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 18px;
gap: 6px;
color: var(--muted);
font-size: 0.94rem;
white-space: nowrap;
}
.nav-links a {
border: 1px solid transparent;
border-radius: 999px;
padding: 7px 12px;
transition:
color 0.2s ease,
background-color 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.nav-links a:hover,
.site-footer a:hover,
.section-heading a:hover {
color: var(--accent);
}
.nav-links a:hover {
border-color: rgba(255, 255, 255, 0.62);
background: rgba(255, 255, 255, 0.38);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.72),
0 8px 18px rgba(15, 118, 110, 0.1);
transform: translateY(-1px);
}
.hero {
width: min(1120px, calc(100% - 32px));
width: min(var(--wide-width), calc(100% - (var(--page-gutter) * 2)));
min-height: 360px;
margin: 0 auto;
display: grid;
@@ -99,12 +165,96 @@ a {
}
.content-wrap {
width: min(980px, calc(100% - 32px));
width: min(var(--content-width), calc(100% - (var(--page-gutter) * 2)));
margin: 0 auto 72px;
}
.content-wrap.narrow {
width: min(780px, calc(100% - 32px));
width: min(var(--narrow-width), calc(100% - (var(--page-gutter) * 2)));
}
.page-header-card {
position: relative;
overflow: hidden;
margin: 40px 0 26px;
border: 1px solid rgba(255, 255, 255, 0.58);
border-radius: 8px;
padding: clamp(26px, 5vw, 44px);
background:
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.78), transparent 36%),
linear-gradient(135deg, rgba(255, 255, 255, 0.58), rgba(238, 250, 243, 0.36)),
rgba(255, 253, 248, 0.72);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.82),
0 18px 48px rgba(58, 51, 38, 0.12);
backdrop-filter: blur(18px) saturate(165%);
-webkit-backdrop-filter: blur(18px) saturate(165%);
}
.page-header-card::after {
content: "";
position: absolute;
top: 10px;
right: 18px;
left: 18px;
height: 1px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.76);
}
.page-header-eyebrow {
margin: 0 0 8px;
color: var(--accent);
font-size: 0.86rem;
font-weight: 700;
}
.page-header-card h1,
.content-wrap .page-header-card h1 {
margin: 0;
color: #20242a;
font-size: clamp(2rem, 5vw, 3.6rem);
line-height: 1.08;
}
.page-header-subtitle {
max-width: 620px;
margin: 12px 0 0;
color: var(--muted);
font-size: 1rem;
}
.page-with-card {
margin-bottom: 0;
}
.page-article {
margin-top: 0;
}
.post-layout {
width: min(var(--wide-width), calc(100% - (var(--page-gutter) * 2)));
margin: 40px auto 84px;
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
align-items: start;
gap: 28px;
}
.post-layout .article {
width: 100%;
margin: 0;
}
.home-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
align-items: start;
gap: 24px;
}
.home-main {
min-width: 0;
}
.section-heading {
@@ -150,6 +300,396 @@ a {
color: var(--muted);
}
.home-sidebar {
display: grid;
gap: 20px;
}
.sticky-sidebar {
position: sticky;
top: 84px;
display: grid;
gap: 20px;
}
.sidebar-card {
overflow: hidden;
border: 1px solid rgba(222, 216, 204, 0.72);
border-radius: 8px;
padding: 18px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.72), rgba(242, 251, 237, 0.78)),
rgba(255, 253, 248, 0.76);
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
}
.sidebar-card h2 {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 14px;
color: #4d5258;
font-size: 1.08rem;
font-weight: 700;
}
.sidebar-card h2 em {
margin-left: auto;
min-width: 28px;
border-radius: 999px;
padding: 2px 8px;
color: #6d9cc2;
font-size: 0.78rem;
font-style: normal;
text-align: center;
background: rgba(102, 200, 245, 0.18);
}
.profile-card {
text-align: center;
}
.profile-avatar {
width: 112px;
height: 112px;
margin: 0 auto 14px;
border-radius: 999px;
object-fit: cover;
box-shadow: 0 12px 30px rgba(15, 118, 110, 0.18);
}
.profile-name {
margin-bottom: 16px;
font-family: Georgia, "Times New Roman", serif;
color: #585b5e;
font-size: 1.9rem;
letter-spacing: 0;
}
.profile-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-bottom: 18px;
}
.profile-stats a {
display: grid;
gap: 3px;
color: var(--muted);
}
.profile-stats strong {
color: #2f4358;
font-size: 1.22rem;
font-weight: 600;
}
.profile-stats a:first-child strong,
.notice-card strong,
.schedule-left span,
.calendar-summary span {
color: #66c8f5;
}
.follow-button {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 44px;
border-radius: 8px;
color: #fff;
font-weight: 700;
background: #3eb8be;
transition:
transform 0.2s ease,
background-color 0.2s ease;
}
.follow-button:hover {
color: #fff;
transform: translateY(-1px);
background: #279fa5;
}
.github-mark {
font-size: 0.9rem;
}
.profile-links {
display: flex;
justify-content: center;
gap: 18px;
margin-top: 16px;
color: #3f5368;
font-weight: 700;
}
.notice-card p {
margin: 0.65em 0;
color: #596068;
line-height: 1.9;
}
.calendar-widget,
.schedule-widget {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 16px;
align-items: center;
}
.calendar-summary,
.schedule-left {
display: grid;
gap: 4px;
text-align: center;
}
.calendar-summary strong,
.schedule-left strong {
color: #4b5560;
font-size: 0.9rem;
}
.calendar-summary span,
.schedule-left span {
font-size: 2.25rem;
line-height: 1.1;
font-weight: 800;
}
.calendar-summary small,
.schedule-left small {
color: var(--muted);
font-size: 0.78rem;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 5px 7px;
color: #8aa6be;
font-size: 0.78rem;
text-align: center;
}
.calendar-grid span {
display: grid;
place-items: center;
min-height: 20px;
border-radius: 999px;
}
.calendar-grid .muted {
opacity: 0.45;
}
.calendar-grid .today {
color: #fff;
background: #66c8f5;
}
.schedule-bars {
display: grid;
gap: 8px;
}
.schedule-row {
display: grid;
grid-template-columns: 34px 1fr 64px;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 0.78rem;
}
.schedule-row progress {
width: 100%;
height: 18px;
overflow: hidden;
border: 0;
border-radius: 4px;
background: rgba(102, 200, 245, 0.18);
}
.schedule-row progress::-webkit-progress-bar {
background: rgba(102, 200, 245, 0.18);
}
.schedule-row progress::-webkit-progress-value {
background: rgba(102, 200, 245, 0.58);
}
.schedule-row progress::-moz-progress-bar {
background: rgba(102, 200, 245, 0.58);
}
.schedule-row em {
font-style: normal;
}
.sidebar-posts {
display: grid;
}
.sidebar-post {
display: grid;
grid-template-columns: 70px minmax(0, 1fr);
gap: 12px;
align-items: center;
padding: 12px 0;
border-top: 1px dashed rgba(104, 111, 120, 0.28);
}
.sidebar-post:first-child {
border-top: 0;
padding-top: 0;
}
.sidebar-post img,
.post-thumb-fallback {
width: 70px;
aspect-ratio: 1;
border-radius: 6px;
object-fit: cover;
background: linear-gradient(135deg, rgba(62, 184, 190, 0.32), rgba(102, 200, 245, 0.34));
}
.sidebar-post strong {
display: -webkit-box;
overflow: hidden;
color: #4d5258;
font-size: 0.94rem;
font-weight: 500;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.sidebar-post time {
display: block;
margin-top: 3px;
}
.article-sidebar {
position: sticky;
top: 90px;
display: grid;
gap: 20px;
}
.article-author-card {
text-align: center;
background:
radial-gradient(circle at 50% 0%, rgba(102, 200, 245, 0.32), transparent 42%),
linear-gradient(135deg, rgba(255, 255, 255, 0.58), rgba(214, 238, 255, 0.3)),
rgba(255, 253, 248, 0.62);
}
.article-author-card .profile-avatar {
width: 88px;
height: 88px;
}
.article-author-card .profile-name {
margin-bottom: 8px;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 1.45rem;
font-weight: 800;
}
.article-author-meta {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
color: var(--muted);
font-size: 0.86rem;
}
.article-author-meta a {
color: var(--accent);
}
.toc-list,
.same-category-list {
display: grid;
gap: 6px;
}
.toc-list {
position: relative;
padding-top: 10px;
}
.toc-list::before {
content: "";
position: absolute;
top: 0;
right: 0;
left: 0;
height: 5px;
border-radius: 999px;
background-image: radial-gradient(circle, rgba(102, 200, 245, 0.72) 2px, transparent 2px);
background-size: 14px 5px;
}
.toc-list a,
.same-category-list a {
border-radius: 8px;
padding: 9px 11px;
color: #50637a;
line-height: 1.45;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.toc-list a:hover,
.same-category-list a:hover {
color: #1f618d;
background: rgba(102, 200, 245, 0.16);
transform: translateX(2px);
}
.toc-depth-2 {
padding-left: 22px !important;
}
.toc-depth-3 {
padding-left: 34px !important;
font-size: 0.9rem;
}
.same-category-list a {
display: grid;
gap: 3px;
border-top: 1px dashed rgba(104, 111, 120, 0.22);
}
.same-category-list a:first-child {
border-top: 0;
}
.same-category-list strong {
display: -webkit-box;
overflow: hidden;
font-size: 0.94rem;
font-weight: 600;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.sidebar-empty {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
time {
color: var(--muted);
font-size: 0.9rem;
@@ -172,13 +712,20 @@ time {
}
.article {
width: min(820px, calc(100% - 32px));
width: min(var(--article-width), calc(100% - (var(--page-gutter) * 2)));
margin: 64px auto 84px;
border: 1px solid var(--line);
border: 1px solid rgba(255, 255, 255, 0.58);
border-radius: 8px;
padding: clamp(24px, 5vw, 48px);
background: rgba(255, 253, 248, 0.93);
box-shadow: var(--shadow);
background:
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.72), transparent 34%),
linear-gradient(145deg, rgba(255, 255, 255, 0.62), rgba(226, 248, 241, 0.35)),
rgba(255, 253, 248, 0.68);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.82),
0 24px 64px rgba(44, 55, 63, 0.16);
backdrop-filter: blur(18px) saturate(165%);
-webkit-backdrop-filter: blur(18px) saturate(165%);
}
.article-header {
@@ -194,6 +741,7 @@ time {
}
.prose :where(h2, h3, h4) {
scroll-margin-top: 110px;
margin-top: 2em;
line-height: 1.35;
}
@@ -256,7 +804,7 @@ time {
}
.site-footer {
width: min(1120px, calc(100% - 32px));
width: min(var(--wide-width), calc(100% - (var(--page-gutter) * 2)));
margin: 0 auto;
padding: 32px 0 44px;
display: flex;
@@ -265,25 +813,80 @@ time {
color: var(--muted);
}
@media (min-width: 960px) {
.post-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.post-layout {
grid-template-columns: 1fr;
}
.article-sidebar {
position: static;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.toc-card {
grid-column: 1 / -1;
}
.home-layout {
grid-template-columns: 1fr;
}
.home-sidebar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sticky-sidebar {
position: static;
}
}
@media (max-width: 720px) {
:root {
--page-gutter: 16px;
}
.nav {
min-height: auto;
padding: 14px 0;
padding: 12px 14px;
align-items: flex-start;
flex-direction: column;
border-radius: 24px;
}
.nav-links {
width: 100%;
overflow-x: auto;
gap: 14px;
gap: 6px;
padding-bottom: 2px;
}
.nav-links a {
flex: 0 0 auto;
}
.hero {
min-height: 300px;
}
.home-sidebar {
grid-template-columns: 1fr;
}
.calendar-widget,
.schedule-widget {
grid-template-columns: 1fr;
}
.article-sidebar {
grid-template-columns: 1fr;
}
.archive-list a {
grid-template-columns: 1fr;
gap: 2px;