/* Hylab Networks — motion layer.
   Goals: the interface should feel alive and physical without ever getting in the
   way. Rules of the house:
     1. entrances are one-shot (HylabMotion adds a class, removes it when done), so
        the one-second live refresh never replays them;
     2. transitions only touch transform/opacity/colour — never layout;
     3. everything is short (120-340ms) to keep the cursor and taps feeling direct;
     4. prefers-reduced-motion turns all of it off. */

@keyframes hy-rise { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }
@keyframes hy-fade { from { opacity: 0 } to { opacity: 1 } }
@keyframes hy-pop { from { opacity: 0; transform: scale(.965) } to { opacity: 1; transform: none } }
@keyframes hy-in-right { from { opacity: 0; transform: translateX(18px) } to { opacity: 1; transform: none } }
@keyframes hy-in-up { from { opacity: 0; transform: translateY(22px) } to { opacity: 1; transform: none } }
@keyframes hy-sheen { from { background-position: -140% 0 } to { background-position: 240% 0 } }
@keyframes hy-ring { 0% { box-shadow: 0 0 0 0 rgba(129, 201, 149, .5) } 70% { box-shadow: 0 0 0 7px rgba(129, 201, 149, 0) } 100% { box-shadow: 0 0 0 0 rgba(129, 201, 149, 0) } }
@keyframes hy-ripple { from { opacity: .3; transform: scale(.25) } to { opacity: 0; transform: scale(2.4) } }
@keyframes hy-tick { from { color: #eaf1ff; text-shadow: 0 0 14px rgba(138, 180, 248, .55) } to { color: inherit; text-shadow: none } }
@keyframes hy-draw { from { stroke-dashoffset: 1 } to { stroke-dashoffset: 0 } }
@keyframes hy-toast-bar { from { opacity: .85; transform: scaleX(1) } to { opacity: .25; transform: scaleX(0) } }

/* ---------- entrances ---------- */
.motion-enter > * {
  animation: hy-rise .34s cubic-bezier(.2, .7, .25, 1) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}
.motion-enter > *:nth-child(n + 13) { animation-delay: .54s }

.motion-rows > tr {
  animation: hy-rise .3s cubic-bezier(.2, .7, .25, 1) both;
  animation-delay: calc(var(--i, 0) * 22ms);
}

.motion-panel {
  animation: hy-rise .32s cubic-bezier(.2, .7, .25, 1) both;
  animation-delay: calc(var(--i, 0) * 70ms);
}

/* ---------- workspace push / pop ---------- */
/* 进入某台机器的管理页：内容从右滑进来；返回列表：从左滑回来。
   只用 transform/opacity（合成层一次变换），不动布局，也不做整页快照。 */
@keyframes hy-push-in { from { opacity: .45; transform: translate3d(28px, 0, 0) } to { opacity: 1; transform: none } }
@keyframes hy-pop-in { from { opacity: .45; transform: translate3d(-22px, 0, 0) } to { opacity: 1; transform: none } }
.motion-slide-right { animation: hy-push-in .26s var(--ease) both }
.motion-slide-left { animation: hy-pop-in .24s var(--ease) both }

/* ---------- dialogs, drawer, console ---------- */
dialog[open] { animation: hy-pop .22s cubic-bezier(.2, .7, .25, 1) both }
dialog[open]::backdrop { animation: hy-fade .22s ease both }
.drawer[open] { animation: hy-in-right .26s cubic-bezier(.2, .7, .25, 1) both }
.console-dialog[open] { animation: hy-in-up .24s cubic-bezier(.2, .7, .25, 1) both }
@media (max-width: 768px) { .console-dialog[open] { animation: hy-fade .2s ease both } }

/* the console toolbar and status chip settle in after the screen appears */
.console-bar { animation: hy-fade .3s ease .06s both }
/* These two centre themselves with translateX(-50%), so their entrance must not
   animate transform: with fill-mode "both" the animation's final `transform: none`
   replaces the centring and the pill sits off to one side. Fade only. */
.console-toast:not([hidden]), .console-hint:not([hidden]) { animation: hy-fade .2s ease both }

/* ---------- login screen ---------- */
#login:not([hidden]) .hero h1 { animation: hy-rise .5s var(--ease) both }
#login:not([hidden]) .hero p { animation: hy-rise .5s var(--ease) .08s both }
#login:not([hidden]) .login-card { animation: hy-pop .44s var(--ease) .1s both }

/* The workspace paints under a header that slides in; the page head follows. */
#workspace:not([hidden]) .page-head { animation: hy-rise .42s var(--ease) both }

/* ---------- toast ---------- */
/* #notice centres itself with translateX(-50%), so its entrance animates the
   independent `translate` property instead of `transform` (the same trap the
   console toast documents below). */
#notice { overflow: hidden }
#notice:popover-open { animation: hy-notice .32s cubic-bezier(.2, .7, .25, 1) both }
@keyframes hy-notice { from { opacity: 0; translate: 0 16px } to { opacity: 1; translate: 0 0 } }
/* 进度：一条光带沿提示条外框跑一圈（4.2s，正好等于提示条寿命 4200ms）。
   offset-path 的参考盒子是包含块（不是元素自己的盒子），所以给 ::after 一个
   46x2 的小盒子，它就会沿 #notice 的胶囊边缘走；offset-rotate 默认 auto，
   过两端半圆时自己转向。旧版底部那条直线留给不支持 inset() 的浏览器兜底。 */
#notice::after {
  content: ''; position: absolute; left: 0; top: 0; width: 46px; height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, rgba(138, 180, 248, 0), rgba(138, 180, 248, .45) 60%, var(--accent));
  offset-path: inset(2px round 999px);
  animation: hy-toast-beam 4.2s linear both;
}
@keyframes hy-toast-beam { from { offset-distance: 0% } to { offset-distance: 100% } }
@supports not (offset-path: inset(0 round 999px)) {
  #notice::after {
    left: 14px; right: 14px; top: auto; bottom: 5px; width: auto;
    offset-path: none;
    background: linear-gradient(90deg, var(--accent), rgba(138, 180, 248, .15));
    transform-origin: left center;
    animation: hy-toast-bar 4.2s linear both;
  }
}

