2 Commits

Author SHA1 Message Date
gylove1994
63b98f42b4 feat(calendar, souvenir): update food map image source and add coming soon modal
- Updated the food map image source based on locale in CalendarPage.astro.
- Introduced a "coming soon" modal in SouvenirPage.astro with relevant translations and styling.
- Updated the CTA URL in translations.json for both English and Chinese versions.
2026-05-20 06:25:37 +08:00
gylove1994
4f9099abb6 feat(site): migrate from Starlight docs to custom Astro website
- Remove Starlight and MDX docs content
- Add multilingual routing under [lang]/
- Add Home, Calendar, Souvenir, and CMS Guide pages
- Move static assets to public/images
- Add i18n config and translations
- Add client scripts for badge tilt, parallax, and nixflake animation
- Update astro config and dependencies

Signed-off-by: gylove1994 <gylove1994@acgsteps.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 17:05:41 +08:00
39 changed files with 2397 additions and 2711 deletions

View File

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

View File

@@ -1,13 +0,0 @@
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,18 +1,13 @@
// @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,
},
i18n: {
locales: ['zh-CN', 'en'],
defaultLocale: 'zh-CN',
redirects: {
'/': '/zh-CN/',
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,159 +0,0 @@
# 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,11 +14,8 @@
},
"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,21 +11,12 @@ 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)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0)
version: 5.16.4(@types/node@24.10.1)(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
@@ -542,183 +533,155 @@ 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==}
@@ -760,12 +723,6 @@ 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'}
@@ -773,17 +730,9 @@ 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==}
@@ -839,67 +788,56 @@ 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==}
@@ -957,100 +895,6 @@ 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==}
@@ -1370,10 +1214,6 @@ 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'}
@@ -1466,9 +1306,6 @@ 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==}
@@ -1542,10 +1379,6 @@ 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
@@ -1567,80 +1400,6 @@ 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==}
@@ -2072,13 +1831,6 @@ 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==}
@@ -3005,34 +2757,15 @@ 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':
@@ -3162,74 +2895,6 @@ 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
@@ -3354,7 +3019,7 @@ snapshots:
array-iterate@2.0.1: {}
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):
astro@5.16.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@6.0.3)(yaml@2.9.0):
dependencies:
'@astrojs/compiler': 2.13.0
'@astrojs/internal-helpers': 0.7.5
@@ -3411,8 +3076,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)(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))
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))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
@@ -3630,11 +3295,6 @@ 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: {}
@@ -3759,8 +3419,6 @@ snapshots:
glob-to-regexp@0.4.1: {}
graceful-fs@4.2.11: {}
h3@1.15.4:
dependencies:
cookie-es: 1.2.2
@@ -3886,8 +3544,6 @@ snapshots:
dependencies:
is-inside-container: 1.0.0
jiti@2.7.0: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -3902,55 +3558,6 @@ 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: {}
@@ -4685,10 +4292,6 @@ 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: {}
@@ -4831,7 +4434,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0):
vite@6.4.1(@types/node@24.10.1)(yaml@2.9.0):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -4842,13 +4445,11 @@ 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)(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)):
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
vite: 6.4.1(@types/node@24.10.1)(yaml@2.9.0)
volar-service-css@0.0.70(@volar/language-service@2.4.28):
dependencies:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,580 @@
---
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,70 +10,35 @@ 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 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=''
/>
<!-- ======= 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='' />
</div>
<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 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>
</section>
<!-- ======= Guide content ======= -->
<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'
>
<main class='guide-main'>
{
tp.steps.map((step, i) => (
<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>
<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>
</h2>
{'bodyHtml' in step && step.bodyHtml !== undefined && (
<p class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-sm:text-sm max-sm:leading-[1.75]'>
<p class='step-body'>
<Fragment set:html={step.bodyHtml} />
</p>
)}
@@ -81,32 +46,32 @@ const subDelays = ['0.4s', '0.48s', '0.56s'];
{'bodyHtmls' in step &&
step.bodyHtmls !== undefined &&
step.bodyHtmls.map((html) => (
<p class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-sm:text-sm max-sm:leading-[1.75]'>
<p class='step-body'>
<Fragment set:html={html} />
</p>
))}
{'list' in step && step.list !== undefined && (
<ul class='step-list m-0 pl-6 text-[15px] leading-[1.9] text-[#1a2332] max-sm:text-sm'>
<ul class='step-list'>
{step.list.map((item) => (
<li class='pl-1'>{item}</li>
<li>{item}</li>
))}
</ul>
)}
{'afterListHtml' in step && step.afterListHtml !== undefined && (
<p class='text-[15px] leading-[1.8] text-[#1a2332] m-0 tracking-[-0.01em] max-sm:text-sm max-sm:leading-[1.75]'>
<p class='step-body'>
<Fragment set:html={step.afterListHtml} />
</p>
)}
{step.tip?.text && (
<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'>
<div class='tip tip--inline'>
<p>
<span class='tip-bell' aria-hidden='true'>
{tp.tipBell}
</span>
<span class='font-semibold mr-0.5'>{tp.tipLabel}</span>
<span class='tip-label'>{tp.tipLabel}</span>
{step.tip.text}
</p>
</div>
@@ -115,4 +80,355 @@ const subDelays = ['0.4s', '0.48s', '0.56s'];
))
}
</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

@@ -1,133 +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.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,92 +18,46 @@ const url = (p: Parameters<typeof getPageUrl>[1]) => getPageUrl(locale, p);
<Navbar locale={locale} activePage='home' />
<!-- ======= Hero Card ======= -->
<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 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>
<!-- 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>
<!-- Layer 1b: interactive NixOS snowflake — background decoration anchored to bottom-right -->
<div class='hero-decoration' aria-hidden='true'>
<canvas id='nixflake'></canvas>
</div>
<!-- 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'
>
<!-- Layer 2: foreground content (text floats above all background layers) -->
<div class='hero-content'>
<div class='hero-text'>
<p class='conference-title'>
{tp.hero.conferenceTitle}
<span class='text-[#7ebae4]'>{tp.hero.year}</span>
<span class='year'>{tp.hero.year}</span>
</p>
<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 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>
<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>
<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>
</div>
</div>
</div>
</div>
</BaseLayout>
<!-- ======= Scripts ======= -->
<!-- ======= Interactive NixOS snowflake canvas + background parallax ======= -->
<script>
import { initNixflakeAnimation } from '../scripts/nixflake-animation';
import { initBadgeTilt } from '../scripts/badge-tilt';
@@ -113,3 +67,510 @@ 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,61 +16,49 @@ const url = (l: Locale, p: Page) => getPageUrl(l, p);
const switchHref = url(otherLocale, activePage);
---
<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' />
<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' />
</a>
<nav class='flex items-center gap-8 absolute left-1/2 -translate-x-1/2 max-lg:hidden' aria-label={t.nav.ariaMain}>
<nav class='navbar-links' aria-label={t.nav.ariaMain}>
<a
href={url(locale, 'home')}
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',
]}
class:list={['nav-link', activePage === 'home' && 'active']}
aria-current={activePage === 'home' ? 'page' : undefined}
>
{t.nav.home}
</a>
<a
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}
href={url(locale, 'calendar')}
class:list={['nav-link', activePage === 'calendar' && 'active']}
aria-current={activePage === 'calendar' ? 'page' : undefined}
>
{t.nav.eventGuide}
{t.nav.calendar}
</a>
<a
href={url(locale, 'cmsGuide')}
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',
]}
class:list={['nav-link', activePage === 'cmsGuide' && 'active']}
aria-current={activePage === 'cmsGuide' ? 'page' : undefined}
>
{t.nav.cmsGuide}
</a>
<a
href={url(locale, 'souvenir')}
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',
]}
class:list={['nav-link', activePage === 'souvenir' && 'active']}
aria-current={activePage === 'souvenir' ? 'page' : undefined}
>
{t.nav.souvenir}
</a>
</nav>
<div class='flex items-center gap-4 ml-auto max-sm:hidden'>
<div class='navbar-social'>
<a
href='https://t.me/nixcnmeetup'
href='https://t.me/nixos_cn'
target='_blank'
rel='noopener noreferrer'
aria-label={t.nav.ariaTelegram}
class='text-[#a7b8d0] flex items-center no-underline transition-colors duration-200 hover:text-brand-blue'
class='social-link'
>
<svg viewBox='0 0 24 24' fill='currentColor' width='22' height='22' aria-hidden='true'>
<path
@@ -80,11 +68,11 @@ const switchHref = url(otherLocale, activePage);
</svg>
</a>
<a
href='https://matrix.to/#/#nix-cn-meetup:rebmit.moe'
href='https://matrix.to/#/#nixos-cn:matrix.org'
target='_blank'
rel='noopener noreferrer'
aria-label={t.nav.ariaMatrix}
class='text-[#a7b8d0] flex items-center no-underline transition-colors duration-200 hover:text-brand-blue'
class='social-link'
>
<svg viewBox='0 0 24 24' fill='currentColor' width='22' height='22' aria-hidden='true'>
<path
@@ -93,12 +81,8 @@ const switchHref = url(otherLocale, activePage);
</path>
</svg>
</a>
<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'
>
<span class='social-divider' aria-hidden='true'></span>
<a href={switchHref} aria-label={t.nav.ariaLangSwitch} class='social-link'>
<svg
viewBox='0 0 24 24'
fill='none'
@@ -117,70 +101,218 @@ const switchHref = url(otherLocale, activePage);
</a>
</div>
<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>
<details class='mobile-menu'>
<summary class='hamburger' aria-label={t.nav.ariaMenu}>
<span></span>
<span></span>
<span></span>
</summary>
<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}
<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}
</a>
<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'
>
<a href={url(locale, 'cmsGuide')} aria-current={activePage === 'cmsGuide' ? 'page' : undefined}>
{t.nav.cmsGuide}
</a>
<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'
>
<a href={url(locale, 'souvenir')} aria-current={activePage === 'souvenir' ? 'page' : undefined}>
{t.nav.souvenir}
</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>
<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>
</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,68 +17,577 @@ const ts = tp.comingSoon;
<Navbar locale={locale} activePage='souvenir' />
<!-- ======= 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=''
/>
<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='' />
</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.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 class='hero-content'>
<h1 class='hero-title' id='souvenir-title'>{tp.hero.title}</h1>
<p class='hero-sub'>{tp.hero.sub}</p>
</div>
</section>
<!-- ======= Feature Card ======= -->
<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'
>
<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'>
{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: '',
eventGuide: 'event-guide',
calendar: 'calendar',
cmsGuide: 'cms-guide',
souvenir: 'souvenir',
} as const;

