跳转至

NEXUS 2030 酸绿深空首屏

一个科幻发布页首屏:近黑墨绿深空底 + 酸性荧光绿唯一强调色,SVG 笔画描边大标题、 行星轨道核心、等宽 HUD 小字,「观影」按钮弹出一段没有视频文件的纯 CSS 伪影片。

要点

  • 用 SVG <text> + stroke + paint-order:stroke 实现大标题按字体笔画描边,fill 给近透明底色防露背景,vector-effect:non-scaling-stroke 保证缩放不变粗
  • 笔画描边必须配保留笔画重叠轮廓的字体(Noto Sans SC 每一笔是独立闭合路径,描边互相穿过);PingFang / 雅黑等轮廓已合并,只描得出整字外轮廓 —— 标题字用 unicode-range 子集字体钉死,不放任回退
  • 标题换字要重拉子集:fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&text=<新标题字> 直接返回只含那几个字形的 woff2(本页 5 个字仅 1.8KB);联网环境直接整族 @import 也行
  • 全页只用一个酸性荧光绿 #c8ff24 作强调色,收进 --acid 变量,按钮 / 轨道光点 / 进度条 / 状态灯全部引用它
  • 底色用近黑墨绿 #070c0b,叠两坨大半径 radial-gradient 光晕,再用 :after 的椭圆径向渐变压出四周暗角
  • 用 canvas 画 200+ 慢速漂移星点,透明度按 sin(t) 脉动,约 1/6 的点换成强调色,只铺画面右 2/3 让开文案
  • 地面用两组 linear-gradient 画网格线,perspective + rotateX 压成透视地板,mask-image 上下渐隐
  • 行星核心用 repeating-radial-gradient 画同心环,外套三条 rotate + scaleY(.3) 压扁的圆环当轨道,各配不同周期的旋转动画
  • 数据小字(坐标 / 节点数 / 实时时钟)全用等宽字体 + 大字距 + 8–12px 字号,营造 HUD 仪表感
  • 中文大标题用超大字号 + 负字距(letter-spacing:-9px)+ 行高 .85 压出海报感,实心一行 + 描边一行对比
  • 「播放影片」不放真视频:模态里用纯 CSS 动画拼扫描线 + 透视网格 + 核心脉冲 + 16s 进度条,播放状态就是一个 .is-playing
  • 模态的 JS 只管开关、锁滚动、焦点管理(打开聚焦关闭钮 / 关闭还原)和走字计时,Esc 与点遮罩可关
  • 入场动画:文案上滑 + blur 渐显,顶栏整条下滑;prefers-reduced-motion 下全部停摆
  • 移动端裁剪而非缩放:直接隐藏 metrics 和导航,核心球挪到下半屏

源码(折叠)

nexus-2030-demo.html(自包含单页,含内联 style + script)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NEXUS 2030 — 让智能获得空间</title>
<!--
  nexus-2030-demo.html —— 「前端风格收集」的 NEXUS 2030 酸绿深空首屏演示真身。

  原稿为 AI 生成的三文件页面(index.html + styles.css + app.js),2026-07-11
  由对话贴入收录。按 wiki 自包含规矩(skills/writing/mkdocs-wiki/index.md 第 9 节)
  合并为单文件,并做三处最小适配:
  ① 删掉 Google Fonts 的 @import(CDN 禁令)—— 字体栈原样保留,中文落到
     本机 Noto Sans SC / PingFang;"IBM Plex Mono" 后补了 monospace 回退,
     保证等宽 HUD 气质在无该字体的机器上不塌。
  ② 标题「获得空间。」5 个字单独 vendor 了一份 1.8KB 的 Noto Sans SC Light
     子集字体(noto-sans-sc-l300-title.woff2,unicode-range 只圈这 5 个码位)。
     必须用它:「按笔画描边」依赖 Noto Sans SC 的字形保留笔画间重叠轮廓
     (每一笔是独立闭合路径,描边互相穿过);PingFang 等字体轮廓已合并,
     描边只剩整字外轮廓,效果即失。
     ★ 复现注意:子集里只有这 5 个字。若把标题换成其他文字,必须重拉子集:
       curl -A "Mozilla/5.0" "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&text=<URL编码后的新标题字>"
       返回的 @font-face 里有子集 woff2 的下载地址,下载后替换本文件旁的
       woff2 并同步改 unicode-range(或干脆删掉 unicode-range 行)。
       联网环境也可直接用原稿的 Google Fonts @import 整族加载,效果相同。
  ③ 其余代码零改动。特别是 .outline-title:大标题描边不是
     -webkit-text-stroke,而是 SVG <text> 的 stroke + paint-order:stroke,
     描边严格贴着字体笔画勾线(fill 给近透明底色防露背景)—— 这是本页
     的招牌技法,原样保留,勿动。

  页面结构三大块:
  - .hero:canvas 星点粒子(漂移 + sin 脉动)+ 透视网格地面 + 文案区
  - .core:repeating-radial-gradient 同心环行星 + 三条 scaleY 压扁的椭圆轨道
  - .video-modal:「观影」伪视频模态 —— 没有视频文件,扫描线 / 透视网格 /
    核心脉冲 / 16s 进度条全是纯 CSS 动画,JS 只管开关、焦点与计时
