/* ============================================================================
   motion.css — one shared motion vocabulary for every portal.
   Added 2026-08-10 ("modernize … implement animations to delete sweeps,
   shrink/expand, etc").

   PRINCIPLES
   • Motion explains a change; it never decorates. Every animation here maps to
     a real event: something arrived, left, opened, closed, or was saved.
   • Short and interruptible. 140–320ms; nothing blocks a click.
   • Enter animations are CSS-only and attach to the row classes the app
     already renders, so existing screens gain them without touching their
     render code. Exit animations need JS (the element must survive long
     enough to animate) — see js/motion.js.
   • GPU-friendly properties only (opacity / transform), plus grid-rows for
     the height collapse, which is the one modern way to animate to auto.
   • Everything is disabled under prefers-reduced-motion.
   ========================================================================== */

/* ── Enter: list rows, cards, notifications ──────────────────────────────── */
@keyframes mpcRowIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

/* Applied to the containers the app already uses.
   `backwards`, NOT `both` — and this is load-bearing, not a style preference.
   A `forwards` fill keeps the animation in effect forever, and the final
   `transform: none` keyframe then resolves to the identity MATRIX rather than
   the keyword `none`. Any transform other than `none` makes the element the
   containing block for its position:fixed descendants, and every per-session
   modal is rendered INSIDE its .lesson-item — so with `both` each modal was
   sized to its own row instead of the viewport (owner-reported 2026-08-10:
   "modals for mobile view ... does not cover the whole screen").
   `backwards` still holds the start frame through the stagger delay, which is
   the only thing the fill was ever needed for; the `to` frame is identical to
   the natural resting state, so nothing is lost by not filling forwards. */
.lesson-item,
.cashbox-row,
.kebab-row,
.notif-item,
.stat-box,
.branch-card,
.split-list-content > div,
.payout-item {
    animation: mpcRowIn .26s cubic-bezier(.22, .8, .3, 1) backwards;
}

/* A long list should feel like it lands, not like a wave — cap the stagger at
   a handful of rows so the 80th item is not still waiting to appear. */
.lesson-item:nth-child(1),  .cashbox-row:nth-child(1),  .notif-item:nth-child(1)  { animation-delay: 0ms; }
.lesson-item:nth-child(2),  .cashbox-row:nth-child(2),  .notif-item:nth-child(2)  { animation-delay: 22ms; }
.lesson-item:nth-child(3),  .cashbox-row:nth-child(3),  .notif-item:nth-child(3)  { animation-delay: 44ms; }
.lesson-item:nth-child(4),  .cashbox-row:nth-child(4),  .notif-item:nth-child(4)  { animation-delay: 66ms; }
.lesson-item:nth-child(n+5), .cashbox-row:nth-child(n+5), .notif-item:nth-child(n+5) { animation-delay: 84ms; }

/* ── Exit: the delete "sweep" ────────────────────────────────────────────── */
/* Slides out to the trailing edge, fades, then collapses its own height so the
   rows below glide up instead of snapping. Driven by js/motion.js. */
/* `both` is correct HERE (unlike the enter animations): the collapsed final
   frame must be held until js/motion.js removes the node, and a row on its way
   out has no fixed-position descendants left to scope. */
.mpc-sweep-out {
    animation: mpcSweepOut .34s cubic-bezier(.4, 0, .7, .2) both;
    pointer-events: none;   /* no double-clicking a row that is leaving */
    overflow: hidden;
}
@keyframes mpcSweepOut {
    0%   { opacity: 1; transform: none; max-height: var(--mpc-h, 400px); margin-bottom: var(--mpc-mb, 0px); }
    55%  { opacity: 0; transform: translateX(46px); max-height: var(--mpc-h, 400px); margin-bottom: var(--mpc-mb, 0px); }
    100% { opacity: 0; transform: translateX(46px); max-height: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; border-width: 0; }
}

/* A row that was just saved/updated — a brief brand wash, no layout change. */
.mpc-flash-ok { animation: mpcFlashOk 1s ease-out 1; }
@keyframes mpcFlashOk {
    0%   { background-color: rgba(255, 193, 0, .30); }
    100% { background-color: transparent; }
}

/* ── Shrink / expand: collapsible sections ──────────────────────────────── */
/* grid-template-rows 0fr→1fr is the one technique that animates to CONTENT
   height without measuring in JS. The inner wrapper must be overflow:hidden
   for the rows to clip while collapsed. */
