/* =============================================================================
   ATRIUM · 07 · MOTION
   =============================================================================
   The last 2%. Every micro-interaction, arrival and loading state in the
   product, tuned as one language.

   THE PERFORMANCE CONTRACT — this comes before anything aesthetic
   1. ANIMATE TRANSFORM AND OPACITY. Both are handled by the compositor and
      cost no layout and no paint. Anything animating width, height, top,
      margin or padding is a bug in this file.
   2. NO `filter` ON LARGE SURFACES. A filter promotes the element to its own
      layer and repaints its whole subtree each frame. A dialog is the worst
      possible thing to filter; opacity says the same thing for free.
      (This replaced a brightness() settle on every modal.)
   3. INFINITE ANIMATIONS ARE RATIONED. Only skeletons, the typing indicator
      and in-flight spinners loop, and only while something is genuinely
      pending. Nothing loops for decoration.
   4. `will-change` IS NOT A PERFORMANCE HINT. It permanently reserves a
      layer. It appears here exactly zero times; the engines promote what
      they need on their own.
   5. NOTHING ANIMATES DURING SCROLL. No scroll-linked effects, no parallax.

   THE LANGUAGE
     – things ARRIVE by coming into the light and LEAVE by dimming
     – entrances decelerate (--at-ease-out), exits accelerate (--at-ease-in)
     – exits are always shorter: waiting on a dismissal is dead time
     – a press is 1px into the page, never a scale of text or media
     – anything under the finger (sheets, scrollers) obeys physics 1:1
     – motion only ever answers: where from, where to, or "did you hear me?"

   THE SAFETY RULE
     No animation may gate content. A stalled or never-fired animation costs
     polish, never visibility. Feed cards in particular rest visible (§3).

   CONTENTS
     1. Focus                   6. Image & skeleton loading
     2. Dialogs & panels        7. Scrolling & momentum
     3. Feed arrival            8. Chat
     4. Comment expansion       9. Micro-interactions
     5. Notifications & counts 10. Reduced motion
   ========================================================================== */

/* =============================================================================
   1 · FOCUS
   -----------------------------------------------------------------------------
   The ring is drawn with `outline`, so it never enters layout and never
   fights an element's own shadow. It appears instantly — a focus ring that
   fades in is a focus ring you have already moved past.
   ========================================================================== */
:focus-visible {
    outline: 2px solid var(--at-accent);
    outline-offset: 2px;
}

/* Where a control is inside a scroll container or flush against a neighbour,
   the ring turns inward so it cannot be clipped. */
.v-rowitem:focus-visible,
.v-menu__item:focus-visible,
.v-tabbar__item:focus-visible,
.v-post__action:focus-visible { outline-offset: -2px; }

/* Reaching a control by keyboard scrolls it clear of the fixed chrome.
   Stated on the controls themselves, not on :focus-visible — the browser
   scrolls the element into view as focus moves, and a margin that only
   exists once focus has resolved is a margin that arrives too late. */
a, button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"]) {
    scroll-margin-block: var(--at-s8);
}

/* =============================================================================
   2 · DIALOGS & SHEETS
   -----------------------------------------------------------------------------
   One surface, two native presentations by posture:

     DESKTOP  a centred dialog fades in and gently scales up (0.96 → 1) — the
              Apple / Instagram alert feel: it resolves where it stands.
     MOBILE   a large surface is a BOTTOM SHEET: it slides up from the bottom
              edge on the iOS/Android sheet spring, the page dims behind it,
              and it can be swiped down to dismiss (shell script §ATRIUM-sheet
              in footer.php drives the drag; these classes are its rest and
              snap-back states).

   Bootstrap owns show/hide. The .fade lives on .modal (not .modal-dialog) so
   Bootstrap reads a non-zero exit duration off the element it measures; the
   dialog is then free to carry the scale / travel.
   ========================================================================== */
.modal.fade {
    opacity: 0;
    transition: opacity var(--at-t-slow) var(--at-ease-in);
}

