This commit is contained in:
toom1996
2026-09-15 19:59:14 +08:00
parent 199300b203
commit 3f79cef587
16 changed files with 373 additions and 241 deletions

View File

@ -32,20 +32,20 @@ const fullLoginLabel = t('go to full login')
>
<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">
<h3 class="font-serif text-2xl font-black uppercase leading-none tracking-tight">{title}</h3>
<h3 class="text-2xl font-black uppercase leading-none tracking-tight">{title}</h3>
<button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button>
</div>
<p class="mb-7 font-mono text-[11px] uppercase tracking-widest text-zinc-400">{prompt}</p>
<p class="mb-7 text-[11px] uppercase tracking-widest text-zinc-400">{prompt}</p>
<form @submit.prevent="submit()" class="space-y-6" novalidate>
<div>
<label class="mb-2 block font-mono text-[11px] uppercase tracking-widest text-zinc-500">{accountLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-zinc-500">{accountLabel}</label>
<input 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" />
</div>
<div>
<label class="mb-2 block font-mono text-[11px] uppercase tracking-widest text-zinc-500">{passwordLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-zinc-500">{passwordLabel}</label>
<input 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" />
</div>
@ -53,14 +53,14 @@ const fullLoginLabel = t('go to full login')
<p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p>
<button type="submit" :disabled="loading"
class="w-full border border-black bg-black py-3.5 font-mono 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" x-cloak>…</span>
</button>
</form>
<div class="mt-6 border-t border-black/10 pt-4 text-center">
<button type="button" @click="goFullLogin()" class="font-mono text-[10px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">{fullLoginLabel}</button>
<button type="button" @click="goFullLogin()" class="text-[10px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">{fullLoginLabel}</button>
</div>
</div>
</div>

View File

@ -102,13 +102,13 @@
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
}
.showsmodal-tip {
@apply mt-2 text-[11px] leading-snug text-black/45 font-mono;
@apply mt-2 text-[11px] leading-snug text-black/45;
}
.showsmodal-az {
@apply flex flex-wrap gap-1 px-4 pb-3 shrink-0;
}
.showsmodal-az .az {
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none font-mono transition-colors hover:bg-black/5;
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none transition-colors hover:bg-black/5;
}
.showsmodal-az .az.active {
@apply bg-black text-white border-black;

View File

@ -34,7 +34,7 @@ const { locale } = getI18n(Astro)
max-width: 760px;
margin: 0 auto;
padding: 48px 4px 60px;
@apply font-mono text-black;
@apply text-black;
}
.cs-kicker {
display: flex;
@ -44,7 +44,7 @@ const { locale } = getI18n(Astro)
@apply text-[11px] tracking-[0.25em] uppercase text-[#666];
}
.cs-title {
@apply font-serif font-black tracking-[-0.02em];
@apply font-black tracking-[-0.02em];
line-height: 0.9;
font-size: clamp(56px, 13vw, 128px);
margin: 0.16em 0 0.3em;

View File

@ -31,7 +31,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
>
{/* 计数:mono + 0.22em 字距(全站「小标与计数」档),直角细框,零填充 */}
<div
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 font-mono text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
x-text="`${String(lbIndex + 1).padStart(2, '0')} / ${String(imageCount).padStart(2, '0')}`"
></div>
@ -75,7 +75,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
aria-label={`${i + 1}`}
>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm">
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm">
{String(i + 1).padStart(2, "0")}
</span>
</button>
@ -91,7 +91,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
:aria-label="String(5 + i + 1)"
>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(5 + i + 1).padStart(2, '0')"></span>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(5 + i + 1).padStart(2, '0')"></span>
</button>
</template>
</div>

View File

@ -1,7 +1,7 @@
---
// 加载屏:首屏/整页切换时遮盖未渲染内容,避免 FOUC。
// 轻量原则(用户要求"朴实流畅"):
// - 只用静态大字 + mono 小标 + 一条 2px 细线进度(transform/opacity 动画,最便宜);
// - 只用静态大字 + 小标 + 一条 2px 细线进度(transform/opacity 动画,最便宜);
// - 去掉昂贵的多层 text-shadow 位移动画与外部字体;最短展示时间压缩到 ~350ms,跟手不拖沓。
import { config } from '@/lib/config';
@ -37,7 +37,6 @@ const { t } = getI18n(Astro)
}
#loading-screen h1 {
margin: 0;
font-family: ui-serif, Georgia, Cambria, 'Times New Roman', serif;
font-size: clamp(2.2rem, 6vw, 4rem);
font-weight: 900;
line-height: 1;
@ -46,7 +45,6 @@ const { t } = getI18n(Astro)
}
#loading-screen .ls-sub {
margin: 1.1em 0 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.34em;
text-transform: uppercase;

View File

@ -15,8 +15,8 @@ interface Props {
const { article, error = null, preview = false, imageTotal = 0 } = Astro.props
const { locale, t } = getI18n(Astro)
// 改造后:后端详情接口 images 仅返回主图(look 图),细节图在 details 分组,需经「细节入口」展开。
// 故此处 images 即为主图集合,默认只渲染它们;每张主图带 look_index 用于关联其细节图。
// 后端详情接口 images 仅返回主图(look 图),每张主图的细节图嵌套在它自己的 detail 子数组下。
// 故此处 images 即为主图集合,默认只渲染它们;detail 随主图一起带上,供「细节入口」展开。
const images = (article?.images ?? [])
.map((im) => ({
id: im.id,
@ -27,17 +27,28 @@ const images = (article?.images ?? [])
raw: im.image || "", // 相对路径:收藏快照存相对地址,不把宿主/端口写死进库
name: im.name || "",
lookIndex: Number(im.look_index ?? 0),
// 该主图下的细节图:与主图同一套字段映射(递归同构)
detail: (im.detail ?? []).map((d) => ({
id: d.id || "",
url: ssrAbs(d.image || ""),
thumb: ssrAbs(d.thumb || d.image || ""),
raw: d.image || "",
name: d.name || "",
})),
}))
.filter((im) => im.url)
// 每个主图(look_index)对应的细节图数量,用于网格卡片左下角标「该主图有几张细节图」。
// 来源与灯箱右下角细节簇一致:后端详情接口的 details 分组(按 look_index 聚合)。
// SSR 首屏图直接查这张表;登录后追加的 extraImages 由 Alpine 的 detailCount() 读同一份数据。
const detailsByLook = new Map<number, number>()
for (const d of (article?.details ?? []) as { look_index?: number; images?: unknown[] }[]) {
const li = Number(d.look_index ?? 0)
if (li) detailsByLook.set(li, (d.images ?? []).length)
}
// 网格左下角的「细节图张数」角标交给 Alpine 的 detailCount() 响应式控制(而非 SSR 编译期条件):
// 首屏前 PREVIEW_LIMIT 张由下面的 data-details 载荷构建映射,登录后由 gallery:full 用完整图集重建,
// 未登录 / 已登录共用同一条渲染路径。
// 递给浏览器端 Alpine 的细节图数据:只序列化「带细节图的主图」投影(不带主图自身 URL,
// 避免把整份 images 重复塞进 HTML)。元素形状与 images 元素一致(look_index + detail),
// 故 Alpine 的 buildDetailMapFrom() 能与「登录后拿回的完整 images」共用同一套解析逻辑。
// 未登录时后端只返回前 PREVIEW_LIMIT 张主图(细节图随所属主图一起返回),此处即为这几张的细节子树。
const detailPayload = (article?.images ?? [])
.filter((im) => (im.detail?.length ?? 0) > 0)
.map((im) => ({ look_index: Number(im.look_index ?? 0), detail: im.detail }))
// 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示,
// 已登录(客户端 loadAuthedState 拉全量)或图片本就不超上限时全部展示。前 PREVIEW_LIMIT 张服务端已渲染(SEO/无 JS 友好)。
@ -64,7 +75,7 @@ const homeHref = href(locale, ROUTES.home)
data-gallery-title={article?.title ?? ""}
data-gallery-brand={article?.brand_name ?? ""}
data-preview={preview ? "true" : "false"}
data-details={JSON.stringify(article?.details ?? [])}
data-details={JSON.stringify(detailPayload)}
class="min-h-screen bg-white text-black font-sans selection:bg-black selection:text-white"
>
{
@ -78,11 +89,11 @@ const homeHref = href(locale, ROUTES.home)
<section class="mt-16">
{/* 每行张数切换(应对图集图片很多的情况):2/3/4/5 列,移动端固定 2 列 */}
<div class="flex items-center justify-between mb-5 border-b border-black/10 pb-3">
<span class="text-[11px] font-mono uppercase tracking-[0.22em] text-[#999]">
<span class="text-[11px] uppercase tracking-[0.22em] text-[#999]">
{(imageTotal || images.length)} LOOKS
</span>
<div class="flex items-center gap-1" role="group" aria-label="Images per row">
<span class="text-[11px] font-mono uppercase tracking-[0.18em] text-[#999] mr-2 hidden sm:inline">PER ROW</span>
<span class="text-[11px] uppercase tracking-[0.18em] text-[#999] mr-2 hidden sm:inline">PER ROW</span>
{/* 每行张数切换:只有「3 张 / 5 张」两档,用竖条图标直观表达列数(避免文字数字按钮)。
density 只影响 md+;移动端固定 2 列(5 列在手机上不可用)。 */}
<button
@ -125,7 +136,6 @@ const homeHref = href(locale, ROUTES.home)
:class="gridClass()"
>
{previewImages.map((im, i) => {
const dc = detailsByLook.get(im.lookIndex) || 0
return (
<figure
data-image-index={i}
@ -162,22 +172,24 @@ const homeHref = href(locale, ROUTES.home)
</button>
{/* 细节图数量角标:有细节图的主图在左下角标注「张数」(图层图标 + 数字),
点它直接开细节灯箱。纯主图(无细节图)不显示任何角标,保持网格干净。 */}
{dc > 0 && (
<button
type="button"
@click.stop={`openDetailLb(${im.lookIndex})`}
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] font-mono uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
title={`${dc} detail${dc > 1 ? "s" : ""}`}
aria-label={`${dc} detail${dc > 1 ? "s" : ""}`}
>
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="13" height="13" rx="1.5" />
<path d="M8 21h11a2 2 0 0 0 2-2V8" />
</svg>
{dc}
</button>
)}
点它直接开细节灯箱。纯主图(无细节图)不显示任何角标,保持网格干净。
数量走 Alpine 的 detailCount()(响应式):未登录取前 PREVIEW_LIMIT 张的细节子树,
登录后由 gallery:full 重建映射,两态共用同一条渲染路径。 */}
<button
type="button"
x-show={`detailCount(${im.lookIndex}) > 0`}
x-cloak
@click.stop={`openDetailLb(${im.lookIndex})`}
:title={`detailCount(${im.lookIndex}) + ' detail' + (detailCount(${im.lookIndex}) > 1 ? 's' : '')`}
:aria-label={`detailCount(${im.lookIndex}) + ' detail' + (detailCount(${im.lookIndex}) > 1 ? 's' : '')`}
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
>
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="13" height="13" rx="1.5" />
<path d="M8 21h11a2 2 0 0 0 2-2V8" />
</svg>
<span x-text={`detailCount(${im.lookIndex})`}></span>
</button>
</figure>
)
})}
@ -193,18 +205,18 @@ const homeHref = href(locale, ROUTES.home)
class="relative bg-[#f2f2f2] cursor-pointer group overflow-hidden"
>
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
<div class="w-full aspect-[2/3] flex items-center justify-center text-[#c4c4c4] group-hover:text-[#9a9a9a] transition-colors pointer-events-none">
<div class="w-full aspect-[2/3] flex items-center justify-center text-black group-hover:text-[#9a9a9a] transition-colors pointer-events-none">
<div class="flex flex-col items-center gap-2">
<svg class="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
<path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
</svg>
<span class="text-[10px] font-mono uppercase tracking-[0.18em]">{t('login to view')}</span>
<span class="text-[10px] uppercase tracking-[0.18em]">{t('login to view')}</span>
</div>
</div>
{/* 真实图已无 look 序号 caption,占位格也改为绝对定位的 +N photos,
不撑高格子,保证遮罩格与真实图片格总高一致 */}
<span class="absolute left-2 bottom-2 z-10 text-[10px] font-mono uppercase tracking-[0.14em] text-[#b8b8b8] pointer-events-none">+{lockedCount} {t('photos')}</span>
{/* <span class="absolute left-2 bottom-2 z-10 text-[10px] uppercase tracking-[0.14em] text-[#b8b8b8] pointer-events-none">+{lockedCount} {t('photos')}</span> */}
</figure>
))
}
@ -248,7 +260,7 @@ const homeHref = href(locale, ROUTES.home)
type="button"
x-show="detailCount(im.lookIndex) > 0"
@click.stop="openDetailLb(im.lookIndex)"
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] font-mono uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
:title="`${detailCount(im.lookIndex)} detail${detailCount(im.lookIndex) > 1 ? 's' : ''}`"
>
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@ -273,13 +285,13 @@ const homeHref = href(locale, ROUTES.home)
</article>
) : (
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
<p class="font-mono text-sm text-[#888] uppercase tracking-widest mb-4">
<p class="text-sm text-[#888] uppercase tracking-widest mb-4">
{error === "missing" ? t('No article ID specified') : t('Failed to load article. Please refresh or try later.')}
</p>
<a
href={homeHref}
class="text-[12px] font-mono uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
class="text-[12px] uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
>
← {t('Back to Index')}
</a>
@ -287,8 +299,7 @@ const homeHref = href(locale, ROUTES.home)
)
}
{/* 大图灯箱:与街拍详情共用 components/GalleryLightbox.astro(标记)+ lib/gallery.ts(交互)。
走秀独有的「当前 look 的细节图簇」经 details 插槽注入。 */}
{/* 大图灯箱:与街拍详情共用 components/GalleryLightbox.astro(标记)+ lib/gallery.ts(交互)。 走秀独有的「当前 look 的细节图簇」经 details 插槽注入。 */}
{article && previewImages.length > 0 && (
<GalleryLightbox images={previewImages} aspect="2/3">
{/* 当前 look 的细节图:右下角一小簇缩略图(竖版主图右下角本就是空白,不遮挡主图)。
@ -301,7 +312,7 @@ const homeHref = href(locale, ROUTES.home)
x-cloak
class="absolute right-4 sm:right-6 bottom-[72px] z-30 flex flex-col items-end gap-2"
>
<span class="font-mono text-[10px] uppercase tracking-[0.22em] text-[#999]">Details</span>
<span class="text-[10px] uppercase tracking-[0.22em] text-[#999]">Details</span>
<div class="flex max-w-[min(70vw,440px)] gap-2 overflow-x-auto no-scrollbar">
<template x-for="(d, i) in currentDetailList" :key="d.url">
<button
@ -330,7 +341,7 @@ const homeHref = href(locale, ROUTES.home)
aria-hidden="true"
>
<div
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 font-mono text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
x-text="`${String(detailLbIndex + 1).padStart(2, '0')} / ${String(detailImages.length).padStart(2, '0')}`"
></div>
@ -373,7 +384,7 @@ const homeHref = href(locale, ROUTES.home)
x-bind:class="detailLbIndex === i ? 'opacity-100 ring-1 ring-black' : ''"
:aria-label="String(i + 1)"
>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(i + 1).padStart(2, '0')"></span>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(i + 1).padStart(2, '0')"></span>
</button>
</template>
</div>
@ -423,7 +434,7 @@ const homeHref = href(locale, ROUTES.home)
</div>
<div
class="absolute left-0 right-0 bottom-4 z-20 px-6 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-black/60 pointer-events-none"
class="absolute left-0 right-0 bottom-4 z-20 px-6 text-center text-[11px] uppercase tracking-[0.2em] text-black/60 pointer-events-none"
x-text="currentDetailName"
></div>
</div>
@ -464,6 +475,7 @@ const homeHref = href(locale, ROUTES.home)
getImageLookIndex(index: number): number
currentDetailList: DetailImage[]
openDetailLbAt(idx: number): void
buildDetailMapFrom(images: any[]): void
buildDetailMap(): void
detailCount(li: number): number
openDetailLb(li: number, startIndex?: number, fromLb?: boolean): void
@ -511,8 +523,15 @@ const homeHref = href(locale, ROUTES.home)
lbReturnIndex: 0,
init() {
// 本组件独有:解析后端 details 分组(按 look_index 聚合细节图),供「细节入口」展开灯箱。
// 本组件独有:从 SSR 载荷构建「细节入口」用的 look_index -> 细节图 映射。
this.buildDetailMap()
// 未登录时后端只返回前 PREVIEW_LIMIT 张主图的细节子树;登录后 loadAuthedState 拿回完整图集,
// 需据其重建映射,否则第 6 张起的角标 / 细节灯箱仍为空(详见 lib/gallery.ts 的 gallery:full)。
// 先挂监听再初始化:galleryInit() 内部可能立刻触发一次登录态取数。
window.addEventListener("gallery:full", ((e: Event) => {
const imgs = (e as CustomEvent).detail?.images
this.buildDetailMapFrom(Array.isArray(imgs) ? imgs : [])
}) as EventListener)
// 共享初始化(登录态、图片计数、缩略图懒加载、收藏态、深链定位、登录后补全量)
this.galleryInit()
},
@ -551,24 +570,30 @@ const homeHref = href(locale, ROUTES.home)
},
// ===== 细节图分组 =====
// 由根节点 data-details(后端按 look_index 聚合的细节图 JSON)构建 look_index -> 图片数组映射。
// 由「主图数组」构建 look_index -> 细节图数组 映射。两个来源同构,共用本函数:
// ① SSR:根节点 data-details(只含带细节图的主图投影 [{ look_index, detail }])
// ② 登录后:详情接口返回的完整 images(细节图已嵌套在每张主图的 detail 下)
buildDetailMapFrom(images: any[]) {
const map: Record<number, { id: string; url: string; thumb: string; raw: string; name: string }[]> = {}
for (const im of images || []) {
const li = Number(im?.look_index ?? 0)
const kids: any[] = Array.isArray(im?.detail) ? im.detail : []
if (!li || !kids.length) continue
map[li] = kids.map((d: any) => ({
id: d.id || "",
url: toAbs(d.image || ""),
thumb: toAbs(d.thumb || d.image || ""),
raw: d.image || "",
name: d.name || "",
}))
}
this.detailImagesByLook = map
},
buildDetailMap() {
try {
const raw = this.root()?.dataset.details
const arr: any[] = raw ? JSON.parse(raw) : []
const map: Record<number, { id: string; url: string; thumb: string; raw: string; name: string }[]> = {}
for (const d of arr) {
const li = Number(d.look_index ?? 0)
if (!li) continue
map[li] = (d.images || []).map((im: any) => ({
id: im.id || "",
url: toAbs(im.image || ""),
thumb: toAbs(im.thumb || im.image || ""),
raw: im.image || "",
name: im.name || "",
}))
}
this.detailImagesByLook = map
this.buildDetailMapFrom(raw ? JSON.parse(raw) : [])
} catch {
this.detailImagesByLook = {}
}

View File

@ -99,7 +99,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</button>
{im.name && (
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">
<figcaption class="text-[11px] uppercase tracking-widest text-[#999] mt-2">
{im.name}
</figcaption>
)}
@ -123,12 +123,12 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
<path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
</svg>
<span class="text-[10px] font-mono uppercase tracking-[0.18em]">{t('login to view')}</span>
<span class="text-[10px] uppercase tracking-[0.18em]">{t('login to view')}</span>
</div>
</div>
{/* 与真实图 figcaption 同高的 caption 行(正常图显示 img01 等小字):
占位显示 +N photos,保证遮罩格子与真实图片格子总高一致 */}
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#cfcfcf] mt-2">+{lockedCount} {t('photos')}</figcaption>
<figcaption class="text-[11px] uppercase tracking-widest text-[#cfcfcf] mt-2">+{lockedCount} {t('photos')}</figcaption>
</figure>
))
}
@ -166,7 +166,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</svg>
</button>
<template x-if="im.name">
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2" x-text="im.name"></figcaption>
<figcaption class="text-[11px] uppercase tracking-widest text-[#999] mt-2" x-text="im.name"></figcaption>
</template>
</figure>
</template>
@ -177,14 +177,14 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</article>
) : (
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
<p class="font-mono text-sm text-[#888] uppercase tracking-widest mb-4">
<p class="text-sm text-[#888] uppercase tracking-widest mb-4">
{error === "missing" ? t('No street snap ID specified') : t('Failed to load street snap. Please refresh or try later.')}
</p>
<a
href={homeHref}
class="text-[12px] font-mono uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
class="text-[12px] uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
>
← {t('Back to Index')}
</a>

View File

@ -37,9 +37,9 @@ 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">
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">{pleaseLabel}</p>
<p class="mb-12 font-serif text-5xl font-black 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 font-mono text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{pleaseLabel}</p>
<p class="mb-12 text-5xl font-black 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>
</div>
<!-- ===== 已登录 ===== -->
@ -48,14 +48,14 @@ const browseLabel = t("browse the archive")
<!-- 报头:衬线大标题会员名 + 档案标签 -->
<header class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8 border-b-2 border-black pb-10 pt-2">
<div class="min-w-0">
<p class="mb-4 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">{t("member profile")}</p>
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{t("member profile")}</p>
<div class="flex flex-wrap items-end gap-x-4">
<h1 class="font-serif text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1>
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1>
<!-- VIP 徽标:仅高级用户(图集详情页可见高清原图)显示 -->
<span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 font-mono 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>
<div class="shrink-0 pb-1 text-right font-mono text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
<div class="shrink-0 pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
<p>Portfolio Studio</p>
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
@ -69,8 +69,8 @@ const browseLabel = t("browse the archive")
<!-- 左:编号目录式垂直导航(移动端折成横向条) -->
<nav class="lg:sticky lg:top-12 lg:self-start" aria-label="Account sections">
<p class="mb-3 hidden font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-400 lg:block">Index</p>
<div class="flex gap-10 overflow-x-auto border-b border-black/15 font-mono text-[12px] uppercase tracking-[0.2em] lg:flex-col lg:gap-0 lg:overflow-visible lg:border-b-0 lg:border-t lg:border-black/10">
<p class="mb-3 hidden text-[10px] uppercase tracking-[0.3em] text-zinc-400 lg:block">Index</p>
<div class="flex gap-10 overflow-x-auto border-b border-black/15 text-[12px] uppercase tracking-[0.2em] lg:flex-col lg:gap-0 lg:overflow-visible lg:border-b-0 lg:border-t lg:border-black/10">
<button type="button" @click="tab='saved'" :class="tab==='saved' ? 'border-black text-black' : 'border-transparent text-black/40 hover:text-black'" class="-mb-px whitespace-nowrap border-b-2 px-0.5 pb-4 pt-1 text-left transition-colors lg:mb-0 lg:w-full lg:border-b-0 lg:border-l-2 lg:px-5 lg:py-4">
<span class="mr-3 text-[10px] opacity-60">01</span>{savedLooksLabel}
</button>
@ -85,20 +85,20 @@ const browseLabel = t("browse the archive")
<div x-show="tab==='saved'" x-cloak>
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
<h2 class="font-serif text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
<p class="pb-1 font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
<p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
</div>
<!-- 粒度切换:图集 / 单图(服务端 kind 过滤分页) -->
<div class="mb-10 flex items-center gap-8 border-b border-black/15 font-mono text-[11px] uppercase tracking-[0.2em]">
<div class="mb-10 flex items-center gap-8 border-b border-black/15 text-[11px] uppercase tracking-[0.2em]">
<button type="button" @click="setFavKind('gallery')" :class="favKind==='gallery' ? 'border-black text-black' : 'border-transparent text-black/45 hover:text-black'" class="-mb-px border-b-2 py-4 transition-colors">{t('galleries')}</button>
<button type="button" @click="setFavKind('image')" :class="favKind==='image' ? 'border-black text-black' : 'border-transparent text-black/45 hover:text-black'" class="-mb-px border-b-2 py-4 transition-colors">{t('images')}</button>
</div>
<!-- 空态 -->
<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 font-serif text-3xl font-black 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 font-mono text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
<p class="mb-10 text-3xl font-black 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>
</div>
<!-- 收藏网格 -->
@ -108,19 +108,19 @@ const browseLabel = t("browse the archive")
<a :href="itemHref(f)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
<img :src="coverUrl(f)" :alt="f.title" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
</a>
<button @click="removeFav(f.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
<button @click="removeFav(f.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
<div class="mt-3 min-w-0">
<a :href="itemHref(f)" target="_blank" rel="noopener" class="block">
<p class="truncate text-[13px] font-medium leading-snug" x-text="f.title"></p>
</a>
<p class="mt-1.5 truncate font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="f.brand || '—'"></p>
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="f.brand || '—'"></p>
</div>
</div>
</template>
</div>
<!-- 收藏分页(服务端分页,与收藏总量解耦) -->
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2 font-mono" x-show="favLastPage > 1" aria-label="Saved pagination">
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2" x-show="favLastPage > 1" aria-label="Saved pagination">
<button type="button" class="border border-black/20 px-4 py-2 text-[11px] uppercase tracking-widest transition-colors hover:bg-black hover:text-white disabled:opacity-30" :disabled="favPage <= 1" @click="goFavPage(favPage - 1)">‹ Prev</button>
<template x-for="(p, i) in favPageList" :key="i">
<span>
@ -136,17 +136,17 @@ const browseLabel = t("browse the archive")
<div x-show="tab==='history'" x-cloak>
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
<h2 class="font-serif text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2>
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2>
<div class="flex items-baseline gap-8 pb-1">
<p class="font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
<button x-show="histTotal > 0" @click="clearAllHistory()" class="font-mono text-[11px] uppercase tracking-[0.2em] text-zinc-400 underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-400"><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-zinc-400 underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
</div>
</div>
<!-- 空态 -->
<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 font-serif text-3xl font-black 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 font-mono text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
<p class="mb-10 text-3xl font-black 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>
</div>
<!-- 历史网格 -->
@ -156,19 +156,19 @@ const browseLabel = t("browse the archive")
<a :href="itemHref(h)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
<img :src="coverUrl(h)" :alt="h.data?.title || ''" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
</a>
<button @click="removeHist(h.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
<button @click="removeHist(h.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
<div class="mt-3 min-w-0">
<a :href="itemHref(h)" target="_blank" rel="noopener" class="block">
<p class="truncate text-[13px] font-medium leading-snug" x-text="h.data?.title || '…'"></p>
</a>
<p class="mt-1.5 truncate font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="h.data?.brand || '—'"></p>
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="h.data?.brand || '—'"></p>
</div>
</div>
</template>
</div>
<!-- 历史分页 -->
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2 font-mono" x-show="histLastPage > 1" aria-label="History pagination">
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2" x-show="histLastPage > 1" aria-label="History pagination">
<button type="button" class="border border-black/20 px-4 py-2 text-[11px] uppercase tracking-widest transition-colors hover:bg-black hover:text-white disabled:opacity-30" :disabled="histPage <= 1" @click="goHistPage(histPage - 1)">‹ Prev</button>
<template x-for="(p, i) in histPageList" :key="i">
<span>
@ -203,7 +203,7 @@ const browseLabel = t("browse the archive")
const PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400">' +
'<rect width="300" height="400" fill="#f1f1f1"/>' +
'<text x="150" y="200" font-family="monospace" font-size="16" letter-spacing="3" fill="#9ca3af" text-anchor="middle">LOOK</text>' +
'<text x="150" y="200" font-family="sans-serif" font-size="16" letter-spacing="3" fill="#9ca3af" text-anchor="middle">LOOK</text>' +
'</svg>'
)