.mpc-collapse {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .28s cubic-bezier(.22, .8, .3, 1),
                opacity .22s ease;
    opacity: 0;
}
.mpc-collapse > * { overflow: hidden; min-height: 0; }
.mpc-collapse.is-open { grid-template-rows: 1fr; opacity: 1; }

/* Caret that points down when open. Pair with .is-open on the toggle. */
.mpc-caret { transition: transform .24s cubic-bezier(.22, .8, .3, 1); }
.mpc-caret.is-open { transform: rotate(180deg); }

/* ── While a modal is open, the page behind it does not scroll ───────────── */
/* Owner-reported 2026-09-16 from an iPhone (BUG-0080): "we can scroll past the
   screen" — with a modal up, dragging anywhere still scrolled the document
   underneath and then rubber-banded past its end, leaving a band of empty page
   below a fixed, full-viewport overlay. There was no scroll lock anywhere: the
   five portals' openModal/closeModal only ever touched the overlay's class.
   `mpc-scroll-lock` is put on <body> by js/modalExit.js, which already watches
   every overlay's open class — so this covers the ~114 direct close sites and
   the inline onclick ones for free, and it cannot get stuck, because the class
   is re-derived from the DOM on every open and every close.
   ON <body>, NOT <html>, AND THAT IS DELIBERATE: with the root element's
   overflow left `visible`, the body's overflow is what propagates to the
   viewport — the same mechanism the `overflow-x: hidden` in each portal's own
   body rule already relies on, so this is a path known to work on the iPhones
   that reported it. The card gets `contain` so that scrolling its inner list
   to the end does not chain out into a bounce.
   THE BOUNCE ITSELF NEEDS <html> (owner, same evening: "I can still scroll past
   the dimmed background on short modals"). `overscroll-behavior` is NOT carried
   from <body> to the viewport the way `overflow` is — the spec takes only the
   root element's value — so the body rule never stopped anything. modalExit.js
   now puts the class on <html> too, and the rule that matters is the html one.
   It is still not enough on its own (a short card cannot scroll, and older
   iPhones ignore overscroll-behavior), which is why modalExit.js also cancels a
   drag that nothing in the modal can use. */
body.mpc-scroll-lock {
    overflow: hidden;
    overscroll-behavior: none;
}
html.mpc-scroll-lock {
    overscroll-behavior: none;
}
/* iPHONE / iPAD ONLY — js/modalExit.js adds this class only on iOS. Safari could
   draw every fixed element (the modal, the chat button, the Log Book tab)
   against an old scroll position once the page was frozen with overflow alone,
   so a dialog opened far down a tracker came up cut off (owner screenshot,
   2026-09-16 20:11). Pinning the body at its own scroll position leaves the
   document nothing to be scrolled by. --mpc-pin-top is set to -scrollY by the
   script, and the position is put back when the pin comes off. */
body.mpc-scroll-lock-pin {
    position: fixed;
    top: var(--mpc-pin-top, 0px);
    left: 0;
    right: 0;
    width: 100%;
    overflow: hidden;
}
.modal-overlay .alert-card,
.modal-overlay .modal-card {
    overscroll-behavior: contain;
}

/* FULL-SCREEN PANELS (owner, 2026-09-16): "same with all other floating screen
   like the chat box and logbook". js/modalExit.js puts `mpc-scroll-lock-panel`
   on <body> while a `.chat-panel` or `#lbFloat` is open. The lock applies ONLY at
   the portals' own full-screen breakpoint (both panels are `max-width: 992px`
   full-screen in admin.html and teacher.html): above it they are small side
   panels, and the page beside them has to keep scrolling. Same mechanism as the
   modal lock above: body for overflow, html for the bounce. */
@media (max-width: 992px) {
    body.mpc-scroll-lock-panel {
        overflow: hidden;
        overscroll-behavior: none;
    }
    html.mpc-scroll-lock-panel {
        overscroll-behavior: none;
    }
}
/* The panels' own scrolling lists stop at their ends instead of chaining out
   into the page — at every width, since chaining is wrong on a desktop too. */
.chat-panel .chat-body,
.chat-panel .chat-directory,
.chat-panel #chatDirectory,
#lbFloatBody {
    overscroll-behavior: contain;
}