.modal.fade.show {
    opacity: 1;
    transition: opacity var(--at-t-slower) var(--at-ease-out);
}

/* Centred dialogs everywhere: fade + a gentle scale. */
.modal.fade .modal-dialog {
    transform: scale(0.96);
    transition: transform var(--at-t-slow) var(--at-ease-in);
}

.modal.fade.show .modal-dialog {
    transform: none;
    transition: transform var(--at-t-slower) var(--at-ease-out);
}

/* Two surfaces opt out of the scale: the chat panel docks to an edge, and
   any full-screen viewer owns the whole screen — neither should breathe. */
.v-chatpanel.fade .modal-dialog,
.v-chatpanel.fade.show .modal-dialog { transform: none; }

.modal-backdrop.fade { transition: opacity var(--at-t-base) var(--at-ease-in); }
.modal-backdrop.show { transition: opacity var(--at-t-base) var(--at-ease-out); }

/* Offcanvas panels (notifications, chat list) are doors that travel. */
.offcanvas {
    transition: transform var(--at-t-slower) var(--at-ease-inout) !important;
}

/* --- MOBILE: bottom sheets ------------------------------------------------- */
@media (max-width: 47.98em) {
    /* Large surfaces slide from the bottom edge on the sheet spring instead
       of scaling. */
    .telegram-modal.fade .modal-dialog,
    .v-actions.fade .modal-dialog {
        transform: translateY(100%);
        transition: transform var(--at-t-slow) var(--at-ease-in);
    }

    .telegram-modal.fade.show .modal-dialog,
    .v-actions.fade.show .modal-dialog {
        transform: none;
        transition: transform var(--at-t-sheet) var(--at-ease-sheet);
    }

    /* While a finger is dragging the sheet, motion follows the hand 1:1 — no
       transition, or it would lag the drag. `!important` because the .show
       rule above is more specific, and nothing may transition mid-drag; the
       shell script sets the inline transform and toggles this class. */
    .modal .modal-dialog.is-dragging { transition: none !important; }

    /* Full-screen surfaces (chat, post viewer) neither scale nor slide as a
       sheet — they already own the screen. */
    #chatbox.fade .modal-dialog,
    #chatbox.fade.show .modal-dialog,
    [id^="postview"].fade .modal-dialog,
    [id^="postview"].fade.show .modal-dialog { transform: none; }
}

/* Dialog content settles a breath after its frame (desktop only — on a
   sliding sheet a second inner animation reads as a stutter). */
@media (min-width: 48em) {
    .modal.show .modal-body {
        animation: v-rise var(--at-t-slower) var(--at-ease-out) 40ms backwards;
    }
}

/* =============================================================================
   3 · FEED ARRIVAL
   -----------------------------------------------------------------------------
   First paint staggers (product.css §1). Below the fold, vesper-motion.js
   marks cards .v-below before they enter and .v-inview as they do.

   THIS STATE MUST FAIL OPEN. Everywhere else in ATRIUM things arrive by
   coming into the light, but a feed card's resting state may never be
   `opacity: 0` — if the IntersectionObserver callback is never delivered the
   post is simply gone, and that is not hypothetical: the callback does not
   fire at all in some engines. So the resting state is a few pixels of
   offset. An unobserved card is fully readable and merely un-settled.
   ========================================================================== */
.v-below { transform: translateY(8px); }

.v-below.v-inview {
    transform: none;
    transition: transform var(--at-t-slower) var(--at-ease-out);
}

/* `content-visibility: auto` is deliberately absent from .v-post: a skipped
   subtree has its animations PAUSED, so every card below the fold froze on
   the first frame of the arrival stagger. The saving was unmeasurable at
   this list size; the failure was total. */

/* =============================================================================
   4 · COMMENT EXPANSION
   -----------------------------------------------------------------------------
   post.js toggles [hidden]; display:none -> rendered restarts these
   animations, so the panel unfolds every time it opens with no JS change.
   The cascade is capped at four steps — past that a stagger stops reading as
   choreography and starts reading as lag.
   ========================================================================== */