View File

@ -52,12 +52,12 @@ const loginKeywords = locale === 'cn'
<!-- 顶部工具行:返回档案 + 语言切换 -->
<header class="mb-12 flex items-center justify-between gap-6">
<a href={homeUrl} class="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.22em] text-black/50 transition-colors hover:text-black">
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.22em] text-black/50 transition-colors hover:text-black">
<span aria-hidden="true">←</span>{backLabel}
</a>
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false">
<button type="button" @click="open = !open" class="inline-flex items-center gap-2 border border-black/15 px-3 py-2 font-mono text-[11px] uppercase tracking-[0.22em] text-black transition-colors hover:bg-black hover:text-white" aria-label={t('Language')} aria-haspopup="listbox" :aria-expanded="open">
<button type="button" @click="open = !open" class="inline-flex items-center gap-2 border border-black/15 px-3 py-2 text-[11px] uppercase tracking-[0.22em] text-black transition-colors hover:bg-black hover:text-white" aria-label={t('Language')} aria-haspopup="listbox" :aria-expanded="open">
<span>{LOCALE_NAMES[locale].nativeName}</span>
<svg viewBox="0 0 12 12" width="11" height="11" class="transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
<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"/>
@ -66,7 +66,7 @@ const loginKeywords = locale === 'cn'
<ul x-show="open" x-cloak class="absolute right-0 top-full z-20 mt-2 min-w-[170px] border border-black/15 bg-white py-1">
{languages.map((l) => (
<li>
<a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 font-mono text-[11px] uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white', l.code === locale ? 'font-bold text-black' : 'text-black/55']}>{l.label}</a>
<a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 text-[11px] uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white', l.code === locale ? 'font-bold text-black' : 'text-black/55']}>{l.label}</a>
</li>
))}
</ul>
@ -77,10 +77,10 @@ const loginKeywords = locale === 'cn'
<section class="border-b-2 border-black pb-10">
<div class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8">
<div>
<p class="mb-4 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">{membersLabel}</p>
<h1 class="font-serif text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{membersLabel}</p>
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
</div>
<div class="pb-1 text-right font-mono text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
<div class="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
<p>Portfolio Studio</p>
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
@ -94,41 +94,41 @@ const loginKeywords = locale === 'cn'
<div class="grid gap-12 pt-12 md:grid-cols-[minmax(0,1fr)_320px] md:gap-16 md:pt-16">
<div class="w-full max-w-md">
<p class="mb-10 font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-400">{promptLabel}</p>
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-zinc-400">{promptLabel}</p>
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-8" novalidate>
<div>
<label class="mb-2 block font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-500">{accountLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{accountLabel}</label>
<input type="text" x-model="account" autocomplete="username" required autofocus
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" />
</div>
<div>
<label class="mb-2 block font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-500">{passwordLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{passwordLabel}</label>
<input type="password" x-model="password" autocomplete="current-password" required
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" />
</div>
<p x-show="error" x-text="error" x-cloak class="font-mono text-[11px] uppercase tracking-[0.2em] text-red-600"></p>
<p x-show="error" x-text="error" x-cloak class="text-[11px] uppercase tracking-[0.2em] text-red-600"></p>
<button type="submit" :disabled="loading"
class="w-full border border-black bg-black py-4 font-mono text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:cursor-not-allowed disabled:opacity-50">
class="w-full border border-black bg-black py-4 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:cursor-not-allowed disabled:opacity-50">
<span x-show="!loading">{loginLabel}</span>
<span x-show="loading" x-cloak>…</span>
</button>
</form>
<div class="mt-10 border-t border-black/15 pt-5">
<a href={homeUrl} class="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">
<span aria-hidden="true">←</span>{backLabel}
</a>
</div>
</div>
<aside class="border-t border-black/10 pt-8 md:border-l md:border-t-0 md:pl-8 md:pt-0">
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">Archive note</p>
<p class="font-serif text-lg leading-relaxed text-black/80">{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}</p>
<div class="mt-8 border-t border-black/10 pt-5 font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-400">
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">Archive note</p>
<p class="text-lg leading-relaxed text-black/80">{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}</p>
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-zinc-400">
<p class="mb-3">Runway · Street · Archive</p>
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
</div>