Playable frontend: FastAPI + canvas shop UI
Browse files- web/play/app.js +50 -20
- web/play/index.html +91 -53
- web/play/styles.css +646 -315
web/play/app.js
CHANGED
|
@@ -30,12 +30,29 @@ createApp({
|
|
| 30 |
const stockSlot = ref("");
|
| 31 |
const toasts = ref([]);
|
| 32 |
const floaters = ref([]);
|
|
|
|
|
|
|
|
|
|
| 33 |
let autoTimer = null;
|
| 34 |
let autoInFlight = false;
|
| 35 |
let toastSeq = 0;
|
| 36 |
let floatSeq = 0;
|
| 37 |
const imgCache = new Map();
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
const reversedLog = computed(() => [...(view.value?.log || [])].reverse());
|
| 40 |
const catalogOptions = computed(() => view.value?.catalog || []);
|
| 41 |
const stockOptions = computed(() => {
|
|
@@ -304,18 +321,21 @@ createApp({
|
|
| 304 |
|
| 305 |
function drawBubble(ctx, x, y, text) {
|
| 306 |
const t = String(text).slice(0, 18);
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
| 308 |
const tw = ctx.measureText(t).width;
|
| 309 |
const bw = tw + 16;
|
| 310 |
const bh = 24;
|
| 311 |
const bx = x - bw / 2;
|
| 312 |
const by = y - bh - 10;
|
| 313 |
-
ctx.fillStyle =
|
| 314 |
-
ctx.strokeStyle =
|
| 315 |
roundRect(ctx, bx, by, bw, bh, 10);
|
| 316 |
ctx.fill();
|
| 317 |
ctx.stroke();
|
| 318 |
-
ctx.fillStyle =
|
| 319 |
ctx.textAlign = "center";
|
| 320 |
ctx.fillText(t, x, by + 16);
|
| 321 |
ctx.textAlign = "left";
|
|
@@ -327,8 +347,17 @@ createApp({
|
|
| 327 |
const ctx = el.getContext("2d");
|
| 328 |
const w = el.width;
|
| 329 |
const h = el.height;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
ctx.clearRect(0, 0, w, h);
|
| 331 |
-
ctx.fillStyle =
|
| 332 |
ctx.fillRect(0, 0, w, h);
|
| 333 |
|
| 334 |
const v = view.value;
|
|
@@ -342,12 +371,10 @@ createApp({
|
|
| 342 |
const px = x * CELL;
|
| 343 |
const py = y * CELL;
|
| 344 |
const t = tags[`${x},${y}`] || [];
|
| 345 |
-
if (t.includes("wall") && t.includes("window")) ctx.fillStyle = "
|
| 346 |
-
else if (t.includes("wall")) ctx.fillStyle = "
|
| 347 |
-
else if (t.includes("is_entrance")) ctx.fillStyle = "
|
| 348 |
-
else
|
| 349 |
-
ctx.fillStyle =
|
| 350 |
-
(x + y) % 2 === 0 ? "rgba(255,255,255,0.28)" : "rgba(230,210,190,0.22)";
|
| 351 |
ctx.fillRect(px, py, CELL, CELL);
|
| 352 |
}
|
| 353 |
}
|
|
@@ -367,8 +394,8 @@ createApp({
|
|
| 367 |
roundRect(ctx, px + 6, py + 6, CELL - 12, CELL - 12, 6);
|
| 368 |
ctx.fill();
|
| 369 |
}
|
| 370 |
-
ctx.fillStyle =
|
| 371 |
-
ctx.font = "11px 'PingFang SC','Microsoft YaHei',sans-serif";
|
| 372 |
ctx.fillText(String(f.name || "").slice(0, 6), px + 4, py + CELL - 5);
|
| 373 |
}
|
| 374 |
|
|
@@ -396,14 +423,14 @@ createApp({
|
|
| 396 |
ctx.drawImage(im, px - 12, py + 2, 24, 24);
|
| 397 |
ctx.restore();
|
| 398 |
}
|
| 399 |
-
ctx.fillStyle =
|
| 400 |
-
ctx.font = "bold 11px sans-serif";
|
| 401 |
ctx.fillText("×" + d.qty, px + 12, py + 18);
|
| 402 |
}
|
| 403 |
|
| 404 |
for (const [name, xy] of Object.entries(v.grid.markers || {})) {
|
| 405 |
-
ctx.fillStyle =
|
| 406 |
-
ctx.font = "10px 'PingFang SC','Microsoft YaHei',sans-serif";
|
| 407 |
ctx.fillText(markersZh[name] || name, xy[0] * CELL + 3, xy[1] * CELL + 12);
|
| 408 |
}
|
| 409 |
|
|
@@ -434,11 +461,11 @@ createApp({
|
|
| 434 |
ctx.drawImage(portrait, px - 15, py - 15, 30, 30);
|
| 435 |
ctx.restore();
|
| 436 |
}
|
| 437 |
-
ctx.fillStyle =
|
| 438 |
-
ctx.font = "bold 12px 'PingFang SC','Microsoft YaHei',sans-serif";
|
| 439 |
ctx.textAlign = "center";
|
| 440 |
ctx.fillText(a.name || "", px, py + 32);
|
| 441 |
-
ctx.font = "11px 'PingFang SC','Microsoft YaHei',sans-serif";
|
| 442 |
ctx.fillStyle = col;
|
| 443 |
ctx.fillText(a.state_zh || "", px, py + 46);
|
| 444 |
ctx.textAlign = "left";
|
|
@@ -449,6 +476,7 @@ createApp({
|
|
| 449 |
watch(view, () => nextTick(draw), { deep: true });
|
| 450 |
|
| 451 |
onMounted(async () => {
|
|
|
|
| 452 |
await newGame();
|
| 453 |
setInterval(() => {
|
| 454 |
if (view.value) draw();
|
|
@@ -472,6 +500,8 @@ createApp({
|
|
| 472 |
bgStyle,
|
| 473 |
toasts,
|
| 474 |
floaters,
|
|
|
|
|
|
|
| 475 |
isHere,
|
| 476 |
newGame,
|
| 477 |
openShop,
|
|
|
|
| 30 |
const stockSlot = ref("");
|
| 31 |
const toasts = ref([]);
|
| 32 |
const floaters = ref([]);
|
| 33 |
+
const flavor = ref(
|
| 34 |
+
localStorage.getItem("town-shop-flavor") === "latte" ? "latte" : "mocha"
|
| 35 |
+
);
|
| 36 |
let autoTimer = null;
|
| 37 |
let autoInFlight = false;
|
| 38 |
let toastSeq = 0;
|
| 39 |
let floatSeq = 0;
|
| 40 |
const imgCache = new Map();
|
| 41 |
|
| 42 |
+
function setFlavor(name) {
|
| 43 |
+
flavor.value = name === "latte" ? "latte" : "mocha";
|
| 44 |
+
document.documentElement.setAttribute("data-flavor", flavor.value);
|
| 45 |
+
localStorage.setItem("town-shop-flavor", flavor.value);
|
| 46 |
+
const meta = document.querySelector('meta[name="theme-color"]');
|
| 47 |
+
if (meta) meta.setAttribute("content", flavor.value === "latte" ? "#eff1f5" : "#1e1e2e");
|
| 48 |
+
nextTick(draw);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function ctp(name, fallback) {
|
| 52 |
+
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
| 53 |
+
return v || fallback;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
const reversedLog = computed(() => [...(view.value?.log || [])].reverse());
|
| 57 |
const catalogOptions = computed(() => view.value?.catalog || []);
|
| 58 |
const stockOptions = computed(() => {
|
|
|
|
| 321 |
|
| 322 |
function drawBubble(ctx, x, y, text) {
|
| 323 |
const t = String(text).slice(0, 18);
|
| 324 |
+
const peach = ctp("--ctp-peach", "#fab387");
|
| 325 |
+
const base = ctp("--ctp-mantle", "#181825");
|
| 326 |
+
const ink = ctp("--ctp-text", "#cdd6f4");
|
| 327 |
+
ctx.font = "12px 'JetBrains Mono','PingFang SC','Microsoft YaHei',sans-serif";
|
| 328 |
const tw = ctx.measureText(t).width;
|
| 329 |
const bw = tw + 16;
|
| 330 |
const bh = 24;
|
| 331 |
const bx = x - bw / 2;
|
| 332 |
const by = y - bh - 10;
|
| 333 |
+
ctx.fillStyle = base;
|
| 334 |
+
ctx.strokeStyle = peach;
|
| 335 |
roundRect(ctx, bx, by, bw, bh, 10);
|
| 336 |
ctx.fill();
|
| 337 |
ctx.stroke();
|
| 338 |
+
ctx.fillStyle = ink;
|
| 339 |
ctx.textAlign = "center";
|
| 340 |
ctx.fillText(t, x, by + 16);
|
| 341 |
ctx.textAlign = "left";
|
|
|
|
| 347 |
const ctx = el.getContext("2d");
|
| 348 |
const w = el.width;
|
| 349 |
const h = el.height;
|
| 350 |
+
const base = ctp("--ctp-base", "#1e1e2e");
|
| 351 |
+
const surface0 = ctp("--ctp-surface0", "#313244");
|
| 352 |
+
const surface1 = ctp("--ctp-surface1", "#45475a");
|
| 353 |
+
const text = ctp("--ctp-text", "#cdd6f4");
|
| 354 |
+
const subtext = ctp("--ctp-subtext0", "#a6adc8");
|
| 355 |
+
const peach = ctp("--ctp-peach", "#fab387");
|
| 356 |
+
const sky = ctp("--ctp-sky", "#89dceb");
|
| 357 |
+
const crust = ctp("--ctp-crust", "#11111b");
|
| 358 |
+
|
| 359 |
ctx.clearRect(0, 0, w, h);
|
| 360 |
+
ctx.fillStyle = base;
|
| 361 |
ctx.fillRect(0, 0, w, h);
|
| 362 |
|
| 363 |
const v = view.value;
|
|
|
|
| 371 |
const px = x * CELL;
|
| 372 |
const py = y * CELL;
|
| 373 |
const t = tags[`${x},${y}`] || [];
|
| 374 |
+
if (t.includes("wall") && t.includes("window")) ctx.fillStyle = sky + "66";
|
| 375 |
+
else if (t.includes("wall")) ctx.fillStyle = surface1 + "cc";
|
| 376 |
+
else if (t.includes("is_entrance")) ctx.fillStyle = peach + "55";
|
| 377 |
+
else ctx.fillStyle = (x + y) % 2 === 0 ? surface0 + "aa" : surface1 + "66";
|
|
|
|
|
|
|
| 378 |
ctx.fillRect(px, py, CELL, CELL);
|
| 379 |
}
|
| 380 |
}
|
|
|
|
| 394 |
roundRect(ctx, px + 6, py + 6, CELL - 12, CELL - 12, 6);
|
| 395 |
ctx.fill();
|
| 396 |
}
|
| 397 |
+
ctx.fillStyle = text;
|
| 398 |
+
ctx.font = "11px 'JetBrains Mono','PingFang SC','Microsoft YaHei',sans-serif";
|
| 399 |
ctx.fillText(String(f.name || "").slice(0, 6), px + 4, py + CELL - 5);
|
| 400 |
}
|
| 401 |
|
|
|
|
| 423 |
ctx.drawImage(im, px - 12, py + 2, 24, 24);
|
| 424 |
ctx.restore();
|
| 425 |
}
|
| 426 |
+
ctx.fillStyle = text;
|
| 427 |
+
ctx.font = "bold 11px 'JetBrains Mono',sans-serif";
|
| 428 |
ctx.fillText("×" + d.qty, px + 12, py + 18);
|
| 429 |
}
|
| 430 |
|
| 431 |
for (const [name, xy] of Object.entries(v.grid.markers || {})) {
|
| 432 |
+
ctx.fillStyle = subtext;
|
| 433 |
+
ctx.font = "10px 'JetBrains Mono','PingFang SC','Microsoft YaHei',sans-serif";
|
| 434 |
ctx.fillText(markersZh[name] || name, xy[0] * CELL + 3, xy[1] * CELL + 12);
|
| 435 |
}
|
| 436 |
|
|
|
|
| 461 |
ctx.drawImage(portrait, px - 15, py - 15, 30, 30);
|
| 462 |
ctx.restore();
|
| 463 |
}
|
| 464 |
+
ctx.fillStyle = text;
|
| 465 |
+
ctx.font = "bold 12px 'JetBrains Mono','PingFang SC','Microsoft YaHei',sans-serif";
|
| 466 |
ctx.textAlign = "center";
|
| 467 |
ctx.fillText(a.name || "", px, py + 32);
|
| 468 |
+
ctx.font = "11px 'JetBrains Mono','PingFang SC','Microsoft YaHei',sans-serif";
|
| 469 |
ctx.fillStyle = col;
|
| 470 |
ctx.fillText(a.state_zh || "", px, py + 46);
|
| 471 |
ctx.textAlign = "left";
|
|
|
|
| 476 |
watch(view, () => nextTick(draw), { deep: true });
|
| 477 |
|
| 478 |
onMounted(async () => {
|
| 479 |
+
setFlavor(flavor.value);
|
| 480 |
await newGame();
|
| 481 |
setInterval(() => {
|
| 482 |
if (view.value) draw();
|
|
|
|
| 500 |
bgStyle,
|
| 501 |
toasts,
|
| 502 |
floaters,
|
| 503 |
+
flavor,
|
| 504 |
+
setFlavor,
|
| 505 |
isHere,
|
| 506 |
newGame,
|
| 507 |
openShop,
|
web/play/index.html
CHANGED
|
@@ -1,56 +1,76 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html lang="zh-CN">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
|
|
|
|
| 6 |
<title>小镇角落小店</title>
|
|
|
|
|
|
|
|
|
|
| 7 |
<link rel="stylesheet" href="/play/styles.css" />
|
| 8 |
<script src="https://unpkg.com/vue@3.5.13/dist/vue.global.prod.js"></script>
|
| 9 |
</head>
|
| 10 |
<body>
|
| 11 |
<div id="app" v-cloak>
|
| 12 |
-
<
|
|
|
|
|
|
|
| 13 |
<div class="brand">
|
| 14 |
-
<div class="logo">🌷</div>
|
| 15 |
<div>
|
| 16 |
<h1>小镇角落小店</h1>
|
| 17 |
<p class="guide">{{ view?.guide || '正在布置小店…' }}</p>
|
| 18 |
</div>
|
| 19 |
</div>
|
| 20 |
-
|
|
|
|
| 21 |
<span
|
| 22 |
v-for="p in view.phases"
|
| 23 |
:key="p.id"
|
| 24 |
class="phase-pill"
|
| 25 |
:class="{ on: p.active }"
|
|
|
|
| 26 |
>{{ p.label }}</span>
|
| 27 |
-
</
|
|
|
|
| 28 |
<div class="top-meta" v-if="view">
|
| 29 |
<strong>{{ view.meta.day_label }}</strong>
|
| 30 |
<span>{{ view.meta.phase_zh }}</span>
|
| 31 |
-
<span class="money-pill">¥ {{ view.wallet }}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
</div>
|
| 33 |
</header>
|
| 34 |
|
| 35 |
-
<main class="shell" v-if="view">
|
| 36 |
-
<section class="stage-card" :class="'light-' + view.atmosphere.light">
|
| 37 |
-
<div class="stage-bg" :style="bgStyle"></div>
|
| 38 |
-
<canvas ref="canvas" width="576" height="480"></canvas>
|
| 39 |
|
| 40 |
-
<
|
| 41 |
-
<div class="floaters">
|
| 42 |
<div v-for="f in floaters" :key="f.id" class="floater" :class="f.kind">{{ f.text }}</div>
|
| 43 |
</div>
|
| 44 |
|
| 45 |
<div class="mood-bar">
|
| 46 |
-
<span>💭</span>
|
| 47 |
<span>{{ view.atmosphere.mood_line || '店里还很安静。' }}</span>
|
| 48 |
<span class="live" v-if="view.agents.length">店内 {{ view.agents.length }} 人</span>
|
| 49 |
<span class="live dim" v-else-if="view.ui.can_tick">等待客人中…</span>
|
| 50 |
</div>
|
| 51 |
|
| 52 |
-
<
|
| 53 |
-
<div class="guest-rail">
|
| 54 |
<div class="rail-title">今日来客</div>
|
| 55 |
<div class="rail-body" v-if="(view.guests_today || []).length">
|
| 56 |
<div
|
|
@@ -59,8 +79,8 @@
|
|
| 59 |
:key="g.agent_id"
|
| 60 |
:class="{ left: g.left, here: isHere(g.agent_id) }"
|
| 61 |
>
|
| 62 |
-
<img v-if="g.portrait" :src="'/' + g.portrait" :alt="g.name" />
|
| 63 |
-
<div class="avatar-fallback" v-else>{{ (g.name || '?')[0] }}</div>
|
| 64 |
<div>
|
| 65 |
<b>{{ g.name }}</b>
|
| 66 |
<small>{{ g.kind_zh }} · {{ g.left ? '已离开' : g.state_zh }}</small>
|
|
@@ -73,16 +93,15 @@
|
|
| 73 |
</div>
|
| 74 |
</section>
|
| 75 |
|
| 76 |
-
<aside class="hud">
|
| 77 |
-
<!-- 主操作:按阶段只突出一个 -->
|
| 78 |
<div class="card hero-actions">
|
| 79 |
<template v-if="view.meta.phase === 'PREP'">
|
| 80 |
<h2>营业前 · 开张准备</h2>
|
| 81 |
<p class="hint ok">现在可以进货、上架、调灯光。</p>
|
| 82 |
-
<button class="primary big" @click="openShop" :disabled="busy">
|
| 83 |
<div class="btn-grid">
|
| 84 |
-
<button @click="quickPrep" :disabled="busy">一键备货</button>
|
| 85 |
-
<button class="ghost" @click="newGame" :disabled="busy">新开一局</button>
|
| 86 |
</div>
|
| 87 |
</template>
|
| 88 |
|
|
@@ -93,8 +112,8 @@
|
|
| 93 |
<div><em>成交</em><b>{{ view.stats.sales }}</b></div>
|
| 94 |
<div><em>营收</em><b>¥{{ view.stats.revenue }}</b></div>
|
| 95 |
</div>
|
| 96 |
-
<button class="primary big" @click="sleep" :disabled="busy">
|
| 97 |
-
<button class="ghost" @click="newGame" :disabled="busy">新开一局</button>
|
| 98 |
</template>
|
| 99 |
|
| 100 |
<template v-else>
|
|
@@ -106,7 +125,7 @@
|
|
| 106 |
</div>
|
| 107 |
<div class="btn-grid">
|
| 108 |
<button type="button" class="stable" @click="togglePause">
|
| 109 |
-
{{ view.meta.paused ? '
|
| 110 |
</button>
|
| 111 |
<button type="button" class="stable" @click="tickOnce" :disabled="view.meta.paused">
|
| 112 |
推进一步
|
|
@@ -115,8 +134,8 @@
|
|
| 115 |
<button type="button" class="ghost stable" @click="newGame">新开一局</button>
|
| 116 |
</div>
|
| 117 |
<div class="speed-row">
|
| 118 |
-
<
|
| 119 |
-
<select v-model.number="speed" @change="setupAuto">
|
| 120 |
<option :value="0">关闭</option>
|
| 121 |
<option :value="350">正常</option>
|
| 122 |
<option :value="180">两倍速</option>
|
|
@@ -131,31 +150,31 @@
|
|
| 131 |
<h2>进货与上架</h2>
|
| 132 |
<p class="hint" :class="{ ok: view.ui.can_restock }">{{ view.ui.restock_hint }}</p>
|
| 133 |
<div class="form-row">
|
| 134 |
-
<select v-model="restockId" :disabled="!view.ui.can_restock">
|
| 135 |
<option v-for="p in catalogOptions" :key="'r'+p.product_id" :value="p.product_id">
|
| 136 |
{{ p.name }}(¥{{ p.price }})
|
| 137 |
</option>
|
| 138 |
</select>
|
| 139 |
-
<button @click="restock" :disabled="busy || !view.ui.can_restock">进货 +1</button>
|
| 140 |
</div>
|
| 141 |
<p class="hint" :class="{ ok: view.ui.can_stock_display }">{{ view.ui.stock_hint }}</p>
|
| 142 |
<div class="form-row triple" v-if="view.ui.can_stock_display">
|
| 143 |
-
<select v-model="stockPid">
|
| 144 |
<option v-for="p in stockOptions" :key="'s'+p.product_id" :value="p.product_id">
|
| 145 |
{{ p.name }} ×{{ p.qty ?? '—' }}
|
| 146 |
</option>
|
| 147 |
</select>
|
| 148 |
-
<select v-model="stockSlot">
|
| 149 |
<option v-for="d in view.displays" :key="d.slot" :value="d.slot">
|
| 150 |
{{ d.name }} / {{ d.slot }}
|
| 151 |
</option>
|
| 152 |
</select>
|
| 153 |
-
<button @click="stock" :disabled="busy">上架</button>
|
| 154 |
</div>
|
| 155 |
-
<div class="btn-grid three light-row">
|
| 156 |
-
<button :class="{ active: view.atmosphere.light==='warm' }" @click="setLight('warm')">暖光</button>
|
| 157 |
-
<button :class="{ active: view.atmosphere.light==='cool' }" @click="setLight('cool')">冷光</button>
|
| 158 |
-
<button :class="{ active: view.atmosphere.light==='off' }" @click="setLight('off')">关灯</button>
|
| 159 |
</div>
|
| 160 |
</div>
|
| 161 |
|
|
@@ -163,31 +182,32 @@
|
|
| 163 |
<h2>氛围</h2>
|
| 164 |
<div class="atm" v-for="d in view.atmosphere.dims" :key="d.key">
|
| 165 |
<span>{{ d.label }}</span>
|
| 166 |
-
<div class="bar"
|
|
|
|
|
|
|
| 167 |
<span class="pct">{{ d.pct }}</span>
|
| 168 |
</div>
|
| 169 |
</div>
|
| 170 |
|
| 171 |
<div class="card">
|
| 172 |
<h2>店内见闻</h2>
|
| 173 |
-
<ul class="log">
|
| 174 |
<li v-for="(line, i) in reversedLog" :key="i">{{ line }}</li>
|
| 175 |
</ul>
|
| 176 |
</div>
|
| 177 |
</aside>
|
| 178 |
</main>
|
| 179 |
|
| 180 |
-
<div class="loading" v-else>
|
| 181 |
-
<div class="spinner"></div>
|
| 182 |
<p>{{ error || '正在布置小店…' }}</p>
|
| 183 |
-
<button v-if="error" class="primary" @click="newGame">重试</button>
|
| 184 |
</div>
|
| 185 |
|
| 186 |
-
<
|
| 187 |
-
<div class="modal" v-if="showNight && view?.night" @click.self="showNight=false">
|
| 188 |
<div class="modal-card">
|
| 189 |
<div class="modal-kicker">夜记</div>
|
| 190 |
-
<h2>{{ view.meta.day_label }} · 打烊了</h2>
|
| 191 |
<p class="lead">{{ view.night.mood }}</p>
|
| 192 |
<p class="diary">「{{ view.night.diary }}」</p>
|
| 193 |
<div class="night-stats">
|
|
@@ -196,31 +216,49 @@
|
|
| 196 |
<div><em>营收</em><b>¥{{ view.night.revenue }}</b></div>
|
| 197 |
</div>
|
| 198 |
<p class="vibe" v-if="view.night.top_dims?.length">今日气息:{{ view.night.top_dims.join(' · ') }}</p>
|
| 199 |
-
<button class="primary wide" @click="showNight=false">收下这份夜晚</button>
|
| 200 |
</div>
|
| 201 |
</div>
|
| 202 |
|
| 203 |
-
<
|
| 204 |
-
<div class="modal" v-if="showHelp" @click.self="showHelp=false">
|
| 205 |
<div class="modal-card">
|
| 206 |
-
<div class="modal-kicker">怎么玩</div>
|
| 207 |
-
<h2>三步经营你的花店</h2>
|
| 208 |
<ol class="help-list">
|
| 209 |
<li><b>营业前</b>:进货、上架,然后点「开始营业」</li>
|
| 210 |
<li><b>营业中</b>:自动流逝,看客人进店、闲逛、结账</li>
|
| 211 |
<li><b>夜晚</b>:读夜记,点「入睡」进入下一天</li>
|
| 212 |
</ol>
|
| 213 |
-
<p class="hint ok">
|
| 214 |
-
<button class="primary wide" @click="showHelp=false">我知道了,开始</button>
|
| 215 |
</div>
|
| 216 |
</div>
|
| 217 |
|
| 218 |
-
<div class="toast-stack">
|
| 219 |
<div v-for="t in toasts" :key="t.id" class="toast" :class="t.kind">{{ t.text }}</div>
|
| 220 |
</div>
|
| 221 |
|
| 222 |
-
<footer class="foot">
|
|
|
|
|
|
|
|
|
|
| 223 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
<script src="/play/app.js"></script>
|
| 225 |
</body>
|
| 226 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="zh-CN" data-flavor="mocha">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<meta name="color-scheme" content="dark light" />
|
| 7 |
+
<meta name="theme-color" content="#1e1e2e" />
|
| 8 |
<title>小镇角落小店</title>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet" />
|
| 12 |
<link rel="stylesheet" href="/play/styles.css" />
|
| 13 |
<script src="https://unpkg.com/vue@3.5.13/dist/vue.global.prod.js"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
<div id="app" v-cloak>
|
| 17 |
+
<a class="skip-link" href="#main">跳到主内容</a>
|
| 18 |
+
|
| 19 |
+
<header class="topbar" role="banner">
|
| 20 |
<div class="brand">
|
| 21 |
+
<div class="logo" aria-hidden="true">🌷</div>
|
| 22 |
<div>
|
| 23 |
<h1>小镇角落小店</h1>
|
| 24 |
<p class="guide">{{ view?.guide || '正在布置小店…' }}</p>
|
| 25 |
</div>
|
| 26 |
</div>
|
| 27 |
+
|
| 28 |
+
<nav class="phase-track" v-if="view" aria-label="营业时辰">
|
| 29 |
<span
|
| 30 |
v-for="p in view.phases"
|
| 31 |
:key="p.id"
|
| 32 |
class="phase-pill"
|
| 33 |
:class="{ on: p.active }"
|
| 34 |
+
:aria-current="p.active ? 'step' : null"
|
| 35 |
>{{ p.label }}</span>
|
| 36 |
+
</nav>
|
| 37 |
+
|
| 38 |
<div class="top-meta" v-if="view">
|
| 39 |
<strong>{{ view.meta.day_label }}</strong>
|
| 40 |
<span>{{ view.meta.phase_zh }}</span>
|
| 41 |
+
<span class="money-pill" aria-label="店内现金">¥ {{ view.wallet }}</span>
|
| 42 |
+
<div class="flavor-switch" role="group" aria-label="Catppuccin 主题">
|
| 43 |
+
<button
|
| 44 |
+
type="button"
|
| 45 |
+
:aria-pressed="flavor === 'mocha'"
|
| 46 |
+
@click="setFlavor('mocha')"
|
| 47 |
+
>Mocha</button>
|
| 48 |
+
<button
|
| 49 |
+
type="button"
|
| 50 |
+
:aria-pressed="flavor === 'latte'"
|
| 51 |
+
@click="setFlavor('latte')"
|
| 52 |
+
>Latte</button>
|
| 53 |
+
</div>
|
| 54 |
</div>
|
| 55 |
</header>
|
| 56 |
|
| 57 |
+
<main id="main" class="shell" v-if="view">
|
| 58 |
+
<section class="stage-card" :class="'light-' + view.atmosphere.light" aria-label="店铺舞台">
|
| 59 |
+
<div class="stage-bg" :style="bgStyle" aria-hidden="true"></div>
|
| 60 |
+
<canvas ref="canvas" width="576" height="480" role="img" aria-label="店铺俯视图"></canvas>
|
| 61 |
|
| 62 |
+
<div class="floaters" aria-live="polite">
|
|
|
|
| 63 |
<div v-for="f in floaters" :key="f.id" class="floater" :class="f.kind">{{ f.text }}</div>
|
| 64 |
</div>
|
| 65 |
|
| 66 |
<div class="mood-bar">
|
| 67 |
+
<span aria-hidden="true">💭</span>
|
| 68 |
<span>{{ view.atmosphere.mood_line || '店里还很安静。' }}</span>
|
| 69 |
<span class="live" v-if="view.agents.length">店内 {{ view.agents.length }} 人</span>
|
| 70 |
<span class="live dim" v-else-if="view.ui.can_tick">等待客人中…</span>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
+
<div class="guest-rail" aria-label="今日来客">
|
|
|
|
| 74 |
<div class="rail-title">今日来客</div>
|
| 75 |
<div class="rail-body" v-if="(view.guests_today || []).length">
|
| 76 |
<div
|
|
|
|
| 79 |
:key="g.agent_id"
|
| 80 |
:class="{ left: g.left, here: isHere(g.agent_id) }"
|
| 81 |
>
|
| 82 |
+
<img v-if="g.portrait" :src="'/' + g.portrait" :alt="g.name + ' 肖像'" />
|
| 83 |
+
<div class="avatar-fallback" v-else aria-hidden="true">{{ (g.name || '?')[0] }}</div>
|
| 84 |
<div>
|
| 85 |
<b>{{ g.name }}</b>
|
| 86 |
<small>{{ g.kind_zh }} · {{ g.left ? '已离开' : g.state_zh }}</small>
|
|
|
|
| 93 |
</div>
|
| 94 |
</section>
|
| 95 |
|
| 96 |
+
<aside class="hud" aria-label="经营面板">
|
|
|
|
| 97 |
<div class="card hero-actions">
|
| 98 |
<template v-if="view.meta.phase === 'PREP'">
|
| 99 |
<h2>营业前 · 开张准备</h2>
|
| 100 |
<p class="hint ok">现在可以进货、上架、调灯光。</p>
|
| 101 |
+
<button class="primary big" @click="openShop" :disabled="busy">开始营业</button>
|
| 102 |
<div class="btn-grid">
|
| 103 |
+
<button type="button" @click="quickPrep" :disabled="busy">一键备货</button>
|
| 104 |
+
<button type="button" class="ghost" @click="newGame" :disabled="busy">新开一局</button>
|
| 105 |
</div>
|
| 106 |
</template>
|
| 107 |
|
|
|
|
| 112 |
<div><em>成交</em><b>{{ view.stats.sales }}</b></div>
|
| 113 |
<div><em>营收</em><b>¥{{ view.stats.revenue }}</b></div>
|
| 114 |
</div>
|
| 115 |
+
<button class="primary big" @click="sleep" :disabled="busy">入睡 · 下一天</button>
|
| 116 |
+
<button type="button" class="ghost" @click="newGame" :disabled="busy">新开一局</button>
|
| 117 |
</template>
|
| 118 |
|
| 119 |
<template v-else>
|
|
|
|
| 125 |
</div>
|
| 126 |
<div class="btn-grid">
|
| 127 |
<button type="button" class="stable" @click="togglePause">
|
| 128 |
+
{{ view.meta.paused ? '继续' : '暂停' }}
|
| 129 |
</button>
|
| 130 |
<button type="button" class="stable" @click="tickOnce" :disabled="view.meta.paused">
|
| 131 |
推进一步
|
|
|
|
| 134 |
<button type="button" class="ghost stable" @click="newGame">新开一局</button>
|
| 135 |
</div>
|
| 136 |
<div class="speed-row">
|
| 137 |
+
<label for="speed">自动流逝</label>
|
| 138 |
+
<select id="speed" v-model.number="speed" @change="setupAuto">
|
| 139 |
<option :value="0">关闭</option>
|
| 140 |
<option :value="350">正常</option>
|
| 141 |
<option :value="180">两倍速</option>
|
|
|
|
| 150 |
<h2>进货与上架</h2>
|
| 151 |
<p class="hint" :class="{ ok: view.ui.can_restock }">{{ view.ui.restock_hint }}</p>
|
| 152 |
<div class="form-row">
|
| 153 |
+
<select v-model="restockId" :disabled="!view.ui.can_restock" aria-label="进货商品">
|
| 154 |
<option v-for="p in catalogOptions" :key="'r'+p.product_id" :value="p.product_id">
|
| 155 |
{{ p.name }}(¥{{ p.price }})
|
| 156 |
</option>
|
| 157 |
</select>
|
| 158 |
+
<button type="button" @click="restock" :disabled="busy || !view.ui.can_restock">进货 +1</button>
|
| 159 |
</div>
|
| 160 |
<p class="hint" :class="{ ok: view.ui.can_stock_display }">{{ view.ui.stock_hint }}</p>
|
| 161 |
<div class="form-row triple" v-if="view.ui.can_stock_display">
|
| 162 |
+
<select v-model="stockPid" aria-label="上架商品">
|
| 163 |
<option v-for="p in stockOptions" :key="'s'+p.product_id" :value="p.product_id">
|
| 164 |
{{ p.name }} ×{{ p.qty ?? '—' }}
|
| 165 |
</option>
|
| 166 |
</select>
|
| 167 |
+
<select v-model="stockSlot" aria-label="货架位">
|
| 168 |
<option v-for="d in view.displays" :key="d.slot" :value="d.slot">
|
| 169 |
{{ d.name }} / {{ d.slot }}
|
| 170 |
</option>
|
| 171 |
</select>
|
| 172 |
+
<button type="button" @click="stock" :disabled="busy">上架</button>
|
| 173 |
</div>
|
| 174 |
+
<div class="btn-grid three light-row" role="group" aria-label="灯光">
|
| 175 |
+
<button type="button" :class="{ active: view.atmosphere.light==='warm' }" @click="setLight('warm')">暖光</button>
|
| 176 |
+
<button type="button" :class="{ active: view.atmosphere.light==='cool' }" @click="setLight('cool')">冷光</button>
|
| 177 |
+
<button type="button" :class="{ active: view.atmosphere.light==='off' }" @click="setLight('off')">关灯</button>
|
| 178 |
</div>
|
| 179 |
</div>
|
| 180 |
|
|
|
|
| 182 |
<h2>氛围</h2>
|
| 183 |
<div class="atm" v-for="d in view.atmosphere.dims" :key="d.key">
|
| 184 |
<span>{{ d.label }}</span>
|
| 185 |
+
<div class="bar" role="progressbar" :aria-valuenow="d.pct" aria-valuemin="0" aria-valuemax="100" :aria-label="d.label">
|
| 186 |
+
<i :style="{ width: d.pct + '%' }"></i>
|
| 187 |
+
</div>
|
| 188 |
<span class="pct">{{ d.pct }}</span>
|
| 189 |
</div>
|
| 190 |
</div>
|
| 191 |
|
| 192 |
<div class="card">
|
| 193 |
<h2>店内见闻</h2>
|
| 194 |
+
<ul class="log" aria-live="polite">
|
| 195 |
<li v-for="(line, i) in reversedLog" :key="i">{{ line }}</li>
|
| 196 |
</ul>
|
| 197 |
</div>
|
| 198 |
</aside>
|
| 199 |
</main>
|
| 200 |
|
| 201 |
+
<div class="loading" v-else role="status">
|
| 202 |
+
<div class="spinner" aria-hidden="true"></div>
|
| 203 |
<p>{{ error || '正在布置小店…' }}</p>
|
| 204 |
+
<button v-if="error" type="button" class="primary" @click="newGame">重试</button>
|
| 205 |
</div>
|
| 206 |
|
| 207 |
+
<div class="modal" v-if="showNight && view?.night" @click.self="showNight=false" role="dialog" aria-modal="true" aria-labelledby="night-title">
|
|
|
|
| 208 |
<div class="modal-card">
|
| 209 |
<div class="modal-kicker">夜记</div>
|
| 210 |
+
<h2 id="night-title">{{ view.meta.day_label }} · 打烊了</h2>
|
| 211 |
<p class="lead">{{ view.night.mood }}</p>
|
| 212 |
<p class="diary">「{{ view.night.diary }}」</p>
|
| 213 |
<div class="night-stats">
|
|
|
|
| 216 |
<div><em>营收</em><b>¥{{ view.night.revenue }}</b></div>
|
| 217 |
</div>
|
| 218 |
<p class="vibe" v-if="view.night.top_dims?.length">今日气息:{{ view.night.top_dims.join(' · ') }}</p>
|
| 219 |
+
<button type="button" class="primary wide" @click="showNight=false">收下这份夜晚</button>
|
| 220 |
</div>
|
| 221 |
</div>
|
| 222 |
|
| 223 |
+
<div class="modal" v-if="showHelp" @click.self="showHelp=false" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
|
|
| 224 |
<div class="modal-card">
|
| 225 |
+
<div class="modal-kicker">怎么玩 · Catppuccin</div>
|
| 226 |
+
<h2 id="help-title">三步经营你的花店</h2>
|
| 227 |
<ol class="help-list">
|
| 228 |
<li><b>营业前</b>:进货、上架,然后点「开始营业」</li>
|
| 229 |
<li><b>营业中</b>:自动流逝,看客人进店、闲逛、结账</li>
|
| 230 |
<li><b>夜晚</b>:读夜记,点「入睡」进入下一天</li>
|
| 231 |
</ol>
|
| 232 |
+
<p class="hint ok">界面使用 Catppuccin 配色;右上角可在 Mocha / Latte 间切换。</p>
|
| 233 |
+
<button type="button" class="primary wide" @click="showHelp=false">我知道了,开始</button>
|
| 234 |
</div>
|
| 235 |
</div>
|
| 236 |
|
| 237 |
+
<div class="toast-stack" aria-live="polite" aria-relevant="additions">
|
| 238 |
<div v-for="t in toasts" :key="t.id" class="toast" :class="t.kind">{{ t.text }}</div>
|
| 239 |
</div>
|
| 240 |
|
| 241 |
+
<footer class="foot">
|
| 242 |
+
毕业设计演示 · Vue 3 ·
|
| 243 |
+
<span>Catppuccin {{ flavor === 'mocha' ? 'Mocha' : 'Latte' }}</span>
|
| 244 |
+
</footer>
|
| 245 |
</div>
|
| 246 |
+
<style>
|
| 247 |
+
.skip-link {
|
| 248 |
+
position: absolute;
|
| 249 |
+
left: -9999px;
|
| 250 |
+
top: 0;
|
| 251 |
+
z-index: 100;
|
| 252 |
+
padding: 0.5rem 0.75rem;
|
| 253 |
+
background: var(--ctp-mauve, #cba6f7);
|
| 254 |
+
color: var(--ctp-crust, #11111b);
|
| 255 |
+
border-radius: 0 0 8px 0;
|
| 256 |
+
font-weight: 700;
|
| 257 |
+
}
|
| 258 |
+
.skip-link:focus {
|
| 259 |
+
left: 0;
|
| 260 |
+
}
|
| 261 |
+
</style>
|
| 262 |
<script src="/play/app.js"></script>
|
| 263 |
</body>
|
| 264 |
</html>
|
web/play/styles.css
CHANGED
|
@@ -1,290 +1,484 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
--
|
| 11 |
-
--
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"Noto Sans SC", system-ui, sans-serif;
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
html, body {
|
| 18 |
margin: 0;
|
| 19 |
min-height: 100%;
|
| 20 |
-
color: var(--
|
|
|
|
|
|
|
|
|
|
| 21 |
background:
|
| 22 |
-
radial-gradient(
|
| 23 |
-
radial-gradient(
|
|
|
|
| 24 |
var(--bg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
-
[v-cloak] { display: none; }
|
| 27 |
|
| 28 |
#app {
|
| 29 |
-
max-width:
|
| 30 |
margin: 0 auto;
|
| 31 |
-
padding:
|
| 32 |
}
|
| 33 |
|
|
|
|
| 34 |
.topbar {
|
| 35 |
-
display:
|
| 36 |
-
|
| 37 |
-
gap:
|
| 38 |
align-items: center;
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
|
|
|
| 42 |
.brand {
|
| 43 |
display: flex;
|
| 44 |
-
gap:
|
| 45 |
align-items: center;
|
|
|
|
| 46 |
}
|
|
|
|
| 47 |
.logo {
|
| 48 |
width: 48px;
|
| 49 |
height: 48px;
|
| 50 |
-
|
| 51 |
-
|
|
|
|
| 52 |
display: grid;
|
| 53 |
place-items: center;
|
| 54 |
-
font-size: 1.
|
| 55 |
-
box-shadow: var(--shadow);
|
| 56 |
}
|
|
|
|
| 57 |
.brand h1 {
|
| 58 |
margin: 0;
|
| 59 |
-
font-size: 1.
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
|
|
|
|
|
|
| 62 |
.brand p {
|
| 63 |
-
margin:
|
| 64 |
-
color: var(--muted);
|
| 65 |
-
font-size: .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
.phase-track {
|
| 69 |
display: flex;
|
| 70 |
flex-wrap: wrap;
|
| 71 |
-
gap:
|
|
|
|
| 72 |
}
|
|
|
|
| 73 |
.phase-pill {
|
| 74 |
-
font-size: .
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
| 80 |
}
|
|
|
|
| 81 |
.phase-pill.on {
|
| 82 |
-
color:
|
| 83 |
border-color: transparent;
|
| 84 |
-
background: linear-gradient(135deg,
|
| 85 |
-
box-shadow: 0 4px
|
| 86 |
}
|
|
|
|
| 87 |
.top-meta {
|
| 88 |
display: flex;
|
| 89 |
flex-wrap: wrap;
|
| 90 |
-
gap: .
|
| 91 |
align-items: center;
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
-
|
| 96 |
-
.
|
| 97 |
-
background:
|
| 98 |
-
color:
|
| 99 |
-
border-radius: 999px;
|
| 100 |
-
padding: .15rem .55rem;
|
| 101 |
-
font-size: .78rem;
|
| 102 |
}
|
| 103 |
|
|
|
|
| 104 |
.shell {
|
| 105 |
display: grid;
|
| 106 |
grid-template-columns: minmax(0, 1fr) 340px;
|
| 107 |
-
gap:
|
| 108 |
align-items: start;
|
| 109 |
}
|
| 110 |
|
|
|
|
| 111 |
.stage-card {
|
| 112 |
position: relative;
|
| 113 |
-
background: var(--
|
| 114 |
-
border: 1px solid var(--
|
| 115 |
-
border-radius: var(--radius);
|
| 116 |
overflow: hidden;
|
| 117 |
box-shadow: var(--shadow);
|
| 118 |
}
|
|
|
|
| 119 |
.stage-bg {
|
| 120 |
position: absolute;
|
| 121 |
inset: 0;
|
| 122 |
background-size: cover;
|
| 123 |
background-position: center;
|
| 124 |
-
opacity: .
|
| 125 |
pointer-events: none;
|
|
|
|
| 126 |
}
|
| 127 |
-
|
| 128 |
-
.stage-card.light-
|
| 129 |
-
.stage-card.light-
|
|
|
|
|
|
|
| 130 |
canvas {
|
| 131 |
position: relative;
|
| 132 |
display: block;
|
| 133 |
width: 100%;
|
| 134 |
height: auto;
|
|
|
|
| 135 |
}
|
|
|
|
| 136 |
.mood-bar {
|
| 137 |
position: relative;
|
| 138 |
display: flex;
|
| 139 |
-
gap:
|
| 140 |
-
align-items: center;
|
| 141 |
-
padding: .7rem 1rem;
|
| 142 |
-
border-top: 1px solid var(--line);
|
| 143 |
-
background: rgba(255, 253, 249, .92);
|
| 144 |
-
color: var(--muted);
|
| 145 |
-
}
|
| 146 |
-
.agent-strip {
|
| 147 |
-
display: flex;
|
| 148 |
-
gap: .55rem;
|
| 149 |
-
overflow-x: auto;
|
| 150 |
-
padding: .65rem .85rem .85rem;
|
| 151 |
-
border-top: 1px solid var(--line);
|
| 152 |
-
background: #fbf6f0;
|
| 153 |
-
}
|
| 154 |
-
.agent-strip.empty {
|
| 155 |
-
color: var(--muted);
|
| 156 |
-
font-size: .9rem;
|
| 157 |
-
}
|
| 158 |
-
.agent-chip {
|
| 159 |
-
display: flex;
|
| 160 |
-
gap: .45rem;
|
| 161 |
align-items: center;
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
}
|
| 168 |
-
.agent-chip img {
|
| 169 |
-
width: 36px;
|
| 170 |
-
height: 36px;
|
| 171 |
-
border-radius: 50%;
|
| 172 |
-
object-fit: cover;
|
| 173 |
-
}
|
| 174 |
-
.agent-chip b { display: block; font-size: .88rem; }
|
| 175 |
-
.agent-chip small { color: var(--muted); font-size: .74rem; }
|
| 176 |
|
| 177 |
-
.hud { display: flex; flex-direction: column; gap: .75rem; }
|
| 178 |
-
.card {
|
| 179 |
-
background: var(--card);
|
| 180 |
-
border: 1px solid var(--line);
|
| 181 |
-
border-radius: 16px;
|
| 182 |
-
padding: .9rem 1rem;
|
| 183 |
-
box-shadow: var(--shadow);
|
| 184 |
-
}
|
| 185 |
-
.card h2 {
|
| 186 |
-
margin: 0 0 .65rem;
|
| 187 |
-
font-size: .82rem;
|
| 188 |
-
letter-spacing: .08em;
|
| 189 |
-
color: var(--muted);
|
| 190 |
-
font-weight: 700;
|
| 191 |
-
}
|
| 192 |
-
.money-pill {
|
| 193 |
-
background: #f3e2d2;
|
| 194 |
-
color: var(--accent2);
|
| 195 |
-
font-weight: 700;
|
| 196 |
-
padding: .2rem .55rem;
|
| 197 |
-
border-radius: 999px;
|
| 198 |
-
}
|
| 199 |
-
.guide { max-width: 28rem; }
|
| 200 |
-
button.big {
|
| 201 |
-
width: 100%;
|
| 202 |
-
padding: .85rem 1rem;
|
| 203 |
-
font-size: 1.05rem;
|
| 204 |
-
margin: .35rem 0 .55rem;
|
| 205 |
-
}
|
| 206 |
-
.hero-actions .hint { margin-bottom: .55rem; }
|
| 207 |
-
.form-row.triple {
|
| 208 |
-
grid-template-columns: 1fr 1fr auto;
|
| 209 |
-
}
|
| 210 |
-
.light-row { margin-top: .55rem; }
|
| 211 |
.live {
|
| 212 |
margin-left: auto;
|
| 213 |
-
font-size: .
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
| 216 |
}
|
| 217 |
-
.live.dim { color: var(--muted); font-weight: 500; }
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
.guest-rail {
|
| 220 |
-
border-top: 1px solid var(--
|
| 221 |
-
background:
|
| 222 |
-
padding: .
|
| 223 |
}
|
|
|
|
| 224 |
.rail-title {
|
| 225 |
-
font-size: .
|
| 226 |
-
letter-spacing: .
|
| 227 |
-
|
|
|
|
| 228 |
font-weight: 700;
|
| 229 |
-
margin-bottom: .
|
| 230 |
}
|
|
|
|
| 231 |
.rail-body {
|
| 232 |
display: flex;
|
| 233 |
-
gap: .5rem;
|
| 234 |
overflow-x: auto;
|
| 235 |
-
padding-bottom:
|
|
|
|
|
|
|
| 236 |
}
|
|
|
|
| 237 |
.guest-card {
|
| 238 |
display: flex;
|
| 239 |
-
gap: .
|
| 240 |
align-items: center;
|
| 241 |
-
min-width:
|
| 242 |
-
background:
|
| 243 |
-
border: 1px solid var(--
|
| 244 |
-
border-radius:
|
| 245 |
-
padding: .4rem .
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
.guest-card
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
width: 36px;
|
| 251 |
height: 36px;
|
| 252 |
border-radius: 50%;
|
| 253 |
object-fit: cover;
|
|
|
|
| 254 |
}
|
|
|
|
| 255 |
.avatar-fallback {
|
| 256 |
display: grid;
|
| 257 |
place-items: center;
|
| 258 |
-
background:
|
|
|
|
| 259 |
font-weight: 700;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
}
|
| 261 |
-
.guest-card b { display: block; font-size: .86rem; }
|
| 262 |
-
.guest-card small { color: var(--muted); font-size: .72rem; }
|
| 263 |
-
.rail-empty { color: var(--muted); font-size: .88rem; }
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
.floaters {
|
| 266 |
pointer-events: none;
|
| 267 |
position: absolute;
|
| 268 |
left: 50%;
|
| 269 |
-
top:
|
| 270 |
transform: translateX(-50%);
|
| 271 |
z-index: 5;
|
| 272 |
display: flex;
|
| 273 |
flex-direction: column;
|
| 274 |
-
gap: .35rem;
|
| 275 |
align-items: center;
|
| 276 |
}
|
|
|
|
| 277 |
.floater {
|
| 278 |
-
background:
|
| 279 |
-
border: 1px solid var(--
|
| 280 |
-
border-radius:
|
| 281 |
-
padding: .35rem .
|
| 282 |
font-weight: 700;
|
|
|
|
| 283 |
animation: rise 1.5s ease-out forwards;
|
| 284 |
-
box-shadow: var(--shadow);
|
|
|
|
| 285 |
}
|
| 286 |
-
|
| 287 |
-
.floater.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
@keyframes rise {
|
| 289 |
from { opacity: 0; transform: translateY(12px); }
|
| 290 |
20% { opacity: 1; }
|
|
@@ -293,279 +487,416 @@ button.big {
|
|
| 293 |
|
| 294 |
.toast-stack {
|
| 295 |
position: fixed;
|
| 296 |
-
right:
|
| 297 |
-
bottom:
|
| 298 |
z-index: 40;
|
| 299 |
display: flex;
|
| 300 |
flex-direction: column;
|
| 301 |
-
gap: .4rem;
|
| 302 |
max-width: min(320px, 92vw);
|
| 303 |
}
|
|
|
|
| 304 |
.toast {
|
| 305 |
-
background:
|
| 306 |
-
border: 1px solid var(--
|
| 307 |
-
border-
|
| 308 |
-
|
|
|
|
| 309 |
box-shadow: var(--shadow);
|
| 310 |
-
animation: slidein .
|
| 311 |
-
font-size: .9rem;
|
|
|
|
| 312 |
}
|
| 313 |
-
|
| 314 |
-
.toast.
|
| 315 |
-
.toast.
|
|
|
|
|
|
|
|
|
|
| 316 |
@keyframes slidein {
|
| 317 |
from { opacity: 0; transform: translateY(8px); }
|
| 318 |
to { opacity: 1; transform: none; }
|
| 319 |
}
|
| 320 |
-
.help-list {
|
| 321 |
-
margin: .4rem 0 1rem;
|
| 322 |
-
padding-left: 1.2rem;
|
| 323 |
-
color: var(--muted);
|
| 324 |
-
line-height: 1.7;
|
| 325 |
-
}
|
| 326 |
-
.help-list b { color: var(--ink); }
|
| 327 |
|
| 328 |
-
|
|
|
|
| 329 |
display: flex;
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
margin-bottom: .55rem;
|
| 333 |
}
|
| 334 |
-
|
| 335 |
-
.
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
}
|
|
|
|
| 340 |
.stat-grid {
|
| 341 |
display: grid;
|
| 342 |
grid-template-columns: repeat(3, 1fr);
|
| 343 |
-
gap:
|
|
|
|
| 344 |
}
|
|
|
|
| 345 |
.stat-grid div {
|
| 346 |
-
background:
|
| 347 |
-
border
|
| 348 |
-
|
|
|
|
| 349 |
text-align: center;
|
| 350 |
}
|
|
|
|
| 351 |
.stat-grid em {
|
| 352 |
display: block;
|
| 353 |
font-style: normal;
|
| 354 |
-
color: var(--
|
| 355 |
-
font-size: .
|
|
|
|
| 356 |
}
|
| 357 |
-
.stat-grid b { font-size: 1rem; }
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
.atm {
|
| 360 |
display: grid;
|
| 361 |
grid-template-columns: 48px 1fr 34px;
|
| 362 |
-
gap: .4rem;
|
| 363 |
align-items: center;
|
| 364 |
-
font-size: .8rem;
|
| 365 |
-
margin-bottom: .
|
|
|
|
| 366 |
}
|
|
|
|
| 367 |
.bar {
|
| 368 |
height: 8px;
|
| 369 |
-
background:
|
| 370 |
-
border-radius:
|
| 371 |
overflow: hidden;
|
|
|
|
| 372 |
}
|
|
|
|
| 373 |
.bar > i {
|
| 374 |
display: block;
|
| 375 |
height: 100%;
|
| 376 |
-
background: linear-gradient(90deg,
|
| 377 |
-
border-radius:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
}
|
| 379 |
-
.pct { color: var(--muted); text-align: right; }
|
| 380 |
|
|
|
|
| 381 |
.btn-grid {
|
| 382 |
display: grid;
|
| 383 |
grid-template-columns: 1fr 1fr;
|
| 384 |
-
gap: .45rem;
|
| 385 |
}
|
|
|
|
| 386 |
.btn-grid.three { grid-template-columns: repeat(3, 1fr); }
|
| 387 |
-
|
|
|
|
|
|
|
| 388 |
font: inherit;
|
| 389 |
-
border-radius:
|
| 390 |
-
border: 1px solid var(--
|
| 391 |
-
background:
|
| 392 |
-
color: var(--
|
| 393 |
-
padding: .
|
| 394 |
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
}
|
| 396 |
-
|
| 397 |
-
button:
|
| 398 |
-
|
| 399 |
-
button.stable {
|
| 400 |
-
transition: none;
|
| 401 |
}
|
| 402 |
-
|
| 403 |
-
|
|
|
|
|
|
|
| 404 |
}
|
|
|
|
| 405 |
button.primary {
|
| 406 |
-
background: linear-gradient(135deg,
|
| 407 |
border-color: transparent;
|
| 408 |
-
color:
|
| 409 |
-
font-weight:
|
|
|
|
| 410 |
}
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
button.active {
|
| 413 |
-
border-color: var(--
|
| 414 |
-
background:
|
| 415 |
-
color: var(--
|
| 416 |
font-weight: 650;
|
| 417 |
}
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
.speed-row {
|
| 420 |
display: flex;
|
| 421 |
align-items: center;
|
| 422 |
justify-content: space-between;
|
| 423 |
-
gap: .6rem;
|
| 424 |
-
margin-top: .65rem;
|
| 425 |
-
color: var(--muted);
|
| 426 |
-
font-size: .88rem;
|
| 427 |
-
}
|
| 428 |
-
.hint {
|
| 429 |
-
margin: 0 0 .45rem;
|
| 430 |
-
font-size: .78rem;
|
| 431 |
-
color: #9a6a18;
|
| 432 |
-
background: #fff6e8;
|
| 433 |
-
border-radius: 8px;
|
| 434 |
-
padding: .35rem .55rem;
|
| 435 |
-
line-height: 1.4;
|
| 436 |
-
}
|
| 437 |
-
.hint.ok {
|
| 438 |
-
color: var(--green);
|
| 439 |
-
background: #eef6f0;
|
| 440 |
}
|
|
|
|
|
|
|
|
|
|
| 441 |
.form-row {
|
| 442 |
display: grid;
|
| 443 |
grid-template-columns: 1fr auto;
|
| 444 |
-
gap: .4rem;
|
| 445 |
-
margin-bottom: .45rem;
|
| 446 |
}
|
| 447 |
-
|
|
|
|
| 448 |
grid-template-columns: 1fr 1fr auto;
|
| 449 |
}
|
| 450 |
-
select { min-width: 0; width: 100%; }
|
| 451 |
|
| 452 |
-
.
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
|
|
|
|
|
|
| 459 |
}
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
border-radius: 10px;
|
| 466 |
-
padding: .35rem .45rem;
|
| 467 |
-
}
|
| 468 |
-
.shelf img {
|
| 469 |
-
width: 34px;
|
| 470 |
-
height: 34px;
|
| 471 |
-
border-radius: 8px;
|
| 472 |
-
object-fit: cover;
|
| 473 |
}
|
| 474 |
-
.shelf b { display: block; font-size: .84rem; }
|
| 475 |
-
.shelf small { color: var(--muted); font-size: .74rem; }
|
| 476 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
.log {
|
| 478 |
list-style: none;
|
| 479 |
margin: 0;
|
| 480 |
padding: 0;
|
| 481 |
max-height: 180px;
|
| 482 |
overflow: auto;
|
| 483 |
-
font-size: .84rem;
|
| 484 |
-
color: var(--muted);
|
|
|
|
|
|
|
| 485 |
}
|
|
|
|
| 486 |
.log li {
|
| 487 |
-
padding: .
|
| 488 |
-
border-bottom: 1px dashed
|
| 489 |
}
|
| 490 |
-
.log li:first-child { color: var(--ink); font-weight: 550; }
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
.loading {
|
| 493 |
min-height: 50vh;
|
| 494 |
display: grid;
|
| 495 |
place-items: center;
|
| 496 |
-
gap: .
|
| 497 |
-
color: var(--muted);
|
|
|
|
| 498 |
}
|
|
|
|
| 499 |
.spinner {
|
| 500 |
-
width:
|
| 501 |
-
height:
|
| 502 |
border-radius: 50%;
|
| 503 |
-
border: 3px solid
|
| 504 |
-
border-top-color: var(--
|
| 505 |
-
animation: spin .
|
| 506 |
}
|
|
|
|
| 507 |
@keyframes spin { to { transform: rotate(360deg); } }
|
| 508 |
|
|
|
|
| 509 |
.modal {
|
| 510 |
position: fixed;
|
| 511 |
inset: 0;
|
| 512 |
-
background:
|
| 513 |
display: grid;
|
| 514 |
place-items: center;
|
| 515 |
-
padding:
|
| 516 |
z-index: 30;
|
| 517 |
-
backdrop-filter: blur(
|
| 518 |
}
|
|
|
|
| 519 |
.modal-card {
|
| 520 |
width: min(440px, 100%);
|
| 521 |
-
background: var(--
|
| 522 |
-
border-radius:
|
| 523 |
-
border: 1px solid var(--
|
| 524 |
-
padding: 1.
|
| 525 |
-
box-shadow:
|
| 526 |
}
|
|
|
|
| 527 |
.modal-kicker {
|
| 528 |
-
color: var(--
|
| 529 |
-
font-size: .
|
| 530 |
-
letter-spacing: .
|
|
|
|
| 531 |
font-weight: 700;
|
| 532 |
}
|
| 533 |
-
|
| 534 |
-
.modal-card
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
.modal-card .diary {
|
| 536 |
-
color: var(--muted);
|
| 537 |
font-style: italic;
|
| 538 |
margin: 0 0 1rem;
|
| 539 |
}
|
|
|
|
| 540 |
.night-stats {
|
| 541 |
display: grid;
|
| 542 |
grid-template-columns: repeat(3, 1fr);
|
| 543 |
-
gap: .45rem;
|
| 544 |
-
margin-bottom: .
|
| 545 |
}
|
|
|
|
| 546 |
.night-stats div {
|
| 547 |
-
background:
|
| 548 |
-
border
|
|
|
|
| 549 |
text-align: center;
|
| 550 |
-
padding: .55rem .3rem;
|
| 551 |
}
|
|
|
|
| 552 |
.night-stats em {
|
| 553 |
display: block;
|
| 554 |
font-style: normal;
|
| 555 |
-
color: var(--
|
| 556 |
-
font-size: .
|
| 557 |
}
|
| 558 |
-
.vibe { color: var(--muted); font-size: .9rem; }
|
| 559 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
.foot {
|
| 561 |
-
margin-top:
|
| 562 |
text-align: center;
|
| 563 |
-
color: var(--
|
| 564 |
-
font-size: .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
}
|
| 566 |
-
.foot a { color: var(--accent2); }
|
| 567 |
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
.shell { grid-template-columns: 1fr; }
|
| 570 |
-
.form-row
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
}
|
|
|
|
| 1 |
+
/* =========================================================
|
| 2 |
+
小镇角落小店 · Catppuccin Userstyles + UX tokens
|
| 3 |
+
Flavors: Mocha (default, cozy dark) / Latte (light)
|
| 4 |
+
Spacing: 4/8/12/16/24/32 · Radius: 8/12/16/999
|
| 5 |
+
========================================================= */
|
| 6 |
+
|
| 7 |
+
/* --- Catppuccin Mocha (default) --- */
|
| 8 |
+
:root,
|
| 9 |
+
[data-flavor="mocha"] {
|
| 10 |
+
--ctp-rosewater: #f5e0dc;
|
| 11 |
+
--ctp-flamingo: #f2cdcd;
|
| 12 |
+
--ctp-pink: #f5c2e7;
|
| 13 |
+
--ctp-mauve: #cba6f7;
|
| 14 |
+
--ctp-red: #f38ba8;
|
| 15 |
+
--ctp-maroon: #eba0ac;
|
| 16 |
+
--ctp-peach: #fab387;
|
| 17 |
+
--ctp-yellow: #f9e2af;
|
| 18 |
+
--ctp-green: #a6e3a1;
|
| 19 |
+
--ctp-teal: #94e2d5;
|
| 20 |
+
--ctp-sky: #89dceb;
|
| 21 |
+
--ctp-sapphire: #74c7ec;
|
| 22 |
+
--ctp-blue: #89b4fa;
|
| 23 |
+
--ctp-lavender: #b4befe;
|
| 24 |
+
--ctp-text: #cdd6f4;
|
| 25 |
+
--ctp-subtext1: #bac2de;
|
| 26 |
+
--ctp-subtext0: #a6adc8;
|
| 27 |
+
--ctp-overlay2: #9399b2;
|
| 28 |
+
--ctp-overlay1: #7f849c;
|
| 29 |
+
--ctp-overlay0: #6c7086;
|
| 30 |
+
--ctp-surface2: #585b70;
|
| 31 |
+
--ctp-surface1: #45475a;
|
| 32 |
+
--ctp-surface0: #313244;
|
| 33 |
+
--ctp-base: #1e1e2e;
|
| 34 |
+
--ctp-mantle: #181825;
|
| 35 |
+
--ctp-crust: #11111b;
|
| 36 |
+
|
| 37 |
+
--bg: var(--ctp-base);
|
| 38 |
+
--bg-deep: var(--ctp-crust);
|
| 39 |
+
--bg-elevated: var(--ctp-mantle);
|
| 40 |
+
--surface: var(--ctp-surface0);
|
| 41 |
+
--surface-2: var(--ctp-surface1);
|
| 42 |
+
--border: color-mix(in srgb, var(--ctp-overlay0) 45%, transparent);
|
| 43 |
+
--border-strong: var(--ctp-surface2);
|
| 44 |
+
--text: var(--ctp-text);
|
| 45 |
+
--text-muted: var(--ctp-subtext0);
|
| 46 |
+
--text-faint: var(--ctp-overlay1);
|
| 47 |
+
--accent: var(--ctp-peach);
|
| 48 |
+
--accent-2: var(--ctp-pink);
|
| 49 |
+
--accent-soft: color-mix(in srgb, var(--ctp-peach) 18%, var(--ctp-surface0));
|
| 50 |
+
--on-accent: #11111b;
|
| 51 |
+
--ok: var(--ctp-green);
|
| 52 |
+
--warn: var(--ctp-yellow);
|
| 53 |
+
--danger: var(--ctp-red);
|
| 54 |
+
--info: var(--ctp-blue);
|
| 55 |
+
--focus: var(--ctp-lavender);
|
| 56 |
+
--shadow: 0 12px 40px color-mix(in srgb, var(--ctp-crust) 65%, transparent);
|
| 57 |
+
--shadow-sm: 0 4px 14px color-mix(in srgb, var(--ctp-crust) 45%, transparent);
|
| 58 |
+
--glow: 0 0 0 3px color-mix(in srgb, var(--ctp-peach) 35%, transparent);
|
| 59 |
+
--radius-sm: 8px;
|
| 60 |
+
--radius: 14px;
|
| 61 |
+
--radius-lg: 18px;
|
| 62 |
+
--radius-pill: 999px;
|
| 63 |
+
--space-1: 4px;
|
| 64 |
+
--space-2: 8px;
|
| 65 |
+
--space-3: 12px;
|
| 66 |
+
--space-4: 16px;
|
| 67 |
+
--space-5: 24px;
|
| 68 |
+
--space-6: 32px;
|
| 69 |
+
--font: "JetBrains Mono", "SF Mono", "Cascadia Code", "PingFang SC",
|
| 70 |
+
"Microsoft YaHei", "Noto Sans SC", ui-sans-serif, system-ui, sans-serif;
|
| 71 |
+
--font-ui: "Inter", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
| 72 |
"Noto Sans SC", system-ui, sans-serif;
|
| 73 |
+
--stage-floor-a: color-mix(in srgb, var(--ctp-surface0) 80%, var(--ctp-base));
|
| 74 |
+
--stage-floor-b: color-mix(in srgb, var(--ctp-surface1) 55%, var(--ctp-base));
|
| 75 |
}
|
| 76 |
|
| 77 |
+
/* --- Catppuccin Latte (light) --- */
|
| 78 |
+
[data-flavor="latte"] {
|
| 79 |
+
--ctp-rosewater: #dc8a78;
|
| 80 |
+
--ctp-flamingo: #dd7878;
|
| 81 |
+
--ctp-pink: #ea76cb;
|
| 82 |
+
--ctp-mauve: #8839ef;
|
| 83 |
+
--ctp-red: #d20f39;
|
| 84 |
+
--ctp-maroon: #e64553;
|
| 85 |
+
--ctp-peach: #fe640b;
|
| 86 |
+
--ctp-yellow: #df8e1d;
|
| 87 |
+
--ctp-green: #40a02b;
|
| 88 |
+
--ctp-teal: #179299;
|
| 89 |
+
--ctp-sky: #04a5e5;
|
| 90 |
+
--ctp-sapphire: #209fb5;
|
| 91 |
+
--ctp-blue: #1e66f5;
|
| 92 |
+
--ctp-lavender: #7287fd;
|
| 93 |
+
--ctp-text: #4c4f69;
|
| 94 |
+
--ctp-subtext1: #5c5f77;
|
| 95 |
+
--ctp-subtext0: #6c6f85;
|
| 96 |
+
--ctp-overlay2: #7c7f93;
|
| 97 |
+
--ctp-overlay1: #8c8fa1;
|
| 98 |
+
--ctp-overlay0: #9ca0b0;
|
| 99 |
+
--ctp-surface2: #acb0be;
|
| 100 |
+
--ctp-surface1: #bcc0cc;
|
| 101 |
+
--ctp-surface0: #ccd0da;
|
| 102 |
+
--ctp-base: #eff1f5;
|
| 103 |
+
--ctp-mantle: #e6e9ef;
|
| 104 |
+
--ctp-crust: #dce0e8;
|
| 105 |
+
|
| 106 |
+
--bg: var(--ctp-base);
|
| 107 |
+
--bg-deep: var(--ctp-crust);
|
| 108 |
+
--bg-elevated: #ffffff;
|
| 109 |
+
--surface: #ffffff;
|
| 110 |
+
--surface-2: var(--ctp-mantle);
|
| 111 |
+
--border: color-mix(in srgb, var(--ctp-overlay0) 55%, transparent);
|
| 112 |
+
--border-strong: var(--ctp-surface1);
|
| 113 |
+
--text: var(--ctp-text);
|
| 114 |
+
--text-muted: var(--ctp-subtext0);
|
| 115 |
+
--text-faint: var(--ctp-overlay1);
|
| 116 |
+
--accent: var(--ctp-peach);
|
| 117 |
+
--accent-2: var(--ctp-mauve);
|
| 118 |
+
--accent-soft: color-mix(in srgb, var(--ctp-peach) 14%, #fff);
|
| 119 |
+
--on-accent: #ffffff;
|
| 120 |
+
--ok: var(--ctp-green);
|
| 121 |
+
--warn: var(--ctp-yellow);
|
| 122 |
+
--danger: var(--ctp-red);
|
| 123 |
+
--info: var(--ctp-blue);
|
| 124 |
+
--focus: var(--ctp-lavender);
|
| 125 |
+
--shadow: 0 12px 32px color-mix(in srgb, var(--ctp-overlay0) 28%, transparent);
|
| 126 |
+
--shadow-sm: 0 4px 12px color-mix(in srgb, var(--ctp-overlay0) 18%, transparent);
|
| 127 |
+
--glow: 0 0 0 3px color-mix(in srgb, var(--ctp-mauve) 30%, transparent);
|
| 128 |
+
--stage-floor-a: #f8f9fc;
|
| 129 |
+
--stage-floor-b: #eef0f6;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* --- Reset / base --- */
|
| 133 |
+
*,
|
| 134 |
+
*::before,
|
| 135 |
+
*::after { box-sizing: border-box; }
|
| 136 |
+
|
| 137 |
html, body {
|
| 138 |
margin: 0;
|
| 139 |
min-height: 100%;
|
| 140 |
+
color: var(--text);
|
| 141 |
+
font-family: var(--font-ui);
|
| 142 |
+
font-size: 15px;
|
| 143 |
+
line-height: 1.5;
|
| 144 |
background:
|
| 145 |
+
radial-gradient(ellipse 80% 50% at 10% -10%, color-mix(in srgb, var(--ctp-mauve) 18%, transparent), transparent 55%),
|
| 146 |
+
radial-gradient(ellipse 60% 40% at 90% 0%, color-mix(in srgb, var(--ctp-peach) 14%, transparent), transparent 50%),
|
| 147 |
+
radial-gradient(ellipse 50% 30% at 50% 100%, color-mix(in srgb, var(--ctp-pink) 10%, transparent), transparent 45%),
|
| 148 |
var(--bg);
|
| 149 |
+
background-attachment: fixed;
|
| 150 |
+
-webkit-font-smoothing: antialiased;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
[v-cloak] { display: none !important; }
|
| 154 |
+
|
| 155 |
+
:focus-visible {
|
| 156 |
+
outline: 2px solid var(--focus);
|
| 157 |
+
outline-offset: 2px;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
::selection {
|
| 161 |
+
background: color-mix(in srgb, var(--ctp-mauve) 40%, transparent);
|
| 162 |
+
color: var(--text);
|
| 163 |
}
|
|
|
|
| 164 |
|
| 165 |
#app {
|
| 166 |
+
max-width: 1240px;
|
| 167 |
margin: 0 auto;
|
| 168 |
+
padding: var(--space-4) var(--space-4) var(--space-6);
|
| 169 |
}
|
| 170 |
|
| 171 |
+
/* --- Top bar --- */
|
| 172 |
.topbar {
|
| 173 |
+
display: grid;
|
| 174 |
+
grid-template-columns: minmax(200px, 1.2fr) minmax(0, 1.4fr) auto;
|
| 175 |
+
gap: var(--space-3) var(--space-4);
|
| 176 |
align-items: center;
|
| 177 |
+
margin-bottom: var(--space-5);
|
| 178 |
+
padding: var(--space-3) var(--space-4);
|
| 179 |
+
background: color-mix(in srgb, var(--bg-elevated) 88%, transparent);
|
| 180 |
+
border: 1px solid var(--border);
|
| 181 |
+
border-radius: var(--radius-lg);
|
| 182 |
+
box-shadow: var(--shadow-sm);
|
| 183 |
+
backdrop-filter: blur(12px);
|
| 184 |
}
|
| 185 |
+
|
| 186 |
.brand {
|
| 187 |
display: flex;
|
| 188 |
+
gap: var(--space-3);
|
| 189 |
align-items: center;
|
| 190 |
+
min-width: 0;
|
| 191 |
}
|
| 192 |
+
|
| 193 |
.logo {
|
| 194 |
width: 48px;
|
| 195 |
height: 48px;
|
| 196 |
+
flex-shrink: 0;
|
| 197 |
+
border-radius: var(--radius);
|
| 198 |
+
background: linear-gradient(145deg, var(--ctp-peach), var(--ctp-pink));
|
| 199 |
display: grid;
|
| 200 |
place-items: center;
|
| 201 |
+
font-size: 1.35rem;
|
| 202 |
+
box-shadow: var(--shadow-sm), inset 0 1px 0 color-mix(in srgb, #fff 25%, transparent);
|
| 203 |
}
|
| 204 |
+
|
| 205 |
.brand h1 {
|
| 206 |
margin: 0;
|
| 207 |
+
font-size: 1.2rem;
|
| 208 |
+
font-weight: 700;
|
| 209 |
+
letter-spacing: 0.01em;
|
| 210 |
+
color: var(--text);
|
| 211 |
+
white-space: nowrap;
|
| 212 |
+
overflow: hidden;
|
| 213 |
+
text-overflow: ellipsis;
|
| 214 |
}
|
| 215 |
+
|
| 216 |
+
.brand .guide,
|
| 217 |
.brand p {
|
| 218 |
+
margin: 2px 0 0;
|
| 219 |
+
color: var(--text-muted);
|
| 220 |
+
font-size: 0.82rem;
|
| 221 |
+
line-height: 1.35;
|
| 222 |
+
display: -webkit-box;
|
| 223 |
+
-webkit-line-clamp: 2;
|
| 224 |
+
-webkit-box-orient: vertical;
|
| 225 |
+
overflow: hidden;
|
| 226 |
}
|
| 227 |
|
| 228 |
.phase-track {
|
| 229 |
display: flex;
|
| 230 |
flex-wrap: wrap;
|
| 231 |
+
gap: 6px;
|
| 232 |
+
justify-content: center;
|
| 233 |
}
|
| 234 |
+
|
| 235 |
.phase-pill {
|
| 236 |
+
font-size: 0.72rem;
|
| 237 |
+
font-weight: 600;
|
| 238 |
+
letter-spacing: 0.04em;
|
| 239 |
+
padding: 0.28rem 0.65rem;
|
| 240 |
+
border-radius: var(--radius-pill);
|
| 241 |
+
border: 1px solid var(--border);
|
| 242 |
+
background: var(--surface);
|
| 243 |
+
color: var(--text-faint);
|
| 244 |
+
transition: color 0.15s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
|
| 245 |
}
|
| 246 |
+
|
| 247 |
.phase-pill.on {
|
| 248 |
+
color: var(--ctp-crust);
|
| 249 |
border-color: transparent;
|
| 250 |
+
background: linear-gradient(135deg, var(--ctp-peach), var(--ctp-pink));
|
| 251 |
+
box-shadow: 0 4px 14px color-mix(in srgb, var(--ctp-peach) 35%, transparent);
|
| 252 |
}
|
| 253 |
+
|
| 254 |
.top-meta {
|
| 255 |
display: flex;
|
| 256 |
flex-wrap: wrap;
|
| 257 |
+
gap: 0.4rem 0.65rem;
|
| 258 |
align-items: center;
|
| 259 |
+
justify-content: flex-end;
|
| 260 |
+
color: var(--text-muted);
|
| 261 |
+
font-size: 0.88rem;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.top-meta strong { color: var(--text); font-weight: 700; }
|
| 265 |
+
|
| 266 |
+
.money-pill {
|
| 267 |
+
background: var(--accent-soft);
|
| 268 |
+
color: var(--accent);
|
| 269 |
+
font-weight: 700;
|
| 270 |
+
font-variant-numeric: tabular-nums;
|
| 271 |
+
padding: 0.22rem 0.65rem;
|
| 272 |
+
border-radius: var(--radius-pill);
|
| 273 |
+
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.flavor-switch {
|
| 277 |
+
display: inline-flex;
|
| 278 |
+
border: 1px solid var(--border);
|
| 279 |
+
border-radius: var(--radius-pill);
|
| 280 |
+
overflow: hidden;
|
| 281 |
+
background: var(--surface);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.flavor-switch button {
|
| 285 |
+
border: 0;
|
| 286 |
+
border-radius: 0;
|
| 287 |
+
background: transparent;
|
| 288 |
+
color: var(--text-muted);
|
| 289 |
+
padding: 0.28rem 0.7rem;
|
| 290 |
+
font-size: 0.75rem;
|
| 291 |
+
font-weight: 600;
|
| 292 |
}
|
| 293 |
+
|
| 294 |
+
.flavor-switch button[aria-pressed="true"] {
|
| 295 |
+
background: color-mix(in srgb, var(--ctp-mauve) 28%, var(--surface));
|
| 296 |
+
color: var(--text);
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
|
| 299 |
+
/* --- Layout --- */
|
| 300 |
.shell {
|
| 301 |
display: grid;
|
| 302 |
grid-template-columns: minmax(0, 1fr) 340px;
|
| 303 |
+
gap: var(--space-4);
|
| 304 |
align-items: start;
|
| 305 |
}
|
| 306 |
|
| 307 |
+
/* --- Stage --- */
|
| 308 |
.stage-card {
|
| 309 |
position: relative;
|
| 310 |
+
background: var(--bg-elevated);
|
| 311 |
+
border: 1px solid var(--border);
|
| 312 |
+
border-radius: var(--radius-lg);
|
| 313 |
overflow: hidden;
|
| 314 |
box-shadow: var(--shadow);
|
| 315 |
}
|
| 316 |
+
|
| 317 |
.stage-bg {
|
| 318 |
position: absolute;
|
| 319 |
inset: 0;
|
| 320 |
background-size: cover;
|
| 321 |
background-position: center;
|
| 322 |
+
opacity: 0.22;
|
| 323 |
pointer-events: none;
|
| 324 |
+
filter: saturate(1.05);
|
| 325 |
}
|
| 326 |
+
|
| 327 |
+
.stage-card.light-warm .stage-bg { filter: sepia(0.2) saturate(1.15); opacity: 0.28; }
|
| 328 |
+
.stage-card.light-cool .stage-bg { filter: hue-rotate(20deg) brightness(1.05); opacity: 0.26; }
|
| 329 |
+
.stage-card.light-off .stage-bg { filter: brightness(0.4) saturate(0.5); opacity: 0.18; }
|
| 330 |
+
|
| 331 |
canvas {
|
| 332 |
position: relative;
|
| 333 |
display: block;
|
| 334 |
width: 100%;
|
| 335 |
height: auto;
|
| 336 |
+
background: linear-gradient(180deg, var(--stage-floor-a), var(--stage-floor-b));
|
| 337 |
}
|
| 338 |
+
|
| 339 |
.mood-bar {
|
| 340 |
position: relative;
|
| 341 |
display: flex;
|
| 342 |
+
gap: var(--space-2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
align-items: center;
|
| 344 |
+
padding: 0.7rem 1rem;
|
| 345 |
+
border-top: 1px solid var(--border);
|
| 346 |
+
background: color-mix(in srgb, var(--bg-elevated) 92%, var(--surface));
|
| 347 |
+
color: var(--text-muted);
|
| 348 |
+
font-size: 0.92rem;
|
| 349 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
.live {
|
| 352 |
margin-left: auto;
|
| 353 |
+
font-size: 0.78rem;
|
| 354 |
+
font-weight: 700;
|
| 355 |
+
color: var(--ctp-teal);
|
| 356 |
+
background: color-mix(in srgb, var(--ctp-teal) 14%, transparent);
|
| 357 |
+
padding: 0.15rem 0.55rem;
|
| 358 |
+
border-radius: var(--radius-pill);
|
| 359 |
}
|
|
|
|
| 360 |
|
| 361 |
+
.live.dim {
|
| 362 |
+
color: var(--text-faint);
|
| 363 |
+
background: var(--surface);
|
| 364 |
+
font-weight: 500;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/* Guest rail */
|
| 368 |
.guest-rail {
|
| 369 |
+
border-top: 1px solid var(--border);
|
| 370 |
+
background: var(--bg-deep);
|
| 371 |
+
padding: 0.65rem 0.85rem 0.8rem;
|
| 372 |
}
|
| 373 |
+
|
| 374 |
.rail-title {
|
| 375 |
+
font-size: 0.72rem;
|
| 376 |
+
letter-spacing: 0.1em;
|
| 377 |
+
text-transform: uppercase;
|
| 378 |
+
color: var(--text-faint);
|
| 379 |
font-weight: 700;
|
| 380 |
+
margin-bottom: 0.45rem;
|
| 381 |
}
|
| 382 |
+
|
| 383 |
.rail-body {
|
| 384 |
display: flex;
|
| 385 |
+
gap: 0.5rem;
|
| 386 |
overflow-x: auto;
|
| 387 |
+
padding-bottom: 2px;
|
| 388 |
+
scrollbar-width: thin;
|
| 389 |
+
scrollbar-color: var(--surface-2) transparent;
|
| 390 |
}
|
| 391 |
+
|
| 392 |
.guest-card {
|
| 393 |
display: flex;
|
| 394 |
+
gap: 0.45rem;
|
| 395 |
align-items: center;
|
| 396 |
+
min-width: 152px;
|
| 397 |
+
background: var(--surface);
|
| 398 |
+
border: 1px solid var(--border);
|
| 399 |
+
border-radius: var(--radius);
|
| 400 |
+
padding: 0.4rem 0.55rem;
|
| 401 |
+
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.15s;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.guest-card.here {
|
| 405 |
+
border-color: color-mix(in srgb, var(--ctp-peach) 55%, var(--border));
|
| 406 |
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ctp-peach) 22%, transparent);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.guest-card.left { opacity: 0.5; }
|
| 410 |
+
|
| 411 |
+
.guest-card img,
|
| 412 |
+
.avatar-fallback {
|
| 413 |
width: 36px;
|
| 414 |
height: 36px;
|
| 415 |
border-radius: 50%;
|
| 416 |
object-fit: cover;
|
| 417 |
+
flex-shrink: 0;
|
| 418 |
}
|
| 419 |
+
|
| 420 |
.avatar-fallback {
|
| 421 |
display: grid;
|
| 422 |
place-items: center;
|
| 423 |
+
background: linear-gradient(145deg, var(--ctp-mauve), var(--ctp-blue));
|
| 424 |
+
color: var(--ctp-crust);
|
| 425 |
font-weight: 700;
|
| 426 |
+
font-size: 0.9rem;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.guest-card b {
|
| 430 |
+
display: block;
|
| 431 |
+
font-size: 0.86rem;
|
| 432 |
+
color: var(--text);
|
| 433 |
}
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
+
.guest-card small {
|
| 436 |
+
color: var(--text-muted);
|
| 437 |
+
font-size: 0.72rem;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.rail-empty {
|
| 441 |
+
color: var(--text-faint);
|
| 442 |
+
font-size: 0.88rem;
|
| 443 |
+
padding: 0.35rem 0;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
/* Floaters / toasts */
|
| 447 |
.floaters {
|
| 448 |
pointer-events: none;
|
| 449 |
position: absolute;
|
| 450 |
left: 50%;
|
| 451 |
+
top: 16%;
|
| 452 |
transform: translateX(-50%);
|
| 453 |
z-index: 5;
|
| 454 |
display: flex;
|
| 455 |
flex-direction: column;
|
| 456 |
+
gap: 0.35rem;
|
| 457 |
align-items: center;
|
| 458 |
}
|
| 459 |
+
|
| 460 |
.floater {
|
| 461 |
+
background: color-mix(in srgb, var(--bg-elevated) 94%, transparent);
|
| 462 |
+
border: 1px solid var(--border);
|
| 463 |
+
border-radius: var(--radius-pill);
|
| 464 |
+
padding: 0.35rem 0.85rem;
|
| 465 |
font-weight: 700;
|
| 466 |
+
color: var(--text);
|
| 467 |
animation: rise 1.5s ease-out forwards;
|
| 468 |
+
box-shadow: var(--shadow-sm);
|
| 469 |
+
backdrop-filter: blur(8px);
|
| 470 |
}
|
| 471 |
+
|
| 472 |
+
.floater.sale {
|
| 473 |
+
color: var(--ok);
|
| 474 |
+
border-color: color-mix(in srgb, var(--ok) 45%, var(--border));
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.floater.enter {
|
| 478 |
+
color: var(--ctp-peach);
|
| 479 |
+
border-color: color-mix(in srgb, var(--ctp-peach) 40%, var(--border));
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
@keyframes rise {
|
| 483 |
from { opacity: 0; transform: translateY(12px); }
|
| 484 |
20% { opacity: 1; }
|
|
|
|
| 487 |
|
| 488 |
.toast-stack {
|
| 489 |
position: fixed;
|
| 490 |
+
right: var(--space-4);
|
| 491 |
+
bottom: var(--space-4);
|
| 492 |
z-index: 40;
|
| 493 |
display: flex;
|
| 494 |
flex-direction: column;
|
| 495 |
+
gap: 0.4rem;
|
| 496 |
max-width: min(320px, 92vw);
|
| 497 |
}
|
| 498 |
+
|
| 499 |
.toast {
|
| 500 |
+
background: var(--bg-elevated);
|
| 501 |
+
border: 1px solid var(--border);
|
| 502 |
+
border-left: 3px solid var(--ctp-lavender);
|
| 503 |
+
border-radius: var(--radius);
|
| 504 |
+
padding: 0.7rem 0.85rem;
|
| 505 |
box-shadow: var(--shadow);
|
| 506 |
+
animation: slidein 0.22s ease-out;
|
| 507 |
+
font-size: 0.9rem;
|
| 508 |
+
color: var(--text);
|
| 509 |
}
|
| 510 |
+
|
| 511 |
+
.toast.sale { border-left-color: var(--ok); }
|
| 512 |
+
.toast.enter { border-left-color: var(--ctp-peach); }
|
| 513 |
+
.toast.err { border-left-color: var(--danger); color: var(--danger); }
|
| 514 |
+
.toast.phase { border-left-color: var(--ctp-mauve); }
|
| 515 |
+
|
| 516 |
@keyframes slidein {
|
| 517 |
from { opacity: 0; transform: translateY(8px); }
|
| 518 |
to { opacity: 1; transform: none; }
|
| 519 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
|
| 521 |
+
/* --- HUD cards --- */
|
| 522 |
+
.hud {
|
| 523 |
display: flex;
|
| 524 |
+
flex-direction: column;
|
| 525 |
+
gap: var(--space-3);
|
|
|
|
| 526 |
}
|
| 527 |
+
|
| 528 |
+
.card {
|
| 529 |
+
background: var(--bg-elevated);
|
| 530 |
+
border: 1px solid var(--border);
|
| 531 |
+
border-radius: var(--radius-lg);
|
| 532 |
+
padding: var(--space-4);
|
| 533 |
+
box-shadow: var(--shadow-sm);
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.card h2 {
|
| 537 |
+
margin: 0 0 var(--space-3);
|
| 538 |
+
font-size: 0.72rem;
|
| 539 |
+
letter-spacing: 0.12em;
|
| 540 |
+
text-transform: uppercase;
|
| 541 |
+
color: var(--text-faint);
|
| 542 |
+
font-weight: 700;
|
| 543 |
}
|
| 544 |
+
|
| 545 |
.stat-grid {
|
| 546 |
display: grid;
|
| 547 |
grid-template-columns: repeat(3, 1fr);
|
| 548 |
+
gap: var(--space-2);
|
| 549 |
+
margin-bottom: var(--space-3);
|
| 550 |
}
|
| 551 |
+
|
| 552 |
.stat-grid div {
|
| 553 |
+
background: var(--surface);
|
| 554 |
+
border: 1px solid var(--border);
|
| 555 |
+
border-radius: var(--radius-sm);
|
| 556 |
+
padding: 0.5rem 0.35rem;
|
| 557 |
text-align: center;
|
| 558 |
}
|
| 559 |
+
|
| 560 |
.stat-grid em {
|
| 561 |
display: block;
|
| 562 |
font-style: normal;
|
| 563 |
+
color: var(--text-faint);
|
| 564 |
+
font-size: 0.7rem;
|
| 565 |
+
margin-bottom: 2px;
|
| 566 |
}
|
|
|
|
| 567 |
|
| 568 |
+
.stat-grid b {
|
| 569 |
+
font-size: 1.05rem;
|
| 570 |
+
font-variant-numeric: tabular-nums;
|
| 571 |
+
color: var(--text);
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
/* Atmosphere bars */
|
| 575 |
.atm {
|
| 576 |
display: grid;
|
| 577 |
grid-template-columns: 48px 1fr 34px;
|
| 578 |
+
gap: 0.4rem;
|
| 579 |
align-items: center;
|
| 580 |
+
font-size: 0.8rem;
|
| 581 |
+
margin-bottom: 0.4rem;
|
| 582 |
+
color: var(--text-muted);
|
| 583 |
}
|
| 584 |
+
|
| 585 |
.bar {
|
| 586 |
height: 8px;
|
| 587 |
+
background: var(--surface);
|
| 588 |
+
border-radius: var(--radius-pill);
|
| 589 |
overflow: hidden;
|
| 590 |
+
border: 1px solid var(--border);
|
| 591 |
}
|
| 592 |
+
|
| 593 |
.bar > i {
|
| 594 |
display: block;
|
| 595 |
height: 100%;
|
| 596 |
+
background: linear-gradient(90deg, var(--ctp-mauve), var(--ctp-peach));
|
| 597 |
+
border-radius: var(--radius-pill);
|
| 598 |
+
transition: width 0.35s ease;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
.pct {
|
| 602 |
+
color: var(--text-faint);
|
| 603 |
+
text-align: right;
|
| 604 |
+
font-variant-numeric: tabular-nums;
|
| 605 |
+
font-size: 0.75rem;
|
| 606 |
}
|
|
|
|
| 607 |
|
| 608 |
+
/* Buttons & forms */
|
| 609 |
.btn-grid {
|
| 610 |
display: grid;
|
| 611 |
grid-template-columns: 1fr 1fr;
|
| 612 |
+
gap: 0.45rem;
|
| 613 |
}
|
| 614 |
+
|
| 615 |
.btn-grid.three { grid-template-columns: repeat(3, 1fr); }
|
| 616 |
+
|
| 617 |
+
button,
|
| 618 |
+
select {
|
| 619 |
font: inherit;
|
| 620 |
+
border-radius: var(--radius-pill);
|
| 621 |
+
border: 1px solid var(--border);
|
| 622 |
+
background: var(--surface);
|
| 623 |
+
color: var(--text);
|
| 624 |
+
padding: 0.5rem 0.85rem;
|
| 625 |
cursor: pointer;
|
| 626 |
+
transition: background 0.12s, border-color 0.12s, box-shadow 0.12s, opacity 0.12s;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
button:hover:not(:disabled) {
|
| 630 |
+
border-color: var(--border-strong);
|
| 631 |
+
background: var(--surface-2);
|
| 632 |
}
|
| 633 |
+
|
| 634 |
+
button:active:not(:disabled) {
|
| 635 |
+
transform: translateY(1px);
|
|
|
|
|
|
|
| 636 |
}
|
| 637 |
+
|
| 638 |
+
button:disabled {
|
| 639 |
+
opacity: 0.42;
|
| 640 |
+
cursor: not-allowed;
|
| 641 |
}
|
| 642 |
+
|
| 643 |
button.primary {
|
| 644 |
+
background: linear-gradient(135deg, var(--ctp-peach), var(--ctp-pink));
|
| 645 |
border-color: transparent;
|
| 646 |
+
color: var(--on-accent);
|
| 647 |
+
font-weight: 700;
|
| 648 |
+
box-shadow: 0 6px 18px color-mix(in srgb, var(--ctp-peach) 30%, transparent);
|
| 649 |
}
|
| 650 |
+
|
| 651 |
+
button.primary:hover:not(:disabled) {
|
| 652 |
+
filter: brightness(1.05);
|
| 653 |
+
background: linear-gradient(135deg, var(--ctp-peach), var(--ctp-pink));
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
button.ghost {
|
| 657 |
+
background: transparent;
|
| 658 |
+
border-color: var(--border);
|
| 659 |
+
color: var(--text-muted);
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
button.ghost:hover:not(:disabled) {
|
| 663 |
+
background: var(--surface);
|
| 664 |
+
color: var(--text);
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
button.active {
|
| 668 |
+
border-color: color-mix(in srgb, var(--ctp-mauve) 55%, var(--border));
|
| 669 |
+
background: color-mix(in srgb, var(--ctp-mauve) 22%, var(--surface));
|
| 670 |
+
color: var(--ctp-lavender);
|
| 671 |
font-weight: 650;
|
| 672 |
}
|
| 673 |
+
|
| 674 |
+
button.big {
|
| 675 |
+
width: 100%;
|
| 676 |
+
padding: 0.9rem 1rem;
|
| 677 |
+
font-size: 1.05rem;
|
| 678 |
+
margin: 0.2rem 0 0.55rem;
|
| 679 |
+
border-radius: var(--radius);
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
button.wide { width: 100%; border-radius: var(--radius); }
|
| 683 |
+
button.stable { transition: background 0.12s, border-color 0.12s; }
|
| 684 |
+
button.stable:active { transform: none; }
|
| 685 |
+
|
| 686 |
+
select {
|
| 687 |
+
min-width: 0;
|
| 688 |
+
width: 100%;
|
| 689 |
+
appearance: none;
|
| 690 |
+
background-image: linear-gradient(45deg, transparent 50%, var(--text-faint) 50%),
|
| 691 |
+
linear-gradient(135deg, var(--text-faint) 50%, transparent 50%);
|
| 692 |
+
background-position: calc(100% - 16px) calc(50% - 2px), calc(100% - 11px) calc(50% - 2px);
|
| 693 |
+
background-size: 5px 5px, 5px 5px;
|
| 694 |
+
background-repeat: no-repeat;
|
| 695 |
+
padding-right: 1.6rem;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
.speed-row {
|
| 699 |
display: flex;
|
| 700 |
align-items: center;
|
| 701 |
justify-content: space-between;
|
| 702 |
+
gap: 0.6rem;
|
| 703 |
+
margin-top: 0.65rem;
|
| 704 |
+
color: var(--text-muted);
|
| 705 |
+
font-size: 0.88rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
}
|
| 707 |
+
|
| 708 |
+
.speed-row select { width: auto; min-width: 7.5rem; }
|
| 709 |
+
|
| 710 |
.form-row {
|
| 711 |
display: grid;
|
| 712 |
grid-template-columns: 1fr auto;
|
| 713 |
+
gap: 0.4rem;
|
| 714 |
+
margin-bottom: 0.45rem;
|
| 715 |
}
|
| 716 |
+
|
| 717 |
+
.form-row.triple {
|
| 718 |
grid-template-columns: 1fr 1fr auto;
|
| 719 |
}
|
|
|
|
| 720 |
|
| 721 |
+
.hint {
|
| 722 |
+
margin: 0 0 0.5rem;
|
| 723 |
+
font-size: 0.78rem;
|
| 724 |
+
color: var(--warn);
|
| 725 |
+
background: color-mix(in srgb, var(--warn) 12%, var(--surface));
|
| 726 |
+
border: 1px solid color-mix(in srgb, var(--warn) 28%, var(--border));
|
| 727 |
+
border-radius: var(--radius-sm);
|
| 728 |
+
padding: 0.4rem 0.6rem;
|
| 729 |
+
line-height: 1.4;
|
| 730 |
}
|
| 731 |
+
|
| 732 |
+
.hint.ok {
|
| 733 |
+
color: var(--ok);
|
| 734 |
+
background: color-mix(in srgb, var(--ok) 12%, var(--surface));
|
| 735 |
+
border-color: color-mix(in srgb, var(--ok) 28%, var(--border));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
}
|
|
|
|
|
|
|
| 737 |
|
| 738 |
+
.light-row { margin-top: 0.55rem; }
|
| 739 |
+
|
| 740 |
+
.hero-actions .hint { margin-bottom: 0.55rem; }
|
| 741 |
+
|
| 742 |
+
/* Log */
|
| 743 |
.log {
|
| 744 |
list-style: none;
|
| 745 |
margin: 0;
|
| 746 |
padding: 0;
|
| 747 |
max-height: 180px;
|
| 748 |
overflow: auto;
|
| 749 |
+
font-size: 0.84rem;
|
| 750 |
+
color: var(--text-muted);
|
| 751 |
+
scrollbar-width: thin;
|
| 752 |
+
scrollbar-color: var(--surface-2) transparent;
|
| 753 |
}
|
| 754 |
+
|
| 755 |
.log li {
|
| 756 |
+
padding: 0.4rem 0;
|
| 757 |
+
border-bottom: 1px dashed color-mix(in srgb, var(--border) 80%, transparent);
|
| 758 |
}
|
|
|
|
| 759 |
|
| 760 |
+
.log li:first-child {
|
| 761 |
+
color: var(--text);
|
| 762 |
+
font-weight: 600;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
/* Loading */
|
| 766 |
.loading {
|
| 767 |
min-height: 50vh;
|
| 768 |
display: grid;
|
| 769 |
place-items: center;
|
| 770 |
+
gap: 0.7rem;
|
| 771 |
+
color: var(--text-muted);
|
| 772 |
+
text-align: center;
|
| 773 |
}
|
| 774 |
+
|
| 775 |
.spinner {
|
| 776 |
+
width: 40px;
|
| 777 |
+
height: 40px;
|
| 778 |
border-radius: 50%;
|
| 779 |
+
border: 3px solid var(--surface);
|
| 780 |
+
border-top-color: var(--ctp-peach);
|
| 781 |
+
animation: spin 0.75s linear infinite;
|
| 782 |
}
|
| 783 |
+
|
| 784 |
@keyframes spin { to { transform: rotate(360deg); } }
|
| 785 |
|
| 786 |
+
/* Modal */
|
| 787 |
.modal {
|
| 788 |
position: fixed;
|
| 789 |
inset: 0;
|
| 790 |
+
background: color-mix(in srgb, var(--ctp-crust) 62%, transparent);
|
| 791 |
display: grid;
|
| 792 |
place-items: center;
|
| 793 |
+
padding: var(--space-4);
|
| 794 |
z-index: 30;
|
| 795 |
+
backdrop-filter: blur(6px);
|
| 796 |
}
|
| 797 |
+
|
| 798 |
.modal-card {
|
| 799 |
width: min(440px, 100%);
|
| 800 |
+
background: var(--bg-elevated);
|
| 801 |
+
border-radius: var(--radius-lg);
|
| 802 |
+
border: 1px solid var(--border);
|
| 803 |
+
padding: 1.35rem 1.4rem 1.25rem;
|
| 804 |
+
box-shadow: var(--shadow);
|
| 805 |
}
|
| 806 |
+
|
| 807 |
.modal-kicker {
|
| 808 |
+
color: var(--ctp-mauve);
|
| 809 |
+
font-size: 0.75rem;
|
| 810 |
+
letter-spacing: 0.14em;
|
| 811 |
+
text-transform: uppercase;
|
| 812 |
font-weight: 700;
|
| 813 |
}
|
| 814 |
+
|
| 815 |
+
.modal-card h2 {
|
| 816 |
+
margin: 0.3rem 0 0.65rem;
|
| 817 |
+
color: var(--text);
|
| 818 |
+
text-transform: none;
|
| 819 |
+
letter-spacing: 0;
|
| 820 |
+
font-size: 1.25rem;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.modal-card .lead {
|
| 824 |
+
color: var(--text);
|
| 825 |
+
margin: 0 0 0.5rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
.modal-card .diary {
|
| 829 |
+
color: var(--text-muted);
|
| 830 |
font-style: italic;
|
| 831 |
margin: 0 0 1rem;
|
| 832 |
}
|
| 833 |
+
|
| 834 |
.night-stats {
|
| 835 |
display: grid;
|
| 836 |
grid-template-columns: repeat(3, 1fr);
|
| 837 |
+
gap: 0.45rem;
|
| 838 |
+
margin-bottom: 0.85rem;
|
| 839 |
}
|
| 840 |
+
|
| 841 |
.night-stats div {
|
| 842 |
+
background: var(--surface);
|
| 843 |
+
border: 1px solid var(--border);
|
| 844 |
+
border-radius: var(--radius);
|
| 845 |
text-align: center;
|
| 846 |
+
padding: 0.55rem 0.3rem;
|
| 847 |
}
|
| 848 |
+
|
| 849 |
.night-stats em {
|
| 850 |
display: block;
|
| 851 |
font-style: normal;
|
| 852 |
+
color: var(--text-faint);
|
| 853 |
+
font-size: 0.72rem;
|
| 854 |
}
|
|
|
|
| 855 |
|
| 856 |
+
.vibe {
|
| 857 |
+
color: var(--text-muted);
|
| 858 |
+
font-size: 0.9rem;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.help-list {
|
| 862 |
+
margin: 0.4rem 0 1rem;
|
| 863 |
+
padding-left: 1.2rem;
|
| 864 |
+
color: var(--text-muted);
|
| 865 |
+
line-height: 1.75;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.help-list b { color: var(--text); }
|
| 869 |
+
|
| 870 |
+
/* Footer */
|
| 871 |
.foot {
|
| 872 |
+
margin-top: var(--space-5);
|
| 873 |
text-align: center;
|
| 874 |
+
color: var(--text-faint);
|
| 875 |
+
font-size: 0.8rem;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.foot a {
|
| 879 |
+
color: var(--ctp-lavender);
|
| 880 |
+
text-decoration: none;
|
| 881 |
}
|
|
|
|
| 882 |
|
| 883 |
+
.foot a:hover { text-decoration: underline; }
|
| 884 |
+
|
| 885 |
+
/* Responsive */
|
| 886 |
+
@media (max-width: 960px) {
|
| 887 |
+
.topbar {
|
| 888 |
+
grid-template-columns: 1fr;
|
| 889 |
+
}
|
| 890 |
+
.top-meta { justify-content: flex-start; }
|
| 891 |
+
.phase-track { justify-content: flex-start; }
|
| 892 |
.shell { grid-template-columns: 1fr; }
|
| 893 |
+
.form-row.triple { grid-template-columns: 1fr; }
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
@media (prefers-reduced-motion: reduce) {
|
| 897 |
+
*, *::before, *::after {
|
| 898 |
+
animation-duration: 0.01ms !important;
|
| 899 |
+
animation-iteration-count: 1 !important;
|
| 900 |
+
transition-duration: 0.01ms !important;
|
| 901 |
+
}
|
| 902 |
}
|