/* ── Motion tokens + samples (desktop icon, page enter, chat message, sheet, confirm dialog) ──
   One place for every duration / curve / distance, so pages do not each invent their own. Only transform + opacity are animated (cheap on weak phones),
   nothing loops, and a new animation replaces the running one (no stacking). Loaded AFTER the inline <style> of index.html on purpose. */
:root{
  /* 2026-10 interaction pass (claude/interaction-motion-decisions-2026-10-14.md): a "Q 弹" press (deeper dip, a small bounce on release)
     and a "depth" language for pages and dialogs (they float in from slightly smaller / larger instead of sliding sideways) */
  --mo-fast:90ms;                        /* press feedback, small fades */
  --mo-base:180ms;                       /* page enter, chat message */
  --mo-slow:240ms;                       /* sub-pages, sheets and dialogs */
  --mo-release:380ms;                    /* letting go of a pressed control: long enough for the bounce to read */
  --mo-ease-out:cubic-bezier(.22,.9,.3,1);
  --mo-ease-spring:cubic-bezier(.2,.9,.3,1);     /* panels: a quick settle */
  --mo-ease-bounce:cubic-bezier(.34,1.56,.64,1); /* release: overshoots a little and settles */
  --mo-press-scale:.9;                   /* icons */
  --mo-press-scale-sm:.9;                /* small buttons */
  --mo-depth-in:.95;                     /* a page you go INTO grows from this */
  --mo-depth-back:1.04;                  /* a page you come BACK to settles from this */
  --mo-dialog-from:1.06;                 /* a centred dialog floats in from slightly larger */
  --mo-shift:5px;                        /* how far things travel while they fade in */
  --mo-sheet-shift:18px;
}
@media (prefers-reduced-motion:reduce){:root{--mo-shift:0px;--mo-sheet-shift:0px;--mo-press-scale:1;--mo-press-scale-sm:1;--mo-depth-in:1;--mo-depth-back:1;--mo-dialog-from:1}}

@keyframes moPageIn{from{opacity:0;transform:scale(var(--mo-depth-in))}to{opacity:1;transform:none}}
@keyframes moRise{from{opacity:0;transform:translateY(var(--mo-shift))}to{opacity:1;transform:none}}
@keyframes moMsgInA{from{opacity:0;transform:translate(calc(var(--mo-shift) * -.7),var(--mo-shift)) scale(.98)}to{opacity:1;transform:none}}
@keyframes moMsgInU{from{opacity:0;transform:translate(calc(var(--mo-shift) * .7),var(--mo-shift)) scale(.98)}to{opacity:1;transform:none}}
@keyframes moFade{from{opacity:0}to{opacity:1}}
@keyframes moSheetIn{from{opacity:.6;transform:translateY(var(--mo-sheet-shift))}to{opacity:1;transform:none}}
@keyframes moDialogIn{from{opacity:0;transform:scale(var(--mo-dialog-from))}to{opacity:1;transform:none}}

/* 1. desktop: icons and dock icons sink a little when pressed and come back softly */
.desk-item.desk-app .app-icon,.dock-icon{transition:transform var(--mo-release) var(--mo-ease-bounce)}
.desk-item.desk-app .app-btn:active .app-icon,.desk-dock-item .app-btn:active .dock-icon{transform:scale(var(--mo-press-scale));transition-duration:70ms}
/* opening an app / going back to the desktop / switching tabs: the view that becomes visible fades in and rises a few pixels (the old one is display:none, so nothing overlaps) */
.view.active{animation:moPageIn var(--mo-base) var(--mo-ease-out) backwards}

/* 2. chat: a new message comes in from its own side, a short distance; the send button gets the same press feel */
.msg-row.a{animation:moMsgInA var(--mo-base) var(--mo-ease-out) backwards}
.msg-row.u{animation:moMsgInU var(--mo-base) var(--mo-ease-out) backwards}
.msg-row.sys{animation:moFade var(--mo-base) ease-out backwards}
.send-b{transition:transform var(--mo-release) var(--mo-ease-bounce),background var(--mo-fast)}
.send-b:active{transform:scale(var(--mo-press-scale-sm));transition-duration:var(--mo-fast)}