[data-post-comment-panel]:not([hidden]) {
    animation: v-rise var(--at-t-slow) var(--at-ease-out);
}

[data-post-comment-panel]:not([hidden]) .v-comment {
    animation: v-rise var(--at-t-slow) var(--at-ease-out) backwards;
}

[data-post-comment-panel]:not([hidden]) .v-comment:nth-child(1) { animation-delay: 30ms; }
[data-post-comment-panel]:not([hidden]) .v-comment:nth-child(2) { animation-delay: 60ms; }
[data-post-comment-panel]:not([hidden]) .v-comment:nth-child(3) { animation-delay: 90ms; }
[data-post-comment-panel]:not([hidden]) .v-comment:nth-child(n+4) { animation-delay: 120ms; }

[data-post-comment-panel]:not([hidden]) .v-composer {
    animation: v-rise var(--at-t-slow) var(--at-ease-out) 100ms backwards;
}

/* A freshly posted comment settles in (custom.js prepends it). */
.v-post__comments .v-comment:first-child,
[id^="comment-section"] > :first-child {
    animation: v-rise var(--at-t-slow) var(--at-ease-out);
}

/* The comment field's send control fades in with text rather than appearing. */
.v-composer .add-comment {
    transition: opacity var(--at-t-fast) var(--at-ease-out),
                background-color var(--at-t-fast) var(--at-ease-out);
}

/* =============================================================================
   5 · NOTIFICATIONS & COUNTS
   ========================================================================== */
/* A panel's rows cascade as it opens — read top-to-bottom, like a letter.
   Capped at five, for the same reason as the comments. */
.offcanvas.show .v-notif,
.offcanvas.show .chatlist_item {
    animation: v-rise var(--at-t-slower) var(--at-ease-out) backwards;
}

.offcanvas.show .v-notif:nth-child(1),
.offcanvas.show .chatlist_item:nth-child(1) { animation-delay: 40ms; }
.offcanvas.show .v-notif:nth-child(2),
.offcanvas.show .chatlist_item:nth-child(2) { animation-delay: 70ms; }
.offcanvas.show .v-notif:nth-child(3),
.offcanvas.show .chatlist_item:nth-child(3) { animation-delay: 100ms; }
.offcanvas.show .v-notif:nth-child(4) { animation-delay: 130ms; }
.offcanvas.show .v-notif:nth-child(n+5),
.offcanvas.show .chatlist_item:nth-child(n+4) { animation-delay: 160ms; }

/* A count announces itself once — a settle, not a shout. Display toggles
   (#msgcounter) restart it naturally. */
.v-count { animation: v-pop var(--at-t-slow) var(--at-ease-spring); }

/* The unread dot breathes twice on arrival, then holds still. Two
   iterations, not infinite: a dot that pulses forever is a dot you learn to
   ignore, and it keeps a compositor layer alive for the life of the page. */
.v-notif__state .v-dot { animation: v-dot-breathe 1.6s var(--at-ease-inout) 2; }

@keyframes v-dot-breathe {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.25); }
}

/* =============================================================================
   6 · IMAGE & SKELETON LOADING
   -----------------------------------------------------------------------------
   vesper-motion.js marks not-yet-loaded media .v-img-loading and flips to
   .v-img-loaded on load or error — with a hard timeout, so a lost event can
   never leave content invisible. Untouched images are unaffected.
   ========================================================================== */
img.v-img-loading {
    opacity: 0;
    transform: scale(1.015);
}

img.v-img-loaded {
    opacity: 1;
    transform: none;
    transition: opacity var(--at-t-slower) var(--at-ease-out),
                transform var(--at-t-slower) var(--at-ease-out);
}

/* While a piece of MEDIA loads, its case shimmers.
   Scoped to the vitrine on purpose: this used to include .v-avatar, which
   meant a feed of forty posts ran forty simultaneous infinite background
   animations behind avatars barely bigger than the shimmer's own gradient.
   An avatar simply fades in. */
