/* OneOneTyping v6 — on-screen keyboard.
 *
 * An Apple Magic Keyboard, drawn from edclub's key geometry.
 *
 * The POSITIONS come from assets/js/data/key-geometry.js, which is each key's
 * box in edclub's keyboard-ansi.svg (683.3 x 254). That matters: edclub's hand
 * artwork carries one whole-hand pose per key, drawn to sit on those exact
 * positions. Any board of our own — a flex row, a grid — has a different key
 * pitch, and the fingers drift up to 200px by the end of a row.
 *
 * The STYLING is Apple's: a silver aluminium body, white keycaps with a soft
 * bottom edge, SF-style labels, and Apple's modifier symbols.
 */

/* The tray is the same width as the passage column and shares its centre axis,
   so the keyboard and the text line up instead of drifting apart. */
.keyboard {
  position: relative;
  width: 100%;
  max-width: var(--type-width, 910px);
  margin: 0 auto;
  user-select: none;
}

.keyboard.is-compact {
  max-width: var(--type-width, 910px);
}

.kb-loading {
  padding: 40px 10px;
  text-align: center;
  color: var(--text-dim);
  font-size: 13px;
}

/* ---------------------------------------------------------------- tray
   Aluminium: a light vertical gradient, a hairline edge, and rounded corners
   that are much tighter than a web card's. */

