/* Typeface stacks, one per FontClass in the manifest.
   Each stack names the authentic faces first — those ship with Windows/Office and may be
   used but not redistributed, so they are referenced, never bundled — then falls back to
   the OFL subset S4 writes into fonts/, and only then to a generic. Getting the class
   right matters more than any single name: a brush title set in 黑体 reads as a different
   game. */
@font-face {
  font-family: "UIStudio CJK Fallback";
  src: url("fonts/cjk-fallback.woff2") format("woff2");
  font-display: swap;
}
:root {
  --fc-fallback: "UIStudio CJK Fallback";
  --fc-brush: "华文行楷", STXingkai, "方正舒体", FZShuTi, "LXGW WenKai", "霞鹜文楷",
              "华文楷体", STKaiti, var(--fc-fallback), serif;
  --fc-kai: "华文楷体", STKaiti, "楷体", KaiTi, "LXGW WenKai", "霞鹜文楷",
            var(--fc-fallback), serif;
  --fc-clerical: "华文隶书", STLiti, "隶书", LiSu, var(--fc-fallback), serif;
  --fc-serif: "Noto Serif SC", "思源宋体", "宋体", SimSun, var(--fc-fallback), serif;
  --fc-sans: "Microsoft YaHei", "微软雅黑", "黑体", SimHei, system-ui, sans-serif;
  --fc-rounded: "Yuanti SC", "圆体", "HYQiHei", "Microsoft YaHei", system-ui, sans-serif;
  --fc-display: "Arial Black", Impact, "Microsoft YaHei", system-ui, sans-serif;
}
/* Calligraphic faces carry their own weight in the strokes. Asking the browser to embolden
   them synthesises a smear that destroys exactly the brush modulation we picked them for. */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; background: #0c0e14; color: #e7e9f0; font-family: system-ui, "Segoe UI", sans-serif; }

#viewport {
  position: fixed; inset: 0; display: grid; place-items: center; overflow: hidden;
}
/* #stage is sized to the design resolution and scaled with a CSS var (letterbox fit) */
#stage {
  position: relative; transform-origin: top left;
  background: #11131b; box-shadow: 0 0 40px rgba(0,0,0,.6);
}

/* default: keep each asset's aspect ratio (no stretching), centered */
.el { position: absolute; background-size: contain; background-position: center; background-repeat: no-repeat; }
/* containers/frames/full-bleed art are meant to fill their box */
.el.cat-background { z-index: 0; background-size: 100% 100%; }
.el.cat-panel { background-size: 100% 100%; }
.el.cat-decoration { background-size: 100% 100%; }
.el.cat-interactive { cursor: pointer; transition: filter .08s ease, transform .08s ease; }
.el.cat-interactive:hover { filter: brightness(1.15) saturate(1.1); }
.el.cat-interactive:active { filter: brightness(.9); transform: translateY(1px) scale(.99); }
/* when real per-state art exists, swap images instead of faking with filters */
.el.cat-interactive.has-states:hover { filter: none; }
.el.cat-interactive.has-states:active { filter: none; transform: none; }
.el.cat-interactive[data-disabled="1"] { filter: grayscale(1) brightness(.6); cursor: not-allowed; }

.el.cat-value {
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; text-shadow: 0 1px 2px rgba(0,0,0,.9); pointer-events: none;
  line-height: 1.05; overflow: hidden;
}
.el.cat-value.is-bar {
  background: rgba(20,16,30,.55);
  border: 1px solid rgba(255,255,255,.28);
  border-radius: 999px; overflow: hidden; padding: 0;
  box-shadow: inset 0 2px 4px rgba(0,0,0,.45);
  display: block;
}
.el.cat-value.is-bar .bar-fill {
  position: absolute; left: 0; top: 0; height: 100%; width: 50%;
  border-radius: 999px;
  background: linear-gradient(90deg, #43b6ff, #7cf58f);
  box-shadow: 0 0 10px rgba(80,200,255,.55) inset;
  transition: width .45s cubic-bezier(.22,.61,.36,1);
}
.el.cat-value.is-bar .bar-label {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 12px; color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,.95); pointer-events: none; white-space: nowrap;
}
/* --halo is set per element from the fill's luminance: a dark drop shadow under dark ink
   on a pale scroll just muddies the strokes, so light text gets the shadow and dark text
   gets a faint light lift instead. */