/* 3. popups: the bottom sheet rises with a soft settle, the confirm dialog pops in; the dim layer only fades */
.sheet-ov,.profile-ov{animation:moFade var(--mo-fast) ease-out backwards}
.sheet,.profile-sheet{animation:moSheetIn var(--mo-slow) var(--mo-ease-spring) backwards}
.app-confirm-ov{animation:moFade var(--mo-fast) ease-out backwards}
.app-confirm{animation:moDialogIn var(--mo-slow) var(--mo-ease-out) backwards}
/* the App's other centred dialogs (they reuse .sheet but sit in the middle) float in the same way instead of each one's own small pop */
.sheet.cal-dialog,.sheet.character-dialog,.sheet.dating-dialog,.sheet.delivery-share-dialog,.sheet.w-edit,.sheet.mm-dialog,.sheet.pd-dialog,.sheet.period-record-sheet{animation:moDialogIn var(--mo-slow) var(--mo-ease-out) backwards}
.app-confirm-ov.is-closing .app-confirm{transform:scale(1.03);opacity:.9;transition:transform var(--mo-fast) ease-in,opacity var(--mo-fast) ease-in}

/* ── roll-out ── */
@keyframes moTabIn{from{opacity:0}to{opacity:1}}   /* tabs: a plain cross-fade, no movement */
/* sub-pages (js/motion.js adds .mo-enter only when the page title changed) */
.app-body.mo-enter{animation:moPageIn var(--mo-base) var(--mo-ease-out) backwards}
/* tab panes inside an app (display toggled by .active, so the animation restarts on every switch) */
.wechat-panel.active{animation:moTabIn var(--mo-base) var(--mo-ease-out) backwards}
/* lists that already animated their cards: same tokens instead of their own numbers */
.m-card{animation:moRise var(--mo-base) var(--mo-ease-out) backwards}
/* toast: same distance / duration as everything else */
.tiny-toast{transform:translateY(var(--mo-shift));transition:opacity var(--mo-base) ease-out,transform var(--mo-base) var(--mo-ease-out)}
.tiny-toast.show{transform:none}
/* press feedback for buttons, chips and cards (one scale for all) */
.s-btn,.theme-mode-btn,.theme-inline-btn,.choice-chip,.app-back,.tab-bar button,.aic-save-pill,.desk-card,.m-btn,.emoji-opt,.mood-opt{transition:transform var(--mo-release) var(--mo-ease-bounce),background-color var(--mo-fast),color var(--mo-fast),box-shadow var(--mo-fast)}
.s-btn:not(:disabled):active,.theme-mode-btn:active,.theme-inline-btn:active,.choice-chip:active,.app-back:active,.tab-bar button:active,.aic-save-pill:not([aria-disabled="true"]):active,.m-btn:active,.emoji-opt:active,.mood-opt:active{transform:scale(var(--mo-press-scale-sm));transition-duration:var(--mo-fast)}
.desk-card:active{transform:scale(var(--mo-press-scale));transition-duration:var(--mo-fast)}

/* ── opening / closing an App (js/motion.js zooms it from / into its desktop icon; transform + opacity only) ──
   .mo-zoom-hold keeps the view that just stopped being .active painted until the animation ends (the home screen's own rule is in index.html);
   .mo-zoom-top puts the App above the desktop; .mo-zoom-under: the desktop does not run its own page fade meanwhile. Held views take no clicks. */
.view.mo-zoom-hold{display:flex;pointer-events:none}
.view.mo-zoom-top{z-index:30;will-change:transform,opacity;animation:none}
#v-home.mo-zoom-under{animation:none}
/* after a zoom: the view that stayed on screen must not replay its page fade when the zoom classes come off (that was a flash) */
.view.mo-zoom-done{animation:none}
#v-home.mo-zoom-hold{z-index:1}