/* ── Modals: scale-in rather than appear ─────────────────────────────────── */
.modal-overlay { transition: opacity .18s ease; }
/* `.open` AS WELL AS `.active`. partner.html opens its modals with .open — it
   is the one page that does — so it has never matched these rules and has
   never had the entrance animation at all. Adding the selector is a smaller
   and safer change than renaming the class it keys on in eight places. */
.modal-overlay.active > .alert-card,
.modal-overlay.active > .modal-card,
.modal-overlay.active > div:first-child,
.modal-overlay.open > .alert-card,
.modal-overlay.open > .modal-card,
.modal-overlay.open > div:first-child {
    /* `backwards` for the same containing-block reason as the row enter above:
       a card left filling forwards holds an identity matrix, which would scope
       any position:fixed element nested inside a modal (a confirm raised from a
       modal, a fixed dropdown) to the card. */
    animation: mpcModalIn .22s cubic-bezier(.22, .8, .3, 1) backwards;
}
@keyframes mpcModalIn {
    from { opacity: 0; transform: translateY(10px) scale(.975); }
    to   { opacity: 1; transform: none; }
}

/* ── Modals: leaving ─────────────────────────────────────────────────────
   A modal that scales in and then vanishes on a frame feels broken in a way
   that is hard to name — the eye tracked something arriving and then lost it.
   .modal-overlay is `visibility: hidden` at rest, so an exit CANNOT be done by
   removing .active alone: the card is already gone before any animation could
   run. closeModal() therefore adds .mpc-closing, which holds visibility just
   long enough for the reverse, and removes it on a timer.

   `forwards` IS correct here and is not a contradiction of the rule above.
   The containing-block hazard is about an element that keeps a transform while
   people go on using the page; this card is removed from view the moment the
   animation ends, and it must hold its final frame until then or it would
   flick back to full size on the last frame. */
.modal-overlay.mpc-closing {
    visibility: visible;
    /* AND display, which the visibility line alone does not cover. This rule
       was written for admin.html, where the overlay rests at
       `display:flex; visibility:hidden` — so holding visibility was enough.
       partner.html and parents-portal/index.html rest at `display:none`
       instead, and on those two an exit that only restores visibility renders
       nothing at all: the element is already out of the layout before the
       animation can run, so closeModal would appear to do nothing and the
       modal would still vanish on a frame. Forcing flex for the duration of
       the exit makes one rule correct for both arrangements; the three pages
       that were already flex are unaffected. */
    display: flex;
    opacity: 0;
    pointer-events: none;
}
/* admin.html predates this file and slides the card in by transitioning
   margin-top from -30px to 0 on .active. Removing .active starts that running
   BACKWARDS over 0.3s — so without this the card would drift 30px upward while
   the exit animation is trying to drop it 6px down, and the two would fight in
   front of the viewer. Pin it for the duration of the exit. */
.modal-overlay.mpc-closing > .alert-card { margin-top: 0; transition: none; }
.modal-overlay.mpc-closing > .alert-card,
.modal-overlay.mpc-closing > .modal-card,
.modal-overlay.mpc-closing > div:first-child {
    animation: mpcModalOut .15s cubic-bezier(.4, 0, 1, 1) forwards;
}
@keyframes mpcModalOut {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateY(6px) scale(.985); }
}

/* A confirmation mark that lands rather than appears. The slight overshoot is
   the whole point: it reads as "done", where a linear fade reads as "drawing".
   `backwards` only holds the start frame through the delay — nothing keeps a
   transform afterwards, per the containing-block note above. */
@keyframes mpcPop {
    from { opacity: 0; transform: scale(.4); }
    to   { opacity: 1; transform: none; }
}

/* A panel arriving in a split view. It enters from the side its rail is on, so
   the movement matches the click that caused it. `backwards` again — a panel is
   exactly the kind of element that hosts a modal. */
.sd-panel-in { animation: mpcPanelIn .24s cubic-bezier(.22, .8, .3, 1) backwards; }
@keyframes mpcPanelIn {
    from { opacity: 0; transform: translateX(14px); }
    to   { opacity: 1; transform: none; }
}

/* ── Micro-feedback ─────────────────────────────────────────────────────── */
/* Buttons acknowledge the press itself, which matters most on touch where
   there is no hover state to confirm the tap landed. */
