This commit is contained in:
toom1996
2026-10-22 19:58:50 +08:00
parent 1adf71248f
commit 0f4301b5c3
10 changed files with 332 additions and 67 deletions

View File

@ -23,16 +23,18 @@ const fullLoginLabel = t('go to full login')
x-show="isOpen" x-show="isOpen"
x-cloak x-cloak
data-auth-fail={failLabel} data-auth-fail={failLabel}
data-auth-modal
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 px-5" class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 px-5"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={title} aria-label={title}
@click.self="close()" @click.self="close()"
@keydown.escape.window="close()" @keydown.escape.window="close()"
@keydown.tab="trapFocus($event)"
> >
<div class="w-full max-w-sm border-2 border-black bg-white p-8"> <div class="w-full max-w-sm border-2 border-black bg-white p-8">
<div class="mb-6 flex items-start justify-between border-b-2 border-black pb-4"> <div class="mb-6 flex items-start justify-between border-b-2 border-black pb-4">
<h3 class="text-2xl font-black uppercase leading-none tracking-tight">{title}</h3> <h3 class="text-2xl font-semibold uppercase leading-none tracking-display">{title}</h3>
<button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button> <button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button>
</div> </div>
@ -40,19 +42,19 @@ const fullLoginLabel = t('go to full login')
<form @submit.prevent="submit()" class="space-y-6" novalidate> <form @submit.prevent="submit()" class="space-y-6" novalidate>
<div> <div>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{accountLabel}</label> <label for="auth-account" class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{accountLabel}</label>
<input type="text" x-model="account" autocomplete="username" required <input id="auth-account" type="text" x-model="account" autocomplete="username" required
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" /> class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
</div> </div>
<div> <div>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{passwordLabel}</label> <label for="auth-password" class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{passwordLabel}</label>
<input type="password" x-model="password" autocomplete="current-password" required <input id="auth-password" type="password" x-model="password" autocomplete="current-password" required
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" /> class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
</div> </div>
<p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p> <p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p>
<button type="submit" :disabled="loading" <button type="submit" :disabled="loading" :aria-busy="loading"
class="w-full border border-black bg-black py-3.5 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:opacity-40"> class="w-full border border-black bg-black py-3.5 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:opacity-40">
<span x-show="!loading">{title}</span> <span x-show="!loading">{title}</span>
<span x-show="loading" x-cloak>…</span> <span x-show="loading" x-cloak>…</span>
@ -75,6 +77,7 @@ const fullLoginLabel = t('go to full login')
interface AuthStore { interface AuthStore {
isOpen: boolean isOpen: boolean
pending: HTMLElement | null pending: HTMLElement | null
lastFocused: HTMLElement | null
account: string account: string
password: string password: string
error: string error: string
@ -83,6 +86,7 @@ const fullLoginLabel = t('go to full login')
close(): void close(): void
submit(): Promise<void> submit(): Promise<void>
goFullLogin(): void goFullLogin(): void
trapFocus(e: KeyboardEvent): void
} }
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
@ -90,6 +94,7 @@ const fullLoginLabel = t('go to full login')
const authStore: AuthStore = { const authStore: AuthStore = {
isOpen: false, isOpen: false,
pending: null as HTMLElement | null, pending: null as HTMLElement | null,
lastFocused: null as HTMLElement | null,
account: "", account: "",
password: "", password: "",
error: "", error: "",
@ -98,9 +103,15 @@ const fullLoginLabel = t('go to full login')
request(el: HTMLElement): boolean { request(el: HTMLElement): boolean {
if (getUser()) return true // 已登录:放行,交还原生点击 if (getUser()) return true // 已登录:放行,交还原生点击
this.pending = el this.pending = el
this.lastFocused = el
this.error = "" this.error = ""
this.isOpen = true this.isOpen = true
document.body.style.overflow = "hidden" // 锁背景滚动 document.body.style.overflow = "hidden" // 锁背景滚动
// 焦点移入弹窗:屏幕阅读器立即播报 aria-label,键盘用户不必从页面顶端重新 Tab
requestAnimationFrame(() => {
const first = document.querySelector<HTMLElement>("[data-auth-modal] input")
first?.focus()
})
return false return false
}, },
@ -112,6 +123,11 @@ const fullLoginLabel = t('go to full login')
this.pending = null this.pending = null
this.error = "" this.error = ""
document.body.style.overflow = "" document.body.style.overflow = ""
// 关闭后把焦点还给触发它的元素。否则焦点会掉回 <body>,
// 键盘用户等于被「传送」回页面顶部,需要重新 Tab 十几下。
const back = this.lastFocused
this.lastFocused = null
if (back && document.contains(back)) back.focus?.()
}, },
async submit() { async submit() {
@ -142,6 +158,29 @@ const fullLoginLabel = t('go to full login')
this.close() this.close()
window.location.assign(`/${loc}/login?redirect=${encodeURIComponent(p)}`) window.location.assign(`/${loc}/login?redirect=${encodeURIComponent(p)}`)
}, },
// 焦点陷阱:aria-modal="true" 只是「声明」,不会自动把 Tab 关在弹窗里。
// 不实现这个,用户 Tab 几下就会跑到背后被遮住的页面元素上。
trapFocus(e: KeyboardEvent) {
const root = document.querySelector<HTMLElement>("[data-auth-modal]")
if (!root) return
const items = Array.from(
root.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
)
).filter((el) => el.offsetParent !== null)
if (items.length === 0) return
const first = items[0]
const last = items[items.length - 1]
if (!first || !last) return
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first.focus()
}
},
} }
Alpine.store("auth", authStore) Alpine.store("auth", authStore)