.el.cat-text { display: flex; align-items: center; justify-content: center;
  pointer-events: none; font-weight: 600; text-shadow: var(--halo, 0 1px 3px rgba(0,0,0,.9));
  white-space: pre-line; line-height: 1.1; overflow: hidden; text-align: center; }
.el.cat-text.is-longtext { align-items: flex-start; justify-content: flex-start;
  text-align: left; padding: 4px 6px; font-weight: 500; opacity: .92; }
/* Vertical CJK signage (hanging scrolls, banner spines) reads top-to-bottom. It is allowed
   to run past its box: the boxes come back clipped at the bottom, and clipping a title's
   last character is far worse than letting it breathe on an empty plate. */
.el.cat-text.is-vertical { writing-mode: vertical-rl; text-orientation: upright;
  white-space: nowrap; letter-spacing: .04em; overflow: visible;
  /* in vertical-rl the flex main axis runs downward, so justify-content is what pins the
     column to the top of its box — centring would push the overflow up out of the plate */
  justify-content: flex-start; align-items: center; }

/* dynamic feel: engine-driven, not baked. two mechanisms:
   1) particle layer for category=dynamic areas
   2) pulsing glow (drop-shadow follows alpha) for any element with a dynamic hint */
.el.cat-dynamic { pointer-events: none; }
.fx-layer { position: absolute; inset: 0; overflow: visible; }
.fx { position: absolute; left: 0; top: 0; will-change: transform, opacity; border-radius: 50%; }
.fx.ember { background: radial-gradient(circle, #fff2c4 0%, #ffcf6e 40%, rgba(255,170,60,.5) 60%, transparent 72%);
  box-shadow: 0 0 6px rgba(255,190,90,.8); }
.fx.dot { background: radial-gradient(circle, #d8fbff 0%, rgba(110,230,255,.7) 45%, transparent 72%);
  box-shadow: 0 0 6px rgba(90,220,255,.8); }
.fx.shimmer { filter: blur(5px);
  background: radial-gradient(circle, rgba(120,255,210,.55) 0%, transparent 70%); }
#stage.fx-off .fx-layer { display: none; }

.fx-glow { animation: fxGlow 2.4s ease-in-out infinite; }
.fx-glow.fx-warm { animation-name: fxGlowWarm; }
#stage.fx-off .fx-glow { animation: none !important; }
@keyframes fxGlow {
  0%,100% { filter: drop-shadow(0 0 2px rgba(120,230,255,.35)); }
  50%     { filter: drop-shadow(0 0 12px rgba(120,230,255,.9)); }
}
@keyframes fxGlowWarm {
  0%,100% { filter: drop-shadow(0 0 3px rgba(255,200,110,.4)); }
  50%     { filter: drop-shadow(0 0 14px rgba(255,200,110,.95)); }
}

/* ---- motion: living UI (JS drives transforms; CSS provides sheen/shadow) ---- */
.el.motion { will-change: transform; }
.el.cat-portrait.motion { transform-origin: 50% 100%; }

/* soft breathing ground shadow under a character */
.char-shadow {
  position: absolute; left: 50%; bottom: -6px; width: 62%; height: 12px;
  transform: translateX(-50%); border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0,0,0,.42) 0%, rgba(0,0,0,.18) 55%, transparent 75%);
  pointer-events: none; z-index: -1; will-change: transform, opacity;
}
/* blink frame cross-fades over the idle art; inherits the character's transform */
.blink-layer {
  position: absolute; inset: 0; background-size: contain; background-position: center;
  background-repeat: no-repeat; opacity: 0; transition: opacity .09s linear; pointer-events: none;
}
/* layered 2.5D cut-out rig: base + part layers share one contained canvas */
.rig-canvas { position: absolute; will-change: transform; }
.rig-layer { position: absolute; inset: 0; will-change: transform; backface-visibility: hidden; }
#stage.fx-off .rig-layer { transform: none !important; }
.rig-video { will-change: transform; backface-visibility: hidden; user-select: none; }

/* runtime notification badge (data-driven, not baked into art) */
.el.badge {
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 35% 30%, #ff6b6b 0%, #e53935 70%, #c62828 100%);
  color: #fff; font-weight: 800; line-height: 1; border-radius: 50%;
  border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,.4);
  text-shadow: 0 1px 1px rgba(0,0,0,.35); pointer-events: none;
  font-family: system-ui, "Segoe UI", Arial, sans-serif;
}

