40 Commits

Author SHA1 Message Date
8db8623d58 update map
All checks were successful
Homepage Prod Build (NixCN) TeamCity build finished
Homepage Check Build (NixCN) TeamCity build finished
2026-05-21 14:45:26 +08:00
7a98e63b56 update event start time
All checks were successful
Homepage Prod Build (NixCN) TeamCity build finished
Homepage Check Build (NixCN) TeamCity build finished
2026-05-21 14:33:20 +08:00
060eb2bdc8 refactor: use Astro i18n to avoid redirect notice
All checks were successful
Homepage Check Build (NixCN) TeamCity build finished
Homepage Prod Build (NixCN) TeamCity build finished
2026-05-21 02:47:22 +08:00
327ca25272 refactor: rename calendar to event-guide
All checks were successful
Homepage Check Build (NixCN) TeamCity build finished
2026-05-21 02:26:02 +08:00
16b4288136 fix typo 2026-05-21 02:20:01 +08:00
113375f348 fix(i18n): polish copy in nav, home, calendar, and cms-guide
Localizes zh-CN nav home label to "首页" and en to "Home". Rewrites the
English home page lead text for clarity. Sharpens calendar section labels
("Daily Schedule:", "Dining Guide", trims "in the afternoon"). Tightens
four CMS guide step descriptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 02:19:23 +08:00
ab2bb31b6c fix(calendar): strip tracking, tighten list gaps, drop food-map border
Removes tracking-[-0.05em] from schedule and agenda elements. Reduces
vertical gap in agenda bullets (gap-3→gap-1) and schedule flow items
(gap-2→gap-1). Drops the border from the food-map figure. Updates the
English food map image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 02:19:16 +08:00
bf59a1406d fix(souvenir): disable CTA and remove coming-soon modal
Collection form is not ready yet. Replaces the branded blue CTA with a
disabled gray button, removes the ExternalLink icon, and deletes the
coming-soon modal overlay. Updates copy in both zh-CN and en to reflect
the not-yet-open state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 02:19:10 +08:00
7d95b648b6 feat(home): add ArrowRight icon to primary CTA, move arrow out of translation strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:52:06 +08:00
6cd86f5fa1 feat(calendar): add hero banner with shared background image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:51 +08:00
7ca7d2dee0 refactor(souvenir): use shared hero-bg, replace image-based card texture with CSS gradients
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:51 +08:00
51e411295d refactor(cms-guide): use shared hero-bg path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:51 +08:00
b2f95d1c97 refactor(assets): consolidate hero-bg to shared path, remove unused souvenir images
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:51 +08:00
83c7c6f711 feat: add allowedHosts 2026-05-21 01:50:51 +08:00
331e0521ad feat: switch to Caddy for static file serving and update community links
Replace Node preview server with caddy:2-alpine in Containerfile, remove
allowedHosts preview config, and update Telegram/Matrix links to meetup channels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:51 +08:00
ef46939fa0 refactor(i18n): condense cms-guide hero subtitle to single line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:45 +08:00
15ed2b5891 refactor(souvenir): replace cta-arrow.png with inline SVG, remove bottom-circles decoration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:43 +08:00
16e85642fc fix(calendar): remove redundant inline background shorthand from bullet list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:33 +08:00
3198acea53 docs: update tailwind migration plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:31 +08:00
1bbbe86347 feat(deps): add @lucide/astro package
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:50:27 +08:00
e9e82d483a feat(config): add Tailwind CSS v4 setup and update component styles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:50:37 +08:00
b0becd6351 fix(styles): add scrollbar-gutter: stable to prevent layout jump on cms-guide 2026-05-21 00:50:05 +08:00
c47d29aecb fix(styles): restore body bottom padding lost from dropped :global(.page) rule 2026-05-21 00:44:25 +08:00
264cbfb1c7 refactor(souvenir): replace style block with Tailwind utility classes 2026-05-21 00:43:07 +08:00
045a992f73 refactor(home): replace style block with Tailwind utility classes 2026-05-21 00:40:29 +08:00
d04490fef0 refactor(cms-guide): replace style block with Tailwind utility classes 2026-05-21 00:37:05 +08:00
969ecb4def refactor(calendar): replace style block with Tailwind utility classes 2026-05-21 00:33:15 +08:00
3b883c9739 fix(navbar): add last:border-b-0 to mobile nav links, fix hamburger border-radius 2026-05-21 00:31:20 +08:00
e00e663ece refactor(navbar): replace style block with Tailwind utility classes 2026-05-21 00:28:20 +08:00
6b7fa97208 refactor(layout): import global.css, remove style block 2026-05-21 00:25:13 +08:00
774a077c38 fix(styles): rename --font-family-sans to --font-sans for Tailwind v4 compatibility 2026-05-21 00:24:42 +08:00
c7254fd961 feat(styles): set up Tailwind theme tokens, keyframes, and component layer 2026-05-21 00:20:06 +08:00
c4c4036978 docs: add Tailwind migration implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:17:23 +08:00
f636f2c7a1 docs: fix spec — correct easing values, add step-delay and gloss edge cases
All checks were successful
Homepage Check Build (NixCN) TeamCity build finished
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:59:35 +08:00
8487e48932 docs: add Tailwind CSS migration design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:58:48 +08:00
265388253d refactor(calendar): replace guide-card layout with bento grid
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:50:39 +08:00
cf000bfa64 fix(i18n): update copy across calendar, cms-guide, and souvenir pages
Syncs English translations to match updated Chinese text: conference
heading sub-copy, step-03 minor/guardian tip, souvenir hero/card/coming-
soon copy, and removed "哦" tone particle from step-01 email tip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:37:31 +08:00
bda1a87c0e feat(home): link primary CTA to nix.org.cn app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:30:40 +08:00
fb087272de refactor(layout): move page styles to body, add horizontal padding
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:27:45 +08:00
7a3fed3b44 feat(site): nixcn-conf-2605 impt (#20)
Some checks failed
Homepage Check Build (NixCN) TeamCity build failed
## Summary

实现 NixCN Conf 2026.05 活动官网,将站点从 Starlight 文档站替换为自定义 Astro 多页面网站。

- 新增首页、Calendar、CMS Guide、Souvenir 四个活动页面
- 支持 `zh-CN` / `en` 双语,根路径 `/` 重定向至 `/zh-CN/`
- 迁移并整理活动相关静态资源至 `public/images/`
- 接入 badge 倾斜、背景视差、Nix flake 动画等交互
- 移除 Starlight 及旧 MDX 文档内容

## Routes

| 页面 | zh-CN | en |
|------|-------|-----|
| 首页 | `/zh-CN/` | `/en/` |
| 日程 | `/zh-CN/calendar` | `/en/calendar` |
| CMS 指南 | `/zh-CN/cms-guide` | `/en/cms-guide` |
| 纪念品 | `/zh-CN/souvenir` | `/en/souvenir` |

## ⚠️  Breaking Changes

### 内容移除
- 移除 Starlight 及全部 MDX 文档(meetup guide、volunteers、code-of-conduct 等)
- 移除 @astrojs/starlight、@astrojs/mdx、starlight-ui-tweaks 等依赖
- 移除 src/content/docs/ 目录及 content.config.ts
### 架构变更
- 站点由文档驱动(MDX)改为组件驱动(.astro 页面)
- 静态资源从 src/assets/ 迁移至 public/images/
- i18n 由 Starlight locale 改为自定义 [lang]/ 路由 + translations.json

Co-authored-by: gylove1994 <gylove1994@acgsteps.com>
Reviewed-on: #20
2026-05-19 22:28:15 +00:00
39 changed files with 2711 additions and 2397 deletions

7
Caddyfile Normal file
View File

@@ -0,0 +1,7 @@
:3000 {
root * /srv
file_server
@root path /
redir @root /zh-CN/ 302
}

13
Containerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM node:22-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM caddy:2-alpine AS runtime
COPY --from=builder /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 3000
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

View File

@@ -1,13 +1,18 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
site: 'https://meetup.nixos-cn.org',
vite: {
plugins: [tailwindcss()],
},
server: {
host: true,
},
redirects: {
'/': '/zh-CN/',
i18n: {
locales: ['zh-CN', 'en'],
defaultLocale: 'zh-CN',
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
# Tailwind CSS Migration Design
**Date:** 2026-05-20
**Scope:** Replace all hand-written CSS in the project with Tailwind CSS utility classes.
---
## Goal
Eliminate all `<style>` blocks from every `.astro` file. All styling moves to Tailwind utility classes on HTML elements, with shared tokens and keyframes centralized in `src/styles/global.css`. Visual output must be identical to pre-migration.
---
## Approach
Global-first, then parallel component conversion.
1. **Phase 1 (sequential):** Finalize `global.css` and update `BaseLayout.astro`. Must complete before Phase 2 so sub-agents have a definitive token reference.
2. **Phase 2 (parallel):** All 5 component files converted simultaneously by independent sub-agents.
---
## Phase 1: global.css
`src/styles/global.css` is already created with `@import "tailwindcss"`. Extend it with:
### `@theme` block
Register all project-specific design tokens as Tailwind CSS variables:
```css
@theme {
/* Brand colors */
--color-brand-blue: #5277c3;
--color-brand-dark: #030f20;
--color-brand-light: #9bcef1;
--color-surface: rgba(249, 251, 255, 0.9);
--color-border: rgba(155, 206, 241, 0.8);
/* Typography */
--font-family-sans: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Easings */
--ease-hero: cubic-bezier(0.22, 1, 0.36, 1);
--ease-modal: cubic-bezier(0.22, 1, 0.36, 1);
/* Named animations (name maps to @keyframes below) */
--animate-hero-fade-up: heroFadeUp 0.55s var(--ease-hero) both;
--animate-hero-fade-down: heroFadeDown 0.45s var(--ease-hero) both;
--animate-hero-fade-in: heroFadeIn 0.5s var(--ease-hero) both;
--animate-hero-fade-in-70: heroFadeInTo70 0.5s var(--ease-hero) both;
--animate-modal-in: modalDialogIn 0.3s var(--ease-modal) both;
--animate-modal-overlay: modalOverlayIn 0.2s ease both;
}
```
The existing `--hero-ease` / `--modal-ease` CSS variables are renamed to `--ease-hero` / `--ease-modal` to follow Tailwind v4 token conventions. References in any remaining inline styles or JS must use the new names.
### `@keyframes`
Define all kept animations here:
- `heroFadeUp``opacity: 0; transform: translateY(10px)``opacity: 1; transform: none`
- `heroFadeDown``opacity: 0; transform: translateY(-10px)``opacity: 1; transform: none`
- `heroFadeIn``opacity: 0``opacity: 1`
- `heroFadeInTo70``opacity: 0``opacity: 0.7`
- `modalDialogIn` — scale + fade in
- `modalOverlayIn` — fade in
Exact keyframe values must be read from current component source before writing.
### `@layer base`
Replaces the `<style is:global>` block in `BaseLayout.astro`:
```css
@layer base {
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body {
font-family: var(--font-family-sans);
background: #ffffff;
color: var(--color-brand-dark);
-webkit-font-smoothing: antialiased;
min-height: 100dvh;
display: flex;
flex-direction: column;
padding: 0 16px;
}
}
```
---
## Phase 1: BaseLayout.astro
- Add `import '../styles/global.css'` to the frontmatter
- Remove the `<style is:global>` block entirely
---
## Phase 2: Component files
Files: `Navbar.astro`, `HomePage.astro`, `CalendarPage.astro`, `CmsGuidePage.astro`, `SouvenirPage.astro`.
Each is converted by reading its current `<style>` block, mapping every rule to utility classes on the corresponding HTML element, then deleting the `<style>` block.
### Translation conventions
| CSS pattern | Tailwind equivalent |
|---|---|
| `@media (max-width: 1023px)` | `max-lg:` variant |
| `@media (max-width: 639px)` | `max-sm:` variant |
| `@media (prefers-reduced-motion: reduce)` | `motion-reduce:` variant |
| `animation: heroFadeUp 0.55s ...` | `animate-hero-fade-up` |
| `animation-delay: 0.28s` | `[animation-delay:0.28s]` |
| `backdrop-filter: blur(4px)` | `backdrop-blur-sm` |
| `backdrop-filter: blur(10px) saturate(1.4)` | `backdrop-blur-md backdrop-saturate-[140%]` |
| `mix-blend-mode: screen` | `mix-blend-screen` |
| Custom `clamp()` / `calc()` | `[clamp(...)]` / `[calc(...)]` arbitrary values |
| Brand color `#5277c3` | `text-brand-blue` / `bg-brand-blue` / `border-brand-blue` |
| Surface bg `rgba(249,251,255,0.9)` | `bg-surface` |
| Border color `rgba(155,206,241,0.8)` | `border-border` |
### Edge cases
**`--step-delay` (CmsGuidePage):** Each `.step` element sets `--step-delay` as an inline style in the Astro frontmatter JSX. The `<style>` block then reads it via `animation-delay: var(--step-delay, 0.56s)`. After migration, the inline style attribute stays as-is; the Tailwind class uses the same variable: `[animation-delay:var(--step-delay,0.56s)]`.
**`--gloss-x` / `--gloss-y` (HomePage badge):** `badge-tilt.ts` updates these CSS custom properties on the badge element via `style.setProperty(...)`. The existing CSS uses them inside a `radial-gradient`. Because this background value depends on dynamically-changing CSS vars, it cannot be expressed as a static Tailwind arbitrary class. Move this single rule into `@layer components` in `global.css` rather than keeping it in a `<style>` block.
### Dropped patterns
- All `:global()` selectors (including navbar entrance animation)
- `decoFadeIn` keyframe (was used only by removed `.deco-line` elements)
### `prefers-reduced-motion`
Every animated element gets `motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none` added alongside its `animate-*` class. This replaces the per-component `@media (prefers-reduced-motion)` blocks.
---
## Out of scope
- Content, copy, or i18n changes
- JS behavior changes
- Adding new visual features or layout changes
- Refactoring the i18n/translation system
- Changing the dev server or build config beyond what's already done
---
## Verification checklist (per file)
- [ ] No `<style>` block remains in the file
- [ ] No `:global()` selector remains
- [ ] All breakpoint overrides use `max-lg:` / `max-sm:` variants
- [ ] All animated elements have `motion-reduce:animate-none motion-reduce:opacity-100`
- [ ] Brand colors use theme token classes, not arbitrary hex
- [ ] Visual output matches pre-migration on desktop and mobile (dev server spot-check)

View File

@@ -14,8 +14,11 @@
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@lucide/astro": "^1.16.0",
"@tailwindcss/vite": "^4.3.0",
"astro": "^5.6.1",
"sharp": "^0.34.2",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
},
"devDependencies": {

413
pnpm-lock.yaml generated
View File

@@ -11,12 +11,21 @@ importers:
'@astrojs/check':
specifier: ^0.9.9
version: 0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.7.4)(typescript@6.0.3)
'@lucide/astro':
specifier: ^1.16.0
version: 1.16.0(astro@5.16.4(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0))
'@tailwindcss/vite':
specifier: ^4.3.0
version: 4.3.0(vite@6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))
astro:
specifier: ^5.6.1
version: 5.16.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0)
version: 5.16.4(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0)
sharp:
specifier: ^0.34.2
version: 0.34.5
tailwindcss:
specifier: ^4.3.0
version: 4.3.0
typescript:
specifier: ^6.0.3
version: 6.0.3
@@ -533,155 +542,183 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@@ -723,6 +760,12 @@ packages:
cpu: [x64]
os: [win32]
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -730,9 +773,17 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@lucide/astro@1.16.0':
resolution: {integrity: sha512-RWT+9Y1UAMnz/oiJMssnQTSeBM0AbwzK1SsOeY2zQungN7PQxm4Pp4bNxIVp/Wu77TBXkpPoaIyxYqagZVjWGQ==}
peerDependencies:
astro: ^4 || ^5 || ^6
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
@@ -788,56 +839,67 @@ packages:
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.53.3':
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.53.3':
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.53.3':
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.53.3':
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.53.3':
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.53.3':
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.53.3':
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.53.3':
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
@@ -895,6 +957,100 @@ packages:
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
'@tailwindcss/node@4.3.0':
resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
'@tailwindcss/oxide-android-arm64@4.3.0':
resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.3.0':
resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.3.0':
resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.3.0':
resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.3.0':
resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.3.0':
resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==}
engines: {node: '>= 20'}
'@tailwindcss/vite@4.3.0':
resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -1214,6 +1370,10 @@ packages:
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
enhanced-resolve@5.21.5:
resolution: {integrity: sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -1306,6 +1466,9 @@ packages:
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
h3@1.15.4:
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
@@ -1379,6 +1542,10 @@ packages:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
@@ -1400,6 +1567,80 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -1831,6 +2072,13 @@ packages:
engines: {node: '>=16'}
hasBin: true
tailwindcss@4.3.0:
resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==}
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@@ -2757,15 +3005,34 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@lucide/astro@1.16.0(astro@5.16.4(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0))':
dependencies:
astro: 5.16.4(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0)
'@oslojs/encoding@1.1.0': {}
'@poppinss/colors@4.1.5':
@@ -2895,6 +3162,74 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tailwindcss/node@4.3.0':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.21.5
jiti: 2.7.0
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.3.0
'@tailwindcss/oxide-android-arm64@4.3.0':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.3.0':
optional: true
'@tailwindcss/oxide-darwin-x64@4.3.0':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.3.0':
optional: true
'@tailwindcss/oxide@4.3.0':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.3.0
'@tailwindcss/oxide-darwin-arm64': 4.3.0
'@tailwindcss/oxide-darwin-x64': 4.3.0
'@tailwindcss/oxide-freebsd-x64': 4.3.0
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0
'@tailwindcss/oxide-linux-arm64-gnu': 4.3.0
'@tailwindcss/oxide-linux-arm64-musl': 4.3.0
'@tailwindcss/oxide-linux-x64-gnu': 4.3.0
'@tailwindcss/oxide-linux-x64-musl': 4.3.0
'@tailwindcss/oxide-wasm32-wasi': 4.3.0
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
'@tailwindcss/vite@4.3.0(vite@6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
vite: 6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -3019,7 +3354,7 @@ snapshots:
array-iterate@2.0.1: {}
astro@5.16.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0):
astro@5.16.4(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0):
dependencies:
'@astrojs/compiler': 2.13.0
'@astrojs/internal-helpers': 0.7.5
@@ -3076,8 +3411,8 @@ snapshots:
unist-util-visit: 5.0.0
unstorage: 1.17.3
vfile: 6.0.3
vite: 6.4.1(@types/node@24.10.1)(yaml@2.9.0)
vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.1)(yaml@2.9.0))
vite: 6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
@@ -3295,6 +3630,11 @@ snapshots:
emoji-regex@8.0.0: {}
enhanced-resolve@5.21.5:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
entities@4.5.0: {}
entities@6.0.1: {}
@@ -3419,6 +3759,8 @@ snapshots:
glob-to-regexp@0.4.1: {}
graceful-fs@4.2.11: {}
h3@1.15.4:
dependencies:
cookie-es: 1.2.2
@@ -3544,6 +3886,8 @@ snapshots:
dependencies:
is-inside-container: 1.0.0
jiti@2.7.0: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -3558,6 +3902,55 @@ snapshots:
kleur@4.1.5: {}
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-freebsd-x64: 1.32.0
lightningcss-linux-arm-gnueabihf: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
longest-streak@3.1.0: {}
lru-cache@10.4.3: {}
@@ -4292,6 +4685,10 @@ snapshots:
picocolors: 1.1.1
sax: 1.4.3
tailwindcss@4.3.0: {}
tapable@2.3.3: {}
tiny-inflate@1.0.3: {}
tinyexec@1.0.2: {}
@@ -4434,7 +4831,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@6.4.1(@types/node@24.10.1)(yaml@2.9.0):
vite@6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -4445,11 +4842,13 @@ snapshots:
optionalDependencies:
'@types/node': 24.10.1
fsevents: 2.3.3
jiti: 2.7.0
lightningcss: 1.32.0
yaml: 2.9.0
vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(yaml@2.9.0)):
vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)):
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(yaml@2.9.0)
vite: 6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
volar-service-css@0.0.70(@volar/language-service@2.4.28):
dependencies:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -1,580 +0,0 @@
---
import Navbar from './Navbar.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { type Locale, getTranslations } from '../i18n/config';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
const tp = t.calendar;
const foodMapSrc =
locale === 'en' ? '/images/calendar/food-map-en.png' : '/images/calendar/food-map.png';
---
<BaseLayout title={tp.meta.title} description={tp.meta.description} lang={tp.meta.htmlLang}>
<Navbar locale={locale} activePage='calendar' />
<!-- ======= Page Heading ======= -->
<section class='page-heading'>
<h1 class='heading-title'>{tp.heading.title}</h1>
<p class='heading-sub'>{tp.heading.sub}</p>
</section>
<!-- ======= Guide Card ======= -->
<section class='guide-card'>
<div class='guide-inner'>
<!-- 装饰竖线(分隔左右两栏,绝对定位贯穿全高) -->
<span class='deco-line deco-v' aria-hidden='true'></span>
<div class='guide-grid'>
<!-- ============ 左栏 ============ -->
<div class='col col-left'>
<!-- 活动议程要点 -->
<div class='block agenda-points'>
<p class='point-lead'>
<span class='emoji'>{tp.agenda.leadEmoji}</span>
<span class='point-text'><Fragment set:html={tp.agenda.leadHtml} /></span>
</p>
<ul class='bullet-list'>
{
tp.agenda.bullets.map((html) => (
<li>
<Fragment set:html={html} />
</li>
))
}
</ul>
</div>
<!-- 装饰横线in-flow 居于两块之间gap 自动给上下等距)-->
<span class='deco-line deco-h' aria-hidden='true'></span>
<!-- 会议安排 -->
<div class='block schedule-block'>
<p class='section-title'>
<span class='emoji'>{tp.schedule.emoji}</span>
<span>{tp.schedule.title}</span>
</p>
<div class='info-card'>
<div class='info-row'>
<span class='info-label'>{tp.schedule.timeLabel}</span>
<span class='info-value'>{tp.schedule.timeValue}</span>
</div>
<div class='info-row info-row-stack'>
<span class='info-label'>{tp.schedule.flowLabel}</span>
<ul class='timeline-list'>
{
tp.schedule.flow.map((item) => (
<li>
<Fragment set:html={item} />
</li>
))
}
</ul>
</div>
</div>
</div>
</div>
<!-- ============ 右栏 ============ -->
<div class='col col-right'>
<div class='block food-block' id='food-block'>
<p class='section-title'>
<span class='emoji'>{tp.food.emoji}</span>
<span>{tp.food.title}</span>
</p>
<figure class='food-map'>
<img
src={foodMapSrc}
alt={tp.food.mapAlt}
class='food-map-img'
loading='lazy'
decoding='async'
/>
</figure>
</div>
</div>
</div>
</div>
</section>
</BaseLayout>
<style>
:global(.page) {
padding: 0 16px 16px;
--hero-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
@media (max-width: 639px) {
:global(.page) {
padding: 0 8px 8px;
}
}
@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes heroFadeUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heroFadeDown {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes decoFadeIn {
from {
opacity: 0;
}
to {
opacity: 0.65;
}
}
:global(.navbar) {
animation: heroFadeDown 0.45s var(--hero-ease) both;
}
/* ===== 页面标题区 ===== */
.page-heading {
padding: 48px 16px 24px;
text-align: center;
flex-shrink: 0;
}
.heading-title {
font-size: clamp(2.25rem, 5vw, 4rem);
font-weight: 600;
letter-spacing: -0.05em;
color: #030f20;
margin: 0 0 16px;
line-height: 1.1;
text-shadow: 0 4px 12px rgba(3, 15, 32, 0.06);
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.1s;
}
.heading-sub {
font-size: 16px;
color: #030f20;
letter-spacing: -0.05em;
margin: 0;
line-height: 1.4;
animation: heroFadeUp 0.5s var(--hero-ease) both;
animation-delay: 0.18s;
}
/* ===== 主卡片:双层圆角描边 ===== */
.guide-card {
position: relative;
margin: 16px 0 0;
border-radius: 24px;
border: 2px solid rgba(155, 206, 241, 0.8);
background: transparent;
padding: 10px;
overflow: hidden;
animation: heroFadeIn 0.5s var(--hero-ease) both;
animation-delay: 0.22s;
}
.guide-inner {
position: relative;
border-radius: 16px;
border: 2px solid rgba(155, 206, 241, 0.8);
background: rgba(249, 251, 255, 0.85);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
padding: 40px clamp(20px, 4vw, 56px);
min-height: 600px;
overflow: hidden;
}
/* === 装饰:用 nix-tile-100.svg 堆出来的横/竖分隔线 === */
.deco-line {
pointer-events: none;
opacity: 0.65;
background-image: url('/images/shared/nix-tile-100.svg');
background-position: center;
animation: decoFadeIn 0.6s var(--hero-ease) both;
animation-delay: 0.36s;
}
/* 横线:作为 .col-left 的 flex 成员位于两块之间,
依赖 .col { gap } 自动获得上下等距的呼吸距离 */
.deco-h {
position: relative;
display: block;
height: 24px;
background-size: 24px 24px;
background-repeat: repeat-x;
/* 左端:负 margin 反向延伸,让横线贴到卡片内描边(恢复原始 left:3px 的视觉位置) */
margin-left: calc(3px - clamp(20px, 4vw, 56px));
/* 右端:延伸到竖线 .deco-v 的左缘以补齐栏间 grid gap */
margin-right: calc(12px - clamp(24px, 4vw, 56px) / 2);
}
/* 竖线:仍保持绝对定位,贯穿整张卡片的高度 */
.deco-v {
position: absolute;
left: calc(50% - 12px);
top: 0;
width: 24px;
height: 100%;
background-size: 24px 24px;
background-repeat: repeat-y;
}
/* ===== 两栏网格 ===== */
.guide-grid {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: clamp(24px, 4vw, 56px);
z-index: 1;
}
.col {
display: flex;
flex-direction: column;
gap: 36px;
min-width: 0;
}
.block {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 左栏「活动议程要点」整体限宽到 480与下方 .info-card / 右栏 .food-block 三者同宽。
这样 .bullet-list li 底部的浅蓝 underline 右缘也会停在 info-card 的右缘上,
不再伸进中间分割线那条「走廊」里 */
.agenda-points {
max-width: 480px;
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.32s;
}
.emoji {
font-size: 20px;
line-height: 1;
display: inline-block;
margin-right: 12px;
vertical-align: middle;
}
.point-lead,
.section-title {
font-size: 20px;
font-weight: 500;
letter-spacing: -0.05em;
color: #030f20;
margin: 0;
line-height: 1.4;
display: flex;
align-items: center;
}
.point-lead .emoji {
margin-right: 8px;
}
:global(.accent) {
color: #5277c3;
font-weight: 500;
}
.bullet-list {
list-style: none;
margin: 0;
padding: 0 0 0 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.bullet-list li {
position: relative;
/* 左侧 22 用于让位 ::before 的圆点;右侧同样补 22 以保持文案 + 下划线视觉居中,
尤其是在移动端 li 宽度较大时,左右不对称会让整段看上去偏右 */
padding: 0 22px 4px 22px;
font-size: 20px;
line-height: 1.6;
color: #33466d;
letter-spacing: -0.05em;
font-weight: 500;
background: linear-gradient(rgba(126, 186, 228, 0.31), rgba(126, 186, 228, 0.31)) no-repeat;
background-position: 22px 100%;
background-size: calc(100% - 44px) 8px;
}
.bullet-list li::before {
content: '';
position: absolute;
left: 4px;
top: 0.7em;
width: 6px;
height: 6px;
border-radius: 50%;
background: #33466d;
}
.bullet-list li :global(strong) {
font-size: 24px;
font-weight: 600;
color: #33466d;
margin: 0 2px;
}
.info-card {
background: #fff;
border: 1px solid #5277c3;
border-radius: 24px;
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 26px;
box-shadow: 0 6px 22px rgba(82, 119, 195, 0.08);
max-width: 480px;
}
.schedule-block {
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.4s;
}
.info-row {
display: flex;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
}
.info-row-stack {
align-items: flex-start;
}
.info-label {
font-size: 16px;
color: #030f20;
letter-spacing: -0.05em;
line-height: 1.6;
flex-shrink: 0;
padding-top: 4px;
}
.info-value {
font-size: 20px;
font-weight: 500;
color: #030f20;
letter-spacing: -0.05em;
line-height: 1.4;
}
.timeline-list {
margin: 0;
padding-left: 22px;
list-style: disc;
display: flex;
flex-direction: column;
gap: 8px;
}
.timeline-list li {
font-size: 20px;
font-weight: 500;
color: #030f20;
letter-spacing: -0.05em;
line-height: 1.4;
}
.timeline-list li::marker {
color: #030f20;
font-size: 1em;
}
/* 锚点跳转时给目标区块一点呼吸距离 */
#food-block {
scroll-margin-top: 24px;
}
.food-block {
gap: 20px;
/* 与左栏 .info-card 镜像max-width 480 + 整体靠 col-right 右缘对齐,
这样「分割线 → food-map 左缘」的距离 = 「info-card 右缘 → 分割线」的距离 */
align-self: flex-end;
width: 100%;
max-width: 480px;
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.48s;
}
.food-map {
margin: 0;
border-radius: 24px;
overflow: hidden;
border: 1px solid #5277c3;
background: #f6fafc;
box-shadow: 0 8px 28px rgba(82, 119, 195, 0.1);
}
.food-map-img {
display: block;
width: 100%;
height: auto;
object-fit: cover;
}
@media (max-width: 1023px) {
.page-heading {
padding: 32px 16px 20px;
}
.guide-inner {
padding: 32px 32px;
min-height: 0;
}
.guide-grid {
grid-template-columns: 1fr;
gap: 40px;
}
.deco-h,
.deco-v {
display: none;
}
.info-card {
max-width: none;
}
.agenda-points {
max-width: none;
}
.food-block {
align-self: auto;
max-width: none;
width: auto;
}
}
@media (max-width: 639px) {
.page {
padding: 0 8px 8px;
}
.page-heading {
padding: 24px 8px 16px;
}
.heading-title {
margin-bottom: 12px;
}
.heading-sub {
font-size: 14px;
padding: 0 8px;
}
.guide-card {
border-radius: 16px;
padding: 6px;
}
.guide-inner {
border-radius: 10px;
padding: 24px 18px;
}
.col {
gap: 28px;
}
.point-lead,
.section-title {
font-size: 17px;
}
.bullet-list li {
font-size: 16px;
background-size: calc(100% - 44px) 6px;
}
.bullet-list li :global(strong) {
font-size: 19px;
}
.info-card {
padding: 20px 18px;
gap: 20px;
border-radius: 18px;
}
.info-label {
font-size: 14px;
}
.info-value {
font-size: 16px;
}
.timeline-list li {
font-size: 16px;
}
.info-row {
flex-direction: column;
gap: 4px;
}
.food-map {
border-radius: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
:global(.navbar),
.heading-title,
.heading-sub,
.guide-card,
.deco-line,
.agenda-points,
.schedule-block,
.food-block {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
.deco-line {
opacity: 0.65 !important;
}
}
</style>

View File

@@ -10,35 +10,70 @@ interface Props {
const { locale } = Astro.props;
const t = getTranslations(locale);
const tp = t.cmsGuide;
const subDelays = ['0.4s', '0.48s', '0.56s'];
---
<BaseLayout title={tp.meta.title} description={tp.meta.description} lang={tp.meta.htmlLang}>
<Navbar locale={locale} activePage='cmsGuide' />
<!-- ======= Hero BannerFigma Desktop - 4 切图) ======= -->
<section class='hero-banner' aria-labelledby='cms-title'>
<div class='hero-bg' aria-hidden='true'>
<img class='hero-bg-img' src='/images/cms-guide/hero-bg.png' alt='' />
<!-- ======= Hero Banner ======= -->
<section
class='relative pt-[38px] pb-12 px-6 text-center isolate overflow-hidden min-h-[215px] max-lg:pt-8 max-lg:pb-10 max-lg:px-5 max-sm:pt-6 max-sm:pb-8 max-sm:px-4'
aria-labelledby='cms-title'
>
<div class='absolute inset-0 z-0 pointer-events-none overflow-hidden' aria-hidden='true'>
<img
class='absolute inset-0 w-full h-full object-cover object-top block animate-hero-fade-in motion-reduce:animate-none motion-reduce:opacity-100'
src='/images/shared/hero-bg.png'
alt=''
/>
</div>
<div class='hero-content'>
<h1 class='hero-title' id='cms-title'>{tp.hero.title}</h1>
{tp.hero.subs.map((s) => <p class='hero-sub'>{s}</p>)}
<div class='relative z-[1] max-w-[720px] mx-auto'>
<h1
id='cms-title'
class='text-[clamp(2.5rem,5.5vw,4.5rem)] font-semibold text-brand-dark m-0 mb-5 tracking-[-0.05em] leading-[1.1] [text-shadow:0_4px_14px_rgba(3,15,32,0.08)] animate-hero-fade-up [animation-delay:0.1s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:mb-3'
>
{tp.hero.title}
</h1>
{
tp.hero.subs.map((s, i) => (
<p
class='hero-sub text-[clamp(0.875rem,1vw,1rem)] text-[#1a2332] m-0 leading-[1.7] tracking-[-0.02em] animate-hero-fade-up motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:text-[13px] max-sm:leading-[1.65]'
style={`animation-delay:${subDelays[i] ?? '0.56s'}`}
>
{s}
</p>
))
}
</div>
</section>
<!-- ======= Guide content ======= -->
<main class='guide-main'>
<main
class='relative z-[2] w-full max-w-[1120px] mx-auto px-10 pt-6 pb-[120px] flex flex-col gap-10 max-lg:px-7 max-lg:pt-4 max-lg:pb-[100px] max-lg:gap-9 max-sm:px-[18px] max-sm:pt-2 max-sm:pb-20 max-sm:gap-8'
>
{
tp.steps.map((step, i) => (
<article class='step' id={step.id} style={`--step-delay: ${0.56 + i * 0.06}s`}>
<h2 class='step-title'>
<span class='step-num'>{step.num}</span>
<span class='step-divider'></span>
<span class='step-name'>{step.name}</span>
<article
class='flex flex-col gap-4 animate-hero-fade-up [animation-delay:var(--step-delay,0.56s)] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:gap-3'
id={step.id}
style={`--step-delay: ${0.56 + i * 0.06}s`}
>
<h2 class='flex items-baseline gap-0 m-0 font-semibold leading-[1.2] tracking-[-0.04em] text-brand-dark'>
<span class='text-[clamp(1.75rem,2.4vw,2.25rem)] font-bold text-brand-dark [font-feature-settings:"tnum"] mr-2 max-sm:text-[28px]'>
{step.num}
</span>
<span class='text-[clamp(1.5rem,2vw,1.875rem)] text-[#a7b8d0] mr-[6px] font-normal max-sm:text-[22px]'>
</span>
<span class='text-[clamp(1.5rem,2vw,1.875rem)] text-brand-dark font-semibold max-sm:text-[22px]'>
{step.name}
</span>
</h2>
{'bodyHtml' in step && step.bodyHtml !== undefined && (
<p class='step-body'>
<p class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-sm:text-sm max-sm:leading-[1.75]'>
<Fragment set:html={step.bodyHtml} />
</p>
)}
@@ -46,32 +81,32 @@ const tp = t.cmsGuide;
{'bodyHtmls' in step &&
step.bodyHtmls !== undefined &&
step.bodyHtmls.map((html) => (
<p class='step-body'>
<p class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-sm:text-sm max-sm:leading-[1.75]'>
<Fragment set:html={html} />
</p>
))}
{'list' in step && step.list !== undefined && (
<ul class='step-list'>
<ul class='step-list m-0 pl-6 text-[15px] leading-[1.9] text-[#1a2332] max-sm:text-sm'>
{step.list.map((item) => (
<li>{item}</li>
<li class='pl-1'>{item}</li>
))}
</ul>
)}
{'afterListHtml' in step && step.afterListHtml !== undefined && (
<p class='step-body'>
<p class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-sm:text-sm max-sm:leading-[1.75]'>
<Fragment set:html={step.afterListHtml} />
</p>
)}
{step.tip?.text && (
<div class='tip tip--inline'>
<p>
<span class='tip-bell' aria-hidden='true'>
<div class='tip text-sm leading-[1.7] text-[#c66a2a] bg-[rgba(245,166,35,0.06)] border-l-[3px] border-[rgba(245,166,35,0.5)] rounded-r-lg py-3 px-4 mt-1 max-sm:text-[13px] max-sm:py-2.5 max-sm:px-3'>
<p class='m-0'>
<span class='inline-block mr-1' aria-hidden='true'>
{tp.tipBell}
</span>
<span class='tip-label'>{tp.tipLabel}</span>
<span class='font-semibold mr-0.5'>{tp.tipLabel}</span>
{step.tip.text}
</p>
</div>
@@ -80,355 +115,4 @@ const tp = t.cmsGuide;
))
}
</main>
<!-- ======= 底部同心圆装饰Figma 切图) ======= -->
<div class='bottom-circles' aria-hidden='true'>
<img src='/images/cms-guide/bottom-circles.png' alt='' />
</div>
</BaseLayout>
<style>
:global(.page) {
position: relative;
overflow: hidden;
--hero-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes heroFadeUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heroFadeDown {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:global(.navbar) {
animation: heroFadeDown 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* ===== Hero Banner ===== */
.hero-banner {
position: relative;
padding: 38px 24px 48px;
text-align: center;
isolation: isolate;
overflow: hidden;
min-height: 215px;
}
.hero-bg {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.hero-bg-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
display: block;
animation: heroFadeIn 0.6s var(--hero-ease) both;
}
.hero-content {
position: relative;
z-index: 1;
max-width: 720px;
margin: 0 auto;
}
.hero-title {
font-size: clamp(2.5rem, 5.5vw, 4.5rem);
font-weight: 600;
color: #030f20;
margin: 0 0 20px;
letter-spacing: -0.05em;
line-height: 1.1;
text-shadow: 0 4px 14px rgba(3, 15, 32, 0.08);
animation: heroFadeUp 0.45s var(--hero-ease) both;
animation-delay: 0.1s;
}
.hero-sub {
font-size: clamp(0.875rem, 1vw, 1rem);
color: #1a2332;
margin: 0;
line-height: 1.7;
letter-spacing: -0.02em;
animation: heroFadeUp 0.45s var(--hero-ease) both;
}
.hero-sub:nth-of-type(1) {
animation-delay: 0.4s;
}
.hero-sub:nth-of-type(2) {
animation-delay: 0.48s;
}
.hero-sub:nth-of-type(3) {
animation-delay: 0.56s;
}
.hero-sub + .hero-sub {
margin-top: 2px;
}
/* ===== Guide content ===== */
.guide-main {
position: relative;
z-index: 2;
width: 100%;
max-width: 1120px;
margin: 0 auto;
padding: 24px 40px 120px;
display: flex;
flex-direction: column;
gap: 40px;
}
.step {
display: flex;
flex-direction: column;
gap: 16px;
animation: heroFadeUp 0.45s var(--hero-ease) both;
animation-delay: var(--step-delay, 0.56s);
}
.step-title {
display: flex;
align-items: baseline;
gap: 0;
margin: 0;
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.04em;
color: #030f20;
}
.step-num {
font-size: clamp(1.75rem, 2.4vw, 2.25rem);
font-weight: 700;
color: #030f20;
font-feature-settings: 'tnum';
margin-right: 8px;
}
.step-divider {
font-size: clamp(1.5rem, 2vw, 1.875rem);
color: #a7b8d0;
margin: 0 6px 0 0;
font-weight: 400;
}
.step-name {
font-size: clamp(1.5rem, 2vw, 1.875rem);
color: #030f20;
font-weight: 600;
}
.step-body {
font-size: 15px;
line-height: 1.8;
color: #1a2332;
margin: 0;
letter-spacing: -0.01em;
}
.step-list {
margin: 0;
padding-left: 24px;
font-size: 15px;
line-height: 1.9;
color: #1a2332;
}
.step-list li {
padding-left: 4px;
}
.step-list li::marker {
color: #5277c3;
}
/* "前两周" 高亮red 胶囊)—— 通过 set:html 注入,需要 :global */
:global(.hl-warn) {
display: inline-block;
padding: 0 8px;
margin: 0 2px;
color: #d94f4f;
font-weight: 600;
border: 1px solid rgba(217, 79, 79, 0.45);
border-radius: 8px;
background: rgba(217, 79, 79, 0.06);
}
/* 提示框(🔔) */
.tip {
font-size: 14px;
line-height: 1.7;
color: #c66a2a;
background: rgba(245, 166, 35, 0.06);
border-left: 3px solid rgba(245, 166, 35, 0.5);
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin-top: 4px;
}
.tip p {
margin: 0;
}
.tip p + p {
margin-top: 4px;
}
.tip-head {
font-weight: 600;
}
.tip-bell {
display: inline-block;
margin-right: 4px;
}
.tip--inline {
padding: 8px 14px;
}
.tip-label {
font-weight: 600;
margin-right: 2px;
}
/* ===== 底部同心圆装饰 ===== */
.bottom-circles {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: min(710px, 100%);
pointer-events: none;
z-index: 0;
animation: heroFadeIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.92s;
}
.bottom-circles img {
display: block;
width: 100%;
height: auto;
}
@media (max-width: 1023px) {
.hero-banner {
padding: 32px 20px 40px;
}
.guide-main {
padding: 16px 28px 100px;
gap: 36px;
}
.bottom-circles {
width: min(560px, 100%);
}
}
@media (max-width: 639px) {
.hero-banner {
padding: 24px 16px 32px;
}
.hero-title {
margin-bottom: 12px;
}
.hero-sub {
font-size: 13px;
line-height: 1.65;
}
.guide-main {
padding: 8px 18px 80px;
gap: 32px;
}
.step {
gap: 12px;
}
.step-num {
font-size: 28px;
}
.step-divider,
.step-name {
font-size: 22px;
}
.step-body {
font-size: 14px;
line-height: 1.75;
}
.step-list {
font-size: 14px;
}
.tip {
font-size: 13px;
padding: 10px 12px;
}
.bottom-circles {
width: min(380px, 100%);
}
}
@media (prefers-reduced-motion: reduce) {
:global(.navbar),
.hero-bg-img,
.hero-title,
.hero-sub,
.step {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
.bottom-circles {
animation: none !important;
opacity: 1 !important;
transform: translateX(-50%) !important;
}
}
</style>

View File

@@ -0,0 +1,133 @@
---
import Navbar from './Navbar.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { type Locale, getTranslations } from '../i18n/config';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const t = getTranslations(locale);
const tp = t.eventGuide;
const foodMapSrc = locale === 'en' ? '/images/event-guide/food-map-en.png' : '/images/event-guide/food-map.png';
---
<BaseLayout title={tp.meta.title} description={tp.meta.description} lang={tp.meta.htmlLang}>
<Navbar locale={locale} activePage='eventGuide' />
<!-- ======= Hero Banner ======= -->
<section
class='relative pt-[38px] pb-12 px-6 text-center isolate overflow-hidden min-h-[215px] max-lg:pt-8 max-lg:pb-10 max-lg:px-5 max-sm:pt-6 max-sm:pb-8 max-sm:px-4'
aria-labelledby='souvenir-title'
>
<div class='absolute inset-0 z-0 pointer-events-none overflow-hidden' aria-hidden='true'>
<img
class='absolute inset-0 w-full h-full object-cover object-top block animate-hero-fade-in motion-reduce:animate-none motion-reduce:opacity-100'
src='/images/shared/hero-bg.png'
alt=''
/>
</div>
<div class='relative z-[1] max-w-[720px] mx-auto'>
<h1
id='souvenir-title'
class='text-[clamp(2.5rem,5.5vw,4.5rem)] font-semibold text-brand-dark m-0 mb-4 tracking-[-0.05em] leading-[1.1] [text-shadow:0_4px_14px_rgba(3,15,32,0.08)] animate-hero-fade-up [animation-delay:0.1s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:mb-3'
>
{tp.heading.title}
</h1>
<p
class='text-[clamp(0.875rem,1vw,1rem)] text-[#1a2332] m-0 leading-[1.7] tracking-[-0.02em] max-w-[640px] mx-auto animate-hero-fade-up [animation-delay:0.18s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:text-[13px] max-sm:leading-[1.65]'
>
{tp.heading.sub}
</p>
</div>
</section>
<!-- ======= Bento Grid ======= -->
<div
class='mx-auto mt-4 grid grid-cols-[32rem_1fr] grid-rows-[auto_auto] gap-[10px] max-lg:grid-cols-1 max-lg:auto-rows-auto max-sm:gap-2'
>
<!-- Tile: Agenda -->
<div
class='col-start-1 row-start-1 flex flex-col gap-4 bg-surface border-2 border-border rounded-[20px] p-6 px-7 backdrop-blur-sm [-webkit-backdrop-filter:blur(4px)] shadow-[0_4px_16px_rgba(82,119,195,0.06)] animate-hero-fade-up [animation-delay:0.28s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-lg:col-auto max-lg:row-auto max-sm:rounded-2xl max-sm:p-5 max-sm:px-[18px]'
>
<p
class='text-xl font-medium tracking-[-0.05em] text-brand-dark m-0 leading-[1.4] flex items-center max-sm:text-[17px]'
>
<span class='text-xl leading-none inline-block mr-2 align-middle'>{tp.agenda.leadEmoji}</span>
<span><Fragment set:html={tp.agenda.leadHtml} /></span>
</p>
<ul class='bullet-list list-none m-0 pl-3 flex flex-col gap-1'>
{
tp.agenda.bullets.map((html) => (
<li class='relative px-[22px] pb-1 text-xl leading-[1.6] text-[#33466d] tracking-[-0.05em] font-medium [background-position:22px_100%] [background-size:calc(100%-44px)_8px] max-sm:text-base max-sm:[background-size:calc(100%-44px)_6px]'>
<Fragment set:html={html} />
</li>
))
}
</ul>
</div>
<!-- Tile: Schedule -->
<div
class='col-start-1 row-start-2 flex flex-col gap-4 bg-surface border-2 border-border rounded-[20px] p-6 px-7 backdrop-blur-sm [-webkit-backdrop-filter:blur(4px)] shadow-[0_4px_16px_rgba(82,119,195,0.06)] animate-hero-fade-up [animation-delay:0.38s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-lg:col-auto max-lg:row-auto max-sm:rounded-2xl max-sm:p-5 max-sm:px-[18px]'
>
<p
class='text-xl font-medium tracking-[-0.05em] text-brand-dark m-0 leading-[1.4] flex items-center max-sm:text-[17px]'
>
<span class='text-xl leading-none inline-block mr-3 align-middle'>{tp.schedule.emoji}</span>
<span>{tp.schedule.title}</span>
</p>
<div
class='info-card bg-white border border-brand-blue rounded-2xl p-5 px-[22px] flex flex-col gap-[22px] shadow-[0_4px_16px_rgba(82,119,195,0.08)] max-sm:p-4 max-sm:gap-4 max-sm:rounded-[14px]'
>
<div class='flex items-start gap-3 flex-wrap max-sm:flex-col max-sm:gap-1'>
<span class='text-base text-brand-dark leading-[1.6] flex-shrink-0 pt-px max-sm:text-sm'>
{tp.schedule.timeLabel}
</span>
<span class='text-xl font-medium text-brand-dark leading-[1.4] max-sm:text-base'>
{tp.schedule.timeValue}
</span>
</div>
<div class='flex items-start gap-3 flex-wrap max-sm:flex-col max-sm:gap-1'>
<span class='text-base text-brand-dark leading-[1.6] flex-shrink-0 pt-px max-sm:text-sm'>
{tp.schedule.flowLabel}
</span>
<ul class='timeline-list m-0 pl-[22px] list-disc flex flex-col gap-1'>
{
tp.schedule.flow.map((item) => (
<li class='text-xl font-medium text-brand-dark leading-[1.4] max-sm:text-base'>
<Fragment set:html={item} />
</li>
))
}
</ul>
</div>
</div>
</div>
<!-- Tile: Food Map (spans both rows) -->
<div
id='food-block'
class='col-start-2 row-[1/span_2] flex flex-col gap-4 bg-surface border-2 border-border rounded-[20px] p-6 px-7 backdrop-blur-sm [-webkit-backdrop-filter:blur(4px)] shadow-[0_4px_16px_rgba(82,119,195,0.06)] scroll-mt-6 animate-hero-fade-up [animation-delay:0.34s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-lg:col-auto max-lg:row-auto max-sm:rounded-2xl max-sm:p-5 max-sm:px-[18px]'
>
<p
class='text-xl font-medium tracking-[-0.05em] text-brand-dark m-0 leading-[1.4] flex items-center max-sm:text-[17px]'
>
<span class='text-xl leading-none inline-block mr-3 align-middle'>{tp.food.emoji}</span>
<span>{tp.food.title}</span>
</p>
<figure
class='food-map flex-1 m-0 min-h-40 rounded-[14px] overflow-hidden bg-[#f6fafc] flex max-lg:block max-sm:rounded-xl'
>
<img
src={foodMapSrc}
alt={tp.food.mapAlt}
class='food-map-img block w-full object-contain object-top max-lg:h-auto'
loading='lazy'
decoding='async'
/>
</figure>
</div>
</div>
</BaseLayout>

View File

@@ -2,7 +2,7 @@
import Navbar from './Navbar.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { type Locale, getTranslations, getPageUrl } from '../i18n/config';
import { ArrowRight } from '@lucide/astro';
interface Props {
locale: Locale;
}
@@ -18,46 +18,92 @@ const url = (p: Parameters<typeof getPageUrl>[1]) => getPageUrl(locale, p);
<Navbar locale={locale} activePage='home' />
<!-- ======= Hero Card ======= -->
<div class='hero-card'>
<!-- Layer 0: dot-grid texture (bottom-most) -->
<div class='bg-texture' aria-hidden='true'></div>
<!-- Layer 1a: concentric circles (top-left) -->
<div class='bg-circles' aria-hidden='true'>
<div class='circle c1'></div>
<div class='circle c2'></div>
<div class='circle c3'></div>
<div
class='flex-1 relative flex flex-col rounded-3xl border-2 border-border bg-[#f9fbff] overflow-hidden min-h-[640px] animate-hero-fade-in motion-reduce:animate-none motion-reduce:opacity-100 max-sm:rounded-2xl max-sm:min-h-auto'
>
<!-- Layer 0: dot-grid texture -->
<div
class='absolute inset-0 z-0 [background-image:radial-gradient(ellipse_80%_70%_at_60%_48%,transparent_15%,rgba(249,251,255,0.85)_85%),radial-gradient(circle,rgba(126,186,228,0.28)_2.5px,transparent_2.5px)] [background-size:auto,20px_20px] pointer-events-none will-change-transform animate-hero-fade-in [animation-delay:0.06s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:will-change-auto'
aria-hidden='true'
>
</div>
<!-- Layer 1b: interactive NixOS snowflake — background decoration anchored to bottom-right -->
<div class='hero-decoration' aria-hidden='true'>
<canvas id='nixflake'></canvas>
<!-- Layer 1a: concentric circles -->
<div
class='absolute inset-0 z-[1] pointer-events-none will-change-transform motion-reduce:will-change-auto'
aria-hidden='true'
>
<div
class='absolute rounded-full border-[1.5px] border-[rgba(126,186,228,0.28)] top-0 left-0 shadow-[0_2px_8px_rgba(126,186,228,0.12),0_10px_28px_rgba(126,186,228,0.2)] animate-hero-fade-in [animation-delay:0.1s] w-[502px] h-[502px] translate-x-[-269px] translate-y-[-241px] motion-reduce:animate-none motion-reduce:opacity-100 max-lg:w-[300px] max-lg:h-[300px] max-lg:translate-x-[-160px] max-lg:translate-y-[-144px] max-sm:w-[220px] max-sm:h-[220px] max-sm:translate-x-[-120px] max-sm:translate-y-[-108px]'
>
</div>
<div
class='absolute rounded-full border-[1.5px] border-[rgba(126,186,228,0.28)] top-0 left-0 shadow-[0_2px_8px_rgba(126,186,228,0.12),0_10px_28px_rgba(126,186,228,0.2)] animate-hero-fade-in [animation-delay:0.16s] w-[459px] h-[459px] translate-x-[-248px] translate-y-[-220px] motion-reduce:animate-none motion-reduce:opacity-100 max-lg:w-[275px] max-lg:h-[275px] max-lg:translate-x-[-148px] max-lg:translate-y-[-132px] max-sm:w-[200px] max-sm:h-[200px] max-sm:translate-x-[-108px] max-sm:translate-y-[-96px]'
>
</div>
<div
class='absolute rounded-full border-[1.5px] border-[rgba(126,186,228,0.28)] top-0 left-0 shadow-[0_2px_8px_rgba(126,186,228,0.12),0_10px_28px_rgba(126,186,228,0.2)] animate-hero-fade-in [animation-delay:0.22s] w-[408px] h-[408px] translate-x-[-222px] translate-y-[-199px] motion-reduce:animate-none motion-reduce:opacity-100 max-lg:w-[244px] max-lg:h-[244px] max-lg:translate-x-[-133px] max-lg:translate-y-[-119px] max-sm:w-[178px] max-sm:h-[178px] max-sm:translate-x-[-96px] max-sm:translate-y-[-86px]'
>
</div>
</div>
<!-- Layer 2: foreground content (text floats above all background layers) -->
<div class='hero-content'>
<div class='hero-text'>
<p class='conference-title'>
<!-- Layer 1b: NixOS snowflake canvas -->
<div
class='absolute right-[-80px] bottom-[-120px] z-[2] pointer-events-none leading-none animate-hero-fade-in [animation-delay:0.18s] motion-reduce:animate-none motion-reduce:opacity-100 max-lg:right-[-120px] max-lg:bottom-[-140px] max-lg:scale-[0.7] max-lg:origin-bottom-right max-sm:block max-sm:right-[-90px] max-sm:bottom-[-100px] max-sm:scale-[0.7] max-sm:origin-bottom-right max-sm:animate-hero-fade-in-70 motion-reduce:max-sm:opacity-70 [@media(max-width:639px)_and_(max-height:683px)]:hidden'
aria-hidden='true'
>
<canvas id='nixflake' class='block max-w-none pointer-events-none'></canvas>
</div>
<!-- Layer 2: foreground content -->
<div
class='flex-1 relative z-[3] flex items-center min-h-[640px] p-12 px-20 max-lg:p-10 max-lg:px-10 max-lg:pb-12 max-lg:min-h-[520px] max-sm:p-8 max-sm:px-6 max-sm:pb-11 max-sm:min-h-[560px]'
>
<div class='w-full max-w-[640px] will-change-transform motion-reduce:will-change-auto max-lg:max-w-full'>
<p
class='text-[clamp(1.5rem,3.33vw,3rem)] font-semibold tracking-[-0.05em] text-brand-dark m-0 mb-4 leading-[1.2] [text-shadow:0_4px_12px_rgba(3,15,32,0.1)] animate-hero-fade-up [animation-delay:0.12s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none'
>
{tp.hero.conferenceTitle}
<span class='year'>{tp.hero.year}</span>
<span class='text-[#7ebae4]'>{tp.hero.year}</span>
</p>
<h1 class:list={['headline', { 'headline--split-mobile': locale === 'en' }]}>
{tp.hero.headlineBefore}<span class='nix-highlight'>{tp.hero.headlineBadge}</span><span
class='headline-tail'
set:html={tp.hero.headlineAfter}
/>
<h1
class:list={[
'headline text-[clamp(2rem,4.44vw,4rem)] font-semibold tracking-[-0.05em] text-brand-blue m-0 mb-7 leading-[1.15] [text-shadow:0_6px_18px_rgba(82,119,195,0.18)] [perspective:800px] [perspective-origin:50%_50%] animate-hero-fade-up [animation-delay:0.22s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:leading-[1.35] max-sm:[perspective:none]',
{ 'headline--split-mobile': locale === 'en' },
]}
>
{tp.hero.headlineBefore}<span
class='nix-highlight relative inline-block py-0.5 px-[14px] leading-[inherit] rounded-full [background:linear-gradient(135deg,rgba(196,226,247,0.65)_0%,rgba(126,186,228,0.42)_48%,rgba(82,140,200,0.34)_100%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.7),inset_0_-1px_2px_rgba(82,119,195,0.2),0_6px_14px_rgba(82,119,195,0.22),0_14px_30px_rgba(82,119,195,0.14)] [transform-style:preserve-3d] will-change-transform translate-z-0 transition-shadow duration-300 [--gloss-x:30%] [--gloss-y:25%] motion-reduce:will-change-auto motion-reduce:transition-none max-sm:py-0 max-sm:px-[9px] max-sm:pb-0.5 max-sm:rounded-[999px] max-sm:bg-[rgba(197,219,245,0.78)] max-sm:text-brand-blue max-sm:text-[1.22em] max-sm:leading-[1.05] max-sm:align-baseline max-sm:shadow-none max-sm:[text-shadow:3px_4px_0_#9eb8e8] max-sm:![transform:none] max-sm:[transform-style:flat] max-sm:will-change-auto max-sm:transition-none'
>
{tp.hero.headlineBadge}
</span><span class='headline-tail' set:html={tp.hero.headlineAfter} />
</h1>
<p class='lead'>{tp.hero.lead}</p>
<div class='cta-group'>
<a class='btn btn-primary' href={url('cmsGuide')}>{tp.hero.ctaPrimary}</a>
<a class='btn btn-outline' href={url('cmsGuide')}>{tp.hero.ctaSecondary}</a>
<p
class='text-[clamp(1rem,1.39vw,1.25rem)] text-[#1a2332] m-0 mb-10 leading-[1.65] tracking-[-0.02em] max-w-[480px] animate-hero-fade-up [animation-delay:0.32s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:mb-8'
>
{tp.hero.lead}
</p>
<div class='flex gap-3 flex-wrap max-sm:flex-col max-sm:items-start max-sm:gap-[10px]'>
<a
class='inline-flex items-center px-5 py-3 rounded-3xl text-base font-medium tracking-[-0.05em] no-underline transition-[background,border-color,transform,box-shadow] duration-200 whitespace-nowrap cursor-pointer font-[inherit] animate-hero-fade-up [animation-delay:0.42s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none bg-brand-blue text-white border-[1.5px] border-brand-blue hover:bg-[#4269b8] hover:border-[#4269b8] hover:-translate-y-0.5 hover:shadow-[0_6px_20px_rgba(82,119,195,0.35)] max-sm:w-full max-sm:justify-center gap-1'
href='https://nix.org.cn/app/'
>
{tp.hero.ctaPrimary}
<ArrowRight size={24} />
</a>
<a
class='inline-flex items-center px-5 py-3 rounded-3xl text-base font-medium tracking-[-0.05em] no-underline transition-[background,border-color,transform,box-shadow] duration-200 whitespace-nowrap cursor-pointer font-[inherit] animate-hero-fade-up [animation-delay:0.5s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none bg-[rgba(249,251,255,0.45)] text-brand-blue border-[1.5px] border-brand-blue backdrop-blur-[10px] backdrop-saturate-[140%] [-webkit-backdrop-filter:blur(10px)_saturate(1.4)] hover:bg-[rgba(82,119,195,0.12)] hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(82,119,195,0.15)] max-sm:w-full max-sm:justify-center'
href={url('cmsGuide')}
>
{tp.hero.ctaSecondary}
</a>
</div>
</div>
</div>
</div>
</BaseLayout>
<!-- ======= Interactive NixOS snowflake canvas + background parallax ======= -->
<!-- ======= Scripts ======= -->
<script>
import { initNixflakeAnimation } from '../scripts/nixflake-animation';
import { initBadgeTilt } from '../scripts/badge-tilt';
@@ -67,510 +113,3 @@ const url = (p: Parameters<typeof getPageUrl>[1]) => getPageUrl(locale, p);
initBadgeTilt();
initBgParallax();
</script>
<style>
:global(.page) {
padding: 0 16px 16px;
}
@media (max-width: 639px) {
:global(.page) {
padding: 0 8px 8px;
}
}
@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes heroFadeInTo70 {
from {
opacity: 0;
}
to {
opacity: 0.7;
}
}
@keyframes heroFadeUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heroFadeDown {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:global(.navbar) {
animation: heroFadeDown 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.hero-card {
--hero-ease: cubic-bezier(0.22, 1, 0.36, 1);
flex: 1;
position: relative;
display: flex;
flex-direction: column;
border-radius: 24px;
border: 2px solid rgba(155, 206, 241, 0.8);
background: #f9fbff;
overflow: hidden;
min-height: 640px;
animation: heroFadeIn 0.5s var(--hero-ease) both;
}
.bg-circles {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
will-change: transform;
}
.circle {
position: absolute;
border-radius: 50%;
border: 1.5px solid rgba(126, 186, 228, 0.28);
top: 0;
left: 0;
box-shadow:
0 2px 8px rgba(126, 186, 228, 0.12),
0 10px 28px rgba(126, 186, 228, 0.2);
animation: heroFadeIn 0.8s var(--hero-ease) both;
}
.c1 {
width: 502px;
height: 502px;
transform: translate(-269px, -241px);
animation-delay: 0.1s;
}
.c2 {
width: 459px;
height: 459px;
transform: translate(-248px, -220px);
animation-delay: 0.16s;
}
.c3 {
width: 408px;
height: 408px;
transform: translate(-222px, -199px);
animation-delay: 0.22s;
}
.bg-texture {
position: absolute;
inset: 0;
z-index: 0;
background-image:
radial-gradient(ellipse 80% 70% at 60% 48%, transparent 15%, rgba(249, 251, 255, 0.85) 85%),
radial-gradient(circle, rgba(126, 186, 228, 0.28) 2.5px, transparent 2.5px);
background-size:
auto,
20px 20px;
pointer-events: none;
will-change: transform;
animation: heroFadeIn 0.7s var(--hero-ease) both;
animation-delay: 0.06s;
}
.hero-content {
flex: 1;
position: relative;
z-index: 3;
display: flex;
align-items: center;
min-height: 640px;
padding: 48px 80px;
}
.hero-text {
width: 100%;
max-width: 640px;
will-change: transform;
}
.conference-title {
font-size: clamp(1.5rem, 3.33vw, 3rem);
font-weight: 600;
letter-spacing: -0.05em;
color: #030f20;
margin: 0 0 16px;
line-height: 1.2;
text-shadow: 0 4px 12px rgba(3, 15, 32, 0.1);
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.12s;
}
.year {
color: #7ebae4;
}
.headline {
font-size: clamp(2rem, 4.44vw, 4rem);
font-weight: 600;
letter-spacing: -0.05em;
color: #5277c3;
margin: 0 0 28px;
line-height: 1.15;
text-shadow: 0 6px 18px rgba(82, 119, 195, 0.18);
perspective: 800px;
perspective-origin: 50% 50%;
animation: heroFadeUp 0.6s var(--hero-ease) both;
animation-delay: 0.22s;
}
.nix-highlight {
position: relative;
display: inline-block;
padding: 2px 14px;
line-height: inherit;
border-radius: 100px;
background: linear-gradient(
135deg,
rgba(196, 226, 247, 0.65) 0%,
rgba(126, 186, 228, 0.42) 48%,
rgba(82, 140, 200, 0.34) 100%
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.7),
inset 0 -1px 2px rgba(82, 119, 195, 0.2),
0 6px 14px rgba(82, 119, 195, 0.22),
0 14px 30px rgba(82, 119, 195, 0.14);
transform-style: preserve-3d;
will-change: transform;
transform: translateZ(0);
transition: box-shadow 0.3s ease;
--gloss-x: 30%;
--gloss-y: 25%;
}
.nix-highlight::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
circle at var(--gloss-x) var(--gloss-y),
rgba(255, 255, 255, 0.55) 0%,
rgba(255, 255, 255, 0.18) 28%,
rgba(255, 255, 255, 0) 60%
);
mix-blend-mode: screen;
pointer-events: none;
transform: translateZ(1px);
}
.nix-highlight::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
border: 1px solid rgba(255, 255, 255, 0.45);
pointer-events: none;
transform: translateZ(1px);
}
/* 英文桌面:徽章与后半句间距(移动端由 block 换行,不需前导空格) */
.headline--split-mobile .nix-highlight {
margin-inline-end: 0.22em;
}
.lead {
font-size: clamp(1rem, 1.39vw, 1.25rem);
color: #1a2332;
margin: 0 0 40px;
line-height: 1.65;
letter-spacing: -0.02em;
max-width: 480px;
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.32s;
}
.cta-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
padding: 12px 20px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
letter-spacing: -0.05em;
text-decoration: none;
transition:
background 0.2s,
border-color 0.2s,
transform 0.15s,
box-shadow 0.2s;
white-space: nowrap;
cursor: pointer;
font-family: inherit;
animation: heroFadeUp 0.5s var(--hero-ease) both;
}
.cta-group .btn:nth-child(1) {
animation-delay: 0.42s;
}
.cta-group .btn:nth-child(2) {
animation-delay: 0.5s;
}
.btn-primary {
background: #5277c3;
color: #fff;
border: 1.5px solid #5277c3;
}
.btn-primary:hover {
background: #4269b8;
border-color: #4269b8;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(82, 119, 195, 0.35);
}
.btn-outline {
background: rgba(249, 251, 255, 0.45);
color: #5277c3;
border: 1.5px solid #5277c3;
backdrop-filter: blur(10px) saturate(1.4);
-webkit-backdrop-filter: blur(10px) saturate(1.4);
}
.btn-outline:hover {
background: rgba(82, 119, 195, 0.12);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(82, 119, 195, 0.15);
}
.hero-decoration {
position: absolute;
right: -80px;
bottom: -120px;
z-index: 2;
pointer-events: none;
line-height: 0;
animation: heroFadeIn 0.9s var(--hero-ease) both;
animation-delay: 0.18s;
}
.hero-decoration canvas {
display: block;
max-width: none;
pointer-events: none;
}
@media (max-width: 1023px) {
.hero-content {
padding: 40px 40px 48px;
min-height: 520px;
}
.hero-text {
max-width: 100%;
}
.hero-decoration {
right: -120px;
bottom: -140px;
transform: scale(0.7);
transform-origin: bottom right;
}
.c1 {
width: 300px;
height: 300px;
transform: translate(-160px, -144px);
}
.c2 {
width: 275px;
height: 275px;
transform: translate(-148px, -132px);
}
.c3 {
width: 244px;
height: 244px;
transform: translate(-133px, -119px);
}
}
@media (max-width: 639px) {
.headline :global(.headline-break) {
display: none;
}
.headline {
line-height: 1.35;
}
/* 英文:徽章后整段换行,第二行与第一行左缘对齐 */
.headline--split-mobile .nix-highlight {
margin-inline-end: 0;
}
.headline--split-mobile .headline-tail {
display: block;
margin: 0;
padding: 0;
}
.hero-card {
border-radius: 16px;
min-height: auto;
}
.hero-content {
padding: 32px 24px 44px;
min-height: 560px;
}
.lead {
margin-bottom: 32px;
}
.hero-decoration {
display: block;
right: -90px;
bottom: -100px;
transform: scale(0.7);
transform-origin: bottom right;
animation-name: heroFadeInTo70;
}
.cta-group {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.btn {
width: 100%;
justify-content: center;
}
.c1 {
width: 220px;
height: 220px;
transform: translate(-120px, -108px);
}
.c2 {
width: 200px;
height: 200px;
transform: translate(-108px, -96px);
}
.c3 {
width: 178px;
height: 178px;
transform: translate(-96px, -86px);
}
}
@media (max-width: 639px) and (max-height: 683px) {
.hero-decoration {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
:global(.navbar),
.hero-card,
.bg-texture,
.circle,
.hero-decoration,
.conference-title,
.headline,
.lead,
.btn {
animation: none !important;
opacity: 1 !important;
}
.hero-decoration,
.bg-circles,
.bg-texture,
.hero-text,
.nix-highlight {
will-change: auto;
transition: none !important;
}
.conference-title,
.headline,
.lead,
.btn,
:global(.navbar) {
transform: none !important;
}
.nix-highlight {
transform: none !important;
}
}
@media (prefers-reduced-motion: reduce) and (max-width: 639px) {
.hero-decoration {
opacity: 0.7 !important;
}
}
/* 移动端:设计稿静态徽章(浅蓝胶囊 + 叠字阴影) */
@media (max-width: 639px) {
.headline {
perspective: none;
}
.nix-highlight {
position: relative;
display: inline-block;
padding: 0 9px 2px;
margin-inline: 0;
border-radius: 999px;
background: rgba(197, 219, 245, 0.78);
color: #5277c3;
font-size: 1.22em;
line-height: 1.05;
vertical-align: baseline;
box-shadow: none;
/* 单层文字 + 右下偏移浅色影,避免 ::before 叠在主字上方 */
text-shadow: 3px 4px 0 #9eb8e8;
transform: none !important;
transform-style: flat;
will-change: auto;
transition: none;
}
.nix-highlight::before,
.nix-highlight::after {
display: none;
}
}
</style>

View File

@@ -16,49 +16,61 @@ const url = (l: Locale, p: Page) => getPageUrl(l, p);
const switchHref = url(otherLocale, activePage);
---
<header class='navbar'>
<a href={url(locale, 'home')} class='navbar-logo' aria-label={t.nav.ariaLogoHome}>
<img src='/images/shared/nix-cn.svg' alt='NixCN' width='110' height='31' class='logo-img' />
<header class='flex items-center px-5 h-14 flex-shrink-0 relative z-20 max-sm:px-4'>
<a href={url(locale, 'home')} class='flex items-center flex-shrink-0 no-underline' aria-label={t.nav.ariaLogoHome}>
<img src='/images/shared/nix-cn.svg' alt='NixCN' width='110' height='31' class='h-[31px] w-auto block' />
</a>
<nav class='navbar-links' aria-label={t.nav.ariaMain}>
<nav class='flex items-center gap-8 absolute left-1/2 -translate-x-1/2 max-lg:hidden' aria-label={t.nav.ariaMain}>
<a
href={url(locale, 'home')}
class:list={['nav-link', activePage === 'home' && 'active']}
class:list={[
'nav-link text-sm font-medium text-[#a7b8d0] no-underline tracking-[-0.05em] whitespace-nowrap transition-colors duration-200 relative pb-0.5 hover:text-brand-blue',
activePage === 'home' && 'active text-brand-blue',
]}
aria-current={activePage === 'home' ? 'page' : undefined}
>
{t.nav.home}
</a>
<a
href={url(locale, 'calendar')}
class:list={['nav-link', activePage === 'calendar' && 'active']}
aria-current={activePage === 'calendar' ? 'page' : undefined}
href={url(locale, 'eventGuide')}
class:list={[
'nav-link text-sm font-medium text-[#a7b8d0] no-underline tracking-[-0.05em] whitespace-nowrap transition-colors duration-200 relative pb-0.5 hover:text-brand-blue',
activePage === 'eventGuide' && 'active text-brand-blue',
]}
aria-current={activePage === 'eventGuide' ? 'page' : undefined}
>
{t.nav.calendar}
{t.nav.eventGuide}
</a>
<a
href={url(locale, 'cmsGuide')}
class:list={['nav-link', activePage === 'cmsGuide' && 'active']}
class:list={[
'nav-link text-sm font-medium text-[#a7b8d0] no-underline tracking-[-0.05em] whitespace-nowrap transition-colors duration-200 relative pb-0.5 hover:text-brand-blue',
activePage === 'cmsGuide' && 'active text-brand-blue',
]}
aria-current={activePage === 'cmsGuide' ? 'page' : undefined}
>
{t.nav.cmsGuide}
</a>
<a
href={url(locale, 'souvenir')}
class:list={['nav-link', activePage === 'souvenir' && 'active']}
class:list={[
'nav-link text-sm font-medium text-[#a7b8d0] no-underline tracking-[-0.05em] whitespace-nowrap transition-colors duration-200 relative pb-0.5 hover:text-brand-blue',
activePage === 'souvenir' && 'active text-brand-blue',
]}
aria-current={activePage === 'souvenir' ? 'page' : undefined}
>
{t.nav.souvenir}
</a>
</nav>
<div class='navbar-social'>
<div class='flex items-center gap-4 ml-auto max-sm:hidden'>
<a
href='https://t.me/nixos_cn'
href='https://t.me/nixcnmeetup'
target='_blank'
rel='noopener noreferrer'
aria-label={t.nav.ariaTelegram}
class='social-link'
class='text-[#a7b8d0] flex items-center no-underline transition-colors duration-200 hover:text-brand-blue'
>
<svg viewBox='0 0 24 24' fill='currentColor' width='22' height='22' aria-hidden='true'>
<path
@@ -68,11 +80,11 @@ const switchHref = url(otherLocale, activePage);
</svg>
</a>
<a
href='https://matrix.to/#/#nixos-cn:matrix.org'
href='https://matrix.to/#/#nix-cn-meetup:rebmit.moe'
target='_blank'
rel='noopener noreferrer'
aria-label={t.nav.ariaMatrix}
class='social-link'
class='text-[#a7b8d0] flex items-center no-underline transition-colors duration-200 hover:text-brand-blue'
>
<svg viewBox='0 0 24 24' fill='currentColor' width='22' height='22' aria-hidden='true'>
<path
@@ -81,8 +93,12 @@ const switchHref = url(otherLocale, activePage);
</path>
</svg>
</a>
<span class='social-divider' aria-hidden='true'></span>
<a href={switchHref} aria-label={t.nav.ariaLangSwitch} class='social-link'>
<span class='block w-px h-5 bg-[#a7b8d0] opacity-40' aria-hidden='true'></span>
<a
href={switchHref}
aria-label={t.nav.ariaLangSwitch}
class='text-[#a7b8d0] flex items-center no-underline transition-colors duration-200 hover:text-brand-blue'
>
<svg
viewBox='0 0 24 24'
fill='none'
@@ -101,218 +117,70 @@ const switchHref = url(otherLocale, activePage);
</a>
</div>
<details class='mobile-menu'>
<summary class='hamburger' aria-label={t.nav.ariaMenu}>
<span></span>
<span></span>
<span></span>
<details class='hidden ml-2 max-lg:block'>
<summary
class='hamburger flex flex-col gap-[5px] p-[8px_4px] cursor-pointer list-none'
aria-label={t.nav.ariaMenu}
>
<span class='block w-[22px] h-0.5 bg-brand-blue rounded-[2px] transition-all duration-200'></span>
<span class='block w-[22px] h-0.5 bg-brand-blue rounded-[2px] transition-all duration-200'></span>
<span class='block w-[22px] h-0.5 bg-brand-blue rounded-[2px] transition-all duration-200'></span>
</summary>
<nav class='mobile-nav' aria-label={t.nav.ariaMobile}>
<a href={url(locale, 'home')} aria-current={activePage === 'home' ? 'page' : undefined}>{t.nav.home}</a>
<a href={url(locale, 'calendar')} aria-current={activePage === 'calendar' ? 'page' : undefined}>
{t.nav.calendar}
<nav
class='mobile-nav fixed top-14 left-2 right-2 bg-[rgba(249,251,255,0.97)] backdrop-blur-[10px] [-webkit-backdrop-filter:blur(10px)] border border-[rgba(155,206,241,0.5)] rounded-2xl py-4 px-5 flex flex-col gap-1 z-[100] shadow-[0_8px_32px_rgba(82,119,195,0.12)]'
aria-label={t.nav.ariaMobile}
>
<a
href={url(locale, 'home')}
aria-current={activePage === 'home' ? 'page' : undefined}
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
{t.nav.home}
</a>
<a href={url(locale, 'cmsGuide')} aria-current={activePage === 'cmsGuide' ? 'page' : undefined}>
<a
href={url(locale, 'eventGuide')}
aria-current={activePage === 'eventGuide' ? 'page' : undefined}
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
{t.nav.eventGuide}
</a>
<a
href={url(locale, 'cmsGuide')}
aria-current={activePage === 'cmsGuide' ? 'page' : undefined}
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
{t.nav.cmsGuide}
</a>
<a href={url(locale, 'souvenir')} aria-current={activePage === 'souvenir' ? 'page' : undefined}>
<a
href={url(locale, 'souvenir')}
aria-current={activePage === 'souvenir' ? 'page' : undefined}
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
{t.nav.souvenir}
</a>
<hr class='mobile-nav-divider' />
<a href='https://t.me/nixos_cn' target='_blank' rel='noopener noreferrer'>Telegram</a>
<a href='https://matrix.to/#/#nixos-cn:matrix.org' target='_blank' rel='noopener noreferrer'>Matrix</a>
<a href={switchHref}>{t.nav.ariaLangSwitch}</a>
<hr class='border-0 border-t border-[rgba(155,206,241,0.4)] my-1' />
<a
href='https://t.me/nixcnmeetup'
target='_blank'
rel='noopener noreferrer'
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
Telegram
</a>
<a
href='https://matrix.to/#/#nix-cn-meetup:rebmit.moe'
target='_blank'
rel='noopener noreferrer'
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
Matrix
</a>
<a
href={switchHref}
class='text-[15px] font-medium text-brand-blue no-underline py-2.5 border-b border-[rgba(155,206,241,0.3)] transition-opacity duration-150 hover:opacity-70 last:border-b-0'
>
{t.nav.ariaLangSwitch}
</a>
</nav>
</details>
</header>
<style>
.navbar {
display: flex;
align-items: center;
padding: 0 20px;
height: 56px;
flex-shrink: 0;
position: relative;
z-index: 20;
}
.navbar-logo {
display: flex;
align-items: center;
flex-shrink: 0;
text-decoration: none;
}
.logo-img {
height: 31px;
width: auto;
display: block;
}
.navbar-links {
display: flex;
align-items: center;
gap: 32px;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.nav-link {
font-size: 14px;
font-weight: 500;
color: #a7b8d0;
text-decoration: none;
letter-spacing: -0.05em;
white-space: nowrap;
transition: color 0.2s;
position: relative;
padding-bottom: 2px;
}
.nav-link:hover {
color: #5277c3;
}
.nav-link.active {
color: #5277c3;
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: -18px;
left: 50%;
transform: translateX(-50%);
width: 71px;
height: 22px;
background: url('/images/shared/tab-line.svg') center / contain no-repeat;
pointer-events: none;
}
.navbar-social {
display: flex;
align-items: center;
gap: 16px;
margin-left: auto;
}
.social-link {
color: #a7b8d0;
display: flex;
align-items: center;
text-decoration: none;
transition: color 0.2s;
}
.social-link:hover {
color: #5277c3;
}
.social-divider {
display: block;
width: 1px;
height: 20px;
background: #a7b8d0;
opacity: 0.4;
}
.mobile-menu {
display: none;
margin-left: 8px;
}
.hamburger {
display: flex;
flex-direction: column;
gap: 5px;
padding: 8px 4px;
cursor: pointer;
list-style: none;
}
.hamburger::marker,
.hamburger::-webkit-details-marker {
display: none;
}
.hamburger span {
display: block;
width: 22px;
height: 2px;
background: #5277c3;
border-radius: 2px;
transition: all 0.2s;
}
.mobile-nav {
position: fixed;
top: 56px;
left: 8px;
right: 8px;
background: rgba(249, 251, 255, 0.97);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(155, 206, 241, 0.5);
border-radius: 16px;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 4px;
z-index: 100;
box-shadow: 0 8px 32px rgba(82, 119, 195, 0.12);
}
.mobile-nav a {
font-size: 15px;
font-weight: 500;
color: #5277c3;
text-decoration: none;
padding: 10px 0;
border-bottom: 1px solid rgba(155, 206, 241, 0.3);
transition: opacity 0.15s;
}
.mobile-nav a:last-child {
border-bottom: none;
}
.mobile-nav a:hover {
opacity: 0.7;
}
.mobile-nav a[aria-current='page'] {
font-weight: 700;
}
.mobile-nav-divider {
border: none;
border-top: 1px solid rgba(155, 206, 241, 0.4);
margin: 4px 0;
}
@media (max-width: 1023px) {
.navbar-links {
display: none;
}
.mobile-menu {
display: block;
}
.navbar-social {
margin-left: auto;
}
}
@media (max-width: 639px) {
.navbar {
padding: 0 16px;
}
.navbar-social {
display: none;
}
}
</style>

View File

@@ -17,577 +17,68 @@ const ts = tp.comingSoon;
<Navbar locale={locale} activePage='souvenir' />
<!-- ======= Hero Banner ======= -->
<section class='hero-banner' aria-labelledby='souvenir-title'>
<div class='hero-bg' aria-hidden='true'>
<img class='hero-bg-img' src='/images/souvenir/hero-bg.png' alt='' />
<section
class='relative pt-[38px] pb-12 px-6 text-center isolate overflow-hidden min-h-[215px] max-lg:pt-8 max-lg:pb-10 max-lg:px-5 max-sm:pt-6 max-sm:pb-8 max-sm:px-4'
aria-labelledby='souvenir-title'
>
<div class='absolute inset-0 z-0 pointer-events-none overflow-hidden' aria-hidden='true'>
<img
class='absolute inset-0 w-full h-full object-cover object-top block animate-hero-fade-in motion-reduce:animate-none motion-reduce:opacity-100'
src='/images/shared/hero-bg.png'
alt=''
/>
</div>
<div class='hero-content'>
<h1 class='hero-title' id='souvenir-title'>{tp.hero.title}</h1>
<p class='hero-sub'>{tp.hero.sub}</p>
<div class='relative z-[1] max-w-[720px] mx-auto'>
<h1
id='souvenir-title'
class='text-[clamp(2.5rem,5.5vw,4.5rem)] font-semibold text-brand-dark m-0 mb-4 tracking-[-0.05em] leading-[1.1] [text-shadow:0_4px_14px_rgba(3,15,32,0.08)] animate-hero-fade-up [animation-delay:0.1s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:mb-3'
>
{tp.hero.title}
</h1>
<p
class='text-[clamp(0.875rem,1vw,1rem)] text-[#1a2332] m-0 leading-[1.7] tracking-[-0.02em] max-w-[640px] mx-auto animate-hero-fade-up [animation-delay:0.18s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:text-[13px] max-sm:leading-[1.65]'
>
{tp.hero.sub}
</p>
</div>
</section>
<!-- ======= Feature Card ======= -->
<main class='souvenir-main'>
<section class='feature-card' aria-labelledby='feature-title'>
<div class='card-bg-texture' aria-hidden='true'>
<img class='card-texture-img' src='/images/souvenir/card-texture.png' alt='' />
</div>
<div class='card-snowflake' aria-hidden='true'>
<img src='/images/souvenir/card-snowflake.png' alt='' width='327' height='325' />
</div>
<div class='card-content'>
<h2 class='card-title' id='feature-title'>{tp.card.title}</h2>
<p class='card-body'>{tp.card.body}</p>
<button type='button' class='cta-btn' id='souvenir-cta-btn' aria-haspopup='dialog'>
<main
class='relative z-[2] w-full max-w-[1152px] mx-auto px-10 pt-4 pb-[120px] max-lg:px-7 max-lg:pt-3 max-lg:pb-[100px] max-sm:px-[18px] max-sm:pt-2 max-sm:pb-20'
>
<section
class='relative min-h-[400px] rounded-3xl border-2 border-border overflow-hidden flex items-stretch animate-hero-fade-in [animation-delay:0.22s] motion-reduce:animate-none motion-reduce:opacity-100 max-lg:min-h-[360px] max-sm:min-h-0 [background-image:radial-gradient(ellipse_80%_70%_at_60%_48%,transparent_15%,rgba(249,251,255,0.85)_85%),radial-gradient(circle,rgba(126,186,228,0.28)_2.5px,transparent_2.5px)] [background-size:auto,20px_20px]'
aria-labelledby='feature-title'
>
<div
class='relative z-[2] flex flex-col justify-center gap-6 p-[60px_48px] max-w-[720px] max-lg:p-[48px_36px] max-sm:p-[32px_24px_40px] max-sm:gap-5'
>
<h2
id='feature-title'
class='text-[clamp(1.5rem,2.2vw,2rem)] font-semibold text-brand-dark m-0 tracking-[-0.04em] leading-[1.25] animate-hero-fade-up [animation-delay:0.32s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:text-[22px]'
>
{tp.card.title}
</h2>
<p
class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-w-[560px] animate-hero-fade-up [animation-delay:0.4s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:text-sm max-sm:leading-[1.75]'
>
{tp.card.body}
</p>
<!-- hover:bg-[#4269b8] hover:-translate-y-0.5 hover:shadow-[0_6px_20px_rgba(82,119,195,0.35)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-brand-blue focus-visible:outline-offset-[3px] -->
<button
type='button'
class='inline-flex items-center gap-[10px] self-start py-[18px] px-8 border-none rounded-[36px] bg-gray-400 text-white text-base font-medium font-[inherit] tracking-[-0.05em] cursor-pointer transition-[background,transform,box-shadow] duration-200 whitespace-nowrap animate-hero-fade-up [animation-delay:0.48s] motion-reduce:animate-none motion-reduce:opacity-100 motion-reduce:transform-none max-sm:w-full max-sm:justify-center max-sm:py-4 max-sm:px-6'
id='souvenir-cta-btn'
aria-haspopup='dialog'
>
{tp.card.cta}
<img class='cta-arrow' src='/images/souvenir/cta-arrow.png' alt='' width='35' height='35' />
</button>
</div>
</section>
</main>
<!-- ======= 等待开放弹窗 ======= -->
<div class='coming-soon-overlay' id='coming-soon-overlay' hidden aria-hidden='true'>
<button
type='button'
class='coming-soon-backdrop'
id='coming-soon-backdrop'
tabindex='-1'
aria-label={ts.ariaClose}></button>
<div
class='coming-soon-dialog'
role='dialog'
aria-modal='true'
aria-labelledby='coming-soon-title'
id='coming-soon-dialog'
>
<div class='coming-soon-icon' aria-hidden='true'>
<svg width='48' height='48' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'>
<circle cx='24' cy='24' r='22' stroke='currentColor' stroke-width='2' opacity='0.25'></circle>
<path
d='M24 14v12l8 4'
stroke='currentColor'
stroke-width='2.5'
stroke-linecap='round'
stroke-linejoin='round'></path>
</svg>
</div>
<h3 class='coming-soon-title' id='coming-soon-title'>{ts.title}</h3>
<p class='coming-soon-body'>{ts.body}</p>
<button type='button' class='coming-soon-close' id='coming-soon-close'>
{ts.close}
</button>
</div>
</div>
<!-- ======= 底部同心圆装饰Figma 切图) ======= -->
<div class='bottom-circles' aria-hidden='true'>
<img src='/images/souvenir/bottom-circles.png' alt='' />
</div>
</BaseLayout>
<style>
:global(.page) {
position: relative;
overflow: hidden;
}
@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes heroFadeUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heroFadeDown {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:global(.navbar) {
animation: heroFadeDown 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* ===== Hero Banner ===== */
.hero-banner {
--hero-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
padding: 38px 24px 48px;
text-align: center;
isolation: isolate;
overflow: hidden;
min-height: 215px;
}
.hero-bg {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.hero-bg-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
display: block;
animation: heroFadeIn 0.6s var(--hero-ease) both;
}
.hero-content {
position: relative;
z-index: 1;
max-width: 720px;
margin: 0 auto;
}
.hero-title {
font-size: clamp(2.5rem, 5.5vw, 4.5rem);
font-weight: 600;
color: #030f20;
margin: 0 0 16px;
letter-spacing: -0.05em;
line-height: 1.1;
text-shadow: 0 4px 14px rgba(3, 15, 32, 0.08);
animation: heroFadeUp 0.55s var(--hero-ease) both;
animation-delay: 0.1s;
}
.hero-sub {
font-size: clamp(0.875rem, 1vw, 1rem);
color: #1a2332;
margin: 0;
line-height: 1.7;
letter-spacing: -0.02em;
max-width: 640px;
margin-inline: auto;
animation: heroFadeUp 0.5s var(--hero-ease) both;
animation-delay: 0.18s;
}
/* ===== Feature Card ===== */
.souvenir-main {
position: relative;
z-index: 2;
width: 100%;
max-width: 1152px;
margin: 0 auto;
padding: 16px 40px 120px;
}
.feature-card {
position: relative;
min-height: 400px;
border-radius: 24px;
border: 2px solid rgba(155, 206, 241, 0.8);
background: #f9fbff;
overflow: hidden;
display: flex;
align-items: stretch;
animation: heroFadeIn 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.22s;
}
.card-bg-texture {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.card-texture-img {
position: absolute;
/* Figma 198:294 — 底纹在卡片内偏移 */
left: -15%;
top: -35%;
width: 130%;
height: auto;
min-height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
.card-snowflake {
position: absolute;
/* Figma 202:325 — x=737 y=155 within 1120×400 card */
right: 56px;
top: 155px;
z-index: 1;
pointer-events: none;
animation: heroFadeIn 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.36s;
}
.card-snowflake img {
display: block;
width: 327px;
height: auto;
}
.card-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
gap: 24px;
padding: 60px 48px;
max-width: 720px;
}
.card-title {
font-size: clamp(1.5rem, 2.2vw, 2rem);
font-weight: 600;
color: #030f20;
margin: 0;
letter-spacing: -0.04em;
line-height: 1.25;
animation: heroFadeUp 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.32s;
}
.card-body {
font-size: 15px;
line-height: 1.8;
color: #1a2332;
margin: 0;
letter-spacing: -0.01em;
max-width: 560px;
animation: heroFadeUp 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.4s;
}
.cta-btn {
display: inline-flex;
align-items: center;
gap: 10px;
align-self: flex-start;
padding: 18px 32px;
border: none;
border-radius: 36px;
background: #5277c3;
color: #ffffff;
font-size: 16px;
font-weight: 500;
font-family: inherit;
letter-spacing: -0.05em;
cursor: pointer;
transition:
background 0.2s,
transform 0.15s,
box-shadow 0.2s;
white-space: nowrap;
animation: heroFadeUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.48s;
}
.cta-btn:hover {
background: #4269b8;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(82, 119, 195, 0.35);
}
.cta-btn:focus-visible {
outline: 2px solid #5277c3;
outline-offset: 3px;
}
/* ===== Coming Soon Modal ===== */
@keyframes modalOverlayIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modalDialogIn {
from {
opacity: 0;
transform: translateY(12px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.coming-soon-overlay {
--modal-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 24px;
}
.coming-soon-overlay[hidden] {
display: none;
}
.coming-soon-overlay.is-open {
animation: modalOverlayIn 0.28s var(--modal-ease) both;
}
.coming-soon-backdrop {
position: absolute;
inset: 0;
border: none;
padding: 0;
margin: 0;
background: rgba(3, 15, 32, 0.45);
backdrop-filter: blur(4px);
cursor: pointer;
}
.coming-soon-dialog {
position: relative;
z-index: 1;
width: min(100%, 420px);
padding: 36px 32px 28px;
border-radius: 24px;
border: 2px solid rgba(155, 206, 241, 0.8);
background: #f9fbff;
box-shadow:
0 24px 48px rgba(3, 15, 32, 0.14),
0 0 0 1px rgba(255, 255, 255, 0.6) inset;
text-align: center;
animation: modalDialogIn 0.35s var(--modal-ease) both;
animation-delay: 0.04s;
}
.coming-soon-icon {
display: flex;
justify-content: center;
margin-bottom: 16px;
color: #5277c3;
}
.coming-soon-title {
margin: 0 0 12px;
font-size: clamp(1.25rem, 2.5vw, 1.5rem);
font-weight: 600;
color: #030f20;
letter-spacing: -0.04em;
line-height: 1.3;
}
.coming-soon-body {
margin: 0 0 28px;
font-size: 15px;
line-height: 1.75;
color: #1a2332;
letter-spacing: -0.01em;
}
.coming-soon-close {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 140px;
padding: 14px 28px;
border: none;
border-radius: 36px;
background: #5277c3;
color: #ffffff;
font-size: 15px;
font-weight: 500;
font-family: inherit;
letter-spacing: -0.03em;
cursor: pointer;
transition:
background 0.2s,
transform 0.15s,
box-shadow 0.2s;
}
.coming-soon-close:hover {
background: #4269b8;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(82, 119, 195, 0.35);
}
.coming-soon-close:focus-visible {
outline: 2px solid #5277c3;
outline-offset: 3px;
}
.cta-arrow {
display: block;
width: 35px;
height: 35px;
flex-shrink: 0;
}
/* ===== 底部同心圆装饰 ===== */
.bottom-circles {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: min(710px, 100%);
pointer-events: none;
z-index: 0;
animation: heroFadeIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.4s;
}
.bottom-circles img {
display: block;
width: 100%;
height: auto;
}
@media (max-width: 1023px) {
.hero-banner {
padding: 32px 20px 40px;
}
.souvenir-main {
padding: 12px 28px 100px;
}
.feature-card {
min-height: 360px;
}
.card-content {
padding: 48px 36px;
}
.card-snowflake {
right: 24px;
top: auto;
bottom: 16px;
}
.card-snowflake img {
width: min(260px, 32vw);
}
.bottom-circles {
width: min(560px, 100%);
}
}
@media (max-width: 639px) {
.hero-banner {
padding: 24px 16px 32px;
}
.hero-title {
margin-bottom: 12px;
}
.hero-sub {
font-size: 13px;
line-height: 1.65;
}
.souvenir-main {
padding: 8px 18px 80px;
}
.feature-card {
min-height: auto;
}
.card-content {
padding: 32px 24px 40px;
gap: 20px;
}
.card-title {
font-size: 22px;
}
.card-body {
font-size: 14px;
line-height: 1.75;
}
.card-snowflake {
right: -12px;
bottom: -8px;
top: auto;
}
.card-snowflake img {
width: 180px;
}
.cta-btn {
width: 100%;
justify-content: center;
padding: 16px 24px;
}
.cta-arrow {
width: 28px;
height: 28px;
}
.bottom-circles {
width: min(380px, 100%);
}
}
@media (prefers-reduced-motion: reduce) {
:global(.navbar),
.hero-bg-img,
.hero-title,
.hero-sub,
.feature-card,
.card-snowflake,
.card-title,
.card-body,
.cta-btn {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
.bottom-circles {
animation: none !important;
opacity: 1 !important;
transform: translateX(-50%) !important;
}
.coming-soon-overlay.is-open,
.coming-soon-dialog {
animation: none !important;
}
}
</style>
<script>
function initComingSoonModal() {
const overlay = document.getElementById('coming-soon-overlay');

View File

@@ -14,7 +14,7 @@ export function getTranslations(locale: Locale) {
export const PAGE_SLUGS = {
home: '',
calendar: 'calendar',
eventGuide: 'event-guide',
cmsGuide: 'cms-guide',
souvenir: 'souvenir',
} as const;

View File

@@ -1,8 +1,8 @@
{
"zh-CN": {
"nav": {
"home": "HOME",
"calendar": "活动指南",
"home": "首页",
"eventGuide": "活动指南",
"cmsGuide": "CMS 系统指引",
"souvenir": "纪念品信息",
"ariaLogoHome": "NixCN 首页",
@@ -26,11 +26,11 @@
"headlineBadge": "Nix",
"headlineAfter": "\u00a0实践被看见",
"lead": "欢迎回来相聚。这是又一次面向中文社区的 Nix / NixOS 线下聚会——更丰富,也更好玩。我们希望把正在使用 Nix、研究 Nix或刚刚开始了解 Nix 的人聚在一起。让真实的实践、问题与探索,在现场被看见。",
"ctaPrimary": "报名参会\u00a0→",
"ctaSecondary": "CMS 系统指引\u00a0👈"
"ctaPrimary": "报名参会",
"ctaSecondary": "CMS 系统指引"
}
},
"calendar": {
"eventGuide": {
"meta": {
"htmlLang": "zh-CN",
"title": "活动指南 · NixCN Conference 2605",
@@ -38,7 +38,7 @@
},
"heading": {
"title": "活动指南",
"sub": "如果你对 Nix 有一点好奇,这里可能会给你一个更具体的答案。"
"sub": "如果你对 Conference 有一点好奇,这里可能会给你一个更具体的答案。"
},
"agenda": {
"leadEmoji": "🌟",
@@ -56,8 +56,8 @@
"timeValue": "2026/06/13 (Sat.) ~ 2026/06/14 (Sun.)",
"flowLabel": "当天流程:",
"flow": [
"签到时间08:30 09:30",
"议题分享:09:30 开始",
"签到时间08:30 10:00",
"议题分享:10:00 开始",
"午休时间12:00 14:00",
"下午议题14:00 18:00",
"活动结束18:30 前"
@@ -77,10 +77,7 @@
},
"hero": {
"title": "CMS 系统指引",
"subs": [
"通过报名系统,你可以完成活动报名、身份认证、议程提交,以及后续入场签到等操作。",
"首次使用无需提前注册,使用邮箱登录后,系统会自动创建账号。"
]
"subs": ["通过报名系统完成活动报名、身份认证、议程提交以及入场签到等操作。"]
},
"tipBell": "🔔",
"tipLabel": "提示:",
@@ -92,7 +89,7 @@
"bodyHtml": "进入报名系统后,输入任意可正常接收邮件的邮箱,即可获取登录链接。<br />首次登录时,系统会自动为你创建账号。你只需要根据提示设置用户名和昵称,即可进入系统。",
"tip": {
"kind": "inline",
"text": "请尽量使用常用邮箱登录。后续活动报名、身份认证、议程提交和会务通知,都将与该邮箱关联。"
"text": "请尽量使用常用邮箱登录。后续活动报名、身份认证、议程提交和会务通知,都将与该邮箱关联。"
}
},
{
@@ -115,7 +112,7 @@
"afterListHtml": "完成认证后,即可继续参与活动相关操作。",
"tip": {
"kind": "inline",
"text": "需要填写个人有效证件的真实信息哦。"
"text": "对于未成年参会者,需要额外签署一份监护人知情同意书,请在报名结束前后留意群内通知和参会邮箱获取签署指南。"
}
},
{
@@ -149,17 +146,17 @@
},
"hero": {
"title": "纪念品信息",
"sub": "打造你的专属参会名片!你可以在此 DIY 个性化参会证,并提前解锁更多活动限定纪念品信息。"
"sub": "你可以在此 DIY 个性化参会证,并提前解锁更多活动限定纪念品信息。"
},
"card": {
"title": "NixCN Conference 2605 打造你的专属参会证 ✨",
"body": "提供纪念品的个性化设定服务。点击下方按钮跳转至外部定制通道,录入你的专属身份参数以生成定制化参会证。",
"cta": "开启专属定制",
"title": "打造你的专属参会证 ✨",
"body": "提供纪念品的个性化设定服务。点击下方按钮跳转至定制通道,录入你的身份信息和偏好以生成定制化参会证。",
"cta": "定制收集表暂未开放",
"ctaUrl": "https://forms.office.com/Pages/DesignPageV2.aspx?subpage=design&FormId=DQSIkWdsW0yxEjajBLZtrQAAAAAAAAAAAAZ__u0jrMtUODNTWUpJSEs0STZZWThNTTJVUlNHN01OQy4u&Token=f0e798fadec04532bf2ef8b474a4d7b2"
},
"comingSoon": {
"title": "定制通道暂未开放",
"body": "纪念品个性化定制功能正在准备中,敬请期待。通道开放后,点击下方按钮即可跳转至外部表单完成定制。",
"body": "纪念品详细设计与个性化定制功能正在准备中,敬请期待",
"close": "知道了",
"ariaClose": "关闭弹窗"
}
@@ -167,8 +164,8 @@
},
"en": {
"nav": {
"home": "HOME",
"calendar": "Event Guide",
"home": "Home",
"eventGuide": "Event Guide",
"cmsGuide": "CMS Guide",
"souvenir": "Souvenirs",
"ariaLogoHome": "NixCN Home",
@@ -191,12 +188,12 @@
"headlineBefore": "Let your\u00a0",
"headlineBadge": "Nix",
"headlineAfter": "practice <br class=\"headline-break\" />be seen",
"lead": "Welcome back to the table. This is another in-person Nix / NixOS meetup for the Chinese community — richer, and more fun. We want to bring together people who are already using Nix, studying Nix, or just starting to learn about Nix. Let real practice, questions and exploration be seen onstage.",
"lead": "Welcome back. This is another in-person Nix / NixOS meetup for the Chinese community — richer, and more fun. We want to bring together veteran Nix users, deep divers, and absolute beginners alike. Let real-world applications, deep questions, and new explorations take center stage.",
"ctaPrimary": "Sign up\u00a0→",
"ctaSecondary": "CMS Guide\u00a0👈"
}
},
"calendar": {
"eventGuide": {
"meta": {
"htmlLang": "en",
"title": "Event Guide · NixCN Conference 2605",
@@ -204,7 +201,7 @@
},
"heading": {
"title": "Event Guide",
"sub": "If you are a little curious about Nix, this page may give you a more concrete answer."
"sub": "If you're curious about what to expect at the Conference, this guide will give you a clearer picture."
},
"agenda": {
"leadEmoji": "🌟",
@@ -212,7 +209,7 @@
"bullets": [
"Each talk runs for about <strong>30</strong> minutes",
"Every talk is followed by about <strong>10</strong> minutes of discussion / Q&amp;A",
"A tea break is scheduled around <strong>15:00</strong> in the afternoon"
"A tea break is scheduled around <strong>15:00</strong>"
]
},
"schedule": {
@@ -220,10 +217,10 @@
"title": "Conference schedule",
"timeLabel": "Conference dates:",
"timeValue": "2026/06/13 (Sat.) ~ 2026/06/14 (Sun.)",
"flowLabel": "Daily flow:",
"flowLabel": "Daily Schedule:",
"flow": [
"Check-in: 08:30 09:30",
"Talks start: 09:30",
"Check-in: 08:30 10:00",
"Talks start: 10:00",
"Lunch break: 12:00 14:00",
"Afternoon talks: 14:00 18:00",
"Wrap up before: 18:30"
@@ -231,7 +228,7 @@
},
"food": {
"emoji": "🍜",
"title": "Food guide",
"title": "Dining Guide",
"mapAlt": "NixCN Conference 2605 nearby food map, with recommended restaurants within a 213 minute walk from the venue"
}
},
@@ -255,7 +252,7 @@
"id": "step-01",
"num": "01",
"name": "Log in with your email",
"bodyHtml": "Once you open the registration system, enter any email address that can receive mail and you will get a login link.<br />On first login the system creates an account for you automatically. Just follow the prompts to set a username and display name, and you are in.",
"bodyHtml": "Once you open the registration system, enter a valid email address and you will get a login link.<br />On first login the system creates an account for you automatically. Just follow the prompts to set a username and display name, and you are in.",
"tip": {
"kind": "inline",
"text": "Please use an email address you check often. Future event sign-ups, identity verification, talk submissions and event notifications will all be tied to this address."
@@ -265,7 +262,7 @@
"id": "step-02",
"num": "02",
"name": "Browse upcoming events",
"bodyHtml": "Once logged in, you can browse currently open events on the Events page.<br />Select an event to open its detail page, where you can see the date, venue, description, registration status and the action entries you need.<br />Here you can:",
"bodyHtml": "Once logged in, you can browse currently open events on the Events page.<br />Select an event to open its detail page, where you can see the date, venue, description, registration status and available actions.<br />Here you can:",
"list": ["Join the event", "View event details", "Submit a talk", "View events you have joined"]
},
{
@@ -281,7 +278,7 @@
"afterListHtml": "Once verified, you can continue with the rest of the event actions.",
"tip": {
"kind": "inline",
"text": "Please fill in real information from a valid personal ID document."
"text": "For underage attendees, a guardian consent form must also be signed. Please watch for group notifications and check your registration email for the signing guide around the time registration closes."
}
},
{
@@ -295,7 +292,7 @@
"num": "05",
"name": "Talk review and scheduling",
"bodyHtmls": [
"Submitted talks are reviewed by the organizers as a whole.<br />We curate the program based on topic relevance, content completeness, sharing value and the overall conference plan. Accepted talks then move into the scheduling phase.",
"Submitted talks are reviewed collectively by the organizers.<br />We curate the program based on topic relevance, content completeness, sharing value and the overall conference plan. Accepted talks then move into the scheduling phase.",
"The final program is expected to be announced two weeks before the conference.<br />Please refer to the latest information on the website or event page closer to the date."
]
},
@@ -303,7 +300,7 @@
"id": "step-06",
"num": "06",
"name": "Entry code and check-in code",
"bodyHtml": "Once the event starts, the system will generate an entry code and a check-in code for every attendee who has joined.<br />These codes are your essential credentials on-site. Once you arrive at the venue, please present them at the matching step under staff guidance."
"bodyHtml": "Once the event starts, the system will generate an entry code and a check-in code for every attendee who has joined.<br />These codes are your essential credentials on-site. Once you arrive at the venue, please present them at the respective checkpoints step under staff guidance."
}
]
},
@@ -315,17 +312,17 @@
},
"hero": {
"title": "Souvenirs",
"sub": "Create your exclusive conference name card! DIY your personalized badge here and unlock more event-limited souvenir information in advance."
"sub": "Customize your badge here and unlock more exclusive event souvenir details in advance."
},
"card": {
"title": "NixCN Conference 2605 — Create Your Exclusive Badge ✨",
"body": "We offer personalized souvenir settings. Click the button below to open the external customization channel and enter your identity details to generate a customized conference badge.",
"cta": "Start Customization",
"title": "Create Your Exclusive Badge ✨",
"body": "Personalize your souvenirs. Click the button below to open the customization form and enter your details to generate a customized conference badge.",
"cta": "Not Open Yet",
"ctaUrl": "https://forms.office.com/Pages/DesignPageV2.aspx?subpage=design&FormId=DQSIkWdsW0yxEjajBLZtrQAAAAAAAAAAAAZ__u0jrMtUODNTWUpJSEs0STZZWThNTTJVUlNHN01OQy4u&Token=f0e798fadec04532bf2ef8b474a4d7b2"
},
"comingSoon": {
"title": "Customization Not Open Yet",
"body": "Personalized souvenir customization is still being prepared. Please check back later — once live, the button below will take you to the external form.",
"body": "Souvenir design and personalized customization is still being prepared. Please check back later.",
"close": "Got it",
"ariaClose": "Close dialog"
}

View File

@@ -1,4 +1,6 @@
---
import '../styles/global.css';
interface Props {
title: string;
description?: string;
@@ -18,42 +20,6 @@ const { title, description, lang } = Astro.props;
<title>{title}</title>
</head>
<body>
<div class='page'>
<slot />
</div>
<slot />
</body>
</html>
<style is:global>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
min-height: 100%;
}
body {
font-family:
'PingFang SC',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
background: #ffffff;
color: #030f20;
-webkit-font-smoothing: antialiased;
}
.page {
min-height: 100dvh;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -1,10 +0,0 @@
---
import CalendarPage from '../../components/CalendarPage.astro';
import { getLocalePaths, type Locale } from '../../i18n/config';
export const getStaticPaths = getLocalePaths;
const { lang } = Astro.params as { lang: Locale };
---
<CalendarPage locale={lang} />

View File

@@ -1,10 +0,0 @@
---
import CmsGuidePage from '../../components/CmsGuidePage.astro';
import { getLocalePaths, type Locale } from '../../i18n/config';
export const getStaticPaths = getLocalePaths;
const { lang } = Astro.params as { lang: Locale };
---
<CmsGuidePage locale={lang} />

View File

@@ -1,10 +0,0 @@
---
import HomePage from '../../components/HomePage.astro';
import { getLocalePaths, type Locale } from '../../i18n/config';
export const getStaticPaths = getLocalePaths;
const { lang } = Astro.params as { lang: Locale };
---
<HomePage locale={lang} />

View File

@@ -1,10 +0,0 @@
---
import SouvenirPage from '../../components/SouvenirPage.astro';
import { getLocalePaths, type Locale } from '../../i18n/config';
export const getStaticPaths = getLocalePaths;
const { lang } = Astro.params as { lang: Locale };
---
<SouvenirPage locale={lang} />

View File

@@ -0,0 +1,5 @@
---
import CmsGuidePage from '../../components/CmsGuidePage.astro';
---
<CmsGuidePage locale='en' />

View File

@@ -0,0 +1,5 @@
---
import EventGuidePage from '../../components/EventGuidePage.astro';
---
<EventGuidePage locale='en' />

5
src/pages/en/index.astro Normal file
View File

@@ -0,0 +1,5 @@
---
import HomePage from '../../components/HomePage.astro';
---
<HomePage locale='en' />

View File

@@ -0,0 +1,5 @@
---
import SouvenirPage from '../../components/SouvenirPage.astro';
---
<SouvenirPage locale='en' />

View File

@@ -0,0 +1,5 @@
---
import CmsGuidePage from '../../components/CmsGuidePage.astro';
---
<CmsGuidePage locale='zh-CN' />

View File

@@ -0,0 +1,5 @@
---
import EventGuidePage from '../../components/EventGuidePage.astro';
---
<EventGuidePage locale='zh-CN' />

View File

@@ -0,0 +1,5 @@
---
import HomePage from '../../components/HomePage.astro';
---
<HomePage locale='zh-CN' />

View File

@@ -0,0 +1,5 @@
---
import SouvenirPage from '../../components/SouvenirPage.astro';
---
<SouvenirPage locale='zh-CN' />

269
src/styles/global.css Normal file
View File

@@ -0,0 +1,269 @@
@import 'tailwindcss';
@theme {
/* Brand colors */
--color-brand-blue: #5277c3;
--color-brand-dark: #030f20;
--color-brand-light: #9bcef1;
--color-surface: rgba(249, 251, 255, 0.9);
--color-border: rgba(155, 206, 241, 0.8);
/* Typography */
--font-sans: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Easings */
--ease-hero: cubic-bezier(0.22, 1, 0.36, 1);
--ease-modal: cubic-bezier(0.22, 1, 0.36, 1);
/* Named animations */
--animate-hero-fade-up: heroFadeUp 0.55s var(--ease-hero) both;
--animate-hero-fade-down: heroFadeDown 0.45s var(--ease-hero) both;
--animate-hero-fade-in: heroFadeIn 0.5s var(--ease-hero) both;
--animate-hero-fade-in-70: heroFadeInTo70 0.5s var(--ease-hero) both;
--animate-modal-in: modalDialogIn 0.35s var(--ease-modal) both;
--animate-modal-overlay: modalOverlayIn 0.28s var(--ease-modal) both;
}
@keyframes heroFadeUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heroFadeDown {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes heroFadeInTo70 {
from {
opacity: 0;
}
to {
opacity: 0.7;
}
}
@keyframes modalDialogIn {
from {
opacity: 0;
transform: translateY(12px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes modalOverlayIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@layer base {
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
margin: 0;
padding: 0;
min-height: 100%;
scrollbar-gutter: stable;
}
body {
margin: 0;
padding: 0;
min-height: 100%;
}
body {
font-family: var(--font-sans);
background: #ffffff;
color: var(--color-brand-dark);
-webkit-font-smoothing: antialiased;
min-height: 100dvh;
display: flex;
flex-direction: column;
padding: 0 16px 16px;
}
}
@layer components {
/* Navbar: active-link tab underline (SVG, cannot express as utility) */
.nav-link.active::after {
content: '';
position: absolute;
bottom: -18px;
left: 50%;
transform: translateX(-50%);
width: 71px;
height: 22px;
background: url('/images/shared/tab-line.svg') center / contain no-repeat;
pointer-events: none;
}
/* Navbar: suppress <details> marker */
.hamburger::marker,
.hamburger::-webkit-details-marker {
display: none;
}
/* Navbar: mobile nav current page */
.mobile-nav a[aria-current='page'] {
font-weight: 700;
}
/* Navbar: last mobile nav link no bottom border */
.mobile-nav a:last-child {
border-bottom: none;
}
/* event-guide: bullet dot */
.bullet-list li::before {
content: '';
position: absolute;
left: 4px;
top: 0.7em;
width: 6px;
height: 6px;
border-radius: 50%;
background: #33466d;
}
/* event-guide: injected <strong> inside bullet */
.bullet-list li strong {
font-size: 24px;
font-weight: 600;
color: #33466d;
margin: 0 2px;
}
/* event-guide: smaller strong on mobile */
@media (max-width: 639px) {
.bullet-list li strong {
font-size: 19px;
}
}
/* event-guide: timeline marker */
.timeline-list li::marker {
color: #030f20;
font-size: 1em;
}
/* Global: injected .accent spans */
.accent {
color: #5277c3;
font-weight: 500;
}
/* CmsGuide: warning capsule injected via set:html */
.hl-warn {
display: inline-block;
padding: 0 8px;
margin: 0 2px;
color: #d94f4f;
font-weight: 600;
border: 1px solid rgba(217, 79, 79, 0.45);
border-radius: 8px;
background: rgba(217, 79, 79, 0.06);
}
/* CmsGuide: step list bullet color */
.step-list li::marker {
color: #5277c3;
}
/* CmsGuide: adjacent tip paragraphs */
.tip p + p {
margin-top: 4px;
}
/* HomePage: gloss pseudo with JS-driven CSS vars */
.nix-highlight::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
circle at var(--gloss-x) var(--gloss-y),
rgba(255, 255, 255, 0.55) 0%,
rgba(255, 255, 255, 0.18) 28%,
rgba(255, 255, 255, 0) 60%
);
mix-blend-mode: screen;
pointer-events: none;
transform: translateZ(1px);
}
/* HomePage: highlight border pseudo */
.nix-highlight::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
border: 1px solid rgba(255, 255, 255, 0.45);
pointer-events: none;
transform: translateZ(1px);
}
/* HomePage: English badge inline-end spacing */
.headline--split-mobile .nix-highlight {
margin-inline-end: 0.22em;
}
/* HomePage mobile: badge second-line block */
@media (max-width: 639px) {
.headline--split-mobile .nix-highlight {
margin-inline-end: 0;
}
.headline--split-mobile .headline-tail {
display: block;
margin: 0;
padding: 0;
}
.nix-highlight::before,
.nix-highlight::after {
display: none;
}
}
/* Souvenir: modal overlay state */
.coming-soon-overlay[hidden] {
display: none;
}
.coming-soon-overlay.is-open {
animation: var(--animate-modal-overlay);
}
/* Souvenir: modal dialog animation (triggered when overlay opens) */
.coming-soon-overlay.is-open .coming-soon-dialog {
animation: var(--animate-modal-in);
animation-delay: 0.04s;
}
}