View File

@@ -1,8 +1,8 @@
{
"zh-CN": {
"nav": {
"home": "首页",
"eventGuide": "活动指南",
"home": "HOME",
"calendar": "活动指南",
"cmsGuide": "CMS 系统指引",
"souvenir": "纪念品信息",
"ariaLogoHome": "NixCN 首页",
@@ -26,11 +26,11 @@
"headlineBadge": "Nix",
"headlineAfter": "\u00a0实践被看见",
"lead": "欢迎回来相聚。这是又一次面向中文社区的 Nix / NixOS 线下聚会——更丰富,也更好玩。我们希望把正在使用 Nix、研究 Nix或刚刚开始了解 Nix 的人聚在一起。让真实的实践、问题与探索,在现场被看见。",
"ctaPrimary": "报名参会",
"ctaSecondary": "CMS 系统指引"
"ctaPrimary": "报名参会\u00a0→",
"ctaSecondary": "CMS 系统指引\u00a0👈"
}
},
"eventGuide": {
"calendar": {
"meta": {
"htmlLang": "zh-CN",
"title": "活动指南 · NixCN Conference 2605",
@@ -38,7 +38,7 @@
},
"heading": {
"title": "活动指南",
"sub": "如果你对 Conference 有一点好奇,这里可能会给你一个更具体的答案。"
"sub": "如果你对 Nix 有一点好奇,这里可能会给你一个更具体的答案。"
},
"agenda": {
"leadEmoji": "🌟",
@@ -56,8 +56,8 @@
"timeValue": "2026/06/13 (Sat.) ~ 2026/06/14 (Sun.)",
"flowLabel": "当天流程:",
"flow": [
"签到时间08:30 10:00",
"议题分享:10:00 开始",
"签到时间08:30 09:30",
"议题分享:09:30 开始",
"午休时间12:00 14:00",
"下午议题14:00 18:00",
"活动结束18:30 前"
@@ -77,7 +77,10 @@
},
"hero": {
"title": "CMS 系统指引",
"subs": ["通过报名系统完成活动报名、身份认证、议程提交以及入场签到等操作。"]
"subs": [
"通过报名系统,你可以完成活动报名、身份认证、议程提交,以及后续入场签到等操作。",
"首次使用无需提前注册,使用邮箱登录后,系统会自动创建账号。"
]
},
"tipBell": "🔔",
"tipLabel": "提示:",
@@ -89,7 +92,7 @@
"bodyHtml": "进入报名系统后,输入任意可正常接收邮件的邮箱,即可获取登录链接。<br />首次登录时,系统会自动为你创建账号。你只需要根据提示设置用户名和昵称,即可进入系统。",
"tip": {
"kind": "inline",
"text": "请尽量使用常用邮箱登录。后续活动报名、身份认证、议程提交和会务通知,都将与该邮箱关联。"
"text": "请尽量使用常用邮箱登录。后续活动报名、身份认证、议程提交和会务通知,都将与该邮箱关联。"
}
},
{
@@ -112,7 +115,7 @@
"afterListHtml": "完成认证后,即可继续参与活动相关操作。",
"tip": {
"kind": "inline",
"text": "对于未成年参会者,需要额外签署一份监护人知情同意书,请在报名结束前后留意群内通知和参会邮箱获取签署指南。"
"text": "需要填写个人有效证件的真实信息哦。"
}
},
{
@@ -146,17 +149,17 @@
},
"hero": {
"title": "纪念品信息",
"sub": "你可以在此 DIY 个性化参会证,并提前解锁更多活动限定纪念品信息。"
"sub": "打造你的专属参会名片!你可以在此 DIY 个性化参会证,并提前解锁更多活动限定纪念品信息。"
},
"card": {
"title": "打造你的专属参会证 ✨",
"body": "提供纪念品的个性化设定服务。点击下方按钮跳转至定制通道,录入你的身份信息和偏好以生成定制化参会证。",
"cta": "定制收集表暂未开放",
"title": "NixCN Conference 2605 打造你的专属参会证 ✨",
"body": "提供纪念品的个性化设定服务。点击下方按钮跳转至外部定制通道,录入你的专属身份参数以生成定制化参会证。",
"cta": "开启专属定制",
"ctaUrl": "https://forms.office.com/Pages/DesignPageV2.aspx?subpage=design&FormId=DQSIkWdsW0yxEjajBLZtrQAAAAAAAAAAAAZ__u0jrMtUODNTWUpJSEs0STZZWThNTTJVUlNHN01OQy4u&Token=f0e798fadec04532bf2ef8b474a4d7b2"
},
"comingSoon": {
"title": "定制通道暂未开放",
"body": "纪念品详细设计与个性化定制功能正在准备中,敬请期待",
"body": "纪念品个性化定制功能正在准备中,敬请期待。通道开放后,点击下方按钮即可跳转至外部表单完成定制。",
"close": "知道了",
"ariaClose": "关闭弹窗"
}
@@ -164,8 +167,8 @@
},
"en": {
"nav": {
"home": "Home",
"eventGuide": "Event Guide",
"home": "HOME",
"calendar": "Event Guide",
"cmsGuide": "CMS Guide",
"souvenir": "Souvenirs",
"ariaLogoHome": "NixCN Home",
@@ -188,12 +191,12 @@
"headlineBefore": "Let your\u00a0",
"headlineBadge": "Nix",
"headlineAfter": "practice <br class=\"headline-break\" />be seen",
"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.",
"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.",
"ctaPrimary": "Sign up\u00a0→",
"ctaSecondary": "CMS Guide\u00a0👈"
}
},
"eventGuide": {
"calendar": {
"meta": {
"htmlLang": "en",
"title": "Event Guide · NixCN Conference 2605",
@@ -201,7 +204,7 @@
},
"heading": {
"title": "Event Guide",
"sub": "If you're curious about what to expect at the Conference, this guide will give you a clearer picture."
"sub": "If you are a little curious about Nix, this page may give you a more concrete answer."
},
"agenda": {
"leadEmoji": "🌟",
@@ -209,7 +212,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>"
"A tea break is scheduled around <strong>15:00</strong> in the afternoon"
]
},
"schedule": {
@@ -217,10 +220,10 @@
"title": "Conference schedule",
"timeLabel": "Conference dates:",
"timeValue": "2026/06/13 (Sat.) ~ 2026/06/14 (Sun.)",
"flowLabel": "Daily Schedule:",
"flowLabel": "Daily flow:",
"flow": [
"Check-in: 08:30 10:00",
"Talks start: 10:00",
"Check-in: 08:30 09:30",
"Talks start: 09:30",
"Lunch break: 12:00 14:00",
"Afternoon talks: 14:00 18:00",
"Wrap up before: 18:30"
@@ -228,7 +231,7 @@
},
"food": {
"emoji": "🍜",
"title": "Dining Guide",
"title": "Food guide",
"mapAlt": "NixCN Conference 2605 nearby food map, with recommended restaurants within a 213 minute walk from the venue"
}
},
@@ -252,7 +255,7 @@
"id": "step-01",
"num": "01",
"name": "Log in with your email",
"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.",
"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.",
"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."
@@ -262,7 +265,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 available actions.<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 the action entries you need.<br />Here you can:",
"list": ["Join the event", "View event details", "Submit a talk", "View events you have joined"]
},
{
@@ -278,7 +281,7 @@
"afterListHtml": "Once verified, you can continue with the rest of the event actions.",
"tip": {
"kind": "inline",
"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."
"text": "Please fill in real information from a valid personal ID document."
}
},
{
@@ -292,7 +295,7 @@
"num": "05",
"name": "Talk review and scheduling",
"bodyHtmls": [
"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.",
"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.",
"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."
]
},
@@ -300,7 +303,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 respective checkpoints 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 matching step under staff guidance."
}
]
},
@@ -312,17 +315,17 @@
},
"hero": {
"title": "Souvenirs",
"sub": "Customize your badge here and unlock more exclusive event souvenir details in advance."
"sub": "Create your exclusive conference name card! DIY your personalized badge here and unlock more event-limited souvenir information in advance."
},
"card": {
"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",
"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",
"ctaUrl": "https://forms.office.com/Pages/DesignPageV2.aspx?subpage=design&FormId=DQSIkWdsW0yxEjajBLZtrQAAAAAAAAAAAAZ__u0jrMtUODNTWUpJSEs0STZZWThNTTJVUlNHN01OQy4u&Token=f0e798fadec04532bf2ef8b474a4d7b2"
},
"comingSoon": {
"title": "Customization Not Open Yet",
"body": "Souvenir design and personalized customization is still being prepared. Please check back later.",
"body": "Personalized souvenir customization is still being prepared. Please check back later — once live, the button below will take you to the external form.",
"close": "Got it",
"ariaClose": "Close dialog"
}

View File

@@ -1,6 +1,4 @@
---
import '../styles/global.css';
interface Props {
title: string;
description?: string;
@@ -20,6 +18,42 @@ const { title, description, lang } = Astro.props;
<title>{title}</title>
</head>
<body>
<slot />
<div class='page'>
<slot />
</div>
</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

@@ -0,0 +1,10 @@
---
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

@@ -0,0 +1,10 @@
---
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

@@ -0,0 +1,10 @@
---
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

@@ -0,0 +1,10 @@
---
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,269 +0,0 @@
@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;
}
}