/* a countless badge is a bare dot: thinner ring, gentle pulse so it reads as "new" */
.el.badge.dot { border-width: 1.5px; animation: dot-pulse 1.8s ease-in-out infinite; }
@keyframes dot-pulse {
  0%, 100% { box-shadow: 0 2px 6px rgba(0,0,0,.4), 0 0 0 0 rgba(229,57,53,.55); }
  60%      { box-shadow: 0 2px 6px rgba(0,0,0,.4), 0 0 0 5px rgba(229,57,53,0); }
}

/* active navigation tab */
.el.nav-tab { transition: transform .16s ease, filter .16s ease; transform-origin: center bottom; }
.el.nav-tab.tab-active { transform: scale(1.14); filter: drop-shadow(0 4px 10px rgba(90,150,255,.55)) brightness(1.06); z-index: 5; }

/* transient action feedback */
.toast {
  position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(12px);
  background: rgba(20,18,32,.92); color: #fff; padding: 9px 16px; border-radius: 999px;
  font-size: 14px; font-weight: 600; letter-spacing: .3px; pointer-events: none;
  border: 1px solid rgba(255,255,255,.14); box-shadow: 0 8px 24px rgba(0,0,0,.4);
  opacity: 0; transition: opacity .2s ease, transform .2s ease; z-index: 9999;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

.fx.spark {
  background: radial-gradient(circle, #fffbe6 0%, #ffe38a 45%, rgba(255,205,90,.55) 62%, transparent 74%);
  box-shadow: 0 0 8px rgba(255,225,140,.95);
}

/* moving highlight sweep for filled surfaces (bars, primary buttons).
   NB: do NOT set position:relative here — it would override .el's absolute
   positioning (same specificity, later rule) and drop elements into normal
   flow, stacking bars/buttons and pushing them off their bbox. .el is already
   a positioned containing block for the ::after sweep. */
.fx-sheen { overflow: hidden; }
.fx-sheen::after {
  content: ""; position: absolute; top: 0; left: 0; height: 100%; width: 60%;
  background: linear-gradient(105deg, transparent 0%, rgba(255,255,255,.55) 50%, transparent 100%);
  transform: skewX(-18deg); pointer-events: none; mix-blend-mode: screen;
  animation: sheenSweep 3.4s ease-in-out infinite;
}
.el.cat-value.is-bar.fx-sheen::after { mix-blend-mode: screen; opacity: .85; }
@keyframes sheenSweep {
  0%   { left: -70%; }
  55%  { left: 130%; }
  100% { left: 130%; }
}
#stage.fx-off .fx-sheen::after { animation: none; opacity: 0; }
#stage.fx-off .char-shadow { transform: translateX(-50%) !important; }

/* debug boxes */
#stage.show-boxes .el { outline: 1px solid rgba(255,255,255,.25); }
#stage.show-boxes .el.cat-interactive { outline-color: #ff5050; }
#stage.show-boxes .el.cat-value { outline-color: #ffc828; }
#stage.show-boxes .el.cat-dynamic { outline-color: #b450ff; }

#hud {
  position: fixed; left: 12px; bottom: 12px; display: flex; gap: 16px; align-items: center;
  background: rgba(0,0,0,.55); padding: 8px 14px; border-radius: 10px; font-size: 13px;
  backdrop-filter: blur(6px);
}
#hud label { display: flex; gap: 6px; align-items: center; cursor: pointer; }
#hud-name { font-weight: 700; color: #8fd0ff; }

/* Typeface classes come last on purpose: they share specificity with .el.cat-text, so
   only source order lets them win. Calligraphic faces carry their weight in the strokes —
   letting the browser embolden them synthesises a smear that destroys the brush modulation
   they were chosen for. */
.el.ff-brush    { font-family: var(--fc-brush);    font-weight: 400; }
.el.ff-kai      { font-family: var(--fc-kai);      font-weight: 400; }
.el.ff-clerical { font-family: var(--fc-clerical); font-weight: 400; }
.el.ff-serif    { font-family: var(--fc-serif);    font-weight: 600; }
.el.ff-sans     { font-family: var(--fc-sans); }
.el.ff-rounded  { font-family: var(--fc-rounded); }
.el.ff-display  { font-family: var(--fc-display);  font-weight: 900; }

/* a control that carries its own bound caption still has to centre it over its plate */
.el.cat-interactive.cat-text-on-control {
  display: flex; align-items: center; justify-content: center;
  line-height: 1.05; text-align: center; text-shadow: var(--halo);
}
.el.cat-interactive.cat-text-on-control.is-vertical {
  writing-mode: vertical-rl; text-orientation: upright; white-space: nowrap;
}
