This commit is contained in:
SepComet
2026-05-04 21:56:20 +08:00
commit 1ab1717e64
68 changed files with 10667 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
---
import { site } from '../config';
---
<footer class="footer">
<div class="container">
<div class="footer-card">
<div>
<p class="eyebrow">Built for public sharing</p>
<p class="footer-meta">
<span>© {new Date().getFullYear()} {site.name}</span>
<span>·</span>
<span>Astro static site</span>
</p>
</div>
<div class="inline-links">
<a class="section-link" href="/logs">查看开发日志</a>
<a class="section-link" href="/projects">浏览项目</a>
</div>
</div>
</div>
</footer>
+88
View File
@@ -0,0 +1,88 @@
---
const heatmapLevels = [
'#0f172a',
'#13233e',
'#1d4ed8',
'#2563eb',
'#22c55e',
];
const heatmap = [
0, 1, 2, 0, 3, 1, 4, 2, 0, 1,
1, 3, 0, 2, 1, 4, 2, 1, 0, 3,
2, 0, 1, 3, 4, 1, 0, 2, 1, 3,
0, 1, 4, 2, 0, 3, 1, 2, 4, 1,
1, 2, 0, 3, 1, 4, 2, 0, 1, 2,
0, 4, 1, 2, 3, 0, 2, 1, 4, 1,
2, 1, 3, 0, 2, 1, 4, 2, 0, 1,
];
const recentActivities = [
{
title: 'homepage landing',
detail: '重建首页骨架与模块层级',
time: '今天',
},
{
title: 'design spec',
detail: '完成工程编辑部风格设计文档',
time: '今天',
},
{
title: 'content seeds',
detail: '补齐示例日志、项目与分享数据',
time: '今天',
},
];
---
<section class="panel activity-panel">
<div class="section-heading" style="margin-bottom: 1rem;">
<div>
<span class="eyebrow">Gitea Activity</span>
<h2>活跃度概览</h2>
<p>首版先用静态面板承载未来的热力图与活动流位置,后续再接真实 API。</p>
</div>
<a class="section-link" href="https://gitea.sepcomet.xyz" target="_blank" rel="noreferrer">
打开 Gitea
</a>
</div>
<div class="activity-grid">
<div>
<p class="activity-copy">贡献热力图区域预留给未来的客户端渲染模块。当前先用静态密度块确认布局节奏与深色模块层级。</p>
<div class="heatmap" aria-label="示例贡献热力图">
{
heatmap.map((level) => (
<span
class="heatmap-cell"
style={`background:${heatmapLevels[level]};`}
aria-hidden="true"
/>
))
}
</div>
<div class="heatmap-legend">
<span>贡献强度</span>
<span class="legend-scale" aria-hidden="true">
{heatmapLevels.map((color) => <span style={`background:${color};`} />)}
</span>
<span>低 → 高</span>
</div>
</div>
<div class="activity-list">
{
recentActivities.map((item) => (
<article class="activity-item">
<strong class="mono">{item.title}</strong>
<p class="activity-copy">{item.detail}</p>
<p class="activity-meta" style="margin-top: 0.55rem;">
<span>{item.time}</span>
</p>
</article>
))
}
</div>
</div>
</section>
+53
View File
@@ -0,0 +1,53 @@
---
import { site } from '../config';
interface Props {
currentPath?: string;
}
const { currentPath = '/' } = Astro.props;
const navItems = [
{ href: '/', label: 'Home' },
{ href: '/logs', label: 'Logs' },
{ href: '/projects', label: 'Projects' },
{ href: '/shares', label: 'Shares' },
];
const isActive = (href: string) =>
href === '/'
? currentPath === '/'
: currentPath === href || currentPath.startsWith(`${href}/`);
---
<header class="site-header">
<a class="site-brand" href="/" aria-label="返回首页">
<span class="site-brand__mark" aria-hidden="true">SC</span>
<span class="site-brand__text">
<span class="site-brand__title">{site.name}</span>
<span class="site-brand__subtitle">{site.tagline}</span>
</span>
</a>
<nav class="site-nav" aria-label="主导航">
{
navItems.map((item) => (
<a
class="nav-link"
href={item.href}
aria-current={isActive(item.href) ? 'page' : undefined}
>
{item.label}
</a>
))
}
</nav>
<div class="site-links" aria-label="外部链接">
{site.links.map((link) => (
<a class="header-link" href={link.url} target="_blank" rel="noreferrer">
{link.name}
</a>
))}
</div>
</header>
+32
View File
@@ -0,0 +1,32 @@
---
interface Props {
slug: string;
title: string;
date: Date;
repo: string;
summary: string;
tags: string[];
}
const { slug, title, date, repo, summary, tags } = Astro.props;
const formattedDate = date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
---
<article class="card">
<p class="log-meta">
<span class="mono">{formattedDate}</span>
<span class="mono">{repo}</span>
</p>
<h3 class="log-title">
<a href={`/logs/${slug}/`}>{title}</a>
</h3>
<p>{summary}</p>
<div class="tag-list" style="margin-top: 1rem;">
{tags.map((tag) => <span class="tag tag--tech">{tag}</span>)}
</div>
</article>
+57
View File
@@ -0,0 +1,57 @@
---
interface Props {
name: string;
description: string;
coverImage: string;
tags: string[];
repo: string;
featured?: boolean;
demoVideo?: string;
downloadLink?: string;
}
const {
name,
description,
coverImage,
tags,
repo,
featured = false,
demoVideo = '',
downloadLink = '',
} = Astro.props;
---
<article class="card">
<div class="project-cover">
<img src={coverImage} alt={`${name} 项目封面`} loading="lazy" />
</div>
<div class="project-meta">
<span class="mono">{repo}</span>
{featured ? <span class="tag">featured</span> : null}
</div>
<h3 class="project-title">{name}</h3>
<p class="project-description">{description}</p>
<div class="tag-list" style="margin: 1rem 0 0.95rem;">
{tags.map((tag) => <span class="tag tag--tech">{tag}</span>)}
</div>
<div class="project-links">
<a class="section-link" href={`https://gitea.sepcomet.xyz/${repo}`} target="_blank" rel="noreferrer">
仓库
</a>
{demoVideo ? (
<a class="section-link" href={demoVideo} target="_blank" rel="noreferrer">
Demo
</a>
) : null}
{downloadLink ? (
<a class="section-link" href={downloadLink} target="_blank" rel="noreferrer">
下载
</a>
) : null}
</div>
</article>
+74
View File
@@ -0,0 +1,74 @@
---
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const galleryDirectory = fileURLToPath(
new URL('../../public/images/gallery', import.meta.url),
);
const supportedExtensions = new Set([
'.avif',
'.gif',
'.jpeg',
'.jpg',
'.png',
'.svg',
'.webp',
]);
const images = fs.existsSync(galleryDirectory)
? fs
.readdirSync(galleryDirectory)
.filter((file) => supportedExtensions.has(path.extname(file).toLowerCase()))
.sort((a, b) => a.localeCompare(b))
.map((file) => ({
src: `/images/gallery/${file}`,
alt: path
.parse(file)
.name.replace(/[-_]+/g, ' ')
.replace(/\b\w/g, (match) => match.toUpperCase()),
}))
: [];
const fallbackImage = images[0];
---
{
images.length > 0 ? (
<div class="corner-gallery" data-random-corner-gallery aria-hidden="true">
<img
class="corner-gallery__image"
src={fallbackImage.src}
alt={fallbackImage.alt}
width="120"
height="120"
loading="eager"
/>
<script type="application/json" data-corner-gallery-data set:html={JSON.stringify(images)} />
<script>
const galleries = document.querySelectorAll('[data-random-corner-gallery]');
galleries.forEach((gallery) => {
const dataElement = gallery.querySelector('[data-corner-gallery-data]');
const imageElement = gallery.querySelector('.corner-gallery__image');
if (!dataElement || !imageElement) return;
try {
const images = JSON.parse(dataElement.textContent || '[]');
if (!Array.isArray(images) || images.length === 0) return;
const randomImage = images[Math.floor(Math.random() * images.length)];
if (!randomImage?.src) return;
imageElement.src = randomImage.src;
imageElement.alt = randomImage.alt || '随机图库图片';
} catch {
// Keep fallback image if parsing fails.
}
});
</script>
</div>
) : null
}
+25
View File
@@ -0,0 +1,25 @@
---
interface Props {
eyebrow: string;
title: string;
description: string;
href?: string;
linkLabel?: string;
}
const { eyebrow, title, description, href, linkLabel } = Astro.props;
---
<div class="section-heading">
<div>
<span class="eyebrow">{eyebrow}</span>
<h2>{title}</h2>
<p>{description}</p>
</div>
{href && linkLabel ? (
<a class="section-link" href={href}>
{linkLabel}
</a>
) : null}
</div>
+19
View File
@@ -0,0 +1,19 @@
export const site = {
name: 'SepComet',
title: '个人主页',
tagline: 'Unity 开发者 / 持续构建中的个人工作台',
description:
'记录开发过程,整理项目输出,把日常构建沉淀成可浏览、可复盘的公开主页。',
avatar: '/images/avatar.svg',
location: 'Shanghai / Remote',
gitea: {
url: 'https://gitea.sepcomet.xyz',
username: 'sepcomet',
},
links: [
{ name: 'GitHub', url: 'https://github.com/sepcomet' },
{ name: 'Gitea', url: 'https://gitea.sepcomet.xyz' },
{ name: 'Blog', url: 'https://sepcomet.xyz' },
],
stacks: ['Unity', 'Astro', 'TypeScript', 'Docker'],
} as const;
+16
View File
@@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const logs = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/logs' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
repo: z.string(),
tags: z.array(z.string()),
summary: z.string(),
commit: z.string().optional(),
}),
});
export const collections = { logs };
@@ -0,0 +1,18 @@
---
title: "首页设计系统落稿"
date: 2026-05-03
repo: "sepcomet/personal-homepage"
tags: ["design", "ui", "editorial"]
summary: "确定工程编辑部风格:浅色编辑布局为主,局部保留终端式技术感。"
commit: "draft"
---
风格上不做炫技型作品集,而是做一个更像工程师公开工作台的主页。
重点约束:
- 首页以 **Hero / Logs / Projects / Activity** 为主顺序
- 主体颜色保持黑白灰,强调色只保留蓝色
- Gitea 统计模块允许独立使用深色面板,拉开视觉层次
这样能兼顾专业感、可读性和开发者身份辨识度。
@@ -0,0 +1,18 @@
---
title: "首页首版落地"
date: 2026-05-03
repo: "sepcomet/personal-homepage"
tags: ["implementation", "homepage", "astro"]
summary: "从零重建 Astro 首页骨架,补齐导航、内容卡片、活动面板和基础详情路由。"
commit: "draft"
---
原有首页内容已经清空,这一版从零开始重建。
首版关注点:
- 首页信息架构是否清楚
- 内容卡片是否真实可点击
- 日志、项目、分享入口是否能通到对应页面
等首页结构稳定后,再继续补内容系统与交互细节。
+17
View File
@@ -0,0 +1,17 @@
---
title: "个人主页需求梳理"
date: 2026-05-03
repo: "sepcomet/personal-homepage"
tags: ["planning", "astro", "content-model"]
summary: "整理首页、日志、项目和分享页范围,确定内容由 Markdown 与 JSON 驱动。"
commit: "draft"
---
这次先把个人主页定位为一个公开静态站点,而不是带后台能力的应用。
核心原则有两个:
1. **内容文件就是数据源**,站点本身不关心数据由谁写入。
2. **页面结构优先服务求职与展示**,所以首页要同时承担身份介绍、日志入口和项目入口。
接下来的实现会先把首页骨架搭出来,再继续补日志与项目页。
+32
View File
@@ -0,0 +1,32 @@
[
{
"name": "personal-homepage",
"description": "一个由 Markdown 与 JSON 驱动的公开个人主页,用于展示项目、开发日志与外部分享。",
"gitea_repo": "sepcomet/personal-homepage",
"cover_image": "/images/projects/personal-homepage.svg",
"demo_video": "",
"download_link": "",
"tags": ["astro", "static-site", "content-driven"],
"featured": true
},
{
"name": "devlog-pipeline",
"description": "规划中的开发日志生成与写入链路,用于把日常开发过程沉淀为结构化 Markdown 内容。",
"gitea_repo": "sepcomet/devlog-pipeline",
"cover_image": "/images/projects/devlog-pipeline.svg",
"demo_video": "",
"download_link": "",
"tags": ["automation", "markdown", "workflow"],
"featured": true
},
{
"name": "gitea-activity-panel",
"description": "面向个人主页的 Gitea 活动概览模块,计划提供贡献热力图与近期活动列表。",
"gitea_repo": "sepcomet/gitea-activity-panel",
"cover_image": "/images/projects/gitea-activity-panel.svg",
"demo_video": "",
"download_link": "",
"tags": ["gitea", "visualization", "dashboard"],
"featured": true
}
]
+14
View File
@@ -0,0 +1,14 @@
[
{
"name": "个人主页需求文档",
"description": "站点范围、页面结构、数据模型与部署思路的初稿。",
"url": "#",
"time": "2026-05-03"
},
{
"name": "首页设计说明",
"description": "工程编辑部风格的首页设计约束与组件边界。",
"url": "#",
"time": "2026-05-03"
}
]
+38
View File
@@ -0,0 +1,38 @@
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
interface Props {
title?: string;
description?: string;
currentPath?: string;
}
const {
title = 'SepComet | 个人主页',
description = '公开、静态、数据驱动的个人主页',
currentPath = '/',
} = Astro.props;
---
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<div class="page-shell">
<Header currentPath={currentPath} />
<main>
<slot />
</main>
<Footer />
</div>
</body>
</html>
+126
View File
@@ -0,0 +1,126 @@
---
import { getCollection } from 'astro:content';
import GiteaActivity from '../components/GiteaActivity.astro';
import LogCard from '../components/LogCard.astro';
import ProjectCard from '../components/ProjectCard.astro';
import RandomCornerImage from '../components/RandomCornerImage.astro';
import SectionTitle from '../components/SectionTitle.astro';
import Layout from '../layouts/Layout.astro';
import { site } from '../config';
import projects from '../content/projects/index.json';
const logs = (await getCollection('logs')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
const latestLogs = logs.slice(0, 3);
const featuredProjects = projects.filter((project) => project.featured).slice(0, 3);
const latestLog = latestLogs[0];
---
<Layout title={`${site.name} | ${site.title}`} description={site.description} currentPath="/">
<section class="page-hero">
<div class="container hero-grid">
<article class="hero-card">
<span class="hero-kicker">公开静态站点 / 数据驱动内容</span>
<h1 class="hero-title">{site.name}</h1>
<p class="hero-subtitle">{site.tagline}</p>
<p class="hero-meta">{site.description}</p>
<div class="hero-actions">
<a class="button-primary" href="/projects">查看精选项目</a>
<a class="button-secondary" href="/logs">阅读开发日志</a>
</div>
<div class="inline-links" style="margin-top: 1rem;">
{site.links.map((link) => (
<a class="section-link" href={link.url} target="_blank" rel="noreferrer">
{link.name}
</a>
))}
</div>
</article>
<aside class="hero-card status-panel" aria-label="站点状态摘要">
<div class="status-panel__head">
<div>
<p class="eyebrow">Current status</p>
<h2 class="status-panel__title">持续构建中的个人工作台</h2>
</div>
</div>
<div class="status-panel__grid">
<section class="status-item">
<p class="status-item__label">Latest log</p>
<p class="status-item__value">{latestLog?.data.title ?? '暂无日志'}</p>
</section>
<section class="status-item">
<p class="status-item__label">Active repo</p>
<p class="status-item__value status-item__mono">sepcomet/personal-homepage</p>
</section>
<section class="status-item">
<p class="status-item__label">Stack</p>
<p class="status-item__value status-item__mono">{site.stacks.join(' / ')}</p>
</section>
<section class="status-item">
<p class="status-item__label">Gitea</p>
<p class="status-item__value status-item__mono">{site.gitea.username}</p>
</section>
</div>
</aside>
</div>
</section>
<section class="section">
<div class="container">
<SectionTitle
eyebrow="Latest Logs"
title="最近开发日志"
description="用日志记录做了什么、为什么这么做,以及每一步是如何落到实际内容结构里的。"
href="/logs"
linkLabel="查看全部日志"
/>
<div class="card-grid">
{latestLogs.map((log) => <LogCard slug={log.id} {...log.data} />)}
</div>
</div>
</section>
<section class="section">
<div class="container">
<SectionTitle
eyebrow="Featured Projects"
title="精选项目"
description="首页先展示最能代表当前方向的几个项目或模块,作为后续内容填充的真实入口。"
href="/projects"
linkLabel="打开项目列表"
/>
<div class="card-grid">
{
featuredProjects.map((project) => (
<ProjectCard
name={project.name}
description={project.description}
coverImage={project.cover_image}
tags={project.tags}
repo={project.gitea_repo}
featured={project.featured}
demoVideo={project.demo_video}
downloadLink={project.download_link}
/>
))
}
</div>
</div>
</section>
<section class="section">
<div class="container">
<GiteaActivity />
</div>
</section>
<RandomCornerImage />
</Layout>
+52
View File
@@ -0,0 +1,52 @@
---
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { site } from '../../config';
export async function getStaticPaths() {
const logs = await getCollection('logs');
return logs.map((log) => ({
params: { slug: log.id },
props: { log },
}));
}
const { log } = Astro.props;
const { Content } = await render(log);
const formattedDate = log.data.date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
---
<Layout
title={`${log.data.title} | ${site.name}`}
description={log.data.summary}
currentPath="/logs"
>
<section class="list-page">
<div class="container">
<div class="page-intro">
<span class="eyebrow">Log Detail</span>
<h1>{log.data.title}</h1>
<p>{log.data.summary}</p>
</div>
<article class="prose">
<p class="log-meta">
<span class="mono">{formattedDate}</span>
<span class="mono">{log.data.repo}</span>
{log.data.commit ? <span class="mono">commit: {log.data.commit}</span> : null}
</p>
<div class="tag-list" style="margin-bottom: 1.25rem;">
{log.data.tags.map((tag: string) => <span class="tag tag--tech">{tag}</span>)}
</div>
<Content />
</article>
</div>
</section>
</Layout>
+30
View File
@@ -0,0 +1,30 @@
---
import { getCollection } from 'astro:content';
import LogCard from '../../components/LogCard.astro';
import Layout from '../../layouts/Layout.astro';
import { site } from '../../config';
const logs = (await getCollection('logs')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
---
<Layout
title={`开发日志 | ${site.name}`}
description="按时间倒序展示开发日志摘要。"
currentPath="/logs"
>
<section class="list-page">
<div class="container">
<div class="page-intro">
<span class="eyebrow">Logs</span>
<h1>开发日志</h1>
<p>这里承载由 Markdown 驱动的开发日志。当前先放入 3 条示例内容,后续可由外部系统继续写入。</p>
</div>
<div class="card-grid">
{logs.map((log) => <LogCard slug={log.id} {...log.data} />)}
</div>
</div>
</section>
</Layout>
+39
View File
@@ -0,0 +1,39 @@
---
import Layout from '../../layouts/Layout.astro';
import ProjectCard from '../../components/ProjectCard.astro';
import { site } from '../../config';
import projects from '../../content/projects/index.json';
---
<Layout
title={`项目展示 | ${site.name}`}
description="展示精选项目与结构化项目数据。"
currentPath="/projects"
>
<section class="list-page">
<div class="container">
<div class="page-intro">
<span class="eyebrow">Projects</span>
<h1>项目展示</h1>
<p>项目数据当前来自本地 JSON 文件,首页与此页面共享同一份结构化数据来源。</p>
</div>
<div class="card-grid">
{
projects.map((project) => (
<ProjectCard
name={project.name}
description={project.description}
coverImage={project.cover_image}
tags={project.tags}
repo={project.gitea_repo}
featured={project.featured}
demoVideo={project.demo_video}
downloadLink={project.download_link}
/>
))
}
</div>
</div>
</section>
</Layout>
+36
View File
@@ -0,0 +1,36 @@
---
import Layout from '../layouts/Layout.astro';
import { site } from '../config';
import shares from '../content/shares/index.json';
---
<Layout
title={`分享链接 | ${site.name}`}
description="公开分享链接与资源入口。"
currentPath="/shares"
>
<section class="list-page">
<div class="container">
<div class="page-intro">
<span class="eyebrow">Shares</span>
<h1>分享链接</h1>
<p>这里预留给 Seafile 或其他公开资源的索引入口。当前先用静态数据确认列表版式。</p>
</div>
<div class="card-grid" style="grid-template-columns: 1fr;">
{shares.map((share) => (
<article class="card">
<p class="log-meta">
<span class="mono">{share.time}</span>
</p>
<h2 class="project-title">{share.name}</h2>
<p>{share.description}</p>
<div class="project-links" style="margin-top: 1rem;">
<a class="section-link" href={share.url}>打开链接</a>
</div>
</article>
))}
</div>
</div>
</section>
</Layout>
+657
View File
@@ -0,0 +1,657 @@
@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=Fira+Code:wght@400;500&family=Space+Grotesk:wght@400;500;700&display=swap');
:root {
--bg: #fafafa;
--surface: rgba(255, 255, 255, 0.88);
--surface-strong: #ffffff;
--surface-muted: #f4f4f5;
--panel-dark: #0f172a;
--panel-border: rgba(148, 163, 184, 0.22);
--text: #09090b;
--muted: #52525b;
--muted-strong: #27272a;
--border: #e4e4e7;
--border-strong: #d4d4d8;
--accent: #2563eb;
--accent-soft: rgba(37, 99, 235, 0.08);
--success: #22c55e;
--shadow-sm: 0 10px 30px rgba(15, 23, 42, 0.04);
--shadow-md: 0 18px 40px rgba(15, 23, 42, 0.08);
--shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.14);
--radius-sm: 14px;
--radius-md: 22px;
--radius-lg: 30px;
--container: 1180px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
font-family: 'Space Grotesk', system-ui, sans-serif;
color: var(--text);
background:
radial-gradient(circle at top, rgba(37, 99, 235, 0.07), transparent 34%),
linear-gradient(180deg, #ffffff 0%, var(--bg) 42%, #f5f7fb 100%);
}
a {
color: inherit;
text-decoration: none;
}
img {
display: block;
max-width: 100%;
}
button,
a[role='button'] {
cursor: pointer;
}
main {
display: block;
}
.container {
width: min(var(--container), calc(100% - 2rem));
margin: 0 auto;
}
.page-shell {
position: relative;
padding: 1rem 0 4rem;
}
.site-header {
position: sticky;
top: 1rem;
z-index: 50;
width: min(var(--container), calc(100% - 2rem));
margin: 0 auto 2rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 1rem;
border: 1px solid rgba(228, 228, 231, 0.9);
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(18px);
}
.site-brand {
display: flex;
align-items: center;
gap: 0.85rem;
min-width: 0;
}
.site-brand__mark {
width: 2.4rem;
height: 2.4rem;
display: grid;
place-items: center;
border-radius: 50%;
font-family: 'Archivo', sans-serif;
font-weight: 700;
color: white;
background: linear-gradient(135deg, #2563eb, #0f172a);
}
.site-brand__text {
min-width: 0;
}
.site-brand__title {
font-family: 'Archivo', sans-serif;
font-size: 0.95rem;
font-weight: 700;
}
.site-brand__subtitle {
font-size: 0.78rem;
color: var(--muted);
}
.site-nav,
.site-links {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.nav-link,
.header-link {
padding: 0.55rem 0.85rem;
border-radius: 999px;
font-size: 0.94rem;
color: var(--muted);
transition: background-color 180ms ease, color 180ms ease;
}
.nav-link:hover,
.nav-link:focus-visible,
.header-link:hover,
.header-link:focus-visible {
color: var(--text);
background: var(--surface-muted);
}
.nav-link[aria-current='page'] {
color: var(--accent);
background: var(--accent-soft);
}
.page-hero {
padding: 2rem 0 1rem;
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.95fr);
gap: 1.5rem;
align-items: stretch;
}
.hero-card,
.card,
.panel {
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(14px);
}
.hero-card {
padding: 2rem;
}
.hero-kicker,
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.7rem;
border-radius: 999px;
background: var(--surface-muted);
color: var(--muted-strong);
font-size: 0.85rem;
font-weight: 500;
}
.hero-title {
margin: 1.2rem 0 0.8rem;
font-family: 'Archivo', sans-serif;
font-size: clamp(2.6rem, 6vw, 4.8rem);
line-height: 0.95;
letter-spacing: -0.04em;
}
.hero-subtitle {
margin: 0 0 1rem;
max-width: 48rem;
font-size: clamp(1.05rem, 2vw, 1.28rem);
line-height: 1.7;
color: var(--muted-strong);
}
.hero-meta {
margin: 0 0 1.2rem;
font-size: 0.96rem;
line-height: 1.7;
color: var(--muted);
}
.hero-actions,
.inline-links,
.tag-list,
.project-links {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.button-primary,
.button-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
min-height: 2.9rem;
padding: 0.7rem 1rem;
border-radius: 999px;
border: 1px solid transparent;
font-size: 0.95rem;
font-weight: 600;
transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease, border-color 180ms ease;
}
.button-primary {
color: #fff;
background: linear-gradient(135deg, var(--accent), #1d4ed8);
box-shadow: 0 16px 30px rgba(37, 99, 235, 0.2);
}
.button-secondary {
color: var(--text);
background: var(--surface-strong);
border-color: var(--border);
}
.button-primary:hover,
.button-primary:focus-visible,
.button-secondary:hover,
.button-secondary:focus-visible {
transform: translateY(-1px);
}
.status-panel {
padding: 1.5rem;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 244, 245, 0.95)),
var(--surface);
}
.status-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.2rem;
}
.status-panel__title {
margin: 0;
font-family: 'Archivo', sans-serif;
font-size: 1.1rem;
}
.status-panel__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem;
}
.status-item {
padding: 1rem;
border: 1px solid var(--border);
border-radius: 1.1rem;
background: rgba(255, 255, 255, 0.9);
}
.status-item__label {
margin: 0 0 0.35rem;
font-size: 0.78rem;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted);
}
.status-item__value {
margin: 0;
font-family: 'Archivo', sans-serif;
font-size: 1.05rem;
}
.status-item__mono {
font-family: 'Fira Code', monospace;
font-size: 0.88rem;
}
.section {
padding: 1rem 0;
}
.section-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
}
.section-heading h2 {
margin: 0.2rem 0 0;
font-family: 'Archivo', sans-serif;
font-size: clamp(1.7rem, 3.5vw, 2.4rem);
}
.section-heading p {
margin: 0;
max-width: 38rem;
color: var(--muted);
line-height: 1.7;
}
.section-link {
color: var(--accent);
font-weight: 600;
}
.card-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.stack-grid {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 1rem;
}
.card {
padding: 1.35rem;
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
}
.card:hover,
.card:focus-within {
transform: translateY(-2px);
border-color: var(--border-strong);
box-shadow: var(--shadow-md);
}
.log-meta,
.project-meta,
.activity-meta,
.footer-meta {
display: flex;
flex-wrap: wrap;
gap: 0.55rem 0.8rem;
margin: 0 0 0.85rem;
color: var(--muted);
font-size: 0.84rem;
}
.mono {
font-family: 'Fira Code', monospace;
}
.log-title,
.project-title {
margin: 0 0 0.7rem;
font-family: 'Archivo', sans-serif;
font-size: 1.18rem;
line-height: 1.3;
}
.card p,
.project-description,
.activity-copy,
.empty-state {
margin: 0;
color: var(--muted);
line-height: 1.75;
}
.tag {
display: inline-flex;
align-items: center;
min-height: 1.9rem;
padding: 0.25rem 0.7rem;
border-radius: 999px;
background: var(--surface-muted);
color: var(--muted-strong);
font-size: 0.82rem;
}
.tag--tech {
font-family: 'Fira Code', monospace;
font-size: 0.78rem;
}
.project-cover {
aspect-ratio: 16 / 9;
overflow: hidden;
margin-bottom: 1rem;
border-radius: calc(var(--radius-md) - 8px);
border: 1px solid var(--border);
background: #eef2ff;
}
.activity-panel {
padding: 1.5rem;
color: #f8fafc;
border-color: var(--panel-border);
background:
radial-gradient(circle at top right, rgba(37, 99, 235, 0.4), transparent 28%),
linear-gradient(180deg, #111c33, #0f172a 70%);
box-shadow: var(--shadow-lg);
}
.activity-panel .section-heading p,
.activity-panel .section-heading .eyebrow,
.activity-panel .activity-copy,
.activity-panel .activity-meta,
.activity-panel .heatmap-legend {
color: rgba(226, 232, 240, 0.86);
}
.activity-grid {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 1rem;
align-items: start;
}
.heatmap {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 0.45rem;
margin-top: 1rem;
}
.heatmap-cell {
aspect-ratio: 1;
border-radius: 0.5rem;
border: 1px solid rgba(148, 163, 184, 0.22);
}
.heatmap-legend {
display: flex;
align-items: center;
gap: 0.6rem;
margin-top: 0.9rem;
font-size: 0.8rem;
}
.legend-scale {
display: inline-flex;
gap: 0.35rem;
}
.legend-scale span {
width: 0.95rem;
height: 0.95rem;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.22);
}
.activity-list {
display: grid;
gap: 0.75rem;
}
.activity-item {
padding: 0.95rem 1rem;
border: 1px solid rgba(148, 163, 184, 0.22);
border-radius: 1rem;
background: rgba(15, 23, 42, 0.32);
}
.activity-item strong {
display: block;
margin-bottom: 0.25rem;
color: #fff;
}
.corner-gallery {
position: absolute;
right: clamp(0.75rem, 3vw, 2rem);
bottom: 1.25rem;
z-index: 1;
pointer-events: none;
user-select: none;
}
.corner-gallery__image {
width: 120px;
height: 120px;
object-fit: contain;
object-position: right bottom;
opacity: 0.95;
filter: drop-shadow(0 18px 36px rgba(15, 23, 42, 0.18));
}
.list-page {
padding: 1.5rem 0 2rem;
}
.page-intro {
margin-bottom: 1.5rem;
}
.page-intro h1 {
margin: 0.35rem 0 0.8rem;
font-family: 'Archivo', sans-serif;
font-size: clamp(2rem, 4vw, 3.3rem);
}
.page-intro p {
margin: 0;
max-width: 44rem;
color: var(--muted);
line-height: 1.8;
}
.prose {
padding: 2rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow-sm);
}
.prose h1,
.prose h2,
.prose h3 {
font-family: 'Archivo', sans-serif;
line-height: 1.2;
}
.prose p,
.prose li,
.prose blockquote {
color: var(--muted-strong);
line-height: 1.85;
}
.prose pre,
.prose code {
font-family: 'Fira Code', monospace;
}
.footer {
padding: 2rem 0 1rem;
}
.footer-card {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.2rem 1.4rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.8);
}
.footer-card p {
margin: 0;
color: var(--muted);
}
@media (max-width: 1024px) {
.hero-grid,
.stack-grid,
.activity-grid {
grid-template-columns: 1fr;
}
.card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.site-header {
border-radius: 1.25rem;
flex-direction: column;
align-items: stretch;
}
}
@media (max-width: 720px) {
.page-shell {
padding-bottom: 3rem;
}
.hero-card,
.activity-panel,
.card,
.prose {
padding: 1.25rem;
}
.card-grid,
.status-panel__grid {
grid-template-columns: 1fr;
}
.section-heading,
.footer-card {
align-items: flex-start;
flex-direction: column;
}
.hero-title {
font-size: 2.6rem;
}
.corner-gallery {
right: 0.6rem;
bottom: 0.6rem;
}
.corner-gallery__image {
width: 96px;
height: 96px;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}