HH/.agents/skills/huashu-design/demos/c6-expert-review.html
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

895 lines
26 KiB
HTML

<!doctype html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8" />
<title>c6 · 五个维度,给你一份手术单</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #000000;
--ink: #FFFFFF;
--ink-80: rgba(255,255,255,0.82);
--ink-60: rgba(255,255,255,0.58);
--muted: rgba(255,255,255,0.40);
--dim: rgba(255,255,255,0.18);
--hairline: rgba(255,255,255,0.12);
--accent: #D97757;
--accent-deep: #B85D3D;
--cd-bg: #F5F4F0;
--cd-panel: #FFFFFF;
--cd-ink: #1A1918;
--serif-zh: "Noto Serif SC", "Songti SC", serif;
--serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
--sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
}
html, body {
margin: 0; padding: 0;
background: #000;
overflow: hidden;
font-family: var(--sans);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
* { box-sizing: border-box; }
.stage {
position: fixed;
top: 50%; left: 50%;
width: 1920px; height: 1080px;
transform-origin: center center;
background: var(--bg);
overflow: hidden;
}
/* Film grain */
.stage::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
opacity: 0.02;
pointer-events: none;
z-index: 100;
}
/* Chrome */
.mark {
position: absolute;
top: 48px; left: 64px;
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.2em;
color: rgba(255,255,255,1);
opacity: 0.16;
pointer-events: none;
z-index: 50;
}
.mark-right {
position: absolute;
top: 48px; right: 64px;
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.2em;
color: rgba(255,255,255,1);
opacity: 0.16;
pointer-events: none;
z-index: 50;
}
/* Title */
.title-line {
position: absolute;
top: 108px;
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.28em;
color: var(--muted);
text-transform: uppercase;
opacity: 0;
will-change: opacity, transform;
}
/* Main composition: camera wrapper for push-in at Beat 3 */
.camera {
position: absolute;
inset: 0;
transform-origin: 1000px 940px; /* center of Fix first-row */
will-change: transform;
}
/* ============ LEFT: under-review artwork ============ */
.subject {
position: absolute;
left: 150px;
top: 310px;
width: 640px;
height: 460px;
background: #0B0B0B;
border: 1px solid var(--hairline);
border-radius: 8px;
overflow: hidden;
opacity: 0;
will-change: opacity, transform, filter;
transform: translateY(12px);
}
.subject::after {
/* subtle inner vignette */
content: '';
position: absolute;
inset: 0;
box-shadow: inset 0 0 120px rgba(0,0,0,0.6);
pointer-events: none;
}
.subject-label {
position: absolute;
left: 20px;
top: 18px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.25em;
color: var(--muted);
z-index: 3;
}
.subject-dot {
position: absolute;
right: 20px;
top: 18px;
width: 6px;
height: 6px;
background: var(--accent);
border-radius: 50%;
z-index: 3;
box-shadow: 0 0 10px rgba(217,119,87,0.6);
}
/* Subject wireframe: abstract design mockup */
.subject-canvas {
position: absolute;
inset: 50px 36px 36px;
}
.wf-h1 {
width: 62%;
height: 18px;
background: rgba(255,255,255,0.28);
border-radius: 2px;
margin-bottom: 10px;
}
.wf-h2 {
width: 38%;
height: 10px;
background: rgba(255,255,255,0.14);
border-radius: 2px;
margin-bottom: 28px;
}
.wf-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.wf-row .bar {
height: 8px;
background: rgba(255,255,255,0.10);
border-radius: 2px;
}
.wf-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 14px;
margin-top: 28px;
}
.wf-card {
height: 82px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 6px;
position: relative;
}
.wf-card::before {
content: '';
position: absolute;
left: 12px; top: 14px;
width: 40%;
height: 6px;
background: rgba(255,255,255,0.22);
border-radius: 2px;
}
.wf-card::after {
content: '';
position: absolute;
left: 12px; bottom: 16px;
width: 64%;
height: 4px;
background: rgba(255,255,255,0.10);
border-radius: 2px;
}
.wf-card.accent { border-color: rgba(217,119,87,0.55); background: rgba(217,119,87,0.06); }
.wf-card.accent::before { background: var(--accent); }
.wf-foot {
position: absolute;
left: 0; right: 0;
bottom: 0;
height: 44px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 4px;
}
.wf-chip {
height: 22px;
padding: 0 10px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 11px;
flex: 0 0 auto;
width: 68px;
}
.wf-chip.wide { width: 120px; }
/* ============ Light sweep ============ */
.sweep {
position: absolute;
left: 130px;
top: 250px;
width: 680px;
height: 140px;
background: linear-gradient(180deg,
rgba(217,119,87,0) 0%,
rgba(217,119,87,0.12) 20%,
rgba(255,220,200,0.62) 50%,
rgba(217,119,87,0.18) 80%,
rgba(217,119,87,0) 100%);
filter: blur(14px);
opacity: 0;
pointer-events: none;
z-index: 4;
mix-blend-mode: screen;
will-change: opacity, transform;
}
.sweep-line {
position: absolute;
left: 150px;
top: 310px;
width: 640px;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255,220,200,0.2) 10%,
rgba(255,220,200,0.9) 50%,
rgba(255,220,200,0.2) 90%,
transparent 100%);
filter: blur(0.6px);
box-shadow: 0 0 14px rgba(217,119,87,0.8), 0 0 30px rgba(217,119,87,0.3);
opacity: 0;
pointer-events: none;
z-index: 6;
will-change: opacity, transform;
}
/* ============ RIGHT: radar chart ============ */
.radar-wrap {
position: absolute;
right: 280px;
top: 200px;
width: 520px;
height: 520px;
opacity: 0;
will-change: opacity, transform;
}
.radar-wrap svg {
width: 100%;
height: 100%;
overflow: visible;
}
.radar-grid path {
fill: none;
stroke: rgba(255,255,255,0.10);
stroke-width: 1;
}
.radar-spoke {
stroke: rgba(255,255,255,0.08);
stroke-width: 1;
}
.radar-poly {
fill: rgba(217,119,87,0.16);
stroke: var(--accent);
stroke-width: 2;
stroke-linejoin: round;
}
.radar-point {
fill: var(--accent);
stroke: #1A1918;
stroke-width: 2;
}
.radar-label {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.2em;
fill: var(--ink-80);
text-transform: uppercase;
}
.radar-label-zh {
font-family: var(--serif-zh);
font-size: 22px;
font-weight: 300;
fill: var(--ink);
letter-spacing: 0.05em;
}
.radar-score {
font-family: var(--mono);
font-size: 13px;
fill: var(--accent);
letter-spacing: 0.08em;
}
.radar-title {
position: absolute;
right: 280px;
top: 160px;
width: 520px;
text-align: center;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.28em;
color: var(--muted);
text-transform: uppercase;
opacity: 0;
will-change: opacity;
}
.radar-score-total {
position: absolute;
left: 150px;
top: 170px;
width: 640px;
text-align: left;
opacity: 0;
will-change: opacity;
}
.radar-score-total .score-row {
display: flex;
align-items: baseline;
gap: 24px;
}
.radar-score-total .score-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.28em;
color: var(--muted);
text-transform: uppercase;
}
.radar-score-total .score-num {
font-family: var(--serif-en);
font-size: 72px;
font-weight: 300;
color: var(--ink);
letter-spacing: -0.02em;
line-height: 1;
}
.radar-score-total .score-num .accent { color: var(--accent); }
.radar-score-total .score-total {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.28em;
color: var(--muted);
margin-top: 8px;
text-transform: uppercase;
}
/* ============ Single Fix row (Concept Card lean) ============ */
.fix-lane {
position: absolute;
left: 150px;
bottom: 120px;
width: 1620px;
opacity: 0;
will-change: opacity, transform;
}
.fix-head {
display: flex;
align-items: baseline;
gap: 14px;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--hairline);
}
.fix-mark {
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.28em;
color: var(--accent);
text-transform: uppercase;
}
.fix-zh {
font-family: var(--serif-zh);
font-size: 28px;
font-weight: 400;
color: var(--ink);
}
.fix-count {
margin-left: auto;
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
letter-spacing: 0.2em;
}
.fix-row {
position: relative;
font-family: var(--sans);
font-size: 28px;
font-weight: 300;
color: var(--ink);
line-height: 1.45;
padding: 12px 0;
display: flex;
gap: 20px;
align-items: center;
}
.fix-row .idx {
font-family: var(--mono);
font-size: 12px;
color: var(--muted);
letter-spacing: 0.2em;
flex: 0 0 40px;
padding-top: 2px;
}
.fix-row .mono {
font-family: var(--mono);
font-size: 26px;
letter-spacing: 0;
color: var(--accent);
font-weight: 400;
}
.fix-row .arrow {
color: var(--muted);
margin: 0 4px;
}
.fix-severity {
display: inline-block;
padding: 3px 10px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.22em;
color: var(--accent);
border: 1px solid rgba(217,119,87,0.5);
border-radius: 3px;
margin-right: 10px;
vertical-align: 3px;
}
.fix-pulse {
position: absolute;
inset: 4px -12px 4px -12px;
border: 1px solid var(--accent);
border-radius: 4px;
opacity: 0;
pointer-events: none;
will-change: opacity;
box-shadow: 0 0 24px rgba(217,119,87,0.35);
}
/* ============ Brand Reveal (hero-v10 signature) ============ */
.stage-dimmer {
position: absolute;
inset: 0;
background: #000000;
opacity: 0;
z-index: 40;
pointer-events: none;
will-change: opacity;
}
.brand-panel {
position: absolute;
inset: 0;
background: #F5F4F0;
transform: translateY(100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 50;
will-change: transform;
}
.brand-wordmark {
font-family: var(--serif-en);
font-size: 72px;
font-weight: 100;
font-variation-settings: "wght" 100;
letter-spacing: -0.02em;
color: #1A1918;
text-align: center;
line-height: 1;
opacity: 0;
transform: translateY(20px);
will-change: opacity, transform, font-variation-settings, font-weight;
}
.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
.brand-line {
margin-top: 60px;
height: 2px;
width: 0;
background: #D97757;
align-self: center;
will-change: width;
}
</style>
</head>
<body>
<div class="stage" id="stage">
<div class="mark">HUASHU · DESIGN</div>
<div class="mark-right">V2 · 2026</div>
<div class="title-line" id="titleLine">c6 · 专家评审 · 五个维度</div>
<div class="camera" id="camera">
<!-- Subject: design under review -->
<div class="subject" id="subject">
<div class="subject-label">SUBJECT · DRAFT_V3</div>
<div class="subject-dot"></div>
<div class="subject-canvas">
<div class="wf-h1"></div>
<div class="wf-h2"></div>
<div class="wf-row"><div class="bar" style="width:24%"></div><div class="bar" style="width:14%"></div><div class="bar" style="width:20%"></div></div>
<div class="wf-row"><div class="bar" style="width:30%"></div><div class="bar" style="width:10%"></div></div>
<div class="wf-grid">
<div class="wf-card"></div>
<div class="wf-card accent"></div>
<div class="wf-card"></div>
</div>
<div class="wf-foot">
<div class="wf-chip wide"></div>
<div class="wf-chip"></div>
<div class="wf-chip"></div>
</div>
</div>
</div>
<!-- Scanning light -->
<div class="sweep" id="sweep"></div>
<div class="sweep-line" id="sweepLine"></div>
<!-- Radar chart (right) -->
<div class="radar-title" id="radarTitle">五维诊断 · RADAR</div>
<div class="radar-wrap" id="radarWrap">
<svg viewBox="-270 -270 540 540" xmlns="http://www.w3.org/2000/svg">
<!-- Grid rings (5 levels) -->
<g class="radar-grid" id="radarGrid"></g>
<!-- Spokes to 5 axes -->
<g id="radarSpokes"></g>
<!-- Filled polygon -->
<polygon id="radarPoly" class="radar-poly" points="" />
<!-- Points -->
<g id="radarPoints"></g>
<!-- Axis labels -->
<g id="radarLabels"></g>
</svg>
</div>
<div class="radar-score-total" id="radarTotal">
<div class="score-row">
<div class="score-num"><span id="scoreNum">0</span><span class="accent">/50</span></div>
<div>
<div class="score-label">总评 · PASSED</div>
<div class="score-total">五维加权 · 7.4</div>
</div>
</div>
</div>
<!-- Single Fix row: Concept Card lean -->
<div class="fix-lane" id="fixLane">
<div class="fix-head">
<span class="fix-mark">FIX</span>
<span class="fix-zh">修复</span>
<span class="fix-count">01 / 01</span>
</div>
<div class="fix-row">
<span class="idx">01</span>
<span><span class="fix-severity"></span>字距 <span class="mono">0.02em</span><span class="arrow"></span><span class="mono">0.04em</span></span>
<div class="fix-pulse" id="fixPulse"></div>
</div>
</div>
</div>
<!-- Brand Reveal (hero-v10 signature) -->
<div class="stage-dimmer" id="stageDimmer"></div>
<div class="brand-panel" id="brandPanel">
<div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
<div class="brand-line" id="brandLine"></div>
</div>
</div>
<script>
// Auto-scale
function fitStage() {
const stage = document.getElementById('stage');
const sx = window.innerWidth / 1920;
const sy = window.innerHeight / 1080;
const s = Math.min(sx, sy);
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
}
fitStage();
window.addEventListener('resize', fitStage);
// Easings
const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const cubicOut = t => 1 - Math.pow(1 - t, 3);
function lerp(t, a, b, easing) {
if (t <= 0) return a;
if (t >= 1) return b;
const e = easing ? easing(t) : t;
return a + (b - a) * e;
}
function seg(time, start, end) {
if (time <= start) return 0;
if (time >= end) return 1;
return (time - start) / (end - start);
}
// ============ Build radar SVG ============
const RADIUS = 210;
const AXES = [
{ zh: '哲学', en: 'PHILOSOPHY', score: 8 },
{ zh: '层级', en: 'HIERARCHY', score: 6 },
{ zh: '执行', en: 'EXECUTION', score: 8 },
{ zh: '功能', en: 'FUNCTION', score: 7 },
{ zh: '创新', en: 'INNOVATION', score: 8 },
];
const N = AXES.length;
function axisPoint(i, r) {
// Start at top (-90deg), clockwise
const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
return [Math.cos(angle) * r, Math.sin(angle) * r];
}
// Grid rings (polygons at 5 levels)
const gridG = document.getElementById('radarGrid');
for (let level = 1; level <= 5; level++) {
const r = (RADIUS * level) / 5;
const pts = [];
for (let i = 0; i < N; i++) {
const [x, y] = axisPoint(i, r);
pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
}
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
poly.setAttribute('points', pts.join(' '));
poly.setAttribute('fill', 'none');
poly.setAttribute('stroke', level === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)');
poly.setAttribute('stroke-width', '1');
gridG.appendChild(poly);
}
// Spokes
const spokesG = document.getElementById('radarSpokes');
for (let i = 0; i < N; i++) {
const [x, y] = axisPoint(i, RADIUS);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', 0);
line.setAttribute('y1', 0);
line.setAttribute('x2', x.toFixed(2));
line.setAttribute('y2', y.toFixed(2));
line.setAttribute('class', 'radar-spoke');
spokesG.appendChild(line);
}
// Labels (position outside). ZH sits at a base radial distance; EN stacks
// below it with a fixed vertical offset to avoid overlap on the side axes.
const labelsG = document.getElementById('radarLabels');
AXES.forEach((axis, i) => {
const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
const dirX = Math.cos(angle);
const dirY = Math.sin(angle);
// text-anchor based on horizontal direction
let anchor = 'middle';
if (dirX > 0.3) anchor = 'start';
else if (dirX < -0.3) anchor = 'end';
const baseRadial = RADIUS + 36;
const [bx, by] = axisPoint(i, baseRadial);
// ZH label
const zhText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
zhText.setAttribute('x', bx.toFixed(2));
zhText.setAttribute('y', by.toFixed(2));
zhText.setAttribute('text-anchor', anchor);
zhText.setAttribute('dominant-baseline', 'middle');
zhText.setAttribute('class', 'radar-label-zh');
zhText.textContent = axis.zh;
labelsG.appendChild(zhText);
// EN label stacks vertically below ZH (always +22px in y)
const enText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
enText.setAttribute('x', bx.toFixed(2));
enText.setAttribute('y', (by + 22).toFixed(2));
enText.setAttribute('text-anchor', anchor);
enText.setAttribute('dominant-baseline', 'middle');
enText.setAttribute('class', 'radar-label');
enText.textContent = axis.en;
enText.setAttribute('opacity', '0');
enText.setAttribute('data-type', 'en-label');
labelsG.appendChild(enText);
});
// Points (initial: center)
const pointsG = document.getElementById('radarPoints');
const pointEls = AXES.map((axis, i) => {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', 0);
circle.setAttribute('cy', 0);
circle.setAttribute('r', 5);
circle.setAttribute('class', 'radar-point');
circle.setAttribute('opacity', '0');
pointsG.appendChild(circle);
return circle;
});
const radarPoly = document.getElementById('radarPoly');
// ============ Timeline (10s) ============
// Beat 1 (0-2s): title + subject enters
// Beat 2 (2-8s):
// 2.0-3.8: light sweep top → bottom (1.8s)
// 3.2-4.8: radar grid fades in + polygon + points grow from center
// 4.8-5.2: score count up
// 5.0-6.0: Keep col ripple in
// 5.5-6.5: Fix col ripple in
// 6.0-7.0: Quick Wins col ripple in
// 7.0-8.0: hold
// Beat 3 (8-10s): push-in camera to fix[0] + pulse (8-9), brand reveal (8.0-10.0)
const titleLine = document.getElementById('titleLine');
const subject = document.getElementById('subject');
const sweep = document.getElementById('sweep');
const sweepLine = document.getElementById('sweepLine');
const radarTitle = document.getElementById('radarTitle');
const radarWrap = document.getElementById('radarWrap');
const radarTotal = document.getElementById('radarTotal');
const scoreNum = document.getElementById('scoreNum');
const fixLane = document.getElementById('fixLane');
const fixPulse = document.getElementById('fixPulse');
const camera = document.getElementById('camera');
const stageDimmer = document.getElementById('stageDimmer');
const brandPanel = document.getElementById('brandPanel');
const brandMark = document.getElementById('brandMark');
const brandLine = document.getElementById('brandLine');
const DURATION = 10.0;
let startTime = null;
let loop = true;
if (window.__recording === true) loop = false;
function tick(now) {
if (startTime === null) startTime = now;
let t = (now - startTime) / 1000;
if (t >= DURATION) {
if (loop) { startTime = now; t = 0; }
else { t = DURATION; }
}
// Title fade in/out
const titleIn = seg(t, 0.2, 1.2);
const titleOut = seg(t, 7.6, 8.0);
titleLine.style.opacity = Math.min(cubicOut(titleIn), 1 - titleOut);
titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -6, 0, cubicOut)}px)`;
// Subject appears Beat 1
const subjectIn = seg(t, 0.4, 1.8);
subject.style.opacity = expoOut(subjectIn);
subject.style.transform = `translateY(${lerp(subjectIn, 14, 0, expoOut)}px)`;
// Subject dims after sweep completes (during Beat 2 to keep focus right)
const subjectDim = seg(t, 4.4, 5.6);
const dimFactor = lerp(subjectDim, 1.0, 0.38, cubicInOut);
subject.style.filter = `saturate(${lerp(subjectDim, 1.0, 0.5, cubicInOut)}) brightness(${dimFactor})`;
// Light sweep: 2.0-3.8 top to bottom
const sweepProgress = seg(t, 2.0, 3.8);
const sweepOp = (t < 2.0 || t > 4.2) ? 0 :
(t < 2.2 ? seg(t, 2.0, 2.2) :
t < 3.7 ? 1 :
1 - seg(t, 3.7, 4.2));
sweep.style.opacity = sweepOp * 0.95;
sweepLine.style.opacity = sweepOp * 1.0;
// Move from y=250 to y=700 (subject top 310 to bottom 770)
const sweepY = lerp(sweepProgress, -70, 410, cubicInOut);
sweep.style.transform = `translateY(${sweepY}px)`;
sweepLine.style.transform = `translateY(${sweepY + 70}px)`;
// Radar title + wrap appear 3.2
const radarIn = seg(t, 3.2, 4.0);
radarTitle.style.opacity = cubicOut(radarIn);
radarWrap.style.opacity = cubicOut(radarIn);
radarWrap.style.transform = `scale(${lerp(radarIn, 0.92, 1.0, expoOut)})`;
// Radar grid strokes already visible once wrap fades; animate grid via stroke-dasharray trick would be overkill.
// Instead, grow polygon + points from center (3.6-4.8)
const polyGrow = seg(t, 3.6, 4.8);
const polyT = expoOut(polyGrow);
const polyPts = [];
AXES.forEach((axis, i) => {
const targetR = (axis.score / 10) * RADIUS;
const r = targetR * polyT;
const [x, y] = axisPoint(i, r);
polyPts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
const pt = pointEls[i];
pt.setAttribute('cx', x.toFixed(2));
pt.setAttribute('cy', y.toFixed(2));
pt.setAttribute('opacity', polyT.toFixed(2));
});
radarPoly.setAttribute('points', polyPts.join(' '));
// EN labels fade in slightly later
const enLabelIn = seg(t, 4.2, 4.8);
document.querySelectorAll('[data-type="en-label"]').forEach(el => {
el.setAttribute('opacity', cubicOut(enLabelIn).toFixed(2));
});
// Score count up 4.6-5.4, target total = 37
const scoreT = seg(t, 4.6, 5.4);
const total = AXES.reduce((s, a) => s + a.score, 0); // 37
const shown = Math.round(lerp(scoreT, 0, total, cubicOut));
scoreNum.textContent = shown;
radarTotal.style.opacity = cubicOut(seg(t, 4.4, 5.0));
// Fix lane ripple in (5.3-6.1)
const fixRip = seg(t, 5.3, 6.1);
fixLane.style.opacity = expoOut(fixRip);
fixLane.style.transform = `translateY(${lerp(fixRip, 24, 0, expoOut)}px)`;
// Beat 3: Push-in camera to Fix row + pulse (7.4-8.0)
const pushT = seg(t, 7.4, 8.0);
const scale = lerp(pushT, 1.0, 1.18, cubicInOut);
camera.style.transform = `scale(${scale})`;
// Fix pulse border: blink 2 times between 7.6-8.0
const pulseOp = t < 7.6 ? 0 :
t < 8.0 ? (0.4 + 0.6 * Math.abs(Math.sin((t - 7.6) * Math.PI * 2.4))) :
0;
fixPulse.style.opacity = pulseOp;
// ============ Brand Reveal (hero-v10 signature, aligned) ============
// [T-2.0 → T-1.7s] i.e. 8.0-8.3: scene fade to black (0.3s)
const soK = seg(t, 8.0, 8.3);
stageDimmer.style.opacity = cubicOut(soK);
const sceneFade = seg(t, 8.0, 8.3);
camera.style.opacity = 1 - cubicOut(sceneFade);
// [T-1.7 → T-1.3s] i.e. 8.3-8.7: cream panel slides from bottom (0.4s, expoOut)
const panelT = seg(t, 8.3, 8.7);
const panelY = lerp(panelT, 100, 0, expoOut);
brandPanel.style.transform = `translateY(${panelY}%)`;
// [T-1.3 → T-0.7s] i.e. 8.7-9.3: wordmark wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
const markT = seg(t, 8.7, 9.3);
const markE = expoOut(markT);
const wght = 100 + (500 - 100) * markE;
brandMark.style.opacity = markE;
brandMark.style.transform = `translateY(${20 * (1 - markE)}px)`;
brandMark.style.fontWeight = Math.round(wght);
brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
// [T-0.7 → T-0.3s] i.e. 9.3-9.7: orange line width 0→280 (0.4s, cubicOut)
const lineT = seg(t, 9.3, 9.7);
brandLine.style.width = `${lerp(lineT, 0, 280, cubicOut)}px`;
// [T-0.3 → T] hold
if (!window.__ready) window.__ready = true;
if (loop || t < DURATION) requestAnimationFrame(tick);
}
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
.then(() => requestAnimationFrame(tick));
</script>
</body>
</html>