feat: close complete numbering and responsive fifth-domain UI

This commit is contained in:
冰朔 2026-08-19 13:16:32 +08:00
commit e8b3082ed5
42 changed files with 3243 additions and 417 deletions

View file

@ -0,0 +1,108 @@
import { useState, type CSSProperties } from 'react'
import './private-channel-surface.css'
export interface PrivateChannelAction {
id: string
title: string
meta: string
badge?: string
onOpen: () => void
}
export interface InstalledChannelModule extends PrivateChannelAction {
number: string
}
interface PrivateChannelSurfaceProps {
ownerName: string
ownerNumber: string
knowledgeCount: number
onBack: () => void
onMarketplace: () => void
nativeActions: PrivateChannelAction[]
installedModules: InstalledChannelModule[]
}
type PrivateChannelView = 'home' | 'native' | 'modules'
interface WaterBayProps {
title: string
meta: string
detail?: string
warm?: boolean
position: number
onOpen: () => void
}
function WaterBay({ title, meta, detail, warm = false, position, onOpen }: WaterBayProps) {
return <button
type="button"
className={`private-water-bay${warm ? ' is-warm' : ''}`}
style={{ '--bay-position': position } as CSSProperties}
onClick={onOpen}
title={detail || meta}
>
<span className="private-bay-light" aria-hidden="true"><i/><i/><i/></span>
<b>{title}</b>
<small>{meta}</small>
</button>
}
export function PrivateChannelSurface({ ownerName, ownerNumber, knowledgeCount, onBack, onMarketplace, nativeActions, installedModules }: PrivateChannelSurfaceProps) {
const [view, setView] = useState<PrivateChannelView>('home')
const leave = () => view === 'home' ? onBack() : setView('home')
const backLabel = view === 'home' ? '永恒湖心' : '心跳核心频道'
return <section className="private-channel-surface" data-private-view={view} aria-label={`${ownerName}的私人频道`}>
<div className="private-channel-sky" aria-hidden="true"><i/><i/><i/><i/><i/><i/><i/><i/></div>
<button className="private-channel-back" type="button" onClick={leave}> 退{backLabel}</button>
<span className="private-channel-number">{ownerNumber}</span>
<header className="private-channel-location">
<h1>{view === 'home' ? '心跳核心频道' : view === 'native' ? '频道原生器官' : '已安装模块'}</h1>
<p>{view === 'home'
? `${ownerName} · ${ownerNumber} · 私人频道全景`
: view === 'native'
? `随频道建立 · ${nativeActions.length} 条原生路径`
: `编号登记 · ${installedModules.length} 个本地成品模块`}</p>
</header>
<div className="private-channel-mist" aria-hidden="true"/>
{view === 'home' && <div className="private-bay-field private-bay-field-home" aria-label="私人频道路径">
<WaterBay title="频道原生器官" meta={`${nativeActions.length} 条原生路径`} detail="频道全景、知识空间、时间主控与湖面天气" position={0} onOpen={() => setView('native')}/>
<WaterBay title="已安装模块" meta={`${installedModules.length} 个编号模块`} detail="已部署在当前私人频道中的成品模块" warm position={1} onOpen={() => setView('modules')}/>
<WaterBay title="分域模块商城" meta="公共资源 · 主动前往" detail="前往分域选择成品模块与人格体大脑技能包" position={2} onOpen={onMarketplace}/>
</div>}
{view === 'native' && <div className="private-bay-field private-bay-field-items" aria-label="频道原生器官">
{nativeActions.map((action, index) => <WaterBay
key={action.id}
title={action.title}
meta={action.badge || '原生'}
detail={action.meta}
warm={index === 0}
position={index}
onOpen={action.onOpen}
/>)}
</div>}
{view === 'modules' && <div className="private-bay-field private-bay-field-items" aria-label="已安装模块">
{installedModules.length ? installedModules.map((module, index) => <WaterBay
key={module.id}
title={module.title}
meta={module.badge || '已安装'}
detail={`${module.meta} · ${module.number}`}
warm={index === 0}
position={index}
onOpen={module.onOpen}
/>) : <p className="private-module-empty"></p>}
<button className="private-market-link" type="button" onClick={onMarketplace}> </button>
</div>}
<footer className="private-channel-boundary">
<b>{view === 'home' ? `${knowledgeCount} 个知识坐标 · 边界已校验` : '水湾即路径 · 点击继续展开'}</b>
{view === 'home' && <span>访 {ownerNumber} </span>}
</footer>
</section>
}

View file