/* ---------- trend sparks draw themselves ---------- */
/* The line carries stroke="currentColor", the shaded area under it carries
   fill="url(#…-shade)"; matching on those values keeps the two apart (matching
   on the bare `fill` attribute would also catch the line, which then loses its
   draw-in and is left as a one-pixel dash). */
.sparkline path[stroke] {
  stroke-dasharray: 1;
  animation: hy-draw .9s cubic-bezier(.3, .7, .3, 1) both;
}
.sparkline path[fill^="url("] { animation: hy-fade .7s ease .16s both }
.sparkline .spark-baseline { animation: hy-fade .5s ease both }

/* ---------- micro-interactions ---------- */
button, .drawer-item, nav button, .task-state, .ops-badge {
  transition: background-color .16s ease, border-color .16s ease, color .16s ease,
              box-shadow .2s ease, transform .12s cubic-bezier(.2, .7, .25, 1);
}
button:not(:disabled):active { transform: translateY(1px) scale(.985) }
.drawer-item:active { transform: translateX(2px) }
nav button.active { animation: hy-pop .2s ease both }

/* the fill of a usage bar is the same element across updates, so it can glide */
.usage-track i { transition: width .55s cubic-bezier(.25, .8, .3, 1), background-color .3s ease }
.trend-value, .usage-row strong, .live-rates b { transition: color .3s ease }

.ripple-host { position: relative; overflow: hidden }
.ripple {
  position: absolute; border-radius: 50%; pointer-events: none;
  background: currentColor; animation: hy-ripple .5s ease-out forwards;
}

.is-tick { animation: hy-tick .7s ease-out }

/* The header's live badge is a persistent node, so it can breathe forever.
   The dots inside the cards are rebuilt on every live message (about once a
   second), which would restart an animation mid-cycle and look like a stutter —
   they get a steady glow instead. */
.live-status[data-state=live] i, .status-dot.running {
  animation: hy-ring 2.6s ease-out infinite;
}
.vm-status.is-running i {
  box-shadow: 0 0 0 3px rgba(129, 201, 149, .16), 0 0 10px rgba(129, 201, 149, .45);
}

/* ---------- loading placeholders ---------- */
.empty.is-loading, .chart-loading.is-loading {
  background-image: linear-gradient(100deg, transparent 18%, rgba(255, 255, 255, .05) 36%, transparent 54%);
  background-size: 220% 100%;
  animation: hy-sheen 1.5s linear infinite;
}

/* ---------- loading skeletons ---------- */
.sk {
  border-radius: 9px;
  background-image: linear-gradient(100deg, rgba(255, 255, 255, .045) 20%, rgba(255, 255, 255, .1) 36%, rgba(255, 255, 255, .045) 52%);
  background-size: 220% 100%;
  animation: hy-sheen 1.4s linear infinite;
}
.sk-sm { height: 12px }
.sk-md { height: 15px }
.sk-lg { height: 26px; border-radius: 10px }
.sk-w20 { width: 20% } .sk-w35 { width: 35% } .sk-w45 { width: 45% }
.sk-w55 { width: 55% } .sk-w70 { width: 70% } .sk-w100 { width: 100% }
.sk-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 16px }
.vm-card.is-skeleton { pointer-events: none }
.vm-card.is-skeleton .vm-head { height: 12px }

@media (prefers-reduced-motion: reduce) { .sk { animation: none } }

/* ---------- desktop-only depth ---------- */
@media (hover: hover) and (pointer: fine) {
  .vm-card, .surface, .trend-card {
    transition: transform .26s cubic-bezier(.2, .7, .25, 1), border-color .26s ease, box-shadow .26s ease;
  }
  .vm-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 22px 44px -26px rgba(0, 0, 0, .8);
    border-color: rgba(138, 180, 248, .3);
  }
  button:not(:disabled):hover { box-shadow: 0 6px 16px -8px rgba(0, 0, 0, .6) }

}

/* ---------- respect the user's system setting ---------- */
@media (prefers-reduced-motion: reduce) {
  .motion-enter > *, .motion-rows > tr, .motion-panel,
  dialog[open], dialog[open]::backdrop, .drawer[open], .console-dialog[open],
  .console-bar, .console-toast, .console-hint, .ripple, .is-tick,
  .live-status i, .status-dot, nav button.active,
  #notice, #notice::after,
  #login .hero h1, #login .hero p, #login .login-card,
  #workspace.motion-slide-right, #workspace.motion-slide-left,
  #workspace .page-head,
  .sparkline path {
    animation: none !important;
  }
  .empty.is-loading, .chart-loading.is-loading { animation: none !important; background-image: none }
  button, .drawer-item, .vm-card, .surface, .trend-card, .usage-track i, .trend-value {
    transition: none !important;
  }
  button:not(:disabled):active, .drawer-item:active { transform: none }
  .vm-card:hover { transform: none }
}