.kb-tray {
  position: relative;
  padding: 6px;
  border: 1px solid rgba(0, 0, 0, 0.14);
  border-radius: 10px;
  background: linear-gradient(180deg, #e9eaec 0%, #dcdde0 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
  /* The hands are wider than the tray; the tray must not clip them. */
  overflow: visible;
}

.keyboard.is-compact .kb-tray {
  padding: 5px;
  border-radius: 9px;
}

/* --------------------------------------------------------------- board
   Absolutely positioned keys in the artwork's own units, scaled as one block. */

.kb-board {
  position: relative;
  z-index: 1;
  transform-origin: 0 0;
}

/* ---------------------------------------------------------------- keys */

.kb-key {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border-radius: 5px;
  /* Apple's keycap: white with a faint vertical falloff and a soft bottom
     edge. No hard border — the key reads as a moulded cap, not a button. */
  background: linear-gradient(180deg, #ffffff 0%, #f7f7f9 100%);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.16), inset 0 -1px 0 rgba(0, 0, 0, 0.04);
  color: #1d1d1f;
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue',
    'PingFang SC', system-ui, sans-serif;
  font-size: 13px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.01em;
  transition: background 0.1s linear, color 0.1s linear, box-shadow 0.1s linear;
}

.kb-label {
  display: block;
  transform: translateY(0.5px);
}

/* Symbol keys carry both legends, shifted above, like a real keycap
   (keyboard.js SHIFT_OF); in Chinese lessons the Chinese punctuation the
   key types takes their place (ZH_OF, setPunct('zh')). */
.kb-key.is-dual { flex-direction: column; gap: 1px; font-size: 12px; }
.kb-key.is-dual .kb-shift { font-size: 10.5px; opacity: 0.62; line-height: 1; }
.kb-zh { display: none; line-height: 1; }
.keyboard.is-zh .kb-key.has-zh .kb-shift,
.keyboard.is-zh .kb-key.has-zh .kb-label { display: none; }
.keyboard.is-zh .kb-key.has-zh .kb-zh { display: block; }
.keyboard.is-zh .kb-key.has-zh .kb-zh-shift { font-size: 10.5px; opacity: 0.62; }
.keyboard.has-layer .kb-key .kb-shift, .keyboard.has-layer .kb-key .kb-zh { display: none; }

/* Modifier keys print a symbol then the word, in Apple's order. */
.kb-key.is-word {
  font-size: 10.5px;
  letter-spacing: 0;
  color: #3a3a3c;
}

.kb-key.is-word .kb-glyph {
  font-size: 12px;
  line-height: 1;
}

.kb-key.is-word .kb-word {
  transform: translateY(0.5px);
}

/* Arrow keys carry only a symbol, a little larger. */
/* Arrow keys carry only a symbol, a little larger than a letter. */
.kb-key[data-key^='arrow'] .kb-label {
  font-size: 14px;
  line-height: 1;
}

/* ------------------------------------------------------------ key state */

/* The key to press now. */
.kb-key.is-next {
  background: linear-gradient(180deg, #3d8fd8 0%, #0d76c9 100%);
  box-shadow: 0 1px 0 #0a5c9e, inset 0 1px 0 rgba(255, 255, 255, 0.25);
  color: #ffffff;
}

/* Struck correctly. */
.kb-key.is-hit {
  background: linear-gradient(180deg, #86c281 0%, #71b16b 100%);
  box-shadow: 0 1px 0 #4f8a4a, inset 0 1px 0 rgba(255, 255, 255, 0.25);
  color: #ffffff;
}

/* Struck wrong, and held wrong until it is fixed. */
.kb-key.is-miss {
  background: linear-gradient(180deg, #d45555 0%, #c53030 100%);
  box-shadow: 0 1px 0 #992424, inset 0 1px 0 rgba(255, 255, 255, 0.2);
  color: #ffffff;
}

/* The key the learner actually pressed by mistake (keyboard.js mistake()):
   the board mirrors the hand, so the wrong key itself goes red and shakes,
   ringed so it reads apart from .is-miss (a passage error still to fix).
   !important: the keyboard skins style every idle key with a long :not()
   chain that would otherwise win. */
.keyboard .kb-key.is-wrongpress {
  background: linear-gradient(180deg, #e5484d 0%, #c9252b 100%) !important;
  color: #fff !important;
  box-shadow: 0 0 0 3px rgba(229, 72, 77, 0.35), 0 1px 0 #8f1d22 !important;
  animation: kb-wrong 0.32s ease-out;
  z-index: 3;
}
@keyframes kb-wrong {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  50% { transform: translateX(3px); }
  75% { transform: translateX(-2px); }
}
@media (prefers-reduced-motion: reduce) { .keyboard .kb-key.is-wrongpress { animation: none; } }

/* The whole row, for the "which row is it on" hint. */
.kb-key.is-row {
  background: linear-gradient(180deg, #f2f3f5 0%, #e7e9ec 100%);
}

.kb-key.is-next.is-row {
  background: linear-gradient(180deg, #3d8fd8 0%, #0d76c9 100%);
}

/* --------------------------------------------------------------- hands
   edclub's artwork, scaled with the board so the two stay locked together.
   The hands paint OVER the keys: the fingers rest on the caps, so the key you
   are aiming at is partly covered by the finger that reaches it. */

.kb-hands {
  position: absolute;
  left: 6px;
  top: 6px;
  z-index: 2;
  opacity: 0;
  pointer-events: none;
  transform-origin: center center;
  -webkit-mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
}

.kb-hands svg {
  display: block;
  height: 363px;
  width: auto;
}

/* ----------------------------------------------------------- dark theme */

html[data-theme='dark'] .kb-tray {
  border-color: rgba(255, 255, 255, 0.1);
  background: linear-gradient(180deg, #3a3d41 0%, #2c2f33 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}

html[data-theme='dark'] .kb-key {
  background: linear-gradient(180deg, #56595e 0%, #46494e 100%);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
  color: #f2f2f4;
}

html[data-theme='dark'] .kb-key.is-word {
  color: #c9c9ce;
}

html[data-theme='dark'] .kb-key.is-row {
  background: linear-gradient(180deg, #62666b 0%, #52565b 100%);
}

html[data-theme='dark'] .kb-hands {
  filter: invert(1) hue-rotate(180deg);
}

/* ------------------------------------------------------------ responsive */

.keyboard-mount {
  width: 100%;
  max-width: 100%;
  align-self: stretch;
}

/* A legible board cannot also fit a 390px phone, so the wrapper scrolls. */
@media (max-width: 640px) {
  .keyboard-mount {
    overflow-x: auto;
    padding-bottom: 6px;
  }

  .keyboard {
    max-width: none;
    width: 620px;
    margin: 0;
  }
}

/* keyboard.js inserts this under the board only when the mount really does
   overflow, so a phone is told the board scrolls instead of just seeing it
   cut off. Hidden everywhere else. */
.kb-scroll-hint {
  margin: 6px 0 0;
  color: var(--text-dim);
  font-size: 12px;
  text-align: center;
}

.kb-scroll-hint[hidden] { display: none; }

/* ============================================ optional finger colour chart
   Off by default (the settings panel turns it on). */
.keyboard {
  --z-pinky: #e2609f;
  --z-ring: #8a64e0;
  --z-middle: #1f86d8;
  --z-index: #3fae65;
  --z-thumb: #e0a132;
}

/* ---------------------------------------------------------- finger zones */

.kb-key[data-zone='pinky'] { --fin: var(--z-pinky); }
.kb-key[data-zone='ring'] { --fin: var(--z-ring); }
.kb-key[data-zone='middle'] { --fin: var(--z-middle); }
.kb-key[data-zone='index'] { --fin: var(--z-index); }
.kb-key[data-zone='thumb'] { --fin: var(--z-thumb); }

/* A pale wash and a coloured bottom lip: enough to read the zones at a glance
   without the board turning into a rainbow. Modifier keys stay plain. */
.keyboard.has-zones .kb-key[data-zone]:not(.is-word) {
  background: linear-gradient(180deg, #ffffff 0%, color-mix(in srgb, var(--fin) 9%, #ffffff) 100%);
  box-shadow: inset 0 -2.5px 0 color-mix(in srgb, var(--fin) 55%, transparent), 0 1px 0 rgba(0, 0, 0, 0.14);
}

/* The next key takes its finger's colour, so key and finger match below. */
.keyboard.has-zones .kb-key.is-next[data-zone] {
  background: var(--fin);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--fin) 70%, #000), 0 6px 14px -6px var(--fin);
  color: #ffffff;
}



html[data-theme='dark'] .keyboard.has-zones .kb-key[data-zone]:not(.is-word) {
  background: linear-gradient(180deg, #56595e 0%, color-mix(in srgb, var(--fin) 16%, #46494e) 100%);
}

/* Hit / miss flashes must win over the zone wash, or the white label sits on
   a near-white cap and the key goes blank. */
.keyboard.has-zones .kb-key.is-hit {
  background: linear-gradient(180deg, #86c281 0%, #71b16b 100%);
  box-shadow: 0 1px 0 #4f8a4a;
  color: #ffffff;
}

.keyboard.has-zones .kb-key.is-miss {
  background: linear-gradient(180deg, #d45555 0%, #c53030 100%);
  box-shadow: 0 1px 0 #992424;
  color: #ffffff;
}

.keyboard.has-zones .kb-key.is-next.is-miss {
  background: var(--fin);
}

/* ---------------------------------------------------- coach spotlight */

.keyboard.is-spotlit .kb-key:not(.is-spot):not(.is-got):not(.is-next) {
  opacity: 0.38;
  filter: saturate(0.6);
  transition: opacity 0.25s, filter 0.25s;
}

.kb-key.is-spot {
  z-index: 3;
  background: linear-gradient(180deg, #ffffff 0%, #eaf4fd 100%);
  box-shadow: 0 0 0 3px #0d76c9, 0 0 0 7px rgba(13, 118, 201, 0.22), 0 8px 18px -6px rgba(13, 118, 201, 0.6);
  color: #0a5ca6;
  animation: kb-spot 1.1s ease-in-out infinite;
}

@keyframes kb-spot {
  0%, 100% { box-shadow: 0 0 0 3px #0d76c9, 0 0 0 6px rgba(13, 118, 201, 0.22), 0 8px 18px -6px rgba(13, 118, 201, 0.6); transform: translateY(-1px); }
  50% { box-shadow: 0 0 0 3px #0d76c9, 0 0 0 12px rgba(13, 118, 201, 0.08), 0 10px 22px -6px rgba(13, 118, 201, 0.7); transform: translateY(-3px); }
}

.kb-key.is-got {
  z-index: 3;
  background: linear-gradient(180deg, #86c281 0%, #5ea85a 100%);
  box-shadow: 0 0 0 3px rgba(94, 168, 90, 0.35);
  color: #fff;
  animation: kb-got 0.4s cubic-bezier(0.3, 1.6, 0.5, 1);
}

@keyframes kb-got {
  0% { transform: scale(0.86); }
  60% { transform: scale(1.12); }
  100% { transform: scale(1); }
}

.kb-burst {
  position: absolute;
  left: 50%;
  top: 50%;
  pointer-events: none;
}

.kb-burst i {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px;
  border-radius: 2px;
  background: #f4c965;
  animation: kb-spark 0.6s ease-out forwards;
}

.kb-burst i:nth-child(3n) { background: #5ea85a; border-radius: 50%; }
.kb-burst i:nth-child(4n) { background: #4ca0eb; }

@keyframes kb-spark {
  from { opacity: 1; transform: translate(0, 0) scale(1); }
  to { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.4) rotate(120deg); }
}

@media (prefers-reduced-motion: reduce) {
  .kb-key.is-spot, .kb-key.is-got { animation: none; }
}

/* ---- 小鹤双拼 label layer (keyboard.setLayer) ----------------------------
   The letter moves to the top-left corner; the initial the key stands for
   (zh / ch / sh) sits top-right in purple; the finals it types are listed
   along the bottom. */
.keyboard.has-layer .kb-key { position: absolute; }
.keyboard.has-layer .kb-key .kb-label { position: absolute; top: 5px; left: 7px; font: 700 13px var(--font-sans); }
.kb-layer { position: absolute; inset: 0; pointer-events: none; }
/* the initial: a small purple tag, so zh / ch / sh read as one sound */
.kb-sm {
  position: absolute; top: 5px; right: 5px;
  padding: 0 4px;
  border-radius: 5px;
  background: #f1e6ff;
  color: #6b2bb3;
  font: 700 10.5px/15px var(--font-sans);
  font-style: normal;
  letter-spacing: 0.01em;
}
/* the finals: centred along the bottom, one line when they fit */
.kb-ym {
  position: absolute; right: 3px; bottom: 5px; left: 3px;
  color: #0a5ca6;
  font: 700 10.5px/1.1 var(--font-sans);
  font-style: normal;
  letter-spacing: 0.01em;
  text-align: center;
  white-space: nowrap;
}
.kb-ym b { margin: 0 1px; color: #7fa9d6; font-weight: 700; }
.kb-ym.is-stack { bottom: 3px; font-size: 9.5px; line-height: 1.02; }
.kb-key.is-next .kb-sm { background: rgba(255, 255, 255, 0.22); color: #fff; }
.kb-key.is-next .kb-ym, .kb-key.is-next .kb-ym b { color: #fff; }
html[data-theme='dark'] .kb-sm { background: rgba(201, 160, 255, 0.16); color: #d8bbff; }
html[data-theme='dark'] .kb-ym { color: #8cc4f5; }

/* =============================================== real key travel (P4b)
   A cap has a moulded skirt: it stands 2px proud of the tray at rest and
   sinks flush while its key is down (keyboard.js tracks the real keys).
   Down is fast, the return is a touch springy, like a scissor switch. */

.kb-key {
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.17), 0 3px 4px -2px rgba(0, 0, 0, 0.16), inset 0 -1px 0 rgba(0, 0, 0, 0.04);
  transition: background 0.1s linear, color 0.1s linear, box-shadow 0.12s ease-out,
    transform 0.14s cubic-bezier(0.3, 1.6, 0.5, 1);
}

.kb-key.is-next:not(.is-spot) { box-shadow: 0 2px 0 #0a5c9e, 0 3px 5px -2px rgba(10, 92, 158, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.25); }
.kb-key.is-hit { box-shadow: 0 2px 0 #4f8a4a, inset 0 1px 0 rgba(255, 255, 255, 0.25); }
.kb-key.is-miss { box-shadow: 0 2px 0 #992424, inset 0 1px 0 rgba(255, 255, 255, 0.2); }

.keyboard .kb-key.is-down {
  transform: translateY(2px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0.17), inset 0 1px 2px rgba(0, 0, 0, 0.14);
  filter: brightness(0.94);
  transition: background 0.05s linear, color 0.05s linear, box-shadow 0.03s linear, transform 0.03s linear, filter 0.03s linear;
  animation: none;
}

/* Held (Shift while the other hand types): stays down and says so. */
.keyboard .kb-key.is-held {
  z-index: 3;
  background: linear-gradient(180deg, #e9f2fc 0%, #d8e8fa 100%);
  box-shadow: 0 0 0 2px #0d76c9, 0 0 0 6px rgba(13, 118, 201, 0.16), inset 0 1px 2px rgba(0, 0, 0, 0.12);
  color: #0a5ca6;
}

.keyboard .kb-key.is-held::after {
  content: '按住';
  position: absolute;
  left: 50%;
  bottom: calc(100% + 7px);
  padding: 3px 7px 4px;
  border-radius: 6px;
  background: #0d76c9;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.08em;
  white-space: nowrap;
  transform: translateX(-50%);
  animation: kb-held-tag 0.18s ease-out;
}

@keyframes kb-held-tag {
  from { opacity: 0; transform: translate(-50%, 4px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

/* The finger that presses: the whole pose dips toward the key, the blue
   finger flares. A held pose stays dipped. */
.kb-hands .kb-pose {
  transform-box: view-box;
  transition: transform 0.12s ease-out;
}

.kb-hands .kb-pose.is-tap { animation: kb-tap 0.17s cubic-bezier(0.3, 0.7, 0.4, 1); }
.kb-hands .kb-pose.is-tap .st5 { animation: kb-tap-finger 0.24s ease-out; }
.kb-hands .kb-pose.is-hold { transform: translateY(2px); }
.kb-hands .kb-pose.is-hold .st5 { stroke-width: 4.5; }

@keyframes kb-tap {
  0% { transform: translateY(0); }
  38% { transform: translateY(2.6px); }
  100% { transform: translateY(0); }
}

@keyframes kb-tap-finger {
  0% { stroke-width: 3; }
  30% { stroke-width: 5.5; }
  100% { stroke-width: 3; }
}

html[data-theme='dark'] .kb-key { box-shadow: 0 2px 0 rgba(0, 0, 0, 0.55), 0 3px 4px -2px rgba(0, 0, 0, 0.4); }
html[data-theme='dark'] .keyboard .kb-key.is-held { background: linear-gradient(180deg, #2c4f73 0%, #244463 100%); color: #e6f1fc; }

@media (prefers-reduced-motion: reduce) {
  .kb-hands .kb-pose.is-tap,
  .kb-hands .kb-pose.is-tap .st5 { animation: none; }
}

/* =============================================== 键盘皮肤 (经验 rewards)
   practice-ui.js sets data-kbt on the keyboard root; the colours are in
   base.css. A skin paints the case and the resting caps: the next / hit /
   miss / held keys, the row tint and the finger-zone colours keep their
   meaning, so nothing a learner reads from the board changes. */
.keyboard[data-kbt] .kb-tray {
  border-color: var(--kbt-edge);
  background: var(--kbt-tray);
}
.keyboard[data-kbt]:not(.has-zones) .kb-key:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) {
  background: linear-gradient(180deg, var(--kbt-key) 0%, var(--kbt-key2) 100%);
  box-shadow: 0 2px 0 var(--kbt-shadow), 0 3px 4px -2px var(--kbt-shadow), inset 0 -1px 0 rgba(0, 0, 0, 0.04);
  color: var(--kbt-text);
}
.keyboard[data-kbt]:not(.has-zones) .kb-key.is-word:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) {
  background: linear-gradient(180deg, var(--kbt-mod) 0%, var(--kbt-mod2) 100%);
  color: var(--kbt-modtext);
}
.keyboard[data-kbt]:not(.has-zones) .kb-key[data-key='enter']:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) {
  background: var(--kbt-acc);
  color: var(--kbt-acctext);
}
.keyboard[data-kbt]:not(.has-zones) .kb-key.is-word .kb-ym,
.keyboard[data-kbt]:not(.has-zones) .kb-key .kb-sm { color: inherit; opacity: 0.75; }

/* 有个性的皮肤（等级奖励 2026-10-13）：不只换颜色，也换键帽的形状和光。
   只动「平常」的键（和上面同一串 :not），下一键 / 按对 / 按错的颜色照旧，读键盘的意思不变。 */
.keyboard[data-kbt='pixel'] .kb-tray { border-radius: 0; border-width: 3px; box-shadow: 4px 4px 0 #11112a; }
.keyboard[data-kbt='pixel'] .kb-key { border-radius: 0 !important; font-family: var(--font-mono, ui-monospace, monospace); font-weight: 700; }
.keyboard[data-kbt='pixel']:not(.has-zones) .kb-key:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { box-shadow: 0 3px 0 #11112a; }
/* 彩虹糖：每根手指管的键一个颜色 */
.keyboard[data-kbt='candy']:not(.has-zones) .kb-key[data-zone='pinky']:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { background: linear-gradient(180deg, #ffd1dc, #ffb8c9); }
.keyboard[data-kbt='candy']:not(.has-zones) .kb-key[data-zone='ring']:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { background: linear-gradient(180deg, #ffe7b8, #ffd98f); }
.keyboard[data-kbt='candy']:not(.has-zones) .kb-key[data-zone='middle']:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { background: linear-gradient(180deg, #d4f5c9, #bdeeae); }
.keyboard[data-kbt='candy']:not(.has-zones) .kb-key[data-zone='index']:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { background: linear-gradient(180deg, #cfe9ff, #b3dbff); }
.keyboard[data-kbt='candy']:not(.has-zones) .kb-key[data-zone='thumb']:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { background: linear-gradient(180deg, #e6d9ff, #d5c2ff); }
/* 圆键打字机：圆形的键帽、金属圈 */
.keyboard[data-kbt='typewriter'] .kb-key { border-radius: 999px !important; font-family: 'Courier New', Courier, monospace; font-weight: 700; }
.keyboard[data-kbt='typewriter']:not(.has-zones) .kb-key:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { box-shadow: 0 0 0 2px #9a958a, 0 3px 0 2px #55514a; }
/* 赛博、终端：字会发光 */
.keyboard[data-kbt='cyber'] .kb-key, .keyboard[data-kbt='terminal'] .kb-key { text-shadow: 0 0 6px currentColor; }
/* RGB 呼吸灯：键盘底下一圈流动的彩光 */
.keyboard[data-kbt='rgb'] .kb-tray { box-shadow: 0 0 0 2px #000, 0 0 18px 2px hsl(var(--rgb-h, 0) 90% 60% / 0.55); animation: kbt-rgb 6s linear infinite; }
.keyboard[data-kbt='rgb']:not(.has-zones) .kb-key:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { color: hsl(var(--rgb-h, 0) 95% 70%); box-shadow: 0 2px 0 #000, inset 0 -2px 0 hsl(var(--rgb-h, 0) 90% 55% / 0.6); }
@property --rgb-h { syntax: '<number>'; inherits: true; initial-value: 0; }
@keyframes kbt-rgb { to { --rgb-h: 360; } }
/* 透明键帽：磨砂玻璃 */
.keyboard[data-kbt='glass']:not(.has-zones) .kb-key:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); border: 1px solid rgba(255, 255, 255, 0.7); }
/* 星河流动：底色慢慢流动 */
.keyboard[data-kbt='galaxy'] .kb-tray { background-size: 300% 300%; animation: kbt-flow 14s ease-in-out infinite; }
@keyframes kbt-flow { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
@media (prefers-reduced-motion: reduce) { .keyboard[data-kbt='rgb'] .kb-tray, .keyboard[data-kbt='galaxy'] .kb-tray { animation: none; } }
/* ---- 会员奖励线的键盘 (store.js MEMBER_TRACK) ---- */
/* 流光极光、鎏金流光、全息：底色缓缓流动 */
.keyboard[data-kbt='m-aurora'] .kb-tray, .keyboard[data-kbt='m-holo'] .kb-tray { background-size: 300% 300%; animation: kbt-flow 12s ease-in-out infinite; }
.keyboard[data-kbt='m-aurora'] .kb-key { text-shadow: 0 0 6px rgba(62, 224, 181, 0.6); }
.keyboard[data-kbt='m-goldflow'] .kb-tray { background-size: 260% 100%; animation: kbt-sheen 5s linear infinite; }
@keyframes kbt-sheen { from { background-position: 130% 0; } to { background-position: -130% 0; } }
.keyboard[data-kbt='m-goldflow'] .kb-key { text-shadow: 0 0 5px rgba(255, 210, 110, 0.45); }
/* 霓虹脉冲：键的边一圈霓虹，粉和青交替呼吸 */
.keyboard[data-kbt='m-neon'] .kb-key { text-shadow: 0 0 6px currentColor; }
.keyboard[data-kbt='m-neon']:not(.has-zones) .kb-key:not(.is-next):not(.is-hit):not(.is-miss):not(.is-spot):not(.is-got):not(.is-held):not(.is-row) { box-shadow: 0 0 0 1px hsl(var(--neon-h, 310) 100% 65% / 0.75), 0 0 8px hsl(var(--neon-h, 310) 100% 60% / 0.45); animation: kbt-neon 3.2s ease-in-out infinite; }
@property --neon-h { syntax: '<number>'; inherits: true; initial-value: 310; }
@keyframes kbt-neon { 50% { --neon-h: 185; } }
/* 樱吹雪：花瓣从左往右飘过底板 */
.keyboard[data-kbt='m-sakura'] .kb-tray { animation: kbt-petals 18s linear infinite; }
@keyframes kbt-petals { to { background-position: 460px 190px, 340px 150px, 0 0; } }
/* 深空：星星闪 */
.keyboard[data-kbt='m-space'] .kb-tray { animation: kbt-twinkle 4s ease-in-out infinite; }
@keyframes kbt-twinkle { 50% { background-position: 2px 1px, 9px 12px, 0 0; filter: brightness(1.15); } }
/* 全息：键帽带一层随时间变的虹彩 */
.keyboard[data-kbt='m-holo'] .kb-key { backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); }
@media (prefers-reduced-motion: reduce) {
  .keyboard[data-kbt^='m-'] .kb-tray, .keyboard[data-kbt^='m-'] .kb-key { animation: none !important; }
}