/* ── leaving (js/app-motion.js ghostOut): the lifeless copy of a closing page / sheet / dialog. It is a freshly inserted element, so its own
   entry animations would replay — they are switched off; it takes no clicks and is removed when its exit animation ends. ── */
.mo-ghost,.mo-ghost *{animation:none!important;transition-property:none!important;pointer-events:none!important}

/* ── sub-pages inside an App say which way you went, in depth: going in, the new page grows from slightly smaller; going back (a back button /
   the system back), the page you return to settles from slightly larger. js/motion.js still decides WHEN (page title changed); js/app-motion.js
   marks <html class="mo-back"> for a moment after a back tap. */
@keyframes moSubIn{from{opacity:0;transform:scale(var(--mo-depth-in))}to{opacity:1;transform:none}}
@keyframes moSubBack{from{opacity:0;transform:scale(var(--mo-depth-back))}to{opacity:1;transform:none}}
html .app-body.mo-enter{animation-name:moSubIn;animation-duration:var(--mo-slow)}
html.mo-back .app-body.mo-enter{animation-name:moSubBack}
/* full pages opened over an App: float in like any sub-page, and shrink away when closed (ghostOut). The calendar's own side-slide is replaced too. */
.mm-page-full,.pd-page,.crs-page,.calendar-form-view{animation:moSubIn var(--mo-slow) var(--mo-ease-out) backwards}
.app-view.calendar-app{animation:none}   /* opening 日历 is the depth open in js/app-motion.js; its old slide on top of that was a second motion */

/* ── chat long press (batch 2): the blurred layer fades in, the pressed bubble's copy lifts above it with a bounce, the menu pops from the
   corner nearest the bubble (index.html openChatBubbleMenu sets its transform-origin) ── */
@keyframes moLift{from{transform:scale(1)}to{transform:scale(1.04)}}
@keyframes moMenuPop{from{opacity:0;transform:scale(.85)}to{opacity:1;transform:none}}
.chat-bubble-pop-ov{animation:moFade var(--mo-base) ease-out backwards}
.chat-bubble-lift{position:fixed;margin:0;box-sizing:border-box;pointer-events:none;z-index:1;animation:moLift var(--mo-release) var(--mo-ease-bounce) forwards}
.chat-bubble-pop-ov .chat-bubble-pop{z-index:2;animation:moMenuPop var(--mo-release) var(--mo-ease-bounce) backwards}
/* the title while 对方正在输入… shows (js/app-motion.js swaps the text) */
#wechat-chat-title.is-typing{color:var(--ink2);font-weight:500;animation:moFade var(--mo-base) ease-out}

/* ── switches (batch 3): the knob stretches while held and springs over on release (the "Q 弹" switch chosen 2026-10-14) ──
   The stretch uses the separate `scale` property, so it never fights the many per-App rules that set the knob's transform; it grows from the
   side the knob rests on. */
.toggle::after{transition:transform var(--mo-release) var(--mo-ease-bounce),scale var(--mo-release) var(--mo-ease-bounce);transform-origin:0 50%}
.toggle.on::after{transform-origin:100% 50%}
.toggle:active::after{scale:1.3 1;transition-duration:var(--mo-fast)}

/* ── forms (batch 4): the fields already have their title above the box (the grey text inside is only an example), so instead of a
   floating label the title of the field you are typing in takes the accent colour ── */
:where(div,label,section,p):has(> :is(input,textarea,select):focus) > :is(label,.label,[class$="-label"],[class$="-lbl"]),
:where(div,section):has(> :is(input,textarea,select):focus) > label > span,
label:has(> :is(input,textarea,select):focus) > span{color:var(--pri-deep,var(--pri));transition:color var(--mo-base) ease}