.v-vitrine:has(> img.v-img-loading) {
    background-image: var(--at-skeleton);
    background-size: 200% 100%;
    animation: v-shimmer 1.6s var(--at-ease-inout) infinite;
}

/* =============================================================================
   7 · SCROLLING & MOMENTUM
   ========================================================================== */
/* Native momentum wherever content scrolls inside a panel, and a scroll that
   reaches its end does not drag the page behind it. */
.modal-body,
.offcanvas-body,
.v-sheet__body,
.v-search__panel,
.v-scroller,
#user_chat,
.v-table-wrap {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

/* Horizontal rows glide and settle on an item rather than stopping mid-card. */
.v-profile__stats,
.v-scroller,
.post-share-row { scroll-snap-type: x proximity; }

.v-profile__stats > *,
.v-scroller > *,
.post-share-btn { scroll-snap-align: start; }

/* Panels scroll-pad their own headers; the document pads the fixed lintel
   (base.css). */
.offcanvas-body { scroll-padding-top: var(--at-s4); }

/* =============================================================================
   8 · CHAT
   -----------------------------------------------------------------------------
   custom.js applies .receive-anim per arriving message. Transform-only — an
   unstyled message is simply already seated.
   ========================================================================== */
.sent-anim    { animation: v-bubble-in var(--at-t-slower) var(--at-ease-out); transform-origin: bottom right; }
.receive-anim { animation: v-bubble-in var(--at-t-slower) var(--at-ease-out); transform-origin: bottom left; }

@keyframes v-bubble-in {
    from { transform: translateY(6px) scale(0.97); }
    to   { transform: none; }
}

/* Held bubble (double-click menu): opacity and transform only. */
.chat-bubble {
    transition: opacity var(--at-t-base) var(--at-ease-out),
                transform var(--at-t-base) var(--at-ease-out);
}

.typing-indicator span { animation-timing-function: var(--at-ease-inout); }

/* The send button acknowledges the press; it does not bounce. */
.send-message-btn { transition-property: background-color, box-shadow, opacity, translate; }

/* =============================================================================
   9 · MICRO-INTERACTIONS
   -----------------------------------------------------------------------------
   Each of these answers "did you hear me?" in under a tenth of a second.
   Icons carry the feedback; the surfaces they sit on stay still, because a
   moving container drags the eye and a moving glyph does not.
   ========================================================================== */
/* Rows press like matter. */
.v-rowitem__icon,
.v-menu__item .v-icon { transition: transform var(--at-t-fast) var(--at-ease-spring); }

.v-rowitem:active .v-rowitem__icon,
.v-menu__item:active .v-icon {
    transform: scale(0.92);
    transition-duration: var(--at-t-instant);
}

/* The chosen option's check pops once when a picker row renders as current. */
.v-rowitem__check { animation: v-pop var(--at-t-slow) var(--at-ease-spring); }

/* Gallery tiles respond under the finger. */
.gallery-wrapper { transition: transform var(--at-t-fast) var(--at-ease-spring); }
.gallery-wrapper:hover  { transform: translateY(-2px); }
.gallery-wrapper:active { transform: scale(0.98); }

/* Avatars in interactive rows lift a hair. */
.v-person > .v-avatar,
.v-notif > .v-avatar { transition: transform var(--at-t-base) var(--at-ease-out); }
.v-person--action:hover > .v-avatar,
.v-notif:hover > .v-avatar { transform: scale(1.04); }

/* The compose prompt's inline pill leans into the hover. */
.v-feedprompt .v-btn { transition: transform var(--at-t-base) var(--at-ease-spring); }
.v-feedprompt:hover .v-btn { transform: scale(1.03); }

/* Post actions: the glyph carries the feedback, the pill stays still. */
.v-post__action .fi { transition: transform var(--at-t-base) var(--at-ease-spring); }
.v-post__action:hover .fi { transform: scale(1.1); }

/* Stat plates tick their numerals — the dial acknowledges you. */
.v-statbtn b { transition: transform var(--at-t-base) var(--at-ease-spring); }
.v-statbtn:hover b { transform: translateY(-1px); }

/* The rail's current-place icon settles when the route changes. */
.v-rail__link:active .v-icon {
    transform: scale(0.9);
    transition-duration: var(--at-t-instant);
}

/* Current-place markers draw themselves in rather than blinking on. */
.v-tabbar__item.is-active::before,
.v-tabbar__item[aria-current="page"]::before {
    animation: v-underline-in var(--at-t-slow) var(--at-ease-out);
}

@keyframes v-underline-in {
    from { transform: translateX(-50%) scaleX(0.4); }
    to   { transform: translateX(-50%) scaleX(1); }
}

.v-tab[aria-selected="true"]::after,
.v-tab.is-active::after,
.btn-tab.active::after {
    animation: v-underline-in-static var(--at-t-slow) var(--at-ease-out);
    transform-origin: center;
}

@keyframes v-underline-in-static {
    from { transform: scaleX(0.4); }
    to   { transform: scaleX(1); }
}

/* The search panel resolves where it stands each time custom.js shows it
   (the display toggle restarts the animation — no JS involvement). */
.v-search__panel { animation: v-panel-in var(--at-t-base) var(--at-ease-out); }

@keyframes v-panel-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* The wallet balance settles into place when its dialog opens. */
.modal.show .v-wallet__balance {
    animation: v-rise var(--at-t-slower) var(--at-ease-out) 80ms backwards;
}

/* A veil lifts like fabric, not like a cut. */
.v-veil { transition: opacity var(--at-t-slower) var(--at-ease-in); }

.v-vitrine > img,
.v-vitrine > video {
    transition: filter var(--at-t-slower) var(--at-ease-out),
                transform var(--at-t-slower) var(--at-ease-out);
}

/* Chips settle their shadow in rather than snapping it on. */
.v-chip,
.market-chip,
.filter-button {
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                color var(--at-t-fast) var(--at-ease-out),
                box-shadow var(--at-t-base) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.v-footer__links a { transition: color var(--at-t-fast) var(--at-ease-out); }

/* NOTE: there is deliberately no blanket `transition-property` rule for
   .v-btn / .v-input / .v-navicon here. There used to be, and because those
   components declare a full `transition` shorthand with per-property
   durations, re-listing the properties re-mapped the duration list by
   index — transform silently inherited background-color's timing. Each
   component owns its own transition. */

/* =============================================================================
   10 · REDUCED MOTION — a designed state, not a fallback
   -----------------------------------------------------------------------------
   base.css collapses every duration. What matters here is that collapsing
   them must not leave anything INVISIBLE or mid-gesture: every state that
   an animation would have resolved is stated outright.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
    /* Media and cards are simply present. */
    img.v-img-loading { opacity: 1; transform: none; }
    .v-below, .v-below.v-inview { transform: none; }

    /* Nothing loops. */
    .v-vitrine:has(> img.v-img-loading) { background-image: none; animation: none; }
    .v-skeleton { background-image: none; }
    .v-notif__state .v-dot { animation: none; }
    .typing-indicator span { animation: none; opacity: 0.6; }

    /* Doors stop travelling; they are just open or closed. Dialogs neither
       scale (desktop) nor slide (mobile sheet) — they resolve on opacity
       alone, which base.css has already collapsed to instant. */
    .offcanvas { transition: none !important; }
    .modal.fade .modal-dialog,
    .modal.fade.show .modal-dialog { transform: none; }

    @media (max-width: 47.98em) {
        .telegram-modal.fade .modal-dialog,
        .v-actions.fade .modal-dialog,
        .telegram-modal.show .modal-dialog,
        .v-actions.show .modal-dialog { transform: none; }
    }

    /* Hover and press feedback becomes colour only — no travel, no scale. */
    .gallery-wrapper:hover,
    .gallery-wrapper:active,
    .v-person--action:hover > .v-avatar,
    .v-notif:hover > .v-avatar,
    .v-feedprompt:hover .v-btn,
    .v-post__action:hover .fi,
    .v-statbtn:hover b { transform: none; }
}