-->
<style>
/* 标题专用子集字体:只含「获得空间。」5 个字形。笔画描边效果依赖
   Noto Sans SC 未合并的笔画轮廓,不能放任回退到本机字体(见头注释②) */
@font-face{font-family:"Noto Sans SC";font-style:normal;font-weight:300;src:url(noto-sans-sc-l300-title.woff2) format("woff2");unicode-range:U+3002,U+5F97,U+7A7A,U+83B7,U+95F4}
:root{--acid:#c8ff24;--line:rgba(255,255,255,.14);font-family:"Noto Sans SC","PingFang SC",sans-serif;color:#e8ebe6;background:#070c0b;color-scheme:dark}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:#070c0b}a{color:inherit;text-decoration:none}.topbar{height:84px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;padding:0 3.4vw;border-bottom:1px solid var(--line);position:fixed;z-index:20;inset:0 0 auto;background:rgba(5,9,8,.8);backdrop-filter:blur(12px)}
.brand{display:flex;align-items:center;width:max-content;gap:12px}.brand svg{width:35px;fill:none;stroke:#f0f2ee;stroke-width:3}.brand b{font-size:20px;letter-spacing:-1px}.brand sup{align-self:flex-start;margin:26px 0 0 -8px;color:var(--acid);font:600 8px "IBM Plex Mono",monospace}
nav{display:flex;gap:44px;color:#bdc0bd;font-size:14px}nav a,.future{transition:.25s}nav a:hover{color:var(--acid)}.future{justify-self:end;border:1px solid #3a403e;border-radius:40px;height:58px;padding:0 12px 0 22px;display:flex;gap:22px;align-items:center;font-size:14px;font-weight:600}.future i,.enter i{display:grid;place-items:center;background:var(--acid);color:#071005;border-radius:50%;font-style:normal}.future i{width:34px;height:34px;font-size:17px}.future:hover{border-color:var(--acid)}
/* 深空底:近黑墨绿 + 两坨大半径光晕,:after 再压一圈椭圆暗角 */
.hero{height:100vh;min-height:720px;position:relative;overflow:hidden;background:radial-gradient(circle at 75% 43%,rgba(31,58,45,.32),transparent 35%),radial-gradient(circle at 43% 36%,rgba(24,44,38,.22),transparent 42%),#070c0b}.hero:after{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse at center,transparent 40%,rgba(0,0,0,.52) 100%)}#space,.grid{position:absolute;inset:0;width:100%;height:100%}.grid{top:55%;height:52%;opacity:.24;background-image:linear-gradient(rgba(88,107,99,.22) 1px,transparent 1px),linear-gradient(90deg,rgba(88,107,99,.18) 1px,transparent 1px);background-size:180px 140px;transform:perspective(400px) rotateX(59deg) scale(1.35);transform-origin:center top;mask-image:linear-gradient(to bottom,transparent,#000 35%,transparent)}
/* 招牌:SVG <text> 描边标题。stroke 沿字体笔画勾线,paint-order 先描后填,
   fill 是近透明的底色(防止背景透过字腔),vector-effect 保证缩放不变粗 */
.copy{position:absolute;z-index:4;left:8.2%;top:18.5%;width:48vw}.status{font:400 12px "IBM Plex Mono",monospace;letter-spacing:1.7px;color:#afb4b0;margin:0 0 24px 12px}.status i{display:inline-block;width:7px;height:7px;margin-right:18px;border-radius:50%;background:#7e981e;box-shadow:0 0 8px #99bd1b}.copy h1{margin:0;font-size:clamp(78px,9.2vw,148px);font-weight:800;line-height:.85;letter-spacing:-9px;color:#eceee9}.outline-title{display:block;width:min(760px,52vw);height:auto;margin-top:-2px;overflow:visible}.outline-title text{font-family:"Noto Sans SC","PingFang SC",sans-serif;font-size:118px;font-weight:300;letter-spacing:-10px;fill:rgba(7,12,11,.18);stroke:#aeb3af;stroke-width:1.25;paint-order:stroke;vector-effect:non-scaling-stroke}
.intro{display:flex;align-items:center;gap:7vw;margin:25px 0 0 24vw}.intro p{flex:none;margin:0;font-size:16px;font-weight:300;line-height:2;color:#b8bdb9;white-space:nowrap}.enter{width:224px;height:62px;border-radius:34px;background:var(--acid);color:#091008;display:flex;align-items:center;justify-content:space-between;padding:0 10px 0 26px;font-size:13px;font-weight:800;transition:.25s;box-shadow:0 0 28px rgba(177,255,24,.08)}.enter i{width:44px;height:44px;border:1px solid rgba(20,35,5,.25);font-size:20px}.enter:hover{transform:translateY(-3px);box-shadow:0 0 30px rgba(177,255,24,.25)}
/* 行星核心:repeating-radial-gradient 画同心环,三条 rotate+scaleY(.3) 压扁的
   圆环当轨道(不同周期旋转),.flare 一颗高光球 + 双层 box-shadow 当恒星耀斑 */
.core{position:absolute;z-index:3;right:8%;top:30%;width:35vw;aspect-ratio:1}.rings{position:absolute;inset:24%;border-radius:50%;display:grid;place-items:center;background:repeating-radial-gradient(circle,transparent 0 17px,rgba(169,234,39,.44) 18px 19px),radial-gradient(circle at 58% 63%,#d8ff51 0,rgba(183,255,48,.5) 10%,rgba(88,118,21,.36) 32%,rgba(0,0,0,.75) 62%);box-shadow:0 0 55px rgba(150,225,35,.17),inset 0 0 45px rgba(210,255,71,.2)}.rings span{font:500 10px "IBM Plex Mono",monospace;letter-spacing:1px;text-align:center;color:#e7f2da}.orbit{position:absolute;inset:12%;border:1px solid rgba(198,255,56,.23);border-radius:50%;transform:rotate(-18deg) scaleY(.38)}.orbit.o2{inset:2%;border-color:rgba(180,209,172,.15);transform:rotate(8deg) scaleY(.33)}.orbit.o3{inset:-12%;transform:rotate(-4deg) scaleY(.28);opacity:.45}.orbit i{position:absolute;width:7px;height:4px;background:var(--acid);border-radius:50%;left:7%;top:47%;box-shadow:0 0 8px var(--acid)}.o2 i{left:82%;top:17%}.flare{position:absolute;left:56%;top:59%;width:8%;aspect-ratio:1;border-radius:50%;background:#e5ff73;filter:blur(4px);box-shadow:0 0 18px 8px #b9ff25,0 0 70px 30px rgba(184,255,41,.38)}
.film{position:absolute;z-index:5;left:65%;top:62%;display:flex;align-items:center;gap:14px;border:0;background:transparent;color:#d6dad5;text-align:left}.film>i{width:52px;height:52px;border:1px solid #59625e;border-radius:50%;display:grid;place-items:center;font-size:12px;font-style:normal}.film span{font-size:11px;line-height:1.35}.film small{display:block;margin-top:5px;font:9px/1.7 "IBM Plex Mono",monospace;color:#7f8782}.metrics{position:absolute;z-index:4;right:3.2%;bottom:2%;width:160px}.metrics div{border-top:1px solid #45504b;padding:13px 0 19px}.metrics small{display:block;font-size:8px;color:#777f7b;margin-bottom:12px;letter-spacing:2px}.metrics b{font:400 10px/2.2 "IBM Plex Mono",monospace;color:#a6ada8;letter-spacing:.6px}.scroll-mark{position:absolute;z-index:5;left:2.8%;bottom:0;height:54px;border-left:2px solid var(--acid)}
.copy{animation:copyIn 1.15s cubic-bezier(.2,.75,.2,1) both}.topbar{animation:barIn .8s ease both}.rings{animation:corePulse 3.2s ease-in-out infinite}.o1{animation:orbitOne 12s linear infinite}.o2{animation:orbitTwo 17s linear infinite reverse}.o3{animation:orbitThree 22s linear infinite}.flare{animation:flarePulse 2.4s ease-in-out infinite}.status i{animation:statusBlink 1.5s ease-in-out infinite}.film>i{transition:.3s}.film:hover>i{color:#071005;border-color:var(--acid);background:var(--acid);box-shadow:0 0 22px rgba(200,255,36,.35);transform:scale(1.08)}
.video-modal[hidden]{display:none}.video-modal{position:fixed;z-index:100;inset:0;display:grid;place-items:center;padding:5vw;background:rgba(1,5,4,.9);backdrop-filter:blur(16px);animation:modalIn .35s ease both}.video-shell{width:min(1120px,88vw);border:1px solid rgba(200,255,36,.36);background:#050908;box-shadow:0 0 80px rgba(151,220,28,.13)}.video-head{height:48px;padding:0 18px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.13);font:10px "IBM Plex Mono",monospace;letter-spacing:2px;color:#9da69f}.video-head b{color:var(--acid);font-weight:400}.video-shell video{display:block;width:100%;max-height:72vh;background:#000}.video-close{position:absolute;right:3vw;top:3vw;width:50px;height:50px;border:1px solid #4b5651;border-radius:50%;color:#e9eee9;background:transparent;font:300 28px/1 sans-serif;cursor:pointer;transition:.25s}.video-close:hover{color:#071005;background:var(--acid);border-color:var(--acid);transform:rotate(90deg)}body.modal-open{overflow:hidden}
@keyframes copyIn{from{opacity:0;transform:translateY(32px);filter:blur(7px)}to{opacity:1;transform:none;filter:none}}@keyframes barIn{from{opacity:0;transform:translateY(-100%)}to{opacity:1;transform:none}}@keyframes corePulse{0%,100%{transform:scale(.97);filter:brightness(.88)}50%{transform:scale(1.03);filter:brightness(1.18)}}@keyframes orbitOne{to{transform:rotate(342deg) scaleY(.38)}}@keyframes orbitTwo{to{transform:rotate(368deg) scaleY(.33)}}@keyframes orbitThree{to{transform:rotate(356deg) scaleY(.28)}}@keyframes flarePulse{0%,100%{opacity:.72;transform:scale(.72)}50%{opacity:1;transform:scale(1.22)}}@keyframes statusBlink{50%{opacity:.35;box-shadow:0 0 2px #99bd1b}}@keyframes modalIn{from{opacity:0}to{opacity:1}}
@media(max-width:900px){.topbar{grid-template-columns:1fr auto;height:70px}.topbar nav{display:none}.future{height:46px}.hero{min-height:760px}.copy{left:7%;top:16%;width:88vw}.copy h1{font-size:clamp(72px,20vw,120px)}.outline-title{width:88vw}.outline-title text{font-size:112px}.intro{margin:18px 0 0;gap:22px;flex-wrap:wrap}.intro p{font-size:14px}.core{width:72vw;right:-16%;top:48%}.film{left:7%;top:79%}.metrics{display:none}.status{margin-left:2px}.scroll-mark{left:7%}}
@media(max-width:520px){.brand b{font-size:17px}.future span{display:none}.future{padding:5px}.copy{top:17%}.copy h1{letter-spacing:-5px}.outline-title{margin-top:8px}.outline-title text{font-size:96px;letter-spacing:-8px}.intro p{font-size:13px}.enter{width:190px;height:54px}.core{top:53%;width:90vw;right:-28%}}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important}.orbit,.flare{animation:none!important}}
/* 伪视频影片:没有 <video>,扫描线 / 透视网格 / 核心脉冲 / 打字闪烁 / 16s 进度条
   全部纯 CSS 动画拼成,进度条由 .is-playing 类触发 —— 播放状态是一个 class */
.motion-film{position:relative;aspect-ratio:16/9;overflow:hidden;background:radial-gradient(circle at 50% 50%,#193424 0,#07100d 32%,#020504 72%)}.scanlines,.film-grid{position:absolute;inset:0}.scanlines{z-index:8;pointer-events:none;opacity:.16;background:repeating-linear-gradient(0deg,transparent 0 3px,#b7ff4b 4px);animation:scanMove 8s linear infinite}.film-grid{opacity:.23;background-image:linear-gradient(rgba(181,255,66,.25) 1px,transparent 1px),linear-gradient(90deg,rgba(181,255,66,.2) 1px,transparent 1px);background-size:8% 14%;transform:perspective(500px) rotateX(65deg) scale(1.8);transform-origin:center 75%;animation:gridDrift 7s linear infinite}.film-code{position:absolute;left:4%;top:4%;font:10px "IBM Plex Mono",monospace;letter-spacing:2px;color:#a8b7ad;animation:codeBlink .9s steps(2) infinite}.film-core{position:absolute;left:50%;top:48%;width:28%;aspect-ratio:1;transform:translate(-50%,-50%);display:grid;place-items:center;border-radius:50%;background:repeating-radial-gradient(circle,transparent 0 18px,rgba(184,255,43,.35) 19px 20px),radial-gradient(circle,#d9ff69 0,#6ba51b 10%,rgba(22,50,30,.25) 50%,transparent 70%);filter:drop-shadow(0 0 30px rgba(176,255,42,.35));animation:filmCore 4s ease-in-out infinite}.film-core i{position:absolute;inset:-20%;border:1px solid rgba(190,255,70,.35);border-radius:50%;transform:scaleY(.38);animation:filmOrbit 5s linear infinite}.film-core i:nth-child(2){inset:-36%;animation-duration:8s;animation-direction:reverse}.film-core i:nth-child(3){inset:-55%;animation-duration:11s}.film-core b{font:500 9px/1.2 "IBM Plex Mono",monospace;text-align:center;color:#071006}.film-message{position:absolute;left:5%;bottom:11%;display:grid;gap:6px}.film-message small{font:9px "IBM Plex Mono",monospace;letter-spacing:3px;color:var(--acid)}.film-message strong{font-size:clamp(20px,3vw,42px);letter-spacing:.2em}.film-progress{position:absolute;left:5%;right:5%;bottom:5%;height:1px;background:rgba(255,255,255,.25)}.film-progress i{display:block;width:0;height:100%;background:var(--acid);box-shadow:0 0 8px var(--acid)}.video-modal.is-playing .film-progress i{animation:filmProgress 16s linear forwards}@keyframes scanMove{to{background-position:0 120px}}@keyframes gridDrift{to{background-position:0 160px}}@keyframes codeBlink{50%{opacity:.35}}@keyframes filmCore{50%{transform:translate(-50%,-50%) scale(1.13);filter:drop-shadow(0 0 55px rgba(176,255,42,.65))}}@keyframes filmOrbit{to{rotate:360deg}}@keyframes filmProgress{to{width:100%}}
</style>
</head>
<body>
<header class="topbar">
  <a class="brand" href="#" aria-label="Nexus 首页">
    <svg viewBox="0 0 44 28" aria-hidden="true"><path d="M4 24V4l36 20V4"/></svg>
    <b>NEXUS</b><sup>2030</sup>
  </a>
  <nav aria-label="主导航"><a href="#system">系统</a><a href="#city">城市</a><a href="#robot">机器人</a><a href="#vision">愿景</a></nav>
  <a class="future" href="#system"><span>开启未来</span><i></i></a>
</header>

<main id="system" class="hero">
  <canvas id="space" aria-hidden="true"></canvas>
  <div class="grid" aria-hidden="true"></div>
  <div class="copy">
    <p class="status"><i></i>SYSTEM STATUS: ONLINE / 2030</p>
    <h1>让智能</h1>
    <!-- 实心行用 h1,描边行换 SVG <text>:描边沿笔画走,缩放不糊 -->
    <svg class="outline-title" viewBox="0 0 760 140" role="img" aria-label="获得空间。">
      <text x="0" y="112">获得空间。</text>
    </svg>
    <div class="intro">
      <p>我们为下一代文明构建基础设施——<br>让 AI 感知世界,让机器人理解空间,<br>让城市像生命一样进化。</p>
      <a class="enter" href="#city"><span>进入 NEXUS</span><i></i></a>
    </div>
  </div>

  <section class="core" aria-label="Nexus 核心系统">
    <div class="orbit o1"><i></i></div><div class="orbit o2"><i></i></div><div class="orbit o3"></div>
    <div class="rings"><span>NX<br>CORE</span></div>
    <div class="flare"></div>
  </section>

  <button class="film" type="button" aria-label="播放观影短片" aria-haspopup="dialog"><i></i><span>观看影<br><small>00:16<br>MIN</small></span></button>
  <aside class="metrics">
    <div><small>位置</small><b>31.2304° N<br>121.4737° E</b></div>
    <div><small>连接节点</small><b>7,256</b></div>
    <div><small>系统时钟</small><b id="clock">20:30:00</b></div>
  </aside>
  <div class="scroll-mark" aria-hidden="true"></div>
</main>
<div class="video-modal" role="dialog" aria-modal="true" aria-label="Nexus 观影片" hidden>
  <button class="video-close" type="button" aria-label="关闭影片">×</button>
  <div class="video-shell">
    <div class="video-head"><span>NEXUS / VISION SEQUENCE</span><b id="film-time">00:00 / 00:16</b></div>
    <div class="motion-film" aria-label="Nexus 概念动画">
      <div class="scanlines"></div><div class="film-grid"></div>
      <p class="film-code">NX–2030 / SPATIAL INTELLIGENCE</p>
      <div class="film-core"><i></i><i></i><i></i><b>NX<br>CORE</b></div>
      <div class="film-message"><small>INITIATING NEW WORLD</small><strong>感知 · 理解 · 进化</strong></div>
      <div class="film-progress"><i></i></div>
    </div>
  </div>
</div>
<script>
// 星点粒子:数量随视口宽度缩放(上限 230),只铺在画面右 2/3(让开左侧文案),
// 慢速漂移 + 边界回绕,透明度按 sin(t) 脉动,~16% 的点用酸绿色
const canvas=document.querySelector('#space');const ctx=canvas.getContext('2d');let dots=[];
function size(){const d=devicePixelRatio||1;canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0);dots=Array.from({length:Math.min(230,Math.floor(innerWidth/6))},()=>({x:innerWidth*(.34+Math.random()*.62),y:innerHeight*(.15+Math.random()*.72),r:Math.random()*1.25+.2,a:Math.random()*.6+.12,g:Math.random()>.84,vx:(Math.random()-.5)*.025,vy:(Math.random()-.5)*.018}));}
function draw(t=0){ctx.clearRect(0,0,innerWidth,innerHeight);for(const p of dots){p.x+=p.vx;p.y+=p.vy;if(p.x>innerWidth+4)p.x=innerWidth*.34;if(p.x<innerWidth*.33)p.x=innerWidth;if(p.y>innerHeight)p.y=innerHeight*.15;if(p.y<innerHeight*.14)p.y=innerHeight*.88;const pulse=p.a*(.62+.38*Math.sin(t/650+p.x));ctx.fillStyle=p.g?`rgba(186,255,37,${pulse})`:`rgba(190,211,203,${pulse})`;ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);ctx.fill();}requestAnimationFrame(draw)}
addEventListener('resize',size);size();draw();
setInterval(()=>{const d=new Date();document.querySelector('#clock').textContent=[d.getHours(),d.getMinutes(),d.getSeconds()].map(v=>String(v).padStart(2,'0')).join(':')},1000);
// 伪视频模态:视觉动画全在 CSS,JS 只做四件事 —— 开关 .is-playing、锁滚动、
// 焦点管理(打开聚焦关闭钮 / 关闭还原)、setInterval 走字计时;Esc 与点遮罩可关
const filmButton=document.querySelector('.film');const modal=document.querySelector('.video-modal');const closeButton=document.querySelector('.video-close');const filmTime=document.querySelector('#film-time');let filmTimer;
function openFilm(){modal.hidden=false;document.body.classList.add('modal-open');requestAnimationFrame(()=>modal.classList.add('is-playing'));let elapsed=0;filmTime.textContent='00:00 / 00:16';filmTimer=setInterval(()=>{elapsed++;filmTime.textContent=`00:${String(Math.min(elapsed,16)).padStart(2,'0')} / 00:16`;if(elapsed>=16)clearInterval(filmTimer)},1000);closeButton.focus()}
function closeFilm(){clearInterval(filmTimer);modal.classList.remove('is-playing');modal.hidden=true;document.body.classList.remove('modal-open');filmButton.focus()}
filmButton.addEventListener('click',openFilm);closeButton.addEventListener('click',closeFilm);modal.addEventListener('click',event=>{if(event.target===modal)closeFilm()});addEventListener('keydown',event=>{if(event.key==='Escape'&&!modal.hidden)closeFilm()});
</script>
</body>
</html>