View File

@ -48,7 +48,7 @@ const moduleLabel = locale === 'cn' ? moduleZh || module : module || moduleZh
@apply text-[11px] tracking-[0.25em] uppercase text-[var(--c-gray-800)]; @apply text-[11px] tracking-[0.25em] uppercase text-[var(--c-gray-800)];
} }
.cs-title { .cs-title {
@apply font-black tracking-[-0.02em]; @apply font-bold tracking-[-0.02em];
line-height: 0.9; line-height: 0.9;
font-size: clamp(56px, 13vw, 128px); font-size: clamp(56px, 13vw, 128px);
margin: 0.16em 0 0.3em; margin: 0.16em 0 0.3em;

View File

@ -132,7 +132,7 @@ const hasImages = previewImages.length > 0
{ {
hasItem ? ( hasItem ? (
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16"> <article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8"> <h1 class="font-bold leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
{title} {title}
</h1> </h1>

View File

@ -38,7 +38,7 @@ const browseLabel = t("browse the archive")
<!-- ===== 未登录:整版引导 ===== --> <!-- ===== 未登录:整版引导 ===== -->
<div x-show="!user" x-cloak class="flex flex-col items-center justify-center border border-black/15 px-6 py-32 text-center"> <div x-show="!user" x-cloak class="flex flex-col items-center justify-center border border-black/15 px-6 py-32 text-center">
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{pleaseLabel}</p> <p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{pleaseLabel}</p>
<p class="mb-12 text-5xl font-black uppercase leading-none tracking-tight md:text-7xl">{titleLabel}</p> <p class="mb-12 text-5xl font-bold uppercase leading-none tracking-tight md:text-7xl">{titleLabel}</p>
<a href={loginUrl} class="inline-flex items-center justify-center border border-black bg-black px-10 py-4 text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a> <a href={loginUrl} class="inline-flex items-center justify-center border border-black bg-black px-10 py-4 text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
</div> </div>
@ -50,7 +50,7 @@ const browseLabel = t("browse the archive")
<div class="min-w-0"> <div class="min-w-0">
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{t("member profile")}</p> <p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{t("member profile")}</p>
<div class="flex flex-wrap items-end gap-x-4"> <div class="flex flex-wrap items-end gap-x-4">
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1> <h1 class="text-6xl font-bold uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1>
<!-- VIP 徽标:仅高级用户(图集详情页可见高清原图)显示 --> <!-- VIP 徽标:仅高级用户(图集详情页可见高清原图)显示 -->
<span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.3em] text-white">{t("vip")}</span> <span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.3em] text-white">{t("vip")}</span>
</div> </div>
@ -85,7 +85,7 @@ const browseLabel = t("browse the archive")
<div x-show="tab==='saved'" x-cloak> <div x-show="tab==='saved'" x-cloak>
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4"> <div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2> <h2 class="text-4xl font-bold uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
<p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p> <p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
</div> </div>
@ -97,7 +97,7 @@ const browseLabel = t("browse the archive")
<!-- 空态 --> <!-- 空态 -->
<div x-show="favorites.length === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center"> <div x-show="favorites.length === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center">
<p class="mb-10 text-3xl font-black uppercase tracking-tight text-black/80 md:text-4xl">{noSavedLabel}</p> <p class="mb-10 text-3xl font-bold uppercase tracking-tight text-black/80 md:text-4xl">{noSavedLabel}</p>
<a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a> <a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
</div> </div>
@ -136,7 +136,7 @@ const browseLabel = t("browse the archive")
<div x-show="tab==='history'" x-cloak> <div x-show="tab==='history'" x-cloak>
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4"> <div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2> <h2 class="text-4xl font-bold uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2>
<div class="flex items-baseline gap-8 pb-1"> <div class="flex items-baseline gap-8 pb-1">
<p class="text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p> <p class="text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
<button x-show="histTotal > 0" @click="clearAllHistory()" class="text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button> <button x-show="histTotal > 0" @click="clearAllHistory()" class="text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
@ -145,7 +145,7 @@ const browseLabel = t("browse the archive")
<!-- 空态 --> <!-- 空态 -->
<div x-show="histTotal === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center"> <div x-show="histTotal === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center">
<p class="mb-10 text-3xl font-black uppercase tracking-tight text-black/80 md:text-4xl">{noHistoryLabel}</p> <p class="mb-10 text-3xl font-bold uppercase tracking-tight text-black/80 md:text-4xl">{noHistoryLabel}</p>
<a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a> <a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
</div> </div>

View File

@ -46,10 +46,10 @@ const streetItems: {
<div x-data="{ activeTab: 'backstage' }" class="flex flex-col min-h-screen"> <div x-data="{ activeTab: 'backstage' }" class="flex flex-col min-h-screen">
<!-- 二级分类标签页 --> <!-- 二级分类标签页 -->
<div id="tabbar" class="mt-16 md:mt-24"> <div id="tabbar" class="mt-20 md:mt-28">
<ul class="flex items-center gap-6 list-none p-0 m-0 overflow-x-auto"> <ul class="flex items-center gap-7 md:gap-9 list-none p-0 m-0 overflow-x-auto no-scrollbar">
<li <li
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" class="tab text-[clamp(1.875rem,4.6vw,3.25rem)] font-bold tracking-display leading-[0.95] cursor-pointer whitespace-nowrap"
:class="{ 'is-active': activeTab === 'backstage' }" :class="{ 'is-active': activeTab === 'backstage' }"
@click="activeTab = 'backstage'" @click="activeTab = 'backstage'"
> >
@ -57,7 +57,7 @@ const streetItems: {
</li> </li>
<li <li
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" class="tab text-[clamp(1.875rem,4.6vw,3.25rem)] font-bold tracking-display leading-[0.95] cursor-pointer whitespace-nowrap"
:class="{ 'is-active': activeTab === 'street' }" :class="{ 'is-active': activeTab === 'street' }"
@click="activeTab = 'street'" @click="activeTab = 'street'"
> >
@ -65,7 +65,7 @@ const streetItems: {
</li> </li>
<li <li
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" class="tab text-[clamp(1.875rem,4.6vw,3.25rem)] font-bold tracking-display leading-[0.95] cursor-pointer whitespace-nowrap"
:class="{ 'is-active': activeTab === 'events' }" :class="{ 'is-active': activeTab === 'events' }"
@click="activeTab = 'events'" @click="activeTab = 'events'"
> >
@ -75,11 +75,11 @@ const streetItems: {
</div> </div>
<!-- 中间内容展示区 --> <!-- 中间内容展示区 -->
<main class="grow min-h-[400px] mt-10"> <main class="grow min-h-[400px] mt-14">
<!-- Runway --> <!-- Runway -->
<div class="panel" x-show="activeTab === 'backstage'" x-transition.opacity.duration.300ms> <div class="panel" x-show="activeTab === 'backstage'" x-transition.opacity.duration.300ms>
<div class="text-2xl font-medium mb-4"> <div class="text-[18px] uppercase tracking-label font-semibold mb-6 text-[var(--c-gray-800)]">
Hot Brand Hot Brand
</div> </div>
@ -122,7 +122,7 @@ const streetItems: {
</div> </div>
</div> </div>
<div class="hs-controls flex justify-between items-center text-[13px] font-bold"> <div class="hs-controls flex justify-between items-center text-[13px] font-medium">
<div <div
class="text-[16px] hs-arrow hs-prev cursor-pointer hover:opacity-60 transition-opacity duration-200" class="text-[16px] hs-arrow hs-prev cursor-pointer hover:opacity-60 transition-opacity duration-200"
role="button" role="button"
@ -209,14 +209,15 @@ const streetItems: {
/* 标签 */ /* 标签 */
.tab { .tab {
@apply relative pb-[6px] text-[var(--c-gray-600)] transition-colors duration-200 no-underline; @apply relative pb-[6px] text-[var(--c-gray-500)] transition-colors duration-[240ms] no-underline;
} }
.tab:hover { .tab:hover {
@apply text-[var(--c-gray-800)]; @apply text-[var(--c-gray-800)];
} }
.tab::after { .tab::after {
content: ""; content: "";
@apply absolute left-0 right-0 bottom-0 h-[2px] bg-black scale-x-0 origin-left transition-transform duration-[250ms] ease-in-out; @apply absolute left-0 right-0 bottom-0 h-[1px] bg-black scale-x-0 origin-left transition-transform duration-[240ms];
transition-timing-function: var(--ease-editorial, ease);
} }
.tab.is-active { .tab.is-active {
@apply text-black; @apply text-black;
@ -367,18 +368,18 @@ const streetItems: {
.hs-cap-brand { .hs-cap-brand {
@apply font-sans; @apply font-sans;
font-size: 21px; font-size: 21px;
font-weight: 700; font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.02em; letter-spacing: 0.06em;
line-height: 1.1; line-height: 1.1;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55); text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
} }
.hs-cap-meta { .hs-cap-meta {
@apply font-sans; @apply font-sans;
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 400;
opacity: 0.9; opacity: 0.85;
letter-spacing: 0.04em; letter-spacing: 0.02em;
line-height: 1.3; line-height: 1.3;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
max-width: 90%; max-width: 90%;
@ -405,8 +406,12 @@ const streetItems: {
@apply mb-9; @apply mb-9;
} }
.ss-grid { .ss-grid {
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-[14px] md:gap-[18px]; @apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 md:gap-5;
} }
/* 刻意不做逐项入场动画。
frontend-design skill 把它列为「AI 生成的识别特征」:
每个 section 淡入上滑、每张卡片都错落出现,是通用水印,不承载信息。
这里只保留图片加载的 blur/opacity 淡入(那是真实的状态变化)。 */
.ss-card { .ss-card {
@apply relative block w-full overflow-hidden bg-[var(--c-gray-100)] no-underline text-inherit; @apply relative block w-full overflow-hidden bg-[var(--c-gray-100)] no-underline text-inherit;
aspect-ratio: 3 / 4; aspect-ratio: 3 / 4;
@ -483,17 +488,17 @@ const streetItems: {
pointer-events: none; pointer-events: none;
} }
.ss-cap-loc { .ss-cap-loc {
font-size: clamp(18px, 2vw, 26px); font-size: clamp(1.125rem, 2vw, 1.625rem);
font-weight: 700; font-weight: 500;
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.01em; letter-spacing: -0.01em;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55); text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
} }
.ss-cap-event { .ss-cap-event {
font-size: clamp(10px, 0.8vw, 12px); font-size: clamp(0.625rem, 0.8vw, 0.75rem);
font-weight: 500; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.16em; letter-spacing: 0.18em;
line-height: 1.4; line-height: 1.4;
opacity: 0.85; opacity: 0.85;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

View File

@ -171,7 +171,7 @@ const sheet = [...new Set([...showCovers, ...snapCovers])].slice(0, 6)
<!-- 底部刊名(压在图上) --> <!-- 底部刊名(压在图上) -->
<div class="absolute inset-x-0 bottom-0 p-8 md:p-12"> <div class="absolute inset-x-0 bottom-0 p-8 md:p-12">
<p class="text-[10px] uppercase tracking-[0.4em] text-white/60">{t('archive')}</p> <p class="text-[10px] uppercase tracking-[0.4em] text-white/60">{t('archive')}</p>
<p class="mt-3 text-5xl font-black uppercase leading-[0.8] tracking-[-0.04em] text-white lg:text-7xl">{SITE_NAME}</p> <p class="mt-3 text-5xl font-bold uppercase leading-[0.8] tracking-[-0.04em] text-white lg:text-7xl">{SITE_NAME}</p>
<p class="mt-5 text-[10px] uppercase tracking-[0.34em] text-white/50">{t('paris · milan · tokyo')}</p> <p class="mt-5 text-[10px] uppercase tracking-[0.34em] text-white/50">{t('paris · milan · tokyo')}</p>
</div> </div>
</aside> </aside>
@ -184,7 +184,7 @@ const sheet = [...new Set([...showCovers, ...snapCovers])].slice(0, 6)
<span class="inline-block h-[7px] w-[7px] bg-black"></span>{t('members only')} <span class="inline-block h-[7px] w-[7px] bg-black"></span>{t('members only')}
</p> </p>
<h1 class="rise rise-2 mt-5 text-[clamp(52px,8vw,104px)] font-black uppercase leading-[0.82] tracking-[-0.045em]"> <h1 class="rise rise-2 mt-5 text-[clamp(52px,8vw,104px)] font-bold uppercase leading-[0.82] tracking-[-0.045em]">
{loginLabel} {loginLabel}
</h1> </h1>

View File

@ -62,6 +62,11 @@ const pathWithoutLocale = locales.includes(pathName.split('/')[1])
? pathName.replace(`/${locale}`, '') ? pathName.replace(`/${locale}`, '')
: pathName; : pathName;
// 导航当前项:给对应链接加 aria-current="page",屏幕阅读器据此播报「当前页面」。
// 只比较去尾斜杠后的路径,避免 /runway-looks 与 /runway-looks/ 被判成两项。
const normPath = (p: string) => p.replace(/\/+$/, '') || '/';
const isCurrent = (target: string) => normPath(pathWithoutLocale) === normPath(target);
// 顶部导航数据模型(替代原 Alpine x-data 的 menu/state)。 // 顶部导航数据模型(替代原 Alpine x-data 的 menu/state)。
// 下拉菜单与语言列表均由此驱动渲染,逻辑用 Astro 原生 script 控制开合。 // 下拉菜单与语言列表均由此驱动渲染,逻辑用 Astro 原生 script 控制开合。
const headers = { const headers = {
@ -136,11 +141,18 @@ const headers = {
<!-- 加载动画屏:独立组件,标记/样式/隐藏逻辑封装在 LoadingOverlay.astro --> <!-- 加载动画屏:独立组件,标记/样式/隐藏逻辑封装在 LoadingOverlay.astro -->
<LoadingOverlay /> <LoadingOverlay />
<!-- 键盘用户跳过导航:平时视觉隐藏,Tab 聚焦时出现在左上角。
没有它,键盘用户每进一页都要按十几次 Tab 才能摸到正文。 -->
<a
href="#main"
class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[100] focus:bg-black focus:px-4 focus:py-2 focus:text-[12px] focus:uppercase focus:tracking-label focus:text-white"
>跳到主内容</a>
<!-- 顶部主导航栏 --> <!-- 顶部主导航栏 -->
<nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase"> <nav class="flex justify-between items-center text-[13px] font-semibold tracking-label uppercase">
<div class="flex flex-wrap items-center gap-6 md:gap-8"> <div class="flex flex-wrap items-center gap-6 md:gap-8">
<a href={href(locale, ROUTES.home)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Home')}</a> <a href={href(locale, ROUTES.home)} class="nav-link cursor-pointer" aria-current={isCurrent(ROUTES.home) ? 'page' : undefined}>{t('Home')}</a>
<a href={href(locale, ROUTES.home)} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent"> <a href={href(locale, ROUTES.home)} class="nav-link cursor-pointer flex items-center gap-1.5 opacity-70">
{t('Portfolio')} {t('Portfolio')}
<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full tracking-tight leading-none"> <span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full tracking-tight leading-none">
2026 2026
@ -149,7 +161,7 @@ const headers = {
<!-- 全部的 runway:菜单项用 Astro 渲染(headers.runways.map),开合逻辑用 Alpine.js --> <!-- 全部的 runway:菜单项用 Astro 渲染(headers.runways.map),开合逻辑用 Alpine.js -->
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false"> <div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<a href={href(locale, ROUTES.runwayLooks)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('all runway looks')}</a> <a href={href(locale, ROUTES.runwayLooks)} class="nav-link cursor-pointer" aria-current={isCurrent(ROUTES.runwayLooks) ? 'page' : undefined}>{t('all runway looks')}</a>
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表" aria-haspopup="true" :aria-expanded="open"> <button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表" aria-haspopup="true" :aria-expanded="open">
<svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg> <svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button> </button>
@ -167,7 +179,7 @@ const headers = {
<!-- 街拍:菜单项用 Astro 渲染(headers.streets.map),开合逻辑用 Alpine.js --> <!-- 街拍:菜单项用 Astro 渲染(headers.streets.map),开合逻辑用 Alpine.js -->
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false"> <div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<a href={href(locale, ROUTES.streetSnaps)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Street Style')}</a> <a href={href(locale, ROUTES.streetSnaps)} class="nav-link cursor-pointer" aria-current={isCurrent(ROUTES.streetSnaps) ? 'page' : undefined}>{t('Street Style')}</a>
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 street style 列表" aria-haspopup="true" :aria-expanded="open"> <button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 street style 列表" aria-haspopup="true" :aria-expanded="open">
<svg viewBox="0 0 12 12" width="10" height="10"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg viewBox="0 0 12 12" width="10" height="10"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button> </button>
@ -207,7 +219,7 @@ const headers = {
@click={`open = false; pick('${l.code}')`} @click={`open = false; pick('${l.code}')`}
class:list={[ class:list={[
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors', 'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
l.code === locale ? 'font-bold' : 'opacity-60', l.code === locale ? 'font-medium' : 'opacity-60',
]} ]}
>{l.label}</a> >{l.label}</a>
</li> </li>
@ -218,7 +230,7 @@ const headers = {
<!-- 未登录:Login 链接(Alpine 登录后置为隐藏)。 <!-- 未登录:Login 链接(Alpine 登录后置为隐藏)。
login-redirect:交给 AuthModal 的全局门禁捕获,带上「当前页」跳登录页, login-redirect:交给 AuthModal 的全局门禁捕获,带上「当前页」跳登录页,
登录成功后回到原页,而不是一律落到个人中心(无 JS 时回落 href 到 /login)。 --> 登录成功后回到原页,而不是一律落到个人中心(无 JS 时回落 href 到 /login)。 -->
<a x-show="!user" x-cloak login-redirect href={href(locale, ROUTES.login)} class="hover:opacity-60 transition-opacity duration-200 uppercase tracking-[0.5px] text-[13px]">{t('Login')}</a> <a x-show="!user" x-cloak login-redirect href={href(locale, ROUTES.login)} class="nav-link uppercase tracking-label text-[13px]">{t('Login')}</a>
<!-- 已登录:账号下拉(hover 展开,与语言切换器同款交互) --> <!-- 已登录:账号下拉(hover 展开,与语言切换器同款交互) -->
<div x-show="user" x-cloak class="relative" @mouseenter="open = true" @mouseleave="open = false"> <div x-show="user" x-cloak class="relative" @mouseenter="open = true" @mouseleave="open = false">
@ -242,7 +254,22 @@ const headers = {
</div> </div>
</nav> </nav>
<div class="flex-1 flex flex-col"> <!-- 公告条:放在导航之下,作为头部的收边。
纯 CSS 循环(内容重复两遍,轨道左移 50% 即为一个周期),零 JS。 -->
<div class="marquee -mx-5 md:-mx-[50px] mt-6 border-b border-black/10 py-2 text-[11px] uppercase tracking-wide text-[var(--c-gray-800)]">
<div class="marquee__track">
{Array.from({ length: 2 }).map(() => (
<Fragment>
<span>✦ 2026 秋冬时装周档案</span>
<span>✦ Runway · Street · Archive</span>
<span>✦ 全球品牌持续收录</span>
<span>✦ New Season</span>
</Fragment>
))}
</div>
</div>
<div id="main" tabindex="-1" class="flex-1 flex flex-col">
<slot /> <slot />
</div> </div>
@ -349,21 +376,21 @@ const headers = {
{/* 品牌 */} {/* 品牌 */}
<div class="md:col-span-6"> <div class="md:col-span-6">
<p class="text-[10px] uppercase tracking-[0.34em] text-white/35">{t('Archive')}</p> <p class="text-[10px] uppercase tracking-wide text-white/35">{t('Archive')}</p>
<p class="mt-5 text-5xl font-black uppercase leading-[0.85] tracking-tight md:text-7xl"> <p class="mt-5 text-5xl font-bold uppercase leading-[0.85] tracking-display md:text-7xl">
{SITE_NAME} {SITE_NAME}
</p> </p>
<p class="mt-7 max-w-md text-sm leading-relaxed text-white/45"> <p class="mt-7 max-w-md text-sm leading-relaxed text-white/45">
{t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')} {t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')}
</p> </p>
<div class="mt-8 flex flex-wrap items-center gap-x-3 gap-y-2 text-[10px] uppercase tracking-[0.3em] text-white/30"> <div class="mt-8 flex flex-wrap items-center gap-x-3 gap-y-2 text-[10px] uppercase tracking-wide text-white/30">
<span>Paris</span><span>·</span><span>Milan</span><span>·</span><span>Tokyo</span><span>·</span><span>New York</span> <span>Paris</span><span>·</span><span>Milan</span><span>·</span><span>Tokyo</span><span>·</span><span>New York</span>
</div> </div>
</div> </div>
{/* 导航 */} {/* 导航 */}
<div class="md:col-span-3 md:col-start-8"> <div class="md:col-span-3 md:col-start-8">
<h3 class="text-[10px] uppercase tracking-[0.3em] text-white/35">{t('Explore')}</h3> <h3 class="text-[10px] uppercase tracking-wide text-white/35">{t('Explore')}</h3>
<ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]"> <ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]">
<li><a href={href(locale, ROUTES.runwayLooks)} class="text-white/65 transition-colors hover:text-white">{t('all runway looks')}</a></li> <li><a href={href(locale, ROUTES.runwayLooks)} class="text-white/65 transition-colors hover:text-white">{t('all runway looks')}</a></li>
<li><a href={href(locale, ROUTES.streetSnaps)} class="text-white/65 transition-colors hover:text-white">{t('Street Style')}</a></li> <li><a href={href(locale, ROUTES.streetSnaps)} class="text-white/65 transition-colors hover:text-white">{t('Street Style')}</a></li>
@ -373,7 +400,7 @@ const headers = {
{/* 站点操作(全部是真实可用的入口,不留死链) */} {/* 站点操作(全部是真实可用的入口,不留死链) */}
<div class="md:col-span-3"> <div class="md:col-span-3">
<h3 class="text-[10px] uppercase tracking-[0.3em] text-white/35">{t('Site')}</h3> <h3 class="text-[10px] uppercase tracking-wide text-white/35">{t('Site')}</h3>
<ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]"> <ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]">
<li> <li>
<a href={href(locale === 'cn' ? 'en' : 'cn', ROUTES.home)} class="text-white/65 transition-colors hover:text-white"> <a href={href(locale === 'cn' ? 'en' : 'cn', ROUTES.home)} class="text-white/65 transition-colors hover:text-white">
@ -394,9 +421,9 @@ const headers = {
</div> </div>
{/* 底部条 */} {/* 底部条 */}
<div class="mt-14 flex flex-col gap-4 border-t border-white/10 pt-6 text-[10px] uppercase tracking-[0.26em] text-white/35 sm:flex-row sm:items-center sm:justify-between"> <div class="mt-14 flex flex-col gap-4 border-t border-white/10 pt-6 text-[10px] uppercase tracking-label text-white/35 sm:flex-row sm:items-center sm:justify-between">
<p>© 2026 {SITE_NAME}. {t('all rights reserved.')}</p> <p>© 2026 {SITE_NAME}. {t('all rights reserved.')}</p>
<p class="tracking-[0.3em]">{t('Runway · Street · Archive')}</p> <p class="tracking-wide">{t('Runway · Street · Archive')}</p>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -10,7 +10,7 @@ Astro.response.status = 404
<Layout title={t('Page not found')}> <Layout title={t('Page not found')}>
<div class="flex flex-1 flex-col items-center justify-center min-h-[50vh] text-center gap-6"> <div class="flex flex-1 flex-col items-center justify-center min-h-[50vh] text-center gap-6">
<h1 class="text-6xl font-black tracking-tight">404</h1> <h1 class="text-6xl font-medium tracking-tight">404</h1>
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-[0.2em]">{t('Page not found')}</p> <p class="text-sm text-[var(--c-gray-700)] uppercase tracking-[0.2em]">{t('Page not found')}</p>
<a <a
href={href(locale, ROUTES.home)} href={href(locale, ROUTES.home)}

View File

@ -1,6 +1,57 @@
@import "tailwindcss"; @import "tailwindcss";
/* 中性灰阶令牌:统一全站散落的 #f1f1f1/#efefef/#e6e6e6/#ccc/#bdbdbd/#999/#888/#666 等 */ /* ============================================================
设计令牌(Tailwind v4 @theme)
参考规律(NABOUN 实站抓取 + ui-ux-pro-max 方法论):
1. 字重只用 400 / 500,高级感来自字距与留白,不来自粗体
2. 全大写 + 0.12em 字距 + 11~13px 是标签的统一签名
3. 1px 发丝线分块,不用色块;全站只保留黑白灰三层明度
4. 图片统一竖版,卡片无圆角无遮罩
约束:不加载任何 webfont(体积与首屏优先),层级靠字重 + 字距 + 尺度建立。
============================================================ */
@theme {
--font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC",
"Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
/* 字距:全站只允许这四档。
历史上散落的 0.5px / 0.04em / 0.14em / 0.22em / 0.26em 全部收敛到这里。 */
--tracking-display: -0.03em;
--tracking-label: 0.12em;
--tracking-wide: 0.18em;
--tracking-meta: 0.02em;
/* 动效:统一缓动 + 三档时长,替代 150/200/250/300/400/450ms 混用。
0.22,1,0.36,1 是「快出慢停」曲线,进入视口时更稳,不弹。 */
--ease-editorial: cubic-bezier(0.22, 1, 0.36, 1);
/* 字号阶梯:全部用 rem,而不是 px。
默认根字号 16px 下数值与原来的 px 完全等价,视觉零变化;
但用户把浏览器「字体大小」调大时,rem 会跟着放大,px 不会(Vercel #1.3)。
括号里是 16px 基准下的等价像素。 */
--text-micro: 0.625rem; /* 10px */
--text-micro--line-height: 1.4;
--text-label: 0.6875rem; /* 11px */
--text-label--line-height: 1.2;
--text-meta: 0.8125rem; /* 13px */
--text-meta--line-height: 1.55;
--text-body: 0.9375rem; /* 15px */
--text-body--line-height: 1.75;
--text-lead: 1.0625rem; /* 17px */
--text-lead--line-height: 1.55;
--text-h3: 1.375rem; /* 22px */
--text-h3--line-height: 1.25;
--text-h2: clamp(1.625rem, 3vw, 2.125rem); /* 26 → 34px */
--text-h2--line-height: 1.15;
--text-h1: clamp(2.125rem, 5.5vw, 4rem); /* 34 → 64px */
--text-h1--line-height: 0.95;
--text-display: clamp(2.625rem, 7vw, 5.25rem); /* 42 → 84px */
--text-display--line-height: 0.92;
}
/* 中性灰阶 + 发丝线令牌:统一全站散落的 #f1f1f1/#efefef/#e6e6e6/#ccc/#bdbdbd/#999/#888/#666 等 */
:root { :root {
--c-gray-50: #f6f6f6; --c-gray-50: #f6f6f6;
--c-gray-100: #f1f1f1; --c-gray-100: #f1f1f1;
@ -11,6 +62,20 @@
--c-gray-600: #999; --c-gray-600: #999;
--c-gray-700: #888; --c-gray-700: #888;
--c-gray-800: #666; --c-gray-800: #666;
/* 发丝线:现代极简电商的主要分隔手段。1px + 12% 黑,比纯黑细框更透气。 */
--line: rgba(0, 0, 0, 0.12);
--line-strong: rgba(0, 0, 0, 0.24);
}
/* 英文站字距单独收紧一档。
原因:英文标签是全大写 + 宽字距,大写字母本身等高、无升降部,
加上 0.12em 字间空隙,整行密度明显低于中文,视觉上会「变轻」。
收窄到 0.09em 让单词重新聚拢,重量感就回来了。
中文站保持原值(方块字之间需要更多呼吸,收太紧会糊)。 */
html[lang="en"] {
--tracking-label: 0.09em;
--tracking-wide: 0.14em;
} }
[x-cloak] { display: none !important; } [x-cloak] { display: none !important; }
@ -49,4 +114,109 @@ select:focus-visible,
outline-offset: 2px; outline-offset: 2px;
} }
/* ============================================================
全局排版细节
============================================================ */
/* 数字等宽:页码 / 年份 / 计数在内容切换时不会左右跳动 */
body {
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 翻页 / 筛选导致内容高度变化时,避免滚动条出现消失引起的整页横向抖动。
来自 Vercel web-design-guidelines #3.6 */
scrollbar-gutter: stable;
}
/* 长标题折行更均衡,避免最后一行只剩一个词(#1.5)。
目前只作用于大标题,正文不设,避免影响折行节奏。 */
h1,
.display-title {
text-wrap: balance;
}
::selection {
background: #111;
color: #fff;
}
/* 导航链接:hover 时 1px 黑线从左滑入。
比原来的整体 opacity 变化更克制,也更像编辑类网站的处理。 */
.nav-link {
position: relative;
}
.nav-link::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -3px;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left center;
transition: transform 240ms var(--ease-editorial, ease);
}
.nav-link:hover::after {
transform: scaleX(1);
}
/* 触屏(粗指针)上把点击热区撑到 ≥44px 高,但不动布局。
13px 的导航文字在手机上可点区域只有 ~16px 高,拇指很容易点空(Vercel #3.3)。
用伪元素扩展命中区,而不是加 padding —— 加 padding 会把下划线一起推下去。 */
@media (pointer: coarse) {
.nav-link::before {
content: "";
position: absolute;
inset: -0.75rem -0.5rem;
}
}
/* ============================================================
顶部公告条(marquee)
NABOUN 式的循环滚动条:零 JS、纯 CSS、无缝衔接。
用法:内容在容器里重复两遍,轨道整体左移 50% 即为一个周期。
============================================================ */
.marquee {
overflow: hidden;
white-space: nowrap;
}
.marquee__track {
display: inline-flex;
align-items: center;
gap: 2.5rem;
padding-left: 2.5rem;
animation: marquee-scroll 32s linear infinite;
will-change: transform;
}
.marquee:hover .marquee__track {
animation-play-state: paused;
}
@keyframes marquee-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
/* 列表入场:逐项错落 40ms,避免整屏同时出现造成的「跳一下」 */
@keyframes rise-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@ -23,10 +23,12 @@
@apply flex justify-between items-end gap-6 py-5 pb-[22px]; @apply flex justify-between items-end gap-6 py-5 pb-[22px];
} }
.showsh1 { .showsh1 {
@apply text-[clamp(40px,6vw,72px)] leading-[0.9] font-black tracking-[-0.02em] my-0 mb-[10px]; @apply text-[clamp(2.5rem,6vw,4.5rem)] leading-[0.9] font-bold my-0 mb-[10px];
letter-spacing: var(--tracking-display, -0.03em);
} }
.snapsh1 { .snapsh1 {
@apply text-[clamp(34px,5.5vw,64px)] leading-[0.95] font-black tracking-[-0.02em] my-0 mb-[8px]; @apply text-[clamp(2.125rem,5.5vw,4rem)] leading-[0.95] font-bold my-0 mb-[8px];
letter-spacing: var(--tracking-display, -0.03em);
} }
/* 已激活筛选 pills */ /* 已激活筛选 pills */
@ -34,7 +36,8 @@
.snapspills { @apply flex flex-wrap gap-2 mt-4; } .snapspills { @apply flex flex-wrap gap-2 mt-4; }
.showspill, .showspill,
.snapspill { .snapspill {
@apply inline-flex items-center gap-2 border-[1.5px] border-black p-[6px_10px] text-[10.5px] uppercase tracking-[0.14em] text-black bg-white; @apply inline-flex items-center gap-2 border border-black p-[6px_10px] text-[10px] uppercase text-black bg-white;
letter-spacing: var(--tracking-label, 0.12em);
} }
.showspill .x, .showspill .x,
.snapspill .x { .snapspill .x {
@ -56,11 +59,14 @@
} }
.showsaside-head, .showsaside-head,
.snapsaside-head { .snapsaside-head {
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0; @apply flex items-center justify-between p-[14px_14px_12px] border-b border-black/15 shrink-0;
} }
.showsaside-h, .showsaside-h,
.snapsaside-h { .snapsaside-h {
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200; @apply text-[11px] uppercase font-medium m-0 leading-none transition-opacity duration-[240ms];
/* 不用 @apply tracking-*:本文件 @reference 的是 tailwind 默认主题,
取不到 global.css 里 @theme 定义的值,只能走运行时 CSS 变量。 */
letter-spacing: var(--tracking-wide, 0.18em);
} }
.showsaside-body, .showsaside-body,
.snapsaside-body { .snapsaside-body {
@ -74,7 +80,8 @@
} }
.showsg-h, .showsg-h,
.snapsg-h { .snapsg-h {
@apply flex justify-between items-center text-[11px] tracking-[0.22em] uppercase font-bold text-black mb-2 cursor-pointer select-none; @apply flex justify-between items-center text-[11px] uppercase font-medium text-black mb-2 cursor-pointer select-none;
letter-spacing: var(--tracking-wide, 0.18em);
} }
.showsg-right { .showsg-right {
@apply inline-flex items-center gap-2.5; @apply inline-flex items-center gap-2.5;
@ -113,7 +120,8 @@
.snapschecks--flat li label { @apply inline-flex items-center gap-2 cursor-pointer w-full; } .snapschecks--flat li label { @apply inline-flex items-center gap-2 cursor-pointer w-full; }
.showsmore { .showsmore {
@apply block w-full mt-[12px] text-center text-[11px] uppercase tracking-[0.22em] text-black font-bold cursor-pointer; @apply block w-full mt-[12px] text-center text-[11px] uppercase text-black font-medium cursor-pointer;
letter-spacing: var(--tracking-wide, 0.18em);
} }
.showsclear, .showsclear,
.snapsclear { .snapsclear {
@ -126,7 +134,7 @@
.showsgrid, .showsgrid,
.snapsgrid { .snapsgrid {
@apply grid grid-cols-2 gap-[22px] relative max-sm:grid-cols-1; @apply grid grid-cols-2 gap-6 relative max-sm:grid-cols-1;
} }
.showsgrid.is-loading::before, .showsgrid.is-loading::before,
.snapsgrid.is-loading::before { .snapsgrid.is-loading::before {
@ -137,7 +145,17 @@
.showscard, .showscard,
.snapscard { .snapscard {
@apply relative flex gap-[14px] no-underline text-inherit bg-white p-3 transition-colors duration-200 hover:border-black max-md:flex-col max-md:gap-[10px]; @apply relative flex gap-[14px] no-underline text-inherit bg-white p-3 border border-transparent transition-[color,background-color,border-color,transform] duration-[240ms] hover:border-black/20 max-md:flex-col max-md:gap-[10px];
transition-timing-function: var(--ease-editorial, ease);
}
/* hover 时整体上浮 2px:比单纯变色更能表达「可点」,且不触发重排。
必须包在 (hover: hover) 里:触屏浏览器会「粘滞 hover」,
手指离开后卡片停在上浮状态,看起来像坏了。 */
@media (hover: hover) {
.showscard:hover,
.snapscard:hover {
@apply -translate-y-[2px];
}
} }
.showscard:hover .showscard-img img, .showscard:hover .showscard-img img,
.snapscard:hover .snapscard-img img { @apply scale-[1.04]; } .snapscard:hover .snapscard-img img { @apply scale-[1.04]; }
@ -176,11 +194,13 @@
} }
.showscard-title, .showscard-title,
.snapscard-title { .snapscard-title {
@apply text-[15.5px] font-bold leading-[1.32] my-0 mb-[10px] text-[#111] tracking-[-0.01em] line-clamp-2 h-[41px] overflow-hidden; @apply text-[16px] font-bold leading-[1.32] my-0 mb-[10px] text-[#111] line-clamp-2 h-[42px] overflow-hidden;
letter-spacing: var(--tracking-display, -0.03em);
} }
.showscard-meta, .showscard-meta,
.snapscard-meta { .snapscard-meta {
@apply text-[9.5px] text-[var(--c-gray-700)] my-0 mb-[14px] leading-[1.55] tracking-[0.04em] uppercase line-clamp-2 h-[29px] overflow-hidden; @apply text-[10px] text-[var(--c-gray-700)] my-0 mb-[14px] leading-[1.55] uppercase line-clamp-2 h-[31px] overflow-hidden;
letter-spacing: var(--tracking-meta, 0.02em);
} }
.showscard-strip, .showscard-strip,
@ -200,7 +220,8 @@
.showscard-thumb.placeholder, .showscard-thumb.placeholder,
.snapscard-thumb.placeholder { @apply bg-[var(--c-gray-50)]; } .snapscard-thumb.placeholder { @apply bg-[var(--c-gray-50)]; }
.showscard-thumb-more { .showscard-thumb-more {
@apply absolute inset-0 bg-black/55 text-white text-[11px] font-bold tracking-[0.04em] flex items-center justify-center; @apply absolute inset-0 bg-black/55 text-white text-[11px] font-medium flex items-center justify-center;
letter-spacing: var(--tracking-meta, 0.02em);
} }
/* 骨架屏 */ /* 骨架屏 */
@ -218,7 +239,10 @@
.showspager, .showspager,
.snapspager { @apply flex flex-wrap items-center justify-center gap-2 mt-[38px]; } .snapspager { @apply flex flex-wrap items-center justify-center gap-2 mt-[38px]; }
.pg { .pg {
@apply min-w-[40px] h-[40px] px-3 border-[1.5px] border-black bg-white text-black text-[11px] uppercase tracking-[0.12em] font-bold cursor-pointer rounded-none transition-colors duration-200; /* 边框不透明度提到 50%:UI 组件的边界对比度需 ≥3:1(WCAG 2.2 / Vercel #2.3),
black/25 只有约 1.8:1,弱视用户几乎看不见分页框。 */
@apply min-w-[40px] h-[40px] px-3 border border-black/50 bg-white text-black text-[11px] uppercase font-medium cursor-pointer rounded-none transition-colors duration-[240ms];
letter-spacing: var(--tracking-label, 0.12em);
} }
.pg.is-current { @apply bg-black text-white cursor-default; } .pg.is-current { @apply bg-black text-white cursor-default; }
.pg:disabled { @apply opacity-30 cursor-default; } .pg:disabled { @apply opacity-30 cursor-default; }