/* 数字一笔 · Web 壳样式。令牌与组件规格见仓库 docs/design.md(改令牌先改文档,再同步 src/theme.ts)。 */ :root { /* ---- 颜色 ---- */ --bg: #ffffff; --bg-app: #f2f2f6; --surface-2: #f3f3f7; --cell: #e6e7ec; --line: #e1e2ea; --ink: #23243a; --ink-2: #6b6d80; --ink-3: #a3a5b5; --primary: #6558f5; --primary-bright: #7d72ff; --primary-soft: #eceafe; --on-primary: #ffffff; --danger: #e5484d; --danger-soft: #fdecec; --hint: #f5a524; --hint-soft: #fef3dd; --clue-ring: #23243a; --overlay: rgba(35, 36, 58, 0.92); --shadow-primary: 0 6px 18px rgba(101, 88, 245, 0.28); --shadow-primary-pressed: 0 3px 10px rgba(101, 88, 245, 0.22); --shadow-frame: 0 20px 60px rgba(20, 20, 40, 0.14); /* ---- 字体 ---- */ --font: ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif; --fs-display: 34px; --fs-title: 22px; --fs-heading: 17px; --fs-body: 15px; --fs-label: 14px; --fs-caption: 13px; --fs-micro: 12px; /* ---- 圆角 ---- */ --r-sm: 10px; --r-md: 14px; --r-lg: 18px; --r-xl: 24px; --r-pill: 999px; /* ---- 间距(4pt 网格) ---- */ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-7: 28px; /* ---- 动效 ---- */ --t-fast: 120ms; --t-base: 200ms; --t-slow: 320ms; --t-hold: 500ms; --ease-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* ---- 尺寸 ---- */ --tap: 44px; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); --safe-left: env(safe-area-inset-left, 0px); --safe-right: env(safe-area-inset-right, 0px); } * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } html, body { margin: 0; height: 100%; background: var(--bg-app); color: var(--ink); font-family: var(--font); font-size: var(--fs-body); line-height: 1.3; -webkit-font-smoothing: antialiased; font-variant-numeric: tabular-nums; user-select: none; -webkit-user-select: none; touch-action: manipulation; overscroll-behavior: none; } #app { position: relative; margin: 0 auto; width: 100%; max-width: 480px; height: 100vh; height: 100dvh; background: var(--bg); overflow: hidden; } .screen { position: absolute; inset: 0; display: flex; flex-direction: column; padding: calc(var(--safe-top) + var(--sp-3)) calc(var(--safe-right) + var(--sp-5)) calc(var(--safe-bottom) + var(--sp-4)) calc(var(--safe-left) + var(--sp-5)); } .screen.hidden { display: none; } .screen.enter { animation: screen-in var(--t-base) var(--ease-out) both; } button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; -webkit-touch-callout: none; transition: transform var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out), opacity var(--t-fast) var(--ease-out); } button:active { transform: scale(0.97); } button:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; } /* ---------- 选关 ---------- */ .select-header { padding: var(--sp-7) var(--sp-1) var(--sp-2); } .select-header h1 { margin: 0; font-size: var(--fs-display); font-weight: 800; letter-spacing: 0.02em; line-height: 1.15; } .select-header .sub { margin: var(--sp-2) 0 0; color: var(--ink-2); font-size: var(--fs-body); font-weight: 500; } .overall { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); } .overall-bar { flex: 1; height: 6px; border-radius: var(--r-pill); background: var(--cell); overflow: hidden; } .overall-bar i { display: block; height: 100%; width: 0; border-radius: var(--r-pill); background: var(--primary); transition: width var(--t-slow) var(--ease-out); } .overall-text { color: var(--ink-3); font-size: var(--fs-caption); font-weight: 600; min-width: 52px; text-align: right; } .pack-list { flex: 1; overflow-y: auto; margin-top: var(--sp-3); padding-bottom: var(--sp-5); /* 继续按钮固定在底部,包列表自己滚;底部 20px 渐隐,避免被按钮“切断” */ min-height: 0; -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 20px), transparent); mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 20px), transparent); } .pack { margin-top: var(--sp-7); } .pack-list > .pack:first-child { margin-top: var(--sp-2); } .pack-title { display: flex; align-items: baseline; justify-content: space-between; padding: 0 var(--sp-1) var(--sp-3); font-size: var(--fs-heading); font-weight: 700; } .pack-progress { color: var(--ink-3); font-size: var(--fs-label); font-weight: 600; } .pack.done .pack-progress { color: var(--primary); } .level-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-3); } .level-tile { position: relative; aspect-ratio: 1; border-radius: var(--r-md); background: var(--cell); color: var(--ink); font-size: 20px; font-weight: 700; display: flex; align-items: center; justify-content: center; } .level-tile:active { transform: scale(0.94); } .level-tile.cleared { background: var(--primary); color: var(--on-primary); } /* 已过关:右上角一个小勾(纯 CSS 画,不依赖字体里的 ✓) */ .level-tile.cleared::after { content: ""; position: absolute; right: 8px; top: 6px; width: 4px; height: 8px; border: solid rgba(255, 255, 255, 0.9); border-width: 0 2px 2px 0; transform: rotate(45deg); } .level-tile.current { box-shadow: inset 0 0 0 3px var(--primary); background: var(--primary-soft); color: var(--primary); animation: breathe 2.4s var(--ease-in-out) infinite; } .level-tile.locked { color: var(--ink-3); background: var(--surface-2); } .level-tile.locked::before { content: ""; width: 10px; height: 12px; border: 2px solid var(--ink-3); border-radius: 3px; border-top-width: 3px; } .level-tile.locked span { display: none; } .level-tile.shake { animation: shake var(--t-slow) var(--ease-out); } .primary { margin-top: auto; height: 54px; border-radius: var(--r-lg); background: var(--primary); color: var(--on-primary); font-size: var(--fs-heading); font-weight: 700; letter-spacing: 0.02em; box-shadow: var(--shadow-primary); } .primary:active { transform: scale(0.98); box-shadow: var(--shadow-primary-pressed); } .select-footer { display: flex; justify-content: center; gap: var(--sp-2); padding-top: var(--sp-4); } .chip { height: 34px; padding: 0 var(--sp-4); border-radius: var(--r-pill); background: var(--surface-2); color: var(--ink-2); font-size: var(--fs-caption); font-weight: 600; } .chip.on { background: var(--primary-soft); color: var(--primary); } .chip.danger { color: var(--danger); } /* ---------- 对局 ---------- */ .topbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 52px; } .topbar .icon { justify-self: start; width: var(--tap); height: var(--tap); display: flex; align-items: center; justify-content: center; color: var(--ink-2); border-radius: var(--r-pill); margin-left: -8px; } .topbar .icon:active { background: var(--surface-2); } .level-info { text-align: center; line-height: 1.1; } .level-title { font-size: var(--fs-title); font-weight: 800; letter-spacing: 0.02em; } .level-sub { margin-top: 3px; color: var(--ink-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: 0.04em; white-space: nowrap; } .remaining { justify-self: end; display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 var(--sp-3) 0 10px; border-radius: var(--r-pill); background: var(--surface-2); color: var(--ink-2); font-size: var(--fs-label); font-weight: 700; min-width: 52px; justify-content: center; transition: background-color var(--t-base) var(--ease-out), color var(--t-base) var(--ease-out); } .remaining-icon { color: var(--ink-3); transition: color var(--t-base) var(--ease-out); } .remaining-icon.check { display: none; } .remaining.bump { animation: bump var(--t-base) var(--ease-spring); } .remaining.done { background: var(--primary-soft); color: var(--primary); } .remaining.done .remaining-icon { color: var(--primary); } .remaining.done .remaining-icon.tile { display: none; } .remaining.done .remaining-icon.check { display: block; } /* 本包进度条:每关一段。灰 = 未到,淡紫 = 已过,实紫 = 当前 */ .level-progress { display: flex; gap: 3px; height: 4px; margin: var(--sp-2) var(--sp-1) 0; } .level-progress i { flex: 1; border-radius: 2px; background: var(--cell); transition: background-color var(--t-slow) var(--ease-out), opacity var(--t-slow) var(--ease-out); } .level-progress i.cleared { background: var(--primary); opacity: 0.4; } .level-progress i.current { background: var(--primary); opacity: 1; } .caption { height: 36px; margin-top: var(--sp-2); display: flex; align-items: center; justify-content: center; color: var(--ink-2); font-size: var(--fs-body); font-weight: 500; text-align: center; transition: opacity var(--t-base) var(--ease-out); } .caption:empty { opacity: 0; } .board-wrap { flex: 1; min-height: 0; position: relative; } #board { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; display: block; } .bottombar { display: flex; justify-content: center; gap: var(--sp-3); padding-top: var(--sp-3); } .gesture-help { margin: 0; padding-top: var(--sp-2); color: var(--ink-2); font-size: var(--fs-caption); text-align: center; } .action { flex: 1; max-width: 170px; height: 56px; border-radius: var(--r-lg); background: var(--surface-2); color: var(--ink); font-size: var(--fs-body); font-weight: 700; display: flex; align-items: center; justify-content: center; gap: var(--sp-1); } .action svg { color: var(--ink-2); } .action:active { background: var(--cell); } .action.pulse { animation: pulse 0.9s var(--ease-in-out) infinite; } .action:disabled { opacity: 0.4; pointer-events: none; } .toast { position: absolute; left: 50%; bottom: calc(var(--safe-bottom) + 128px); transform: translate(-50%, 8px); padding: 10px var(--sp-4); border-radius: var(--r-md); background: var(--overlay); color: #fff; font-size: var(--fs-label); font-weight: 600; width: max-content; max-width: calc(100% - 40px); text-align: center; opacity: 0; pointer-events: none; transition: opacity var(--t-base) var(--ease-out), transform var(--t-base) var(--ease-out); } .toast.show { opacity: 1; transform: translate(-50%, 0); } /* ---------- 动效 ---------- */ @keyframes screen-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 50% { transform: translateX(5px); } 75% { transform: translateX(-3px); } } @keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 165, 36, 0); } 50% { box-shadow: 0 0 0 5px rgba(245, 165, 36, 0.35); } } @keyframes bump { 0% { transform: scale(1); } 40% { transform: scale(1.14); } 100% { transform: scale(1); } } @keyframes breathe { 0%, 100% { box-shadow: inset 0 0 0 3px var(--primary), 0 0 0 0 rgba(101, 88, 245, 0); } 50% { box-shadow: inset 0 0 0 3px var(--primary), 0 0 0 6px rgba(101, 88, 245, 0.16); } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } } @media (min-width: 520px) { body { padding: var(--sp-6) 0; } #app { height: calc(100vh - 48px); height: calc(100dvh - 48px); border-radius: 28px; box-shadow: var(--shadow-frame); } }