@ -0,0 +1,89 @@
/* 2026-08-16 official lightweight inner-screen source: water bays are paths. */
.private-channel-surface {
position:absolute;
inset:0;
overflow:hidden;
color:var(--content-primary);
container-type:inline-size;
background:
radial-gradient(ellipse 62% 38% at 50% -10%,color-mix(in srgb,var(--primitive-cool-glow) 12%,transparent),transparent 72%),
linear-gradient(180deg,transparent 0 36%,color-mix(in srgb,var(--surface-depth) 34%,transparent) 100%);
}
.private-channel-sky { position:absolute; inset:0; overflow:hidden; pointer-events:none; }
.private-channel-sky::before { content:''; position:absolute; left:8%; right:8%; top:54%; height:34%; filter:blur(24px); opacity:.55;
background:radial-gradient(ellipse at 50% 0,color-mix(in srgb,var(--primitive-cool-glow) 13%,transparent),transparent 68%); }
.private-channel-sky > i { position:absolute; width:3px; height:3px; border-radius:50%; background:var(--accent-light); opacity:.34; box-shadow:0 0 7px color-mix(in srgb,var(--accent-light) 60%,transparent); animation:private-star 5.8s ease-in-out infinite; }
.private-channel-sky > i:nth-child(1){left:9%;top:18%}.private-channel-sky > i:nth-child(2){left:23%;top:68%;animation-delay:-2s}.private-channel-sky > i:nth-child(3){left:38%;top:25%;animation-delay:-4s}.private-channel-sky > i:nth-child(4){left:54%;top:74%;animation-delay:-1s}.private-channel-sky > i:nth-child(5){left:67%;top:19%;animation-delay:-3s}.private-channel-sky > i:nth-child(6){left:82%;top:62%;animation-delay:-5s}.private-channel-sky > i:nth-child(7){left:93%;top:31%;animation-delay:-2.8s}.private-channel-sky > i:nth-child(8){left:14%;top:82%;animation-delay:-4.7s}
@keyframes private-star { 0%,100%{opacity:.12;transform:scale(.72)} 50%{opacity:.68;transform:scale(1.12)} }
.private-channel-back { position:absolute; z-index:8; top:18px; left:clamp(18px,3vw,44px); padding:0; border:0; background:transparent; color:var(--content-secondary); cursor:pointer; font:650 12px/1.4 inherit; letter-spacing:.06em; transition:color .24s ease; }
.private-channel-back:hover { color:var(--accent-light); }
.private-channel-number { position:absolute; z-index:8; top:18px; right:clamp(18px,3vw,44px); color:var(--accent-light); font:700 10px/1.4 inherit; letter-spacing:.12em; }
.private-channel-location { position:absolute; z-index:6; top:66px; left:16px; right:16px; display:flex; flex-direction:column; align-items:center; text-align:center; gap:9px; }
.private-channel-location h1 { margin:0; color:var(--content-primary); font-size:clamp(28px,3.1vw,42px); font-weight:700; letter-spacing:.1em; }
.private-channel-location p { margin:0; color:var(--content-secondary); font-size:12px; font-weight:600; letter-spacing:.18em; }
.private-channel-mist { position:absolute; z-index:1; left:7%; right:7%; top:45%; height:72px; filter:blur(13px); pointer-events:none;
background:radial-gradient(ellipse at center,color-mix(in srgb,var(--primitive-cool-glow) 15%,transparent),color-mix(in srgb,var(--primitive-warm-glow) 5%,transparent) 52%,transparent 78%); }
.private-bay-field { position:absolute; z-index:5; inset:0; }
.private-water-bay { --bay-width:210px; --bay-height:66px; position:absolute; left:50%; top:50%; width:var(--bay-width); height:118px; padding:0; border:0; background:transparent; color:var(--content-primary); cursor:pointer; text-align:center; transform:translate(-50%,-50%); animation:private-bay-rise .7s cubic-bezier(.22,1,.36,1) both; animation-delay:calc(var(--bay-position) * 70ms); }
.private-bay-light { position:absolute; left:50%; top:0; width:var(--bay-width); height:var(--bay-height); transform:translateX(-50%); animation:private-bay-float 6.6s ease-in-out calc(var(--bay-position) * -1.2s) infinite; }
.private-bay-light > i { position:absolute; left:50%; top:50%; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; }
.private-bay-light > i:first-child { width:100%; height:100%; filter:blur(2px); opacity:.88; background:radial-gradient(closest-side,color-mix(in srgb,var(--primitive-cool-glow) 31%,transparent),color-mix(in srgb,var(--primitive-cool-glow) 10%,transparent) 48%,transparent 74%); }
.private-bay-light > i:nth-child(2) { width:20px; height:20px; background:radial-gradient(circle at 40% 34%,#fff,color-mix(in srgb,var(--accent-light) 74%,var(--primitive-cool-glow)) 48%,transparent 80%); box-shadow:0 0 18px color-mix(in srgb,var(--primitive-cool-glow) 45%,transparent); }
.private-bay-light > i:last-child { width:38%; height:38%; border:1px solid color-mix(in srgb,var(--primitive-cool-glow) 25%,transparent); animation:private-ripple 5.5s ease-out calc(var(--bay-position) * -1s) infinite; }
.private-water-bay.is-warm .private-bay-light > i:first-child { background:radial-gradient(closest-side,color-mix(in srgb,var(--primitive-warm-glow) 37%,transparent),color-mix(in srgb,var(--primitive-warm-glow) 12%,transparent) 48%,transparent 74%); }
.private-water-bay.is-warm .private-bay-light > i:nth-child(2) { background:radial-gradient(circle at 40% 32%,#fff,var(--accent-light) 40%,transparent 80%); box-shadow:0 0 25px color-mix(in srgb,var(--primitive-warm-glow) 65%,transparent),0 0 62px color-mix(in srgb,var(--primitive-warm-glow) 24%,transparent); }
.private-water-bay.is-warm .private-bay-light > i:last-child { border-color:color-mix(in srgb,var(--accent-light) 35%,transparent); }
.private-water-bay > b { position:absolute; top:76px; left:50%; transform:translateX(-50%); white-space:nowrap; color:var(--content-primary); font-size:17px; font-weight:700; letter-spacing:.06em; transition:color .24s ease; }
.private-water-bay > small { position:absolute; top:104px; left:50%; transform:translateX(-50%); white-space:nowrap; color:var(--content-secondary); font-size:11px; font-weight:600; letter-spacing:.09em; }
.private-water-bay.is-warm > b,.private-water-bay:hover > b { color:var(--accent-light); }
.private-water-bay:hover .private-bay-light > i:nth-child(2) { box-shadow:0 0 26px var(--accent-light),0 0 70px color-mix(in srgb,var(--primitive-warm-glow) 32%,transparent); }
@keyframes private-bay-float { 0%,100%{transform:translateX(-50%) translateY(0)} 50%{transform:translateX(-50%) translateY(-7px)} }
@keyframes private-ripple { 0%{width:34%;height:34%;opacity:.52} 100%{width:120%;height:120%;opacity:0} }
@keyframes private-bay-rise { from{opacity:0;translate:0 12px} to{opacity:1;translate:0 0} }
.private-bay-field-home .private-water-bay:nth-child(1) { left:25%; top:51%; }
.private-bay-field-home .private-water-bay:nth-child(2) { left:50%; top:58%; }
.private-bay-field-home .private-water-bay:nth-child(3) { left:75%; top:51%; }
.private-bay-field-items { inset:150px clamp(22px,5vw,82px) 92px; display:grid; grid-template-columns:repeat(3,minmax(180px,1fr)); place-items:center; align-content:center; gap:52px clamp(18px,4vw,72px); }
.private-bay-field-items .private-water-bay { position:relative; left:auto; top:auto; transform:none; }
.private-bay-field-items .private-water-bay:nth-last-of-type(1):nth-child(4),
.private-bay-field-items .private-water-bay:nth-child(4):nth-last-of-type(1) { grid-column:2; }
.private-bay-field-items .private-water-bay:nth-child(4):not(:last-child) { grid-column:1 / 2; margin-left:55%; }
.private-bay-field-items .private-water-bay:nth-child(5) { grid-column:3 / 4; margin-right:55%; }
.private-module-empty { grid-column:1/-1; color:var(--content-secondary); font-size:13px; letter-spacing:.1em; }
.private-market-link { position:absolute; right:0; bottom:-44px; padding:0; border:0; background:transparent; color:var(--accent-light); cursor:pointer; font:650 11px/1.4 inherit; letter-spacing:.08em; }
.private-channel-boundary { position:absolute; z-index:7; left:24px; right:24px; bottom:22px; display:flex; flex-direction:column; align-items:center; gap:6px; text-align:center; color:var(--content-muted); font-size:9px; font-weight:600; letter-spacing:.07em; }
.private-channel-boundary b { color:color-mix(in srgb,var(--accent-light) 78%,var(--content-secondary)); font-size:10px; }
@container (max-width:820px) {
.private-bay-field-home .private-water-bay:nth-child(1) { left:22%; top:48%; }
.private-bay-field-home .private-water-bay:nth-child(2) { left:50%; top:64%; }
.private-bay-field-home .private-water-bay:nth-child(3) { left:78%; top:48%; }
.private-water-bay { --bay-width:180px; }
.private-bay-field-items { grid-template-columns:repeat(2,minmax(160px,1fr)); gap:48px 24px; overflow:auto; align-content:start; padding-top:54px; }
.private-bay-field-items .private-water-bay:nth-child(n) { grid-column:auto; margin:0; }
.private-bay-field-items .private-water-bay:last-of-type:nth-child(odd) { grid-column:1/-1; }
}
@container (max-width:560px) {
.private-channel-location { top:58px; }
.private-channel-location h1 { font-size:26px; }
.private-channel-location p { max-width:88%; font-size:10px; letter-spacing:.1em; }
.private-channel-number { display:none; }
.private-bay-field-home { inset:142px 0 76px; display:grid; grid-template-columns:1fr; align-content:center; justify-items:center; gap:24px; overflow:auto; }
.private-bay-field-home .private-water-bay:nth-child(n) { position:relative; left:auto; top:auto; transform:none; }
.private-bay-field-items { inset:130px 12px 72px; grid-template-columns:1fr; padding-top:36px; gap:28px; }
.private-bay-field-items .private-water-bay:last-of-type:nth-child(odd) { grid-column:auto; }
.private-market-link { position:relative; right:auto; bottom:auto; grid-column:1; padding:12px 0 24px; }
.private-channel-boundary span { display:none; }
}
@media (prefers-reduced-motion:reduce) {
.private-channel-sky > i,.private-bay-light,.private-bay-light > i:last-child,.private-water-bay { animation:none; }
}

View file

@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
import './starlake-surface.css'
import { resolveLayoutMetrics } from './layout-profile'
export type DomainId = 'BRANCH_DOMAIN' | 'ZERO_SENSE_DOMAIN' | 'ZERO_DOMAIN' | 'MAIN_DOMAIN' | 'FIFTH_DOMAIN'
export type Phase = 'dawn' | 'day' | 'dusk' | 'night'
@ -19,11 +20,11 @@ const WEEK = ['日', '一', '二', '三', '四', '五', '六']
const PHASE_WORD: Record<Phase, string> = { dawn: '晨', day: '午', dusk: '暮', night: '夜' }
const DOMAINS = [
{ id: 'BRANCH_DOMAIN' as const, name: '分域', stage: 1, stageName: '聚光尘', hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)', y: -34 },
{ id: 'ZERO_SENSE_DOMAIN' as const, name: '零感域', stage: 2, stageName: '凝星核', hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)', y: -56 },
{ id: 'ZERO_DOMAIN' as const, name: '零域', stage: 3, stageName: '初生环', hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)', y: -40 },
{ id: 'MAIN_DOMAIN' as const, name: '主域', stage: 4, stageName: '行星态', hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)', y: -66 },
{ id: 'FIFTH_DOMAIN' as const, name: '第五域', stage: 5, stageName: '主星态', hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)', y: -48 },
{ id: 'BRANCH_DOMAIN' as const, name: '分域', role: '行业模块 · 大脑技能商城', stage: 2, hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)' },
{ id: 'MAIN_DOMAIN' as const, name: '主域', role: '公告与版本通知', stage: 3, hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)' },
{ id: 'FIFTH_DOMAIN' as const, name: '第五域', role: '冰朔私域 · 编号授权', stage: 5, hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)' },
{ id: 'ZERO_DOMAIN' as const, name: '零域', role: '实验 · 热更新 · 系统架构', stage: 3, hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)' },
{ id: 'ZERO_SENSE_DOMAIN' as const, name: '零感域', role: '人类主控团队 · 不公开', stage: 2, hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)' },
]
function seeded(index: number) {
@ -43,6 +44,7 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
const raining = ['RAIN', 'STORM'].includes(weather.toUpperCase())
const dayNumber = Math.floor((now.getTime() - EPOCH_MS) / 86400000) + 1
const dayIndex = ((dayNumber - 1) % DOMAINS.length + DOMAINS.length) % DOMAINS.length
const layout = resolveLayoutMetrics(viewport.width, viewport.height)
const stars = useMemo(() => Array.from({ length: 170 }, (_, index) => ({
id: index,
@ -88,25 +90,14 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
}, [])
const pad = (value: number, length = 2) => String(value).padStart(length, '0')
// Qoder 验收稿按大画布设计;窗口版必须把整组“天体 + 水影 + 标签”一起缩放,
// 不能只挪坐标,否则第五域在常见 768px 高度下会把名称挤出视口。
const naturalScale = Math.min(Math.max(Math.min(viewport.width / 1400, viewport.height / 1000), .64), 1.28)
const spacingScale = Math.max(.64, Math.min(1.28, (viewport.width - 72) / (5 * 180)))
const scale = Math.min(naturalScale, spacingScale)
const safeEdge = Math.max(46, 76 * scale)
const sideCenters = Array.from({ length: 5 }, (_, index) => safeEdge + (viewport.width - safeEdge * 2) * index / 4)
const sourceHour = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600
const isDay = sourceHour >= 6 && sourceHour < 18
const sourceProgress = isDay ? (sourceHour - 6) / 12 : ((sourceHour >= 18 ? sourceHour - 18 : sourceHour + 6) / 12)
const sourceX = 8 + sourceProgress * 84
const sourceY = 46 - Math.sin(sourceProgress * Math.PI) * 26
const sourceStyle = { left: `${sourceX}vw`, top: `${sourceY}vh`, '--src-mid': isDay ? '#ffe9a8' : '#dfe6fa', '--src-g1': isDay ? 'rgba(255,236,180,.7)' : 'rgba(214,226,255,.6)', '--src-g2': isDay ? 'rgba(255,220,140,.26)' : 'rgba(180,200,255,.22)', '--src-g3': isDay ? 'rgba(255,220,140,.12)' : 'rgba(180,200,255,.1)' } as CSSProperties
const glowStyle = { left: `${sourceX}vw`, top: `${sourceY}vh`, '--glow-tint': isDay ? 'rgba(255,236,180,.12)' : 'rgba(190,208,255,.09)' } as CSSProperties
const baseline = Math.min(viewport.height - 142 * scale, Math.max(viewport.height * .61, viewport.height * .665))
const slots = [0, 1, 3, 4]
let sideCursor = 0
return <section ref={rootRef} className={`starlake-scene${raining ? ' rain' : ''}${awake ? ' awake' : ''}`} style={{ '--sl-scale': scale } as CSSProperties} aria-label="光湖语言世界">
const sourceStyle = { left: `${sourceX}%`, top: `${sourceY}%`, '--src-mid': isDay ? '#ffe9a8' : '#dfe6fa', '--src-g1': isDay ? 'rgba(255,236,180,.7)' : 'rgba(214,226,255,.6)', '--src-g2': isDay ? 'rgba(255,220,140,.26)' : 'rgba(180,200,255,.22)', '--src-g3': isDay ? 'rgba(255,220,140,.12)' : 'rgba(180,200,255,.1)' } as CSSProperties
const glowStyle = { left: `${sourceX}%`, top: `${sourceY}%`, '--glow-tint': isDay ? 'rgba(255,236,180,.12)' : 'rgba(190,208,255,.09)' } as CSSProperties
return <section ref={rootRef} className={`starlake-scene${raining ? ' rain' : ''}${awake ? ' awake' : ''}`} data-layout={layout.profile} data-density={layout.density} style={{ '--sl-gutter': `${layout.gutter}px` } as CSSProperties} aria-label="光湖语言世界">
<div className="milky" aria-hidden="true"/>
<div className="stars" aria-hidden="true">{stars.map(star => <i key={star.id} style={{ width: star.size, height: star.size, left: `${star.left}%`, top: `${star.top}%`, '--go': star.opacity, '--gt': `${star.duration}s`, animationDelay: `${star.delay}s`, background: star.warm ? '#ffe9b8' : undefined } as CSSProperties}/>)}</div>
<div className="lake" aria-hidden="true"><div className="sheen"/><div className="orbit o1"/><div className="orbit o2"/><div className="orbit o3"/><div className="lakeStars">{lakeStars.map(star => <i key={star.id} style={{ width: star.size, height: star.size, left: `${star.left}%`, top: `${star.top}%`, '--go': star.opacity, '--gt': `${star.duration}s`, animationDelay: `${star.delay}s`, background: star.warm ? '#ffe9b8' : undefined } as CSSProperties}/>)}</div></div>
@ -115,19 +106,18 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
<div className="rainfall" aria-hidden="true">{rain.map(drop => <i key={drop.id} style={{ left: `${drop.left}%`, animationDuration: `${drop.duration}s`, animationDelay: `${drop.delay}s`, opacity: drop.opacity }}/>)}</div>
<div className="glow" style={glowStyle} aria-hidden="true"/>
<div className="masthead"><small>HoloLake</small><b> · </b><span>GH-AIOS · GUANGHU AI OPERATING SYSTEM</span></div>
<div className="masthead">{!authenticated && <small>HoloLake</small>}<b> · </b><span>GH-AIOS · GUANGHU AI OPERATING SYSTEM</span></div>
<aside className="hud" aria-label="今日光湖历"><div className="h-top"><small></small><b> {dayNumber} </b></div><div className="h-row"><span>{now.getFullYear()}-{pad(now.getMonth() + 1)}-{pad(now.getDate())} · {WEEK[now.getDay()]}</span><em>{PHASE_WORD[livePhase]}</em></div><div className="h-row"><span>{pad(now.getHours())}:{pad(now.getMinutes())}:{pad(now.getSeconds())}.{pad(now.getMilliseconds(), 3)}</span></div><div className="h-wx"> <i>{raining ? '雨' : weather === 'SNOW' ? '雪' : weather === 'FOG' ? '雾' : weather === 'CLOUD' ? '云' : '晴'}</i> · <i>{DOMAINS[dayIndex].name}</i></div><button className="h-link" type="button" onClick={onOpenEra}> · </button></aside>
<button className="source" style={sourceStyle} type="button" aria-label="展开曜冥纪元演化史" onClick={onOpenEra}><span className="src-core"/></button>
<div className="domains">{DOMAINS.map((domain, index) => {
const onDuty = index === dayIndex
const slot = onDuty ? 2 : slots[sideCursor++]
const size = [30, 40, 50, 62, 76][domain.stage - 1] * scale
const size = [30, 40, 50, 62, 76][domain.stage - 1]
const halo = size * 2.6
const ring = size * 2.1
const sat = size * 1.55
const style = { width: 150 * scale, left: sideCenters[slot] - 75 * scale, top: baseline + domain.y * scale, '--d-hi': domain.hi, '--d-lo': domain.lo, '--d-glow': domain.glow, '--d-glow-soft': domain.glowSoft, '--d-glow-r': `${size * .7}px`, '--d-ring-c': domain.glow, '--d-sat-r': `${sat}px` } as CSSProperties
return <button type="button" key={domain.id} className={`domain${onDuty ? ' on-duty' : ''}`} style={style} onClick={() => onDomain(domain.id)}>
const style = { '--d-hi': domain.hi, '--d-lo': domain.lo, '--d-glow': domain.glow, '--d-glow-soft': domain.glowSoft, '--d-glow-r': `${size * .7}px`, '--d-ring-c': domain.glow, '--d-sat-r': `${sat}px` } as CSSProperties
return <button type="button" key={domain.id} className={`domain domain-${domain.id.toLowerCase()}${onDuty ? ' on-duty' : ''}`} style={style} onClick={() => onDomain(domain.id)}>
<span className="d-body" style={{ width: halo, height: halo }}>
{domain.stage >= 2 && <span className="d-halo" style={{ width: halo, height: halo }}/>}
{domain.stage >= 3 && <span className="d-ring" style={{ width: ring, height: ring }}/>}
@ -135,7 +125,7 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
{domain.stage >= 2 && <span className="d-core" style={{ width: size, height: size }}/>}
{domain.stage === 1 && <><span className="d-core" style={{ width: size * .55, height: size * .55, opacity: .7 }}/><span className="d-dust">{Array.from({ length: 6 }, (_, dust) => <i key={dust} style={{ '--dust-a': `${dust * 60}deg`, '--dust-r0': `${26 + dust % 3 * 7}px`, '--dust-t': `${4.6 + dust * .7}s`, animationDelay: `${-dust * .9}s` } as CSSProperties}/>)}</span></>}
</span>
<span className="d-water" style={{ width: size * 3.2, height: size * .85 }}/><span className="d-mirror" style={{ width: size * 2.2, height: size * .5 }}/><span className="d-label">{domain.name}</span><span className="d-stage">{domain.stageName}</span>
<span className="d-water" style={{ width: size * 3.2, height: size * .85 }}/><span className="d-mirror" style={{ width: size * 2.2, height: size * .5 }}/><span className="d-label">{domain.name}</span><span className="d-stage">{domain.role}</span>
</button>
})}</div>
<button className="channel-entry" type="button" onClick={onEnterChannel}>{authenticated ? '返回我的频道' : '编号验证 · 进入我的频道'}</button>

View file

@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react'
import './traditional-surface.css'
import { resolveLayoutMetrics } from './layout-profile'
/* Qoder 锁定验收稿的传统工作台结构。宿主只注入真实投影,不在视觉层制造广播或指标。 */
export type FinishId = 'aurora' | 'nebula' | 'abyss' | 'jade' | 'cinnabar' | 'champagne' | 'porcelain' | 'snow'
@ -59,12 +60,12 @@ export function TraditionalSurface({ awake = false, finish, onFinish, onActivity
if (!element) return
const layout = () => {
const { width, height } = element.getBoundingClientRect()
const metrics = resolveLayoutMetrics(width, height)
const vmin = Math.min(width, height)
element.style.setProperty('--gap', `${Math.round(Math.min(18, Math.max(10, vmin * .016)))}px`)
element.style.setProperty('--pad', `${Math.round(Math.min(26, Math.max(12, vmin * .02)))}px`)
const narrow = width < 880
element.classList.toggle('stack', narrow)
if (!narrow) element.style.setProperty('--cols', `${Math.min(1.32, Math.max(1.08, width / height * .66)).toFixed(2)}fr 1fr`)
element.dataset.layout = metrics.profile
element.dataset.density = metrics.density
}
const observer = new ResizeObserver(layout)
observer.observe(element)
@ -96,7 +97,7 @@ export function TraditionalSurface({ awake = false, finish, onFinish, onActivity
<div className="card glass"><div className="hd"><span className="lbl"> </span><span className="tag" style={{ '--tc': 'var(--good)' } as React.CSSProperties}> {channels.length} </span></div><div className="rows">{channels.map(channel => <div className="row" key={channel.title}><span className="dot" style={{ color: channel.color, background: channel.color }}/><div className="t"><b>{channel.title}</b><span>{channel.meta}</span></div><span className="tag" style={{ '--tc': channel.tone } as React.CSSProperties}>{channel.tag}</span><span className="num">{channel.count}</span></div>)}</div></div>
<div className="card glass"><div className="hd"><span className="lbl"> </span><span className="tag" style={{ '--tc': 'var(--good)' } as React.CSSProperties}></span></div><div className="rows">{systems.map(system => <div className="row" key={system.title}><div className="t"><b>{system.title}</b><span>{system.meta}</span></div><span className="tag" style={system.tone ? { '--tc': system.tone } as React.CSSProperties : undefined}>{system.tag}</span></div>)}</div></div>
</div>
<div className="finisher glass"><small></small>{FINISHES.map(item => <button key={item.id} type="button" className={finish === item.id ? 'on' : ''} onClick={() => pickFinish(item.id)}>{item.name}</button>)}</div>
</div>
<div className="finisher glass"><small></small>{FINISHES.map(item => <button key={item.id} type="button" className={finish === item.id ? 'on' : ''} onClick={() => pickFinish(item.id)}>{item.name}</button>)}</div>
</div>
}

View file

@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { resolveLayoutMetrics } from './layout-profile'
describe('resolveLayoutMetrics', () => {
it.each([
[1920, 1080, 'panoramic'],
[1440, 900, 'wide'],
[1024, 768, 'compact'],
[820, 1180, 'compact'],
[720, 900, 'stacked'],
[1280, 600, 'compact'],
] as const)('maps %d×%d to %s without whole-canvas scaling', (width, height, profile) => {
expect(resolveLayoutMetrics(width, height).profile).toBe(profile)
})
it('compacts a short window before text becomes unreadable', () => {
const metrics = resolveLayoutMetrics(1280, 600)
expect(metrics.density).toBe('compact')
expect(metrics.gutter).toBeGreaterThanOrEqual(16)
})
})

View file

@ -0,0 +1,34 @@
export type LayoutProfile = 'panoramic' | 'wide' | 'compact' | 'stacked'
export interface LayoutMetrics {
width: number
height: number
profile: LayoutProfile
density: 'comfortable' | 'compact'
gutter: number
}
/**
* Responsive layout is selected from the actual component box, not the monitor.
* Width decides how many semantic columns fit; height can compact or stack a
* landscape layout before any readable text is scaled down.
*/
export function resolveLayoutMetrics(width: number, height: number): LayoutMetrics {
const safeWidth = Math.max(320, Number.isFinite(width) ? width : 320)
const safeHeight = Math.max(420, Number.isFinite(height) ? height : 420)
const short = safeHeight < 690
let profile: LayoutProfile
if (safeWidth >= 1500 && safeHeight >= 760) profile = 'panoramic'
else if (safeWidth >= 1120 && safeHeight >= 650) profile = 'wide'
else if (safeWidth >= 760 && safeHeight >= 560) profile = 'compact'
else profile = 'stacked'
return {
width: safeWidth,
height: safeHeight,
profile,
density: short || profile === 'stacked' ? 'compact' : 'comfortable',
gutter: Math.round(Math.min(32, Math.max(16, Math.min(safeWidth, safeHeight) * .026))),
}
}

View file

@ -1,5 +1,5 @@
/* ═══ 星湖套 v1.3 · 语言世界首页视觉层(作用域 .starlake-scene不污染功能层═══ */
.starlake-scene { position:fixed; inset:0; z-index:0; pointer-events:none; overflow:hidden;
.starlake-scene { position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; container-type:size;
font-family:"PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif;
background:linear-gradient(180deg, #04060e 0%, #0a1128 34%, #0e1532 56%, #0b1129 74%, #060a18 100%); }
@ -15,9 +15,9 @@
@keyframes sl-glint { 0%,100%{opacity:var(--go)} 50%{opacity:calc(var(--go) * .2)} }
/* 星湖:天幕向下熔成的镜 */
.starlake-scene .lake { position:absolute; left:0; right:0; top:62vh; bottom:0; overflow:hidden;
.starlake-scene .lake { position:absolute; left:0; right:0; top:58%; bottom:0; overflow:hidden;
background:linear-gradient(180deg, transparent 0%, rgba(9,13,32,.42) 16%, rgba(6,9,24,.78) 44%, rgba(4,6,15,.94) 100%); }
.starlake-scene .lake .sheen { position:absolute; left:0; right:0; top:-4vh; height:9vh;
.starlake-scene .lake .sheen { position:absolute; left:0; right:0; top:-9%; height:22%;
background:linear-gradient(180deg, transparent, rgba(160,178,255,.10) 46%, rgba(255,236,190,.12) 52%, transparent);
filter:blur(2px); }
.starlake-scene .lake .orbit { position:absolute; left:50%; border-radius:50%; border:1px solid rgba(150,172,255,.12); transform:translateX(-50%); }
@ -55,16 +55,16 @@
@keyframes sl-src-breathe { 0%,100%{transform:scale(1)} 50%{transform:scale(1.1)} }
/* 官方门面:主标软著全称 + 副行技术名 */
.starlake-scene .masthead { position:absolute; top:7.2vh; left:0; right:0; text-align:center; }
.starlake-scene .masthead { position:absolute; top:clamp(34px, 7%, 68px); left:var(--sl-gutter); right:var(--sl-gutter); text-align:center; }
.starlake-scene .masthead small { display:block; color:#f7ebc8; font-size:15px; font-weight:700;
letter-spacing:.3em; text-indent:.3em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
.starlake-scene .masthead b { display:block; color:rgba(234,238,248,.96); font-size:clamp(25px, calc(42px * var(--sl-scale)), 54px); font-weight:800;
margin-top:2.6vh; letter-spacing:.12em; text-indent:.12em; text-shadow:0 2px 26px rgba(0,0,0,.7); }
.starlake-scene .masthead b { display:block; color:rgba(234,238,248,.96); font-size:clamp(27px, 3.15cqw, 48px); font-weight:800;
margin-top:clamp(16px, 2.2cqh, 25px); letter-spacing:.1em; text-indent:.1em; text-shadow:0 2px 26px rgba(0,0,0,.7); }
.starlake-scene .masthead span { display:block; margin-top:1.8vh; color:#f7ebc8; font-size:14px;
font-weight:700; letter-spacing:.42em; text-indent:.42em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
/* 右侧轻文字:今日光湖历 */
.starlake-scene .hud { position:absolute; right:32px; top:29vh; z-index:7; text-align:right; }
.starlake-scene .hud { position:absolute; right:var(--sl-gutter); top:clamp(185px, 27%, 270px); z-index:7; text-align:right; }
.starlake-scene .hud .h-top small { color:rgba(196,206,228,.62); font-size:11px; font-weight:650; letter-spacing:.34em; }
.starlake-scene .hud .h-top b { display:block; margin-top:7px; color:#f7ebc8; font-size:24px; font-weight:760;
letter-spacing:.08em; font-variant-numeric:tabular-nums; text-shadow:0 0 18px rgba(247,235,200,.35); }
@ -80,8 +80,12 @@
padding-bottom:4px; transition:color .18s ease, border-color .18s ease; }
.starlake-scene .hud .h-link:hover { color:#f7ebc8; border-color:rgba(247,235,200,.5); }
/* 五域:成长天体五相,悬浮星湖 */
.starlake-scene .domain { position:absolute; z-index:6; width:150px; padding:0; border:0; text-align:center; cursor:pointer; pointer-events:auto; color:inherit; background:transparent; font:inherit; }
/* 五域:语义位置固定,按容器宽高重排。天体可缩,文字和点击区不跟随缩放。 */
.starlake-scene .domains { position:absolute; z-index:6; left:var(--sl-gutter); right:var(--sl-gutter); bottom:clamp(74px, 8%, 98px);
display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); align-items:end; gap:clamp(8px,1.8cqw,28px); }
.starlake-scene .domain { position:relative; z-index:6; min-width:0; width:100%; padding:0 3px; border:0; text-align:center; cursor:pointer; pointer-events:auto; color:inherit; background:transparent; font:inherit; }
.starlake-scene .domain-branch_domain,.starlake-scene .domain-zero_sense_domain { transform:translateY(20px); }
.starlake-scene .domain-fifth_domain { transform:translateY(-18px); }
.starlake-scene .d-body { position:relative; display:block; margin:0 auto; }
.starlake-scene .d-core { position:absolute; left:50%; top:50%; border-radius:50%; transform:translate(-50%,-50%);
background:
@ -116,16 +120,16 @@
0% { transform:rotate(var(--dust-a)) translateX(var(--dust-r0)); opacity:0; }
12% { opacity:.9; } 88% { opacity:.75; }
100% { transform:rotate(calc(var(--dust-a) + 300deg)) translateX(3px); opacity:0; } }
.starlake-scene .d-label { display:block; margin-top:calc(10px * var(--sl-scale)); color:rgba(222,228,242,.88); font-size:clamp(12px, calc(16px * var(--sl-scale)), 20px); font-weight:720;
.starlake-scene .d-label { display:block; margin-top:9px; color:rgba(222,228,242,.88); font-size:clamp(13px, 1.2cqw, 17px); font-weight:720;
letter-spacing:.3em; text-indent:.3em; transition:color .18s ease; text-shadow:0 1px 10px rgba(0,0,0,.8); }
.starlake-scene .domain:hover .d-label { color:rgba(234,238,248,.96); }
.starlake-scene .d-stage { display:block; margin-top:calc(5px * var(--sl-scale)); color:rgba(196,206,228,.62); font-size:clamp(9px, calc(12px * var(--sl-scale)), 15px); letter-spacing:.28em; text-indent:.28em;
opacity:0; transition:opacity .25s ease; text-shadow:0 1px 8px rgba(0,0,0,.8); }
.starlake-scene .domain:hover .d-stage { opacity:1; }
.starlake-scene .d-stage { display:block; min-height:2.6em; margin:5px auto 0; max-width:20em; color:rgba(196,206,228,.62); font-size:clamp(9px, .82cqw, 12px); line-height:1.35; letter-spacing:.08em;
opacity:.86; transition:opacity .25s ease,color .25s ease; text-shadow:0 1px 8px rgba(0,0,0,.8); }
.starlake-scene .domain:hover .d-stage { opacity:1; color:rgba(222,228,242,.9); }
.starlake-scene .domain.on-duty .d-label { color:#f7ebc8; }
.starlake-scene .domain.on-duty .d-stage { opacity:1; color:rgba(247,235,200,.72); }
.starlake-scene .domain.open .d-label { color:#f7ebc8; }
.starlake-scene .channel-entry { position:absolute; left:24px; bottom:25px; z-index:9; pointer-events:auto; cursor:pointer;
.starlake-scene .channel-entry { position:absolute; left:var(--sl-gutter); bottom:22px; z-index:9; pointer-events:auto; cursor:pointer;
padding:9px 15px; border:1px solid rgba(202,216,255,.13); border-radius:999px; color:rgba(222,228,242,.78);
background:rgba(8,12,28,.44); backdrop-filter:blur(16px); font:650 12px/1.2 inherit; letter-spacing:.08em; }
.starlake-scene .channel-entry:hover { color:#f7ebc8; border-color:rgba(247,235,200,.42); }
@ -142,12 +146,37 @@
.starlake-host > .surface-pill,
.trad-surface ~ .surface-pill { top:12px; }
@media (max-width: 900px) {
.starlake-scene .masthead b { font-size:clamp(24px, 5vw, 36px); }
.starlake-scene .masthead span { font-size:10px; }
.starlake-scene .hud { right:18px; top:25vh; transform:scale(.86); transform-origin:top right; }
.starlake-scene .era-foot { display:none; }
}
.starlake-scene[data-layout="compact"] .masthead { top:52px; text-align:left; right:320px; }
.starlake-scene[data-layout="compact"] .masthead small { display:none; }
.starlake-scene[data-layout="compact"] .masthead b { font-size:clamp(22px,3.5cqw,32px); margin-top:0; letter-spacing:.06em; }
.starlake-scene[data-layout="compact"] .masthead span { margin-top:8px; font-size:9px; letter-spacing:.2em; }
.starlake-scene[data-layout="compact"] .hud { top:44px; transform:scale(.82); transform-origin:top right; }
.starlake-scene[data-layout="compact"] .domains { top:37%; bottom:70px; grid-template-columns:repeat(3,minmax(0,1fr)); grid-template-rows:repeat(2,minmax(0,1fr)); align-items:end; gap:4px 16px; }
.starlake-scene[data-layout="compact"] .domain { transform:none; align-self:end; }
.starlake-scene[data-layout="compact"] .domain-zero_domain { grid-column:1 / 2; }
.starlake-scene[data-layout="compact"] .domain-zero_sense_domain { grid-column:3 / 4; }
.starlake-scene[data-layout="compact"] .d-body { transform:scale(.82); transform-origin:bottom center; margin-bottom:-16px; }
.starlake-scene[data-layout="compact"] .d-water,.starlake-scene[data-layout="compact"] .d-mirror { transform:scale(.82); }
.starlake-scene[data-layout="compact"] .d-stage { max-width:15em; }
.starlake-scene[data-layout="compact"] .lake { top:34%; }
.starlake-scene[data-layout="compact"] .era-foot { display:none; }
.starlake-scene[data-layout="stacked"] { overflow:auto; min-height:620px; }
.starlake-scene[data-layout="stacked"] .masthead { top:64px; text-align:left; }
.starlake-scene[data-layout="stacked"] .masthead small { display:none; }
.starlake-scene[data-layout="stacked"] .masthead b { font-size:clamp(21px,5.6cqw,30px); margin-top:0; letter-spacing:.04em; }
.starlake-scene[data-layout="stacked"] .masthead span { margin-top:8px; font-size:8px; letter-spacing:.12em; }
.starlake-scene[data-layout="stacked"] .hud { top:150px; right:var(--sl-gutter); transform:scale(.78); transform-origin:top right; }
.starlake-scene[data-layout="stacked"] .domains { top:285px; bottom:auto; padding-bottom:92px; grid-template-columns:repeat(2,minmax(0,1fr)); align-items:end; gap:24px 8px; }
.starlake-scene[data-layout="stacked"] .domain { transform:none; }
.starlake-scene[data-layout="stacked"] .domain-fifth_domain { grid-column:1 / -1; grid-row:1; }
.starlake-scene[data-layout="stacked"] .domain-branch_domain { grid-row:2; }
.starlake-scene[data-layout="stacked"] .domain-main_domain { grid-row:2; }
.starlake-scene[data-layout="stacked"] .domain-zero_domain { grid-row:3; }
.starlake-scene[data-layout="stacked"] .domain-zero_sense_domain { grid-row:3; }
.starlake-scene[data-layout="stacked"] .d-body { transform:scale(.75); transform-origin:bottom center; margin-bottom:-21px; }
.starlake-scene[data-layout="stacked"] .lake { top:27%; bottom:-360px; }
.starlake-scene[data-layout="stacked"] .source,.starlake-scene[data-layout="stacked"] .era-foot { display:none; }
.starlake-scene .d-core,.starlake-scene .d-ring,.starlake-scene .d-sat-arm,.starlake-scene .d-dust i,
.starlake-scene .d-mirror,.starlake-scene .d-water,.starlake-scene .source .src-core,

View file

@ -8,7 +8,6 @@
color:var(--ink); -webkit-font-smoothing:antialiased;
background:var(--bg-base); transition:background .5s ease;
}
.trad-surface.stack { overflow:auto; }
/* ── 八式氛围令牌 ── */
.trad-surface,
@ -100,7 +99,7 @@
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
/* ── 地基二层:玻璃本体 ── */
.trad-surface .glass { position:relative; border-radius:22px;
.trad-surface .glass { position:relative; border-radius:16px;
background:linear-gradient(165deg, var(--glass-hi), var(--glass-lo) 55%);
-webkit-backdrop-filter:blur(26px) saturate(1.5); backdrop-filter:blur(26px) saturate(1.5);
border:1px solid var(--glass-edge);
@ -133,14 +132,12 @@
.trad-surface .row .num { flex:none; color:var(--ink-sub); font-size:12px; font-weight:650; font-variant-numeric:tabular-nums; }
/* ── 布局:悬浮导航 + 两列 ── */
.trad-surface .stage { position:relative; z-index:2; height:100%; display:flex; flex-direction:column;
padding:var(--pad, 18px) calc(var(--pad, 18px) + 4px); gap:var(--gap, 14px); max-width:1320px; margin:0 auto; }
.trad-surface:not(.stack) .stage { padding-top:max(62px, var(--pad, 18px)); }
.trad-surface.stack .stage { height:auto; min-height:100%; }
.trad-surface .nav { display:flex; align-items:center; gap:20px; padding:11px 20px; border-radius:999px; flex:none; }
.trad-surface .stage { position:relative; z-index:2; height:100%; display:grid; grid-template-rows:auto minmax(0,1fr) auto;
padding:max(62px,var(--pad,18px)) calc(var(--pad,18px) + 4px) var(--pad,18px); gap:var(--gap,14px); max-width:1440px; margin:0 auto; }
.trad-surface .nav { display:flex; align-items:center; gap:18px; padding:10px 16px; border-radius:16px; min-width:0; }
.trad-surface .nav .brand { font-size:15.5px; font-weight:800; letter-spacing:.16em; color:var(--ink); }
.trad-surface .nav .brand small { margin-left:9px; color:var(--ink-faint); font-size:9.5px; font-weight:700; letter-spacing:.22em; }
.trad-surface .nav .tabs { display:flex; gap:3px; margin-left:8px; }
.trad-surface .nav .tabs { display:flex; gap:3px; margin-left:4px; min-width:0; overflow-x:auto; scrollbar-width:none; }
.trad-surface .nav .tabs button { border:0; background:transparent; color:var(--ink-sub); font-size:13px; font-weight:650;
letter-spacing:.05em; padding:7px 15px; border-radius:999px; cursor:pointer;
transition:color .18s ease, background .18s ease; }
@ -157,12 +154,8 @@
transition:color .18s ease, border-color .18s ease; }
.trad-surface .nav .back:hover { color:var(--ink); border-color:color-mix(in srgb, var(--accent) 55%, var(--glass-edge)); }
.trad-surface .grid { flex:1; min-height:0; display:grid; gap:var(--gap, 14px);
grid-template-columns:var(--cols, 1.28fr 1fr); grid-template-rows:1fr 1fr; }
.trad-surface.stack .grid { grid-template-columns:1fr; grid-template-rows:none; }
.trad-surface.stack .hero { grid-row:auto; min-height:500px; }
.trad-surface.stack .card { min-height:250px; }
.trad-surface.stack .nav { flex-wrap:wrap; border-radius:26px; }
.trad-surface .grid { min-height:0; display:grid; gap:var(--gap,14px);
grid-template-columns:minmax(0,1.18fr) minmax(320px,.92fr); grid-template-rows:minmax(0,1fr) minmax(0,1fr); }
.trad-surface .hero { grid-row:span 2; padding:26px 28px; display:flex; flex-direction:column; overflow:hidden; }
.trad-surface .hero .topline { display:flex; justify-content:space-between; align-items:baseline; }
.trad-surface .hero .day { margin-top:14px; font-size:clamp(38px, 5.2vw, 60px); font-weight:850; letter-spacing:.02em; color:var(--ink);
@ -206,7 +199,7 @@
.trad-surface .card .rows { flex:1; min-height:0; overflow:auto; }
/* ── 氛围切换器 ── */
.trad-surface .finisher { position:absolute; right:22px; bottom:18px; z-index:9; display:flex; gap:3px; padding:5px; border-radius:999px; }
.trad-surface .finisher { z-index:9; justify-self:end; display:flex; gap:3px; padding:4px; border-radius:14px; max-width:100%; overflow-x:auto; }
.trad-surface .finisher small { align-self:center; color:var(--ink-faint); font-size:9.5px; font-weight:700;
letter-spacing:.2em; padding:0 6px 0 10px; }
.trad-surface .finisher button { border:0; background:transparent; color:var(--ink-sub); font-size:11.5px; font-weight:700;
@ -217,6 +210,35 @@
.trad-surface .nav .back { display:none; }
.trad-surface[data-finish="porcelain"] .finisher button.on, .trad-surface[data-finish="snow"] .finisher button.on { background:rgba(255,255,255,.7); }
.trad-surface[data-density="compact"] .stage { padding-top:56px; }
.trad-surface[data-density="compact"] .hero { padding:18px 20px; }
.trad-surface[data-density="compact"] .hero .day { margin-top:8px; font-size:clamp(34px,4.5vw,48px); }
.trad-surface[data-density="compact"] .hero .clock { margin-top:12px; }
.trad-surface[data-density="compact"] .broadcast { margin-top:14px; }
.trad-surface[data-density="compact"] .row { padding:8px 10px; }
.trad-surface[data-density="compact"] .card { padding:11px; }
.trad-surface[data-layout="compact"] .nav { gap:10px; }
.trad-surface[data-layout="compact"] .nav .brand small,.trad-surface[data-layout="compact"] .nav .me { display:none; }
.trad-surface[data-layout="compact"] .nav .tabs button { padding-inline:10px; }
.trad-surface[data-layout="compact"] .grid { grid-template-columns:minmax(0,1.12fr) minmax(280px,.88fr); }
.trad-surface[data-layout="compact"] .hero .stats { grid-template-columns:1fr; }
.trad-surface[data-layout="compact"] .hero .stat:last-child { display:none; }
.trad-surface[data-layout="compact"] .finisher small { display:none; }
.trad-surface[data-layout="compact"] .finisher button { padding-inline:9px; }
.trad-surface[data-layout="stacked"] { overflow:auto; }
.trad-surface[data-layout="stacked"] .stage { height:auto; min-height:100%; display:flex; flex-direction:column; padding-top:58px; }
.trad-surface[data-layout="stacked"] .nav { flex-wrap:wrap; }
.trad-surface[data-layout="stacked"] .nav .brand { width:100%; }
.trad-surface[data-layout="stacked"] .nav .brand small { float:right; }
.trad-surface[data-layout="stacked"] .nav .tabs { order:3; width:100%; }
.trad-surface[data-layout="stacked"] .nav .me { display:none; }
.trad-surface[data-layout="stacked"] .grid { grid-template-columns:1fr; grid-template-rows:none; }
.trad-surface[data-layout="stacked"] .hero { grid-row:auto; min-height:520px; }
.trad-surface[data-layout="stacked"] .card { min-height:260px; }
.trad-surface[data-layout="stacked"] .finisher { align-self:stretch; justify-self:auto; justify-content:flex-start; }
/* ── 语言世界侧入口按钮world-footer 内) ── */
.world-footer .surface-switch { margin-left:auto; border:1px solid currentColor; background:transparent;
color:inherit; font-size:11px; font-weight:700; letter-spacing:.14em; padding:5px 14px; border-radius:999px;
@ -332,21 +354,14 @@
backdrop-filter:none;
}
.global-finish-rail {
position:fixed; z-index:121; right:18px; bottom:13px; display:flex; flex-wrap:wrap; max-width:calc(100vw - 36px);
gap:3px; padding:5px; border:1px solid var(--glass-edge); border-radius:999px;
background:color-mix(in srgb,var(--bg-base) 72%,transparent); box-shadow:inset 0 1px 0 var(--glass-inner),0 10px 30px rgba(0,0,0,.25);
-webkit-backdrop-filter:blur(18px); backdrop-filter:blur(18px);
}
.global-finish-rail button { border:0; border-radius:999px; padding:5px 10px; color:var(--ink-sub); background:transparent;
.global-finish-menu { position:relative; z-index:3; color:var(--ink-sub); font:700 10px/1.2 "PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif; }
.global-finish-menu summary { list-style:none; cursor:pointer; padding:5px 9px; border:1px solid var(--glass-edge); border-radius:9px; background:var(--glass-lo); }
.global-finish-menu summary::-webkit-details-marker { display:none; }
.global-finish-menu > div { position:absolute; right:0; top:calc(100% + 7px); display:grid; grid-template-columns:repeat(2,1fr); width:178px; gap:3px; padding:6px; border:1px solid var(--glass-edge); border-radius:12px; background:color-mix(in srgb,var(--bg-base) 92%,transparent); box-shadow:0 12px 30px rgba(0,0,0,.34); }
.global-finish-menu button { border:0; border-radius:8px; padding:6px 8px; color:var(--ink-sub); background:transparent;
cursor:pointer; font:700 10.5px/1.2 "PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif; letter-spacing:.08em; }
.global-finish-rail button:hover { color:var(--ink); }
.global-finish-rail button.on { color:var(--ink); background:var(--glass-hi); }
.official-world.surface-traditional:not(.motion-awake) .global-finish-rail { -webkit-backdrop-filter:none; backdrop-filter:none; }
@media (max-width:760px) {
.global-finish-rail { right:12px; bottom:9px; border-radius:18px; }
}
.global-finish-menu button:hover { color:var(--ink); }
.global-finish-menu button.on { color:var(--ink); background:var(--glass-hi); }
@media (prefers-reduced-motion: reduce) {
.trad-surface .sky::before { animation:none; }