button:not(:disabled):active,
.inline-action-btn:not(:disabled):active,
.btn:not(:disabled):active {
    transform: scale(.97);
}
button, .inline-action-btn, .btn, .kebab-item {
    transition: transform .12s ease, background-color .15s ease,
                box-shadow .15s ease, border-color .15s ease, opacity .15s ease;
}

/* Kebab / dropdown menus grow from their anchor instead of blinking in. */
.kebab-dropdown {
    transform-origin: top right;
    transition: opacity .16s ease, transform .16s cubic-bezier(.22, .8, .3, 1), visibility .16s;
}
.kebab-dropdown:not(.show) { opacity: 0; transform: scale(.96) translateY(-4px); }
.kebab-dropdown.show       { opacity: 1; transform: none; }

/* OPENING is not the same event as closing and must not share its duration.
   Measured 2026-08-10 (owner: "tapping on a drop down hangs a bit/delays
   opening"): 20ms after the tap the menu was still at 8% opacity and only
   reached full at ~140ms, so the menu appeared to arrive late rather than fade.
   A menu is a response to a tap — it should be legible almost immediately.
   Closing keeps the full .16s, where the fade is what makes it feel deliberate
   and where the visibility transition holds the menu on screen while it goes. */
.kebab-dropdown.show { transition-duration: .09s; }

/* `input, select, textarea { transition: all 0.2s }` (admin.html:360) makes the
   browser watch every animatable property on every control, and stretches the
   focus ring / border acknowledgement of a tap over a full 200ms — which is the
   other half of why opening a picker feels sluggish. Narrow it to the three
   properties that actually change on focus, and halve the time. Selects only:
   text inputs and textareas keep their existing behaviour. */
select {
    transition: border-color .12s ease, box-shadow .12s ease, background-color .12s ease;
}

/* Skeleton shimmer for anything still loading. */
.mpc-skeleton {
    background: linear-gradient(90deg, #eceef3 25%, #f6f7fa 37%, #eceef3 63%);
    background-size: 400% 100%;
    animation: mpcShimmer 1.3s ease-in-out infinite;
    border-radius: 8px;
}
@keyframes mpcShimmer {
    from { background-position: 100% 50%; }
    to   { background-position: 0 50%; }
}

/* ── iOS: no backdrop blur on touch devices ─────────────────────────────── */
/* Owner-reported 2026-08-10: "the drop down is only slow on the first time it
   was clicked", on iOS.
   Measured on desktop Chromium first: openModal runs in 0.1–0.4ms and there is
   no first-open frame penalty at all, so no JavaScript and no layout work
   explains it. What differs on iOS is compositing — `backdrop-filter` makes
   WebKit snapshot and blur the ENTIRE viewport behind the overlay, and the first
   time it does that for a given layer it must allocate and rasterize the blur
   texture. Opening a native <select> on top forces that path. Later taps reuse
   the cached texture, which is exactly the "slow once, fine afterwards" shape.
   The dim is what actually communicates "the page behind is inactive"; the blur
   is decoration. Kept on desktop, where it is free, and traded for a slightly
   deeper dim on touch so the modal reads as strongly separated either way. */
@media (hover: none) and (pointer: coarse) {
    .modal-overlay::before {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(0, 0, 0, .62);
    }
}

/* ── Accessibility ──────────────────────────────────────────────────────── */
/* Users who ask their OS for less motion get none of it: state still changes,
   it simply changes instantly. */
@media (prefers-reduced-motion: reduce) {
    .lesson-item, .cashbox-row, .kebab-row, .notif-item, .stat-box,
    .branch-card, .split-list-content > div, .payout-item,
    .mpc-sweep-out, .mpc-flash-ok, .mpc-skeleton,
    .modal-overlay.active > .alert-card,
    .modal-overlay.active > .modal-card,
    .modal-overlay.active > div:first-child,
    .modal-overlay.mpc-closing > .alert-card,
    .modal-overlay.mpc-closing > .modal-card,
    .modal-overlay.mpc-closing > div:first-child,
    .sd-result-ring,
    .sd-panel-in {
        animation: none !important;
    }
    .mpc-collapse, .mpc-caret, .kebab-dropdown, select,
    button, .inline-action-btn, .btn, .kebab-item {
        transition: none !important;
    }
    button:not(:disabled):active,
    .inline-action-btn:not(:disabled):active,
    .btn:not(:disabled):active { transform: none; }
}
