-
+
@@ -28,27 +49,114 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) {
function showResult() {
if (!result || timer?.status !== 'completed' || user !== getUser()) return;
const score = result.score;
- field('.dx-result-kicker b').textContent = String(result.dailyTrack || 1).padStart(2, '0');
- field('.dx-result-kicker').title = '当天第 ' + (result.dailyTrack || 1) + ' 道 AC 的题目';
- const color = cfRatingColor(result.difficulty);
- field('.dx-result-track').dataset.ratingTone = color.tone;
- field('.dx-result-track').title = `题目难度配色参考 CF 名称颜色:${color.label}(${color.range})`;
- field('.dx-result-problem').textContent = 'CODEFORCES / '+timer.problem_id;
- field('.dx-result-name').textContent = result.title;
- field('.dx-result-level strong').textContent = result.difficulty ?? '待定';
- field('.dx-result-achievement-value').textContent = score ? score.achievementShown.toFixed(4)+'%' : '已通关';
- field('.dx-result-rank').textContent = score?.rank ?? 'AC';
- field('.dx-result-kind').textContent = {first:'INDEPENDENT',repeat:'REPLAY',assisted:'ASSISTED',unknown:'PRACTICE'}[result.practiceKind];
+ // 这几处用官方精灵拼:CLEAR! / TRACK / 段位 / 大数字,形和配色都是客户端那套
+ const previewParams = new URLSearchParams(location.search);
+ field('.dx-result-clear').replaceChildren(clearWord());
+ // 轨道号只在段位/课题模式里有意义(客户端那边也是跑课程才显示 TRACK n)。
+ // 普通练习模式这里是当天第几道题的固定值,没有意义。
+ // ⚠️ 只藏内容、不删元素 —— 删掉会把 kicker 这一行压塌,上面的组件整体位移。
+ field('.dx-result-track-word').style.visibility = 'hidden';
+ field('.dx-result-kicker b').style.visibility = 'hidden';
+ // ?track=03 时强制显示这一行,方便核对 TRACK 和数字的字体(正常用户看不到)。
+ const trackPreview = previewParams.get('track');
+ if (trackPreview !== null) {
+ field('.dx-result-track-word').replaceChildren(trackLabel());
+ field('.dx-result-track-word').style.visibility = '';
+ field('.dx-result-kicker b').replaceChildren(
+ trackNumber(trackPreview.replace(/\D/g, '').padStart(2, '0').slice(-2)));
+ field('.dx-result-kicker b').style.visibility = '';
+ }
+ // 卡片整块用官方乐曲底板 + 官方等级数字重画(maimai-glyphs.js 的 problemCard)。
+ // 配色还是那一条:难度 → cfRatingColor 的 tone → 底板/滤镜/板面底色。
+ const difficulty = difficultyOf(result.difficulty);
+ const color = cfRatingColor(difficulty);
+ const [toneMain, toneDeep] = cfToneColors(color.tone);
+ const tone = cardTone(color.tone);
+ const card = field('.dx-result-track');
+ card.dataset.ratingTone = color.tone;
+ card.title = `题目难度配色参考 CF 名称颜色:${color.label}(${color.range})`;
+ card.style.setProperty('--mm-plate', `url('/assets/mm-result/${tone.plate}')`);
+ card.style.setProperty('--mm-field', tone.field);
+ card.style.setProperty('--mm-plate-filter', tone.filter);
+ card.style.setProperty('--track-main', toneMain);
+ card.style.setProperty('--track-deep', toneDeep);
+ // 题面来源的平台**不传**:服务端今天只发 Codeforces,result 和 timer 上都没有 platform
+ // 字段(见 src/dx/timer.ts 的 timerResult —— 平台在 SQL 里就是写死的 'codeforces'),
+ // 所以这里不能凭空写 result.platform。不传 → problemCard → platformLabel() 落回
+ // problem-source.js 的默认值,卡片照旧印 CODEFORCES;哪天服务端把平台发下来了,
+ // 在这里补一个 platform: result.platform 就切换完了。
+ card.replaceChildren(problemCard({
+ problemId: timer.problem_id,
+ kind: PRACTICE_KIND_LABEL[result.practiceKind] ?? result.practiceKind ?? '',
+ title: result.title,
+ difficulty,
+ tone: color.tone,
+ }));
+ const achievementText = score ? score.achievementShown.toFixed(4) : null;
+ // 显示哪个数字:调试用的 ?achievement= 优先,否则用本次成绩
+ // 两条路都补齐到四位小数 —— 结算单上的达成率永远是 4 位(100.0000%)
+ const achievementOverride = previewParams.get('achievement');
+ const shownAchievement = achievementOverride && /^\d+(\.\d+)?$/.test(achievementOverride)
+ ? Number(achievementOverride).toFixed(4) : achievementText;
+ // 达成率 < 80% 不算 clear(客户端也是这个门槛),CLEAR! 就不该出现。
+ // 这里必须放在 shownAchievement 算出来之后 —— 放在前面会踩 TDZ,
+ // 直接抛 ReferenceError 让整个 showResult() 中断(什么精灵都画不出来)。
+ // shownAchievement 为 null 表示这条没有成绩数字、按已通关处理,仍然显示。
+ const isCleared = !shownAchievement || Number(shownAchievement) >= 80;
+ // 用 visibility 而不是 hidden:达成率 <80 时只是把文字藏掉,
+ // 这一块的高度必须留着 —— 否则底下的组件会整体往上顶。
+ field('.dx-result-clear').style.visibility = isCleared ? '' : 'hidden';
+ // 配色由达成率决定(<80 蓝、>=97 金、其余红);调试时可以用 ?achvColor= 覆盖来看别的配色
+ const numberColour = shownAchievement
+ ? (NUMBER_COLOURS.includes(previewParams.get('achvColor')) ? previewParams.get('achvColor') : achievementColour(shownAchievement))
+ : 'Gold';
+ // 达成率标题用原版素材(UI_RSL_Base_Achievement 长条 + UI_RSL_Text_Achievement 字)
+ field('.dx-result-achievement-label').replaceChildren(achievementLabel());
+ field('.dx-result-achievement-value').replaceChildren(
+ shownAchievement ? scoreNumber(shownAchievement + '%', { colour: numberColour }) : document.createTextNode('已通关'));
+ // 段位只跟达成率有关,所以直接由上面显示的数字推 —— 两个数据源打架就会画出「88.8888 配 S」
+ const rankText = previewParams.get('rank') || (shownAchievement ? rankOf(shownAchievement) : score?.rank) || 'AC';
+ field('.dx-result-rank').replaceChildren(rankWord(rankText));
+ // 结算底图:嵌逆向出来的客户端动态结算背景页(云/星屑/点阵/流星都在里面),
+ // 按评级切色版。它只有 A/B/S/SS/SSS 五档,细分档(AA/AAA/BB/BBB 等)按下表归并,
+ // 低于最低档、以及 AC 兜底(AC = Accepted,不是段位),都落到 B。
+ const bgRank = { 'SSS+':'SSS', 'SSS':'SSS', 'SS+':'SS', 'SS':'SS',
+ 'S+':'S', 'S':'S', 'AAA':'A', 'AA':'A', 'A':'A'
+ }[String(rankText).trim()] || 'B';
+ const sheet = field('.dx-result-sheet');
+ sheet.dataset.bg = bgRank.toLowerCase();
+ if (!sheet.querySelector('.dx-result-bg')) {
+ const bf = document.createElement('iframe');
+ bf.className = 'dx-result-bg';
+ bf.setAttribute('scrolling', 'no');
+ bf.setAttribute('aria-hidden', 'true');
+ bf.tabIndex = -1;
+ sheet.insertBefore(bf, sheet.firstChild);
+ // 背景比结算单大一圈:四周外扩 BLEED 像素,缩放取宽/高比例的较大者,
+ // 这样无论哪一维差一点都不会在边上露出底色(用户要的"四周多几个像素")。
+ const BLEED = 24;
+ sheet.style.setProperty('--result-bg-bleed', BLEED + 'px');
+ const fit = () => {
+ const kw = (sheet.clientWidth + BLEED * 2) / 903;
+ const kh = (sheet.clientHeight + BLEED * 2) / 906;
+ sheet.style.setProperty('--result-bg-scale',
+ Math.max(kw, kh, 0.2).toFixed(4));
+ };
+ fit();
+ if (window.ResizeObserver) new ResizeObserver(fit).observe(sheet);
+ window.addEventListener('resize', fit);
+ }
+ // ?bgStill=1 时只留静态色版兜底,不嵌 iframe(排查用)
+ if (previewParams.get('bgStill') !== '1') {
+ sheet.querySelector('.dx-result-bg').src =
+ '/mm-result-bg.html?ui=0&clear=1&t=0&rank=' + bgRank;
+ }
field('.dx-result-time strong').textContent = duration(result.seconds);
field('.dx-result-rating strong').textContent = score ? score.rating.toFixed(1) : '—';
const comparison = result.comparison, previous = comparison?.previousScore;
const signed = (number, digits) => (number>=0?'+':'−')+Math.abs(number).toFixed(digits);
const previousAchievement = previous?.achievementShown ?? 0;
const improvement = score && comparison ? score.achievementShown-previousAchievement : null;
- field('.dx-result-best b').textContent = comparison ? previousAchievement.toFixed(4)+'%' : '—';
- field('.dx-result-achievement-delta').textContent = improvement!==null ? signed(improvement,4)+'%' : '暂无对比';
- field('.dx-result-achievement-delta').dataset.direction = improvement!==null && improvement<0 ? 'down' : 'up';
- field('.dx-result-record').hidden = !comparison || !score || result.practiceKind==='assisted' || (previous && improvement<=0);
field('.dx-result-single-delta').textContent = score && previous ? signed(score.rating-previous.rating,1) : '';
const totalText = comparison ? comparison.ratingAfter.toFixed(1) : '—';
const totalFrame = field('.dx-result-total');
@@ -67,10 +175,13 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) {
digits.append(digit);
}
field('.dx-result-total-delta').textContent = comparison ? signed(comparison.ratingDelta,1) : '—';
+ // cleanSolve 原本用来切换 AC/CS 那对 CSS 勋章;现在勋章是官方精灵
+ // (medalPair():底座 + AP/FSD),这一段只剩判定,留着以后接精灵的亮暗。
const cleanSolve = result.verdicts.AC > 0 && Object.entries(result.verdicts).every(([verdict, count]) => verdict === 'AC' || count === 0);
- field('.dx-result-award-cs').hidden = !cleanSolve;
- field('.dx-result-medal-cs').setAttribute('aria-label','clean solve');
- field('.dx-result-award-cs').title = 'Clean Solve · AC only';
+ // 勋章挂在这里:Accepted 有 AC 就有;Clean Solve 要求 AC 之外全为 0。
+ field('.dx-result-badges').replaceChildren(medalPair({ cleanSolve }));
+ // 勋章:官方底座 + 两枚圆章。Accepted 有 AC 就有;Clean Solve 要求 AC 之外全为 0。
+ // cleanSolve 在下面才算,所以这里必须放在它后面挂载。
field('.dx-result-verdicts').replaceChildren();
for (const verdict of ['AC','WA','TLE','RE','CE','MLE','OTHER','PENDING']) {
if (['MLE','OTHER','PENDING'].includes(verdict) && !result.verdicts[verdict]) continue;
@@ -80,6 +191,9 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) {
tag.append(name,count);
field('.dx-result-verdicts').append(tag);
}
+ // 看板娘每次都重画:用户可能在设置里刚换过,图源就是那串 lolicount.top 链接。
+ // 远端挂了 mascotImage 会自己退回本地占位图,这里的排版不用管加载成功与否。
+ field('.dx-result-mascot').replaceChildren(mascotImage({ className: 'dx-result-mascot-img' }));
if (!dialog.open) dialog.showModal();
}
dialog.addEventListener('close', async () => {
@@ -198,5 +312,16 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) {
startPolling();
window.addEventListener('pageshow',event=>{if(event.persisted){startPolling();void check();}});
window.addEventListener('pagehide',()=>{clearInterval(clock);clearInterval(poll);});
- return {refresh,running:()=>timer?.status==='running'};
+ /**
+ * 开发/设计预览:塞一份样例把结算弹窗直接弹出来,不必真的做完一道题。
+ * 只改这份闭包里的状态,showResult 照常走;页面用 ?resultPreview=1 触发。
+ */
+ function previewResult(sample) {
+ timer = { problem_id: sample.problemId, status: 'completed' };
+ result = sample.result;
+ user = getUser();
+ showResult();
+ }
+
+ return {refresh,running:()=>timer?.status==='running',previewResult};
}
diff --git a/public/dx.html b/public/dx.html
index 86b02f5..ffda91d 100644
--- a/public/dx.html
+++ b/public/dx.html
@@ -4,19 +4,21 @@
Algorithm DX · Rating 总览
-
+
+
-
+
-
+
-
+
+
@@ -292,5 +299,8 @@
支持项目
微信扫一扫,或点击图片查看原图。
+
+
+
diff --git a/public/dx.js b/public/dx.js
index b4e0d6a..95f32ce 100644
--- a/public/dx.js
+++ b/public/dx.js
@@ -1165,6 +1165,31 @@ async function focusResultOnBoard({ userId, problemId }) {
const timerController=initDxTimer({getUser:()=>state.userId,onComplete:()=>load(true),onResultClose:focusResultOnBoard});
await loadMeta();
await timerController.refresh().catch(error=>{$('timerStatus').textContent=error.message;});
+// ?resultPreview=1 用样例把结算弹窗直接弹出来:调这块 UI 时不用真做完一道题。
+// 再配 ?achievement=98.7654 能试不同达成率下的排版(段位/数字配色都跟着它走);
+// ?difficulty=3000 / ?difficulty= 能试题目卡换难度配色、以及「还没难度」那一档(?difficulty= 给空值);
+// ?rating=2500 能试 DX RATING 那块底板的六档配色(绿/青/蓝/紫/橙/红,见 cf-rating-colors.js)。
+if (pageParams.get('resultPreview') === '1') {
+ const previewDifficulty = pageParams.get('difficulty');
+ const previewRating = pageParams.get('rating');
+ const rating = previewRating === null || previewRating.trim() === ''
+ ? 1523.4 : Number(previewRating);
+ timerController.previewResult({
+ problemId: pageParams.get('problem') ?? '1999A',
+ result: {
+ dailyTrack: 3,
+ difficulty: previewDifficulty === null ? '1700' : (previewDifficulty.trim() === '' ? null : previewDifficulty),
+ title: pageParams.get('title') ?? 'Two Permutations',
+ practiceKind: 'first',
+ seconds: 1837,
+ // 段位不用给:弹窗按达成率自己查表(评级只跟达成率有关),
+ // 这样显示的数字和段位不会打架
+ score: { achievementShown: 98.7654, rating },
+ comparison: { previousScore: { achievementShown: 97.2, rating: rating - 23.3 }, ratingAfter: rating, ratingDelta: 23.3 },
+ verdicts: { AC: 12, WA: 0, TLE: 0, RE: 0, CE: 0 },
+ },
+ });
+}
renderYearOptions([currentYear()]);
await refresh();
if (pageParams.get('add') === '1' && state.userId !== null) {
diff --git a/public/help.html b/public/help.html
index b22843c..bb9847c 100644
--- a/public/help.html
+++ b/public/help.html
@@ -1,13 +1,15 @@
-
使用指南 · Algorithm DX
+
+
使用指南 · Algorithm DX
-
+
+
GETTING STARTED 使用指南 从绑定第一个账号,到记录练习、看懂成绩与统计。
前往账号管理 ↗
首次使用 名称与账号 ID 通用 Cookie 教程 洛谷数据范围 同步与历史 CF 自建比赛 码蹄集同步 用时与 B50 常见问题 评分规则 模型与验证 随机抽题
@@ -136,4 +142,6 @@ 2. 取得 API key / secret 后配置
-
+
+
+
diff --git a/public/index.html b/public/index.html
index 8445a4b..6ff52d7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,18 +4,22 @@
Algorithm DX · AC 记录
-
+
+
+
+
-
+
+
-
PRACTICE JOURNAL 每一次通过,都值得记录 汇集各平台的解题足迹,回看尝试,也发现下一步。
+
PRACTICE JOURNAL 每一次通过,都值得记录 汇集各平台的解题足迹,回看尝试,也发现下一步。
@@ -210,6 +220,8 @@
这个筛选条件下没有记录
+
+
diff --git a/public/maimai-glyphs.css b/public/maimai-glyphs.css
new file mode 100644
index 0000000..1e3f719
--- /dev/null
+++ b/public/maimai-glyphs.css
@@ -0,0 +1,41 @@
+/**
+ * 官方字形精灵的排版。
+ *
+ * 这些精灵是**方格切片**(每格带透明边距),所以直接并排会显得字距很松;
+ * 这里用负外边距把字收紧。每个族的收紧量不同,是按实际观感定的。
+ */
+
+.mm-glyph { display: block; width: auto; }
+
+.mm-clear-word,
+.mm-track-label,
+.mm-rank-word,
+.mm-number { display: inline-flex; align-items: flex-end; justify-content: center; }
+
+/* CLEAR! 六格,格子 80x80,字母只占中间一块,收 -0.2em 左右才连成词。
+ 高度按官方的比例给:官方抓图里 CLEAR! 约占画面高的 7.5%。 */
+.mm-clear-word { margin: 0 -0.2em; }
+.mm-clear-word .mm-glyph { height: 0.95em; }
+
+.mm-track-label .mm-glyph { height: 1em; }
+
+/* 段位:S/A/B/C 的格子同样是方图,+ 另有一张 */
+.mm-rank-word { margin: 0 -0.12em; }
+.mm-rank-word .mm-glyph { height: 1em; }
+/* 加号的位置/大小是对着官方 SSS+ 抓图做模板匹配量出来的(素材确认就是 UI_RSL_Rank_Plus):
+ · 墨水高 ≈ 字母格的 0.38(素材字形只占格子 83%,所以图取 0.445)
+ · 顶边在字母顶边下方约 0.08 个格子高 —— 也就是**右上角**,不是和字母底边对齐
+ · 左边紧贴最后一个字母的右缘
+ 之前底对齐 + 偏大,看着就像"挂"在字下面。 */
+.mm-rank-word .mm-rank-plus { height: 0.445em; align-self: flex-start; margin-top: 0.05em; margin-left: -0.02em; }
+
+/* 大数字:官方是两套图集混排 + 单独的 '%',三者的格子底边对齐、共用一条基线。
+ 缩放比 98 : 82 : 54 是对着官方抓图量的(整数格 98、小数格 82、'%' 54)。
+ 高度由调用方给 --n(整数部分的格子高),其余按比例推。 */
+.mm-number { display: inline-flex; align-items: flex-end; margin: 0; }
+.mm-number .mm-number-int { height: var(--n, 1em); }
+.mm-number .mm-number-frac { height: calc(var(--n, 1em) * 0.837); }
+/* 相邻字形重叠 8%:格子带透明边距,不收会散开,收太多(-24%)又会挤成一团 */
+.mm-number .mm-number-int + .mm-number-int { margin-left: calc(var(--n, 1em) * -0.08); }
+.mm-number .mm-number-frac + .mm-number-frac { margin-left: calc(var(--n, 1em) * -0.067); }
+.mm-number .mm-number-percent { height: calc(var(--n, 1em) * 0.551); margin-left: calc(var(--n, 1em) * -0.04); }
diff --git a/public/maimai-glyphs.js b/public/maimai-glyphs.js
new file mode 100644
index 0000000..42ea3c1
--- /dev/null
+++ b/public/maimai-glyphs.js
@@ -0,0 +1,410 @@
+/**
+ * 官方结算素材拼字:CLEAR! / TRACK / 段位字母 / 大数字。
+ *
+ * 页面上这些原本是 CSS 画的花体字,形似但终归不是官方的。客户端里这几样都有现成精灵,
+ * 尺寸和配色都是官方那套,这里只负责把格子拼成词,交给调用方塞进页面。
+ *
+ * 几个只看文件名读不出来的地方(都是对着抓图核过的):
+ * · CLEAR! 是六个字母精灵,有 01/02/03 三套配色;抓图那套是 _01_(白字 + 蓝描边)
+ * · 段位没有 SS/SSS,是用 S 的颜色变体拼的:三个 S 就是 Blue/Red/Yellow
+ * · 大数字用分数那套金色精灵,格子号 0-9 是数字,10='+'、11='-'、12='/'、13='.'
+ * · '%' 是单独一张 UI_RSL_Score_Per_Gold
+ */
+
+import { platformLabel } from './problem-source.js';
+
+const ASSETS = '/assets/mm-result/';
+const RANK_LETTERS = new Set(['S', 'A', 'B', 'C', 'D']);
+
+/** 大数字可选的四套配色,和客户端里的族名一致 */
+export const NUMBER_COLOURS = ['Gold', 'Red', 'Blue', 'Base'];
+/** 数字的两套图集:同一字体的两个尺寸,1110000 是大号 */
+export const NUMBER_ATLASES = ['1110000', '0001111'];
+
+/** 大数字的格子号:和 layout 里的编号一致 */
+const DIGIT_CELL = { ...Object.fromEntries([...'0123456789'].map((c, i) => [c, i])), '+': 10, '-': 11, '/': 12, '.': 13 };
+
+/**
+ * 小数点的坑号**不是每族都一样**,这是逐族比字形才看出来的:
+ * 0001111_Red 的 12/13 是反的(13 画的是逗号,12 才是小数点)。其余族都是 13。
+ * 只影响小数点,数字 0~9 各族的坑号一致。
+ */
+const DOT_CELL = { Red: 12 };
+
+function glyph(file, { className = '', height = '' } = {}) {
+ const img = document.createElement('img');
+ img.className = 'mm-glyph' + (className ? ' ' + className : '');
+ img.src = ASSETS + file;
+ img.alt = '';
+ img.decoding = 'async';
+ if (height) img.style.height = height;
+ return img;
+}
+
+/** CLEAR! —— 六个字母精灵;抓图用的是 _01_ 那套配色 */
+export function clearWord({ variant = '01' } = {}) {
+ const word = document.createElement('span');
+ word.className = 'mm-clear-word';
+ for (let i = 1; i <= 6; i += 1) word.append(glyph(`UI_RSL_Text_Clear_${variant}_${String(i).padStart(2, '0')}.png`));
+ return word;
+}
+
+/** TRACK —— 官方就一张图。prefab 里 UI_Track 是 93x30 抓图 = 112x36 原生,
+ 素材 UI_RSL_Text_TRACK 正好 110x36,所以是按原生尺寸贴的。 */
+export function trackLabel() {
+ const label = document.createElement('span');
+ label.className = 'mm-track-label';
+ label.append(glyph('UI_RSL_Text_TRACK.png'));
+ return label;
+}
+
+/**
+ * 达成率标题那一行 —— 用原版素材,不用页面自己拼的文字。
+ *
+ * 只画 UI_RSL_Text_Achievement(prefab: AchievementLogo,296x41,抓图 247x40)。
+ * prefab 里还有一条 UI_RSL_Base_Achievement(Achievment_Base/Base_01,抓图 1667x107)
+ * —— 那是 1667px 宽、比 903 的画面还宽的长条,本是溢出裁切的装饰底;
+ * 整条铺进这一行会变成一大片空色(文字只占最左一点),看着就是"意义不明的色块",
+ * 所以不画它。
+ */
+export function achievementLabel() {
+ const wrap = document.createElement('span');
+ wrap.className = 'mm-achv-label';
+ wrap.append(glyph('UI_RSL_Text_Achievement.png', { className: 'mm-glyph mm-achv-label-text' }));
+ return wrap;
+}
+
+/**
+ * 结算勋章:一块底座 + 两枚徽章。
+ *
+ * prefab(UI_MedalDisplay,capture 250x125@(144,553)):
+ * Medal_Base UI_RSL_MedalBase 326x153 原生 -> capture 273x130@(172,552) 底座
+ * v110 (左槽) capture 83x83@(227,574) -> AP_Base/AP_Text_01
+ * Null_FS_FSD_Medal(右槽) capture 83x83@(318,574) -> FSD_Base/FSD_Text_01
+ * 两槽间距 318-227 = 91 capture;槽相对底座左边 227-172 = 55 capture。
+ * 整组中心 x=314,段位中心 x=292(差 22 capture ~ 26 原生),基本水平居中。
+ */
+export function medalPair({ cleanSolve = true } = {}) {
+ const wrap = document.createElement('span');
+ wrap.className = 'mm-medals';
+ wrap.append(glyph('UI_RSL_MedalBase.png', { className: 'mm-glyph mm-medal-base' }));
+ // 两枚圆片是 Codex 生图产出的(青绿 Accepted / 香槟金 Clean Solve),
+ // 官方素材 UI_RSL_AP_* / UI_RSL_FSD_* 是分「底 + 字」两层、画风偏写实,
+ // 这里按需求换成配套的一对圆章。
+ const ap = document.createElement('img');
+ ap.className = 'mm-medal mm-medal--ap';
+ ap.src = '/assets/maimai/medal-accepted.png';
+ ap.alt = 'Accepted';
+ wrap.append(ap);
+ const cs = document.createElement('img');
+ cs.className = 'mm-medal mm-medal--fsd';
+ cs.src = '/assets/maimai/medal-cleansolve.png';
+ cs.alt = 'Clean Solve';
+ // 语义:有 AC 就有 Accepted;AC 之外还有 WA/TLE/RE/CE 就不算 Clean Solve
+ cs.dataset.earned = cleanSolve ? 'true' : 'false';
+ wrap.append(cs);
+ return wrap;
+}
+
+/**
+ * 轨道号码("03")。
+ *
+ * 官方不是写文字,是【数字精灵】+【同位的一块实心白块】两层:
+ * prefab: TrackNumber/UI_Track/NUM_TrackNumber UI_CMN_Num_26p_N 50x28 抓图
+ * TrackNumber/UI_Track/NUM_TrackNumber_Outline UI_CMN_Num_26p_Outline_N 50x28 抓图(同位)
+ * 反编译: KOP_ResultTrackData 里 `_trackNoImages[0].ChangeSprite(trackNumber)`
+ * —— _trackNoImages 是数组,每个元素显示一位数字
+ *
+ * ⚠️ 那个 Outline **不是描边图,是一整块实心白**(实测不透明 56.8%,ASCII 是一片白)。
+ * 它靠 MultipleImage 的 Subtract 混合(MultiImageBlendType.Subtract / _BlendOp=2)
+ * 去"减掉"周围做镂空,**不能当普通图层叠上去** —— 叠上去数字会被整块盖住变全白。
+ * 所以这里只画数字,描边用 CSS drop-shadow 近似;等解出 Subtract 的真实参数再换。
+ * 数字本身是白色,这里按像素尺寸从 prefab 推:容器 60x34 原生放两位 => 每格 30x34 原生。
+ */
+export function trackNumber(value) {
+ const row = document.createElement('span');
+ row.className = 'mm-track-num';
+ for (const ch of String(value)) {
+ if (!/[0-9]/.test(ch)) continue;
+ const cell = document.createElement('span');
+ cell.className = 'mm-track-digit';
+ cell.append(glyph(`UI_CMN_Num_26p_${ch}.png`, { className: 'mm-glyph mm-track-face' }));
+ row.append(cell);
+ }
+ return row;
+}
+
+/**
+ * 段位:从 D 到 SSS+。
+ *
+ * 官方只有 D/C/B/A/S 五张**单字**精灵,多字母的段位是重复拼出来的(SS/SSS、AA/AAA、BB/BBB)。
+ * S 另有 Blue/Red/Yellow 三个配色:**三个 S 用主色 + Blue + Red,少于三个用 Yellow**
+ * (客户端里就是这么配的)。末尾带 + 时补一张 UI_RSL_Rank_Plus。
+ *
+ * 注意:名字里带 _White 的那几张**不是白字**,是段位动画的碎裂特效,别拿来当字母用。
+ */
+export function rankWord(rank) {
+ const word = document.createElement('span');
+ word.className = 'mm-rank-word';
+ const raw = String(rank ?? '').toUpperCase();
+ const plus = raw.endsWith('+');
+ const letters = raw.replace(/\+$/, '');
+ if (!letters || ![...letters].every((c) => RANK_LETTERS.has(c))) {
+ // 认不出来的段位(例如 AC)原样显示,至少不会画错
+ word.textContent = String(rank ?? '');
+ return word;
+ }
+ const files = letters === 'SSS'
+ ? ['UI_RSL_Rank_S.png', 'UI_RSL_Rank_S_Blue.png', 'UI_RSL_Rank_S_Red.png']
+ : [...letters].map((letter) => letter === 'S'
+ ? 'UI_RSL_Rank_S_Yellow.png'
+ : `UI_RSL_Rank_${letter}.png`);
+ // ⭐ 多字母段位里,后面的字母比第一个小 —— 这是官方的排法,不是等大重复。
+ // 量官方预制整图(UI_GAM_Rank_*)的内容宽度:
+ // A 单字 35 | AA 58 | AAA 82 => AAA 只有 3x35 的 78%
+ // B 单字 30 | BB 52 | BBB 75 => BBB 只有 3x30 的 83%
+ // S 单字 30 | SS 58 | SSS 85 => SSS 是 3x30 的 94%(基本等大)
+ // 所以 A/B 族的第 2 个起缩到 0.72,S 族保持等大。
+ const smallTail = letters[0] === 'A' || letters[0] === 'B';
+ files.forEach((file, i) => {
+ const cls = 'mm-rank-letter' + (smallTail && i > 0 ? ' mm-rank-letter-sm' : '');
+ word.append(glyph(file, { className: cls }));
+ });
+ if (plus) word.append(glyph('UI_RSL_Rank_Plus.png', { className: 'mm-rank-plus' }));
+ return word;
+}
+
+/**
+ * 大数字:'98.7654' 或 '100.1445%'。
+ *
+ * 官方这套排法和普通文字排版不一样,是**两套图集混排**(对着抓图核过):
+ * · 整数部分用大号图集 1110000(格子 72x98,字形 103 原生)
+ * · 小数点和小数部分用小号图集 0001111(格子 62x82,字形 83 原生)
+ * · '%' 用单独的 UI_RSL_Score_Per_*(58x54)
+ * 三者的格子**底边对齐**,所以整行共用一条基线;缩放比 98 : 82 : 54。
+ *
+ * 配色由达成率决定:< 80 蓝、>= 97 金、其余红。
+ */
+export function scoreNumber(text, { colour = 'Gold' } = {}) {
+ const row = document.createElement('span');
+ row.className = 'mm-number';
+ const value = String(text);
+ const dot = value.indexOf('.');
+ const whole = dot === -1 ? value.replace('%', '') : value.slice(0, dot);
+ const fraction = dot === -1 ? '' : value.slice(dot + 1).replace('%', '');
+ const hasPercent = value.endsWith('%');
+ // '%' 只有 Gold/Red/Blue 三个变体,没有 Base,缺的落到 Gold
+ const percentColour = ['Gold', 'Red', 'Blue'].includes(colour) ? colour : 'Gold';
+
+ const digit = (ch, cls) => {
+ const cell = ch === '.' ? (DOT_CELL[colour] ?? DIGIT_CELL['.']) : DIGIT_CELL[ch];
+ if (cell === undefined) return null;
+ const atlas = cls === 'mm-number-int' ? '1110000' : '0001111';
+ return glyph(`UI_NUM_Score_${atlas}_${colour}_${cell}.png`, { className: 'mm-glyph ' + cls });
+ };
+ for (const ch of whole) {
+ const img = digit(ch, 'mm-number-int');
+ if (img) row.append(img);
+ }
+ if (fraction) {
+ // 小数点单独挂一个 mm-number-dot:它在格子里的留白和数字完全不是一回事
+ // (左16.9 / 右15.4 / 底12.3 px,数字是 2.3 / 2.3 / 3.8),
+ // 排版要单独压、两侧单独收,见 dx-result.css 里那段「达成率大数字」。
+ const point = digit('.', 'mm-number-frac mm-number-dot');
+ if (point) row.append(point);
+ for (const ch of fraction) {
+ const img = digit(ch, 'mm-number-frac');
+ if (img) row.append(img);
+ }
+ }
+ if (hasPercent) row.append(glyph(`UI_RSL_Score_Per_${percentColour}.png`, { className: 'mm-glyph mm-number-percent' }));
+ return row;
+}
+
+/** 达成率对应哪套配色:< 80 蓝、>= 97 金、其余红 */
+export function achievementColour(value) {
+ const number = Number(value);
+ if (!Number.isFinite(number)) return 'Gold';
+ if (number >= 97) return 'Gold';
+ if (number < 80) return 'Blue';
+ return 'Red';
+}
+
+/* ------------------------------------------------------------------ *
+ * 题目卡(原「乐曲卡」,prefab 里是 UI_MusicData)
+ * ------------------------------------------------------------------ */
+
+/**
+ * 卡片底板:官方 `UI_RSL_MBase_*`,745x142 原生,一张一个难度色。
+ *
+ * prefab 量出来的构件(原生 px,相对底板左上角)——下面 CSS 里的百分比都按这张表算:
+ * IMG_Background UI_RSL_MBase_ADV 整张 745x142
+ * JacketImage 槽位 x11..95 / y47..131(板上烤着一块纯黑,游戏里拿曲绘盖住)
+ * Mask 深蓝标题条 x106..709 / y88..128(也烤在板上)
+ * 难度名 x≈106..335 / y≈50..80(BASIC/ADVANCED/EXPERT/MASTER/…)
+ * GameType UI_RSL_MBase_Parts_01 84x28 → 摆在 x≈488..572 / y≈51..75
+ *
+ * ⚠️ 板上烤着难度名,题目卡不能直接显示它 —— 见 `problemCard()` 里那块 cover:
+ * 用同一张板的**干净底色**把那一小段盖掉,位置也是按 prefab 量的。
+ *
+ * ⚠️ 官方七张板(BSC/ADV/EXP/MST/MST_Re/UTG/DMY)里**没有蓝也没有青**
+ * —— maimai 的难度色本来就没有蓝。所以按色相挑一张最近的板,
+ * 再用 saturate()+hue-rotate() 把板面主色拉到 cfRatingColor 那一档的颜色上。
+ * 每组参数都是按 CSS 滤镜矩阵解出来的(sRGB 误差 < 25/255):
+ * green 板面 #45c124 → #4aa85c cyan #45c124 → #3fa8c4
+ * blue #9f51dc → #4a7fe0 violet #9f51dc → #8b6cf0
+ * orange #ffba01 → #f08c3a red #ff5a66 → #e8564f
+ * gray #969696 → #7b8391
+ * 深蓝标题条会被这个滤镜带偏,所以脚本另外压一条自己的 navy(`.dx-result-track-bar`)。
+ */
+const CARD_TONES = {
+ green: { plate: 'UI_RSL_MBase_BSC.png', level: 'BSC', field: '#45c124', filter: 'saturate(0.66) hue-rotate(15deg)' },
+ cyan: { plate: 'UI_RSL_MBase_BSC.png', level: 'BSC', field: '#45c124', filter: 'saturate(0.68) hue-rotate(79deg)' },
+ blue: { plate: 'UI_RSL_MBase_MST.png', level: 'MST', field: '#9f51dc', filter: 'saturate(0.98) hue-rotate(-44deg)' },
+ violet: { plate: 'UI_RSL_MBase_MST.png', level: 'MST', field: '#9f51dc', filter: 'saturate(1.08) hue-rotate(-14deg)' },
+ orange: { plate: 'UI_RSL_MBase_ADV.png', level: 'EXP', field: '#ffba01', filter: 'saturate(1.2) hue-rotate(-37deg)' },
+ red: { plate: 'UI_RSL_MBase_EXP.png', level: 'EXP', field: '#ff5a66', filter: 'saturate(0.9) hue-rotate(12deg)' },
+ gray: { plate: 'UI_RSL_MBase_DMY.png', level: 'MST', field: '#969696', filter: 'saturate(0) brightness(0.87)' },
+ /* 未评级(题目还没难度):底板用官方 **UTAGE**(宴)卡 UI_RSL_MBase_UTG.png(745x141)。
+ 它的轮廓和灰 DMY(板上烤着 DUMMY)**逐点相同**(alpha 包围盒、圆盘拟合完全一致),
+ 差别只有配色:UTG 板面是粉紫,板上烤着「U・TA・GE」(x110..243/y53..70,
+ 落在 cover 那块补丁 x96..304/y41..87 里,会被完全盖掉)。
+ 所以灰档配 DMY 其实等于没换 —— 要的是 UTAGE 那张卡本身。
+ filter 取 none:那套 saturate/hue-rotate 是为了把七张难度板的板面主色拉到
+ CF 那一档的颜色上(蓝/青/绿…),unrated 没有对应档位,UTAGE 卡是什么色就该是什么色。
+ field #ff86fe:这是 cover 那圈**紧邻的板面平色**,和另外五张板同一个取法
+ (BSC→#45c124、MST→#9f51dc、DMY→#969696 都是这么取的,逐个核对过)。
+ ⚠️ 不能拿 cover **里面**的主色:UTG 那块补丁里压着一片洋红 (249,45,218),
+ 而补丁四周是浅粉 (255,134,254),照搬里面那块会在标题左边留下一个
+ 深洋红的方块(上下两条边都露)。补丁必须和**外面**接得上。
+ ⚠️ level 仍是 MST:图集里没有 UTG 族(只有 BSC/EXP/MST/MST_Re),
+ 「LV」那个字母只能借一族,MST 的紫最接近这张粉卡。 */
+ unrated: { plate: 'UI_RSL_MBase_UTG.png', level: 'MST', field: '#ff86fe', filter: 'none' },
+};
+
+/** tone → { plate, level, field, filter },认不出来的一律按未评级(官方那张灰 DMY) */
+export function cardTone(tone) {
+ return CARD_TONES[tone] ?? CARD_TONES.unrated;
+}
+
+/** 等级数字图集 `UI_CMN_MusicLevel_{族}_{格}.png` 的格子号:0-9 是数字,14 是「LV」 */
+const LEVEL_CELL = { '+': 10, '-': 11, ',': 12, '.': 13, LV: 14 };
+
+/** 难度未知时顶在数字位置上的那个字。图集里没有问号(15 格 = 0-9 + - , . LV),只能排字 */
+const UNKNOWN_LEVEL = '?';
+
+/** 「?」那颗字。样式在 dx-result.css 的 .mm-level-unknown(按数字精灵的白芯配大小/基线) */
+function unknownLevelMark() {
+ const node = document.createElement('span');
+ node.className = 'mm-level-unknown';
+ node.textContent = UNKNOWN_LEVEL;
+ node.setAttribute('aria-label', '难度待定');
+ return node;
+}
+
+/**
+ * 官方等级数字。等宽格子 48x60,数字/符号直接按格子号取图。
+ * ⚠️ 第 14 格画的是 **LV** 两个字母(不是任务描述里说的 '%')——
+ * 对着官方抓图核过:卡片右端那个小 `LV` 就是这个格子,所以不用 UI_KLD_MSS_Level_*
+ * (那五张是「LEVEL」美字的**噪声动画帧**,单张拿出来是散开的色偏切片)。
+ * `value` 里的 '?' 走 unknownLevelMark()(文字,不是精灵),其余认不出的字符跳过。
+ *
+ * 结构:`.mm-level` 里两个**各自绝对定位**的子节点 ——
+ * `.mm-level-lv` LV,钉死在卡片里一个固定 x(见 dx-result.css)
+ * `.mm-level-digits` 这一串数字(或那个 `?`),整体以圆盘圆心为心居中
+ * 分开包一层是必须的:LV 固定、数字居中,两者不能还在同一条 flex 行里,
+ * 否则 LV 的 x 一定会跟着数字宽度跑(改之前就是这样)。见 dx-result.css 那段注释。
+ */
+export function levelNumber(value, { tone = 'unrated' } = {}) {
+ const { level } = cardTone(tone);
+ const row = document.createElement('span');
+ row.className = 'mm-level';
+ const cell = ch => (/^[0-9]$/.test(ch) ? Number(ch) : LEVEL_CELL[ch]);
+ const lv = cell('LV');
+ row.append(glyph(`UI_CMN_MusicLevel_${level}_${lv}.png`, { className: 'mm-level-lv' }));
+ const digits = document.createElement('span');
+ digits.className = 'mm-level-digits';
+ for (const ch of String(value)) {
+ if (ch === UNKNOWN_LEVEL) { digits.append(unknownLevelMark()); continue; }
+ const index = cell(ch);
+ if (index === undefined) continue;
+ digits.append(glyph(`UI_CMN_MusicLevel_${level}_${index}.png`, { className: 'mm-level-digit' }));
+ }
+ row.append(digits);
+ return row;
+}
+
+/** GameType 徽章(官方 `UI_RSL_MBase_Parts_01`,就是抓图上那块「でらっくす」小牌) */
+export function gameTypeBadge() {
+ const wrap = document.createElement('span');
+ wrap.className = 'mm-gametype';
+ wrap.append(glyph('UI_RSL_MBase_Parts_01.png'));
+ return wrap;
+}
+
+/**
+ * 题目卡内容:官方底板 + 官方等级数字。
+ *
+ * 结构(外层 `.dx-result-track` 保留在弹窗里,只往里塞这些东西):
+ * art 底板的画布 —— saturate/hue-rotate 只作用在这一层
+ * plate 底板本体
+ * cover 盖掉板上烤着的难度名(用同一张板的干净底色,所以滤镜一加还是严丝合缝)
+ * bar 深蓝标题条(自己压的 navy,免得被滤镜带偏)
+ * slot 曲绘槽 → 放题号
+ * heading 难度名那一段 → 放赛制/平台
+ * badge でらっくす
+ * title 标题条 → 放题名
+ * level 右侧圆盘 → LV + 难度分
+ *
+ * `difficulty` 为 null 时(题目还没难度)不画数字,等级那一格画一个文字「?」,
+ * 底板落到 UTAGE 那张卡(`tone` 会是 unrated,见 CARD_TONES.unrated)。
+ *
+ * `kind` 是**已经翻好的字面**(练习类型:INDEPENDENT/REPLAY/…,调用方从 dx-timer.js 的
+ * PRACTICE_KIND_LABEL 取),`platform` 是**平台 key**(codeforces/atcoder/…,不是字面),
+ * 由 problem-source.js 的 PLATFORM_LABELS 翻成卡片上的字。给 key 而不是字面,
+ * 是为了让「平台词」永远有值:不传/认不出都会落回那边的默认值,卡片不会空一格。
+ * 两个词都不在这份模板里写死。
+ */
+export function problemCard({ problemId = '', kind = '', platform = '', title = '', difficulty = null, tone = 'unrated' } = {}) {
+ const info = cardTone(tone);
+ const card = document.createDocumentFragment();
+ const span = (className, text) => {
+ const node = document.createElement('span');
+ node.className = className;
+ if (text !== undefined) node.textContent = text;
+ return node;
+ };
+
+ const art = span('dx-result-track-art');
+ art.setAttribute('aria-hidden', 'true');
+ art.append(span('dx-result-track-plate'), span('dx-result-track-cover'));
+ const bar = span('dx-result-track-bar');
+ bar.setAttribute('aria-hidden', 'true');
+
+ const slot = span('dx-result-track-slot');
+ slot.textContent = String(problemId).replace(/[:\s]/g, '');
+ slot.setAttribute('aria-label', `题目 ${problemId}`);
+
+ // 难度名那一段换成赛制 + 平台:左边的题号已经在曲绘槽里了,这里不重复。
+ // 两个词都来自 key→字面的表,不在模板里写死字符串(见上面的参数说明)。
+ const heading = span('dx-result-track-heading');
+ heading.append(span('dx-result-kind', kind), span('dx-result-problem', platformLabel(platform)));
+
+ const name = document.createElement('h3');
+ name.className = 'dx-result-name';
+ const titleText = span('dx-result-title', String(title || problemId));
+ name.append(titleText);
+
+ const level = span('dx-result-level dx-result-level-disc');
+ if (Number.isFinite(difficulty)) level.append(levelNumber(String(difficulty), { tone }));
+ /* 没难度:底板是 UTAGE 卡(见 CARD_TONES.unrated),等级那一格画「?」——
+ 官方图集里没有问号精灵,用文字顶上,字号/基线对着数字精灵的白芯配(见 CSS)。
+ 原来这里退回文字「待定」(.dx-result-level-pending,样式还留在 dx-result.css 里,
+ 要换回来改这一行就行):「LV ?」和「待定」说的是同一件事,并排写重复;
+ 而且两段加起来会把整组往左推 38px(1200 视口实测盒 x 683 → 645)。 */
+ else level.append(levelNumber(UNKNOWN_LEVEL, { tone }));
+
+ card.append(art, bar, slot, heading, gameTypeBadge(), name, level);
+ return card;
+}
+
diff --git a/public/mascot.css b/public/mascot.css
new file mode 100644
index 0000000..2bc361c
--- /dev/null
+++ b/public/mascot.css
@@ -0,0 +1,99 @@
+/**
+ * 看板娘设置弹窗的样式。
+ *
+ * 和 dx-result.css 分开:这一套是全站设置,任何页面都能挂;结算弹窗里那块看板娘的
+ * 样式跟着结算走,写在 dx-result.css。配色沿用站点既有的深浅主题变量。
+ */
+
+.mm-mascot-dialog {
+ width: min(460px, calc(100vw - 32px));
+ padding: 0;
+ border: 1px solid var(--line, #d5d9e6);
+ border-radius: 14px;
+ background: var(--panel, #fff);
+ color: var(--ink, #1b1f2a);
+ box-shadow: 0 24px 70px #0308244d;
+}
+
+.mm-mascot-dialog::backdrop { background: #071126a8; backdrop-filter: blur(4px); }
+
+.mm-mascot-form { position: relative; padding: 18px 20px 16px; }
+
+.mm-mascot-form h2 { margin: 0 0 8px; font-size: 17px; }
+
+.mm-mascot-close {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ width: 28px;
+ height: 28px;
+ border: 1px solid var(--line, #d5d9e6);
+ border-radius: 50%;
+ background: transparent;
+ color: inherit;
+ font-size: 19px;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.mm-mascot-hint { margin: 0 0 12px; font-size: 12.5px; line-height: 1.6; opacity: .82; }
+.mm-mascot-hint a { color: var(--accent, #2f6fed); }
+.mm-mascot-hint code {
+ padding: 1px 4px;
+ border-radius: 4px;
+ background: var(--code-bg, #00000010);
+ font-size: 12px;
+}
+
+.mm-mascot-label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 700; }
+
+.mm-mascot-input {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid var(--line, #d5d9e6);
+ border-radius: 8px;
+ background: var(--input-bg, #f7f8fc);
+ color: inherit;
+ font: 12.5px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace;
+ word-break: break-all;
+}
+
+.mm-mascot-input:focus { outline: 2px solid var(--accent, #2f6fed); outline-offset: 1px; }
+
+/* 预览固定在一条高度里:看板娘是竖长图,高度不定会把弹窗顶来顶去 */
+.mm-mascot-preview {
+ display: grid;
+ place-items: center;
+ height: 190px;
+ margin: 12px 0;
+ border: 1px dashed var(--line, #d5d9e6);
+ border-radius: 10px;
+ overflow: hidden;
+ background: var(--input-bg, #f7f8fc);
+}
+
+.mm-mascot-preview-img { max-width: 100%; max-height: 190px; object-fit: contain; }
+
+.mm-mascot-actions { display: flex; justify-content: flex-end; gap: 8px; }
+
+/* 顶栏入口:和深色模式开关并排,沿用同一个圆钮语言 */
+body.mm-theme .mm-mascot-btn {
+ flex: 0 0 auto;
+ height: 44px;
+ margin-left: 6px;
+ padding: 0 16px;
+ border: 3px solid var(--mm-purple, #8b6fd6);
+ border-radius: 100px;
+ background: #fff;
+ color: var(--mm-purple, #6b4bbd);
+ font-size: 13px;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+body.mm-theme .mm-mascot-btn:hover { background: #f3efff; }
+body.mm-theme[data-mm-theme="dark"] .mm-mascot-btn { background: #221c33; color: #d8ccff; }
+body.mm-theme[data-mm-theme="dark"] .mm-mascot-btn:hover { background: #2c2542; }
+
+/* 结算弹窗里的看板娘没加载出来时别留破图 */
+.mm-mascot-failed { display: none; }
diff --git a/public/mascot.js b/public/mascot.js
new file mode 100644
index 0000000..241084f
--- /dev/null
+++ b/public/mascot.js
@@ -0,0 +1,181 @@
+/**
+ * 看板娘:一处全局设置,一处展示。
+ *
+ * 图片来自 lolicount.top —— 那个站按 URL 参数出图(`@用户名` 选人、`theme` 选主题、
+ * `scale`/`fsize` 调大小、`text` 是计数文字)。直接用
引用就行,不走 fetch:
+ *
不会碰上跨域,站上那边换图我们也不用改代码。
+ *
+ * 链路:设置弹窗写 localStorage(mm-mascot)→ 发 mm-mascot-change →
+ * 要展示的地方(目前是结算弹窗)自己重画。无依赖,原生 ESM,和 theme.js 一个路数。
+ *
+ * 站点:https://lolicount.top —— 设置页里给出入口,让用户自己去挑。
+ */
+
+const MASCOT_KEY = 'mm-mascot';
+
+/** 默认看板娘:一串普通的 lolicount.top 图片链接,用户随时可以换成自己的 */
+export const MASCOT_DEFAULT =
+ 'https://lolicount.top/@ttttt?theme=manosaba-ema&fsize=16&scale=1&unshowf=true&text=%7Bn%7D';
+
+/** 站上「获取链接」的入口,设置页引导用户去这里挑 */
+const MASCOT_SITE = 'https://lolicount.top';
+
+/**
+ * 远端图挂了就退回这张本地立绘(仓库自带,不联外网)。
+ *
+ * 不是随便找的占位图:这是客户端里结算画面那位导航角色的立绘
+ * `UI_Navichara_01_P1_02`(524×812 RGBA,全身站姿、底色全透明)——
+ * 结算画面的 `Null_NavicharaRoot` 挂的就是 `Navi_Default` 这一族。
+ * 同族的 02/03/04/05 是同一姿势的几帧表情差分,02 是两眼睁开、
+ * 站得最"正"的那一帧。
+ *
+ * 之前挑过 `UI_ADV_Img_chara155_01_01`(616×1160,撑伞的紫发角色),
+ * 那张本身没问题,但她是淡紫配白,放在成绩板那块近白玻璃后面整片糊掉,
+ * 立绘就"读"不出来了;这一位是红裙 + 黑色条纹袜,浅底板上反差够,
+ * 隔着板子还看得见人。(同族的 `UI_ADV_Img_chara155_01_shadow`
+ * 证明 chara155 那族也是"站到背景上"的图,只是不适合这个底板。)
+ *
+ * 出图路子见 `scripts/extract-result-assets.py`(UnityPy 读 resources.assets
+ * 的 Texture2D),这里只放结果。
+ */
+export const MASCOT_FALLBACK = '/assets/mm-parts/navichara-01-p1-02.png';
+
+export function getMascotUrl() {
+ try {
+ const value = localStorage.getItem(MASCOT_KEY);
+ return value && value.trim() ? value.trim() : MASCOT_DEFAULT;
+ } catch {
+ return MASCOT_DEFAULT;
+ }
+}
+
+export function setMascotUrl(url) {
+ const value = typeof url === 'string' ? url.trim() : '';
+ try {
+ // 空的就删掉键,回到默认;这样「恢复默认」和「填了自己的」共用一条路径
+ if (!value || value === MASCOT_DEFAULT) localStorage.removeItem(MASCOT_KEY);
+ else localStorage.setItem(MASCOT_KEY, value);
+ } catch {
+ /* 隐私模式下写不进去,当次会话照样能用 */
+ }
+ document.dispatchEvent(new CustomEvent('mm-mascot-change', { detail: { url: getMascotUrl() } }));
+}
+
+/**
+ * 造一个看板娘
。
+ *
+ * lolicount.top 给的是 SVG,浏览器能直接在
里画;这里不解析它的内容,
+ * 将来它换成别的位图格式也照用。
+ *
+ * 三级降级:用户链接 → 本地占位图(fallback)→ 彻底藏起来。
+ * 前两级任何一级失败都不会露出浏览器的破图标;最后一级加 .mm-mascot-failed,
+ * 由 mascot.css 设成 display:none —— 布局照样是完整的一块,只是里面没有图。
+ */
+export function mascotImage({ className = '', alt = '看板娘', fallback = MASCOT_FALLBACK } = {}) {
+ const img = document.createElement('img');
+ img.className = className;
+ img.alt = alt;
+ img.loading = 'lazy';
+ img.decoding = 'async';
+ img.referrerPolicy = 'no-referrer';
+ let dead = false; // 已经退到尽头,别再互相触发 error
+ const fail = () => {
+ img.classList.add('mm-mascot-failed');
+ img.alt = '看板娘加载失败,检查一下链接';
+ img.removeAttribute('src');
+ };
+ const show = (url) => {
+ if (!url) { dead = true; fail(); return; }
+ dead = false;
+ img.classList.remove('mm-mascot-failed');
+ img.src = url;
+ };
+ img.addEventListener('error', () => {
+ if (dead) return;
+ // 先退回本地占位图;占位图自己也没了就藏起来,别留破图
+ if (fallback && img.getAttribute('src') !== fallback) { img.src = fallback; return; }
+ dead = true;
+ fail();
+ });
+ show(getMascotUrl());
+ // 设置改了就换图;同一个元素上只挂一次
+ const onchange = (event) => show(event.detail?.url || getMascotUrl());
+ img.addEventListener('mm-mascot-detach', () => document.removeEventListener('mm-mascot-change', onchange));
+ document.addEventListener('mm-mascot-change', onchange);
+ return img;
+}
+
+let settingsDialog = null;
+
+/** 打开看板娘设置:一个链接输入框 + 实时预览 + 去站上挑的引导 */
+export function openMascotSettings() {
+ if (!settingsDialog) settingsDialog = buildSettings();
+ const input = settingsDialog.querySelector('.mm-mascot-input');
+ const preview = settingsDialog.querySelector('.mm-mascot-preview');
+ input.value = getMascotUrl();
+ preview.replaceChildren(mascotImage({ className: 'mm-mascot-preview-img' }));
+ if (!settingsDialog.open) settingsDialog.showModal();
+ input.focus();
+ input.select();
+}
+
+function buildSettings() {
+ const dialog = document.createElement('dialog');
+ dialog.className = 'mm-mascot-dialog';
+ dialog.setAttribute('aria-labelledby', 'mmMascotTitle');
+ dialog.innerHTML = `
+
`;
+ const input = dialog.querySelector('.mm-mascot-input');
+ const preview = dialog.querySelector('.mm-mascot-preview');
+
+ const redraw = () => {
+ // 预览走同一个地址,所见即所得;这里不写 localStorage,避免边打字边存进半截链接
+ const img = document.createElement('img');
+ img.className = 'mm-mascot-preview-img';
+ img.alt = '看板娘预览';
+ img.referrerPolicy = 'no-referrer';
+ img.src = input.value.trim() || MASCOT_DEFAULT;
+ preview.replaceChildren(img);
+ };
+
+ dialog.querySelector('.mm-mascot-save').addEventListener('click', () => {
+ setMascotUrl(input.value);
+ dialog.close();
+ });
+ dialog.querySelector('.mm-mascot-reset').addEventListener('click', () => {
+ input.value = MASCOT_DEFAULT;
+ redraw();
+ });
+ input.addEventListener('input', redraw);
+ dialog.addEventListener('close', () => {
+ // 关闭时丢弃没保存的编辑,免得下次打开显示的是没生效的内容
+ input.value = getMascotUrl();
+ });
+ document.body.append(dialog);
+ return dialog;
+}
+
+const button = document.getElementById('mmMascotBtn');
+if (button) button.addEventListener('click', () => openMascotSettings());
+
+// ?mascot=1 直接打开设置:截图、演示、自动化测试都用得上
+if (new URLSearchParams(location.search).get('mascot') === '1') {
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => openMascotSettings());
+ else openMascotSettings();
+}
diff --git a/public/mm-result-bg.html b/public/mm-result-bg.html
new file mode 100644
index 0000000..d066993
--- /dev/null
+++ b/public/mm-result-bg.html
@@ -0,0 +1,279 @@
+
+
+
maimai DX 结算 · 纯动态背景
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
评级
+
+ · ?rank= | ?ui=0
+
diff --git a/public/problem-source.js b/public/problem-source.js
new file mode 100644
index 0000000..dfbe284
--- /dev/null
+++ b/public/problem-source.js
@@ -0,0 +1,39 @@
+/**
+ * 结算卡片那行小字里的**平台词**:平台 key → 卡片上印的字面。
+ *
+ * 卡片右上那行小字是两个词:练习类型(INDEPENDENT / REPLAY / …,由 dx-timer.js 的
+ * PRACTICE_KIND_LABEL 给)+ 判题平台(这一份)。平台词原来是写死在
+ * maimai-glyphs.js 的 problemCard() 里的字符串 'CODEFORCES',以后题目会来自别的判题站,
+ * 所以收到这张表里,全站只有这一处决定卡片印什么。
+ *
+ * key 用的是 `src/fetchers/registry.ts` 里那套平台标识(accounts.platform 用的也是它),
+ * 别另造一套拼法。字面走**全大写拉丁**:底板那一格官方烤的是难度名(BASIC/EXPERT…),
+ * 都是大写拉丁,这里跟着它。注意这和 stat-labels.js 的 platformNames 不是一件事 ——
+ * 那是账号/统计页的中文名(Codeforces、洛谷、码蹄集),卡片要的是板上的短词,
+ * 分开放,谁的措辞要改都不会带偏对方。
+ *
+ * 今天服务端只会发 Codeforces(见 dx-timer.js 里的说明),所以实际渲染仍然是 CODEFORCES;
+ * 以后接别的判题站时,只要那边把 platform 一起发下来、
+ * 调用方把 key 传给 problemCard({ platform }),这张表直接就能用。
+ */
+export const PLATFORM_LABELS = {
+ codeforces: 'CODEFORCES',
+ atcoder: 'ATCODER',
+ leetcode: 'LEETCODE',
+ 'leetcode-cn': 'LEETCODE CN',
+ luogu: 'LUOGU',
+ nowcoder: 'NOWCODER',
+ matiji: 'MATIJI',
+};
+
+/** 没有平台信息时(今天就是这种)卡片印哪一个。 */
+export const DEFAULT_PLATFORM = 'codeforces';
+
+/**
+ * 平台 key → 卡片上的字面。空值、认不出的 key 一律落回 DEFAULT_PLATFORM:
+ * 卡片这一格不能空着。
+ */
+export function platformLabel(platform) {
+ const key = String(platform ?? '').trim().toLowerCase();
+ return PLATFORM_LABELS[key] ?? PLATFORM_LABELS[DEFAULT_PLATFORM];
+}
diff --git a/public/stat-charts.js b/public/stat-charts.js
index 14f7410..3e76789 100644
--- a/public/stat-charts.js
+++ b/public/stat-charts.js
@@ -1,7 +1,15 @@
const colors = ['#5bceac','#62a8ff','#ad93ff','#f2c66d','#f38a96','#68d5e7','#c1cbdd'];
const node = (tag,text,cls) => { const n=document.createElement(tag); if(text)n.textContent=text; if(cls)n.className=cls;return n; };
-const font = 'system-ui, "Microsoft YaHei", sans-serif';
+// 画布里的字也要跟官方那套字体(@font-face 已加载,render 前会 await document.fonts.ready)
+const font = '"M PLUS 1p", "M PLUS 1p Symbols", "Microsoft YaHei", "PingFang SC", sans-serif';
function text(ctx, value, x, y, size=12, color='#a9b8cd',align='left') { ctx.font=`${size}px ${font}`;ctx.fillStyle=color;ctx.textAlign=align;ctx.fillText(String(value),x,y); }
+// 画布上的底色、网格和字号不跟 CSS 变量走,得按主题自己取一套;
+// 切换主题时由 stats-page 重新画一遍(导出图片始终用深色海报底,不受这里影响)
+const PALETTE = {
+ light: { plot:'#f2eefb', grid:'#ded6ee', track:'#e7e1f4', text:'#5a5470', muted:'#7b7490', value:'#2f2a44', stroke:'#00000022' },
+ dark: { plot:'#141e2e', grid:'#27364b', track:'#233147', text:'#a9b8cd', muted:'#8798b2', value:'#e0eafb', stroke:'#ffffff55' },
+};
+const pal = () => PALETTE[document.documentElement.dataset.mmTheme === 'dark' ? 'dark' : 'light'];
function shorten(value,n=24) { return value.length>n ? value.slice(0,n-1)+'…' : value; }
export function chart(title, input, note='', type='bar') {
@@ -11,16 +19,16 @@ export function chart(title, input, note='', type='bar') {
const height=type==='bar' ? Math.max(160,rows.length*29+30) : type==='donut' ? Math.max(250,rows.length*27+40) : 260;
const canvas=node('canvas',null,'stat-canvas');canvas.width=1040;canvas.height=height*2;canvas.setAttribute('role','img');
canvas.setAttribute('aria-label',`${title}。${rows.map(r=>`${r.label}:${r.count}`).join(';')}`);
- const ctx=canvas.getContext('2d');ctx.scale(2,2);ctx.fillStyle='#141e2e';ctx.fillRect(0,0,520,height);
- if (!total) {text(ctx,'此范围暂无数据',260,height/2,15,'#8798b2','center');}
+ const ctx=canvas.getContext('2d');ctx.scale(2,2);const p=pal();ctx.fillStyle=p.plot;ctx.fillRect(0,0,520,height);
+ if (!total) {text(ctx,'此范围暂无数据',260,height/2,15,p.muted,'center');}
else if (type==='donut') {
let angle=-Math.PI/2;
rows.forEach((r,i)=> {const end=angle+r.count/total*Math.PI*2;ctx.beginPath();ctx.arc(115,125,76,angle,end);ctx.strokeStyle=colors[i%colors.length];ctx.lineWidth=24;ctx.stroke();angle=end;
- ctx.fillStyle=colors[i%colors.length];ctx.fillRect(225,22+i*27,9,9);text(ctx,shorten(r.label,23),243,31+i*27);text(ctx,`${r.count} · ${(r.count/total*100).toFixed(1)}%`,505,31+i*27,11,'#e0eafb','right');});
- text(ctx,total,115,127,30,'#e6edf3','center');text(ctx,'次提交',115,151,12,'#8798b2','center');
+ ctx.fillStyle=colors[i%colors.length];ctx.fillRect(225,22+i*27,9,9);text(ctx,shorten(r.label,23),243,31+i*27,12,p.text);text(ctx,`${r.count} · ${(r.count/total*100).toFixed(1)}%`,505,31+i*27,11,p.value,'right');});
+ text(ctx,total,115,127,30,p.value,'center');text(ctx,'次提交',115,151,12,p.muted,'center');
} else if(type==='line' || type==='columns') {
const left=42,right=500,top=28,bottom=210,axisMax=Math.ceil(max/4)*4;
- for(let i=0;i<=4;i++){const y=bottom-(bottom-top)*i/4;ctx.strokeStyle='#27364b';ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(right,y);ctx.stroke();text(ctx,axisMax*i/4,left-8,y+4,10,'#8798b2','right');}
+ for(let i=0;i<=4;i++){const y=bottom-(bottom-top)*i/4;ctx.strokeStyle=p.grid;ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(right,y);ctx.stroke();text(ctx,axisMax*i/4,left-8,y+4,10,p.muted,'right');}
const x=i=>left+(right-left)*(rows.length===1?0.5:i/(rows.length-1));
const y=r=>bottom-r.count/axisMax*(bottom-top);
if(type==='line') {
@@ -28,10 +36,10 @@ export function chart(title, input, note='', type='bar') {
ctx.lineTo(x(rows.length-1),bottom);ctx.lineTo(x(0),bottom);ctx.closePath();const g=ctx.createLinearGradient(0,top,0,bottom);g.addColorStop(0,'#5bceac55');g.addColorStop(1,'#5bceac00');ctx.fillStyle=g;ctx.fill();
rows.forEach((r,i)=>{if(rows.length<=40){ctx.beginPath();ctx.arc(x(i),y(r),3,0,Math.PI*2);ctx.fillStyle=colors[0];ctx.fill();}});
} else rows.forEach((r,i)=>{const w=(right-left)/rows.length;ctx.fillStyle=colors[1];ctx.fillRect(left+i*w+2,y(r),Math.max(1,w-4),bottom-y(r));});
- rows.forEach((r,i)=>{if(i===0||i===rows.length-1||i%Math.max(1,Math.ceil(rows.length/6))===0) text(ctx,r.label.length>7?r.label.slice(5):r.label,type==='line'?x(i):left+(i+.5)*(right-left)/rows.length,233,10,'#8798b2','center');});
- text(ctx,'提交次数',left,14,10);
+ rows.forEach((r,i)=>{if(i===0||i===rows.length-1||i%Math.max(1,Math.ceil(rows.length/6))===0) text(ctx,r.label.length>7?r.label.slice(5):r.label,type==='line'?x(i):left+(i+.5)*(right-left)/rows.length,233,10,p.muted,'center');});
+ text(ctx,'提交次数',left,14,10,p.muted);
} else {
- rows.forEach((r,i)=>{const y=20+i*29; text(ctx,shorten(r.label,25),5,y+12,12);ctx.fillStyle='#233147';ctx.fillRect(195,y,250,12);ctx.fillStyle=r.color||colors[i%colors.length];ctx.fillRect(195,y,250*r.count/max,12);if(r.color){ctx.strokeStyle='#ffffff55';ctx.lineWidth=1;ctx.strokeRect(195,y,250*r.count/max,12);}text(ctx,r.count,508,y+12,12,'#e0eafb','right');});
+ rows.forEach((r,i)=>{const y=20+i*29; text(ctx,shorten(r.label,25),5,y+12,12,p.text);ctx.fillStyle=p.track;ctx.fillRect(195,y,250,12);ctx.fillStyle=r.color||colors[i%colors.length];ctx.fillRect(195,y,250*r.count/max,12);if(r.color){ctx.strokeStyle=p.stroke;ctx.lineWidth=1;ctx.strokeRect(195,y,250*r.count/max,12);}text(ctx,r.count,508,y+12,12,p.value,'right');});
}
const viewport=node('div',null,'stat-canvas-frame');viewport.append(canvas);box.append(viewport);
const detail=node('details',null,'chart-values');detail.append(node('summary','查看完整数据'));
diff --git a/public/stats-page.js b/public/stats-page.js
index c4b2455..6f0cabc 100644
--- a/public/stats-page.js
+++ b/public/stats-page.js
@@ -12,6 +12,9 @@ const heatmap = initHeatmap(document.getElementById('heatmapHost'), syncUrl);
let revision = 0;
let heatRevision = 0;
let detailReady = false, heatReady = false;
+// 图表是画布画的,换主题得按新配色重画一遍(数据没变,直接用上一次的响应)
+let lastAnalytics = null;
+document.addEventListener('mm-theme-change', () => { if (lastAnalytics) analytics.render(lastAnalytics); });
const saveButton = document.getElementById('saveStatsImage');
const updateSave = () => { saveButton.disabled = !detailReady || !heatReady; };
saveButton.addEventListener('click', async () => {
@@ -51,6 +54,7 @@ async function refresh(includeHeat = true, background = false) {
if (platform.value) params.platform = platform.value;
const detail = get('/api/stats', params).then(body => {
if (current !== revision) return;
+ lastAnalytics = body.analytics;
analytics.render(body.analytics);
renderToday(body.today);
detailReady = true; updateSave();
diff --git a/public/stats.html b/public/stats.html
index 173686e..4784c9e 100644
--- a/public/stats.html
+++ b/public/stats.html
@@ -3,19 +3,21 @@
-
+
+
Algorithm DX · 练习数据
-
-
+
+
-
+
+
@@ -40,5 +46,7 @@
+
+
diff --git a/public/theme-dark.css b/public/theme-dark.css
new file mode 100644
index 0000000..a2621fb
--- /dev/null
+++ b/public/theme-dark.css
@@ -0,0 +1,383 @@
+/* Algorithm DX · 深色模式
+ *
+ * 只在 时生效,作为 theme.css 之后的一层「同结构换材质」:
+ * 选择器都带 [data-mm-theme="dark"] 前缀,权重高于 theme.css 的 body.mm-theme 规则,
+ * 所以浅色那一套一个字节都不用改。
+ *
+ * 背景同样是官方那份动态背景,只是换成夜版贴图:theme.js 给 iframe 发
+ * postMessage({mmTheme:'dark'}),bg.html 挂上 theme-night.css(见那两个文件)。
+ *
+ * 底色沿用夜景:深靛蓝面板 + 官方那套紫/粉横条与 14px 硬阴影;卡片、输入框、气泡、
+ * 表格、说明卡全部转深色,正文色跟着 token 一起翻。
+ *
+ * 不翻的两类「成品屏」:段位卡/题库卡的卡面(styles.css 里写死的深色渐变)和点开后的
+ * 成绩单(.dx-result / .dan-result-*)—— 它们本来就是一张画好的屏,明暗两版都保留自己的脸,
+ * 翻成夜版反而两不像;外壳(弹窗遮罩、关闭钮)照常跟主题。
+ */
+
+/* ============ 1. 调色 ============ */
+
+[data-mm-theme="dark"] {
+ color-scheme: dark;
+
+ /* 夜景里直接用的几档色(与 assets/night/sky.png 同源) */
+ --mm-night: #101538;
+ --mm-night-plate: #1b2148; /* 顶栏胶囊 */
+ --mm-night-surface: #191e42; /* 大盒子 */
+ --mm-night-surface-2: #1d2348; /* 盒子里的块、输入框 */
+ --mm-night-surface-3: #232a55; /* 再抬一层:hover、选中 */
+ --mm-night-line: rgba(255, 255, 255, .12);
+ --mm-night-line-soft: rgba(255, 255, 255, .08);
+ --mm-night-ink: #eceefb;
+ --mm-night-dim: #c6cbe8;
+ --mm-night-muted: #9aa0c4;
+
+ /* 官方调色在深色底上的几处调整 */
+ --mm-navy: #e3e7ff; /* 这个 token 在本站当「标题色」用 */
+ --mm-ink: #eceefb;
+ --mm-grey: rgba(255, 255, 255, .18);
+ --mm-purple-tint: #231f4c;
+ --mm-purple-deep: #d3a1ff;
+ /* 只改硬阴影的颜色,偏移留给 theme.css 里那三档 vw 阶梯 */
+ --mm-card-shadow-color: rgba(0, 0, 0, .45);
+ --mm-brand-mark: url(/algorithm-dx-icon.svg);
+ --mm-nav-shadow: 10px 10px 0 rgba(0, 0, 0, .45);
+ --mm-soft: 0 10px 26px -14px rgba(0, 0, 0, .65);
+ --mm-gradient: linear-gradient(0deg, #1a1f45 6%, #1d2150 16%, #262a63 30%, #332c72 46%,
+ #402f80 56%, #453a8f 67%, #2a3a74 85%, #151f47 95%);
+
+ /* 仓库既有 token 一并翻过来(styles.css:131-154 那套深色语义在这里复活) */
+ --bg: var(--mm-night);
+ --bg-soft: #141a3c;
+ --panel: var(--mm-night-surface);
+ --panel-2: var(--mm-night-surface-2);
+ --border: var(--mm-night-line);
+ --border-soft: var(--mm-night-line-soft);
+ --text: var(--mm-night-ink);
+ --text-dim: var(--mm-night-dim);
+ --muted: var(--mm-night-muted);
+ --accent: var(--mm-purple-soft);
+ --accent-soft: rgba(186, 94, 234, .18);
+ --ac: #58e08a;
+ --ac-soft: rgba(75, 224, 112, .16);
+ --warn: #ffc266;
+ --warn-soft: rgba(255, 151, 42, .16);
+ --bad: #ff7d92;
+ --bad-soft: rgba(231, 33, 57, .18);
+ --shadow: 8px 8px 0 0 rgba(0, 0, 0, .45);
+ --surface-gradient: var(--mm-night-surface-2);
+ --focus-ring: var(--mm-purple-soft);
+}
+
+html[data-mm-theme="dark"],
+[data-mm-theme="dark"] body { background: var(--mm-night); }
+
+[data-mm-theme="dark"] body.mm-theme {
+ background: var(--mm-night);
+ background-image: var(--mm-gradient);
+ color: var(--mm-night-ink);
+}
+
+[data-mm-theme="dark"] body.mm-theme ::selection { background: var(--mm-purple); color: #fff; }
+
+/* ============ 2. 顶栏与开关 ============ */
+
+[data-mm-theme="dark"] body.mm-theme .site-header {
+ background: var(--mm-night-plate);
+ border-color: var(--mm-night-line-soft);
+ box-shadow: var(--mm-nav-shadow);
+}
+[data-mm-theme="dark"] body.mm-theme .site-header .brand,
+[data-mm-theme="dark"] body.mm-theme .site-header .brand-text h1 { color: var(--mm-night-ink); }
+[data-mm-theme="dark"] body.mm-theme .site-header .nav .btn { color: var(--mm-night-dim); }
+[data-mm-theme="dark"] body.mm-theme .site-header .nav .btn:hover { color: var(--mm-purple-soft); }
+[data-mm-theme="dark"] body.mm-theme .site-header .global-sync-bar {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line-soft);
+}
+
+[data-mm-theme="dark"] .mm-menu-btn { background: var(--mm-night-plate); }
+[data-mm-theme="dark"] .mm-menu-btn span { background: var(--mm-pink); }
+[data-mm-theme="dark"] body.mm-menu-open .mm-menu-btn { background: var(--mm-purple); border-color: var(--mm-purple); }
+[data-mm-theme="dark"] body.mm-menu-open .mm-menu-btn span { background: #fff; }
+@media (max-width: 1000px) {
+ [data-mm-theme="dark"] body.mm-theme .site-header .nav { background: var(--mm-night); }
+ [data-mm-theme="dark"] body.mm-theme .site-header .nav .btn:not(:last-child) { border-bottom-color: var(--mm-night-line); }
+}
+
+/* 开关本身:深色下换成太阳,底色转深 */
+[data-mm-theme="dark"] body.mm-theme .mm-theme-btn {
+ border-color: var(--mm-purple-soft);
+ background: var(--mm-night-surface-3);
+ box-shadow: 4px 4px 0 rgba(0, 0, 0, .45);
+}
+[data-mm-theme="dark"] body.mm-theme .mm-theme-btn__moon { opacity: 0; transform: scale(.4) rotate(90deg); }
+[data-mm-theme="dark"] body.mm-theme .mm-theme-btn__sun { opacity: 1; transform: none; }
+[data-mm-theme="dark"] body.mm-theme .mm-theme-btn:hover { background: var(--mm-purple); }
+[data-mm-theme="dark"] body.mm-theme .mm-theme-btn:hover .mm-theme-btn__sun { fill: #fff; }
+
+/* ============ 3. 文字:白色描边换成暗色投影 ============ */
+
+[data-mm-theme="dark"] body.mm-theme .ac-page-heading h2,
+[data-mm-theme="dark"] body.mm-theme .stats-toolbar h2,
+[data-mm-theme="dark"] body.mm-theme .circle-hero h2,
+[data-mm-theme="dark"] body.mm-theme .dan-hero h2,
+[data-mm-theme="dark"] body.mm-theme .help-heading h2,
+[data-mm-theme="dark"] body.mm-theme .dx-page-intro h2,
+[data-mm-theme="dark"] body.mm-theme .circle-feed-head h2,
+[data-mm-theme="dark"] body.mm-theme .today-section-title {
+ color: var(--mm-navy);
+ text-shadow: 0 2px 8px rgba(0, 0, 0, .55);
+}
+[data-mm-theme="dark"] body.mm-theme .analytics-note,
+[data-mm-theme="dark"] body.mm-theme #message,
+[data-mm-theme="dark"] body.mm-theme .dan-msg,
+[data-mm-theme="dark"] body.mm-theme .dan-disclaimer,
+[data-mm-theme="dark"] body.mm-theme .circle-note,
+[data-mm-theme="dark"] body.mm-theme .pager-hint {
+ color: var(--mm-night-dim);
+ text-shadow: 0 1px 4px rgba(0, 0, 0, .6);
+}
+[data-mm-theme="dark"] body.mm-theme .help-page > p {
+ color: var(--mm-night-dim);
+ text-shadow: 0 1px 4px rgba(0, 0, 0, .6);
+}
+[data-mm-theme="dark"] body.mm-theme .help-page .help-heading p,
+[data-mm-theme="dark"] body.mm-theme .help-page > section p,
+[data-mm-theme="dark"] body.mm-theme .help-page > section li,
+[data-mm-theme="dark"] body.mm-theme .help-page details p { color: var(--mm-night-dim); }
+[data-mm-theme="dark"] body.mm-theme .circle-hero p,
+[data-mm-theme="dark"] body.mm-theme .ac-page-heading p,
+[data-mm-theme="dark"] body.mm-theme .stats-toolbar p,
+[data-mm-theme="dark"] body.mm-theme .dx-page-intro p { color: var(--mm-night-muted); }
+/* 数据条里两个写死的深色数字(§9) */
+[data-mm-theme="dark"] body.mm-theme .stat.is-ac .stat-value { color: #5fe08f; }
+[data-mm-theme="dark"] body.mm-theme .stat.is-unac .stat-value { color: #ffc266; }
+
+/* ============ 4. 玻璃板(页头/工具条/侧栏这些浮在背景上的块) ============ */
+
+[data-mm-theme="dark"] body.mm-theme .ac-page-heading,
+[data-mm-theme="dark"] body.mm-theme .stats-toolbar,
+[data-mm-theme="dark"] body.mm-theme .circle-hero,
+[data-mm-theme="dark"] body.mm-theme .dan-hero,
+[data-mm-theme="dark"] body.mm-theme .help-heading,
+[data-mm-theme="dark"] body.mm-theme .help-toc,
+[data-mm-theme="dark"] body.mm-theme .pager,
+[data-mm-theme="dark"] body.mm-theme .dx-page-intro,
+[data-mm-theme="dark"] body.mm-theme .circle-feed-head {
+ background: rgba(19, 24, 52, .88);
+ border: 1px solid var(--mm-night-line);
+ box-shadow: 0 18px 40px -30px rgba(0, 0, 0, .8);
+}
+@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
+ [data-mm-theme="dark"] body.mm-theme .ac-page-heading,
+ [data-mm-theme="dark"] body.mm-theme .stats-toolbar,
+ [data-mm-theme="dark"] body.mm-theme .circle-hero,
+ [data-mm-theme="dark"] body.mm-theme .dan-hero,
+ [data-mm-theme="dark"] body.mm-theme .help-heading,
+ [data-mm-theme="dark"] body.mm-theme .help-toc,
+ [data-mm-theme="dark"] body.mm-theme .pager,
+ [data-mm-theme="dark"] body.mm-theme .dx-page-intro,
+ [data-mm-theme="dark"] body.mm-theme .circle-feed-head {
+ background: rgba(19, 24, 52, .88);
+ -webkit-backdrop-filter: blur(12px) saturate(1.2);
+ backdrop-filter: blur(12px) saturate(1.2);
+ }
+}
+
+/* 落在背景花纹上的那几处小字:浅色层给的是白底板,这里换成夜色底板 */
+[data-mm-theme="dark"] body.mm-theme .app > .analytics-note,
+[data-mm-theme="dark"] body.mm-theme .app > #message,
+[data-mm-theme="dark"] body.mm-theme .app > .dan-msg,
+[data-mm-theme="dark"] body.mm-theme .circle-main > #message,
+[data-mm-theme="dark"] body.mm-theme .heatmap-top > .analytics-note,
+[data-mm-theme="dark"] body.mm-theme .help-page > p {
+ background: var(--mm-night-surface-2);
+ box-shadow: 0 8px 20px -14px rgba(0, 0, 0, .9);
+ text-shadow: none;
+}
+
+/* ============ 5. 官方盒子与通用块 ============ */
+
+[data-mm-theme="dark"] body.mm-theme .filters,
+[data-mm-theme="dark"] body.mm-theme .admin,
+[data-mm-theme="dark"] body.mm-theme .foot,
+[data-mm-theme="dark"] body.mm-theme .section,
+[data-mm-theme="dark"] body.mm-theme .stat-row,
+[data-mm-theme="dark"] body.mm-theme .ac-platform-strip,
+[data-mm-theme="dark"] body.mm-theme .placeholder,
+[data-mm-theme="dark"] body.mm-theme .empty,
+[data-mm-theme="dark"] body.mm-theme.ac-page .filters,
+[data-mm-theme="dark"] body.mm-theme .analytics,
+[data-mm-theme="dark"] body.mm-theme .today-panel,
+[data-mm-theme="dark"] body.mm-theme .dx-timer,
+[data-mm-theme="dark"] body.mm-theme .circle-sidebar,
+[data-mm-theme="dark"] body.mm-theme .circle-filters,
+[data-mm-theme="dark"] body.mm-theme .dx-rules {
+ background: var(--mm-night-surface);
+ color: var(--mm-night-ink);
+}
+[data-mm-theme="dark"] body.mm-theme .analytics-chart,
+[data-mm-theme="dark"] body.mm-theme .today-card,
+[data-mm-theme="dark"] body.mm-theme .dx-pages,
+[data-mm-theme="dark"] body.mm-theme .dx-records-filters,
+[data-mm-theme="dark"] body.mm-theme .circle-card,
+[data-mm-theme="dark"] body.mm-theme .help-page > section {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line-soft);
+ color: var(--mm-night-ink);
+}
+/* 这块左侧有官方绿条,别被上面的 border-color 一起抹掉 */
+[data-mm-theme="dark"] body.mm-theme .analytics-metrics > div {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line-soft);
+ border-left: 4px solid var(--mm-green);
+ color: var(--mm-night-ink);
+}
+/* 空格子原来和面板几乎同色(#1b2145 对 #191e42),整年没练的日子直接看不见,
+ 图例最短的一格也是空的:抬亮一点,靠白描边分层 */
+[data-mm-theme="dark"] body.mm-theme .heat-cell { border-color: rgba(255, 255, 255, .14); }
+[data-mm-theme="dark"] body.mm-theme .level-0 { background: rgba(255, 255, 255, .09); }
+[data-mm-theme="dark"] body.mm-theme .level-1 { background: #1d4a37; }
+[data-mm-theme="dark"] body.mm-theme .level-2 { background: #256f4a; }
+[data-mm-theme="dark"] body.mm-theme .level-3 { background: #2f9d62; }
+[data-mm-theme="dark"] body.mm-theme .level-4 { background: var(--mm-green); }
+
+[data-mm-theme="dark"] body.mm-theme .foot-note { color: var(--mm-night-muted); }
+[data-mm-theme="dark"] body.mm-theme.ac-page .card:hover { background: var(--mm-night-surface-3); }
+[data-mm-theme="dark"] body.mm-theme .ac-page .card-stats .k { color: var(--mm-night-muted); }
+[data-mm-theme="dark"] body.mm-theme .ac-page .card-stats .v { color: var(--mm-night-ink); }
+[data-mm-theme="dark"] body.mm-theme .dx-card.is-empty { color: var(--mm-night-muted); }
+[data-mm-theme="dark"] body.mm-theme .dx-more a {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: var(--mm-navy);
+}
+[data-mm-theme="dark"] body.mm-theme .dx-ranked,
+[data-mm-theme="dark"] body.mm-theme .dx-rank,
+[data-mm-theme="dark"] body.mm-theme .today-total { color: #7ef0a4; }
+
+/* ============ 6. 控件:输入、按钮、气泡、表格、弹窗 ============ */
+
+[data-mm-theme="dark"] body.mm-theme .input,
+[data-mm-theme="dark"] body.mm-theme .select {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: var(--mm-night-ink);
+ color-scheme: dark;
+}
+[data-mm-theme="dark"] body.mm-theme .input::placeholder { color: #8f94b4; }
+[data-mm-theme="dark"] body.mm-theme .analytics-controls .input { color: var(--mm-night-ink); color-scheme: dark; }
+[data-mm-theme="dark"] body.mm-theme .btn-ghost {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: var(--mm-night-dim);
+}
+[data-mm-theme="dark"] body.mm-theme .btn-ghost:hover { background: var(--mm-night-surface-3); color: var(--mm-navy); }
+[data-mm-theme="dark"] body.mm-theme .chip {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: var(--mm-night-dim);
+}
+/* 浅色里这组是「淡紫底 + 白药丸」,深色照搬会变成「紫底 + 深蓝药丸」,看着像反的。
+ 夜里的对应关系是「比盒子更深一层的底 + 比底更亮的药丸」 */
+[data-mm-theme="dark"] body.mm-theme .tabs { background: #151a3c; }
+[data-mm-theme="dark"] body.mm-theme .tab.is-active { background: #262d5c; color: var(--mm-navy); box-shadow: 0 2px 0 rgba(0, 0, 0, .35); }
+[data-mm-theme="dark"] body.mm-theme .db-chip {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: #a8e6c8;
+}
+[data-mm-theme="dark"] body.mm-theme .ac-page .db-chip { color: #a8e6c8; border-color: var(--mm-night-line); }
+[data-mm-theme="dark"] body.mm-theme .expand-btn {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: var(--mm-night-dim);
+}
+[data-mm-theme="dark"] body.mm-theme .rt-table { background: var(--mm-night-surface-2); }
+[data-mm-theme="dark"] body.mm-theme .rt-table th { background: var(--mm-purple-tint); color: var(--mm-navy); }
+[data-mm-theme="dark"] body.mm-theme .rt-table td { border-color: var(--mm-night-line-soft); }
+[data-mm-theme="dark"] body.mm-theme .modal-card,
+[data-mm-theme="dark"] body.mm-theme dialog {
+ background: var(--mm-night-surface);
+ border-color: var(--mm-night-line);
+ color: var(--mm-night-ink);
+}
+[data-mm-theme="dark"] body.mm-theme dialog::backdrop { background: rgba(6, 9, 26, .62); }
+[data-mm-theme="dark"] body.mm-theme .account-owner { background: var(--mm-night-surface-2); }
+[data-mm-theme="dark"] body.mm-theme .account-owner-content { background: #1c224a; }
+[data-mm-theme="dark"] body.mm-theme .help-inline { background: var(--mm-night-surface-2); border-color: var(--mm-night-line-soft); }
+[data-mm-theme="dark"] body.mm-theme .help-inline summary { color: var(--mm-navy); }
+[data-mm-theme="dark"] body.mm-theme .account-owner-avatar { background: var(--mm-purple-tint); color: var(--mm-purple-soft); }
+
+/* 气泡:底色转深,字色跟着暗色不刺眼 */
+[data-mm-theme="dark"] body.mm-theme .badge,
+[data-mm-theme="dark"] body.mm-theme .ac-card-tags .badge { background: rgba(255, 255, 255, .1); color: #cdd3f2; border-color: transparent; }
+[data-mm-theme="dark"] body.mm-theme .badge.ac { background: rgba(75, 224, 112, .16); color: #7ef0a4; }
+[data-mm-theme="dark"] body.mm-theme .badge.warn { background: rgba(255, 151, 42, .16); color: #ffc266; }
+[data-mm-theme="dark"] body.mm-theme .badge.bad { background: rgba(231, 33, 57, .18); color: #ff93a5; }
+[data-mm-theme="dark"] body.mm-theme .badge.diff { background: rgba(53, 132, 254, .18); color: #9cc6ff; }
+[data-mm-theme="dark"] body.mm-theme .badge.contest { background: rgba(186, 94, 234, .18); color: #d7aeff; }
+[data-mm-theme="dark"] body.mm-theme .badge.platform { background: rgba(2, 200, 211, .16); color: #7fe6ee; }
+[data-mm-theme="dark"] body.mm-theme .badge.score { background: rgba(255, 151, 42, .16); color: #ffc266; }
+
+/* 提示条:浅色那版是实心暖白底,所以深色也必须是实心底 —— 只铺一层 10% 的色纱的话,
+ 夜里的云和彩虹会直接透到字缝里(这块正好压在彩虹上)。色纱按同一个比例调进实心底里,
+ 颜色关系不变,字下面有底。 */
+[data-mm-theme="dark"] body.mm-theme .notice { background: #2e283c; color: #f0dcb4; }
+[data-mm-theme="dark"] body.mm-theme .notice strong { color: #ffd79a; }
+[data-mm-theme="dark"] body.mm-theme .notice-warn { background: #2e283c; color: #ecd9a4; }
+[data-mm-theme="dark"] body.mm-theme .notice-warn strong { color: #f6dfa8; }
+[data-mm-theme="dark"] body.mm-theme .notice-warn span { color: #d9c48d; }
+[data-mm-theme="dark"] body.mm-theme .notice-ac { background: #1c2f43; color: #b6e3bc; }
+[data-mm-theme="dark"] body.mm-theme .notice-ac strong { color: #d6f0da; }
+[data-mm-theme="dark"] body.mm-theme .notice-ac.is-bad,
+[data-mm-theme="dark"] body.mm-theme .notice-danger { background: #301c3d; color: #ffb9b4; }
+[data-mm-theme="dark"] body.mm-theme .notice-ac.is-bad strong,
+[data-mm-theme="dark"] body.mm-theme .notice-danger strong { color: #ff9d97; }
+[data-mm-theme="dark"] body.mm-theme .notice-neutral { background: #25294a; color: var(--mm-night-muted); }
+
+/* ============ 7. 题友圈 ============ */
+
+[data-mm-theme="dark"] body.mm-theme .circle-eyebrow { color: var(--mm-purple-soft); }
+[data-mm-theme="dark"] body.mm-theme .circle-person:hover { background: var(--mm-night-surface-3); }
+[data-mm-theme="dark"] body.mm-theme .circle-person[aria-pressed="true"] {
+ background: rgba(186, 94, 234, .22);
+ border-color: var(--mm-purple-soft);
+ color: #e3c6ff;
+}
+[data-mm-theme="dark"] body.mm-theme .circle-avatar {
+ background: hsl(var(--hue, 160) 34% 26%);
+ color: hsl(var(--hue, 160) 70% 84%);
+}
+[data-mm-theme="dark"] body.mm-theme .circle-result { background: rgba(255, 255, 255, .08); color: #c3c9e6; }
+[data-mm-theme="dark"] body.mm-theme .circle-result.ac { background: rgba(75, 224, 112, .14); color: #7ef0a4; }
+[data-mm-theme="dark"] body.mm-theme .circle-coverage-status.needs-attention {
+ background: rgba(255, 151, 42, .14);
+ color: #ffc266;
+}
+[data-mm-theme="dark"] body.mm-theme .circle-card-head,
+[data-mm-theme="dark"] body.mm-theme .circle-card-foot { border-color: var(--mm-night-line-soft); }
+
+/* ============ 8. 使用指南 ============ */
+
+[data-mm-theme="dark"] body.mm-theme .help-page .help-toc a {
+ background: var(--mm-night-surface-2);
+ border-color: var(--mm-night-line);
+ color: var(--mm-navy);
+}
+[data-mm-theme="dark"] body.mm-theme .help-page .help-toc a:hover {
+ background: var(--mm-purple-tint);
+ border-color: var(--mm-purple-soft);
+ color: #e3c6ff;
+}
+[data-mm-theme="dark"] body.mm-theme kbd {
+ background: var(--mm-purple-tint);
+ border-color: var(--mm-purple-soft);
+ color: var(--mm-navy);
+}
+
+/* ============ 9. 底图之外还需要单独一提的两处 ============ */
+
+/* 背景 iframe 之上、正文之下的兜底层,深色下别闪白 */
+[data-mm-theme="dark"] body.mm-theme .app { position: relative; }
\ No newline at end of file
diff --git a/public/theme-night.css b/public/theme-night.css
new file mode 100644
index 0000000..dfe6618
--- /dev/null
+++ b/public/theme-night.css
@@ -0,0 +1,81 @@
+/* ============ 夜版背景 ============
+ *
+ * 结构、层级、尺寸、动画全部沿用官方 style.css,这里只做一件事:换贴图素材
+ * (全部在 /assets/night/,由官方素材重上色生成,官方文件一个没动)。
+ *
+ * 用哪一层:
+ * sky / sky-frame 天空渐变(内层渐变块 / 圆角外那圈)
+ * pattern 渐变上的图案叠加
+ * shines 闪点(官方在 ≥769px 换成 PC 那张,这里跟着换)
+ * cloud-* 六片云(白体换成夜蓝,原来的粉/橙暖色描边按原色留着)
+ * rainbow-back 彩虹后面那片云
+ * rainbow-bottom 彩虹两端那两片云(同样分 SP / PC 两份)
+ * rainbow 彩虹(降成月虹)
+ * aurora 曙光(配合 screen 叠加 + 模糊化成柔光)
+ * 月亮和流星保持官方素材:夜里它们本来就该是最亮的。
+ *
+ * **带 -sp / -pc 的素材必须跟着官方的断点走**:官方在基础规则里用 `_sp`、在
+ * `@media (min-width:769px)` 里换成 `_pc`。只挂一份的话,宽屏会拿手机那张顶 PC 那格 ——
+ * 彩虹两端那片云的 SP 版是被图片边界切平的窄裁版(左右两边 85% / 44% 的像素是实心的),
+ * 铺进 PC 那格就会在画面中间露出两条平直的切边。下面每个断点都跟官方一一对应。
+ *
+ * 换素材之外还有四处按夜版需要动了样式,都在下面并注明原因:
+ * html/body 底色、彩虹底边淡出、曙光的叠加方式与柔光、月亮那圈月光。
+ */
+
+/* iframe 底色(渐变块圆角之外露出的那一圈由 .bg 的贴图负责) */
+html, body { background: #0e1234; }
+
+/* —— 天空 —— */
+.bg { background-image: url(/assets/night/sky-frame.png); }
+.bg--gradient { background-image: url(/assets/night/sky.png); }
+.bg--gradient:before { background-image: url(/assets/night/pattern.png); }
+
+/* —— 手机档(官方基础规则那套素材)—— */
+.bg--shine { background-image: url(/assets/night/shines-sp.png); }
+.bg--aurora,
+.bg--aurora:before,
+.bg--aurora:after { background-image: url(/assets/night/aurora-sp.png); }
+.bg--rainbow:before { background-image: url(/assets/night/rainbow-sp.png); }
+.bg--rainbow__cloud { background-image: url(/assets/night/rainbow-bottom-sp.png); }
+.bg--rainbow__cloud:after { background-image: url(/assets/night/rainbow-back-sp.png); }
+
+/* —— 云(两张云官方不分档,同一张素材)—— */
+.bg--cloud__item.i1 { background-image: url(/assets/night/cloud-back-l.png); }
+.bg--cloud__item.i2 { background-image: url(/assets/night/cloud-back-r.png); }
+.bg--cloud__item.i3 { background-image: url(/assets/night/cloud-back-c.png); }
+.bg--cloud__item.i4 { background-image: url(/assets/night/cloud-front-l.png); }
+.bg--cloud__item.i5 { background-image: url(/assets/night/cloud-front-r.png); }
+.bg--cloud__item.i6 { background-image: url(/assets/night/cloud-front-c.png); }
+
+/* —— 宽屏档:官方在这里把闪点、曙光、彩虹、彩虹两端那片云都换成 PC 素材 ——
+ 曙光的 :before/:after 官方在宽屏没有换图(还是 SP 那张),所以它们只在上面那一档里换。 */
+@media only screen and (min-width:769px) {
+ .bg--shine { background-image: url(/assets/night/shines-pc.png); }
+ .bg--aurora { background-image: url(/assets/night/aurora-pc.png); }
+ .bg--rainbow:before { background-image: url(/assets/night/rainbow-pc.png); }
+ .bg--rainbow__cloud { background-image: url(/assets/night/rainbow-bottom-pc.png); }
+}
+
+/* —— 彩虹:弧贴近地平线处淡出 ——
+ 官方那张彩虹贴图的两个「脚」是被图片边界切平的。白天那里正好是渐变底部的白色地平光,
+ 切边看不出来;夜里背景压深之后,平直的切边就露出来了,所以让弧在贴近地平线处淡出。
+ 两端那片云的底边同理,那一刀直接做在 rainbow-bottom-pc 素材里(见生成脚本)。 */
+.bg--rainbow:before {
+ -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 13%);
+ mask-image: linear-gradient(0deg, transparent 0, #000 13%);
+}
+
+/* —— 曙光:换素材之外还要叠加方式,这两行没法省 ——
+ 官方那几道竖向光幕在深色天空上会显出硬边方块,所以夜里用 screen 只加光、再模糊成柔光;
+ 贴图两端本来是硬接缝(官方自己在浅色天空上被背景亮度盖住了),夜版素材两端各淡了 4%。 */
+.bg--aurora,
+.bg--aurora:before,
+.bg--aurora:after {
+ mix-blend-mode: screen;
+ filter: blur(1.1vw) saturate(1.2) brightness(.95);
+}
+.bg--aurora { opacity: .32; }
+
+/* —— 月亮:素材保持官方,这里只加一圈月光(夜里唯一需要「加光」的地方)—— */
+.bg--moon { filter: drop-shadow(0 0 2.6vw rgba(168, 205, 255, .5)); }
\ No newline at end of file
diff --git a/public/theme.css b/public/theme.css
new file mode 100644
index 0000000..50582a7
--- /dev/null
+++ b/public/theme.css
@@ -0,0 +1,1482 @@
+/* Algorithm DX · 前端皮肤层:maimai DX 国际版官网(PRiSM PLUS 时期)
+ *
+ * 参数来源:`public/vendor/maimai/style.css`(2025-08-04 归档快照里的
+ * `https://maimai.sega.com/assets/css/style.css`)。每条关键数值后面标了官方选择器,
+ * 以后要调整就照着官方那行改。
+ *
+ * 载入顺序:fonts.css → styles.css → 各页自己的 CSS(circle/dx-result/dan…)→ ui.css
+ * → theme.css → theme-dark.css。所以在同权重下本文件覆盖前面几个;
+ * 少数带 #id 或 .ac-page 前缀的旧规则用 `body.mm-theme` 提权重压过去。
+ *
+ * 深色模式是叠加在后面的 theme-dark.css:里面的规则都带 [data-mm-theme="dark"] 前缀,
+ * 只覆盖材质与配色,改浅色那套不用动这个文件。
+ *
+ * 背景整页复用官网那份动态背景,见 public/bg.html。
+ *
+ * 与官方的八处有意偏差,见文件末尾「与官方的偏差」。
+ */
+
+/* ============ 1. 调色与尺度(数值全部取自官方) ============ */
+
+:root {
+ color-scheme: light;
+ font-synthesis: none;
+
+ --mm-purple: #ba5eea; /* .common--box:before、.header--nav__link.on::before */
+ --mm-purple-deep: #a051dc;
+ --mm-purple-soft: #d3a1ff;
+ --mm-purple-tint: #f6ecff; /* 由主紫派生的浅底,官方只有纯色,用于表格/内嵌块 */
+ --mm-pink: #ffcedf; /* .common--box:after */
+ --mm-pink-soft: #ffd5cf; /* .bg--gradient */
+ --mm-pink-hot: #ff699d; /* .header--nav__language__link:hover */
+ --mm-magenta: #dc39b8;
+ --mm-red: #e72139; /* .common--btn */
+ --mm-red-dark: #a51426; /* .common--btn 的 3D 底影 */
+ --mm-navy: #0b3871; /* .footer--bottom color、导航字形 fill */
+ --mm-ink: #000; /* body,html color */
+ --mm-grey: #e5e5e5; /* .header--nav__item:not(:last-child)::after */
+ --mm-green: #4be070;
+ --mm-cyan: #02c8d3;
+ --mm-blue: #3584fe;
+ --mm-orange: #ff972a;
+ --mm-yellow: #f8b73d;
+
+ /* 背景渐变:官方 .bg--gradient 的 linear-gradient(0deg,#fff 14%,…) 原样搬过来 */
+ --mm-gradient: linear-gradient(0deg, #fff 14%, #ffd5cf 24%, #ffd5cf 46%, #ffc5d5 56%,
+ #eaabff 67%, #72bcfe 85%, #65f2df 95%);
+
+ /* 卡片:官方 .common--box 是三档 vw 阶梯,下面按档照搬(默认这档就是官方 @min-width:1100px
+ 的 900px / 30px / 14px 14px 0 / 600×8),另两档写在后面的媒体查询里 */
+ --mm-card-radius: 30px;
+ /* 阴影拆成「偏移」与「颜色」两段:窄屏只改偏移(官方那三档),深色只改颜色 */
+ --mm-card-shadow-x: 14px;
+ --mm-card-shadow-color: rgba(0, 0, 0, .2);
+ --mm-card-shadow: var(--mm-card-shadow-x) var(--mm-card-shadow-x) 0 0 var(--mm-card-shadow-color);
+ --mm-bar-w: min(66.6667%, 600px);
+ --mm-bar-h: 8px;
+
+ /* 品牌图标:浅色用浅色版,深色版在 theme-dark.css 里换掉(首帧前由 上的主题决定,
+ 所以不会先亮后暗) */
+ --mm-brand-mark: url(/algorithm-dx-icon-light.svg);
+
+ /* 顶栏胶囊:官方 .header--nav @min-width:1400px → 1200×100 / 50px / 10px 10px 0 */
+ --mm-nav-h: clamp(64px, 7.1429vw, 100px);
+ --mm-nav-radius: 50px;
+ --mm-nav-shadow: 10px 10px 0 rgba(0, 0, 0, .2);
+ /* 版心:官方顶栏 1200px、内容盒 900px。顶栏保持 1200,正文收窄一点(见「偏差」) */
+ --mm-shell: min(1200px, 91.4286vw);
+ --mm-content: min(1080px, 91.4286vw);
+ /* 区块之间的呼吸:官方盒子内部 padding 是 50px,这里按同一量级放宽 */
+ --mm-gap: clamp(26px, 2.8vw, 44px);
+ --mm-pad: clamp(20px, 2.8vw, 40px);
+ /* 缓动:UI 交互统一用一条强 ease-out(技能库给的曲线) */
+ --mm-ease-out: cubic-bezier(.23, 1, .32, 1);
+
+ /* 字体:官方 `body,html{font-family:"M PLUS 1p",sans-serif}` */
+ --mm-sans: "M PLUS 1p Symbols", "M PLUS 1p", "Segoe UI", "Hiragino Kaku Gothic Pro", "Yu Gothic",
+ "Microsoft YaHei", "PingFang SC", "Noto Sans SC", sans-serif;
+ --mm-mono: "M PLUS 1p Symbols", "M PLUS 1p", ui-monospace, SFMono-Regular, Consolas, monospace;
+
+ /* —— 仓库既有 token 的重映射:暗色 → 官方亮色(styles.css:131-154、ui.css:2-6) —— */
+ --bg: #fff;
+ --bg-soft: #fffafd;
+ --panel: #fff;
+ --panel-2: #fbf3ff;
+ --border: var(--mm-grey);
+ --border-soft: #efe7f7;
+ --text: var(--mm-ink);
+ --text-dim: var(--mm-navy);
+ --muted: #6f6a7a;
+ --accent: var(--mm-purple);
+ --accent-soft: #f3e6ff;
+ --ac: #16883f;
+ --ac-soft: #e6fbe9;
+ --warn: #a35c00;
+ --warn-soft: #fff3e2;
+ --bad: #b3122a;
+ --bad-soft: #ffe9ec;
+ --radius: var(--mm-card-radius);
+ --radius-sm: 16px;
+ --shadow: 8px 8px 0 0 rgba(0, 0, 0, .2);
+ /* 小东西(行、内嵌块)用柔和投影,硬阴影只留给官方大盒子 */
+ --mm-soft: 0 10px 26px -14px rgba(11, 56, 113, .35);
+ --surface-gradient: #fff;
+ --focus-ring: var(--mm-purple);
+ --sans: var(--mm-sans);
+ --mono: var(--mm-mono);
+}
+
+/* —— 官方 .common--box 的另外两档(原样取自 style.css,数值一个没改)——
+ :root 里那档是 ≥1100px;下面两档在窄屏把圆角、硬阴影与横条一起缩小。
+ 少了它们,手机上会一直用 30px 圆角 + 14px 阴影,比官方重得多。 */
+@media only screen and (max-width: 768.98px) {
+ :root {
+ --mm-card-radius: 3.75vw;
+ --mm-card-shadow-x: 1.75vw;
+ --mm-bar-w: 75vw;
+ --mm-bar-h: 1.5vw;
+ }
+}
+@media only screen and (min-width: 769px) and (max-width: 1099.98px) {
+ :root {
+ --mm-card-radius: 2.7272727273vw;
+ --mm-card-shadow-x: 1.2727272727vw;
+ --mm-bar-w: 54.5454545455vw;
+ --mm-bar-h: .7272727273vw;
+ }
+}
+
+/* ============ 2. 全局 ============ */
+
+html, body { background: #fff; }
+
+body.mm-theme {
+ background: #fff;
+ color: var(--mm-ink);
+ font-family: var(--mm-sans);
+ /* 官方 body 是 line-height:1(lib.css 的重置),这里保留可读行高(见「偏差」) */
+ overflow-x: hidden;
+}
+
+body.mm-theme a { color: var(--mm-purple-deep); }
+body.mm-theme a:hover { color: var(--mm-pink-hot); }
+
+body.mm-theme ::selection { background: var(--mm-purple-soft); color: var(--mm-navy); }
+
+/* 背景:官网那份动态背景整页复用(./bg.html),
+ 官方 CSS 里的全局选择器关在 iframe 里,不会碰到本站布局。
+ body 自己只留一层兜底渐变,iframe 没加载出来时不至于白屏。 */
+body.mm-theme {
+ background-image: var(--mm-gradient);
+ background-attachment: fixed;
+ background-size: cover;
+}
+
+.mm-bg-frame {
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .mm-bg-frame { display: none; }
+}
+
+/* 官方 .wrapper{position:relative;z-index:2}:内容压在背景层之上 */
+body.mm-theme .site-header,
+body.mm-theme .app { position: relative; z-index: 2; }
+
+/* ============ 3. 顶栏(官方 .header--nav 桌面胶囊 / .header--btn 移动汉堡) ============ */
+
+body.mm-theme .site-header {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ max-width: var(--mm-shell);
+ /* 顶栏往下挪一档:官方背景是内缩 3.14vw 的圆角渐变块,顶栏压到渐变开始线以下,
+ 上面这一段就露出动态背景 */
+ margin: clamp(26px, 3.8vw, 58px) auto 0;
+ min-height: var(--mm-nav-h);
+ padding: 0 clamp(14px, 1.7143vw, 24px) 0 clamp(18px, 2.3571428571vw, 33px);
+ background: #fff;
+ border: 1px solid transparent;
+ border-radius: var(--mm-nav-radius);
+ box-shadow: var(--mm-nav-shadow);
+}
+
+body.mm-theme .site-header .brand { gap: 12px; color: var(--mm-navy); text-decoration: none; }
+body.mm-theme .site-header .brand .mark {
+ display: block;
+ width: 56px;
+ height: 34px;
+ /* 图标走 CSS 背景而不是
:主题在 上,首帧前就定好了,
+ 换成图片标签的话浅色主题会先闪一下深色版图标 */
+ background: var(--mm-brand-mark) center / contain no-repeat;
+}
+body.mm-theme .site-header .brand-text h1 {
+ margin: 0;
+ font-size: 21px;
+ font-weight: 800;
+ letter-spacing: .2px;
+ color: var(--mm-navy);
+}
+body.mm-theme .site-header .brand-text p { margin: 3px 0 0; font-size: 12px; color: var(--muted); }
+
+/* 导航项:官方 .header--nav__link(字色 #0b3871、选中态紫色下横条 5px) */
+body.mm-theme .site-header .nav {
+ justify-self: auto;
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: clamp(12px, 1.7143vw, 24px);
+ padding: 4px 0;
+ max-width: 100%;
+ /* 这里不能开 overflow:当前页的紫色下横条画在项框之外,会被裁掉 */
+ overflow: visible;
+}
+
+body.mm-theme .site-header .nav .btn,
+body.mm-theme .site-header .nav .btn.btn-primary,
+body.mm-theme .site-header .nav .btn.btn-ghost {
+ position: relative;
+ flex: 0 0 auto;
+ min-height: 0;
+ padding: 8px 2px;
+ border: 0;
+ border-radius: 0;
+ background: none;
+ box-shadow: none;
+ color: var(--mm-navy);
+ font-family: var(--mm-sans);
+ font-size: 17px;
+ font-weight: 800;
+ line-height: 1.55;
+ letter-spacing: .2px;
+ text-decoration: none;
+ transition: color .15s;
+}
+body.mm-theme .site-header .nav .btn:hover { color: var(--mm-purple); transform: none; box-shadow: none; }
+
+/* 分隔线:官方 .header--nav__item:not(:last-child)::after → 1px × 14px #e5e5e5 */
+body.mm-theme .site-header .nav .btn:not(:last-child)::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ right: calc(-1 * clamp(6px, .8571428571vw, 12px));
+ width: 1px;
+ height: 14px;
+ background: var(--mm-grey);
+ transform: translateY(-50%);
+}
+
+/* 当前页 / 悬停:官方 .header--nav__link.on::before → 高 5px、圆角 100px 100px 0 0 */
+body.mm-theme .site-header .nav .btn[aria-current="page"]::before,
+body.mm-theme .site-header .nav .btn:hover::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: calc((var(--mm-nav-h) - 42px) / -2);
+ height: 5px;
+ border-radius: 100px 100px 0 0;
+ background: var(--mm-purple);
+}
+
+/* 汉堡按钮:官方 .header--btn(白底 + 1.25vw 红边 + 10px 硬阴影),开合时变 .header--nav__btn 的紫底叉 */
+.mm-menu-btn {
+ display: none;
+ position: relative;
+ flex: 0 0 auto;
+ width: 44px;
+ height: 44px;
+ padding: 0;
+ border: 3px solid var(--mm-red);
+ border-radius: 100px;
+ background: #fff;
+ box-shadow: 4px 4px 0 rgba(0, 0, 0, .2);
+ cursor: pointer;
+}
+.mm-menu-btn span {
+ display: block;
+ width: 20px;
+ height: 3px;
+ margin: 4px auto;
+ border-radius: 100px;
+ background: var(--mm-red);
+ transition: transform .2s, background .2s;
+}
+body.mm-menu-open .mm-menu-btn { border-color: var(--mm-purple); background: var(--mm-purple); }
+body.mm-menu-open .mm-menu-btn span { background: #fff; }
+body.mm-menu-open .mm-menu-btn span:first-child { transform: translateY(3.5px) rotate(45deg); }
+body.mm-menu-open .mm-menu-btn span:last-child { transform: translateY(-3.5px) rotate(-45deg); }
+
+/* 窄一点的桌面端:七项中文标签会顶到胶囊右沿,先收字号与间距。
+ 1001~1015px 是最挤的一档,收不够就会折成两层(开关掉到第二行),所以这里收得狠一点 */
+@media (max-width: 1200px) and (min-width: 1001px) {
+ body.mm-theme .site-header { gap: 12px; }
+ body.mm-theme .site-header .nav { gap: 12px; }
+ body.mm-theme .site-header .nav .btn,
+ body.mm-theme .site-header .nav .btn.btn-primary,
+ body.mm-theme .site-header .nav .btn.btn-ghost { font-size: 15px; }
+}
+
+@media (max-width: 1000px) {
+ body.mm-theme .site-header {
+ justify-content: space-between;
+ padding: 0 clamp(14px, 3vw, 20px);
+ min-height: 68px;
+ gap: 10px;
+ }
+ .mm-menu-btn { display: block; }
+ /* 窄屏要同时站下品牌、深色开关和汉堡:品牌收一档,否则 header 会折行成两层
+ (开关自己的尺寸在 §16 的媒体查询里,因为那节的规则排在更后面) */
+ body.mm-theme .site-header { gap: 12px; }
+ body.mm-theme .site-header .brand .mark { width: 46px; height: 28px; }
+ body.mm-theme .site-header .brand-text h1 { font-size: 18px; }
+ /* 49px 的圆圈才够手指点:两个圆钮各 44px,中间留 12px,别再挤在一起 */
+ .mm-menu-btn { width: 44px; height: 44px; }
+ /* 菜单展开时整屏铺的是 .nav(z-index:40),品牌、开关、汉堡要浮在它上面,
+ 否则开着菜单就够不到深浅色开关了 */
+ body.mm-theme .site-header .brand,
+ body.mm-theme .site-header .mm-theme-btn,
+ body.mm-theme .site-header .mm-menu-btn { position: relative; z-index: 41; }
+ body.mm-theme .site-header .nav {
+ position: fixed;
+ inset: 0;
+ z-index: 40;
+ display: none;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0;
+ margin: 0;
+ padding: 84px 24px 40px;
+ overflow: auto;
+ background: #fff;
+ box-shadow: var(--mm-nav-shadow);
+ }
+ body.mm-menu-open .site-header .nav { display: flex; }
+ /* 顶栏平时和 .app 同层(都是 z-index:2),展开菜单时必须抬到 .app 之上,
+ 否则白色面板会被正文盖住 */
+ body.mm-menu-open .site-header { z-index: 30; }
+ /* html 上也锁一份:只锁 body 拦不住 html 那只滚动条,深色面板右上角会露一截浅色滑块。
+ 这两条都必须留在媒体查询里 —— 拉宽窗口后菜单按钮已经隐藏,锁还在就谁也解不开了 */
+ body.mm-menu-open,
+ html.mm-menu-open { overflow: hidden; }
+ body.mm-theme .site-header .nav .btn,
+ body.mm-theme .site-header .nav .btn.btn-primary,
+ body.mm-theme .site-header .nav .btn.btn-ghost {
+ margin: 0;
+ padding: 16px 4px;
+ font-size: 20px;
+ text-align: center;
+ }
+ body.mm-theme .site-header .nav .btn::after { display: none; }
+ body.mm-theme .site-header .nav .btn:not(:last-child) {
+ border-bottom: 3px solid var(--mm-grey);
+ }
+ body.mm-theme .site-header .nav .btn[aria-current="page"]::before,
+ body.mm-theme .site-header .nav .btn:hover::before { display: none; }
+ body.mm-theme .site-header .nav .btn[aria-current="page"] { color: var(--mm-purple); }
+ .mm-menu-btn { position: relative; z-index: 50; }
+ body.mm-theme .site-header .global-sync-bar {
+ left: clamp(8px, 2vw, 16px);
+ right: clamp(8px, 2vw, 16px);
+ justify-content: space-between;
+ }
+ body.mm-theme .site-header .global-sync-bar .auto-sync-status { max-width: none; }
+}
+
+/* 再窄一点(320~380px)品牌、开关、汉堡才挤得下同一行 */
+@media (max-width: 380px) {
+ body.mm-theme .site-header { padding: 0 10px; }
+ body.mm-theme .site-header .brand .mark { width: 40px; height: 24px; }
+ body.mm-theme .site-header .brand-text h1 { font-size: 17px; }
+}
+/* 到 344px 以下只留图标当品牌,把宽度全让给开关和汉堡 */
+@media (max-width: 344px) {
+ body.mm-theme .site-header .brand-text { display: none; }
+}
+
+/* 后台同步状态:绑在胶囊右下角外侧(app.js/auto-sync.js 把它塞进
),
+ 不占导航的位置,也不改变胶囊的官方比例 */
+body.mm-theme .site-header .global-sync-bar {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: clamp(6px, 1.4286vw, 16px);
+ z-index: 6;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ margin: 0;
+ padding: 3px 5px 3px 14px;
+ max-width: calc(100% - 28px);
+ background: #fff;
+ border-radius: 100px;
+ box-shadow: 4px 4px 0 rgba(0, 0, 0, .15);
+}
+body.mm-theme .site-header .global-sync-bar:has(.auto-sync-status[hidden]):has(button[hidden]) { display: none; }
+body.mm-theme .site-header .global-sync-bar .auto-sync-status {
+ position: static;
+ margin: 0;
+ max-width: min(420px, 46vw);
+ font-size: 12px;
+ line-height: 1.6;
+ color: var(--mm-navy);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+body.mm-theme .site-header .global-sync-bar .btn { min-height: 30px; padding: 4px 14px; font-size: 12px; }
+
+/* 回到顶部:官方 .common--top(右下 120px,桌面 1.4286vw 边距) */
+.mm-top {
+ position: fixed;
+ right: 2.5vw;
+ bottom: 2.5vw;
+ z-index: 3;
+ display: block;
+ width: 15vw;
+ max-width: 120px;
+ transition: transform .3s, opacity .3s;
+}
+.mm-top.off { opacity: 0; transform: translateY(30%); pointer-events: none; }
+.mm-top:hover { transform: translateY(-5%); }
+.mm-top img { display: block; width: 100%; height: auto; }
+
+/* ============ 4. 版心与页面节奏 ============ */
+
+body.mm-theme .app,
+body.mm-theme.ac-page .app {
+ max-width: var(--mm-content);
+ margin: 0 auto;
+ padding: clamp(26px, 3vw, 46px) clamp(16px, 1.6vw, 22px) clamp(76px, 7vw, 120px);
+}
+
+/* 页头:左边标题块、右边主按钮,一行放下;剩余的次级操作归到下面的工具行 */
+body.mm-theme .ac-page-heading {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: clamp(20px, 2.4vw, 36px);
+ flex-wrap: wrap;
+ margin-bottom: clamp(24px, 2.6vw, 38px);
+}
+
+/* 背景太花,文字不能直接落在上面:页面顶部这几块统一加一层「玻璃板」。
+ 不支持的浏览器回退成更实的白(见下面的 @supports)。 */
+body.mm-theme .ac-page-heading,
+body.mm-theme .stats-toolbar,
+body.mm-theme .circle-hero,
+body.mm-theme .dan-hero,
+body.mm-theme .help-heading,
+body.mm-theme .help-toc,
+body.mm-theme .pager {
+ position: relative;
+ background: rgba(255, 255, 255, .92);
+ border: 1px solid rgba(255, 255, 255, .75);
+ border-radius: 28px;
+ padding: clamp(18px, 2.2vw, 32px) clamp(20px, 2.6vw, 38px);
+ box-shadow: 0 20px 44px -30px rgba(11, 56, 113, .6);
+}
+@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
+ body.mm-theme .ac-page-heading,
+ body.mm-theme .stats-toolbar,
+ body.mm-theme .circle-hero,
+ body.mm-theme .dan-hero,
+ body.mm-theme .help-heading,
+ body.mm-theme .help-toc,
+ body.mm-theme .pager {
+ background: rgba(255, 255, 255, .88);
+ -webkit-backdrop-filter: blur(12px) saturate(1.2);
+ backdrop-filter: blur(12px) saturate(1.2);
+ }
+}
+body.mm-theme .help-toc { padding: 12px 18px; border-radius: 100px; }
+body.mm-theme .pager { border-radius: 100px; padding: 10px 18px; }
+/* 玻璃板里的标题再加一圈细白描边,字更贴地 */
+body.mm-theme .ac-page-heading h2,
+body.mm-theme .stats-toolbar h2,
+body.mm-theme .circle-hero h2,
+body.mm-theme .dan-hero h2,
+body.mm-theme .help-heading h2 {
+ text-shadow: 0 1px 0 rgba(255, 255, 255, .9), 0 0 14px rgba(255, 255, 255, .8);
+}
+
+/* 小字:在卡片或玻璃板里的留一圈很淡的白描边就够了;直接落在背景花纹上的那几处
+ (.app 的直接子元素、题友动态的状态行、说明页正文、热力图加载提示)另给一块
+ 不透明底板,否则花哨图案会从字底下透出来 */
+body.mm-theme .analytics-note,
+body.mm-theme #message,
+body.mm-theme .dan-msg,
+body.mm-theme .circle-note,
+body.mm-theme .pager-hint {
+ text-shadow: 0 1px 0 rgba(255, 255, 255, .95), 0 0 10px rgba(255, 255, 255, .9);
+}
+body.mm-theme .app > .analytics-note,
+body.mm-theme .app > #message,
+body.mm-theme .app > .dan-msg,
+body.mm-theme .circle-main > #message,
+body.mm-theme .heatmap-top > .analytics-note,
+body.mm-theme .help-page > p {
+ background: #fff;
+ border-radius: 12px;
+ padding: 8px 14px;
+ box-shadow: 0 8px 20px -14px rgba(11, 56, 113, .55);
+ text-shadow: none;
+}
+body.mm-theme .stats-eyebrow { display: block; margin-bottom: 14px; }
+body.mm-theme .ac-page-heading h2 { margin: 0 0 14px; }
+body.mm-theme .ac-page-heading p { margin: 0; }
+body.mm-theme .ac-page-heading .form-actions { flex-wrap: nowrap; }
+
+body.mm-theme .page-toolbar {
+ justify-content: flex-end;
+ gap: 12px;
+ margin-bottom: clamp(28px, 3.2vw, 48px);
+}
+
+/* ============ 5. 官方大盒子 .common--box(白底 + 上紫下粉横条 + 14px 硬阴影) ============ */
+
+body.mm-theme .filters,
+body.mm-theme .admin,
+body.mm-theme .foot,
+body.mm-theme .section,
+body.mm-theme .stat-row,
+body.mm-theme .ac-platform-strip,
+body.mm-theme .placeholder,
+body.mm-theme .empty {
+ position: relative;
+ background: #fff;
+ border: 0;
+ color: var(--mm-ink);
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+ overflow: visible;
+}
+
+/* 横条只画在该有横条的盒子上:概览、平台、筛选、记录、页脚、账号管理 */
+body.mm-theme .filters::before,
+body.mm-theme .admin::before,
+body.mm-theme .foot::before,
+body.mm-theme .section::before,
+body.mm-theme .stat-row::before,
+body.mm-theme .ac-platform-strip::before,
+body.mm-theme .analytics::before,
+body.mm-theme .today-panel::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 50%;
+ width: var(--mm-bar-w);
+ height: var(--mm-bar-h);
+ border-radius: 0 0 var(--mm-bar-h) var(--mm-bar-h);
+ background: var(--mm-purple);
+ transform: translateX(-50%);
+}
+body.mm-theme .filters::after,
+body.mm-theme .admin::after,
+body.mm-theme .foot::after,
+body.mm-theme .section::after,
+body.mm-theme .stat-row::after,
+body.mm-theme .ac-platform-strip::after,
+body.mm-theme .analytics::after,
+body.mm-theme .today-panel::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ width: var(--mm-bar-w);
+ height: var(--mm-bar-h);
+ border-radius: var(--mm-bar-h) var(--mm-bar-h) 0 0;
+ background: var(--mm-pink);
+ transform: translateX(-50%);
+}
+
+/* 大盒子统一的内边距与节奏 */
+body.mm-theme .section,
+body.mm-theme .stat-row,
+body.mm-theme .ac-platform-strip {
+ margin-bottom: var(--mm-gap);
+ padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad);
+}
+
+body.mm-theme.ac-page .filters {
+ display: grid;
+ gap: clamp(22px, 2.6vw, 34px) clamp(20px, 2.4vw, 32px);
+ padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad);
+ background: #fff;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+ margin-bottom: var(--mm-gap);
+}
+/* 官方三栏只在大屏成立。这条 grid-template-columns 原来没带媒体查询,把 styles.css /
+ ui.css 里窄屏的 1~2 栏折叠全压掉了:手机上硬排三栏,每栏只剩 70~100px,
+ 平台 chips 竖着堆、状态三连被挤成两行、搜索框只剩一半宽 */
+@media (min-width: 1001px) {
+ body.mm-theme.ac-page .filters { grid-template-columns: 1fr 1.4fr 1fr; }
+}
+@media (max-width: 1000px) {
+ body.mm-theme.ac-page .filters { gap: 16px; }
+}
+
+/* ============ 6. 按钮(官方 .common--btn:红底 + 同色系 3D 底影,按下时沉到阴影上) ============ */
+
+body.mm-theme .btn {
+ border: 0;
+ border-radius: 100px;
+ font-family: var(--mm-sans);
+ font-size: 14px;
+ font-weight: 800;
+ letter-spacing: .2px;
+ min-height: 40px;
+ padding: 8px 22px;
+ box-shadow: 0 5px 0 0 rgba(11, 56, 113, .18);
+ transition: transform .15s, box-shadow .15s, background .15s, color .15s;
+}
+
+body.mm-theme .btn-primary {
+ background: var(--mm-red);
+ color: #fff;
+ box-shadow: 0 5px 0 0 var(--mm-red-dark);
+}
+body.mm-theme .btn-primary:hover { background: var(--mm-red); color: #fff; transform: translateY(5px); box-shadow: none; }
+
+body.mm-theme .btn-ghost {
+ background: #fff;
+ color: var(--mm-navy);
+ border: 1px solid var(--mm-grey);
+}
+body.mm-theme .btn-ghost:hover {
+ background: #fff;
+ color: var(--mm-purple);
+ border-color: var(--mm-purple-soft);
+ transform: translateY(5px);
+ box-shadow: none;
+}
+
+body.mm-theme .btn:active { transform: translateY(5px); box-shadow: none; }
+body.mm-theme .btn:focus-visible { outline: 3px solid var(--mm-purple-soft); outline-offset: 2px; }
+body.mm-theme .btn[disabled] { opacity: .45; box-shadow: none; transform: none; }
+
+body.mm-theme .btn-small { min-height: 32px; padding: 6px 16px; font-size: 12.5px; }
+body.mm-theme .btn-mini { min-height: 28px; padding: 4px 12px; font-size: 12px; }
+
+/* ============ 7. 表单 ============ */
+
+body.mm-theme .input,
+body.mm-theme .select {
+ font-family: var(--mm-sans);
+ font-size: 14px;
+ color: var(--mm-ink);
+ background: #fff;
+ border: 1px solid var(--mm-grey);
+ border-radius: 100px;
+ min-height: 44px;
+ padding: 8px 18px;
+ box-shadow: none;
+}
+body.mm-theme .input::placeholder { color: #a8a2b4; }
+body.mm-theme .input:focus,
+body.mm-theme .select:focus {
+ outline: none;
+ border-color: var(--mm-purple);
+ box-shadow: 0 0 0 3px rgba(186, 94, 234, .2);
+}
+body.mm-theme .select { cursor: pointer; }
+
+body.mm-theme .field-label,
+body.mm-theme .filter-label {
+ color: var(--mm-navy);
+ font-weight: 800;
+ font-size: 13px;
+ letter-spacing: .2px;
+}
+body.mm-theme .field-hint,
+body.mm-theme .section-hint { color: var(--muted); }
+
+/* ============ 8. 标签、胶囊、分页 ============ */
+
+body.mm-theme .chip {
+ font-family: var(--mm-sans);
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--mm-navy);
+ background: #fff;
+ border: 1px solid var(--mm-grey);
+ border-radius: 100px;
+ padding: 6px 14px;
+}
+body.mm-theme .chip:hover { border-color: var(--mm-purple-soft); color: var(--mm-purple); }
+body.mm-theme .chip.is-on {
+ background: var(--mm-purple);
+ border-color: var(--mm-purple);
+ color: #fff;
+ box-shadow: 0 3px 0 0 var(--mm-purple-deep);
+}
+body.mm-theme .chip.is-on .chip-n { color: rgba(255, 255, 255, .85); }
+body.mm-theme .chip .chip-n { color: var(--muted); font-family: var(--mm-mono); }
+
+body.mm-theme .tabs {
+ background: var(--mm-purple-tint);
+ border: 0;
+ border-radius: 100px;
+ padding: 3px;
+}
+body.mm-theme .tab {
+ font-family: var(--mm-sans);
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--mm-navy);
+ background: transparent;
+ border-radius: 100px;
+ padding: 7px 18px;
+}
+body.mm-theme .tab:hover { color: var(--mm-purple); }
+body.mm-theme .tab.is-active {
+ background: #fff;
+ color: var(--mm-purple);
+ box-shadow: 0 2px 0 rgba(11, 56, 113, .12);
+}
+
+body.mm-theme .badge {
+ font-family: var(--mm-sans);
+ font-size: 11px;
+ font-weight: 700;
+ padding: 3px 10px;
+ border: 0;
+ border-radius: 100px;
+ background: #f2eef7;
+ color: #5f5870;
+}
+body.mm-theme .badge.ac { background: #e6fbe9; color: #137a3a; }
+body.mm-theme .badge.warn { background: #fff2e0; color: #a35c00; }
+body.mm-theme .badge.bad { background: #ffe9ec; color: #b3122a; }
+body.mm-theme .badge.diff { background: #e8f0ff; color: #1c53b8; }
+body.mm-theme .badge.contest { background: #f3e6ff; color: #7d2fb0; }
+body.mm-theme .badge.platform { background: #e9f6ff; color: #135a8f; text-transform: none; }
+body.mm-theme .badge.score { background: #fff2e0; color: #a35c00; }
+
+body.mm-theme .db-chip {
+ font-family: var(--mm-mono);
+ font-size: 12px;
+ color: var(--mm-navy);
+ background: #fff;
+ border: 1px solid var(--mm-grey);
+ border-radius: 100px;
+ padding: 6px 14px;
+}
+body.mm-theme.ac-page .db-chip { color: var(--mm-navy); border-color: var(--mm-grey); max-width: 260px; }
+
+body.mm-theme .pager { padding: 12px 0 4px; }
+body.mm-theme .pager-hint { color: var(--muted); font-family: var(--mm-mono); }
+
+/* ============ 9. 概览数字(盒子是官方的,格子是平的) ============ */
+
+body.mm-theme .stat-row,
+body.mm-theme.ac-page #statRow {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
+ gap: 6px;
+ align-items: stretch;
+}
+/* 六个指标就该是一行:auto-fit 在 1000px 出头会掉成 5+1,很难看 */
+@media (min-width: 1080px) {
+ body.mm-theme .stat-row,
+ body.mm-theme.ac-page #statRow { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+}
+@media (max-width: 1079px) and (min-width: 760px) {
+ body.mm-theme .stat-row,
+ body.mm-theme.ac-page #statRow { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+@media (max-width: 759px) {
+ body.mm-theme .stat-row,
+ body.mm-theme.ac-page #statRow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+body.mm-theme .stat,
+body.mm-theme.ac-page #statRow .stat {
+ background: transparent;
+ border: 0;
+ border-radius: 20px;
+ padding: 14px 18px;
+ box-shadow: none;
+ transition: background .2s var(--mm-ease-out);
+}
+body.mm-theme .stat:hover,
+body.mm-theme.ac-page #statRow .stat:hover { background: var(--mm-purple-tint); }
+
+body.mm-theme .stat-label { color: var(--mm-navy); font-weight: 700; font-size: 12.5px; }
+body.mm-theme .stat-value {
+ font-family: var(--mm-sans);
+ font-size: clamp(28px, 2.4vw, 34px);
+ font-weight: 800;
+ color: var(--mm-navy);
+ letter-spacing: -.5px;
+}
+body.mm-theme .stat-sub {
+ color: var(--muted);
+ font-size: 11.5px;
+ line-height: 1.5;
+ text-wrap: pretty;
+}
+body.mm-theme .stat.is-ac .stat-value { color: #16883f; }
+body.mm-theme .stat.is-unac .stat-value { color: #c47f00; }
+body.mm-theme .stat.is-sub .stat-value { color: var(--mm-purple-deep); }
+
+body.mm-theme.ac-page #statRow .stat.is-ac { border: 0; background: transparent; }
+body.mm-theme.ac-page #statRow .stat:last-child .stat-value { font-size: 20px; line-height: 1.8; }
+
+/* 平台条:同样是「一个白盒子 + 平铺的格子」 */
+body.mm-theme .ac-platform-strip {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
+ gap: 6px;
+}
+body.mm-theme .ac-platform-strip .stat {
+ background: transparent;
+ border: 0;
+ border-radius: 18px;
+ box-shadow: none;
+ padding: 12px 16px;
+ transition: background .2s var(--mm-ease-out);
+}
+body.mm-theme .ac-platform-strip .stat:hover { background: var(--mm-purple-tint); }
+body.mm-theme .ac-platform-strip .stat-value { font: 800 18px var(--mm-sans); color: var(--mm-navy); }
+body.mm-theme .ac-platform-strip .stat-sub { color: var(--muted); }
+
+/* ============ 10. 记录列表(区块是官方盒子,每条记录是盒子里的一行) ============ */
+
+/* 区块标题占一行,下面用发丝线接记录行 —— 与官网 top--update 的列表同一套做法 */
+body.mm-theme.ac-page .section-head {
+ margin-bottom: 6px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid var(--border-soft);
+}
+
+body.mm-theme.ac-page .cards { display: block; }
+
+body.mm-theme.ac-page .card {
+ display: grid;
+ grid-template-columns: minmax(0, 1.6fr) minmax(150px, .62fr) auto;
+ grid-template-areas:
+ "top stats foot"
+ "context stats foot";
+ align-items: center;
+ column-gap: clamp(14px, 1.6vw, 28px);
+ row-gap: 2px;
+ padding: 12px 4px;
+ background: transparent;
+ border: 0;
+ border-bottom: 1px solid var(--border-soft);
+ border-radius: 0;
+ box-shadow: none;
+ transform: none;
+ transition: background .18s var(--mm-ease-out);
+}
+body.mm-theme.ac-page .card:last-child { border-bottom: 0; }
+body.mm-theme.ac-page .card.is-ac,
+body.mm-theme.ac-page .card.is-unac { background: transparent; border-left: 0; }
+@media (hover: hover) and (pointer: fine) {
+ body.mm-theme.ac-page .card:hover { background: #fdf8ff; }
+}
+
+body.mm-theme.ac-page .card .card-top { grid-area: top; min-height: 0; justify-content: flex-start; align-items: center; gap: 10px; }
+body.mm-theme.ac-page .card .ac-card-context { grid-area: context; gap: 3px; }
+body.mm-theme.ac-page .card .card-stats { grid-area: stats; }
+body.mm-theme.ac-page .card .card-foot { grid-area: foot; }
+/* 题号/平台/难度挤在一行里就别铺开了:小一号、间距收掉,一行读完 */
+body.mm-theme.ac-page .card-meta > .badge { padding: 2px 8px; font-size: 11px; }
+body.mm-theme.ac-page .ac-card-identity { padding-top: 0; font-size: 11.5px; }
+
+body.mm-theme.ac-page .card-title {
+ font-size: 16px;
+ font-weight: 800;
+ line-height: 1.35;
+ color: var(--mm-navy);
+}
+body.mm-theme .card-title a:hover { color: var(--mm-purple); border-bottom-color: var(--mm-purple-soft); }
+body.mm-theme .card-meta { color: var(--muted); }
+
+body.mm-theme.ac-page .card-stats {
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ padding: 0;
+ gap: 2px 14px;
+}
+/* 「通过时间 2026-10-10 14:50」并成一行,右侧那一栏才不空得发慌 */
+body.mm-theme.ac-page .card-stats > span { display: flex; align-items: baseline; gap: 8px; }
+body.mm-theme.ac-page .card-stats .k { color: var(--muted); font-size: 11.5px; }
+body.mm-theme.ac-page .card-stats .v { color: var(--mm-navy); font-weight: 700; font-size: 14px; }
+body.mm-theme.ac-page .card-stats > span:first-child { border-bottom: 0; padding-bottom: 0; }
+
+body.mm-theme.ac-page .card-foot {
+ border-top: 0;
+ padding-top: 0;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 5px;
+ flex-wrap: nowrap;
+ color: var(--muted);
+ text-align: right;
+}
+body.mm-theme .ac-card-tags .badge { background: #f2eef7; border: 0; color: #5f5870; }
+
+/* 窄屏回到「一张一张卡片」的堆叠形态(与汉堡菜单同一个断点) */
+@media (max-width: 1000px) {
+ /* 工具行排成两列网格:旧样式在窄屏会一行一个按钮、还全部右对齐,边缘很毛 */
+ body.mm-theme .page-toolbar {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ }
+ body.mm-theme .page-toolbar .db-chip { grid-column: 1 / -1; justify-self: start; max-width: 100%; }
+ body.mm-theme .page-toolbar .btn {
+ width: 100%;
+ justify-content: center;
+ padding: 8px 12px;
+ font-size: 13px;
+ white-space: nowrap;
+ }
+ body.mm-theme .ac-page-heading .form-actions,
+ body.mm-theme .ac-page-heading .btn-primary { width: 100%; }
+
+ body.mm-theme.ac-page .section-head { padding-bottom: 14px; }
+ body.mm-theme.ac-page .cards { display: flex; flex-direction: column; gap: 16px; }
+ body.mm-theme.ac-page .card {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 14px;
+ padding: 18px;
+ border: 1px solid var(--border-soft);
+ border-radius: 20px;
+ box-shadow: var(--mm-soft);
+ }
+ body.mm-theme.ac-page .card:last-child { border-bottom: 1px solid var(--border-soft); }
+ body.mm-theme.ac-page .card-foot { justify-content: flex-start; text-align: left; align-items: flex-start; }
+
+ /* 状态三连(全部/已AC/未AC)在窄屏只有 100px 出头,会被挤成两行、
+ 右半边还被圆形的选中块盖住:让每组平分整行、文字不许折行,选中块改成圆角矩形 */
+ body.mm-theme.ac-page .filters .tabs { display: flex; width: 100%; }
+ body.mm-theme.ac-page .filters .tab {
+ flex: 1 1 0;
+ min-width: 0;
+ padding: 9px 6px;
+ border-radius: 12px;
+ font-size: 13px;
+ white-space: nowrap;
+ }
+
+ /* 点得着的目标:手机上一行的操作按钮别只有 23px 高 */
+ body.mm-theme .collapse-toggle,
+ body.mm-theme .expand-btn,
+ body.mm-theme .dx-more a,
+ body.mm-theme button.circle-name { min-height: 40px; }
+
+ /* 全年热力图一行放不下(53 列),给它一句滑动提示,别让人以为只有半年 */
+ body.mm-theme #heatmapHost::before {
+ content: '热力图一行放不下一年,左右滑动查看 →';
+ display: block;
+ margin-bottom: 8px;
+ font-size: 11.5px;
+ color: var(--muted);
+ }
+}
+
+/* 手机上按钮不排到半格,直接一列到底 */
+@media (max-width: 560px) {
+ body.mm-theme .page-toolbar { grid-template-columns: minmax(0, 1fr); }
+}
+
+/* 卡片页脚的小操作:旧样式是深蓝实心小块,这里换成官方语言的白底胶囊 */
+body.mm-theme.ac-page .expand-btn {
+ font-family: var(--mm-sans);
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--mm-navy);
+ background: #fff;
+ border: 1px solid var(--mm-grey);
+ border-radius: 100px;
+ padding: 6px 14px;
+ gap: 6px;
+ box-shadow: 0 3px 0 0 rgba(11, 56, 113, .15);
+ transition: transform .15s, box-shadow .15s, color .15s, border-color .15s;
+}
+body.mm-theme.ac-page .expand-btn:hover {
+ color: var(--mm-purple);
+ border-color: var(--mm-purple-soft);
+ text-decoration: none;
+ transform: translateY(3px);
+ box-shadow: none;
+}
+
+/* ============ 11. 区块标题与提示 ============ */
+
+body.mm-theme .section-head h2,
+body.mm-theme.ac-page .section-head h2 {
+ font-size: 20px;
+ font-weight: 800;
+ color: var(--mm-navy);
+}
+body.mm-theme .section-count {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 2px 12px;
+ background: var(--mm-purple-tint);
+ border-radius: 100px;
+ color: var(--mm-purple-deep);
+ font-family: var(--mm-sans);
+ font-size: 12px;
+ font-weight: 800;
+}
+body.mm-theme .collapse-toggle { color: var(--mm-navy); font-weight: 800; background: none; border: 0; }
+body.mm-theme .collapse-toggle:hover { color: var(--mm-purple); }
+body.mm-theme .caret { color: var(--mm-purple); }
+body.mm-theme .timeline { border-top: 1px dashed var(--border-soft); }
+body.mm-theme .tl-time { color: var(--muted); }
+body.mm-theme .tl-row { border-color: var(--border-soft); }
+
+body.mm-theme .ac-page-heading h2,
+body.mm-theme .stats-toolbar h2 { font-size: 30px; font-weight: 800; color: var(--mm-navy); }
+body.mm-theme .ac-page-heading p,
+body.mm-theme .stats-toolbar p { color: #4b4b4b; }
+body.mm-theme .stats-eyebrow {
+ color: var(--mm-purple);
+ letter-spacing: 3px;
+ font-weight: 800;
+}
+
+/* 通知:不抢戏 —— 一点暖底 + 一条状态色 + 发丝分隔行 */
+body.mm-theme .notice {
+ background: #fffcf7;
+ border: 0;
+ border-left: 5px solid var(--mm-orange);
+ border-radius: 14px;
+ padding: 14px 18px;
+ box-shadow: none;
+ color: var(--mm-ink);
+ font-size: 12.5px;
+ line-height: 1.7;
+}
+body.mm-theme .notice + .notice { margin-top: 10px; }
+body.mm-theme .notice > div + div {
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid var(--border-soft);
+}
+body.mm-theme .notice strong { color: var(--mm-navy); }
+body.mm-theme .notice-ac { background: #f7fdf9; border-left-color: var(--mm-green); }
+body.mm-theme .notice-ac strong { color: var(--mm-navy); }
+body.mm-theme .notice-ac.is-bad,
+body.mm-theme .notice-danger { background: #fff8f9; border-left-color: var(--mm-red); }
+body.mm-theme .notice-ac.is-bad strong,
+body.mm-theme .notice-danger strong { color: var(--mm-red-dark); }
+body.mm-theme .notice-warn { background: #fffcf7; border-left-color: var(--mm-orange); }
+body.mm-theme .notice-warn strong { color: var(--mm-navy); }
+body.mm-theme .notice-warn span { color: #6f6a7a; }
+body.mm-theme .notice-neutral { background: #fcfcfc; border-left-color: var(--mm-grey); color: var(--muted); }
+
+body.mm-theme .placeholder,
+body.mm-theme .empty {
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+ padding: 56px 20px;
+ color: var(--muted);
+}
+body.mm-theme .empty h3 { color: var(--mm-navy); font-weight: 800; }
+body.mm-theme .spinner { border-color: var(--mm-purple-tint); border-top-color: var(--mm-purple); }
+
+/* ============ 12. 页脚(官方 .footer--bottom:白底、#0b3871 字色、上沿软阴影) ============ */
+
+body.mm-theme .foot {
+ margin-top: clamp(30px, 3.4vw, 52px);
+ padding: calc(var(--mm-bar-h) + clamp(20px, 2.4vw, 34px)) var(--mm-pad);
+ color: var(--mm-navy);
+ font-size: 12.5px;
+ box-shadow: var(--mm-card-shadow);
+}
+body.mm-theme .foot-note { color: var(--muted); }
+body.mm-theme .foot a { color: var(--mm-purple-deep); }
+
+/* ============ 13. 暗色残留(旧规则里写死的浅色字 / 深色底,逐条扳回亮色) ============ */
+
+body.mm-theme .ac-card-identity,
+body.mm-theme.ac-page .ac-card-identity { color: var(--muted); }
+body.mm-theme .form-status { color: var(--mm-navy); }
+body.mm-theme .form-status.is-bad { color: var(--bad); }
+body.mm-theme .form-status.is-ok { color: var(--ac); }
+body.mm-theme .form-status.is-warn { color: var(--warn); }
+
+body.mm-theme .stat-row { gap: 18px; }
+
+body.mm-theme .admin-head h2 { color: var(--mm-navy); font-weight: 800; font-size: 16px; }
+body.mm-theme .admin-block { background: var(--mm-purple-tint); border: 1px solid var(--border-soft); }
+body.mm-theme .admin-block h3 { color: var(--mm-navy); font-weight: 800; }
+
+body.mm-theme .account-owner { background: #fff; border: 1px solid var(--border-soft); }
+body.mm-theme .account-owner > summary:hover { background: var(--mm-purple-tint); }
+body.mm-theme .account-owner-avatar { color: var(--mm-purple-deep); background: var(--mm-purple-tint); }
+body.mm-theme .account-owner-content { background: #fffafd; }
+body.mm-theme .account-row { border-color: var(--border-soft); }
+
+body.mm-theme .help-inline { background: #fff; border-color: var(--border-soft); }
+body.mm-theme .help-inline summary { color: var(--mm-navy); }
+
+@media (max-width: 600px) {
+ body.mm-theme .ac-page-heading h2,
+ body.mm-theme .stats-toolbar h2 { font-size: 26px; }
+}
+
+/* ============ 14. 表格与弹窗 ============ */
+
+body.mm-theme .rt-table {
+ background: #fff;
+ border-radius: 18px;
+ overflow: hidden;
+}
+body.mm-theme .rt-table th { color: var(--mm-navy); font-weight: 800; background: var(--mm-purple-tint); }
+body.mm-theme .rt-table td,
+body.mm-theme .rt-table th { border-color: var(--border-soft); }
+body.mm-theme .rt-table tbody th { color: var(--mm-navy); font-weight: 700; }
+
+body.mm-theme .modal-card,
+body.mm-theme dialog {
+ background: #fff;
+ color: var(--mm-ink);
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+}
+body.mm-theme dialog::backdrop { background: rgba(11, 56, 113, .35); }
+body.mm-theme .modal-card h2,
+body.mm-theme dialog h2,
+body.mm-theme dialog h3 { color: var(--mm-navy); }
+
+/* ============ 15. 其余页面自带组件(旧文件里写死的深色,逐条扳回官方亮色) ============
+ *
+ * styles.css / ui.css / circle.css 里有一批硬编码的深色面(分析卡、今日面板、热力图、
+ * DX 计时器、题友圈人物、帮助目录…),token 重映射管不到,只能按选择器覆盖。
+ * 另:这些页面原本没有「盒子」,正文直接落在动态背景上,所以这里一并给它们官方白盒。
+ */
+
+/* —— 统计页:分析卡 / 今日面板 / 热力图 —— */
+body.mm-theme .analytics {
+ position: relative;
+ background: #fff;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad);
+}
+body.mm-theme .analytics-chart {
+ background: #fff;
+ border: 1px solid var(--border-soft);
+ border-radius: 22px;
+ padding: 22px 24px;
+ box-shadow: var(--mm-soft);
+}
+/* 热力图那块是直接挂在 #heatmapHost 下的一级面板(不像别的图表嵌在 .analytics 里),
+ 所以它也得算一个官方大盒子:同样的圆角、硬阴影和上下横条 */
+body.mm-theme .heatmap-top > .analytics-chart {
+ position: relative;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad);
+ box-shadow: var(--mm-card-shadow);
+}
+body.mm-theme .heatmap-top > .analytics-chart::before,
+body.mm-theme .heatmap-top > .analytics-chart::after {
+ content: "";
+ position: absolute;
+ left: 50%;
+ width: var(--mm-bar-w);
+ height: var(--mm-bar-h);
+ background: var(--mm-purple);
+ transform: translateX(-50%);
+}
+body.mm-theme .heatmap-top > .analytics-chart::before {
+ top: 0;
+ border-radius: 0 0 var(--mm-bar-h) var(--mm-bar-h);
+}
+body.mm-theme .heatmap-top > .analytics-chart::after {
+ bottom: 0;
+ border-radius: var(--mm-bar-h) var(--mm-bar-h) 0 0;
+ background: var(--mm-pink);
+}
+body.mm-theme .analytics-chart h3 { color: var(--mm-navy); font-weight: 800; }
+body.mm-theme .analytics-chart h4 { color: var(--muted); }
+body.mm-theme .analytics-head { gap: 12px; }
+body.mm-theme .analytics-controls {
+ background: var(--mm-purple-tint);
+ border: 0;
+ border-radius: 100px;
+ padding: 8px 10px;
+ gap: 8px;
+}
+body.mm-theme .analytics-controls .input { color: var(--mm-ink); color-scheme: light; }
+body.mm-theme .chart-values { color: var(--muted); }
+body.mm-theme .chart-values dl { background: var(--mm-purple-tint); border-radius: 14px; }
+body.mm-theme .chart-values dd { color: var(--mm-navy); }
+body.mm-theme .analytics-metrics > div {
+ background: #fff;
+ border: 1px solid var(--border-soft);
+ border-radius: 18px;
+ border-left: 4px solid var(--mm-green);
+ padding: 16px 18px;
+ box-shadow: var(--mm-soft);
+}
+body.mm-theme .analytics-metrics strong { color: var(--mm-navy); }
+body.mm-theme .analytics-metrics span { color: var(--muted); }
+body.mm-theme .analytics-track { background: var(--mm-purple-tint); height: 10px; }
+body.mm-theme .analytics-track i { background: linear-gradient(90deg, #38c46b, var(--mm-green)); }
+body.mm-theme .analytics-bar-row b { color: var(--mm-navy); }
+
+body.mm-theme .today-panel {
+ position: relative;
+ background: #fff;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad);
+ box-shadow: var(--mm-card-shadow);
+}
+body.mm-theme .today-metrics > div,
+body.mm-theme .today-rhythm {
+ background: var(--mm-purple-tint);
+ border: 0;
+ border-radius: 18px;
+}
+body.mm-theme .today-metrics strong { color: var(--mm-navy); }
+body.mm-theme .today-metrics span,
+body.mm-theme .today-metrics small,
+body.mm-theme .today-performance span { color: var(--muted); }
+body.mm-theme .today-card { background: #fff; border-color: var(--border-soft); box-shadow: var(--mm-soft); }
+
+/* 热力图格子:深色系 → 官方绿渐变 */
+body.mm-theme .heat-cell { border: 1px solid rgba(11, 56, 113, .08); border-radius: 4px; }
+body.mm-theme .level-0 { background: #eeeaf5; }
+body.mm-theme .level-1 { background: #d8f6e2; }
+body.mm-theme .level-2 { background: #a9eebf; }
+body.mm-theme .level-3 { background: #74e49b; }
+body.mm-theme .level-4 { background: var(--mm-green); }
+body.mm-theme .heat-weekdays,
+body.mm-theme .heat-months,
+body.mm-theme .heat-legend { color: var(--muted); }
+body.mm-theme .heat-cell:hover,
+body.mm-theme .heat-cell:focus-visible { outline: 2px solid var(--mm-purple); outline-offset: 1px; }
+
+/* —— DX Rating 页 —— */
+/* 旧样式把 .dx-eyebrow 固定成浅蓝 #8e9fff,白底上只有 2.46 的对比度,
+ 和另外两个 eyebrow 一样收进官方紫里 */
+body.mm-theme .dx-eyebrow { color: var(--mm-purple); letter-spacing: 2px; font-weight: 800; }
+body.mm-theme .dx-pages {
+ background: #fff;
+ border: 1px solid var(--border-soft);
+ border-radius: 100px;
+ padding: 8px 10px;
+ box-shadow: var(--mm-soft);
+}
+body.mm-theme .dx-timer {
+ background: #fff;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+ padding: var(--mm-pad);
+}
+body.mm-theme .dx-timer h2 { color: var(--mm-navy); }
+body.mm-theme .dx-more a { background: #fff; color: var(--mm-navy); border: 1px solid var(--mm-grey); }
+body.mm-theme .dx-ranked { color: #a35c00; }
+body.mm-theme .dx-records-filters { background: #fff; border: 0; box-shadow: var(--mm-soft); }
+
+/* —— 题友圈 —— */
+body.mm-theme .circle-eyebrow { color: var(--mm-purple); letter-spacing: 2px; font-weight: 800; }
+body.mm-theme .circle-hero h2 { color: var(--mm-navy); }
+body.mm-theme .circle-hero p { color: #4b4b4b; }
+body.mm-theme .circle-sidebar,
+body.mm-theme .circle-main { color: var(--mm-ink); }
+body.mm-theme .circle-sidebar {
+ background: #fff;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+ padding: var(--mm-pad);
+}
+body.mm-theme .circle-person { color: var(--mm-navy); border-radius: 18px; }
+/* 窄屏原样式把「我的关注」排成一条横向滚动条:卡片里看不出还能滑,
+ 第二个人就停在卡片边缘,名字后面的通过数被切一半。干脆换行排成格子,全都看得见 */
+@media (max-width: 850px) {
+ body.mm-theme #people {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(146px, 1fr));
+ gap: 8px;
+ overflow: visible;
+ }
+ body.mm-theme #people .circle-person { min-width: 0; width: 100%; }
+}
+body.mm-theme .circle-person:hover { background: var(--mm-purple-tint); }
+body.mm-theme .circle-person[aria-pressed="true"] {
+ background: var(--mm-purple-tint);
+ border-color: var(--mm-purple-soft);
+ color: var(--mm-purple-deep);
+}
+body.mm-theme .circle-avatar {
+ background: hsl(var(--hue, 160) 62% 92%);
+ color: hsl(var(--hue, 160) 48% 30%);
+}
+body.mm-theme .circle-name:hover { color: var(--mm-purple); }
+body.mm-theme .circle-result { background: #f2eef7; color: #5f5870; }
+body.mm-theme .circle-result.ac { background: #e6fbe9; color: #137a3a; }
+body.mm-theme .circle-coverage-status.needs-attention { color: #a35c00; background: #fff3e2; }
+body.mm-theme .circle-card {
+ background: #fff;
+ border: 1px solid var(--border-soft);
+ border-radius: 22px;
+ box-shadow: var(--mm-soft);
+}
+body.mm-theme .circle-filters {
+ background: #fff;
+ border: 0;
+ border-radius: var(--mm-card-radius);
+ box-shadow: var(--mm-card-shadow);
+ padding: var(--mm-pad);
+}
+body.mm-theme .circle-card-head,
+body.mm-theme .circle-card-foot { border-color: var(--border-soft); }
+
+/* —— 使用指南:正文直接落在背景上,给每节一个白盒 —— */
+body.mm-theme .help-page > section {
+ background: #fff;
+ border-radius: 24px;
+ padding: clamp(20px, 2.2vw, 30px) clamp(20px, 2.4vw, 34px);
+ margin-bottom: 18px;
+ box-shadow: var(--mm-soft);
+}
+body.mm-theme .help-page > section h2 { color: var(--mm-navy); }
+body.mm-theme .help-page > section > p,
+body.mm-theme .help-page > section li { color: #3d3a46; }
+body.mm-theme .help-page .help-toc a {
+ background: #fff;
+ border: 1px solid var(--mm-grey);
+ border-radius: 100px;
+ color: var(--mm-navy);
+}
+body.mm-theme .help-page .help-toc a:hover { background: var(--mm-purple-tint); color: var(--mm-purple-deep); border-color: var(--mm-purple-soft); }
+body.mm-theme kbd {
+ background: var(--mm-purple-tint);
+ border: 1px solid var(--mm-purple-soft);
+ border-radius: 6px;
+ color: var(--mm-navy);
+}
+
+/* 扫描剩下的浅色字(深色面已经处理完,这几处是字色本身写死的浅色) */
+body.mm-theme .today-total { color: #16883f; }
+body.mm-theme .dx-rank { color: #a35c00; }
+/* 段位卡本身是深色卡面(深蓝底 + 位阶色描边),和主题无关;浅色主题把页面刷白以后,
+ 卡面上的字必须跟着卡面走 —— 否则黑字压深蓝,题名/完成度/难度全看不见 */
+body.mm-theme .dx-card { color: #eef1fb; }
+body.mm-theme .dx-card .dx-library-card-foot,
+body.mm-theme .dx-card .dx-library-metrics span,
+body.mm-theme .dx-card .dx-card-id,
+body.mm-theme .dx-card .dx-card-meta,
+body.mm-theme .dx-card .dx-card-name,
+body.mm-theme .dx-card .dx-card-time,
+body.mm-theme .dx-card .dx-pending-id { color: #bcc4dc; }
+body.mm-theme .dx-card .dx-rank { color: var(--rank, #ffd166); }
+body.mm-theme .dx-card .dx-library-title a { color: #f4f6ff; }
+body.mm-theme .dx-card .dx-library-title a:hover { color: #d3a1ff; }
+body.mm-theme .help-page .help-heading p,
+body.mm-theme .help-page > section p,
+body.mm-theme .help-page > section li,
+body.mm-theme .help-page details p { color: #3d3a46; }
+body.mm-theme .help-page #message { color: var(--muted); }
+/* 还有几处页内标题/说明直接落在背景上(不在上面的页头列表里) */
+body.mm-theme .dx-page-intro,
+body.mm-theme .circle-feed-head {
+ background: rgba(255, 255, 255, .92);
+ border: 1px solid rgba(255, 255, 255, .75);
+ border-radius: 24px;
+ padding: clamp(16px, 1.8vw, 24px) clamp(20px, 2.4vw, 32px);
+ box-shadow: 0 18px 40px -30px rgba(11, 56, 113, .55);
+}
+@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
+ body.mm-theme .dx-page-intro,
+ body.mm-theme .circle-feed-head {
+ background: rgba(255, 255, 255, .88);
+ -webkit-backdrop-filter: blur(12px) saturate(1.2);
+ backdrop-filter: blur(12px) saturate(1.2);
+ }
+}
+body.mm-theme .dx-page-intro h2,
+body.mm-theme .circle-feed-head h2 { color: var(--mm-navy); text-shadow: 0 1px 0 rgba(255, 255, 255, .9); }
+body.mm-theme .dx-page-intro p { color: #4b4b4b; }
+body.mm-theme .dx-rules {
+ background: #fff;
+ border: 0;
+ border-radius: 24px;
+ padding: 20px 26px;
+ box-shadow: var(--mm-soft);
+ color: var(--mm-ink);
+}
+body.mm-theme .dan-disclaimer,
+body.mm-theme .dan-msg { color: var(--muted); }
+body.mm-theme .today-section-title { color: var(--mm-navy); }
+body.mm-theme .help-page > p { color: #3d3a46; }
+/* ============ 16. 深色模式开关(顶栏右端,官方 .header--btn 的圆钮语言) ============ */
+
+body.mm-theme .mm-theme-btn {
+ position: relative;
+ flex: 0 0 auto;
+ width: 44px;
+ height: 44px;
+ margin-left: 6px;
+ padding: 0;
+ border: 3px solid var(--mm-purple);
+ border-radius: 100px;
+ background: #fff;
+ box-shadow: 4px 4px 0 rgba(0, 0, 0, .2);
+ cursor: pointer;
+ transition: background .2s, border-color .2s, box-shadow .2s;
+}
+body.mm-theme .mm-theme-btn svg { display: block; width: 22px; height: 22px; margin: 0 auto; }
+body.mm-theme .mm-theme-btn svg > * {
+ transform-origin: 50% 50%;
+ transition: opacity .2s, transform .3s var(--mm-ease-out);
+}
+body.mm-theme .mm-theme-btn__moon { fill: var(--mm-navy); }
+body.mm-theme .mm-theme-btn__sun { fill: var(--mm-yellow); }
+/* 浅色模式显示月亮(点了变深色);深色模式的另一半在 theme-dark.css */
+body.mm-theme .mm-theme-btn__sun { opacity: 0; transform: scale(.4) rotate(-90deg); }
+body.mm-theme .mm-theme-btn:hover { border-color: var(--mm-purple); background: var(--mm-purple); }
+body.mm-theme .mm-theme-btn:hover .mm-theme-btn__moon,
+body.mm-theme .mm-theme-btn:hover .mm-theme-btn__sun { fill: #fff; }
+body.mm-theme .mm-theme-btn:focus-visible { outline: 3px solid var(--mm-pink-hot); outline-offset: 3px; }
+
+@media (max-width: 1000px) {
+ body.mm-theme .mm-theme-btn { width: 44px; height: 44px; margin-left: auto; }
+ body.mm-theme .mm-theme-btn svg { width: 21px; height: 21px; }
+}
+
+/* ============ 17. 换主题的那一帧 ============
+ 页面里只有一部分元件带 transition(卡片、按钮、输入框),换主题时它们会慢半拍淡过去,
+ 看上去就像「有的卡片没跟着换皮」。theme.js 在改 data-mm-theme 之前先加上
+ .mm-theme-switching,两帧后摘掉 —— 这一帧内所有过渡停用,整页同时换色。 */
+html.mm-theme-switching,
+html.mm-theme-switching *,
+html.mm-theme-switching *::before,
+html.mm-theme-switching *::after { transition: none !important; }
+/* 只有开关自己该动:月亮沉下去、太阳升起来,这一下是要给人看的 */
+html.mm-theme-switching .mm-theme-btn { transition: background .2s, border-color .2s, box-shadow .2s !important; }
+html.mm-theme-switching .mm-theme-btn svg > * { transition: opacity .2s, transform .3s var(--mm-ease-out) !important; }
+
+/* ============ 18. 与官方的偏差(改这里之前先想清楚) ============
+ *
+ * 1) 中日韩字形:官方 `font-family:"M PLUS 1p",sans-serif`,但 M PLUS 1p 没有简体字,
+ * 所以中文回落到系统字体(雅黑/苹方/思源)。数字与拉丁字母仍是官方的 M PLUS 1p ——
+ * 这一层观感(大数字、编号、DX Rating)已经和官网一致。
+ * 2) 版心宽度:官方顶栏 1200px、内容盒 900px;本站顶栏保持 1200,正文收到 1080,
+ * 否则表格与列表会被压得太窄。
+ * 3) 版式:官网没有等价的界面,这里按同一套语言重新编排 —— 每块内容是一个官方
+ * `.common--box`(白底 + 上紫下粉横条 + 14px 硬影),记录列表照官网 top--update
+ * 的做法排成「盒子里的行 + 发丝分隔」,不再是一堆各自带阴影的小卡片。
+ * 4) 按钮尺寸:官方 .common--btn 是 60px 高、28px 字、10px 底影;列表里的操作按钮
+ * 按同一比例缩到 40px 高、5px 底影,避免一行按钮撑满半屏。
+ * 5) 移动端断点:官方在 769px 就收成汉堡 + 语言胶囊;本站导航是七个中文词,比官方的
+ * SVG 字标更宽,所以断点挪到 1000px(769~1000px 之间也走汉堡菜单)。
+ * 6) 记录列表密度:官方 top--update 一行 157px 高,一屏只看得到三条;本站记录里
+ * 题名、平台、通过时间都是短字段,行高收到 95px(一屏六条),大盒子的外观不变。
+ * 7) 数据页的图表:官方没有等价界面,图表由 stat-charts.js 用画布画,配色按明暗两套
+ * 写在脚本里(CSS 变量管不到 canvas),换主题时由 stats-page.js 重画。
+ * 8) 符号字形:官方那套 webfont 自己画箭头、方块、✓ 这类符号;M PLUS 1p 的 latin 子集
+ * 没覆盖它们(会被画成一根没有箭头的斜线),所以 fonts.css 另外声明了一个只圈这些码位、
+ * 指向系统符号字体的 "M PLUS 1p Symbols",排在 M PLUS 1p 之后。
+ *
+ * 背景没有偏差:整页复用官网那份动态背景(public/bg.html,
+ * 结构与样式都是归档快照原文),用 iframe 铺在内容下面,官方 CSS 的全局选择器
+ * (html/body/.wrapper…)不会碰到本站布局。夜版只是换 /assets/night/ 那套贴图,
+ * 结构与参数不变(见 theme-night.css)。
+ */
\ No newline at end of file
diff --git a/public/theme.js b/public/theme.js
new file mode 100644
index 0000000..a7b42f1
--- /dev/null
+++ b/public/theme.js
@@ -0,0 +1,124 @@
+/**
+ * maimai 官网皮肤的行为层:深色模式开关、移动端菜单开关、回到顶部。
+ *
+ * 只做这一层需要的行为,不碰 app.js 的数据逻辑;无依赖、原生 ESM。
+ *
+ * 深色模式(主题的唯一写入口,其它模块只读 dataset.mmTheme):
+ * · 主题落在 ,首屏由各页 里的一小段脚本先定好,
+ * 这里负责开关、记忆(localStorage)、跟随系统,以及下面这些跟着主题走的东西:
+ * 开关的 aria-checked 与 title、iframe 背景的主题、
+ * 画布重画(发 mm-theme-change)、换皮那一帧的过渡开关。
+ * 品牌图标不用 JS:它读 上的主题走 CSS 背景,首帧就是对的。
+ * · 背景是 iframe 里的官方动态页,换主题不重载它,改发 postMessage 就地换贴图,
+ * 官方那套入场动画不会重播。
+ */
+
+const THEME_KEY = 'mm-theme';
+
+const prefersDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+function storedTheme() {
+ try {
+ const value = localStorage.getItem(THEME_KEY);
+ return value === 'dark' || value === 'light' ? value : null;
+ } catch {
+ return null;
+ }
+}
+
+const currentTheme = () => (document.documentElement.dataset.mmTheme === 'dark' ? 'dark' : 'light');
+
+const bgFrame = document.querySelector('.mm-bg-frame');
+
+function tellBackground(theme) {
+ if (!bgFrame || !bgFrame.contentWindow) return;
+ // 背景页认的就是 data-mm-theme 那两个词,见 bg.html 的 message 监听
+ bgFrame.contentWindow.postMessage({ mmTheme: theme }, location.origin);
+}
+
+function applyTheme(theme, { remember = true, notify = true } = {}) {
+ // 换皮那一帧先把过渡关掉:只有一部分卡片/按钮带 transition,不关的话它们会慢半拍,
+ // 看上去像「有的卡片没跟着换主题」。两帧后摘掉,之后 hover 之类的过渡照旧。
+ const root = document.documentElement;
+ root.classList.add('mm-theme-switching');
+ root.dataset.mmTheme = theme;
+ root.style.colorScheme = theme;
+ const button = document.getElementById('mmThemeBtn');
+ if (button) {
+ button.setAttribute('aria-checked', String(theme === 'dark'));
+ button.title = theme === 'dark' ? '切换到浅色模式' : '切换到深色模式';
+ }
+ if (remember) {
+ try {
+ localStorage.setItem(THEME_KEY, theme);
+ } catch {
+ /* 隐私模式下写不进去就算了,当次会话照样能用 */
+ }
+ }
+ if (notify) tellBackground(theme);
+ // 画布里的图形是 JS 画的,CSS 换皮管不到,交给需要重画的模块自己接
+ document.dispatchEvent(new CustomEvent('mm-theme-change', { detail: { theme } }));
+ requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove('mm-theme-switching')));
+}
+
+// 首屏脚本已经定过主题,这里补齐图标/开关状态,并把主题同步给背景
+applyTheme(storedTheme() ?? (prefersDark() ? 'dark' : 'light'), { remember: false });
+
+const themeBtn = document.getElementById('mmThemeBtn');
+
+if (themeBtn) {
+ themeBtn.addEventListener('click', () => {
+ applyTheme(currentTheme() === 'dark' ? 'light' : 'dark');
+ });
+}
+
+// 用户没手动选过时,跟随系统切换
+window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
+ if (!storedTheme()) applyTheme(event.matches ? 'dark' : 'light', { remember: false });
+});
+
+// iframe 的 src 放在 data-src 里,这里按当前主题挂上(深色直接带 ?night=1,
+// 免得先亮后暗地闪一下);之后切换主题只发消息,不重载,官方入场动画不会重播。
+if (bgFrame) {
+ const baseSrc = bgFrame.getAttribute('data-src') || '/bg.html';
+ if (!bgFrame.getAttribute('src')) {
+ bgFrame.setAttribute('src', currentTheme() === 'dark' ? `${baseSrc}?night=1` : baseSrc);
+ }
+ bgFrame.addEventListener('load', () => tellBackground(currentTheme()));
+}
+
+const menuBtn = document.getElementById('mmMenuBtn');
+
+if (menuBtn) {
+ const setOpen = (open) => {
+ document.body.classList.toggle('mm-menu-open', open);
+ // html 上也挂一份:只锁 body 拦不住 html 那只滚动条,深色面板右上角会露一截浅色滑块
+ document.documentElement.classList.toggle('mm-menu-open', open);
+ menuBtn.setAttribute('aria-expanded', String(open));
+ };
+ menuBtn.addEventListener('click', () => setOpen(!document.body.classList.contains('mm-menu-open')));
+ document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape') setOpen(false);
+ });
+ // 窄屏展开、随后把窗口拉宽的话:菜单按钮这时已经隐藏,锁滚动的那条规则也出了媒体查询,
+ // 不收起来就会卡在一个看不见、又关不掉的展开态上
+ window.matchMedia('(min-width: 1001px)').addEventListener('change', (event) => {
+ if (event.matches) setOpen(false);
+ });
+ // 菜单里点走一项就收起来,否则新页面加载前会停在展开态
+ document.querySelectorAll('.site-header .nav a').forEach((link) => {
+ link.addEventListener('click', () => setOpen(false));
+ });
+}
+
+const topLink = document.querySelector('.mm-top');
+
+if (topLink) {
+ const sync = () => topLink.classList.toggle('off', window.scrollY < 600);
+ sync();
+ window.addEventListener('scroll', sync, { passive: true });
+ topLink.addEventListener('click', (event) => {
+ event.preventDefault();
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ });
+}
\ No newline at end of file
diff --git a/public/vendor/maimai/README.md b/public/vendor/maimai/README.md
new file mode 100644
index 0000000..75bab1d
--- /dev/null
+++ b/public/vendor/maimai/README.md
@@ -0,0 +1,37 @@
+# vendor/maimai —— maimai DX 国际版官网素材(PRiSM PLUS 时期)
+
+前端重构的参照物:`maimai DX International ver.` 官网在 **PRiSM PLUS** 时期的样式与皮肤素材。
+这里只放「原样搬过来」的东西;把它落到本站组件上的那一层在 [`public/theme.css`](../../theme.css)。
+
+## 来源
+
+| 文件 | 原始地址 | 快照 |
+| --- | --- | --- |
+| `style.css` | `https://maimai.sega.com/assets/css/style.css` | `web.archive.org/web/20250804084236` |
+| `img/**`(92 个) | `https://maimai.sega.com/assets/img/**` | 同上(线上仍在,直接抓线上) |
+| `fonts/**` + `fonts.css` | `https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;500;700;800;900` | 2026-10-10 抓取 |
+
+归档页面本身(`https://web.archive.org/web/20250804084236/https://maimai.sega.com/`)与线上现站
+(已更新到 CiRCLE PLUS)**样式表不同**:PRiSM PLUS 版的 `.common--box` 是上紫 `#ba5eea` /
+下粉 `#ffcedf`,CiRCLE PLUS 版换成了 `#f93eac` / `#6dbefe`。所以这里以归档快照为准。
+
+## 对原文件做了什么
+
+- `style.css`:只改了 URL。wayback 前缀与 `https://maimai.sega.com/assets/img/` 全部改写成
+ `/vendor/maimai/img/`(73 处),Google Fonts 的 `@import` 换成注释(字体改为自托管,
+ 见 `fonts.css`)。**选择器与数值一个都没动**,要核对官方参数直接在这个文件里查。
+ 归档页还在文件末尾追加了两段 wayback 自己的注释(版权声明与 playback timings),
+ 不属于官方文件,已删掉。
+- `img/**`:原始字节,未做任何压缩或转码。`img/MANIFEST.txt` 记录每个文件的字节数与 MD5。
+- `fonts.css`:只自托管 latin / latin-ext 子集(5 个字重 × 2 子集,共 10 个 woff2)。
+ M PLUS 1p 没有简体字,中文由 `theme.css` 里的系统字体栈回退。另外补了一个 `"M PLUS 1p Symbols"`
+ 家族:M PLUS 1p 的 latin 子集只覆盖 ↑↓ 两个箭头,↗ ✓ ▲ 这类符号会被画成一根没有箭头的
+ 斜线,所以只把那些码位圈出来交给系统符号字体(`theme.css` 的 `--mm-sans` 排在 M PLUS 1p 之后)。
+- 本站的背景宿主页 `public/bg.html` 不放在这里:它带着自己那三行 reset 和夜版切换脚本,
+ 放在 `public/` 根下,好让这个目录只留「原样搬过来」的素材。
+
+## 授权
+
+- `M PLUS 1p` 是 SIL Open Font License 1.1,可自由再分发。
+- `style.css` 与 `img/**` 是 **SEGA 的版权素材**,这里作为个人自用项目的样式参照/复刻保留,
+ 不属于 MIT 授权范围,也不代表 SEGA 授权。若要公开发布,请自行替换或取得许可。
\ No newline at end of file
diff --git a/public/vendor/maimai/fonts.css b/public/vendor/maimai/fonts.css
new file mode 100644
index 0000000..9545824
--- /dev/null
+++ b/public/vendor/maimai/fonts.css
@@ -0,0 +1,103 @@
+/* maimai DX International 官网(PRiSM PLUS 时期)使用的字体:M PLUS 1p(SIL OFL 1.1)。
+ 这里只自托管 latin / latin-ext 子集(数字与拉丁字形是官方观感的关键),
+ 中日韩字形由 theme.css 里的系统字体栈回退。
+ 来源: https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;500;700;800;900 抓取于 2026-10-10 */
+
+/* M PLUS 1p 的 latin 子集只覆盖 U+2191/U+2193 两个箭头,↗ ✓ ▲ 这类符号会被它画成一根
+ 没有箭头的斜线。这里单独声明一个只圈「M PLUS 1p 没覆盖的符号码位」的家族,
+ 交给系统符号字体渲染;其余字符仍旧走 M PLUS 1p。 */
+@font-face {
+ font-family: "M PLUS 1p Symbols";
+ src: local("Segoe UI Symbol"), local("Segoe UI"), local("Apple Symbols"), local("Noto Sans Symbols 2");
+ unicode-range: U+2190, U+2192, U+2194-21FF, U+25A0-27BF, U+2B00-2BFF, U+FE0F;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2") format("woff2");
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-400-latin.woff2") format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2") format("woff2");
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-500-latin.woff2") format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2") format("woff2");
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-700-latin.woff2") format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 800;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2") format("woff2");
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 800;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-800-latin.woff2") format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 900;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2") format("woff2");
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "M PLUS 1p";
+ font-style: normal;
+ font-weight: 900;
+ font-display: swap;
+ src: url("/vendor/maimai/fonts/mplus1p-900-latin.woff2") format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/public/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2
new file mode 100644
index 0000000..4e06334
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-400-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-400-latin.woff2
new file mode 100644
index 0000000..ac77e19
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-400-latin.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2
new file mode 100644
index 0000000..636c799
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-500-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-500-latin.woff2
new file mode 100644
index 0000000..cfa6e04
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-500-latin.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2
new file mode 100644
index 0000000..1b8d09e
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-700-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-700-latin.woff2
new file mode 100644
index 0000000..b2eb083
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-700-latin.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2
new file mode 100644
index 0000000..d0c3b48
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-800-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-800-latin.woff2
new file mode 100644
index 0000000..cc8f341
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-800-latin.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2
new file mode 100644
index 0000000..66f2b0e
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2 differ
diff --git a/public/vendor/maimai/fonts/mplus1p-900-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-900-latin.woff2
new file mode 100644
index 0000000..8f3a39e
Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-900-latin.woff2 differ
diff --git a/public/vendor/maimai/img/MANIFEST.txt b/public/vendor/maimai/img/MANIFEST.txt
new file mode 100644
index 0000000..5326d0a
--- /dev/null
+++ b/public/vendor/maimai/img/MANIFEST.txt
@@ -0,0 +1,94 @@
+source: https://maimai.sega.com/assets/img/ (PRiSM PLUS era)
+captured: 2026-10-10
+common/arrow.svg 329 cd00d3b2a1aa2e2b5949f52dad24a558
+common/arrow_double.svg 446 0b5879f64d05aa1dc3e9fdc23ad55348
+common/download.png 4205 827094c65cef3383d9bb6dec7d876227
+common/icon_blank.png 175 89c27d1b7cd4fc7bb82ff39610665523
+common/icon_download.png 1258 b86c9e5c949b768dccc6d02da8e76838
+common/icon_next.png 667 705ee4253d5288fb297c76901452ecd4
+common/icon_select.png 3199 c03b0478d7a33f1bb4207464f26a610e
+common/top.png 27682 00c901d61c45b23c51b37d87edcb91ca
+favicon.png 6608 ef1d521081485dcd0d243f5bf923b217
+footer/footer_banner.jpg 39146 13a49137d35613da2cbb54a7ca36c47f
+footer/footer_logo.png 5910 44e2b3c77749e055c5715474dd9c9e23
+footer/footer_sns_fb.png 2601 2882e1d277bb11d37fc2e68fc56cbf83
+footer/footer_sns_li.png 3497 19b49a359ac818230aff99911d17dfda
+footer/footer_sns_x.png 3556 9f88bbfd72fee27ad8125598dd58897a
+header/btn_arrow.png 1062 a6668541db9e0c9ca0a032181a06e372
+header/btn_arrow.svg 257 1c4a56dc66e329e5a6ea4985215d931d
+ogp.png 308605 539bb531128070267670e0ccb8bb9148
+play/dash.png 214 0f646c1459e9c36b7cc3c1c43480f3ba
+prism/bg/bg_aurora_pc.png 77561 5497b5bd7905e9fd74b587068cb860a7
+prism/bg/bg_aurora_sp.png 57495 e97a42bf83603df79ca0915345cfc4e2
+prism/bg/bg_cloud_back_c.png 2481 9911f23ca057640c1edc7ea15d027e50
+prism/bg/bg_cloud_back_l.png 3159 f00ef39719406641db4d8d3e9d991461
+prism/bg/bg_cloud_back_r.png 6380 2323f4753d9236f02bde7c878911dcb1
+prism/bg/bg_cloud_front_c.png 4211 11fba18997bdc5926abeb6cc4049c49c
+prism/bg/bg_cloud_front_l.png 4314 a6f46856c6b759ad45a37dd483b4bc8c
+prism/bg/bg_cloud_front_r.png 4107 f8f985b0df2581ca61485c3ce990a631
+prism/bg/bg_diamond_pink.png 293 48ba7b62ea9c739446893df834f5ff49
+prism/bg/bg_diamond_white.png 207 a6b102103edae42cd6329c2791e3c25b
+prism/bg/bg_diamond_yellow.png 207 bbcc23a8e51be8b5e432614d70ce31a5
+prism/bg/bg_moon.png 14556 f939430c51d1544b10a5dabba4540d60
+prism/bg/bg_pattern.png 227 2fd46e9c9ddcec95cff99f0414791b2e
+prism/bg/bg_rainbow_back_sp.png 6257 02c4872894e69365e2a4a7a97df14032
+prism/bg/bg_rainbow_btm_pc.png 13015 e6da0e89ec1d0fcbb0f4f3ca9cd3b522
+prism/bg/bg_rainbow_btm_sp.png 7951 634fef106b9c6cd363087d36beec21de
+prism/bg/bg_rainbow_pc.png 14960 809799f6ed17a1c12d1926515672a16d
+prism/bg/bg_rainbow_sp.png 13011 63207806f9db589b0df05ff34e1271b0
+prism/bg/bg_shines_pc.png 6610 b1450d76088e46622af0d689ad6ce979
+prism/bg/bg_shines_sp.png 5787 8d4c977e41e5f171db8ef7eb6301e72a
+prism/bg/bg_shootingstar_head.png 1150 43eb091b339f87e0bbeec39cb70fe8a0
+prism/bg/bg_shootingstar_head_green.png 2442 6e60786ee48c82228519b0def2f16337
+prism/bg/bg_shootingstar_head_pink.png 2436 55e939f2f8a2ee7741d0d980c7e0d735
+prism/bg/bg_shootingstar_tail.png 3563 945fa11339fbff7bafb853fecba7eb59
+prism/bg/bg_shootingstar_tail_green.png 7624 65ac85eeba3c2aa6feb62b1a7ba525b4
+prism/bg/bg_shootingstar_tail_pink.png 8409 c091f0c3f4631212e3a111712ca91d30
+prism/bg/bg_star_white.png 2350 ce64b60ef33d71493b63a677744cdb1b
+prism/bg/bg_star_yellow.png 2347 43b38e1237f59cb20a886528e69e0440
+prism/common/icon_header_left.svg 594 4fea9e2436d2fd407ea5c17553d90a76
+prism/common/icon_header_right.svg 592 f0ce643895d280ace29012e1a36c3a53
+prism/common/logo.png 25044 30e42459cafa4f48238b2666b37f1419
+prism/top/kv.png 227129 dac17b4963c92b8189bc7cd0a4386eaf
+prism/top/kv_logo.png 88199 0bd8f99caf8b1ef1f7d405a9c33ae2de
+prism/top/kv_logo_pc.png 188016 e76e9cb6373a9d0c2abc7a1fe0820c6a
+prism/top/kv_pc.png 392031 8d611c4e3891b3d494003f1f631b3bbd
+ranking/line_dashed_pc.png 2954 01ae4b1f39717dbc571ce45d293505e3
+ranking/line_dashed_sp.png 2890 984331ca21584984a04f7b0d040e8a2d
+ranking/rank_num_bz0.png 5181 bef6dddc56330c8b4fec77536783e62e
+ranking/rank_num_bz1.png 3237 03ad6e320c0f8d764b65691283efd548
+ranking/rank_num_bz100.png 6445 3509e9bb8f3222ab9408c6c5a0b0f3ff
+ranking/rank_num_bz2.png 4748 2ecbfca66420d7201f573e84e26ae9b6
+ranking/rank_num_bz3.png 5352 a7ed94f7217f12a6e90a4be4794085f1
+ranking/rank_num_bz4.png 3870 ad8faeaf926db2a73e004e369deb6045
+ranking/rank_num_bz5.png 4771 cdf683c607f29f4d0e14f303edba88bf
+ranking/rank_num_bz6.png 5388 28f21950f383f668b274e235cb7c4405
+ranking/rank_num_bz7.png 4152 7fa594a19a9b17cdcdf006fdd02c4e44
+ranking/rank_num_bz8.png 5407 2b07f8d98eab2b6382404297fee0afeb
+ranking/rank_num_bz9.png 5477 a7765a53524058e5761c0f0f22ba63ed
+ranking/rank_num_gold1.png 3490 a993aae223f0614911c8e72cb91448d0
+ranking/rank_num_sv0.png 5575 29ece737de4f96c10342b1cbc21ff122
+ranking/rank_num_sv1.png 3561 b756bc38f3dc43859e16698dd40b1444
+ranking/rank_num_sv2.png 5113 24bc53a9202110055c1da4d71f56b903
+ranking/rank_num_sv3.png 5719 72ba8459a334b1cee688eb6c664193d5
+ranking/rank_num_sv4.png 4293 5c80541f3d06205c4628542b35118b72
+ranking/rank_num_sv5.png 5092 a8780b5c47e721e7469d4f1bc37879be
+ranking/rank_num_sv6.png 5779 86a8bf17c35a2ee159c07fe67ea7e511
+ranking/rank_num_sv7.png 4481 4a069b41c5cab1418528b4779b096f2a
+ranking/rank_num_sv8.png 5822 22e8fc1381aefc6b512d8b3d63aded97
+ranking/rank_num_sv9.png 5802 f8da52b0d1b47620b628e7251ae86de4
+song/arrow.svg 255 29e1504d49b8b0da15a9a6b3266f4c93
+song/check.svg 214 24c8fa70832962e419b805d7daf04e7d
+song/inner_dx.png 3740 0b100bf7f84ca28288f6ffccfa8886c4
+song/inner_st.png 3400 9a579e3a5bdc52a9840851aa20c2331d
+song/lock.png 8026 baa6e48d518823bc1106c55c289d1f95
+song/piapro_logo.png 1650 8f4b97ddf187f2bd78cda20d5239752f
+song/plus.png 913 ca9172a51a3dc49351097141bdaa0280
+song/search_del.png 1082 196405ba565bcc07f1fff571c8f98d5c
+song/search_lens.svg 549 543411a2724886c04b9ed7a481d1af57
+song/utage/icon_buddy.png 3312 bf55c0de2f42a4d7515e874fe1504c94
+top/btn_chu.png 69468 48331d4bb9cd1da95b399cc1ab212024
+top/btn_top_1.png 12883 474aa55b03dd50d68166ceb0a105af39
+top/btn_top_2.png 13054 52cdb1892cc968c74e9b728aa46a6d90
+top/btn_top_4.png 21799 2e730a41deca13012068fdf12d5c9c5d
+top/btn_top_kop.png 50820 b9ab4e5b5bb9dbbaeb49afd0af70745b
diff --git a/public/vendor/maimai/img/common/arrow.svg b/public/vendor/maimai/img/common/arrow.svg
new file mode 100644
index 0000000..085ef2d
--- /dev/null
+++ b/public/vendor/maimai/img/common/arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/common/arrow_double.svg b/public/vendor/maimai/img/common/arrow_double.svg
new file mode 100644
index 0000000..202e592
--- /dev/null
+++ b/public/vendor/maimai/img/common/arrow_double.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/common/download.png b/public/vendor/maimai/img/common/download.png
new file mode 100644
index 0000000..ffbc8b0
Binary files /dev/null and b/public/vendor/maimai/img/common/download.png differ
diff --git a/public/vendor/maimai/img/common/icon_blank.png b/public/vendor/maimai/img/common/icon_blank.png
new file mode 100644
index 0000000..d337eb3
Binary files /dev/null and b/public/vendor/maimai/img/common/icon_blank.png differ
diff --git a/public/vendor/maimai/img/common/icon_download.png b/public/vendor/maimai/img/common/icon_download.png
new file mode 100644
index 0000000..0110133
Binary files /dev/null and b/public/vendor/maimai/img/common/icon_download.png differ
diff --git a/public/vendor/maimai/img/common/icon_next.png b/public/vendor/maimai/img/common/icon_next.png
new file mode 100644
index 0000000..ccc9dcb
Binary files /dev/null and b/public/vendor/maimai/img/common/icon_next.png differ
diff --git a/public/vendor/maimai/img/common/icon_select.png b/public/vendor/maimai/img/common/icon_select.png
new file mode 100644
index 0000000..5d3f7d8
Binary files /dev/null and b/public/vendor/maimai/img/common/icon_select.png differ
diff --git a/public/vendor/maimai/img/common/top.png b/public/vendor/maimai/img/common/top.png
new file mode 100644
index 0000000..3ded60e
Binary files /dev/null and b/public/vendor/maimai/img/common/top.png differ
diff --git a/public/vendor/maimai/img/favicon.png b/public/vendor/maimai/img/favicon.png
new file mode 100644
index 0000000..1d5328f
Binary files /dev/null and b/public/vendor/maimai/img/favicon.png differ
diff --git a/public/vendor/maimai/img/footer/footer_banner.jpg b/public/vendor/maimai/img/footer/footer_banner.jpg
new file mode 100644
index 0000000..2fa547c
Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_banner.jpg differ
diff --git a/public/vendor/maimai/img/footer/footer_logo.png b/public/vendor/maimai/img/footer/footer_logo.png
new file mode 100644
index 0000000..ddbbf88
Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_logo.png differ
diff --git a/public/vendor/maimai/img/footer/footer_sns_fb.png b/public/vendor/maimai/img/footer/footer_sns_fb.png
new file mode 100644
index 0000000..9598222
Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_sns_fb.png differ
diff --git a/public/vendor/maimai/img/footer/footer_sns_li.png b/public/vendor/maimai/img/footer/footer_sns_li.png
new file mode 100644
index 0000000..1e90faf
Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_sns_li.png differ
diff --git a/public/vendor/maimai/img/footer/footer_sns_x.png b/public/vendor/maimai/img/footer/footer_sns_x.png
new file mode 100644
index 0000000..0eaa1e2
Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_sns_x.png differ
diff --git a/public/vendor/maimai/img/header/btn_arrow.png b/public/vendor/maimai/img/header/btn_arrow.png
new file mode 100644
index 0000000..9b64476
Binary files /dev/null and b/public/vendor/maimai/img/header/btn_arrow.png differ
diff --git a/public/vendor/maimai/img/header/btn_arrow.svg b/public/vendor/maimai/img/header/btn_arrow.svg
new file mode 100644
index 0000000..9492485
--- /dev/null
+++ b/public/vendor/maimai/img/header/btn_arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/ogp.png b/public/vendor/maimai/img/ogp.png
new file mode 100644
index 0000000..5a1ae75
Binary files /dev/null and b/public/vendor/maimai/img/ogp.png differ
diff --git a/public/vendor/maimai/img/play/dash.png b/public/vendor/maimai/img/play/dash.png
new file mode 100644
index 0000000..6d3d38e
Binary files /dev/null and b/public/vendor/maimai/img/play/dash.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_aurora_pc.png b/public/vendor/maimai/img/prism/bg/bg_aurora_pc.png
new file mode 100644
index 0000000..fc884b2
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_aurora_pc.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_aurora_sp.png b/public/vendor/maimai/img/prism/bg/bg_aurora_sp.png
new file mode 100644
index 0000000..95971e1
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_aurora_sp.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_back_c.png b/public/vendor/maimai/img/prism/bg/bg_cloud_back_c.png
new file mode 100644
index 0000000..32552b9
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_back_c.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_back_l.png b/public/vendor/maimai/img/prism/bg/bg_cloud_back_l.png
new file mode 100644
index 0000000..eb0efe3
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_back_l.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_back_r.png b/public/vendor/maimai/img/prism/bg/bg_cloud_back_r.png
new file mode 100644
index 0000000..078b9a2
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_back_r.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_front_c.png b/public/vendor/maimai/img/prism/bg/bg_cloud_front_c.png
new file mode 100644
index 0000000..bbc3017
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_front_c.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_front_l.png b/public/vendor/maimai/img/prism/bg/bg_cloud_front_l.png
new file mode 100644
index 0000000..242e7ab
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_front_l.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_front_r.png b/public/vendor/maimai/img/prism/bg/bg_cloud_front_r.png
new file mode 100644
index 0000000..fb437a2
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_front_r.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_diamond_pink.png b/public/vendor/maimai/img/prism/bg/bg_diamond_pink.png
new file mode 100644
index 0000000..6bead5c
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_diamond_pink.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_diamond_white.png b/public/vendor/maimai/img/prism/bg/bg_diamond_white.png
new file mode 100644
index 0000000..0ce4a74
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_diamond_white.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_diamond_yellow.png b/public/vendor/maimai/img/prism/bg/bg_diamond_yellow.png
new file mode 100644
index 0000000..acd09a4
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_diamond_yellow.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_moon.png b/public/vendor/maimai/img/prism/bg/bg_moon.png
new file mode 100644
index 0000000..1e29b93
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_moon.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_pattern.png b/public/vendor/maimai/img/prism/bg/bg_pattern.png
new file mode 100644
index 0000000..110d6ec
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_pattern.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png
new file mode 100644
index 0000000..7e56299
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png
new file mode 100644
index 0000000..148fea7
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png
new file mode 100644
index 0000000..8be6192
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_pc.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_pc.png
new file mode 100644
index 0000000..959322b
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_pc.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_sp.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_sp.png
new file mode 100644
index 0000000..02eceb0
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_sp.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shines_pc.png b/public/vendor/maimai/img/prism/bg/bg_shines_pc.png
new file mode 100644
index 0000000..5ac4fb6
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shines_pc.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shines_sp.png b/public/vendor/maimai/img/prism/bg/bg_shines_sp.png
new file mode 100644
index 0000000..006cf75
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shines_sp.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_head.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head.png
new file mode 100644
index 0000000..71c9e4f
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png
new file mode 100644
index 0000000..89a9870
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png
new file mode 100644
index 0000000..4e42d4b
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png
new file mode 100644
index 0000000..80496f4
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png
new file mode 100644
index 0000000..247c9d8
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png
new file mode 100644
index 0000000..c08c4e8
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_star_white.png b/public/vendor/maimai/img/prism/bg/bg_star_white.png
new file mode 100644
index 0000000..f35da4c
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_star_white.png differ
diff --git a/public/vendor/maimai/img/prism/bg/bg_star_yellow.png b/public/vendor/maimai/img/prism/bg/bg_star_yellow.png
new file mode 100644
index 0000000..dad9408
Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_star_yellow.png differ
diff --git a/public/vendor/maimai/img/prism/common/icon_header_left.svg b/public/vendor/maimai/img/prism/common/icon_header_left.svg
new file mode 100644
index 0000000..014f7dc
--- /dev/null
+++ b/public/vendor/maimai/img/prism/common/icon_header_left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/prism/common/icon_header_right.svg b/public/vendor/maimai/img/prism/common/icon_header_right.svg
new file mode 100644
index 0000000..1ab4991
--- /dev/null
+++ b/public/vendor/maimai/img/prism/common/icon_header_right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/prism/common/logo.png b/public/vendor/maimai/img/prism/common/logo.png
new file mode 100644
index 0000000..74ae960
Binary files /dev/null and b/public/vendor/maimai/img/prism/common/logo.png differ
diff --git a/public/vendor/maimai/img/prism/top/kv.png b/public/vendor/maimai/img/prism/top/kv.png
new file mode 100644
index 0000000..a58b99c
Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv.png differ
diff --git a/public/vendor/maimai/img/prism/top/kv_logo.png b/public/vendor/maimai/img/prism/top/kv_logo.png
new file mode 100644
index 0000000..68b81cd
Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv_logo.png differ
diff --git a/public/vendor/maimai/img/prism/top/kv_logo_pc.png b/public/vendor/maimai/img/prism/top/kv_logo_pc.png
new file mode 100644
index 0000000..7255a63
Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv_logo_pc.png differ
diff --git a/public/vendor/maimai/img/prism/top/kv_pc.png b/public/vendor/maimai/img/prism/top/kv_pc.png
new file mode 100644
index 0000000..57bd82c
Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv_pc.png differ
diff --git a/public/vendor/maimai/img/ranking/line_dashed_pc.png b/public/vendor/maimai/img/ranking/line_dashed_pc.png
new file mode 100644
index 0000000..18f61dc
Binary files /dev/null and b/public/vendor/maimai/img/ranking/line_dashed_pc.png differ
diff --git a/public/vendor/maimai/img/ranking/line_dashed_sp.png b/public/vendor/maimai/img/ranking/line_dashed_sp.png
new file mode 100644
index 0000000..9ce5c33
Binary files /dev/null and b/public/vendor/maimai/img/ranking/line_dashed_sp.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz0.png b/public/vendor/maimai/img/ranking/rank_num_bz0.png
new file mode 100644
index 0000000..b104593
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz0.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz1.png b/public/vendor/maimai/img/ranking/rank_num_bz1.png
new file mode 100644
index 0000000..9f42309
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz1.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz100.png b/public/vendor/maimai/img/ranking/rank_num_bz100.png
new file mode 100644
index 0000000..31c333b
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz100.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz2.png b/public/vendor/maimai/img/ranking/rank_num_bz2.png
new file mode 100644
index 0000000..f08a927
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz2.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz3.png b/public/vendor/maimai/img/ranking/rank_num_bz3.png
new file mode 100644
index 0000000..cbbc01c
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz3.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz4.png b/public/vendor/maimai/img/ranking/rank_num_bz4.png
new file mode 100644
index 0000000..75cad0f
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz4.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz5.png b/public/vendor/maimai/img/ranking/rank_num_bz5.png
new file mode 100644
index 0000000..ab16905
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz5.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz6.png b/public/vendor/maimai/img/ranking/rank_num_bz6.png
new file mode 100644
index 0000000..3109408
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz6.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz7.png b/public/vendor/maimai/img/ranking/rank_num_bz7.png
new file mode 100644
index 0000000..b56b39c
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz7.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz8.png b/public/vendor/maimai/img/ranking/rank_num_bz8.png
new file mode 100644
index 0000000..642c8c1
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz8.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_bz9.png b/public/vendor/maimai/img/ranking/rank_num_bz9.png
new file mode 100644
index 0000000..33cd78e
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz9.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_gold1.png b/public/vendor/maimai/img/ranking/rank_num_gold1.png
new file mode 100644
index 0000000..eed8b6b
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_gold1.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv0.png b/public/vendor/maimai/img/ranking/rank_num_sv0.png
new file mode 100644
index 0000000..3b994c4
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv0.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv1.png b/public/vendor/maimai/img/ranking/rank_num_sv1.png
new file mode 100644
index 0000000..4816375
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv1.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv2.png b/public/vendor/maimai/img/ranking/rank_num_sv2.png
new file mode 100644
index 0000000..c469e24
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv2.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv3.png b/public/vendor/maimai/img/ranking/rank_num_sv3.png
new file mode 100644
index 0000000..ffb29b6
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv3.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv4.png b/public/vendor/maimai/img/ranking/rank_num_sv4.png
new file mode 100644
index 0000000..9cd5dc1
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv4.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv5.png b/public/vendor/maimai/img/ranking/rank_num_sv5.png
new file mode 100644
index 0000000..212ed60
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv5.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv6.png b/public/vendor/maimai/img/ranking/rank_num_sv6.png
new file mode 100644
index 0000000..4f703e6
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv6.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv7.png b/public/vendor/maimai/img/ranking/rank_num_sv7.png
new file mode 100644
index 0000000..c9a5d77
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv7.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv8.png b/public/vendor/maimai/img/ranking/rank_num_sv8.png
new file mode 100644
index 0000000..9b19a10
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv8.png differ
diff --git a/public/vendor/maimai/img/ranking/rank_num_sv9.png b/public/vendor/maimai/img/ranking/rank_num_sv9.png
new file mode 100644
index 0000000..280574d
Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv9.png differ
diff --git a/public/vendor/maimai/img/song/arrow.svg b/public/vendor/maimai/img/song/arrow.svg
new file mode 100644
index 0000000..7b5af85
--- /dev/null
+++ b/public/vendor/maimai/img/song/arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/song/check.svg b/public/vendor/maimai/img/song/check.svg
new file mode 100644
index 0000000..1bb791f
--- /dev/null
+++ b/public/vendor/maimai/img/song/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vendor/maimai/img/song/inner_dx.png b/public/vendor/maimai/img/song/inner_dx.png
new file mode 100644
index 0000000..ae4a54a
Binary files /dev/null and b/public/vendor/maimai/img/song/inner_dx.png differ
diff --git a/public/vendor/maimai/img/song/inner_st.png b/public/vendor/maimai/img/song/inner_st.png
new file mode 100644
index 0000000..774dd42
Binary files /dev/null and b/public/vendor/maimai/img/song/inner_st.png differ
diff --git a/public/vendor/maimai/img/song/lock.png b/public/vendor/maimai/img/song/lock.png
new file mode 100644
index 0000000..9937467
Binary files /dev/null and b/public/vendor/maimai/img/song/lock.png differ
diff --git a/public/vendor/maimai/img/song/piapro_logo.png b/public/vendor/maimai/img/song/piapro_logo.png
new file mode 100644
index 0000000..7777559
Binary files /dev/null and b/public/vendor/maimai/img/song/piapro_logo.png differ
diff --git a/public/vendor/maimai/img/song/plus.png b/public/vendor/maimai/img/song/plus.png
new file mode 100644
index 0000000..6f7cc03
Binary files /dev/null and b/public/vendor/maimai/img/song/plus.png differ
diff --git a/public/vendor/maimai/img/song/search_del.png b/public/vendor/maimai/img/song/search_del.png
new file mode 100644
index 0000000..f1f3736
Binary files /dev/null and b/public/vendor/maimai/img/song/search_del.png differ
diff --git a/public/vendor/maimai/img/song/search_lens.svg b/public/vendor/maimai/img/song/search_lens.svg
new file mode 100644
index 0000000..5eeac2e
--- /dev/null
+++ b/public/vendor/maimai/img/song/search_lens.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/public/vendor/maimai/img/song/utage/icon_buddy.png b/public/vendor/maimai/img/song/utage/icon_buddy.png
new file mode 100644
index 0000000..5390767
Binary files /dev/null and b/public/vendor/maimai/img/song/utage/icon_buddy.png differ
diff --git a/public/vendor/maimai/img/top/btn_chu.png b/public/vendor/maimai/img/top/btn_chu.png
new file mode 100644
index 0000000..1f6ee2c
Binary files /dev/null and b/public/vendor/maimai/img/top/btn_chu.png differ
diff --git a/public/vendor/maimai/img/top/btn_top_1.png b/public/vendor/maimai/img/top/btn_top_1.png
new file mode 100644
index 0000000..0346248
Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_1.png differ
diff --git a/public/vendor/maimai/img/top/btn_top_2.png b/public/vendor/maimai/img/top/btn_top_2.png
new file mode 100644
index 0000000..ea8efeb
Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_2.png differ
diff --git a/public/vendor/maimai/img/top/btn_top_4.png b/public/vendor/maimai/img/top/btn_top_4.png
new file mode 100644
index 0000000..8126ef7
Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_4.png differ
diff --git a/public/vendor/maimai/img/top/btn_top_kop.png b/public/vendor/maimai/img/top/btn_top_kop.png
new file mode 100644
index 0000000..6587cba
Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_kop.png differ
diff --git a/public/vendor/maimai/style.css b/public/vendor/maimai/style.css
new file mode 100644
index 0000000..94e2e35
--- /dev/null
+++ b/public/vendor/maimai/style.css
@@ -0,0 +1 @@
+@charset "UTF-8";/* 字体自托管:见 /vendor/maimai/fonts.css */input[type=text]{margin:0;padding:0;border:0;background-color:#fff;outline:0}.common--box{position:relative;width:87.5vw;border-radius:3.75vw;box-shadow:1.75vw 1.75vw 0 0 rgba(0,0,0,.2);background-color:#fff;margin:0 auto;padding:8.125vw 0}@media only screen and (min-width:769px){.common--box{width:81.8181818182vw;border-radius:2.7272727273vw;box-shadow:1.2727272727vw 1.2727272727vw 0 0 rgba(0,0,0,.2);padding:4.5454545455vw 0}}@media only screen and (min-width:1100px){.common--box{width:900px;border-radius:30px;box-shadow:14px 14px 0 0 rgba(0,0,0,.2);padding:50px 0}}.common--box:after,.common--box:before{position:absolute;display:block;content:"";width:75vw;height:1.5vw;left:50%;transform:translateX(-50%)}@media only screen and (min-width:769px){.common--box:after,.common--box:before{width:54.5454545455vw;height:.7272727273vw}}@media only screen and (min-width:1100px){.common--box:after,.common--box:before{width:600px;height:8px}}.common--box:before{top:0;border-radius:0 0 1.5vw 1.5vw;background-color:#ba5eea}@media only screen and (min-width:769px){.common--box:before{border-radius:0 0 .7272727273vw .7272727273vw}}@media only screen and (min-width:1100px){.common--box:before{border-radius:0 0 8px 8px}}.common--box:after{bottom:0;border-radius:1.5vw 1.5vw 0 0;background-color:#ffcedf}@media only screen and (min-width:769px){.common--box:after{border-radius:.7272727273vw .7272727273vw 0 0}}@media only screen and (min-width:1400px){.common--box:after{border-radius:8px 8px 0 0}}.common--top{position:fixed;z-index:100;display:block;bottom:2.5vw;right:2.5vw;width:15vw;transform:translateY(0);transition:.3s}.common--top.off{opacity:0;transform:translateY(30%);pointer-events:none}@media only screen and (min-width:769px){.common--top{transition:transform .3s}.common--top:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.common--top{bottom:1.4285714286vw;right:1.4285714286vw;width:8.5714285714vw;transition:.3s}}@media only screen and (min-width:1400px){.common--top{bottom:20px;right:20px;width:120px}}.common--share{position:relative;width:87.5vw;border-radius:3.75vw;box-shadow:1.75vw 1.75vw 0 0 rgba(0,0,0,.2);background-color:#fff;margin:0 auto;padding:8.125vw 0}@media only screen and (min-width:769px){.common--share{width:81.8181818182vw;border-radius:2.7272727273vw;box-shadow:1.2727272727vw 1.2727272727vw 0 0 rgba(0,0,0,.2);padding:4.5454545455vw 0}}@media only screen and (min-width:1100px){.common--share{width:900px;border-radius:30px;box-shadow:14px 14px 0 0 rgba(0,0,0,.2);padding:50px 0}}.common--share:after,.common--share:before{position:absolute;display:block;content:"";width:75vw;height:1.5vw;left:50%;transform:translateX(-50%)}@media only screen and (min-width:769px){.common--share:after,.common--share:before{width:54.5454545455vw;height:.7272727273vw}}@media only screen and (min-width:1100px){.common--share:after,.common--share:before{width:600px;height:8px}}.common--share:before{top:0;border-radius:0 0 1.5vw 1.5vw;background-color:#ba5eea}@media only screen and (min-width:769px){.common--share:before{border-radius:0 0 .7272727273vw .7272727273vw}}@media only screen and (min-width:1100px){.common--share:before{border-radius:0 0 8px 8px}}.common--share:after{bottom:0;border-radius:1.5vw 1.5vw 0 0;background-color:#ffcedf}@media only screen and (min-width:769px){.common--share:after{border-radius:.7272727273vw .7272727273vw 0 0}}@media only screen and (min-width:1400px){.common--share:after{border-radius:8px 8px 0 0}}.common--header{display:flex;justify-content:center;align-items:center;margin:0 auto}.common--header__txt{margin:0 1.125vw}@media only screen and (min-width:769px){.common--header__txt{margin:0 1.8181818182vw}}@media only screen and (min-width:1400px){.common--header__txt{margin:0 20px}}.common--header__txt svg{width:100%;fill:#0b3871;fill-rule:evenodd}.common--header__icon{background-image:url(/vendor/maimai/img/prism/common/icon_header_left.svg);background-size:contain;background-repeat:no-repeat;background-position:center center;display:block;width:14vw;height:7vw}@media only screen and (min-width:769px){.common--header__icon{width:8vw;height:4vw;margin-bottom:0}}@media only screen and (min-width:1400px){.common--header__icon{width:112px;height:56px}}.common--header__icon.right{background-image:url(/vendor/maimai/img/prism/common/icon_header_right.svg)}.common--header__icon svg{width:100%;height:100%;fill:#f8b73d;fill-rule:evenodd}.common--btn{display:flex;align-items:center;justify-content:center;color:#fff;margin:0 auto;width:61.25vw;height:11.25vw;font-size:4.25vw;border-radius:12.5vw;font-weight:900;background-color:#e72139;box-shadow:0 2vw 0 0 #a51426;position:relative;word-break:break-all;white-space:nowrap}@media only screen and (min-width:769px){.common--btn{width:36.3636363636vw;height:5.4545454545vw;font-size:2.5454545455vw;box-shadow:0 .9090909091vw 0 0 #a51426;transition:.3s}.common--btn:hover{box-shadow:none;transform:translateY(.9090909091vw)}}@media only screen and (min-width:1100px){.common--btn{width:400px;height:60px;font-size:28px;box-shadow:0 10px 0 0 #a51426}.common--btn:hover{transform:translateY(10px)}}.common--btn:after{right:4.375vw}@media only screen and (min-width:769px){.common--btn:after{right:1.8181818182vw}}@media only screen and (min-width:1100px){.common--btn:after{right:20px}}.common--btn.next:after{position:absolute;display:block;content:"";width:2.25vw;height:2.5vw;background-image:url(/vendor/maimai/img/common/icon_next.png);background-size:cover;background-repeat:no-repeat}@media only screen and (min-width:769px){.common--btn.next:after{width:1.6363636364vw;height:1.8181818182vw}}@media only screen and (min-width:1100px){.common--btn.next:after{width:18px;height:20px}}.common--btn.blank:after{position:absolute;display:block;content:"";width:3.25vw;height:2.5vw;background-image:url(/vendor/maimai/img/common/icon_blank.png);background-size:cover;background-repeat:no-repeat}@media only screen and (min-width:769px){.common--btn.blank:after{width:2.3636363636vw;height:1.8181818182vw}}@media only screen and (min-width:1100px){.common--btn.blank:after{width:26px;height:20px}}.common--btn.download:after{position:absolute;display:block;content:"";width:4.5vw;height:5vw;background-image:url(/vendor/maimai/img/common/icon_download.png);background-size:cover;background-repeat:no-repeat}@media only screen and (min-width:769px){.common--btn.download:after{width:1.8181818182vw;height:2vw}}@media only screen and (min-width:1100px){.common--btn.download:after{width:20px;height:22px}}.common--title{padding-top:21.25vw;width:88.25vw;margin:0 auto}@media only screen and (min-width:769px){.common--title{padding-top:0;margin:6.3636363636vw auto 0;width:82.3636363636vw}}@media only screen and (min-width:1100px){.common--title{width:906px;margin:70px auto 0}}@keyframes gradient{0%{background-position:0 0}50%{background-position:0 100%}100%{background-position:0 0}}@keyframes pattern{0%{background-position:0 0}100%{background-position:-20% 20%}}@keyframes diamond{0%{transform:scaleX(1)}80%{transform:scaleX(1)}90%{transform:scaleX(0)}100%{transform:scaleX(1)}}@keyframes star{0%{opacity:0;transform:rotate(0)}20%{opacity:1}40%{opacity:0;transform:rotate(1turn)}100%{opacity:0;transform:rotate(1turn)}}@keyframes auroras{0%{opacity:1}20%{opacity:.6}80%{opacity:.6}100%{opacity:1}}@keyframes aurora{0%{opacity:0;transform:translateX(0)}1%{opacity:.8}80%{opacity:.8}99%{opacity:0;transform:translateX(-100%)}100%{opacity:0;transform:translateX(0)}}@keyframes cloud{0%{opacity:0;transform:translateX(0)}20%{opacity:1}80%{opacity:1}90%{opacity:0;transform:translateX(100%)}100%{opacity:0;transform:translateX(0)}}@keyframes moon{0%{transform:rotate(0)}100%{transform:rotate(30deg)}}@keyframes shooting-star{0%{transform:translateX(0) translateY(0);opacity:0}20%{opacity:1}40%{opacity:1}60%{transform:translateX(-100%) translateY(100%);opacity:0}to{transform:translateX(0) translateY(0);opacity:0}}@keyframes shooting-star-head{0%{transform:rotate(0)}to{transform:rotate(-1turn)}}@keyframes rainbow{0%{-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);clip-path:polygon(0 0,0 0,0 100%,0 100%);transform:scale(.8)}to{-webkit-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);clip-path:polygon(0 0,100% 0,100% 100%,0 100%);transform:scale(1)}}@keyframes rainbow-bottom{0%{transform:scale(.8)}to{transform:scale(1)}}.bg{pointer-events:none;background-image:linear-gradient(0deg,#45c7e3,#7dc0fd 10%,#d3a1ff 25%,#ff99c0 50%,#d3a1ff 75%,#7dc0fd 90%,#45c7e3)}.bg.prism{width:100%;height:100%;position:fixed;top:0;left:0;z-index:0;overflow:hidden}.bg--wrap{position:relative;z-index:1;width:100%;height:100%;overflow:hidden}.bg--gradient{position:relative;top:50%;left:50%;width:calc(100% - 5.5vw);height:calc(100% - 5.5vw);border-radius:5vw;background-size:cover;background-image:linear-gradient(0deg,#fff 14%,#ffd5cf 24%,#ffd5cf 46%,#ffc5d5 56%,#eaabff 67%,#72bcfe 85%,#65f2df 95%);overflow:hidden;z-index:-99;transform:translate3d(-50%,-50%,0);animation:gradient 10s infinite linear}@media only screen and (min-width:769px){.bg--gradient{width:calc(100% - 3.1428571429vw);height:calc(100% - 3.1428571429vw);border-radius:2.8571428571vw}}.bg--gradient:before{content:"";display:block;background:transparent url(/vendor/maimai/img/prism/bg/bg_pattern.png) repeat;width:100%;height:100%;background-size:6.5vw auto;background-position:50%;position:fixed;top:0;left:0;mix-blend-mode:overlay;animation:pattern 10s infinite linear}@media only screen and (min-width:769px){.bg--gradient:before{background-size:3.7142857143vw auto}}@media only screen and (min-width:1400px){.bg--gradient:before{background-size:52px auto}}.bg--shine{position:fixed;top:0;left:0;width:100%;height:100%;z-index:3;background-image:url(/vendor/maimai/img/prism/bg/bg_shines_sp.png);background-repeat:repeat;background-size:95vw auto;background-position:center 7vw}@media only screen and (min-width:769px){.bg--shine{background-image:url(/vendor/maimai/img/prism/bg/bg_shines_pc.png);background-size:95.1428571429vw;background-position:center 1.7142857143vw}}@media only screen and (min-width:1400px){.bg--shine{background-size:1332px;background-position:center 24px}}.bg--shine__wrap{position:relative;width:100%;height:100%}.bg--shine__item{position:absolute;width:5vw;height:5vw;top:0;left:0;background-size:contain;background-repeat:no-repeat}@media only screen and (min-width:769px){.bg--shine__item{width:1.4285714286vw;height:1.4285714286vw}}@media only screen and (min-width:1400px){.bg--shine__item{width:20px;height:20px}}.bg--shine__item.diamond-pink{background-image:url(/vendor/maimai/img/prism/bg/bg_diamond_pink.png);top:9vw;left:68.25vw;animation:diamond 3s infinite 1s linear}@media only screen and (max-width:769px){.bg--shine__item.diamond-pink{width:3vw;height:3vw}}@media only screen and (min-width:769px){.bg--shine__item.diamond-pink{top:2.2857142857vw;left:23.1428571429vw}}@media only screen and (min-width:1400px){.bg--shine__item.diamond-pink{top:32px;left:324px}}.bg--shine__item.diamond-yellow{background-image:url(/vendor/maimai/img/prism/bg/bg_diamond_yellow.png);top:6.5vw;left:74.5vw;animation:diamond 3s infinite 2s linear}@media only screen and (min-width:769px){.bg--shine__item.diamond-yellow{top:21.8571428571vw;left:9.7142857143vw}}@media only screen and (min-width:1400px){.bg--shine__item.diamond-yellow{top:306px;left:136px}}.bg--shine__item.diamond-white{background-image:url(/vendor/maimai/img/prism/bg/bg_diamond_white.png);top:16.5vw;left:50.5vw;animation:diamond 3s infinite linear}@media only screen and (min-width:769px){.bg--shine__item.diamond-white{top:18.2857142857vw;left:84.2857142857vw}}@media only screen and (min-width:1400px){.bg--shine__item.diamond-white{top:256px;left:1180px}}.bg--shine__item.star-white{background-image:url(/vendor/maimai/img/prism/bg/bg_star_white.png);width:7.5vw;height:7.5vw;top:21vw;left:79.5vw;opacity:0;animation:star 3s infinite linear}@media only screen and (min-width:769px){.bg--shine__item.star-white{width:2.1428571429vw;height:2.1428571429vw;top:16.4285714286vw;left:39.4285714286vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-white{width:30px;height:30px;top:230px;left:552px}}.bg--shine__item.star-yellow{width:7.5vw;height:7.5vw;opacity:0;animation:star 3s infinite 1s linear}@media only screen and (min-width:769px){.bg--shine__item.star-yellow{width:2.1428571429vw;height:2.1428571429vw;top:16.4285714286vw;left:39.4285714286vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-yellow{width:30px;height:30px;top:230px;left:552px}}.bg--shine__item.star-yellow.i1{background-image:url(/vendor/maimai/img/prism/bg/bg_star_yellow.png);top:28.5vw;left:3vw}@media only screen and (min-width:769px){.bg--shine__item.star-yellow.i1{top:8.1428571429vw;left:.8571428571vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-yellow.i1{top:114px;left:12px}}.bg--shine__item.star-yellow.i2{background-image:url(/vendor/maimai/img/prism/bg/bg_star_yellow.png);top:5vw;left:30vw}@media only screen and (min-width:769px){.bg--shine__item.star-yellow.i2{top:28.8571428571vw;left:96.8571428571vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-yellow.i2{top:404px;left:1356px}}.bg--aurora{position:absolute;top:0;left:0;width:100%;height:61.5vw;background-image:url(/vendor/maimai/img/prism/bg/bg_aurora_sp.png);background-repeat:repeat-x;background-size:415vw;background-position:left bottom 37.5vw;animation:auroras 40s infinite linear;z-index:2}@media only screen and (min-width:769px){.bg--aurora{background-image:url(/vendor/maimai/img/prism/bg/bg_aurora_pc.png);height:17.5714285714vw;background-size:228.2857142857vw auto;background-position:0 100%;animation:auroras 50s infinite linear}}@media only screen and (min-width:1400px){.bg--aurora{height:246px;background-size:3196px auto}}.bg--aurora:after,.bg--aurora:before{content:"";display:block;background-image:url(/vendor/maimai/img/prism/bg/bg_aurora_sp.png);position:absolute;top:0;left:0;background-size:contain;background-repeat:no-repeat;width:200%}.bg--aurora:before{height:42vw;top:0;background-repeat:repeat-x;opacity:0;animation:aurora 120s infinite linear}@media only screen and (min-width:769px){.bg--aurora:before{height:33.7857142857vw;top:-17.8571428571vw;left:0}}@media only screen and (min-width:1400px){.bg--aurora:before{height:473px;top:-250px}}.bg--aurora:after{height:101.75vw;top:-16.5vw;background-repeat:repeat-x;opacity:0;animation:aurora 90s infinite linear reverse}@media only screen and (min-width:769px){.bg--aurora:after{height:12vw;top:-19.2857142857vw;left:0}}@media only screen and (min-width:1400px){.bg--aurora:after{height:168px;top:-270px}}.bg--cloud{position:absolute;width:100%;height:100%;top:0;left:0}.bg--cloud__item{display:block;position:absolute;top:0;left:0;background-size:contain;background-repeat:no-repeat;z-index:1}.bg--cloud__item.i1{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_back_l.png);top:.5vw;left:-20.5vw;width:32.75vw;height:17.5vw;animation:cloud 12s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i1{top:16.8571428571vw;left:-4.1428571429vw;width:18.7142857143vw;height:10vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i1{top:236px;left:-58px;width:262px;height:140px}}.bg--cloud__item.i2{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_back_r.png);top:28.5vw;left:48vw;width:67vw;height:35vw;animation:cloud 14s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i2{top:11.7142857143vw;left:71.4285714286vw;width:38.2857142857vw;height:20vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i2{top:164px;left:1000px;width:536px;height:280px}}.bg--cloud__item.i3{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_back_c.png);top:81vw;left:75vw;width:30.25vw;height:16.125vw;animation:cloud 13s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i3{top:29.2857142857vw;left:81.4285714286vw;width:17.2857142857vw;height:9.2142857143vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i3{top:410px;left:1140px;width:242px;height:129px}}.bg--cloud__item.i4{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_front_l.png);top:67vw;left:4.5vw;width:37.375vw;height:17.25vw;animation:cloud 18s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i4{top:26.4285714286vw;left:7.4285714286vw;width:21.3571428571vw;height:9.8571428571vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i4{top:370px;left:104px;width:299px;height:138px}}.bg--cloud__item.i5{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_front_r.png);top:36.5vw;left:66.5vw;width:35.5vw;height:20vw;animation:cloud 20s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i5{top:16.5vw;left:82.2857142857vw;width:20.3571428571vw;height:11.4285714286vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i5{top:231px;left:1152px;width:185px;height:160px}}.bg--cloud__item.i6{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_front_c.png);top:90vw;left:59vw;width:32.375vw;height:15.375vw;animation:cloud 19s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i6{top:34.5714285714vw;left:72.8571428571vw;width:18.5vw;height:8.7857142857vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i6{top:484px;left:1020px;width:259px;height:123px}}.bg--moon{position:absolute;top:4vw;left:4vw;width:24.75vw;height:24.75vw;z-index:2;background-size:contain;background-repeat:no-repeat;background-image:url(/vendor/maimai/img/prism/bg/bg_moon.png)}@media only screen and (min-width:769px){.bg--moon{top:0;left:1.4285714286vw;width:14.1428571429vw;height:14.1428571429vw;animation:moon 6s infinite linear alternate}}@media only screen and (min-width:1400px){.bg--moon{left:20px;width:198px;height:198px}}.bg--star__item{position:absolute;top:0;left:0;z-index:0;animation:shooting-star 2s infinite linear;opacity:0}.bg--star__item:after,.bg--star__item:before{content:"";display:block;position:absolute;background-size:contain;background-repeat:no-repeat}.bg--star__item:before{top:0;right:0;width:91%;height:91%;background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png);background-size:contain}.bg--star__item:after{position:absolute;bottom:0;left:0;width:14%;height:14%;background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_head.png);background-size:contain;background-repeat:no-repeat;transform-origin:50% 50%;animation:shooting-star-head 2.2s infinite linear}.bg--star__item.green:before{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png)}.bg--star__item.green:after{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png)}.bg--star__item.pink:before{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png)}.bg--star__item.pink:after{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png)}.bg--star__item.i1{width:19.25vw;height:19.25vw;top:-11.5vw;left:4vw}@media only screen and (min-width:769px){.bg--star__item.i1{width:11vw;height:11vw;top:5.7857142857vw;left:10.5vw}}@media only screen and (min-width:1400px){.bg--star__item.i1{width:154px;height:154px;top:81px;left:147px}}.bg--star__item.i2{width:19.25vw;height:19.25vw;top:-11vw;left:88vw;animation:shooting-star 2s infinite .8s linear}@media only screen and (min-width:769px){.bg--star__item.i2{width:11vw;height:11vw;top:5.2857142857vw;left:34.7142857143vw}}@media only screen and (min-width:1400px){.bg--star__item.i2{width:154px;height:154px;top:74px;left:486px}}.bg--star__item.i3{width:38.5vw;height:38.5vw;top:11.5vw;left:9vw}@media only screen and (min-width:769px){.bg--star__item.i3{width:22vw;height:22vw;top:1.4285714286vw;left:31.1428571429vw}}@media only screen and (min-width:1400px){.bg--star__item.i3{width:308px;height:308px;top:20px;left:436px}}.bg--star__item.i4{width:38.5vw;height:38.5vw;top:3vw;left:313vw;animation:shooting-star 2s infinite 1s linear}@media only screen and (min-width:769px){.bg--star__item.i4{width:11vw;height:11vw;top:.8571428571vw;left:89.4285714286vw}}@media only screen and (min-width:1400px){.bg--star__item.i4{width:154px;height:154px;top:12px;left:1252px}}.bg--star__item.i5{display:none}@media only screen and (min-width:769px){.bg--star__item.i5{display:block;width:22vw;height:22vw;top:-4.2857142857vw;left:87.1428571429vw;animation:shooting-star 2s infinite .8s linear}}@media only screen and (min-width:1400px){.bg--star__item.i5{width:308px;height:308px;top:-60px;left:1220px}}.bg--star__item.i6{display:none}@media only screen and (min-width:769px){.bg--star__item.i6{display:block;width:11vw;height:11vw;top:21.7142857143vw;left:87.8571428571vw;animation:shooting-star 2s infinite 1s linear}}@media only screen and (min-width:1400px){.bg--star__item.i6{width:154px;height:154px;top:304px;left:1230px}}.bg--star__item.i7{display:none}@media only screen and (min-width:769px){.bg--star__item.i7{display:block;width:18.1428571429vw;height:18.1428571429vw;top:14.5714285714vw;left:110vw;animation:shooting-star 2s infinite 1s linear}}@media only screen and (min-width:1400px){.bg--star__item.i7{width:254px;height:254px;top:204px;left:1540px}}.bg--rainbow:before{content:"";display:block;position:absolute;background-repeat:no-repeat;width:100%;height:90vw;bottom:44vw;background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_sp.png);background-size:100%;background-repeat:no-repeat;background-position:50%;-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);clip-path:polygon(0 0,0 0,0 100%,0 100%);animation:rainbow 2s ease forwards;z-index:3}@media only screen and (min-width:769px){.bg--rainbow:before{background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_pc.png);background-size:54.5vw 28.5vw;height:28.5vw;bottom:10vw}}@media only screen and (min-width:1400px){.bg--rainbow:before{background-size:763px 399px;height:399px;bottom:140px}}.bg--rainbow__cloud{position:absolute;background-repeat:no-repeat;width:100%;height:57.5vw;bottom:35vw;background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png);background-size:100vw 27vw;background-position:50%;animation:rainbow-bottom 2s ease forwards;z-index:3}@media only screen and (min-width:769px){.bg--rainbow__cloud{background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png);background-size:98.5714285714vw 16.4285714286vw;height:16.4285714286vw;bottom:2.8571428571vw}}@media only screen and (min-width:1400px){.bg--rainbow__cloud{background-size:1380px 230px;height:230px;bottom:40px}}.bg--rainbow__cloud:after{content:"";display:block;position:absolute;background-repeat:no-repeat;width:100%;height:100%;top:-26vw;background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png);background-size:119%;background-position:0;z-index:-4}@media only screen and (min-width:769px){.bg--rainbow__cloud:after{display:none}}.header{position:relative;z-index:1000000}.header--language{width:93.125vw;height:10vw;top:3.75vw;border-radius:100px;background-color:#fff;position:absolute;left:50%;transform:translateX(-50%);box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2);display:flex;align-items:center;justify-content:space-between;padding-left:3.75vw}@media only screen and (min-width:769px){.header--language{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.header--language{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}@media only screen and (min-width:769px){.header--language{display:none}}.header--language__link{background-color:#ba5eea;border-radius:0 100px 100px 0;width:23.875vw;padding-left:2.5vw;height:100%;display:flex;align-items:center}.header--language__text{font-size:2.5vw;font-weight:800;flex:1}.header--language__svg{width:15vw;margin-top:.5vw}.header--language__svg .svg{width:100%;height:auto;fill:#fff;fill-rule:evenodd}.header--language__arrow{width:3.75vw;margin-left:1.25vw;font-size:0;position:relative}.header--language__arrow img{position:relative;z-index:10}.header--language__arrow::after{display:block;content:"";position:absolute;background-color:#ba5eea;width:1.875vw;height:1.875vw;top:50%;left:50%;transform:translate(-50%,-50%);z-index:0}.header--btn{width:15vw;height:15vw;background-color:#fff;border:1.25vw solid #e72139;position:fixed;z-index:10;top:7.5vw;right:2.5vw;border-radius:100px;box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2);display:flex;align-items:center;justify-content:center;flex-direction:column;transition:.3s;transform:translateY(0)}@media only screen and (min-width:769px){.header--btn{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.header--btn{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}.header--btn.off{opacity:0;transform:translateY(-100%);pointer-events:none}@media only screen and (min-width:769px){.header--btn{display:none}}.header--btn span{display:block;width:5.625vw;height:1vw;background-color:#e72139;border-radius:100px;position:relative}.header--btn span:first-child{top:-1vw}.header--btn span:last-child{top:1vw}.header--nav{padding-top:7.5vw;position:fixed;top:-100%;left:0;width:100%;height:100%;background-color:#fff;z-index:1000;transition:all .3s;opacity:0;box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2)}@media only screen and (min-width:769px){.header--nav{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.header--nav{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}@media only screen and (min-width:769px){.header--nav{display:flex;align-items:flex-end;justify-content:space-between;opacity:1;margin:1.4285714286vw auto 0;width:85.7142857143vw;height:7.1428571429vw;border-radius:3.5714285714vw;padding-top:0;padding-left:2.3571428571vw;position:relative;top:auto;left:auto;transition:none}}@media only screen and (min-width:1400px){.header--nav{margin:20px auto 0;width:1200px;height:100px;border-radius:50px;padding-left:33px}}.header--nav.open{top:0;opacity:1}.header--nav__btn{width:15vw;height:15vw;background-color:#ba5eea;position:absolute;top:7.5vw;right:2.5vw;border-radius:100px;display:flex;align-items:center;justify-content:center;flex-direction:column}@media only screen and (min-width:769px){.header--nav__btn{display:none}}.header--nav__btn span{display:block;width:6.25vw;height:1vw;background-color:#fff;border-radius:100px;position:relative}.header--nav__btn span:first-child{top:.5vw;transform:rotate(45deg)}.header--nav__btn span:last-child{top:-.5vw;transform:rotate(-45deg)}.header--nav__logo{display:block;width:52.625vw;transform:translate3d(0,0,0)}@media only screen and (max-width:769px){.header--nav__logo{position:relative;z-index:2;left:50%;transform:translate3d(-50%,0,0)}}@media only screen and (min-width:769px){.header--nav__logo{width:12.7857142857vw;margin:0 0 1.5vw;transition:opacity .3s}.header--nav__logo:hover{opacity:.7}}@media only screen and (min-width:1400px){.header--nav__logo{width:179px;margin:0 0 21px}}.header--nav__inner{margin:6.5vw 0 0 0}@media only screen and (min-width:769px){.header--nav__inner{margin:0 3.5714285714vw 1.4285714286vw 0}}@media only screen and (min-width:1400px){.header--nav__inner{margin:0 50px 20px 0}}@media only screen and (min-width:769px){.header--nav__list{display:flex}}.header--nav__item{margin:5vw 0 0}@media only screen and (min-width:769px){.header--nav__item{display:flex;align-items:center;margin:0}}@media only screen and (min-width:769px){.header--nav__item:not(:last-child){margin:0 .8571428571vw 0 0}}@media only screen and (min-width:1400px){.header--nav__item:not(:last-child){margin:0 12px 0 0}}.header--nav__item:not(:last-child)::after{display:block;content:"";width:3.75vw;height:.5vw;background-color:#e5e5e5;margin:5vw auto 0}@media only screen and (min-width:769px){.header--nav__item:not(:last-child)::after{width:1px;height:1vw;margin:0 0 0 1vw}}@media only screen and (min-width:1400px){.header--nav__item:not(:last-child)::after{height:14px;margin:0 0 0 14px}}.header--nav__link{display:flex;align-items:center;justify-content:center;margin:0 auto;position:relative}@media only screen and (min-width:769px){.header--nav__link.on::before,.header--nav__link:hover::before{position:absolute;bottom:-1.4285714286vw;content:"";display:block;width:100%;background-color:#ba5eea;height:.3571428571vw;border-radius:100px 100px 0 0}}@media only screen and (min-width:1400px){.header--nav__link.on::before,.header--nav__link:hover::before{height:5px;bottom:-20px}}.header--nav__link.blank:after{display:block;content:"";background-image:url('data:image/svg+xml;utf8, ');background-size:contain;background-repeat:no-repeat;width:3.25vw;height:2.5vw;margin-left:1.75vw}@media only screen and (min-width:769px){.header--nav__link.blank:after{width:.7857142857vw;height:.5714285714vw;margin-left:.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__link.blank:after{width:11px;height:8px;margin-left:6px}}.header--nav__link svg{fill:#0b3871;fill-rule:evenodd;height:6.25vw}@media only screen and (min-width:769px){.header--nav__link svg{height:1.2857142857vw}}@media only screen and (min-width:1400px){.header--nav__link svg{height:18px}}.header--nav__link.l01{width:15.375vw}@media only screen and (min-width:769px){.header--nav__link.l01{width:4.5vw}}@media only screen and (min-width:1400px){.header--nav__link.l01{width:63px}}.header--nav__link.l02{width:31.75vw}@media only screen and (min-width:769px){.header--nav__link.l02{width:9.1428571429vw}}@media only screen and (min-width:1400px){.header--nav__link.l02{width:128px}}.header--nav__link.l03{width:24.875vw}@media only screen and (min-width:769px){.header--nav__link.l03{width:7.2142857143vw}}@media only screen and (min-width:1400px){.header--nav__link.l03{width:101px}}.header--nav__link.l04{width:21.25vw}@media only screen and (min-width:769px){.header--nav__link.l04{width:6.0714285714vw}}@media only screen and (min-width:1400px){.header--nav__link.l04{width:85px}}.header--nav__link.l05{width:25.875vw}@media only screen and (min-width:769px){.header--nav__link.l05{width:7.5vw}}@media only screen and (min-width:1400px){.header--nav__link.l05{width:105px}}.header--nav__link.l06{width:19.5vw}@media only screen and (min-width:769px){.header--nav__link.l06{width:5.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__link.l06{width:76px}}.header--nav__link.l07{width:42.625vw}@media only screen and (min-width:769px){.header--nav__link.l07{width:12vw}}@media only screen and (min-width:1400px){.header--nav__link.l07{width:168px}}.header--nav__language{display:none}@media only screen and (min-width:769px){.header--nav__language{display:flex;align-items:center;justify-content:flex-end;margin-bottom:1.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__language{margin-bottom:20px}}@media only screen and (min-width:769px){.header--nav__language__text{font-size:1vw;font-weight:800}}@media only screen and (min-width:1400px){.header--nav__language__text{font-size:14px}}@media only screen and (min-width:769px){.header--nav__language__link{display:flex;align-items:center;justify-content:center;position:relative;background-color:#ba5eea;width:13.5714285714vw;height:2.1428571429vw;border-radius:1000px}}.header--nav__language__link:hover{background-color:#ff699d}.header--nav__language__link:hover:before{background-color:#ff699d}.header--nav__language__link:hover .header--nav__language__linkText svg{fill:#fff;fill-rule:evenodd}@media only screen and (min-width:1400px){.header--nav__language__link{width:190px;height:30px}}@media only screen and (min-width:769px){.header--nav__language__btn{position:relative;width:13.5714285714vw;height:2.1428571429vw;display:flex;align-items:center;justify-content:center;margin-left:1.0714285714vw}}@media only screen and (min-width:1400px){.header--nav__language__btn{width:190px;height:30px;margin-left:15px}}@media only screen and (min-width:769px){.header--nav__language__btn:after{display:block;content:"";background-image:url(/vendor/maimai/img/header/btn_arrow.svg);background-size:contain;background-repeat:no-repeat;position:absolute;width:1vw;height:1vw;right:.7142857143vw}}@media only screen and (min-width:1400px){.header--nav__language__btn:after{width:15px;height:15px;right:10px}}@media only screen and (min-width:769px){.header--nav__language__btn:before{position:absolute;display:block;content:"";background-color:#ba5eea;width:.7142857143vw;height:.7142857143vw;right:.8571428571vw;border-radius:100px}}@media only screen and (min-width:1400px){.header--nav__language__btn:before{width:10px;height:10px;right:12px}}@media only screen and (min-width:769px){.header--nav__language__linkText{width:9.7857142857vw;margin-left:-.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__language__linkText{width:137px;margin-left:-6px}}.header--nav__language__linkText svg{fill:#fff;fill-rule:evenodd}.song .header--btn{top:17.5vw}.footer--share{width:87.5vw;border-radius:12.5vw;background-color:#fff;margin:17.5vw auto 0;box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2);position:relative;padding:4vw 0 5vw}@media only screen and (min-width:769px){.footer--share{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.footer--share{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}@media only screen and (min-width:769px){.footer--share{width:54.5454545455vw;border-radius:9.0909090909vw;margin:7.2727272727vw auto 0;padding:1.8181818182vw 0 2.7272727273vw}}@media only screen and (min-width:1100px){.footer--share{width:600px;border-radius:100px;margin:80px auto 0;padding:20px 0 30px}}.footer--share:after,.footer--share:before{position:absolute;display:block;content:"";width:50vw;height:1.5vw;left:50%;transform:translateX(-50%)}@media only screen and (min-width:769px){.footer--share:after,.footer--share:before{width:31.8181818182vw;height:.7272727273vw}}@media only screen and (min-width:1100px){.footer--share:after,.footer--share:before{width:350px;height:8px}}.footer--share:before{top:0;border-radius:0 0 1.5vw 1.5vw;background-color:#ba5eea}@media only screen and (min-width:769px){.footer--share:before{border-radius:0 0 .7272727273vw .7272727273vw}}@media only screen and (min-width:1100px){.footer--share:before{border-radius:0 0 8px 8px}}.footer--share:after{bottom:0;border-radius:1.5vw 1.5vw 0 0;background-color:#ffcedf}@media only screen and (min-width:769px){.footer--share:after{border-radius:.7272727273vw .7272727273vw 0 0}}@media only screen and (min-width:1400px){.footer--share:after{border-radius:8px 8px 0 0}}.footer--share__header{width:18.25vw;margin:0 auto}@media only screen and (min-width:769px){.footer--share__header{width:11.8181818182vw}}@media only screen and (min-width:1400px){.footer--share__header{width:130px}}.footer--share__header svg{width:100%;fill:#0b3871;fill-rule:evenodd}.footer--sns{display:flex;justify-content:center;align-items:center;margin-top:2.5vw}@media only screen and (min-width:769px){.footer--sns{margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.footer--sns{margin-top:10px}}.footer--sns__link{display:block;width:10.125vw}@media only screen and (min-width:769px){.footer--sns__link{width:6.3636363636vw;transition:.3s}.footer--sns__link:hover{opacity:.7}}@media only screen and (min-width:1100px){.footer--sns__link{width:70px}}.footer--sns__item:nth-child(2){margin:0 2.5vw}@media only screen and (min-width:769px){.footer--sns__item:nth-child(2){margin:0 1.8181818182vw}}@media only screen and (min-width:1100px){.footer--sns__item:nth-child(2){margin:0 20px}}.footer--bottom{background-color:#fff;width:100%;height:106.5vw;margin-top:10vw;padding:18.75vw 6.25vw;color:#0b3871;position:relative}@media only screen and (min-width:769px){.footer--bottom{height:25vw;margin-top:6.4285714286vw;padding:5.9285714286vw 0 0}}@media only screen and (min-width:1400px){.footer--bottom{height:350px;margin-top:90px;padding:83px 0 0}}.footer--bottom:before{content:"";display:block;background-color:#fff;box-shadow:0 -1.25vw 1.25vw -.5vw rgba(28,112,176,.7);height:2.5vw;width:100%;opacity:.5;position:absolute;top:-2.5vw;left:0;right:0}@media only screen and (min-width:769px){.footer--bottom:before{box-shadow:0 -.2857142857vw .3571428571vw -.1428571429vw rgba(28,112,176,.8);height:.7142857143vw;top:-.7142857143vw}}@media only screen and (min-width:1400px){.footer--bottom:before{box-shadow:0 -4px 5px -2px rgba(28,112,176,.8);height:10px;top:-10px}}@media only screen and (min-width:769px){.footer--bottom__bnr{transition:transform .3s}.footer--bottom__bnr:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.footer--bottom__bnr{display:block;width:35.7142857143vw;margin:0 auto}}@media only screen and (min-width:1400px){.footer--bottom__bnr{width:500px}}.footer--bottom__wrapper{margin-top:13.625vw}@media only screen and (min-width:769px){.footer--bottom__wrapper{width:70vw;margin:3.8571428571vw auto 0;display:flex;justify-content:space-between;align-items:flex-start}}@media only screen and (min-width:1400px){.footer--bottom__wrapper{margin:54px auto 0;width:980px}}.footer--bottom__text{font-size:2.75vw}@media only screen and (min-width:769px){.footer--bottom__text{font-size:.7857142857vw}}@media only screen and (min-width:1400px){.footer--bottom__text{font-size:11px}}.footer--bottom__logo{width:31.25vw;margin:7.5vw auto;display:block}@media only screen and (min-width:769px){.footer--bottom__logo{width:8vw;margin:0;transition:.3s}.footer--bottom__logo:hover{opacity:.7}}@media only screen and (min-width:1400px){.footer--bottom__logo{width:112px}}.top--kv{width:100vw;margin:0 auto 1.5vw;position:relative;height:143.875vw;z-index:2;overflow:hidden}@media only screen and (max-width:769px){.top--kv{margin-left:0;margin-right:0;padding-top:3.75vw}}@media only screen and (min-width:769px){.top--kv{margin:0 auto;height:auto;margin-top:-.8571428571vw;padding-left:.7142857143vw;padding-right:11.5vw;width:68.4285714286vw;padding-bottom:9.3571428571vw;margin-bottom:-.8571428571vw}}@media only screen and (min-width:1400px){.top--kv{margin-top:-12px;padding-left:10px;padding-right:161px;width:958px;padding-bottom:131px;margin-bottom:-12px}}.top--kv__logo{position:absolute;bottom:0vw;left:calc(50% - 50vw);width:100vw}@media only screen and (min-width:769px){.top--kv__logo{width:49.2857142857vw;bottom:0;left:calc(50% - 24.1428571429vw)}}@media only screen and (min-width:1400px){.top--kv__logo{width:690px;left:calc(50% - 338px)}}.top--link{display:flex;width:91.25vw;flex-wrap:wrap;margin:-2.5vw auto 0;justify-content:space-between;align-items:flex-end;position:relative;z-index:100}@media only screen and (min-width:769px){.top--link{width:78.5714285714vw;margin-top:0}}@media only screen and (min-width:1400px){.top--link{width:1100px}}@media only screen and (max-width:769px){.top--link__item:nth-child(n+3){margin-top:2.5vw}}.top--link__link{display:block;width:45vw;font-size:0}@media only screen and (min-width:769px){.top--link__link{transition:transform .3s}.top--link__link:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.top--link__link{width:18.8571428571vw}}@media only screen and (min-width:1400px){.top--link__link{width:264px}}.top--info{margin-top:6.25vw}@media only screen and (min-width:769px){.top--info{margin-top:4.2857142857vw}}@media only screen and (min-width:1400px){.top--info{margin-top:60px}}.top--info__header--txt{width:50vw;height:3.75vw}@media only screen and (min-width:769px){.top--info__header--txt{width:32.7272727273vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.top--info__header--txt{width:360px;height:28px}}.top--info__wrapper{position:relative}.top--info__container{width:75vw;margin-top:6.25vw}@media only screen and (min-width:769px){.top--info__container{width:63.6363636364vw;margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__container{width:700px;margin-top:40px}}.top--info__arrow{width:8.75vw;height:8.75vw;background-color:#ff5668;border-radius:12.5vw;transition:.3s}@media only screen and (min-width:769px){.top--info__arrow{width:4.5454545455vw;height:4.5454545455vw}.top--info__arrow:hover{background-color:#ccc}}@media only screen and (min-width:1100px){.top--info__arrow{width:50px;height:50px}}.top--info__arrow.prev{transform:scale(-1,1);left:1.25vw}@media only screen and (min-width:769px){.top--info__arrow.prev{left:2.7272727273vw}}@media only screen and (min-width:1100px){.top--info__arrow.prev{left:30px}}.top--info__arrow.next{right:1.25vw}@media only screen and (min-width:769px){.top--info__arrow.next{right:2.7272727273vw}}@media only screen and (min-width:1400px){.top--info__arrow.next{right:30px}}.top--info__arrow:after{display:block;content:"";background-image:url(/vendor/maimai/img/common/arrow.svg);background-size:cover;width:6.25vw;height:6.25vw}@media only screen and (min-width:769px){.top--info__arrow:after{width:3.6363636364vw;height:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__arrow:after{width:40px;height:40px}}.top--info__pagination{position:relative;display:flex;justify-content:center;margin-top:5vw}@media only screen and (min-width:769px){.top--info__pagination{margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__pagination{margin-top:40px}}.top--info__pagination .swiper-pagination-bullet{background-color:#ccc;opacity:1;width:2vw;height:2vw;margin:0 .625vw}@media only screen and (min-width:769px){.top--info__pagination .swiper-pagination-bullet{width:1.4545454545vw;height:1.4545454545vw;margin:0 .4545454545vw}}@media only screen and (min-width:1100px){.top--info__pagination .swiper-pagination-bullet{width:16px;height:16px;margin:0 5px}}.top--info__pagination .swiper-pagination-bullet-active{background-color:#e72139}.top--info__btn{margin-top:5vw}@media only screen and (max-width:769px){.top--info__btn{width:57.5vw}}@media only screen and (min-width:769px){.top--info__btn{margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__btn{margin-top:40px}}.top--info__btn.blank:after{right:4.375vw}@media only screen and (min-width:769px){.top--info__btn.blank:after{right:2.7272727273vw}}@media only screen and (min-width:1100px){.top--info__btn.blank:after{right:30px}}.top--info__btn.customer{justify-content:flex-start;padding-left:2.875vw}@media only screen and (min-width:769px){.top--info__btn.customer{padding-left:2.9090909091vw}}@media only screen and (min-width:1100px){.top--info__btn.customer{padding-left:32px}}.top--update{margin-top:6.25vw}@media only screen and (min-width:769px){.top--update{margin-top:4.2857142857vw}}@media only screen and (min-width:1400px){.top--update{margin-top:60px}}.top--update__header--txt{width:40.625vw;height:4.375vw}@media only screen and (min-width:769px){.top--update__header--txt{width:26.1818181818vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.top--update__header--txt{width:288px;height:28px}}.top--update__box{width:75vw;height:65vw;margin:8.75vw auto 0;overflow:scroll;padding-right:2.5vw}@media only screen and (min-width:769px){.top--update__box{width:56.3636363636vw;height:17.2727272727vw;margin:3.6363636364vw auto 0;overflow:scroll;padding-right:1.8181818182vw}}@media only screen and (min-width:1100px){.top--update__box{width:620px;height:190px;margin:40px auto 0;overflow:scroll;padding-right:20px}}.top--update__box::-webkit-scrollbar{width:2.5vw}@media only screen and (min-width:769px){.top--update__box::-webkit-scrollbar{width:.9090909091vw}}@media only screen and (min-width:1400px){.top--update__box::-webkit-scrollbar{width:10px}}.top--update__box::-webkit-scrollbar-track{background:0 0}.top--update__box::-webkit-scrollbar-thumb{background:#e72139;border-radius:100px}.top--update__item{width:100%;padding-bottom:5vw;border-bottom:1px solid #000}@media only screen and (min-width:769px){.top--update__item{display:flex;padding-bottom:1.8181818182vw}}@media only screen and (min-width:1400px){.top--update__item{padding-bottom:20px}}.top--update__item:last-child{border:none}.top--update__item:not(:first-child){margin-top:3.75vw}@media only screen and (min-width:769px){.top--update__item:not(:first-child){margin-top:2.7272727273vw}}@media only screen and (min-width:1400px){.top--update__item:not(:first-child){margin-top:30px}}.top--update__time{color:#e72139;font-size:3.5vw;font-weight:900}@media only screen and (min-width:769px){.top--update__time{font-size:1.4545454545vw;margin-top:.4545454545vw}}@media only screen and (min-width:1100px){.top--update__time{font-size:16px;margin-top:5px}}.top--update__text{font-size:3vw;margin-top:1.875vw;line-height:1.5;color:#0b3871;font-weight:500}@media only screen and (min-width:769px){.top--update__text{font-size:1.2727272727vw;margin-left:2.7272727273vw;margin-top:0}}@media only screen and (min-width:1100px){.top--update__text{font-size:14px;margin-left:30px}}.top--update__text>a{text-decoration:underline;color:#e72139}.top--update__text>a:hover{text-decoration:none}.top--fb{margin-top:6.25vw}@media only screen and (min-width:769px){.top--fb{margin-top:4.2857142857vw}}@media only screen and (min-width:1400px){.top--fb{margin-top:60px}}.top--fb__header--txt{width:25.5vw;height:4.375vw}@media only screen and (min-width:769px){.top--fb__header--txt{width:16.3636363636vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.top--fb__header--txt{width:180px;height:28px}}.top--fb__page{margin:6.25vw auto 0;width:62.5vw;max-width:340px}@media only screen and (min-width:769px){.top--fb__page{margin-top:4.5454545455vw;width:45.4545454545vw;max-width:500px}}@media only screen and (min-width:1100px){.top--fb__page{margin-top:50px}}.top--chu{display:block;width:78.75vw;margin:10vw auto 0;padding-left:1.25vw;font-size:0}@media only screen and (min-width:769px){.top--chu{transition:transform .3s}.top--chu:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.top--chu{width:23.4285714286vw;margin:5.7142857143vw auto 0;padding-left:.7142857143vw}}@media only screen and (min-width:1400px){.top--chu{width:328px;margin:80px auto 0;padding-left:10px}}.play--nav{display:flex;justify-content:center;flex-wrap:wrap;width:88.75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.play--nav{width:64.2857142857vw;margin:3.5714285714vw auto 0}}@media only screen and (min-width:1400px){.play--nav{width:915px;margin:50px auto 0}}.play--nav__link{display:block;box-shadow:.625vw .625vw 0 rgba(0,0,0,.2);width:42.75vw;height:10.625vw;border-radius:5.25vw;background-color:#4b4b4b;display:flex;align-items:center;justify-content:center}.play--nav__link.on{background-color:#ba5eea}@media only screen and (min-width:769px){.play--nav__link{transition:.3s;width:20.8571428571vw;height:4.2857142857vw;border-radius:3vw;box-shadow:.3571428571vw .3571428571vw 0 rgba(0,0,0,.2)}.play--nav__link:hover{background-color:#ba5eea}}@media only screen and (min-width:1400px){.play--nav__link{width:292px;height:60px;border-radius:42px;box-shadow:5px 5px 0 rgba(0,0,0,.2)}}@media only screen and (max-width:768px){.play--nav__item:nth-child(n+3){margin-top:2vw}.play--nav__item:nth-of-type(n+2){margin-left:3.25vw}.play--nav__item:nth-of-type(2n+1){margin-left:0}}@media only screen and (min-width:769px){.play--nav__item:nth-of-type(n+2){margin-left:.8571428571vw}.play--nav__item:nth-child(n+4){margin-top:.8571428571vw}.play--nav__item:nth-of-type(4n+0){margin-left:0}}@media only screen and (min-width:1400px){.play--nav__item:nth-of-type(n+3){margin-left:12px}.play--nav__item:nth-child(n+4){margin-top:12px}.play--nav__item:nth-of-type(4n+0){margin-left:0}}.play--nav__text.l1{width:22.25vw;margin-bottom:.625vw}@media only screen and (min-width:769px){.play--nav__text.l1{width:10.3571428571vw;margin-bottom:0}}@media only screen and (min-width:1400px){.play--nav__text.l1{width:145px}}.play--nav__text.l2{width:26.125vw}@media only screen and (min-width:769px){.play--nav__text.l2{width:12.1428571429vw;margin-top:.2857142857vw}}@media only screen and (min-width:1400px){.play--nav__text.l2{width:170px;margin-top:4px}}.play--nav__text.l3{width:31vw;margin-top:.5vw}@media only screen and (min-width:769px){.play--nav__text.l3{width:13.2857142857vw;margin-top:.1428571429vw}}@media only screen and (min-width:1400px){.play--nav__text.l3{width:186px;margin-top:2px}}.play--nav__text.l4{width:30.25vw;margin-bottom:.2857142857vw}@media only screen and (min-width:769px){.play--nav__text.l4{width:13.1428571429vw;margin-bottom:0}}@media only screen and (min-width:1400px){.play--nav__text.l4{width:184px}}.play--nav__text.l5{width:30.875vw;margin-bottom:.2857142857vw}@media only screen and (min-width:769px){.play--nav__text.l5{width:13.2857142857vw;margin-bottom:0}}@media only screen and (min-width:1400px){.play--nav__text.l5{width:186px}}.play--nav__text.l6{width:25vw}@media only screen and (min-width:769px){.play--nav__text.l6{width:10.9285714286vw}}@media only screen and (min-width:1400px){.play--nav__text.l6{width:158px}}.play--text{font-size:4.75vw;text-align:center;color:#e72139;font-weight:900;margin-top:12.5vw;line-height:1.5}@media only screen and (min-width:769px){.play--text{font-size:2.5454545455vw;margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text{font-size:28px;margin-top:50px}}.play--text:after{display:block;content:"";width:5vw;height:1vw;margin:4vw auto 0;background-color:#bebebe;border-radius:100px}@media only screen and (min-width:769px){.play--text:after{width:3.6363636364vw;height:.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--text:after{width:40px;height:8px;margin:40px auto 0}}.play--text.first{margin-top:5vw}@media only screen and (min-width:769px){.play--text.first{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text.first{margin-top:50px}}.play--text.second{margin-top:12vw}@media only screen and (min-width:769px){.play--text.second{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text.second{margin-top:50px}}.play--text.thrd{margin-top:10vw}@media only screen and (min-width:769px){.play--text.thrd{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text.thrd{margin-top:50px}}.play--text.thrd.chinese{font-family:ヒラギノ角ゴ Pro W3,Hiragino Kaku Gothic Pro,メイリオ,Meiryo,sans-serif;font-weight:700}.play--about{margin-top:6.25vw}@media only screen and (min-width:769px){.play--about{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--about{margin-top:50px}}.play--about__header .common--header__txt{width:36.25vw;height:4vw}@media only screen and (min-width:769px){.play--about__header .common--header__txt{width:23.2727272727vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.play--about__header .common--header__txt{width:256px;height:28px}}.play--about__img.i1{width:100%}.play--about__img.i2{width:81.875vw;margin:10.75vw auto 0}@media only screen and (min-width:769px){.play--about__img.i2{width:69.4545454545vw;margin:5.4545454545vw auto 0}}@media only screen and (min-width:1100px){.play--about__img.i2{width:764px;margin:60px auto 0}}.play--about__img.i3{width:84vw}@media only screen and (min-width:769px){.play--about__img.i3{width:72.4545454545vw;margin:3.6363636364vw auto}}@media only screen and (min-width:1100px){.play--about__img.i3{width:797px;margin:40px auto}}.play--about__img.i4{width:82.125vw;margin-left:2.25vw}@media only screen and (min-width:769px){.play--about__img.i4{width:76.8181818182vw;margin:3.6363636364vw auto}}@media only screen and (min-width:1100px){.play--about__img.i4{width:845px;margin:40px auto}}.play--about__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;background-size:3vw auto;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--about__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0;background-size:2.1818181818vw auto}}@media only screen and (min-width:1100px){.play--about__border{width:824px;height:7px;margin:70px auto 0;background-size:24px auto}}.play--about__btn{margin-top:5vw;width:75vw}@media only screen and (min-width:769px){.play--about__btn{margin-top:4.0909090909vw;width:47.2727272727vw}}@media only screen and (min-width:1100px){.play--about__btn{margin-top:45px;width:520px}}.play--about__btn.bt1{height:15vw;margin-top:-11.5vw}@media only screen and (min-width:769px){.play--about__btn.bt1{margin-top:-12.7272727273vw;margin-bottom:4.5454545455vw;margin-left:2.7272727273vw;width:54.5454545455vw;height:5.4545454545vw}}@media only screen and (min-width:1100px){.play--about__btn.bt1{margin-top:-140px;margin-bottom:50px;margin-left:30px;width:600px;height:60px}}.play--about__btn.bt1>span{width:57.875vw;margin-left:-5vw}@media only screen and (min-width:769px){.play--about__btn.bt1>span{width:45.0909090909vw;margin-left:-1.8181818182vw}}@media only screen and (min-width:1100px){.play--about__btn.bt1>span{width:496px;margin-left:-20px}}.play--about__btn.bt2{margin-top:5vw}@media only screen and (max-width:769px){.play--about__btn.bt2.next::after{right:2.25vw}}@media only screen and (min-width:769px){.play--about__btn.bt2{margin-top:0;margin-bottom:10vw}}@media only screen and (min-width:1100px){.play--about__btn.bt2{margin-bottom:110px}}.play--howto{margin-top:6.25vw}@media only screen and (min-width:769px){.play--howto{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--howto{margin-top:50px}}.play--howto__header .common--header__txt{width:42.25vw;height:4.5vw}@media only screen and (min-width:769px){.play--howto__header .common--header__txt{width:27.5454545455vw;height:3vw}}@media only screen and (min-width:1100px){.play--howto__header .common--header__txt{width:303px;height:33px}}.play--howto__img.i1{width:87.5vw;margin:6.25vw auto 0}@media only screen and (min-width:769px){.play--howto__img.i1{width:73.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--howto__img.i1{width:812px;margin-top:40px}}.play--howto__img.i2{width:82.5vw;margin:12.5vw auto 0 3.5vw}@media only screen and (min-width:769px){.play--howto__img.i2{width:66.5454545455vw;margin:10.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--howto__img.i2{width:732px;margin:114px auto 0}}.play--howto__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--howto__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--howto__border{width:824px;height:7px;margin:70px auto 0}}.play--function{margin-top:6.25vw;padding-top:9.625vw;padding-bottom:12.5vw}@media only screen and (min-width:769px){.play--function{margin-top:4.5454545455vw;padding-top:6.1818181818vw;padding-bottom:9.0909090909vw}}@media only screen and (min-width:1100px){.play--function{margin-top:50px;padding-top:68px;padding-bottom:100px}}.play--function__header{position:relative}@media only screen and (max-width:768px){.play--function__header .common--header__icon{position:absolute;top:3.75vw;left:calc(50% - 40.875vw)}.play--function__header .common--header__icon.right{left:calc(50% + 27.125vw)}}.play--function__header .common--header__txt{width:55.25vw;height:11.875vw;margin:0 2vw}@media only screen and (min-width:769px){.play--function__header .common--header__txt{width:39.9090909091vw;height:7.7272727273vw;margin:0 1vw}}@media only screen and (min-width:1100px){.play--function__header .common--header__txt{width:439px;height:85px;margin:0 11px}}.play--function__header .common--header__txt svg{height:100%}.play--function__subheader{background-color:#ba5eea;font-size:4.75vw;text-align:center;color:#fff;font-weight:800;margin-top:8.75vw;margin-bottom:8.75vw;margin-inline:auto;line-height:1.3421052632;display:flex;align-items:center;justify-content:center;width:81.25vw;height:19.375vw;border-radius:9.375vw}@media only screen and (min-width:769px){.play--function__subheader{font-size:2.5454545455vw;margin-top:6.3636363636vw;margin-bottom:6.3636363636vw;line-height:1.2857142857;width:63.6363636364vw;height:9.0909090909vw;border-radius:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function__subheader{font-size:28px;margin-top:70px;margin-bottom:70px;width:700px;height:100px;border-radius:50px}}@media only screen and (min-width:769px){.play--function .play--text{font-size:2vw;margin-top:6.3636363636vw}}@media only screen and (min-width:1100px){.play--function .play--text{font-size:22px;margin-top:70px}}.play--function__img.i1{width:100%;margin:2.5vw auto 0}@media only screen and (min-width:769px){.play--function__img.i1{margin:4.0909090909vw auto 0}}@media only screen and (min-width:1100px){.play--function__img.i1{margin:45px auto 0}}.play--function__img.i2{width:100%;margin:5vw auto 0}@media only screen and (min-width:769px){.play--function__img.i2{margin:5.4545454545vw auto 0}}@media only screen and (min-width:1100px){.play--function__img.i2{margin:60px auto 0}}.play--function__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function__border{width:824px;height:7px;margin:70px auto 0}}.play--function__btn{width:80.5vw;height:15.5vw;box-shadow:none}@media only screen and (min-width:769px){.play--function__btn{width:59.0909090909vw;height:7.2727272727vw;transition:opacity .3s}.play--function__btn:hover{transform:none;opacity:80%}}@media only screen and (min-width:1100px){.play--function__btn{width:650px;height:80px}}.play--function__btn span{display:inline-block;width:80%;text-align:center;line-height:1.2;word-break:keep-all}@media only screen and (min-width:769px){.play--function__btn span{width:100%}}.play--function__btn.blank:after{width:5.75vw;height:4.25vw;right:5.75vw}@media only screen and (min-width:769px){.play--function__btn.blank:after{width:2.7272727273vw;height:2vw;right:2.6363636364vw}}@media only screen and (min-width:1100px){.play--function__btn.blank:after{width:30px;height:22px;right:29px}}.play--function__btn.btn-pink{background-color:#e84472}.play--function__btn.btn-yellow{background-color:#ff699d;margin-top:7.75vw}@media only screen and (min-width:769px){.play--function__btn.btn-yellow{margin-top:5.4545454545vw}}@media only screen and (min-width:1100px){.play--function__btn.btn-yellow{margin-top:60px}}.play--function2{margin-top:6.25vw}@media only screen and (min-width:769px){.play--function2{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function2{margin-top:50px}}.play--function2__header .common--header__txt{width:45.5vw;height:4vw}@media only screen and (min-width:769px){.play--function2__header .common--header__txt{width:29.5454545455vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.play--function2__header .common--header__txt{width:325px;height:30px}}.play--function2__img.i1{width:100%;margin:2.5vw auto 0}@media only screen and (min-width:769px){.play--function2__img.i1{margin:4.0909090909vw auto 0}}@media only screen and (min-width:1100px){.play--function2__img.i1{margin:45px auto 0}}.play--function2__img.i2{width:81vw;margin:5vw auto 0 3.375vw}@media only screen and (min-width:769px){.play--function2__img.i2{width:73.2727272727vw;margin:3.6363636364vw auto 0 3.2727272727vw}}@media only screen and (min-width:1100px){.play--function2__img.i2{width:806px;margin:40px auto 0 36px}}.play--function2__img.i3{width:86.375vw;margin:5vw auto 0 1vw}@media only screen and (min-width:769px){.play--function2__img.i3{width:77.2727272727vw;margin:3.6363636364vw auto 0 4.0909090909vw}}@media only screen and (min-width:1100px){.play--function2__img.i3{width:850px;margin-top:40px;margin-left:45px}}.play--function2__img.i4{width:82vw;margin:5vw auto 0 2.75vw}@media only screen and (min-width:769px){.play--function2__img.i4{width:72.3636363636vw;margin:3.6363636364vw auto 0 4.5454545455vw}}@media only screen and (min-width:1100px){.play--function2__img.i4{width:796px;margin:40px auto 0 50px}}.play--function2__img.i5{width:100%;margin:2.5vw auto 3.75vw 0}@media only screen and (min-width:769px){.play--function2__img.i5{margin:3.6363636364vw auto 4.0909090909vw 0}}@media only screen and (min-width:1100px){.play--function2__img.i5{margin:40px auto 45px 0}}.play--function2__img.i6{width:100%;margin:2.5vw auto 0}@media only screen and (min-width:769px){.play--function2__img.i6{margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function2__img.i6{margin:40px auto 0}}.play--function2__img.i7{width:87.5vw;margin:5vw auto 6.25vw 0}@media only screen and (min-width:769px){.play--function2__img.i7{width:81.8181818182vw;margin:3.6363636364vw auto 4.5454545455vw 0}}@media only screen and (min-width:1100px){.play--function2__img.i7{width:900px;margin:40px auto 50px 0}}.play--function2__img.i8{width:87.5vw;margin:5vw auto 0}@media only screen and (min-width:769px){.play--function2__img.i8{width:81.8181818182vw;margin:0 auto}}@media only screen and (min-width:1100px){.play--function2__img.i8{width:900px;margin:0 auto}}.play--function2__img.i9{width:100%;margin-top:3.5vw}@media only screen and (min-width:769px){.play--function2__img.i9{margin-top:4vw;margin-bottom:5.8181818182vw}}@media only screen and (min-width:1100px){.play--function2__img.i9{margin-top:44px;margin-bottom:64px}}.play--function2__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function2__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function2__border{width:824px;height:7px;margin:70px auto 0}}.play--function3{margin-top:6.25vw}@media only screen and (min-width:769px){.play--function3{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function3{margin-top:50px}}.play--function3__header .common--header__txt{width:46.375vw;height:4vw}@media only screen and (min-width:769px){.play--function3__header .common--header__txt{width:29.8181818182vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.play--function3__header .common--header__txt{width:328px;height:30px}}.play--function3__img.i1{width:100%;margin:3vw auto 12.5vw 0}@media only screen and (min-width:769px){.play--function3__img.i1{margin:4.0909090909vw auto 5.0909090909vw 0}}@media only screen and (min-width:1100px){.play--function3__img.i1{margin:45px 0 56px 0}}.play--function3__img.i2{width:100%;margin:1.25vw auto 0}@media only screen and (min-width:769px){.play--function3__img.i2{margin:0 auto}}.play--function3__img.i3{width:100%}.play--function3__img.i4{width:86.25vw;margin:5vw auto 0 1.25vw}@media only screen and (min-width:769px){.play--function3__img.i4{width:81.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function3__img.i4{width:900px;margin:40px auto 0}}.play--function3__img.i5{width:82.25vw;margin:5vw auto 0 2.625vw}@media only screen and (min-width:769px){.play--function3__img.i5{width:81.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function3__img.i5{width:900px;margin:40px auto 0}}.play--function3__img.i6{width:82vw;margin:5vw auto 0 2.75vw}@media only screen and (min-width:769px){.play--function3__img.i6{width:81.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function3__img.i6{width:900px;margin:40px auto 0}}.play--function3__img.i7{width:81.875vw;margin:5vw auto 7.5vw}@media only screen and (min-width:769px){.play--function3__img.i7{width:81.8181818182vw;margin:3.6363636364vw auto 5.0909090909vw}}@media only screen and (min-width:1100px){.play--function3__img.i7{margin:40px auto 56px;width:900px}}.play--function3__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function3__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function3__border{width:824px;height:7px;margin:70px auto 0}}.play--function4{margin-top:6.25vw}@media only screen and (min-width:769px){.play--function4{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function4{margin-top:50px}}.play--function4__header span{width:68.875vw;height:10.5vw}@media only screen and (min-width:769px){.play--function4__header span{width:44.3636363636vw;height:6.7272727273vw}}@media only screen and (min-width:1100px){.play--function4__header span{width:488px;height:74px}}.play--function4__img.i1{width:87.5vw;margin:5vw auto 0 0vw}@media only screen and (min-width:769px){.play--function4__img.i1{width:81.8181818182vw;margin:3.6363636364vw auto 0 0vw}}@media only screen and (min-width:1100px){.play--function4__img.i1{width:900px;margin-top:40px;margin-left:0}}.play--function4__img.i2{width:81.875vw;margin:5vw auto 0 2.75vw}@media only screen and (min-width:769px){.play--function4__img.i2{width:81.8181818182vw;margin:3.6363636364vw auto 0 0vw}}@media only screen and (min-width:1100px){.play--function4__img.i2{width:900px;margin:40px auto 0 0}}.play--function4__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function4__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function4__border{width:824px;height:7px;margin:70px auto 0}}.play--terms{margin-top:7.5vw;padding-bottom:8.75vw}@media only screen and (min-width:769px){.play--terms{margin-top:4.5454545455vw;padding-bottom:8.75vw}}@media only screen and (min-width:1100px){.play--terms{margin-top:50px;padding-bottom:70px}}.play--terms__header .common--header__txt{width:46.375vw;height:4vw}@media only screen and (min-width:769px){.play--terms__header .common--header__txt{width:27.0909090909vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.play--terms__header .common--header__txt{width:298px;height:30px}}.play--terms__text{font-size:3.5vw;text-align:left;color:#000;font-weight:500;margin:5vw 6.25vw 0 6.25vw;line-height:1.5}@media only screen and (min-width:769px){.play--terms__text{font-size:1.8181818182vw;margin:3.75vw 5vw 0 5vw;text-align:justify}}@media only screen and (min-width:1100px){.play--terms__text{font-size:20px;margin:40px 50px 0 50px}}.play--terms__text li{margin-left:1vw;list-style:"• ";text-align:justify}.play--terms__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--terms__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--terms__border{width:824px;height:7px;margin:70px auto 0}}.song--search{margin-top:6.25vw;position:sticky;top:-75vw;z-index:100}@media only screen and (min-width:769px){.song--search{margin-top:4.5454545455vw;top:-41.8181818182vw}}@media only screen and (min-width:1100px){.song--search{margin-top:50px;top:calc(-460px - .2vw)}}@media only screen and (min-width:1400px){.song--search{top:-458px}}.song--search:after,.song--search:before{display:none}.song--search__text{font-size:2.5vw;text-align:center;line-height:1.5}@media only screen and (min-width:769px){.song--search__text{font-size:1.2727272727vw}}@media only screen and (min-width:1100px){.song--search__text{font-size:14px}}.song--search__btn{width:75vw;height:11.875vw;margin:5vw auto;border-radius:12.5vw;background-color:#4b4b4b;display:flex;align-items:center;justify-content:center;position:relative}.song--search__btn.new{background-color:#fb5f99}.song--search__btn.pops_anime{background-color:#ff972a}.song--search__btn.niconico{background-color:#02c8d3}.song--search__btn.toho{background-color:#ad59ee}.song--search__btn.variety{background-color:#4be070}.song--search__btn.maimai{background-color:#f64849}.song--search__btn.gekichu{background-color:#3584fe}.song--search__btn.utage{background-color:#dc39b8}.song--search__btn.new .song--search__btn__text{width:29.375vw}.song--search__btn.pops_anime .song--search__btn__text{width:33.375vw}.song--search__btn.niconico .song--search__btn__text{width:29.25vw}.song--search__btn.toho .song--search__btn__text{width:27.25vw}.song--search__btn.variety .song--search__btn__text{width:20vw}.song--search__btn.maimai .song--search__btn__text{width:17.375vw}.song--search__btn.gekichu .song--search__btn__text{width:25vw}.song--search__btn.utage .song--search__btn__text{width:13.5vw}.song--search__btn.on:after{transform:scale(1,-1)}.song--search__btn:after{position:absolute;display:block;content:"";background-image:url(/vendor/maimai/img/song/arrow.svg);background-size:contain;background-repeat:no-repeat;width:2.5vw;height:2.25vw;right:4.5vw}@media only screen and (min-width:769px){.song--search__btn{display:none}}.song--search__btn__text svg{fill:#fff;fill-rule:evenodd;height:4vw}@media only screen and (min-width:769px){.song--search__btn__text svg{height:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__btn__text svg{height:18px}}.song--search__list{display:none;width:75vw;margin-top:-2.5vw!important;left:50%;transform:translateX(-50%);position:absolute;box-sizing:border-box;background-color:#242424;z-index:200;text-align:center;color:#fff;font-size:3.5vw;font-weight:900;padding:2.5vw 6.25vw}.song--search__item{padding:5vw 0}.song--search__item:not(:first-child){border-top:1px solid #707070}.song--search__check{display:flex;align-items:center;justify-content:center;flex-wrap:wrap}@media only screen and (min-width:769px){.song--search__check{margin-top:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__check{margin-top:30px}}@media only screen and (min-width:769px){.song--search__check.bt{width:54.5454545455vw;margin:0 auto}}@media only screen and (min-width:1100px){.song--search__check.bt{width:500px}}.song--search__check.bt>label:nth-child(n+4){margin-top:3.75vw}@media only screen and (min-width:769px){.song--search__check.bt>label:nth-child(n+4){margin-top:1.3636363636vw}}.song--search__check.bt>label:not(:first-child){margin-left:2.5vw}@media only screen and (min-width:769px){.song--search__check.bt>label:not(:first-child){margin-left:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__check.bt>label:not(:first-child){margin-left:30px}}.song--search__check>label:not(:first-child){margin-left:4.375vw}@media only screen and (min-width:769px){.song--search__check>label:not(:first-child){margin-left:4.0909090909vw}}@media only screen and (min-width:1100px){.song--search__check>label:not(:first-child){margin-left:45px}}.song--search__check__input{display:none}.song--search__check__input:checked+.song--search__check__text:before{background-color:#e72139;border:.75vw solid #e72139}.song--search__check__input:checked+.song--search__check__text:after{display:block}.song--search__check__text{display:flex;align-items:center;position:relative}.song--search__check__text::before{box-sizing:border-box;content:"";display:block;width:5vw;height:5vw;border:.75vw solid #e5e5e5;border-radius:.625vw}@media only screen and (min-width:769px){.song--search__check__text::before{width:2.7272727273vw;height:2.7272727273vw;border:.3636363636vw solid #e5e5e5;border-radius:.4545454545vw}}@media only screen and (min-width:1100px){.song--search__check__text::before{width:30px;height:30px;border-width:4px;border-radius:5px}}.song--search__check__text:after{box-sizing:border-box;content:"";display:none;position:absolute;left:.75vw;background-image:url(/vendor/maimai/img/song/check.svg);background-size:contain;background-repeat:no-repeat;width:3.375vw;height:2.875vw}@media only screen and (min-width:769px){.song--search__check__text:after{width:1.8181818182vw;height:1.4545454545vw;left:.5454545455vw}}@media only screen and (min-width:1100px){.song--search__check__text:after{width:20px;height:16px;left:6px}}.song--search__check__text>span{display:flex;align-items:center;width:20vw;height:4.125vw;margin-left:2.5vw}@media only screen and (min-width:769px){.song--search__check__text>span{width:10.6363636364vw;height:2.7272727273vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span{width:117px;height:30px;margin-left:10px}}.song--search__check__text>span.re{width:22.375vw}@media only screen and (min-width:769px){.song--search__check__text>span.re{width:11.2727272727vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.re{width:124px}}.song--search__check__text>span.ba{width:12.5vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.ba{width:7vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.ba{width:77px;height:20px;margin-left:10px}}.song--search__check__text>span.bas{width:22.5vw;height:4vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.bas{width:12.8181818182vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.bas{width:141px;height:20px;margin-left:10px}}.song--search__check__text>span.ex{width:16.25vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.ex{width:9.0909090909vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.ex{width:100px;height:20px;margin-left:10px}}.song--search__check__text>span.ma{width:17.5vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.ma{width:9.0909090909vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.ma{width:100px;height:20px;margin-left:10px}}.song--search__check__text>span.re{width:26.25vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.re{width:12.7272727273vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.re{width:140px;height:20px;margin-left:10px}}.song--search__nav{display:none}@media only screen and (min-width:769px){.song--search__nav{width:77.2727272727vw;margin:2.7272727273vw auto;display:flex;align-items:center;justify-content:center;flex-wrap:wrap}}@media only screen and (min-width:1100px){.song--search__nav{width:850px;margin:30px auto 30px}}@media only screen and (min-width:769px){.song--search__nav__item{display:flex;align-items:center;justify-content:center;width:18.1818181818vw;height:7.2727272727vw;background-color:#4b4b4b;border-radius:100px;transition:.3s;cursor:pointer}}@media only screen and (min-width:1100px){.song--search__nav__item{width:200px;height:80px}}.song--search__nav__item.on.new,.song--search__nav__item:hover.new{background-color:#fb5f99}.song--search__nav__item.on.pops_anime,.song--search__nav__item:hover.pops_anime{background-color:#ff972a}.song--search__nav__item.on.niconico,.song--search__nav__item:hover.niconico{background-color:#02c8d3}.song--search__nav__item.on.toho,.song--search__nav__item:hover.toho{background-color:#ad59ee}.song--search__nav__item.on.variety,.song--search__nav__item:hover.variety{background-color:#4be070}.song--search__nav__item.on.maimai,.song--search__nav__item:hover.maimai{background-color:#f64849}.song--search__nav__item.on.gekichu,.song--search__nav__item:hover.gekichu{background-color:#3584fe}.song--search__nav__item.on.utage,.song--search__nav__item:hover.utage{background-color:#dc39b8}@media only screen and (min-width:769px){.song--search__nav__item:nth-child(n+5){margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__nav__item:nth-child(n+5){margin-top:10px}}@media only screen and (min-width:769px){.song--search__nav__item:not(:nth-child(4n+1)){margin-left:1.3636363636vw}}@media only screen and (min-width:1100px){.song--search__nav__item:not(:nth-child(4n+1)){margin-left:15px}}@media only screen and (min-width:769px){.song--search__nav__img{height:1.7272727273vw}}@media only screen and (min-width:1100px){.song--search__nav__img{height:19px}}@media only screen and (min-width:769px){.song--search__nav__img.i01{width:11.8181818182vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i01{width:130px}}@media only screen and (min-width:769px){.song--search__nav__img.i02{width:13.5454545455vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i02{width:149px}}@media only screen and (min-width:769px){.song--search__nav__img.i03{width:12.9090909091vw;height:4.1818181818vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i03{width:142px;height:46px}}@media only screen and (min-width:769px){.song--search__nav__img.i04{width:11vw;height:2vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i04{width:121px;height:22px}}@media only screen and (min-width:769px){.song--search__nav__img.i05{width:9.1818181818vw;height:4.1818181818vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i05{width:101px;height:46px}}@media only screen and (min-width:769px){.song--search__nav__img.i06{width:7vw;height:1.5454545455vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i06{width:77px;height:17px}}@media only screen and (min-width:769px){.song--search__nav__img.i07{width:11.1818181818vw;height:4.1818181818vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i07{width:123px;height:46px}}@media only screen and (min-width:769px){.song--search__nav__img.i08{width:5.1818181818vw;height:1.9090909091vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i08{width:57px;height:21px}}.song--search__input{position:relative;display:flex;align-items:center;justify-content:center;margin-top:5.625vw}@media only screen and (min-width:769px){.song--search__input{margin-top:0}}.song--search__input:before{display:block;content:"";background-image:url(/vendor/maimai/img/song/search_lens.svg);background-size:contain;background-repeat:no-repeat;width:6.25vw;height:6.25vw;left:8.75vw;position:absolute;z-index:10}@media only screen and (min-width:769px){.song--search__input:before{width:2.7272727273vw;height:2.7272727273vw;left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__input:before{width:30px;height:30px;left:10px}}.song--search__input .clear{display:block;content:"";background-image:url(/vendor/maimai/img/song/search_del.png);background-size:contain;background-repeat:no-repeat;width:6.25vw;height:6.25vw;position:absolute;right:8.75vw;z-index:10;cursor:pointer}@media only screen and (min-width:769px){.song--search__input .clear{width:2.7272727273vw;height:2.7272727273vw;right:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__input .clear{width:30px;height:30px;right:10px}}.song--search__input input{position:relative;width:75vw;height:11.25vw;border-radius:100px;background-color:#eee;margin:0 auto;font-size:4.5vw;padding:0 10vw;display:block;font-weight:900}.song--search__input input::placeholder{font-size:2.25vw;font-weight:400}@media only screen and (min-width:769px){.song--search__input input{width:40.6363636364vw;height:3.6363636364vw;font-size:1.8181818182vw;font-weight:900;padding:0 4.5454545455vw}.song--search__input input::placeholder{font-size:1.2727272727vw;font-weight:400}}@media only screen and (min-width:1100px){.song--search__input input{width:447px;height:40px;font-weight:900;font-size:20px;padding:0 50px}.song--search__input input::placeholder{font-size:14px;font-weight:400}}.song--search__attention{margin:2.5vw auto 0;padding:0 6.25vw}@media only screen and (min-width:769px){.song--search__attention{margin:0;padding:0;margin-left:2.2727272727vw}}@media only screen and (min-width:1100px){.song--search__attention{margin-left:25px}}.song--search__attention__text{font-size:2.5vw;padding-left:1em;text-indent:-1em;display:flex;align-items:center;line-height:1.5}.song--search__attention__text:not(:first-child){margin-top:1.25vw}@media only screen and (min-width:769px){.song--search__attention__text:not(:first-child){margin-top:.1818181818vw}}@media only screen and (min-width:1100px){.song--search__attention__text:not(:first-child){margin-top:2px}}@media only screen and (min-width:769px){.song--search__attention__text{font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.song--search__attention__text{font-size:12px}}.song--search__attention__key{display:flex;align-items:center;justify-content:center;width:3.75vw;margin:0 3.25vw 0 .5vw}@media only screen and (min-width:769px){.song--search__attention__key{width:1.8181818182vw;margin:0 1.2727272727vw 0 .1818181818vw}}@media only screen and (min-width:1100px){.song--search__attention__key{width:20px;margin:0 14px 0 2px}}@media only screen and (min-width:769px){.song--search__box{display:flex;align-items:center;justify-content:center;margin-top:2.2727272727vw}}@media only screen and (min-width:1100px){.song--search__box{margin-top:25px}}.song--search__open{position:absolute;width:100%;margin:5vw 0 -8.25vw;height:11.25vw;font-size:4.5vw;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;background-color:#e72139;position:relative;border-radius:0 0 3.75vw 3.75vw;z-index:100;transition:.3s;cursor:pointer}.song--search__open.open{opacity:0;pointer-events:none}@media only screen and (min-width:769px){.song--search__open{margin:3.6363636364vw 0 -4.6363636364vw;height:5.4545454545vw;font-size:2.3636363636vw;border-radius:0 0 2.7272727273vw 2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__open{margin:40px 0 -52px;height:60px;font-size:26px;border-radius:0 0 30px 30px}}.song--search__open:after{display:block;content:"";position:absolute;right:3.75vw;width:6.25vw;height:6.25vw;background-image:url(/vendor/maimai/img/song/plus.png);background-size:cover}@media only screen and (min-width:769px){.song--search__open:after{right:26.3636363636vw;width:2.7272727273vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__open:after{right:290px;width:30px;height:30px}}.song--search__detail{padding:0 6.25vw;position:relative;display:none}@media only screen and (min-width:769px){.song--search__detail{padding:0 2.2727272727vw}}@media only screen and (min-width:1100px){.song--search__detail{padding:0 25px}}.song--search__border{width:100%;height:.75vw;border-radius:100px;background-color:#e5e5e5}@media only screen and (min-width:769px){.song--search__border{height:.5454545455vw}}@media only screen and (min-width:1100px){.song--search__border{height:6px}}.song--search__inner{margin-top:5vw}@media only screen and (min-width:769px){.song--search__inner{margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.song--search__inner{margin-top:40px}}.song--search__lv{margin-top:3.75vw;display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:flex-start}@media only screen and (min-width:769px){.song--search__lv{margin-top:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__lv{margin-top:30px}}.song--search__lv__btn{display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;width:13.75vw;height:11.25vw;border-radius:1.25vw;background-color:#4b4b4b;font-size:3.75vw}@media only screen and (min-width:769px){.song--search__lv__btn{width:5vw;height:4.0909090909vw;border-radius:.9090909091vw;font-size:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__lv__btn{width:55px;height:45px;border-radius:10px;font-size:18px}}.song--search__lv__btn.on{background-color:#e72139}@media only screen and (max-width:769px){.song--search__lv__btn:not(:nth-child(5n+1)){margin-left:1.5vw}.song--search__lv__btn:nth-child(n+6){margin-top:1.875vw}}@media only screen and (min-width:769px){.song--search__lv__btn:not(:nth-child(14n+1)){margin-left:.5454545455vw}.song--search__lv__btn:nth-child(n+15){margin-top:1.0909090909vw}}@media only screen and (min-width:1100px){.song--search__lv__btn:not(:nth-child(14n+1)){margin-left:6px}.song--search__lv__btn:nth-child(n+15){margin-top:12px}}.song--search__select{margin-top:3.75vw;display:flex;justify-content:space-between}@media only screen and (min-width:769px){.song--search__select{width:39.0909090909vw;margin:3.75vw auto 0}}@media only screen and (min-width:1100px){.song--search__select{width:430px;margin:30px auto 0}}.song--search__select__btn{box-sizing:border-box;font-size:3vw;color:#e72139;font-weight:900;width:36.25vw;height:10vw;border:1vw solid #e5e5e5;display:flex;align-items:center;justify-content:center;border-radius:100px}@media only screen and (min-width:769px){.song--search__select__btn{width:18.1818181818vw;height:3.6363636364vw;font-size:1.6363636364vw;border:.3636363636vw solid #e5e5e5;border-radius:1.8181818182vw}}@media only screen and (min-width:1100px){.song--search__select__btn{width:200px;height:40px;font-size:18px;border-width:4px;border-radius:20px}}.song--search__count{width:75vw;height:15vw;margin:3.75vw auto;border:3px solid #e72139;border-radius:125vw;background:linear-gradient(90deg,#e72139 0,#e72139 50%,#eee 50%,#eee 100%);display:flex;overflow:hidden;box-sizing:border-box}@media only screen and (min-width:769px){.song--search__count{background:linear-gradient(90deg,#e72139 0,#e72139 40%,#eee 40%,#eee 100%);width:37.8181818182vw;height:7.2727272727vw;margin:2.7272727273vw auto}}@media only screen and (min-width:1100px){.song--search__count{width:416px;height:80px;margin:30px auto}}.song--search__count__text{color:#fff;width:37.5vw;height:110%;margin-left:-.625vw;margin-top:-.625vw;font-size:3vw;background-color:#e72139;display:flex;justify-content:center;align-items:center;font-weight:700;text-align:center}@media only screen and (min-width:769px){.song--search__count__text{width:18.1818181818vw;margin-left:-.4545454545vw;margin-top:-.2727272727vw;font-size:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__count__text{width:200px;margin-left:-5px;margin-top:-3px;font-size:18px}}.song--search__count__num{width:37.5vw;height:13.75vw;font-size:5vw;display:flex;justify-content:center;align-items:center;font-weight:900;text-align:center;color:#e72139}@media only screen and (min-width:769px){.song--search__count__num{width:19.6363636364vw;height:6.3636363636vw;font-size:2.9090909091vw}}@media only screen and (min-width:1100px){.song--search__count__num{width:216px;height:70px;font-size:32px}}.song--search__count__num>.small{margin-top:1.25vw;margin-left:1.25vw;font-size:2.5vw}@media only screen and (min-width:769px){.song--search__count__num>.small{margin-top:.9090909091vw;margin-left:.9090909091vw;font-size:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__count__num>.small{margin-top:10px;margin-left:10px;font-size:18px}}.song--search__close{width:6.25vw;position:absolute;right:2.5vw;bottom:-9.375vw}@media only screen and (min-width:769px){.song--search__close{width:3.6363636364vw;right:.9090909091vw;bottom:-6.3636363636vw}}@media only screen and (min-width:1100px){.song--search__close{width:40px;right:10px;bottom:-70px}}.song--box{margin-top:6.25vw;position:relative;transform:translate3d(0,0,0)}.song--box__title{display:flex;flex-wrap:wrap;align-items:center;justify-content:center}.song--box__title--icon{background-image:none;width:13.5vw;height:7vw}@media only screen and (min-width:769px){.song--box__title--icon{width:12.5454545455vw;height:6.1818181818vw}}@media only screen and (min-width:1100px){.song--box__title--icon{width:138px;height:68px}}.song--box__tit{margin:0 1.25vw;display:flex;align-items:center;justify-content:center}@media only screen and (min-width:769px){.song--box__tit{margin:0 1.2727272727vw}}@media only screen and (min-width:1100px){.song--box__tit{margin:0 14px}}.song--box.new .song--box__title--icon svg{fill:#fb5f99}.song--box.new .song--box__tit{width:28.625vw}@media only screen and (min-width:769px){.song--box.new .song--box__tit{width:19.6363636364vw}}@media only screen and (min-width:1100px){.song--box.new .song--box__tit{width:216px}}.song--box.new:after,.song--box.new:before{background-color:#fb5f99}.song--box.pops_anime .song--box__title--icon svg{fill:#ff972a}.song--box.pops_anime .song--box__tit{width:36.125vw}@media only screen and (min-width:769px){.song--box.pops_anime .song--box__tit{width:24.3636363636vw}}@media only screen and (min-width:1100px){.song--box.pops_anime .song--box__tit{width:268px}}.song--box.pops_anime:after,.song--box.pops_anime:before{background-color:#ff972a}.song--box.niconico .song--box__title--icon svg{fill:#02c8d3}.song--box.niconico .song--box__tit{width:48.875vw}@media only screen and (min-width:769px){.song--box.niconico .song--box__tit{width:40.7272727273vw}}@media only screen and (min-width:1100px){.song--box.niconico .song--box__tit{width:448px}}.song--box.niconico:after,.song--box.niconico:before{background-color:#02c8d3}.song--box.toho .song--box__title--icon svg{fill:#ad59ee}.song--box.toho .song--box__tit{width:28.625vw}@media only screen and (min-width:769px){.song--box.toho .song--box__tit{width:20.3636363636vw}}@media only screen and (min-width:1100px){.song--box.toho .song--box__tit{width:224px}}.song--box.toho:after,.song--box.toho:before{background-color:#ad59ee}.song--box.maimai .song--box__title--icon svg{fill:#f64849}.song--box.maimai .song--box__tit{width:18.25vw;margin-bottom:.75vw}@media only screen and (min-width:769px){.song--box.maimai .song--box__tit{width:13.5454545455vw;margin-bottom:.7272727273vw}}@media only screen and (min-width:1100px){.song--box.maimai .song--box__tit{width:149px;margin-bottom:8px}}.song--box.maimai:after,.song--box.maimai:before{background-color:#f64849}.song--box.variety .song--box__title--icon svg{fill:#4be070}.song--box.variety .song--box__tit{width:43.75vw}@media only screen and (min-width:769px){.song--box.variety .song--box__tit{width:29.2727272727vw}}@media only screen and (min-width:1100px){.song--box.variety .song--box__tit{width:322px}}.song--box.variety:after,.song--box.variety:before{background-color:#4be070}.song--box.gekichu .song--box__title--icon svg{fill:#3584fe}.song--box.gekichu .song--box__tit{width:45.125vw}@media only screen and (min-width:769px){.song--box.gekichu .song--box__tit{width:36.3636363636vw}}@media only screen and (min-width:1100px){.song--box.gekichu .song--box__tit{width:400px}}.song--box.gekichu:after,.song--box.gekichu:before{background-color:#3584fe}.song--box.utage .song--box__title--icon svg{fill:#dc39b8}.song--box.utage .song--box__tit{width:11.25vw}@media only screen and (min-width:769px){.song--box.utage .song--box__tit{width:8.6363636364vw}}@media only screen and (min-width:1100px){.song--box.utage .song--box__tit{width:95px}}.song--box.utage:after,.song--box.utage:before{background-color:#dc39b8}.song--box__none{font-size:3.75vw;margin-top:5vw;line-height:2;font-weight:900;text-align:center}@media only screen and (min-width:769px){.song--box__none{font-size:1.6363636364vw;margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.song--box__none{font-size:18px;margin-top:40px}}.song--item{padding:6.25vw 6.25vw 0}.song--item:last-child .song--item__border{display:none}@media only screen and (min-width:769px){.song--item{padding:4.5454545455vw 3.6363636364vw 0;display:flex;align-items:flex-start;justify-content:space-between}}@media only screen and (min-width:1100px){.song--item{padding:50px 40px 0}}@media only screen and (min-width:769px){.song--item__box{width:53.4545454545vw}}@media only screen and (min-width:1100px){.song--item__box{width:588px}}.song--item__top{display:flex;align-items:center;justify-content:space-between}.song--item__top.lock:after{display:block;content:"";background-image:url(/vendor/maimai/img/song/lock.png);background-size:contain;width:6.125vw;height:6.125vw}@media only screen and (min-width:769px){.song--item__top.lock:after{width:4.4545454545vw;height:4.4545454545vw}}@media only screen and (min-width:1100px){.song--item__top.lock:after{width:49px;height:49px}}.song--item__genre.pops_anime{width:35.25vw}@media only screen and (min-width:769px){.song--item__genre.pops_anime{width:15.5454545455vw}}@media only screen and (min-width:1100px){.song--item__genre.pops_anime{width:171px}}.song--item__genre.toho{width:29.875vw}@media only screen and (min-width:769px){.song--item__genre.toho{width:13.4545454545vw}}@media only screen and (min-width:1100px){.song--item__genre.toho{width:148px}}.song--item__genre.maimai{width:21.125vw}@media only screen and (min-width:769px){.song--item__genre.maimai{width:9.4545454545vw}}@media only screen and (min-width:1100px){.song--item__genre.maimai{width:104px}}.song--item__genre.variety{width:41.875vw}@media only screen and (min-width:769px){.song--item__genre.variety{width:18.8181818182vw}}@media only screen and (min-width:1100px){.song--item__genre.variety{width:207px}}.song--item__genre.niconico{width:54.875vw}@media only screen and (min-width:769px){.song--item__genre.niconico{width:24.8181818182vw}}@media only screen and (min-width:1100px){.song--item__genre.niconico{width:273px}}.song--item__genre.gekichu{width:51.25vw}@media only screen and (min-width:769px){.song--item__genre.gekichu{width:23.1818181818vw}}@media only screen and (min-width:1100px){.song--item__genre.gekichu{width:255px}}.song--item__genre.utage{width:18vw}@media only screen and (min-width:769px){.song--item__genre.utage{width:8.3636363636vw}}@media only screen and (min-width:1100px){.song--item__genre.utage{width:92px}}.song--item__middle{display:flex;margin-top:3.75vw}@media only screen and (min-width:769px){.song--item__middle{margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__middle{margin-top:10px}}.song--item__info{width:46.25vw;margin-left:3.75vw}@media only screen and (min-width:769px){.song--item__info{margin-left:0;width:100%}}.song--item__info.utage{position:relative}.song--item__info.utage .song--item__icon{background:transparent url(/vendor/maimai/img/song/utage/icon_buddy.png) no-repeat;background-size:contain;width:8.625vw;height:6.625vw;position:absolute;right:0;bottom:0;pointer-events:none}@media only screen and (min-width:769px){.song--item__info.utage .song--item__icon{width:5vw;height:4.4545454545vw}}@media only screen and (min-width:1100px){.song--item__info.utage .song--item__icon{width:55px;height:49px}}.song--item__thumb{width:25vw;font-size:0}@media only screen and (min-width:769px){.song--item__thumb{width:18.1818181818vw}}@media only screen and (min-width:1100px){.song--item__thumb{width:200px}}.song--item__tit{font-size:4vw;font-weight:900;margin-top:-1vw;padding-bottom:1.25vw;border-bottom:1px solid #e5e5e5;line-height:1.5}@media only screen and (min-width:769px){.song--item__tit{margin:0;padding:0;font-size:2.5454545455vw;border:none}}@media only screen and (min-width:1100px){.song--item__tit{font-size:28px}}.song--item__artist{font-size:2.5vw;font-weight:900;margin-top:2.5vw}@media only screen and (min-width:769px){.song--item__artist{font-size:1.2727272727vw;margin-top:1.0909090909vw}}@media only screen and (min-width:1100px){.song--item__artist{font-size:14px;margin-top:12px}}.song--item__artistName{font-size:3vw;font-weight:900;margin-top:1.25vw}@media only screen and (min-width:769px){.song--item__artistName{font-size:1.4545454545vw;margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__artistName{font-size:16px;margin-top:10px}}.song--item__bottom{margin-top:3.75vw}@media only screen and (min-width:769px){.song--item__bottom{margin-top:1.8181818182vw}}@media only screen and (min-width:1100px){.song--item__bottom{margin-top:20px}}.song--item__lv{display:flex;align-items:center;justify-content:space-between}.song--item__lv:not(:first-child){margin-top:1.25vw}@media only screen and (min-width:769px){.song--item__lv:not(:first-child){margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__lv:not(:first-child){margin-top:10px}}.song--item__lv:after{display:block;content:"";background-size:contain;background-repeat:no-repeat;width:19vw;height:3.75vw}@media only screen and (min-width:769px){.song--item__lv:after{width:13.6363636364vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.song--item__lv:after{width:150px;height:30px}}.song--item__lv.st:after{background-image:url(/vendor/maimai/img/song/inner_st.png)}.song--item__lv.dx:after{background-image:url(/vendor/maimai/img/song/inner_dx.png)}.song--item__lv.utage .song--item__lv__inner{width:100%;flex-wrap:wrap}.song--item__lv.utage .song--item__lv__num{background-color:#dc39b8;font-size:4vw;width:37.125vw}@media only screen and (min-width:769px){.song--item__lv.utage .song--item__lv__num{font-size:2.5454545455vw;width:26.5454545455vw}}@media only screen and (min-width:1100px){.song--item__lv.utage .song--item__lv__num{font-size:28px;width:292px}}.song--item__lv.utage:after{display:none}.song--item__lv__inner{width:53.75vw;display:flex;justify-content:space-between}@media only screen and (min-width:769px){.song--item__lv__inner{width:39.0909090909vw}}@media only screen and (min-width:1100px){.song--item__lv__inner{width:430px}}.song--item__lv__num{width:10vw;height:7.5vw;border-radius:1.25vw;font-size:4.25vw;font-weight:900;display:flex;align-items:center;justify-content:center;color:#fff}@media only screen and (min-width:769px){.song--item__lv__num{width:7.2727272727vw;height:5.4545454545vw;border-radius:.9090909091vw;font-size:3.0909090909vw}}@media only screen and (min-width:1100px){.song--item__lv__num{width:80px;height:60px;border-radius:10px;font-size:34px}}.song--item__lv__num:nth-child(1){background-color:#6ed43e}.song--item__lv__num:nth-child(2){background-color:#f7b807}.song--item__lv__num:nth-child(3){background-color:#ff828d}.song--item__lv__num:nth-child(4){background-color:#a051dc}.song--item__lv__num:nth-child(5){border:.5vw solid #a051dc;color:#a051dc;background-color:#fff}@media only screen and (min-width:769px){.song--item__lv__num:nth-child(5){border-width:.3636363636vw}}@media only screen and (min-width:1100px){.song--item__lv__num:nth-child(5){border-width:4px}}.song--item__lv__comment{background-color:#dc39b8;border-radius:1.25vw;display:flex;align-items:center;justify-content:center;color:#fff;width:100%;font-size:3vw;font-weight:900;margin-top:1.25vw;padding:2.25vw 1.25vw}@media only screen and (min-width:769px){.song--item__lv__comment{border-radius:.9090909091vw;justify-content:flex-start;font-size:2.1818181818vw;margin-top:.9090909091vw;padding:1.6363636364vw 1.8181818182vw}}@media only screen and (min-width:1100px){.song--item__lv__comment{border-radius:10px;font-size:24px;margin-top:10px;padding:18px 20px}}.song--item__copy{font-size:2.25vw;margin-top:2.5vw;line-height:2}@media only screen and (min-width:769px){.song--item__copy{font-size:.9090909091vw;margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__copy{font-size:10px;margin-top:10px}}.song--item__border{width:100%;height:.75vw;border-radius:100px;background-color:#e5e5e5;margin-top:6.25vw}@media only screen and (min-width:769px){.song--item__border{height:.5454545455vw;margin-top:4.5454545455vw;width:140%;margin-left:-40%}}@media only screen and (min-width:1100px){.song--item__border{height:6px;margin-top:50px}}.song--copy{position:relative;width:87.5vw;border-radius:3.75vw;box-shadow:1.75vw 1.75vw 0 0 rgba(0,0,0,.2);background-color:rgba(255,255,255,.7);margin:8.75vw auto 0;padding:5.625vw 5vw;font-size:2.5vw;line-height:1.8}@media only screen and (min-width:769px){.song--copy{width:81.8181818182vw;border-radius:2.7272727273vw;box-shadow:1.2727272727vw 1.2727272727vw 0 0 rgba(0,0,0,.2);padding:4.0909090909vw get(40,1100);margin-top:7.2727272727vw;font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.song--copy{width:900px;border-radius:30px;box-shadow:14px 14px 0 0 rgba(0,0,0,.2);padding:45px 40px;margin-top:80px;font-size:12px}}.song--copy .piapro{width:11.875vw;height:2.75vw;display:inline-block;margin:0 .625vw -.625vw;-webkit-backface-visibility:hidden;background-image:url(/vendor/maimai/img/song/piapro_logo.png);background-size:contain;background-repeat:no-repeat}@media only screen and (min-width:769px){.song--copy .piapro{width:6.8181818182vw;height:1.6363636364vw;margin:0 .4545454545vw -.4545454545vw}}@media only screen and (min-width:1100px){.song--copy .piapro{width:75px;height:18px;margin:0 5px -5px}}#song--search.m_fixed{position:fixed;left:50%;transform:translateX(-50%);z-index:200;margin-top:0}.dl--nav{display:flex;justify-content:space-between;flex-wrap:wrap;width:88.75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--nav{width:64.2857142857vw;margin:3.5714285714vw auto 0}}@media only screen and (min-width:1100px){.dl--nav{width:900px;margin:50px auto 0}}.dl--nav__link{display:block;box-shadow:.625vw .625vw 0 rgba(0,0,0,.2);width:42.75vw;height:10.625vw;border-radius:5.25vw;background-color:#4b4b4b;display:flex;align-items:center;justify-content:center}.dl--nav__link.on{background-color:#e72139}@media only screen and (min-width:769px){.dl--nav__link{transition:.3s;width:31.7142857143vw;height:4.2857142857vw;border-radius:3vw;box-shadow:.3571428571vw .3571428571vw 0 rgba(0,0,0,.2)}.dl--nav__link:hover{background-color:#e72139}}@media only screen and (min-width:1100px){.dl--nav__link{width:444px;height:60px;border-radius:42px;box-shadow:5px 5px 0 rgba(0,0,0,.2)}}@media only screen and (max-width:769px){.dl--nav__item:nth-child(n+3){margin-top:2vw}}@media only screen and (min-width:769px){.dl--nav__item:nth-child(n+4){margin-top:.8571428571vw}}@media only screen and (min-width:1100px){.dl--nav__item:nth-child(n+4){margin-top:12px}}.dl--nav__text.l1{width:20.25vw}@media only screen and (min-width:769px){.dl--nav__text.l1{width:9.5vw}}@media only screen and (min-width:1100px){.dl--nav__text.l1{width:133px}}.dl--nav__text.l2{width:31.5vw}@media only screen and (min-width:769px){.dl--nav__text.l2{width:14.7857142857vw}}@media only screen and (min-width:1100px){.dl--nav__text.l2{width:207px}}.dl--pop{margin-top:6.25vw}@media only screen and (min-width:769px){.dl--pop{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.dl--pop{margin-top:50px}}.dl--pop__header .common--header__txt{width:33.25vw;height:4vw}@media only screen and (min-width:769px){.dl--pop__header .common--header__txt{width:21.6363636364vw;height:2.4545454545vw}}@media only screen and (min-width:1100px){.dl--pop__header .common--header__txt{width:238px;height:27px}}.dl--pop__wrapper{width:75vw;margin:8.75vw auto 0}@media only screen and (min-width:769px){.dl--pop__wrapper{display:flex;flex-wrap:wrap;width:72.7272727273vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.dl--pop__wrapper{width:800px;margin:70px auto 0}}@media only screen and (max-width:769px){.dl--pop__item:not(:first-child){margin-top:5.625vw}}@media only screen and (min-width:769px){.dl--pop__item{width:34.5454545455vw}.dl--pop__item:nth-child(even){margin-left:3.6363636364vw}.dl--pop__item:nth-child(n+3){margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.dl--pop__item{width:380px}.dl--pop__item:nth-child(even){margin-left:40px}.dl--pop__item:nth-child(n+3){margin-top:50px}}.dl--pop__head{width:100%;height:5vw;font-size:3.5vw;font-weight:900;background-color:#e72139;justify-content:center;color:#fff;border-radius:100px;display:flex;align-items:center}@media only screen and (min-width:769px){.dl--pop__head{height:2.7272727273vw;font-size:1.8181818182vw}}@media only screen and (min-width:1100px){.dl--pop__head{height:30px;font-size:20px}}.dl--pop__thumb{display:block;width:100%;margin-top:2.5vw;font-size:0;position:relative}@media only screen and (min-width:769px){.dl--pop__thumb{margin-top:1.8181818182vw;transition:.3s}.dl--pop__thumb:hover{opacity:.7}}@media only screen and (min-width:1100px){.dl--pop__thumb{margin-top:20px}}.dl--pop__thumb::after{display:block;content:"";position:absolute;width:10vw;height:10vw;background-image:url(/vendor/maimai/img/common/download.png);background-size:contain;right:3.75vw;bottom:3.75vw}@media only screen and (min-width:769px){.dl--pop__thumb::after{width:4.5454545455vw;height:4.5454545455vw;right:1.8181818182vw;bottom:1.8181818182vw}}@media only screen and (min-width:1100px){.dl--pop__thumb::after{width:50px;height:50px;right:20px;bottom:20px}}.dl--pop__inner{width:75vw;margin:7.5vw auto 0}@media only screen and (min-width:769px){.dl--pop__inner{width:72.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--pop__inner{width:800px;margin:40px auto 0}}.dl--pop__text{font-size:2.5vw;padding-left:1em;text-indent:-1em;line-height:1.5}@media only screen and (min-width:769px){.dl--pop__text{font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.dl--pop__text{font-size:12px}}.dl--pn{display:flex;margin:7.5vw auto 0;width:87.5vw;justify-content:space-between;align-items:center}@media only screen and (min-width:769px){.dl--pn{margin-top:5.4545454545vw;width:73.6363636364vw}}@media only screen and (min-width:1100px){.dl--pn{margin-top:60px;width:810px}}.dl--pn__inner{display:flex}.dl--pn__inner.dl--pn__left .dl--pn__arrow:before{transform:scale(-1,1)}.dl--pn__center{width:40.25vw;display:flex}@media only screen and (min-width:769px){.dl--pn__center{width:48.1818181818vw}}@media only screen and (min-width:1100px){.dl--pn__center{width:530px}}.dl--pn__num{display:flex;align-items:center;justify-content:center;width:8.75vw;height:8.75vw;border-radius:12.5vw;background-color:#4b4b4b;box-shadow:.625vw .625vw 0 0 rgba(0,0,0,.2);cursor:pointer;color:#fff;font-size:3.5vw;font-weight:900}.dl--pn__num.on,.dl--pn__num:hover{background-color:#e72139}.dl--pn__num:not(:first-child){margin-left:1.75vw}@media only screen and (min-width:769px){.dl--pn__num{width:4.5454545455vw;height:4.5454545455vw;box-shadow:.4545454545vw .4545454545vw 0 0 rgba(0,0,0,.2);font-size:1.8181818182vw;font-weight:900}.dl--pn__num:not(:first-child){margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.dl--pn__num{width:50px;height:50px;box-shadow:5px 5px 0 0 rgba(0,0,0,.2);font-size:20px}.dl--pn__num:not(:first-child){margin-left:10px}}.dl--pn__arrow{display:flex;align-items:center;justify-content:center;width:8.75vw;height:8.75vw;border-radius:12.5vw;background-color:#e72139;box-shadow:.625vw .625vw 0 0 rgba(0,0,0,.2);cursor:pointer}@media only screen and (min-width:769px){.dl--pn__arrow{transition:transform .3s}.dl--pn__arrow:hover{transform:translateY(-5%)}}.dl--pn__arrow.off{background-color:#dcdcdc;pointer-events:none}@media only screen and (min-width:769px){.dl--pn__arrow{width:4.5454545455vw;height:4.5454545455vw;box-shadow:.4545454545vw .4545454545vw 0 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1100px){.dl--pn__arrow{width:50px;height:50px;box-shadow:5px 5px 0 0 rgba(0,0,0,.2)}}.dl--pn__arrow:nth-child(2){margin-left:1.75vw}@media only screen and (min-width:769px){.dl--pn__arrow:nth-child(2){margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.dl--pn__arrow:nth-child(2){margin-left:10px}}.dl--pn__arrow:before{display:block;content:"";width:6.875vw;height:6.875vw;background-image:url(/vendor/maimai/img/common/arrow.svg);background-size:contain;background-repeat:no-repeat}@media only screen and (min-width:769px){.dl--pn__arrow:before{width:3.6363636364vw;height:3.6363636364vw}}@media only screen and (min-width:1100px){.dl--pn__arrow:before{width:40px;height:40px}}.dl--pn__arrow.double:before{background-image:url(/vendor/maimai/img/common/arrow_double.svg)}.dl--event{margin-top:6.25vw}@media only screen and (min-width:769px){.dl--event{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.dl--event{margin-top:50px}}.dl--event__header .common--header__txt{width:51.625vw;height:3.75vw}@media only screen and (min-width:769px){.dl--event__header .common--header__txt{width:33.4545454545vw;height:2.4545454545vw}}@media only screen and (min-width:1100px){.dl--event__header .common--header__txt{width:368px;height:27px}}.dl--event__img.i1{width:75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--event__img.i1{width:57.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__img.i1{width:635px;margin:40px auto 0}}.dl--event__img.i2{width:75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--event__img.i2{width:74.5454545455vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__img.i2{width:820px;margin:40px auto 0}}.dl--event__img.i3{width:75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--event__img.i3{width:54.9090909091vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__img.i3{width:604px;margin:40px auto 0}}.dl--event__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.dl--event__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.dl--event__border{width:824px;height:7px;margin:70px auto 0}}.dl--event__inner{width:75vw;margin:7.5vw auto 0}@media only screen and (min-width:769px){.dl--event__inner{width:72.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__inner{width:800px;margin:40px auto 0}}.dl--event__text{font-size:2.5vw;padding-left:1em;text-indent:-1em;line-height:1.5}@media only screen and (min-width:769px){.dl--event__text{font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.dl--event__text{font-size:12px}}.dl--event__btn{margin-top:7.5vw;width:75vw}@media only screen and (min-width:769px){.dl--event__btn{margin-top:5.9090909091vw;width:47.2727272727vw}}@media only screen and (min-width:1100px){.dl--event__btn{margin-top:65px;width:520px}}.dl--event__btn.bt1{height:15vw;margin-top:-11.25vw}@media only screen and (min-width:769px){.dl--event__btn.bt1{margin-top:-12.7272727273vw;margin-bottom:4.5454545455vw;margin-left:2.7272727273vw;width:54.5454545455vw;height:5.4545454545vw}}@media only screen and (min-width:1100px){.dl--event__btn.bt1{margin-top:-140px;margin-bottom:50px;margin-left:30px;width:600px;height:60px}}.dl--event__btn.bt1>span{width:57.875vw;margin-left:-5vw}@media only screen and (min-width:769px){.dl--event__btn.bt1>span{width:45.0909090909vw;margin-left:-1.8181818182vw}}@media only screen and (min-width:1100px){.dl--event__btn.bt1>span{width:496px;margin-left:-20px}}.ranking--box{margin-top:4.375vw}@media only screen and (min-width:769px){.ranking--box{margin-top:2.7272727273vw}}@media only screen and (min-width:1100px){.ranking--box{margin-top:30px}}.ranking--box__header--txt{color:#0b3871;font-size:3vw;font-weight:900;text-align:center;line-height:1.5;width:12.5vw}@media only screen and (min-width:769px){.ranking--box__header--txt{font-size:1.2857142857vw;width:9.0909090909vw}}@media only screen and (min-width:1100px){.ranking--box__header--txt{font-size:18px;width:100px}}.ranking--season{margin:2.5vw auto 0;width:75vw;position:relative;overflow:hidden;border-radius:45px}@media only screen and (min-width:769px){.ranking--season{margin-top:1.2727272727vw;width:76.7272727273vw;border-radius:2.7272727273vw}}@media only screen and (min-width:1100px){.ranking--season{margin-top:14px;width:844px;border-radius:30px}}.ranking--season__selectbox{overflow:hidden;text-align:center;width:100%;max-width:100%}.ranking--season__selectbox select{background:0 0;background-image:none;border:none;box-shadow:none;appearance:none;cursor:pointer;padding-right:1em;text-indent:.01px;text-overflow:ellipsis;outline:0;width:100%}.ranking--season__selectbox select::-ms-expand{display:none}.ranking--season__selectbox.slb{background:0 0;border:1vw solid #ececec;border-radius:45px;position:relative}@media only screen and (min-width:769px){.ranking--season__selectbox.slb{border-width:.3636363636vw;border-radius:2.7272727273vw}}@media only screen and (min-width:1100px){.ranking--season__selectbox.slb{border-width:4px;border-radius:30px}}.ranking--season__selectbox.slb select{color:#0b3871;font-size:3vw;font-weight:900;padding:2.75vw 0 2.75vw 16.25vw}@media only screen and (min-width:769px){.ranking--season__selectbox.slb select{font-size:2.1818181818vw;padding:1.1818181818vw 0 1.0909090909vw 26.3636363636vw}}@media only screen and (min-width:1100px){.ranking--season__selectbox.slb select{font-size:24px;padding:13px 0 12px 290px}}.ranking--season__btn{background-color:#e72139;box-sizing:border-box;pointer-events:none;position:absolute;top:0;right:0;height:110%;width:10.75vw;padding:0;z-index:2}@media only screen and (min-width:769px){.ranking--season__btn{width:5.4545454545vw}}@media only screen and (min-width:1100px){.ranking--season__btn{width:60px}}.ranking--season__btn:after{content:"";display:block;background:url(/vendor/maimai/img/common/icon_select.png) no-repeat;background-size:contain;position:absolute;top:4.625vw;right:4.5vw;height:2.25vw;width:2.5vw;z-index:3}@media only screen and (min-width:769px){.ranking--season__btn:after{top:2.1818181818vw;right:2.1818181818vw;height:1.2727272727vw;width:1.3636363636vw}}@media only screen and (min-width:1100px){.ranking--season__btn:after{top:24px;right:24px;height:14px;width:15px}}.ranking--date{font-size:2.75vw;margin-top:3.5vw;text-align:center}@media only screen and (min-width:769px){.ranking--date{font-size:1.4545454545vw;margin-top:1.6363636364vw}}@media only screen and (min-width:1100px){.ranking--date{font-size:16px;margin-top:18px}}.ranking--list{margin:2vw auto 0;width:75vw}@media only screen and (min-width:769px){.ranking--list{margin-top:1.2727272727vw;width:74.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list{margin-top:14px;width:820px}}.ranking--list__item{padding:4.5vw 0;width:100%;position:relative}@media only screen and (min-width:769px){.ranking--list__item{padding:2.7272727273vw 0 2.4545454545vw}}@media only screen and (min-width:1100px){.ranking--list__item{padding:30px 0 27px}}.ranking--list__item:nth-of-type(n+2):before{content:"";display:block;background-color:#e5e5e5;border-radius:.5vw;height:1vw;width:100%;position:absolute;top:0;left:0;right:0}@media only screen and (min-width:769px){.ranking--list__item:nth-of-type(n+2):before{border-radius:.2727272727vw;height:.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list__item:nth-of-type(n+2):before{border-radius:3px;height:6px}}.ranking--list__wrap{display:flex;flex-wrap:wrap;align-items:center;justify-content:center}.ranking--list__rank{display:flex;flex-wrap:wrap;justify-content:center;width:12.5vw}@media only screen and (min-width:769px){.ranking--list__rank{width:13.5454545455vw;padding-right:3.8181818182vw}}@media only screen and (min-width:1100px){.ranking--list__rank{width:149px;padding-right:42px}}.ranking--list__rank .ranking--list__num{background-repeat:no-repeat;background-size:100% auto;user-select:none;height:6.25vw;width:5.25vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num{height:4.5454545455vw;width:3.8181818182vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num{height:50px;width:42px}}.ranking--list__rank .ranking--list__num:nth-of-type(2n){margin-left:-.5vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num:nth-of-type(2n){margin-left:-.3636363636vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num:nth-of-type(2n){margin-left:-4px}}.ranking--list__rank .ranking--list__num.num0{background-image:url(/vendor/maimai/img/ranking/rank_num_sv0.png);height:6.25vw;width:5.375vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num.num0{height:4.5454545455vw;width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num.num0{height:50px;width:41px}}.ranking--list__rank .ranking--list__num.num1{background-image:url(/vendor/maimai/img/ranking/rank_num_sv1.png);height:6.375vw;width:5.125vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num.num1{height:4.6363636364vw;width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num.num1{height:51px;width:41px}}.ranking--list__rank .ranking--list__num.num2{background-image:url(/vendor/maimai/img/ranking/rank_num_sv2.png)}.ranking--list__rank .ranking--list__num.num3{background-image:url(/vendor/maimai/img/ranking/rank_num_sv3.png)}.ranking--list__rank .ranking--list__num.num4{background-image:url(/vendor/maimai/img/ranking/rank_num_sv4.png)}.ranking--list__rank .ranking--list__num.num5{background-image:url(/vendor/maimai/img/ranking/rank_num_sv5.png)}.ranking--list__rank .ranking--list__num.num6{background-image:url(/vendor/maimai/img/ranking/rank_num_sv6.png)}.ranking--list__rank .ranking--list__num.num7{background-image:url(/vendor/maimai/img/ranking/rank_num_sv7.png)}.ranking--list__rank .ranking--list__num.num8{background-image:url(/vendor/maimai/img/ranking/rank_num_sv8.png)}.ranking--list__rank .ranking--list__num.num9{background-image:url(/vendor/maimai/img/ranking/rank_num_sv9.png)}.ranking--list__rank.gold .ranking--list__num.num1{background-image:url(/vendor/maimai/img/ranking/rank_num_gold1.png);height:6.125vw;width:5vw}@media only screen and (min-width:769px){.ranking--list__rank.gold .ranking--list__num.num1{height:4.5454545455vw;width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank.gold .ranking--list__num.num1{height:50px;width:41px}}.ranking--list__rank.bronze .ranking--list__num.num0{background-image:url(/vendor/maimai/img/ranking/rank_num_bz0.png);height:6.25vw;width:5.375vw}@media only screen and (min-width:769px){.ranking--list__rank.bronze .ranking--list__num.num0{height:4.5454545455vw;width:3.9090909091vw}}@media only screen and (min-width:1100px){.ranking--list__rank.bronze .ranking--list__num.num0{height:50px;width:43px}}.ranking--list__rank.bronze .ranking--list__num.num1{background-image:url(/vendor/maimai/img/ranking/rank_num_bz1.png);width:5.125vw}@media only screen and (min-width:769px){.ranking--list__rank.bronze .ranking--list__num.num1{width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank.bronze .ranking--list__num.num1{width:41px}}.ranking--list__rank.bronze .ranking--list__num.num2{background-image:url(/vendor/maimai/img/ranking/rank_num_bz2.png)}.ranking--list__rank.bronze .ranking--list__num.num3{background-image:url(/vendor/maimai/img/ranking/rank_num_bz3.png)}.ranking--list__rank.bronze .ranking--list__num.num4{background-image:url(/vendor/maimai/img/ranking/rank_num_bz4.png)}.ranking--list__rank.bronze .ranking--list__num.num5{background-image:url(/vendor/maimai/img/ranking/rank_num_bz5.png)}.ranking--list__rank.bronze .ranking--list__num.num6{background-image:url(/vendor/maimai/img/ranking/rank_num_bz6.png)}.ranking--list__rank.bronze .ranking--list__num.num7{background-image:url(/vendor/maimai/img/ranking/rank_num_bz7.png)}.ranking--list__rank.bronze .ranking--list__num.num8{background-image:url(/vendor/maimai/img/ranking/rank_num_bz8.png)}.ranking--list__rank.bronze .ranking--list__num.num9{background-image:url(/vendor/maimai/img/ranking/rank_num_bz9.png)}.ranking--list__rank.bronze .ranking--list__num.num100{background-image:url(/vendor/maimai/img/ranking/rank_num_bz100.png);width:14.125vw}@media only screen and (min-width:769px){.ranking--list__rank.bronze .ranking--list__num.num100{height:4.6363636364vw;width:10.2727272727vw}}@media only screen and (min-width:1100px){.ranking--list__rank.bronze .ranking--list__num.num100{height:51px;width:113px}}.ranking--list__dtl{margin-left:3.75vw;width:57.5vw}@media only screen and (min-width:769px){.ranking--list__dtl{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:baseline;margin-left:0;width:60.9090909091vw}}@media only screen and (min-width:1100px){.ranking--list__dtl{width:670px}}.ranking--list__dtl__txt{display:flex;flex-wrap:wrap;align-items:baseline;font-weight:800}@media only screen and (min-width:769px){.ranking--list__dtl__txt{align-items:center}}.ranking--list__dtl__txt .tag{background-color:#0b3871;border-radius:1.375vw;color:#fff;display:inline-block;font-size:2.5vw;padding:.125vw 0 .25vw;text-align:center;width:10.5vw}@media only screen and (min-width:769px){.ranking--list__dtl__txt .tag{border-radius:.6363636364vw;font-size:1.0909090909vw;padding:.0909090909vw 0;margin-top:.4545454545vw;width:4.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt .tag{border-radius:7px;font-size:12px;padding:1px 0;margin-top:5px;width:50px}}.ranking--list__dtl__txt .name,.ranking--list__dtl__txt .point{font-size:3.5vw;margin-left:1.875vw}@media only screen and (min-width:769px){.ranking--list__dtl__txt .name,.ranking--list__dtl__txt .point{font-size:2.1818181818vw;margin-left:1.8181818182vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt .name,.ranking--list__dtl__txt .point{font-size:24px;margin-left:20px}}.ranking--list__dtl__txt:last-of-type{margin-top:2.75vw;padding-top:2.375vw;position:relative}@media only screen and (min-width:769px){.ranking--list__dtl__txt:last-of-type{margin-top:0;padding:1.6363636364vw 1.0909090909vw 1.6363636364vw 2.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt:last-of-type{padding:18px 12px 18px 28px}}.ranking--list__dtl__txt:last-of-type:before{content:"";display:block;background:transparent url(/vendor/maimai/img/ranking/line_dashed_sp.png) no-repeat;background-size:57.5vw auto;height:.5vw;width:57.5vw;position:absolute;top:0;right:-.625vw}@media only screen and (min-width:769px){.ranking--list__dtl__txt:last-of-type:before{background-image:url(/vendor/maimai/img/ranking/line_dashed_pc.png);background-size:.3636363636vw auto;height:5.4545454545vw;width:.3636363636vw;right:initial;left:0}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt:last-of-type:before{background-size:4px auto;height:60px;width:4px}}@media only screen and (min-width:769px){.ranking--list__dtl__txt:first-of-type{width:35.4545454545vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt:first-of-type{width:390px}}::selection{background:#001518;color:#ffcedf}body,html{height:100%;color:#000;background-color:#fff;font-family:"M PLUS 1p",sans-serif;-webkit-font-smoothing:antialiased}:focus{outline:0}*{box-sizing:border-box}body{width:100%}a{color:#000;text-decoration:none}h1,h2,h3{line-height:1}li,ul{list-style:none}td,th,tr{font-weight:400;text-align:left}img,picture{width:100%}.pc-none{display:none}@media only screen and (max-width:769px){.pc-none{display:block}}@media only screen and (max-width:769px){.sp-none{display:none}}.wrapper{position:relative;z-index:2}
diff --git a/scripts/dan-settlement-e2e.mjs b/scripts/dan-settlement-e2e.mjs
new file mode 100644
index 0000000..5fba8f4
--- /dev/null
+++ b/scripts/dan-settlement-e2e.mjs
@@ -0,0 +1,588 @@
+/**
+ * 段位認定结算页的浏览器 E2E:真的起一个 headless Edge,打开 /dan.html,
+ * 把整轮结算那一屏量一遍,顺手出三张图给 paint-probe.py 数像素:
+ *
+ * 1. 四道都打满(SSS+,达成率全金)—— 版式与着色是否照官方那套
+ * 2. 四道达成率故意拉开(金 / 蓝 / 红)—— 分数分色到底有没有落实
+ * 3. 一道超时取消 —— 不合格那一屏的占位符与红心
+ *
+ * 需要本地已经跑着一个服务(默认 `127.0.0.1:8789`,库 `data/dan-ui.sqlite`):
+ *
+ * node scripts/dan-settlement-e2e.mjs
+ * DAN_BASE=http://127.0.0.1:8788 DAN_DB=data/demo-b50.sqlite node scripts/dan-settlement-e2e.mjs
+ *
+ * 这不是 `node --test` 那套单测(那些不碰浏览器);这个脚本要 Edge、要一个跑着的服务,
+ * 所以不进 CI,只当本地验收用。段位规则本身由 tests/dan.test.ts 覆盖。
+ */
+import { spawn } from 'node:child_process';
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join, resolve } from 'node:path';
+import { setTimeout as sleep } from 'node:timers/promises';
+import { DatabaseSync } from 'node:sqlite';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const DB = process.env.DAN_DB ? resolve(process.env.DAN_DB) : join(REPO, 'data', 'dan-ui.sqlite');
+const BASE = process.env.DAN_BASE ?? 'http://127.0.0.1:8789';
+const SHOTS = process.env.DAN_SHOTS ?? join(tmpdir(), 'dan-e2e-shots');
+const EDGE = process.env.DAN_EDGE ?? 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
+const USER = Number(process.env.DAN_USER ?? 1);
+
+/** 结算盘坐标:底图 980×928,逐题行与底部三块都按底图里量出来的位置。 */
+const ROW = { left: 267, width: 576, tops: [152, 274, 396, 518] };
+const PLATE = { left: 74, top: 672, w: 240, h: 230 }; // 左下段位名牌
+const LIFE = { left: 334, top: 690, w: 200, h: 200 };
+const TOTAL_LABEL = { left: 503, top: 663, w: 354, h: 52 }; // 底图预留的白格
+const DX_ROW = { left: 608, top: 810, w: 360, h: 80 }; // 底图预留的白牌
+const RIGHT_EDGE = 950; // 大数字的右端(与 DX 白牌右端同一条边)
+/**
+ * 逐题行内部的版式(**行内坐标**)。每个数字都是从原版 `UI_CMN_RSL_KopMBase_MST.png`
+ * (636×136 的精裁版,官方设计尺寸 640×140)逐行扫像素量出来的,换算见 public/dan.css:
+ * `行内坐标 = (原版文件坐标 + 2) × 0.9`(我们的行内区 576×126 = 官方 640×140 × 0.9)。
+ */
+const CARD = {
+ frame: { left: 3.6, top: 3.6, w: 568.8, h: 118.8 }, // 边框本体(外圈 2px 主色环)
+ slot: { left: 10.8, top: 10.8, w: 105.3, h: 105.3 }, // 左上的曲绘槽(原版是方槽)
+ titlebar: { left: 122.4, top: 10.8, w: 442.8, h: 18 }, // 深蓝歌名条(占顶部一整行)
+ achbox: { left: 122.4, top: 36, w: 328.5, h: 61.2 }, // 白色达成率盒(右下角一道大 R 弧)
+ band: { left: 122.4, top: 97.2, w: 345.6, h: 23.4 }, // 底部浅色带
+ dxbox: { left: 450.9, top: 97.2, w: 119.7, h: 23.4 }, // 右下角 DX 分数白牌(胶囊)
+ badge: { left: 464, top: 43, w: 99, h: 40 }, // 右上紫场里的评级徽章
+};
+const near = (value, want, tol = 2) => Math.abs(value - want) <= tol;
+const rectNear = (rect, want, tol = 2, keys = ['left', 'top', 'w', 'h']) => Boolean(rect)
+ && keys.every((key) => near(rect[key], want[key], tol));
+const rectIn = (inner, outer, slack = 2) => Boolean(inner && outer)
+ && inner.left >= outer.left - slack && inner.top >= outer.top - slack
+ && inner.left + inner.w <= outer.left + outer.w + slack
+ && inner.top + inner.h <= outer.top + outer.h + slack;
+const fmtRect = (rect) => (rect ? `${rect.left},${rect.top} ${rect.w}×${rect.h}` : '缺失');
+/** 等级 → 素材:官方 `UI_GAM_Rank_*`(游戏内那套单级全套)。 */
+const RANK_FILE = {
+ 'SSS+': 'gam-rank-sssp.png', SSS: 'gam-rank-sss.png', 'SS+': 'gam-rank-ssp.png', SS: 'gam-rank-ss.png',
+ 'S+': 'gam-rank-sp.png', S: 'gam-rank-s.png', AAA: 'gam-rank-aaa.png', AA: 'gam-rank-aa.png',
+ A: 'gam-rank-a.png', BBB: 'gam-rank-bbb.png', BB: 'gam-rank-bb.png', B: 'gam-rank-b.png',
+ C: 'gam-rank-c.png', D: 'gam-rank-d.png',
+};
+
+let failures = 0;
+const check = (label, ok, extra = '') => {
+ console.log(`${ok ? ' PASS' : ' FAIL'} ${label}${extra ? ` — ${extra}` : ''}`);
+ if (!ok) failures += 1;
+};
+const open = () => {
+ const db = new DatabaseSync(DB);
+ db.exec('PRAGMA busy_timeout = 8000');
+ return db;
+};
+const call = async (path, body) => {
+ const res = await fetch(`${BASE}${path}`, body
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
+ : { cache: 'no-store' });
+ return { status: res.status, body: await res.json() };
+};
+const run = async (path, body) => {
+ const res = await call(path, body);
+ if (res.status !== 200) throw new Error(`${path} → HTTP ${res.status} ${JSON.stringify(res.body)}`);
+ return res.body;
+};
+
+if (!existsSync(EDGE)) {
+ console.log(`Edge 不在 ${EDGE},跳过浏览器验收`);
+ process.exit(0);
+}
+if (!existsSync(SHOTS)) mkdirSync(SHOTS, { recursive: true });
+
+/** 题目名:先看本人提交里的真名,没有就尽力从 CF 全量清单里查一次,都没有就退回题号。 */
+let cfNames = null;
+const problemName = async (problemId) => {
+ const known = open().prepare(
+ "SELECT MAX(problem_title) AS title FROM submissions WHERE platform='codeforces' AND problem_id=?").get(problemId);
+ if (known?.title) return known.title;
+ if (cfNames === null) {
+ cfNames = new Map();
+ try {
+ const res = await fetch('https://codeforces.com/api/problemset.problems', { signal: AbortSignal.timeout(20000) });
+ const body = await res.json();
+ for (const p of body?.result?.problems ?? []) cfNames.set(`${p.contestId}:${p.index}`, p.name);
+ } catch { /* 离线就用题号,不影响验收 */ }
+ }
+ return cfNames.get(problemId) ?? problemId;
+};
+
+const { secondsForAchievement } = await import(pathToFileURL(join(REPO, 'src', 'dx', 'rating.ts')).href);
+
+/* ---------- 造一轮:四道都按指定达成率通关 ---------- */
+
+const seeded = [];
+
+async function clearRun({ tier, kind, achievements }) {
+ seeded.length = 0;
+ const started = await run('/api/dan/start', { userId: USER, kind, tier, tz: 480 });
+ const sessionId = started.session.id;
+ const stageCount = started.session.stageCount;
+ for (let round = 1; round <= stageCount; round += 1) {
+ await run('/api/dan/claim', { userId: USER, sessionId });
+ const timer = open().prepare(
+ "SELECT id, problem_id FROM practice_timers WHERE status='running' ORDER BY started_at DESC LIMIT 1").get();
+ const stage = open().prepare(
+ 'SELECT difficulty FROM dan_stages WHERE session_id=? AND stage_index=?').get(sessionId, round);
+ const now = Math.floor(Date.now() / 1000);
+ const target = achievements[round - 1];
+ // `SLOW_SECONDS_FACTOR[i]` 把第 i 道的时间乘一个系数 —— 评分曲线只在 ≥97% 那段可逆,
+ const seconds = Math.round(secondsForAchievement(stage.difficulty, target).seconds);
+ const title = await problemName(timer.problem_id);
+ const write = open();
+ write.prepare('UPDATE practice_timers SET started_at=? WHERE id=?').run(now - seconds, timer.id);
+ write.prepare(`INSERT INTO submissions(account_id,platform,submission_id,problem_id,problem_title,status,submitted_at,difficulty)
+ VALUES(?,?,?,?,?,?,?,?)`).run(accountId, 'codeforces', `dan-e2e-${sessionId.slice(0, 6)}-${round}-${now}`,
+ timer.problem_id, title, 'AC', now, stage.difficulty);
+ write.close();
+ seeded.push({ sessionId, index: round, title });
+ await run('/api/dan/settle', { userId: USER, tz: 480 });
+ if (round < stageCount) await run('/api/dan/next', { userId: USER, tz: 480 });
+ }
+ const row = (await call(`/api/dan?user=${USER}&tz=480`)).body.history.find((r) => r.id === sessionId);
+ return row;
+}
+
+// 清场:收掉遗留的活动会话、清掉 dan 记录,从零开始。
+const overview = (await call(`/api/dan?user=${USER}&tz=480`)).body;
+if (overview.active) await call('/api/dan/abandon', { userId: USER, sessionId: overview.active.id });
+{
+ const clean = open();
+ clean.exec('DELETE FROM dan_stages; DELETE FROM dan_sessions; DELETE FROM practice_timers;');
+ clean.close();
+}
+const accountId = Number(open().prepare(
+ "SELECT id FROM accounts WHERE platform='codeforces' AND is_archived=0 ORDER BY id LIMIT 1").get().id);
+console.log(`账号 id=${accountId},库 ${DB},服务 ${BASE},截图 ${SHOTS}`);
+
+console.log('\n=== 1) 四道都打满(SSS+) ===');
+const perfect = await clearRun({ tier: 'advanced', kind: 'challenge', achievements: [100.8, 100.8, 100.8, 100.8] });
+check('这一轮判为 cleared', perfect?.status === 'cleared', `${perfect?.status}`);
+check('四道都记了达成率与单题 rating',
+ perfect?.stages.every((s) => typeof s.achievement === 'number' && typeof s.rating === 'number'),
+ perfect?.stages.map((s) => `${s.difficulty}:${s.achievement.toFixed(2)}%/${s.rating}`).join(' '));
+const expectAch = perfect.stages.reduce((sum, s) => sum + s.achievement, 0);
+const expectRating = Math.round(perfect.stages.reduce((sum, s) => sum + s.rating, 0) * 10) / 10;
+check('本轮总分 = 四道单题 rating 之和', Math.abs(perfect.totalRating - expectRating) < 0.05,
+ `${perfect.totalRating} vs ${expectRating}`);
+
+/* ---------- 起浏览器 ---------- */
+
+const dir = mkdtempSync(join(tmpdir(), 'dan-e2e-'));
+const edge = spawn(EDGE, ['--headless=new', '--remote-debugging-port=0', `--user-data-dir=${dir}`,
+ '--no-first-run', '--no-default-browser-check', '--window-size=1440,1400', 'about:blank'],
+ { stdio: 'ignore', windowsHide: true });
+
+let cdp = null;
+const problems = [];
+const badResponses = [];
+try {
+ for (let i = 0; i < 40 && !cdp; i += 1) {
+ await sleep(250);
+ try {
+ const [port, wsPath] = readFileSync(join(dir, 'DevToolsActivePort'), 'utf8').trim().split(/\r?\n/);
+ cdp = await (await import(pathToFileURL(join(REPO, 'src', 'fetchers', 'cf-browser.ts')).href))
+ .Cdp.connect(`ws://127.0.0.1:${port}${wsPath}`);
+ } catch { /* Edge 还没起来 */ }
+ }
+ if (!cdp) throw new Error('没连上 Edge 的 CDP');
+ const { targetId } = await cdp.call('Target.createTarget', { url: 'about:blank' });
+ const { sessionId } = await cdp.call('Target.attachToTarget', { targetId, flatten: true });
+ cdp.socket.addEventListener('message', (event) => {
+ const message = JSON.parse(String(event.data));
+ if (message.method === 'Runtime.exceptionThrown') {
+ problems.push(`异常: ${message.params.exceptionDetails.text} ${message.params.exceptionDetails.exception?.description ?? ''}`);
+ }
+ if (message.method === 'Runtime.consoleAPICalled' && ['error', 'warning'].includes(message.params.type)) {
+ problems.push(`console.${message.params.type}: ${message.params.args.map((a) => a.value ?? a.description).join(' ')}`);
+ }
+ if (message.method === 'Network.responseReceived') {
+ const { url, status } = message.params.response;
+ if (status >= 400 && url.includes('127.0.0.1')) badResponses.push(`${status} ${url}`);
+ }
+ });
+ for (const domain of ['Runtime', 'Log', 'Page', 'Network']) await cdp.call(`${domain}.enable`, {}, sessionId);
+ await cdp.call('Emulation.setDeviceMetricsOverride',
+ { width: 1440, height: 1400, deviceScaleFactor: 1, mobile: false }, sessionId);
+
+ const evaluate = async (expression) => {
+ const result = await cdp.call('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }, sessionId);
+ if (result.exceptionDetails) throw new Error(`页面内异常 ${JSON.stringify(result.exceptionDetails)}`);
+ return result.result.value;
+ };
+ const openPage = async (url, wait = 3200) => { await cdp.call('Page.navigate', { url }, sessionId); await sleep(wait); };
+ const shot = async (name) => {
+ const { data } = await cdp.call('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true }, sessionId);
+ writeFileSync(join(SHOTS, name), Buffer.from(data, 'base64'));
+ console.log(` 截图 → ${join(SHOTS, name)}`);
+ };
+ /** 只拍结算盘本身(文档插图用,不带上页面下那堆说明)。 */
+ const shotDialog = async (name) => {
+ const rect = JSON.parse(await evaluate(`(() => {
+ const d = document.querySelector('dialog.dan-result');
+ if (!d) return 'null';
+ const r = d.getBoundingClientRect();
+ return JSON.stringify({ x: r.left + window.scrollX, y: r.top + window.scrollY,
+ width: r.width, height: r.height });
+ })()`));
+ if (!rect) return;
+ const { data } = await cdp.call('Page.captureScreenshot',
+ { format: 'png', clip: { ...rect, scale: 2 }, captureBeyondViewport: true }, sessionId);
+ writeFileSync(join(SHOTS, name), Buffer.from(data, 'base64'));
+ console.log(` 截图 → ${join(SHOTS, name)}`);
+ };
+ const clickByText = (selector, text) => evaluate(`(() => {
+ const node = [...document.querySelectorAll(${JSON.stringify(selector)})].find((n) => n.textContent.includes(${JSON.stringify(text)}));
+ if (!node) return 'NOT_FOUND';
+ node.click();
+ return 'CLICKED';
+ })()`);
+ /** 打开最新一轮的结算页:认定记录里那条「查看结算」。 */
+ const openLatestResult = async () => {
+ await openPage(`${BASE}/dan.html`);
+ const clicked = await clickByText('.dan-actions .btn', '查看结算');
+ await sleep(1500);
+ return clicked;
+ };
+ /** 读结算盘:行内组件用**行底板**做原点,底部组件用底图做原点(都是底图 980×928 那一套坐标)。 */
+ const readLayout = () => evaluate(`(() => {
+ const dialog = document.querySelector('dialog.dan-result');
+ const art = dialog?.querySelector('.dan-result-art');
+ if (!art) return JSON.stringify({ open: false, rows: [] });
+ const box = art.getBoundingClientRect();
+ const relTo = (origin, node) => {
+ if (!node) return null;
+ const r = node.getBoundingClientRect();
+ return { left: Math.round(r.left - origin.left), top: Math.round(r.top - origin.top),
+ w: Math.round(r.width), h: Math.round(r.height) };
+ };
+ const rel = (node) => relTo(box, node);
+ const text = (root, sel) => root.querySelector(sel)?.textContent ?? null;
+ // aria-label 挂在最里面那层 .dan-num 上;选择器可能指的是包着它的容器。
+ const numNode = (root, sel) => {
+ const node = root.querySelector(sel);
+ if (!node) return null;
+ return node.classList.contains('dan-num') ? node : node.querySelector('.dan-num');
+ };
+ const shown = (root, sel) => numNode(root, sel)?.getAttribute('aria-label') ?? null;
+ const font = (root, sel) => {
+ const node = numNode(root, sel);
+ if (!node) return null;
+ // 真正上色/上贴图的是里面那两层 :分数族走 background-image(自带颜色),
+ // 白字族走 mask + background-color(游戏里就是这样运行时染色的)。
+ const fill = node.querySelector('.dan-num-fill');
+ const style = getComputedStyle(fill ?? node);
+ const cell = node.querySelector('.dan-num-char');
+ const cellStyle = cell ? getComputedStyle(cell) : null;
+ const advance = cell
+ ? cell.getBoundingClientRect().width + (parseFloat(cellStyle.marginRight) || 0) : 0;
+ // 图集里没有的字符(「—」这种占位符)走普通文字那条路:颜色落在文字本身上。
+ const plain = node.querySelector('.dan-num-plain');
+ return {
+ fill: plain ? getComputedStyle(plain).color : style.backgroundColor,
+ mask: style.maskImage || style.webkitMaskImage,
+ atlas: style.backgroundImage,
+ outline: Boolean(node.querySelector('.dan-num-outline')),
+ label: node.getAttribute('aria-label'),
+ height: Math.round(node.getBoundingClientRect().height),
+ advance: Math.round(advance * 100) / 100,
+ width: Math.round(node.getBoundingClientRect().width),
+ };
+ };
+ return JSON.stringify({
+ open: Boolean(dialog?.open),
+ plate: { rect: rel(dialog.querySelector('.dan-verdict')), name: text(dialog, '.dan-plate-name'),
+ nameLen: dialog.querySelector('.dan-plate-name')?.dataset.len ?? null,
+ nameFont: (() => { const n = dialog.querySelector('.dan-plate-name'); if (!n) return null;
+ const s = getComputedStyle(n); return { size: s.fontSize, weight: s.fontWeight, fill: s.color }; })(),
+ count: text(dialog, '.dan-verdict-count'),
+ legacyStamp: Boolean(dialog.querySelector('.dan-verdict-img')),
+ nameRect: rel(dialog.querySelector('.dan-plate-name')) },
+ life: { rect: rel(dialog.querySelector('.dan-life')), label: shown(dialog, '.dan-life-count'),
+ base: dialog.querySelector('.dan-life-base')?.getAttribute('src')?.split('/').pop() ?? null },
+ total: { label: text(dialog, '.dan-total-label'), labelRect: rel(dialog.querySelector('.dan-total-label')),
+ num: shown(dialog, '.dan-total-num'), rect: rel(dialog.querySelector('.dan-total-num')),
+ font: font(dialog, '.dan-total-num') },
+ dx: { label: text(dialog, '.dan-dxscore-label'), num: shown(dialog, '.dan-dxscore-num'),
+ rect: rel(numNode(dialog, '.dan-dxscore-num')), rowRect: rel(dialog.querySelector('.dan-dxscore-row')),
+ labelRect: rel(dialog.querySelector('.dan-dxscore-label')), font: font(dialog, '.dan-dxscore-num') },
+ rows: [...dialog.querySelectorAll('.dan-track')].map((track) => {
+ const rowBox = track.getBoundingClientRect();
+ return {
+ rect: rel(track),
+ no: text(track, '.dan-track-no'), name: text(track, '.dan-track-name'),
+ // 档位名牌已经删掉;这两项留着是为了断言它**不在**(并且标题条左移顶上那一格)。
+ diff: text(track, '.dan-track-diff'), diffLines: track.querySelectorAll('.dan-track-diff > *').length,
+ titlebar: relTo(rowBox, track.querySelector('.dan-track-titlebar')),
+ jacket: text(track, '.dan-track-jacket'), jacketBig: text(track, '.dan-track-jacket b'),
+ jacketSmall: text(track, '.dan-track-jacket small'),
+ ach: shown(track, '.dan-track-achvalue'), achFont: font(track, '.dan-track-achvalue'),
+ // 达成率那串数字自己的框(取里面那层 .dan-num,不是外面那个定宽容器)。
+ achNumRect: relTo(rowBox, numNode(track, '.dan-track-achvalue')),
+ dx: shown(track, '.dan-track-dxnum'), dxFont: font(track, '.dan-track-dxnum'),
+ limit: text(track, '.dan-track-limit'), judge: text(track, '.dan-track-judge'),
+ badge: track.querySelector('.dan-track-badge')?.getAttribute('src')?.split('/').pop() ?? '',
+ // badgeRect / stamp 是**底图坐标**(抠图用),badgeRow 才是行内坐标。
+ badgeRect: rel(track.querySelector('.dan-track-badge')),
+ badgeRow: relTo(rowBox, track.querySelector('.dan-track-badge')),
+ stamp: rel(track.querySelector('.dan-track-stamp')),
+ boxes: ['no', 'jacket', 'dxbox', 'dxnum', 'titlebar', 'name', 'diff', 'achbox', 'achvalue',
+ 'badge', 'limit', 'frame', 'band']
+ .map((key) => [key, relTo(rowBox, track.querySelector('.dan-track-' + key))]).filter(([, r]) => r),
+ };
+ }),
+ });
+ })()`);
+
+ console.log('\n=== 2) 结算页版式(四道满分成 → 金) ===');
+ check('认定记录里点得到「查看结算」', await openLatestResult() === 'CLICKED');
+ const page = JSON.parse(await readLayout());
+ check('结算页弹出来了', page.open === true);
+ const rows = page.rows ?? [];
+ check('四道各一张卡', rows.length === 4, `${rows.length} 张`);
+ check('四张卡按官方坐标摆:x 267 宽 576,行距 122',
+ rows.every((row, i) => Math.abs(row.rect.left - ROW.left) <= 2 && Math.abs(row.rect.w - ROW.width) <= 2
+ && Math.abs(row.rect.top - ROW.tops[i]) <= 2),
+ rows.map((row) => `${row.rect.top}@${row.rect.w}`).join(' '));
+ const outOfRow = [];
+ for (const row of rows) {
+ for (const [key, rect] of row.boxes) {
+ if (rect.left < -2 || rect.top < -2 || rect.left + rect.w > row.rect.w + 2 || rect.top + rect.h > row.rect.h + 2) {
+ outOfRow.push(`${row.no}:${key}`);
+ }
+ }
+ }
+ check('行内组件都待在底板里', outOfRow.length === 0, outOfRow.join(','));
+ /** 行内子组件的 rect(`boxes` 里量的,全部是**行内坐标**)。 */
+ const boxOf = (row, key) => (row.boxes.find(([name]) => name === key) ?? [])[1];
+ check('边框本体照原版量出来的比例摆(行内 3.6,3.6 / 568.8×118.8)',
+ rows.every((row) => rectNear(boxOf(row, 'frame'), CARD.frame)),
+ fmtRect(boxOf(rows[0], 'frame')));
+ check('曲绘槽是原版那块方槽(行内 10.8,10.8 / 105.3×105.3),里面写完整题号 + 这一题的 CF rating',
+ rows.every((row) => rectNear(boxOf(row, 'jacket'), CARD.slot)
+ && /^\d+[A-Z]+\d*$/.test(row.jacketBig) && /^\d+$/.test(row.jacketSmall)),
+ `${fmtRect(boxOf(rows[0], 'jacket'))} · ${rows[0].jacketBig}/${rows[0].jacketSmall}`);
+ check('难度名牌已删掉,歌名条占顶部一整行(左端与白盒左端对齐、右端贴到边框内右沿)',
+ rows.every((row) => !row.diff && row.diffLines === 0
+ && rectNear(row.titlebar, CARD.titlebar)
+ // 「占一整行」= 左端跟白盒左端对齐(原版那格难度名牌的位置被歌名条顶掉),
+ // 右端离边框外沿只剩 7.2px(原版 8px × 0.9)。
+ && near(row.titlebar.left, boxOf(row, 'achbox').left)
+ && near(row.titlebar.left + row.titlebar.w, CARD.frame.left + CARD.frame.w - 7.2)
+ && near(row.titlebar.top, CARD.frame.top + 7.2)),
+ rows.map((row) => `diff=${row.diff || '无'} 条 ${row.titlebar?.left}…${row.titlebar?.left + row.titlebar?.w}`).join(' '));
+ // 达成率那串数字必须待在白盒里,而且白盒不能碰到右边那块紫场里的评级徽章
+ // (原版白盒右下角那道大 R 弧就是给徽章让位的)。
+ const spill = rows.map((row) => {
+ const box = boxOf(row, 'achbox');
+ const num = row.achNumRect;
+ if (!box || !num) return 'no-box';
+ const right = num.left + num.w;
+ if (num.left < box.left) return `数字出盒左 ${num.left} < ${box.left}`;
+ if (right > box.left + box.w - 6) return `数字出盒右 ${right} > ${box.left + box.w - 6}`;
+ // 右对齐:整串的右端固定在离白盒右沿 6…20px 的地方(短一位数的串不许往左缩)。
+ if (right < box.left + box.w - 20) return `数字没右对齐 ${right} < ${box.left + box.w - 20}`;
+ if (num.top < box.top || num.top + num.h > box.top + box.h + 1) {
+ return `数字出盒高 ${num.top}..${num.top + num.h} vs ${box.top}..${box.top + box.h}`;
+ }
+ if (row.badgeRow && box.left + box.w + 10 > row.badgeRow.left) {
+ return `白盒碰徽章 ${Math.round(box.left + box.w + 10 - row.badgeRow.left)}px`;
+ }
+ return '';
+ }).filter(Boolean);
+ check('达成率数字不溢出白盒、白盒与右侧徽章留 10px 以上空隙',
+ spill.length === 0,
+ spill.join(',') || `数字 ${fmtRect(rows[0].achNumRect)} 盒 ${fmtRect(boxOf(rows[0], 'achbox'))}`
+ + ` 徽章左 ${rows[0].badgeRow?.left}`);
+ check('DX 分数在卡片右下角、右端贴到边框内右沿(与底带同高的那块白牌)',
+ rows.every((row) => {
+ const dx = boxOf(row, 'dxbox');
+ const box = boxOf(row, 'achbox');
+ return rectNear(dx, CARD.dxbox)
+ && near(dx.left + dx.w, CARD.frame.left + CARD.frame.w - 1.8)
+ && dx.top >= box.top + box.h - 2 && dx.left >= box.left + box.w;
+ }),
+ fmtRect(boxOf(rows[0], 'dxbox')));
+ check('限时写进底带左侧(原版那条浅色带,右端才是 DX 分数白牌)',
+ rows.every((row) => {
+ const band = boxOf(row, 'band');
+ const limit = boxOf(row, 'limit');
+ const dx = boxOf(row, 'dxbox');
+ return rectNear(band, CARD.band) && rectIn(limit, band, 0) && near(limit.left, band.left)
+ && limit.left + limit.w <= dx.left + 1;
+ }),
+ `带 ${fmtRect(boxOf(rows[0], 'band'))} · 限时 ${fmtRect(boxOf(rows[0], 'limit'))}`);
+ check('评级徽章放进右上那块紫场(白盒右边 · 歌名条下边 · 白牌上边)',
+ rows.every((row) => {
+ if (!row.badgeRow) return true; // 超时/未开始那一屏用文字判定,没有徽章
+ const box = boxOf(row, 'achbox');
+ const dx = boxOf(row, 'dxbox');
+ return rectNear(row.badgeRow, CARD.badge)
+ && row.badgeRow.left >= box.left + box.w
+ && row.badgeRow.top >= row.titlebar.top + row.titlebar.h
+ && row.badgeRow.top + row.badgeRow.h <= dx.top;
+ }),
+ fmtRect(rows[0].badgeRow));
+ check('标题条写题目名,不是「段位認定第 N 道」',
+ rows.every((row, i) => row.name && row.name === seeded[i]?.title && !/段位認定第/.test(row.name)),
+ rows.map((row) => row.name).join(' | '));
+ check('评级徽章是游戏内那套单级素材且按原版比例放到 99×40(不再拿「A~AAA」区间图充数)',
+ rows.every((row) => row.badge === 'gam-rank-sssp.png' && Math.abs(row.badgeRect.w - CARD.badge.w) <= 2
+ && Math.abs(row.badgeRect.h - CARD.badge.h) <= 2),
+ `${rows[0]?.badge} ${rows[0]?.badgeRect?.w}×${rows[0]?.badgeRect?.h}`);
+ check('四道达成率都上了官方金色图集',
+ rows.every((row) => /num-score-gold/.test(row.achFont?.atlas ?? '')), rows.map((row) => row.achFont?.atlas).join(' | '));
+ check('单题 DX 分数不叠描边层(小字要干净)',
+ rows.every((row) => row.dxFont?.outline === false && /rgb\(13, 42, 99\)/.test(row.dxFont?.fill ?? '')),
+ `描边层 ${rows[0]?.dxFont?.outline} / ${rows[0]?.dxFont?.fill}`);
+
+ check('左下白框里写档位名(不再是合格印)',
+ page.plate.name === '上级' && page.plate.legacyStamp === false, `${page.plate.name} · 旧印 ${page.plate.legacyStamp}`);
+ check('段位名落在白框里、字重够重',
+ page.plate.rect.left === PLATE.left && page.plate.rect.top === PLATE.top
+ && page.plate.rect.w === PLATE.w && page.plate.rect.h === PLATE.h && Number(page.plate.nameFont.weight) >= 800,
+ `${page.plate.rect.left},${page.plate.rect.top} ${page.plate.rect.w}×${page.plate.rect.h} · ${page.plate.nameFont.size}/${page.plate.nameFont.weight}`);
+ check('结论变成名字下面一行小字', /合格 · 4 \/ 4 道通关/.test(page.plate.count ?? ''), page.plate.count);
+ check('心放大到 200×200 且用官方绿色底盘,心里写通关道数',
+ page.life.rect.w === LIFE.w && page.life.rect.h === LIFE.h
+ && page.life.base === 'life-base-green.png' && page.life.label === '4',
+ `${page.life.rect.w}×${page.life.rect.h} · ${page.life.base} · ${page.life.label}`);
+
+ check('「总达成率」标签落进底图预留的白格',
+ Math.abs(page.total.labelRect.left - TOTAL_LABEL.left) <= 2 && Math.abs(page.total.labelRect.top - TOTAL_LABEL.top) <= 2
+ && Math.abs(page.total.labelRect.w - TOTAL_LABEL.w) <= 2 && Math.abs(page.total.labelRect.h - TOTAL_LABEL.h) <= 2,
+ `${page.total.labelRect.left},${page.total.labelRect.top} ${page.total.labelRect.w}×${page.total.labelRect.h}`);
+ check('总达成率 = 四道达成率之和(官方金色图集)',
+ Math.abs(Number(String(page.total.num).replace('%', '')) - expectAch) < 0.01 && /%$/.test(page.total.num ?? ''),
+ `${page.total.num} vs ${expectAch.toFixed(4)}%`);
+ const totalRight = page.total.rect.left + page.total.rect.w;
+ check('大数字落在色带上、右端对齐 x 950',
+ Math.abs(totalRight - RIGHT_EDGE) <= 6 && page.total.rect.top >= 700 && page.total.rect.top + page.total.rect.h <= DX_ROW.top,
+ `右 ${totalRight} · y ${page.total.rect.top}..${page.total.rect.top + page.total.rect.h}`);
+ const glyphs = String(page.total.num).length + 1;
+ // 官方是密排:分数族那套图集格宽 74 / 格高 98,密排后每字步进 67 ⇒ 0.68 上下。
+ check('数字密排(官方字距,不再一个字一个字分开)',
+ page.total.font.advance / page.total.font.height <= 0.69,
+ `每字步进 ${page.total.font.advance}px / 字高 ${page.total.font.height} = ${(page.total.font.advance / page.total.font.height).toFixed(3)}(占位 ${glyphs} 个字形,整串宽 ${page.total.font.width})`);
+
+ check('DX 分数条就是底图预留的那条白牌',
+ Math.abs(page.dx.rowRect.left - DX_ROW.left) <= 2 && Math.abs(page.dx.rowRect.top - DX_ROW.top) <= 2
+ && Math.abs(page.dx.rowRect.w - DX_ROW.w) <= 2 && Math.abs(page.dx.rowRect.h - DX_ROW.h) <= 2,
+ `${page.dx.rowRect.left},${page.dx.rowRect.top} ${page.dx.rowRect.w}×${page.dx.rowRect.h}`);
+ check('标签写「DX分数」(照官方,不再自造「合计」)', page.dx.label === 'DX分数', page.dx.label);
+ check('DX 分数 = 四道单题 rating 之和(深蓝、不带描边)',
+ Math.abs(Number(page.dx.num) - expectRating) < 0.05 && /rgb\(13, 42, 99\)/.test(page.dx.font?.fill ?? '')
+ && page.dx.font?.outline === false,
+ `${page.dx.num} vs ${expectRating} · ${page.dx.font?.fill} · 描边层 ${page.dx.font?.outline}`);
+ check('DX 数字在白牌右半边、且没出牌面',
+ page.dx.rect.left >= DX_ROW.left + 120 && page.dx.rect.left + page.dx.rect.w <= DX_ROW.left + DX_ROW.w + 2,
+ `${page.dx.rect.left}..${page.dx.rect.left + page.dx.rect.w} 牌面 ${DX_ROW.left}..${DX_ROW.left + DX_ROW.w}`);
+
+ await shot('E-整轮结算页.png');
+ await shotDialog('D-结算盘.png');
+ // 给 paint-probe.py 数像素用的小图(按底图坐标抠)
+ const origin = JSON.parse(await evaluate(`(() => {
+ const b = document.querySelector('dialog.dan-result .dan-result-art').getBoundingClientRect();
+ return JSON.stringify({ x: b.left + window.scrollX, y: b.top + window.scrollY });
+ })()`));
+ const row0 = rows[0];
+ // 行内那些小图是**行内相对坐标**,抠图要加上第 1 行在底图上的原点,否则会拍错地方;
+ // 印与徽章本来就是底图坐标(readLayout 里用 rel(art) 量的),不能再加一次。
+ const inRow = (rel) => (rel ? { left: rel.left + row0.rect.left, top: rel.top + row0.rect.top, w: rel.w, h: rel.h } : null);
+ const rectOf = (key) => inRow((row0.boxes.find(([name]) => name === key) ?? [])[1]);
+ const clipTargets = {
+ ach: rectOf('achvalue'), achbox: rectOf('achbox'), score: rectOf('dxnum'),
+ stamp: row0.stamp, badge: row0.badgeRect,
+ total: page.total.rect, dxtotal: page.dx.rect, life: page.life.rect,
+ };
+ const rectsOut = {};
+ for (const [name, rect] of Object.entries(clipTargets)) {
+ if (!rect) continue;
+ const clip = { x: origin.x + rect.left, y: origin.y + rect.top, width: rect.w, height: rect.h, scale: 1 };
+ rectsOut[name] = clip;
+ const { data } = await cdp.call('Page.captureScreenshot', { format: 'png', clip, captureBeyondViewport: true }, sessionId);
+ writeFileSync(join(SHOTS, `H-${name}.png`), Buffer.from(data, 'base64'));
+ }
+ writeFileSync(join(SHOTS, 'H-rects.json'), JSON.stringify(rectsOut, null, 2));
+
+ /* ---------- 3) 达成率拉开:金 / 蓝 / 红 ---------- */
+
+ console.log('\n=== 3) 四道达成率拉开(金 / 蓝 / 红) ===');
+ /* 注:达成率是由目标值反推秒数再提交的,而评分曲线只在 ≥97% 段可逆 —— 想看到 <80% 的
+ * 蓝档只能从秒数那头放大(见 SLOW_SECONDS_FACTOR)。第 4 道 ×1.6 落到 60% 上下 ⇒ 蓝。 */
+ let slowSeconds = [];
+ const mixed = await clearRun({ tier: 'advanced', kind: 'challenge', achievements: [100.8, 98.5, 96.0, 92.0] });
+ check('这一轮同样判为 cleared(限时内 AC 就算过,跟达成率高低无关)', mixed?.status === 'cleared', `${mixed?.status}`);
+ check('四道达成率确实拉开了',
+ new Set(mixed.stages.map((s) => Math.round(s.achievement))).size === 4,
+ mixed.stages.map((s) => `${s.achievement.toFixed(2)}%`).join(' '));
+ await openLatestResult();
+ const mixedPage = JSON.parse(await readLayout());
+ const tones = mixedPage.rows.map((row) => (/num-score-(\w+)\.png/.exec(row.achFont?.atlas ?? '') ?? [])[1]);
+ check('颜色按达成率换:≥97 金、80~96.99 红(合成用例只够得到这两档)',
+ tones.join(',') === 'gold,gold,red,red', `${tones.join(',')} · ${mixed.stages.map((s) => `${s.achievement.toFixed(2)}%`).join(' ')}`);
+ check('评级徽章跟着达成率走(逐级对应,不再共用一个区间图)',
+ mixedPage.rows.every((row, i) => row.badge === RANK_FILE[mixed.stages[i].rank]),
+ mixedPage.rows.map((row, i) => `${mixed.stages[i].rank}=${row.badge}`).join(' '));
+ check('总达成率是四道之和 ⇒ 仍然 ≥100% ⇒ 金',
+ /num-score-gold/.test(mixedPage.total.font?.atlas ?? '') && /%$/.test(mixedPage.total.num ?? ''),
+ `${mixedPage.total.num} · ${mixedPage.total.font?.atlas}`);
+ await shot('F-分数分色结算页.png');
+ await shotDialog('D-结算盘-分色.png');
+
+ /* ---------- 4) 超时取消 ---------- */
+
+ console.log('\n=== 4) 超时取消那一轮 ===');
+ const timedId = `e2e-timeout-${Date.now()}`;
+ const timerId = `${timedId}-1`;
+ {
+ const t = Math.floor(Date.now() / 1000);
+ const write = open();
+ write.prepare(`INSERT INTO dan_sessions(id,user_id,tier,kind,stage_count,limit_seconds,min_rating,max_rating,status,started_at)
+ VALUES(?,?,?,?,?,?,?,?,?,?)`).run(timedId, USER, 'beginner', 'challenge', 4, 1800, 800, 1100, 'active', t + 1);
+ write.prepare(`INSERT INTO practice_timers(id,user_id,account_id,handle_key,problem_id,practice_kind,status,started_at,ended_at)
+ VALUES(?,?,?,?,?,?,?,?,?)`).run(timerId, USER, accountId, 'dan-e2e', '559:A', 'unknown', 'completed', t - 4000, t - 1000);
+ write.prepare(`INSERT INTO dan_stages(session_id,stage_index,problem_id,difficulty,drawn_at,claimed_at,timer_id,limit_seconds)
+ VALUES(?,?,?,?,?,?,?,?)`).run(timedId, 1, '559:A', 800, t - 4000, t - 4000, timerId, 1800);
+ write.close();
+ }
+ await call('/api/dan/settle', { userId: USER, tz: 480 });
+ const failed = (await call(`/api/dan?user=${USER}&tz=480`)).body.history.find((r) => r.id === timedId);
+ check('超时那一道被记为 timeout', failed?.stages[0]?.outcome === 'timeout', `${failed?.stages[0]?.outcome}`);
+ check('这一轮判为 failed,且没有继续抽第 2 道', failed?.status === 'failed' && failed?.stages.length === 1,
+ `${failed?.status} / ${failed?.stages.length} 道`);
+ await openLatestResult();
+ const failPage = JSON.parse(await readLayout());
+ check('失败那一屏也弹得出来', failPage.open === true);
+ check('档位名照样写在白框里,结论是小字「不合格」',
+ failPage.plate.name === '初级' && /不合格 · 0 \/ 4 道通关/.test(failPage.plate.count ?? ''),
+ `${failPage.plate.name} · ${failPage.plate.count}`);
+ check('只有一张卡、没有评级徽章、判定写「超时」',
+ failPage.rows.length === 1 && failPage.rows[0].badge === '' && failPage.rows[0].judge === '超时',
+ `${failPage.rows.length} 张 / 徽章「${failPage.rows[0]?.badge}」/ 判定「${failPage.rows[0]?.judge}」`);
+ check('没有成绩的两处都是占位「—」', failPage.total.num === '—' && failPage.dx.num === '—',
+ `${failPage.total.num} / ${failPage.dx.num}`);
+ check('占位「—」也染了深蓝(白字族是纯白剪影,白底上会看不见)',
+ /rgb\(13, 42, 99\)/.test(failPage.total.font?.fill ?? '') && /rgb\(13, 42, 99\)/.test(failPage.dx.font?.fill ?? ''),
+ `${failPage.total.font?.fill} / ${failPage.dx.font?.fill}`);
+ check('心换成红色底盘,写 0', failPage.life.base === 'life-base-red.png' && failPage.life.label === '0',
+ `${failPage.life.base} / ${failPage.life.label}`);
+ await shot('G-不合格结算页.png');
+
+ console.log('\n=== 5) 控制台与请求 ===');
+ check('页面没有抛异常 / console.error', problems.length === 0, problems.slice(0, 3).join(' | '));
+ check('没有指向本服务的 4xx/5xx', badResponses.length === 0, badResponses.slice(0, 3).join(' | '));
+} catch (error) {
+ console.log(`\n出错:${error instanceof Error ? error.message : String(error)}`);
+ failures += 1;
+} finally {
+ try { cdp?.socket.close(); } catch { /* 已经关了 */ }
+ edge.kill();
+}
+
+console.log(`\n结果:${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
+process.exit(failures === 0 ? 0 : 1);
\ No newline at end of file
diff --git a/scripts/export-original-card.py b/scripts/export-original-card.py
new file mode 100644
index 0000000..cbafa9b
--- /dev/null
+++ b/scripts/export-original-card.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""把**原版**的乐曲卡片素材原样导出来,供对照。
+
+用途:现在卡片的边框是照着官方贴图的版式用 CSS 画的(`.dan-track-frame`),用户要直接看
+原版素材本身。这里把逐题行那一圈的原图导到 `docs/images/original-card/`,并顺手把当前渲染
+出来的第 1 张卡裁成一张独立小图 `docs/images/dan-card.png`。
+
+用法:
+ python scripts/export-original-card.py "<游戏目录>" # 含 resources.assets 的那一层
+"""
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import UnityPy
+
+# 逐题行那一圈的原版贴图。`UI_CMN_RSL_KopMBase_*` 就是官方乐曲卡片的边框(640×140,
+# 五种难度各一张,颜色与难度名 BASiC/ADVANCED/EXPERT/MASTER/Re:MASTER 都烘在图里)。
+WANTED = [
+ "UI_CMN_RSL_KopMBase_BSC",
+ "UI_CMN_RSL_KopMBase_ADV",
+ "UI_CMN_RSL_KopMBase_EXP",
+ "UI_CMN_RSL_KopMBase_MST",
+ "UI_CMN_RSL_KopMBase_MST_Re",
+ "UI_CMN_RSL_JacketImage_S",
+ "UI_CMN_RSL_MusicJacket_Base",
+ "UI_CMN_RSL_JaketTrack",
+ "UI_CMN_RSL_JaketTrackNo_0",
+ "UI_CMN_RSL_AllScorekBase",
+ "UI_DNM_Result_musicBase_01",
+ "UI_RSL_AchivementCounter",
+ "UI_DNM_AchivementCounter",
+]
+PREFIXES = ("UI_CMN_RSL_", "UI_DNM_Result_music", "UI_RSL_Achivement", "UI_DNM_Achivement")
+
+
+def main() -> int:
+ if len(sys.argv) < 2:
+ print(__doc__)
+ return 2
+ game = Path(sys.argv[1])
+ out = Path(__file__).resolve().parent.parent / "docs" / "images" / "original-card"
+ out.mkdir(parents=True, exist_ok=True)
+ env = UnityPy.load(str(game / "resources.assets"))
+ saved, extra = [], []
+ for obj in env.objects:
+ if obj.type.name not in ("Texture2D", "Sprite"):
+ continue
+ try:
+ data = obj.read()
+ name = getattr(data, "m_Name", "") or ""
+ if not name:
+ continue
+ if name in WANTED:
+ image = data.image
+ image.save(out / f"{name}.png")
+ saved.append((name, image.size))
+ elif any(name.startswith(p) for p in PREFIXES):
+ extra.append(name)
+ except Exception as exc: # 有的贴图数据在别的 bundle 里,读不到就跳过
+ if getattr(obj, "read", None):
+ try:
+ name = obj.read().m_Name
+ except Exception:
+ name = "?"
+ if name in WANTED:
+ print(f" 跳过 {name}:{type(exc).__name__}")
+ for name, size in sorted(saved):
+ print(f" 导出 {name} {size[0]}x{size[1]}")
+ if extra:
+ print("同前缀但未导出的(想看哪个再说):")
+ for name in sorted(extra):
+ print(f" {name}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
\ No newline at end of file
diff --git a/scripts/extract-maimai-assets.py b/scripts/extract-maimai-assets.py
new file mode 100644
index 0000000..061ace6
--- /dev/null
+++ b/scripts/extract-maimai-assets.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+"""从 maimai DX 客户端资源里提取段位認定(Dani Mode)结算页用到的贴图。
+
+这是**开发工具**,不是应用依赖:仓库里已经放好了提取结果
+(public/assets/maimai/*.png),只有要重新提一遍时才跑它。
+
+用法:
+ pip install UnityPy texture2ddecoder Pillow
+ python scripts/extract-maimai-assets.py "<游戏目录>" [输出目录]
+
+`<游戏目录>` 是客户端里含 `resources.assets` 的那一层,例如
+ E:/Downloads/156Sinmai_Data/156Sinmai_Data/156Sinmai_Data
+
+为什么要这么提:段位認定结算那整屏是**成品图**(页头文字、格纹页眉、两侧水引、
+下半部色带与左下角段位名牌白框全部烘焙在底图里),运行时只是把逐题行、达成率数字、
+段位名与总达成率盖上去。所以页面直接用这张底图 + 几件小素材,比自己用 CSS 画近似得多。
+注意:底图下半部那两三块**挖白的空面板**是原作留给运行时文字的位置,不要再拿别的脚本
+把它们续画填掉(第 12 轮干过一次,第 13 轮撤了)。
+"""
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import UnityPy
+
+# 仓库里用的文件名 ← 游戏里的贴图名。只搬这几件,不是整个 UI_DNM 家族(159 张)。
+#
+# 故意**不**搬的一件:逐题行的乐曲边框 `UI_CMN_RSL_KopMBase_{BSC,ADV,EXP,MST,MST_Re}`
+# (640×140,五种难度各一张)。它是逐题行那一圈底色,但颜色与难度名(BASIC/ADVANCED/
+# EXPERT/MASTER/Re:MASTER)都烘死在图里,套不上我们自己的 CF 难度分档。所以边框按它的
+# 版式用 CSS 画一层(见 dan.css 的 .dan-track-frame):外圈浅色环 + 主色场 + 底部浅色带
+# + 左侧白曲绘槽 + 深蓝标题条 + 白色达成率框,尺寸全按这张贴图量出来的像素。
+WANTED: dict[str, str] = {
+ "UI_DNM_Result_Base_01": "result-dani.png", # 「段位認定」整屏底图 980×928
+ "UI_DNM_Result_Base_03": "result-random.png", # 「ランダム段位認定」整屏底图
+ "UI_DNM_Result_musicBase_01": "track-plate.png", # 逐题行底板 576×124
+ "UI_DNM_Icon_Result_01": "stamp-clear.png", # 逐题「可」印 108×108
+ "UI_DNM_Icon_Result_02": "stamp-fail.png", # 逐题「不可」印
+ # 数字图集:规整 4×4 网格,逐格由同名 Sprite 定 rect(见 dan.js 的 NUM_FONTS)。
+ # 图集里全是**纯白剪影**(游戏运行时才染色),所以描边要另外提一层 _Outline。
+ "UI_CMN_Num_26p": "num-26p.png", # 小号数字 136×160,格 34×40
+ "UI_CMN_Num_26p_Outline": "num-26p-outline.png", # 同布局的描边剪影
+ "UI_CMN_Num_90p": "num-90p.png", # 没有成绩时的占位「—」用,356×420,格 89×105
+ "UI_CMN_Num_90p_Outline": "num-90p-outline.png",
+ # 分数数字:同布局的三套(blue / gold / red)296×392,格 74×98,自带颜色、
+ # 不用描边层 —— 原作就是按达成率高低换这一套的颜色。
+ # 同名的 `UI_NUM_Score_0001111_Base` 故意不搬:那是一张**空图集**(16 格里一个字形都没有,
+ # 整张只有几列杂散像素),照它渲染数字会得到一片看不见的灰影。
+ "UI_NUM_Score_0001111_Blue": "num-score-blue.png",
+ "UI_NUM_Score_0001111_Gold": "num-score-gold.png",
+ "UI_NUM_Score_0001111_Red": "num-score-red.png",
+ # 达成率末尾那个大「%」字形:同样按分数分色(没有 base 那版,低档用图集里的 % 格)
+ "UI_RSL_Score_Per_Gold": "score-per-gold.png", # 80×80
+ "UI_RSL_Score_Per_Blue": "score-per-blue.png",
+ "UI_RSL_Score_Per_Red": "score-per-red.png",
+ # 逐题评级徽章:`UI_GAM_Rank_*` 是**游戏内**那套,单级各一张(源图紧裁,36–110 × 42–44)。
+ # 故意不用 `UI_CMN_TabTitle_Rank_*`:那是**页签标题**上的区间图 —— AAA 那张画的是
+ # 「A~AAA」、BBB 是「~BBB」,而且它没有 D/C/B/BB/A/AA,低分全挤在同一张牌上。
+ "UI_GAM_Rank_D": "gam-rank-d.png",
+ "UI_GAM_Rank_C": "gam-rank-c.png",
+ "UI_GAM_Rank_B": "gam-rank-b.png",
+ "UI_GAM_Rank_BB": "gam-rank-bb.png",
+ "UI_GAM_Rank_BBB": "gam-rank-bbb.png",
+ "UI_GAM_Rank_A": "gam-rank-a.png",
+ "UI_GAM_Rank_AA": "gam-rank-aa.png",
+ "UI_GAM_Rank_AAA": "gam-rank-aaa.png",
+ "UI_GAM_Rank_S": "gam-rank-s.png",
+ "UI_GAM_Rank_Sp": "gam-rank-sp.png",
+ "UI_GAM_Rank_SS": "gam-rank-ss.png",
+ "UI_GAM_Rank_SSp": "gam-rank-ssp.png",
+ "UI_GAM_Rank_SSS": "gam-rank-sss.png",
+ "UI_GAM_Rank_SSSp": "gam-rank-sssp.png",
+ # 右下角那个组件:剩余生命底盘(绿 = 通关 / 红 = 未通关)。
+ # `UI_RSL_DXScore_Base`(「でらっくスコア」标签)**故意不搬**:官方那格是单曲按打击
+ # 精度算的 DX 分数,我们显示的是四道单题 rating 之和,口径不同,套那个框等于撒谎。
+ "UI_DNM_Base_Life_01": "life-base-green.png",
+ "UI_DNM_Base_Life_03": "life-base-red.png",
+}
+
+
+def main(argv: list[str]) -> int:
+ if len(argv) < 2:
+ print(__doc__)
+ return 2
+ game_dir = Path(argv[1])
+ out_dir = Path(argv[2]) if len(argv) > 2 else Path("public/assets/maimai")
+ assets = game_dir / "resources.assets"
+ if not assets.exists():
+ print(f"找不到 {assets}")
+ return 1
+
+ out_dir.mkdir(parents=True, exist_ok=True)
+ env = UnityPy.load(str(assets))
+ written: dict[str, str] = {}
+ for obj in env.objects:
+ if obj.type.name != "Texture2D":
+ continue
+ data = obj.read()
+ name = getattr(data, "m_Name", None) or getattr(data, "name", "") or ""
+ target = WANTED.get(name)
+ if target is None or target in written:
+ continue
+ image = data.image
+ image.save(out_dir / target)
+ written[target] = f"{name} ({image.width}x{image.height})"
+
+ for target in WANTED.values():
+ print(f" {written.get(target, '缺失!'):<34} → {target}")
+ return 0 if len(written) == len(WANTED) else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))
\ No newline at end of file