1
0

w1-brand-protocol.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>w1 · 品牌协议 · 五步不能跳</title>
  6. <link rel="preconnect" href="https://fonts.googleapis.com">
  7. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  8. <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">
  9. <style>
  10. :root {
  11. --bg: #000000;
  12. --ink: #FFFFFF;
  13. --ink-80: rgba(255,255,255,0.82);
  14. --ink-60: rgba(255,255,255,0.58);
  15. --muted: rgba(255,255,255,0.40);
  16. --dim: rgba(255,255,255,0.18);
  17. --hairline: rgba(255,255,255,0.12);
  18. --accent: #D97757;
  19. --accent-deep: #B85D3D;
  20. --cd-bg: #F5F4F0;
  21. --cd-panel: #FFFFFF;
  22. --cd-ink: #1A1918;
  23. --serif-zh: "Noto Serif SC", "Songti SC", serif;
  24. --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
  25. --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
  26. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  27. }
  28. html, body {
  29. margin: 0; padding: 0;
  30. background: #000;
  31. overflow: hidden;
  32. font-family: var(--sans);
  33. color: var(--ink);
  34. -webkit-font-smoothing: antialiased;
  35. }
  36. * { box-sizing: border-box; }
  37. .stage {
  38. position: fixed;
  39. top: 50%; left: 50%;
  40. width: 1920px; height: 1080px;
  41. transform-origin: center center;
  42. background: var(--bg);
  43. overflow: hidden;
  44. }
  45. /* Film grain texture (very subtle) */
  46. .stage::before {
  47. content: '';
  48. position: absolute;
  49. inset: 0;
  50. 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>");
  51. opacity: 0.02;
  52. pointer-events: none;
  53. z-index: 100;
  54. }
  55. /* Chrome · watermark */
  56. .mark {
  57. position: absolute;
  58. top: 48px; left: 64px;
  59. font-family: var(--mono);
  60. font-size: 13px;
  61. letter-spacing: 0.2em;
  62. color: rgba(255,255,255,1);
  63. opacity: 0.16;
  64. pointer-events: none;
  65. z-index: 50;
  66. }
  67. .mark-right {
  68. position: absolute;
  69. top: 48px; right: 64px;
  70. font-family: var(--mono);
  71. font-size: 13px;
  72. letter-spacing: 0.2em;
  73. color: rgba(255,255,255,1);
  74. opacity: 0.16;
  75. pointer-events: none;
  76. z-index: 50;
  77. }
  78. /* ====== Title (centered, small, top) ====== */
  79. .title-line {
  80. position: absolute;
  81. top: 128px;
  82. left: 50%;
  83. transform: translateX(-50%);
  84. font-family: var(--mono);
  85. font-size: 14px;
  86. letter-spacing: 0.28em;
  87. color: var(--muted);
  88. text-transform: uppercase;
  89. opacity: 0;
  90. will-change: opacity, transform;
  91. }
  92. /* ====== Chain · 5 cards connected by a line ====== */
  93. .chain {
  94. position: absolute;
  95. top: 50%; left: 50%;
  96. transform: translate(-50%, -50%);
  97. width: 1680px;
  98. height: 360px;
  99. display: flex;
  100. align-items: center;
  101. justify-content: space-between;
  102. padding: 0 80px;
  103. }
  104. /* The connecting line behind the cards */
  105. .chain-line {
  106. position: absolute;
  107. top: 50%;
  108. left: 140px;
  109. right: 140px;
  110. height: 1px;
  111. background: linear-gradient(90deg,
  112. transparent 0%,
  113. rgba(217,119,87,0.0) 2%,
  114. rgba(217,119,87,0.8) 12%,
  115. rgba(217,119,87,0.8) 88%,
  116. rgba(217,119,87,0.0) 98%,
  117. transparent 100%);
  118. transform-origin: left center;
  119. transform: scaleX(0);
  120. will-change: transform;
  121. }
  122. .card {
  123. position: relative;
  124. width: 248px;
  125. height: 320px;
  126. background: rgba(255,255,255,0.02);
  127. border: 1px solid var(--hairline);
  128. border-radius: 14px;
  129. display: flex;
  130. flex-direction: column;
  131. align-items: center;
  132. justify-content: space-between;
  133. padding: 32px 20px 26px;
  134. opacity: 0;
  135. transform: translateY(20px);
  136. will-change: opacity, transform;
  137. backdrop-filter: blur(10px);
  138. }
  139. .card.active {
  140. border-color: rgba(217,119,87,0.6);
  141. box-shadow:
  142. 0 0 0 1px rgba(217,119,87,0.35),
  143. 0 30px 60px -30px rgba(217,119,87,0.35),
  144. 0 10px 24px -10px rgba(0,0,0,0.6);
  145. }
  146. .card-num {
  147. font-family: var(--mono);
  148. font-size: 11px;
  149. letter-spacing: 0.25em;
  150. color: var(--muted);
  151. }
  152. .card.active .card-num {
  153. color: var(--accent);
  154. }
  155. .card-glyph {
  156. width: 88px;
  157. height: 88px;
  158. display: flex;
  159. align-items: center;
  160. justify-content: center;
  161. position: relative;
  162. }
  163. .card-label {
  164. text-align: center;
  165. }
  166. .card-label .zh {
  167. font-family: var(--serif-zh);
  168. font-size: 32px;
  169. font-weight: 300;
  170. color: var(--ink);
  171. letter-spacing: 0.04em;
  172. line-height: 1;
  173. margin-bottom: 10px;
  174. }
  175. .card-label .en {
  176. font-family: var(--mono);
  177. font-size: 11px;
  178. letter-spacing: 0.22em;
  179. color: var(--muted);
  180. text-transform: uppercase;
  181. }
  182. /* Glyph · Step 1 · Ask (question mark inside a circle, drawn minimal) */
  183. .g-ask {
  184. width: 80px; height: 80px;
  185. border: 1px solid var(--ink-60);
  186. border-radius: 50%;
  187. display: flex;
  188. align-items: center;
  189. justify-content: center;
  190. font-family: var(--serif-en);
  191. font-weight: 300;
  192. font-size: 44px;
  193. color: var(--ink-80);
  194. position: relative;
  195. transition: border-color 0.3s, color 0.3s;
  196. }
  197. .card.active .g-ask { border-color: var(--accent); color: var(--accent); }
  198. /* Glyph · Step 2 · Search (magnifier with crosshair) */
  199. .g-search {
  200. width: 80px; height: 80px;
  201. position: relative;
  202. }
  203. .g-search .ring {
  204. position: absolute;
  205. top: 10px; left: 10px;
  206. width: 52px; height: 52px;
  207. border: 1px solid var(--ink-60);
  208. border-radius: 50%;
  209. transition: border-color 0.3s;
  210. }
  211. .g-search .handle {
  212. position: absolute;
  213. bottom: 8px; right: 6px;
  214. width: 22px; height: 1px;
  215. background: var(--ink-60);
  216. transform: rotate(45deg);
  217. transform-origin: right center;
  218. transition: background 0.3s;
  219. }
  220. .g-search .dot {
  221. position: absolute;
  222. top: 26px; left: 26px;
  223. width: 4px; height: 4px;
  224. background: var(--muted);
  225. border-radius: 50%;
  226. opacity: 0;
  227. transition: opacity 0.3s, background 0.3s;
  228. }
  229. .card.active .g-search .ring { border-color: var(--accent); }
  230. .card.active .g-search .handle { background: var(--accent); }
  231. .card.active .g-search .dot { opacity: 1; background: var(--accent); }
  232. /* Glyph · Step 3 · Grab (download arrow into a tray) */
  233. .g-grab {
  234. width: 80px; height: 80px;
  235. position: relative;
  236. }
  237. .g-grab .arrow {
  238. position: absolute;
  239. top: 8px; left: 50%;
  240. transform: translateX(-50%);
  241. width: 1px; height: 36px;
  242. background: var(--ink-60);
  243. transition: background 0.3s;
  244. }
  245. .g-grab .arrow::before {
  246. content: '';
  247. position: absolute;
  248. bottom: -1px; left: 50%;
  249. transform: translateX(-50%) rotate(45deg);
  250. width: 14px; height: 14px;
  251. border-right: 1px solid currentColor;
  252. border-bottom: 1px solid currentColor;
  253. color: var(--ink-60);
  254. transition: color 0.3s;
  255. }
  256. .g-grab .tray {
  257. position: absolute;
  258. bottom: 10px; left: 12px; right: 12px;
  259. height: 20px;
  260. border: 1px solid var(--ink-60);
  261. border-top: none;
  262. border-radius: 0 0 4px 4px;
  263. transition: border-color 0.3s;
  264. }
  265. .card.active .g-grab .arrow { background: var(--accent); }
  266. .card.active .g-grab .arrow::before { color: var(--accent); }
  267. .card.active .g-grab .tray { border-color: var(--accent); }
  268. /* Glyph · Step 4 · Grep (terminal-like code with highlighted match) */
  269. .g-grep {
  270. width: 100px; height: 80px;
  271. font-family: var(--mono);
  272. font-size: 10px;
  273. color: var(--muted);
  274. line-height: 1.5;
  275. display: flex;
  276. flex-direction: column;
  277. justify-content: center;
  278. padding-left: 8px;
  279. position: relative;
  280. }
  281. .g-grep .line { white-space: nowrap; }
  282. .g-grep .hit {
  283. color: var(--accent);
  284. background: rgba(217,119,87,0.12);
  285. padding: 1px 3px;
  286. border-radius: 2px;
  287. }
  288. /* Glyph · Step 5 · Lock (a file with lines) */
  289. .g-lock {
  290. width: 72px; height: 86px;
  291. position: relative;
  292. }
  293. .g-lock .file {
  294. position: absolute;
  295. inset: 0;
  296. border: 1px solid var(--ink-60);
  297. border-radius: 4px;
  298. transition: border-color 0.3s;
  299. }
  300. .g-lock .fold {
  301. position: absolute;
  302. top: -1px; right: -1px;
  303. width: 18px; height: 18px;
  304. background: var(--bg);
  305. border-left: 1px solid var(--ink-60);
  306. border-bottom: 1px solid var(--ink-60);
  307. transition: border-color 0.3s;
  308. }
  309. .g-lock .row {
  310. position: absolute;
  311. left: 10px;
  312. height: 1px;
  313. background: var(--muted);
  314. transition: background 0.3s;
  315. }
  316. .g-lock .row.r1 { top: 22px; width: 40px; }
  317. .g-lock .row.r2 { top: 34px; width: 48px; }
  318. .g-lock .row.r3 { top: 46px; width: 32px; }
  319. .g-lock .row.r4 { top: 58px; width: 44px; }
  320. .g-lock .row.r5 { top: 70px; width: 28px; background: var(--accent); }
  321. .card.active .g-lock .file { border-color: var(--accent); }
  322. .card.active .g-lock .fold { border-color: var(--accent); }
  323. /* ====== Final · brand-spec.md file ====== */
  324. .final-file {
  325. position: absolute;
  326. top: 50%; left: 50%;
  327. transform: translate(-50%, -50%) scale(0.9);
  328. width: 520px;
  329. background: var(--cd-bg);
  330. color: var(--cd-ink);
  331. border-radius: 10px;
  332. padding: 38px 44px 42px;
  333. opacity: 0;
  334. box-shadow:
  335. 0 40px 90px -30px rgba(217,119,87,0.4),
  336. 0 20px 50px -20px rgba(0,0,0,0.6),
  337. 0 0 0 1px rgba(217,119,87,0.3);
  338. will-change: opacity, transform;
  339. }
  340. .final-file .file-name {
  341. font-family: var(--mono);
  342. font-size: 14px;
  343. letter-spacing: 0.08em;
  344. color: var(--accent-deep);
  345. margin-bottom: 20px;
  346. display: flex;
  347. align-items: center;
  348. gap: 10px;
  349. }
  350. .final-file .file-name::before {
  351. content: '';
  352. width: 6px; height: 6px;
  353. background: var(--accent);
  354. border-radius: 50%;
  355. }
  356. .final-file .h1 {
  357. font-family: var(--serif-zh);
  358. font-size: 26px;
  359. font-weight: 400;
  360. margin: 0 0 18px;
  361. letter-spacing: 0.02em;
  362. }
  363. .final-file .kv {
  364. font-family: var(--mono);
  365. font-size: 12px;
  366. line-height: 1.9;
  367. color: rgba(26,25,24,0.65);
  368. }
  369. .final-file .kv .k { color: var(--accent-deep); }
  370. .final-file .kv .swatch {
  371. display: inline-block;
  372. width: 10px; height: 10px;
  373. border-radius: 2px;
  374. vertical-align: middle;
  375. margin-right: 6px;
  376. }
  377. .final-file .caret {
  378. display: inline-block;
  379. width: 7px; height: 14px;
  380. background: var(--accent);
  381. vertical-align: -2px;
  382. margin-left: 2px;
  383. animation: blink 1.1s steps(2) infinite;
  384. }
  385. @keyframes blink { 50% { opacity: 0; } }
  386. /* Brand reveal (final 2 sec, keeps with Motion Spec) */
  387. .brand-sheet {
  388. position: absolute;
  389. inset: 0;
  390. background: var(--cd-bg);
  391. transform: translateY(100%);
  392. will-change: transform;
  393. z-index: 80;
  394. }
  395. .brand-reveal {
  396. position: absolute;
  397. inset: 0;
  398. z-index: 81;
  399. display: flex;
  400. flex-direction: column;
  401. align-items: center;
  402. justify-content: center;
  403. opacity: 0;
  404. will-change: opacity, transform;
  405. }
  406. .brand-reveal .wordmark {
  407. font-family: var(--sans);
  408. font-weight: 100;
  409. font-size: 128px;
  410. letter-spacing: -0.045em;
  411. color: var(--cd-ink);
  412. line-height: 1;
  413. }
  414. .brand-reveal .wordmark .accent { color: var(--accent); }
  415. .brand-reveal .underline {
  416. width: 0;
  417. height: 2px;
  418. background: var(--accent);
  419. margin-top: 36px;
  420. will-change: width;
  421. }
  422. </style>
  423. </head>
  424. <body>
  425. <div class="stage" id="stage">
  426. <div class="mark">HUASHU · DESIGN</div>
  427. <div class="mark-right">V2 · 2026</div>
  428. <div class="title-line" id="titleLine">w1 · 品牌协议</div>
  429. <div class="chain">
  430. <div class="chain-line" id="chainLine"></div>
  431. <div class="card" data-step="1">
  432. <div class="card-num">STEP 01</div>
  433. <div class="card-glyph"><div class="g-ask">?</div></div>
  434. <div class="card-label">
  435. <div class="zh">问</div>
  436. <div class="en">Ask</div>
  437. </div>
  438. </div>
  439. <div class="card" data-step="2">
  440. <div class="card-num">STEP 02</div>
  441. <div class="card-glyph">
  442. <div class="g-search">
  443. <div class="ring"></div>
  444. <div class="handle"></div>
  445. <div class="dot"></div>
  446. </div>
  447. </div>
  448. <div class="card-label">
  449. <div class="zh">搜</div>
  450. <div class="en">Search</div>
  451. </div>
  452. </div>
  453. <div class="card" data-step="3">
  454. <div class="card-num">STEP 03</div>
  455. <div class="card-glyph">
  456. <div class="g-grab">
  457. <div class="arrow"></div>
  458. <div class="tray"></div>
  459. </div>
  460. </div>
  461. <div class="card-label">
  462. <div class="zh">下</div>
  463. <div class="en">Grab</div>
  464. </div>
  465. </div>
  466. <div class="card" data-step="4">
  467. <div class="card-num">STEP 04</div>
  468. <div class="card-glyph">
  469. <div class="g-grep">
  470. <div class="line">#F5F4F0</div>
  471. <div class="line"><span class="hit">#D97757</span></div>
  472. <div class="line">#1A1918</div>
  473. <div class="line">#FFFFFF</div>
  474. </div>
  475. </div>
  476. <div class="card-label">
  477. <div class="zh">grep</div>
  478. <div class="en">Extract</div>
  479. </div>
  480. </div>
  481. <div class="card" data-step="5">
  482. <div class="card-num">STEP 05</div>
  483. <div class="card-glyph">
  484. <div class="g-lock">
  485. <div class="file"></div>
  486. <div class="fold"></div>
  487. <div class="row r1"></div>
  488. <div class="row r2"></div>
  489. <div class="row r3"></div>
  490. <div class="row r4"></div>
  491. <div class="row r5"></div>
  492. </div>
  493. </div>
  494. <div class="card-label">
  495. <div class="zh">定</div>
  496. <div class="en">Lock</div>
  497. </div>
  498. </div>
  499. </div>
  500. <div class="final-file" id="finalFile">
  501. <div class="file-name">brand-spec.md</div>
  502. <div class="h1">资产已固化<span class="caret"></span></div>
  503. <div class="kv">
  504. <div><span class="k">logo</span> · assets/logo.svg</div>
  505. <div><span class="k">hero</span> · product-hero.png</div>
  506. <div><span class="k">accent</span> · <span class="swatch" style="background:#D97757"></span>#D97757</div>
  507. <div><span class="k">bg</span> · <span class="swatch" style="background:#000;border:1px solid rgba(0,0,0,0.15)"></span>#000000</div>
  508. </div>
  509. </div>
  510. <div class="brand-sheet" id="brandSheet"></div>
  511. <div class="brand-reveal" id="brandReveal">
  512. <div class="wordmark">huashu<span class="accent"> · </span>design</div>
  513. <div class="underline" id="brandUnderline"></div>
  514. </div>
  515. </div>
  516. <script>
  517. // ── Auto-scale stage to viewport ─────────────────
  518. function fitStage() {
  519. const stage = document.getElementById('stage');
  520. const sx = window.innerWidth / 1920;
  521. const sy = window.innerHeight / 1080;
  522. const s = Math.min(sx, sy);
  523. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  524. }
  525. fitStage();
  526. window.addEventListener('resize', fitStage);
  527. // ── Easing functions ─────────────────
  528. const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
  529. const expoIn = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
  530. const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
  531. const cubicOut = t => 1 - Math.pow(1 - t, 3);
  532. function lerp(t, a, b, easing) {
  533. if (t <= 0) return a;
  534. if (t >= 1) return b;
  535. const e = easing ? easing(t) : t;
  536. return a + (b - a) * e;
  537. }
  538. function seg(time, start, end) {
  539. if (time <= start) return 0;
  540. if (time >= end) return 1;
  541. return (time - start) / (end - start);
  542. }
  543. // ── Timeline (total 12s) ─────────────────
  544. // Beat 1 (0-2s) · Beat 2 (2-10s) · Beat 3 (10-12s)
  545. //
  546. // Card schedule:
  547. // Card 1 enter 0.8-1.6s, active 1.6-3.0
  548. // Card 2 enter 2.4-3.2s, active 3.2-4.6
  549. // Card 3 enter 4.0-4.8s, active 4.8-6.2
  550. // Card 4 enter 5.6-6.4s, active 6.4-7.8
  551. // Card 5 enter 7.2-8.0s, active 8.0-9.4
  552. // All cards stay visible (frozen after active ends)
  553. //
  554. // Line draws 0.6-8.0s (while cards come in)
  555. // Title fades in 0.2-1.2, fades out 9.6-10.0
  556. // Final file: 8.8-9.8 scale in, hold to 10.0
  557. // Brand reveal: 10.0-12.0
  558. const cards = Array.from(document.querySelectorAll('.card'));
  559. const cardTimings = [
  560. { enter: [0.8, 1.6], active: [1.6, 3.0] },
  561. { enter: [2.4, 3.2], active: [3.2, 4.6] },
  562. { enter: [4.0, 4.8], active: [4.8, 6.2] },
  563. { enter: [5.6, 6.4], active: [6.4, 7.8] },
  564. { enter: [7.2, 8.0], active: [8.0, 9.4] },
  565. ];
  566. const titleLine = document.getElementById('titleLine');
  567. const chainLine = document.getElementById('chainLine');
  568. const finalFile = document.getElementById('finalFile');
  569. const brandSheet = document.getElementById('brandSheet');
  570. const brandReveal = document.getElementById('brandReveal');
  571. const brandUnderline = document.getElementById('brandUnderline');
  572. const DURATION = 12.0;
  573. let startTime = null;
  574. let loop = true;
  575. // Honor recording flag
  576. if (window.__recording === true) loop = false;
  577. function tick(now) {
  578. if (startTime === null) startTime = now;
  579. let t = (now - startTime) / 1000;
  580. if (t >= DURATION) {
  581. if (loop) { startTime = now; t = 0; }
  582. else { t = DURATION; }
  583. }
  584. // Title
  585. const titleIn = seg(t, 0.2, 1.2);
  586. const titleOut = seg(t, 9.6, 10.0);
  587. const titleOpacity = Math.min(cubicOut(titleIn), 1 - titleOut);
  588. titleLine.style.opacity = Math.max(0, titleOpacity);
  589. titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -8, 0, cubicOut)}px)`;
  590. // Chain line — grows left→right as cards arrive
  591. const lineT = seg(t, 0.6, 8.0);
  592. chainLine.style.transform = `scaleX(${cubicInOut(lineT)})`;
  593. // Cards
  594. cards.forEach((card, i) => {
  595. const { enter, active } = cardTimings[i];
  596. const enterT = seg(t, enter[0], enter[1]);
  597. const baseOp = expoOut(enterT);
  598. const ty = lerp(enterT, 20, 0, expoOut);
  599. // Active state during the card's "spotlight" window
  600. const isActive = t >= active[0] && t <= active[1];
  601. card.classList.toggle('active', isActive);
  602. // Cards dim to 25% when final file starts zooming in (8.8-9.6),
  603. // then fade fully when brand reveal takes over (10.0-10.4)
  604. const dimT = seg(t, 8.8, 9.6);
  605. const exitT = seg(t, 10.0, 10.4);
  606. const dimFactor = lerp(dimT, 1.0, 0.22, cubicInOut);
  607. const finalOp = baseOp * dimFactor * (1 - exitT);
  608. if (dimT > 0) card.classList.remove('active');
  609. card.style.opacity = finalOp;
  610. card.style.transform = `translateY(${ty - 10 * exitT}px)`;
  611. });
  612. // Chain line also dims when final file zooms, fades with cards at 10.0-10.4
  613. const chainDim = seg(t, 8.8, 9.6);
  614. const chainExit = seg(t, 10.0, 10.4);
  615. chainLine.style.opacity = lerp(chainDim, 1, 0.22, cubicInOut) * (1 - chainExit);
  616. // Final file: 8.8-9.8 scale+fade in, then 9.8-10.2 scale+settle, hold to ~10.0
  617. const finalInT = seg(t, 8.8, 9.8);
  618. const finalScale = lerp(finalInT, 0.88, 1.0, expoOut);
  619. const finalOp = cubicOut(finalInT);
  620. // fade final file out into brand reveal
  621. const finalOut = seg(t, 10.0, 10.6);
  622. finalFile.style.opacity = finalOp * (1 - finalOut);
  623. finalFile.style.transform = `translate(-50%, -50%) scale(${finalScale * (1 - finalOut * 0.04)})`;
  624. // Brand reveal — sheet slides up from bottom 10.0-10.6, wordmark fades in 10.6-11.4, underline 11.4-11.9
  625. const sheetT = seg(t, 10.0, 10.6);
  626. brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
  627. const wordT = seg(t, 10.6, 11.4);
  628. brandReveal.style.opacity = cubicOut(wordT);
  629. // NOTE: no scale transform on .brand-reveal — it would compound with the
  630. // underline width animation and make the line appear mis-placed. Instead,
  631. // scale the wordmark alone via font-variation-settings-safe approach: none here.
  632. const underT = seg(t, 11.4, 11.9);
  633. brandUnderline.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
  634. // Mark as ready for recorder on first frame
  635. if (!window.__ready) window.__ready = true;
  636. if (loop || t < DURATION) requestAnimationFrame(tick);
  637. }
  638. // Wait for fonts before first paint so Serif glyphs are correct
  639. (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
  640. .then(() => requestAnimationFrame(tick));
  641. </script>
  642. </body>
  643. </html>