/*
 * motion.css — the app's entire animation layer.
 *
 * THE CONTRACT (docs/plans/motion-pass.md). Every rule in this file obeys it,
 * and tests/test_motion_css.py fails the build if one stops:
 *
 *   1. transform and opacity only. Those two composite on the GPU with no
 *      layout and no paint. Animating width/height/top/left/margin/padding/
 *      max-height/box-shadow/border-radius/background-color/filter costs
 *      main-thread work on every frame and is banned here.
 *   2. Never animate an element carrying backdrop-filter. style.css has 52
 *      uses; changing opacity or transform on a blurred element re-runs the
 *      blur over its whole backdrop every frame. Blur on a static parent,
 *      motion on a child.
 *   3. No will-change. Browsers already promote a layer for the duration of a
 *      transform/opacity transition. Hand-placing will-change holds VRAM
 *      permanently for a 150ms effect. Omitting it is simpler and cheaper.
 *   4. Short and small. Nothing here exceeds --motion-base.
 *
 * These classes are added to markup that already exists. They deliberately do
 * not require the JS toggling `d-none` / `style.display` / `.show` to change:
 * `transition-behavior: allow-discrete` plus `@starting-style` is what lets a
 * display:none toggle animate without a line of script.
 *
 * prefers-reduced-motion needs no handling here — style.css:98 is a blanket
 * `*` override with !important that neutralises every duration below.
 */

:root {
    --motion-fast: 120ms;
    --motion-base: 180ms;
    /* One easing. Decelerating: fast out of the gate, settles gently. */
    --ease-out: cubic-bezier(0.2, 0, 0, 1);
}

/* ---------------------------------------------------------------------------
   .m-fade — crossfades and panel swaps.

   `display` is in the transition list so the element stays displayed for the
   duration of its exit; without allow-discrete it would vanish on frame one
   and the fade-out would never be seen.
   --------------------------------------------------------------------------- */
.m-fade {
    opacity: 1;
    transition:
        opacity var(--motion-base) var(--ease-out),
        display var(--motion-base) allow-discrete;
}

.m-fade.d-none,
.m-fade[hidden] {
    opacity: 0;
}

@starting-style {
    .m-fade {
        opacity: 0;
    }
}

/* ---------------------------------------------------------------------------
   .m-pop — things that appear in place: badges, overlays, dialogs.

   Uses the independent `scale:` property rather than `transform: scale()`, so
   it never fights an inline `transform` written by Popper or by positioning
   code. contain:paint bounds invalidation to the element itself, which matters
   for the fullscreen history overlay.
   --------------------------------------------------------------------------- */
.m-pop {
    opacity: 1;
    scale: 1;
    contain: paint;
    transition:
        opacity var(--motion-fast) var(--ease-out),
        scale var(--motion-fast) var(--ease-out),
        display var(--motion-fast) allow-discrete;
}

.m-pop.d-none,
.m-pop[hidden] {
    opacity: 0;
    scale: 0.96;
}

@starting-style {
    .m-pop {
        opacity: 0;
        scale: 0.96;
    }
}

/* ---------------------------------------------------------------------------
   .m-rise — bars that slide up into view: bulk-select, citation insert,
   diff footer. translateY only; the bar's height never animates, so the
   content behind it never reflows.
   --------------------------------------------------------------------------- */
.m-rise {
    opacity: 1;
    translate: 0 0;
    transition:
        opacity var(--motion-base) var(--ease-out),
        translate var(--motion-base) var(--ease-out),
        display var(--motion-base) allow-discrete;
}

.m-rise.d-none,
.m-rise[hidden] {
    opacity: 0;
    translate: 0 8px;
}

@starting-style {
    .m-rise {
        opacity: 0;
        translate: 0 8px;
    }
}

/* ---------------------------------------------------------------------------
   .m-panel-enter + .m-enter-hidden — the page-load reveal for editor.html's
   left-pane (file tree column) only: mark the element with both classes in
   the initial HTML, then have editor.ts's revealWorkspacePanels() remove
   just .m-enter-hidden shortly after load to trigger the transition.

   Deliberately not used on center-area or assistant-sidebar even though they
   are the other two workspace columns: each of those already owns its own
   entrance (#editor's pane-pending fade, #ai-chat-panel's .chat-loading
   fade - style.css's "Pane entrances" section), and opacity compounds with
   children, so wrapping either container in an outer fade made its own
   content-reveal invisible for the wrapper's fade-in window and turned that
   later reveal into a second, separate pop.

   Also deliberately a separate class from .m-rise rather than reusing it:
   .m-rise's own `.m-rise.d-none { opacity: 0 }` exit rule matches *any*
   element carrying both classes, and an element that uses `d-none` for its
   own unrelated responsive show/hide (as assistant-sidebar's `d-none
   d-md-flex` does) would match that exit rule and never appear if given
   .m-rise. This class never touches `display` at all, so it can't collide
   with `.d-none`/`[hidden]` used for something else, nor with a
   `!important` Bootstrap display utility.
   --------------------------------------------------------------------------- */
.m-panel-enter {
    opacity: 1;
    translate: 0 0;
    transition:
        opacity var(--motion-base) var(--ease-out),
        translate var(--motion-base) var(--ease-out);
}

.m-panel-enter.m-enter-hidden {
    opacity: 0;
    translate: 0 8px;
}

/* ---------------------------------------------------------------------------
   .m-list-enter — dashboard project/folder rows (project_list_items.html).
   A plain autoplaying `animation`, not a class-toggle transition, because
   these <li>s are recreated identically by the server-rendered page load and
   by project_list.ts's AJAX list-fragment refresh (create/rename/move/
   delete). A freshly inserted element just plays its own `animation` on
   arrival either way, so both paths get the same entrance for free with no
   JS state to track. --row-index (set inline per <li> by the template's
   loop.index) staggers the rows; capped via min() so a huge list still
   finishes revealing quickly instead of a long visible tail.
   --------------------------------------------------------------------------- */
@keyframes m-list-enter-frames {
    from {
        opacity: 0;
        translate: 0 8px;
    }
}

/* fill-mode: backwards, not `both` - `both` also holds the *finished*
   keyframe forever (a resolved `translate` value, never the literal `none`
   keyword, no matter how the keyframe is written), and any element with a
   non-none translate/transform - even a frozen zero offset - permanently
   creates its own stacking context. That silently defeated list view's
   z-index:auto flattening below (style.css, "Stacking:" comment): every row
   stayed sealed in its own context forever, burying an open dropdown behind
   the next row. `backwards` still shows the 0%-keyframe state during the
   staggered animation-delay so rows don't flash in before their turn, but
   once the animation completes the property stops being animation-driven
   and falls back to the real static default (opacity: 1, translate: none). */
.m-list-enter {
    animation: m-list-enter-frames var(--motion-base) var(--ease-out) backwards;
    animation-delay: calc(min(var(--row-index, 0), 12) * 15ms);
}

/* ---------------------------------------------------------------------------
   .m-menu — dropdowns and context menus.

   There are FOUR unrelated menu systems in this app, and they share a class
   name in only two cases — never a mechanism:

     1. Bootstrap dropdowns (46 of them). JS adds `.show`, Popper positions.
     2. The navbar File/Edit/Insert/View/Help menus. Pure CSS: they open on
        `:hover` via `display: block` (style.css:1474) and never get `.show`.
     3. Monaco's right-click menu. Monaco's own widget layer, inserted into
        `.context-view.monaco-menu-container`. We can style it, nothing more.
     4. The file tree right-click menu, hand-rolled with createElement
        (file_tree.ts:157-165).

   A `.show`-keyed transition therefore animates exactly one of the four, which
   is what the first version of this file did.

   `animation:` is the hook all four actually share. A CSS animation restarts
   whenever an element goes from not-rendered to rendered — which covers a
   display:none -> block flip (1 and 2) *and* a fresh insertion into the DOM
   (3 and 4) — with no class, no JS, and no @starting-style. One keyframe, one
   rule, four systems, and nothing to keep in sync when a fifth appears.

   `scale:` not `transform:`, because Popper writes an inline `transform` for
   placement and Monaco writes inline `top`/`left`; overwriting either throws
   the menu across the page.

   Entrance only, by design: menus are torn down with .remove()
   (file_tree.ts:148), which truncates any exit. Instant dismissal reads as
   responsive, and chasing it would mean animationend handling on every
   teardown path.
   --------------------------------------------------------------------------- */
@keyframes menu-in {
    from {
        opacity: 0;
        scale: 0.97;
    }

    to {
        opacity: 1;
        scale: 1;
    }
}

.dropdown-menu.show,
#main-menu-bar .menu-dropdown:hover>.dropdown-menu,
.ft-context-menu,
.monaco-menu-container,
.context-view.monaco-menu-container,
.action-palette-dropdown,
.m-menu.show {
    animation: menu-in var(--motion-fast) var(--ease-out);
    transform-origin: var(--m-menu-origin, top center);
}

/* Popper writes data-popper-placement; match the origin to it so the menu
   scales out of the control that opened it rather than out of thin air.
   Attribute selectors, so this covers every system that uses Popper at once. */
[data-popper-placement^="top"] {
    --m-menu-origin: bottom center;
}

[data-popper-placement^="bottom"] {
    --m-menu-origin: top center;
}

[data-popper-placement^="left"] {
    --m-menu-origin: center right;
}

[data-popper-placement^="right"] {
    --m-menu-origin: center left;
}

/* Menus positioned at the pointer rather than anchored to a control: they
   should grow from the corner the cursor is in. */
.ft-context-menu,
.monaco-menu-container,
.context-view.monaco-menu-container {
    --m-menu-origin: top left;
}

/* ---------------------------------------------------------------------------
   Toast — the only animation in the app driven by a keyframe rather than a
   transition, because the element is created and destroyed rather than
   toggled.

   Enter, hold and exit are ONE keyframe with percentage stops. That is what
   keeps shared/toast.ts free of animation code: it appends the node and
   removes it on a plain setTimeout, and because the final frame is held by
   animation-fill-mode: forwards, a late timeout can never leave a
   half-faded toast on screen or race a transition.
   --------------------------------------------------------------------------- */
/* Bottom-centre, not the conventional bottom-right: the editor's right edge is
   the AI chat composer, and a toast parked there covers the input the user is
   about to type into. The left edge is the file tree's page controls. Centre is
   the only strip that is transient content on every page.

   The stack itself never takes pointer events, so a toast can't swallow a click
   meant for whatever is underneath it. */
.toast-stack {
    position: fixed;
    bottom: 16px;
    left: 50%;
    translate: -50% 0;
    z-index: 1090;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: none;
}

/* Also pointer-events: none. A toast carries no close button and no link, so
   there is nothing on it to click - and an informational overlay that swallows
   clicks aimed at the UI underneath it for four seconds is a bug, not a
   feature. */
.toast-item {
    pointer-events: none;
    contain: paint;
    animation: toast-life var(--toast-duration, 4000ms) var(--ease-out) forwards;
}

/* Library pane Zotero sync button: the refresh icon turns while a sync runs
   (zotero_auto_sync.ts toggles the class). */
.zotero-syncing svg,
.zotero-syncing i {
    animation: zotero-sync-spin 1s linear infinite;
}

@keyframes zotero-sync-spin {
    to {
        rotate: 360deg;
    }
}

@keyframes toast-life {
    0% {
        opacity: 0;
        translate: 0 12px;
    }

    /* In by 4% of total, out over the last 5%: the hold is the whole middle,
       so one keyframe covers the entire lifetime at any duration. */
    4% {
        opacity: 1;
        translate: 0 0;
    }

    95% {
        opacity: 1;
        translate: 0 0;
    }

    100% {
        opacity: 0;
        translate: 0 12px;
    }
}

/* ---------------------------------------------------------------------------
   Reusable highlight pill.

   Lived in file_tree.css until it was needed outside the editor. file_tree.css
   is loaded by editor.html alone, while dropdown menus - which now use the same
   pill for hover - appear on six templates. motion.css is loaded on all 23, and
   a pill is a motion component before it is a file-tree component, so this is
   its home now. docs/ui/pill-highlighter.md documents the JS side.

   Driven by pill_highlighter.ts, which sets `translate` and `height`. See the
   .pill rule for why it is translate and not top, and why there is no blur.
   --------------------------------------------------------------------------- */
/* ---- Reusable Highlight Pill ---- */
/* The pill moves via `translate` (set by pill_highlighter.ts), never `top`.
   Transitioning `top` costs a layout pass on every frame of every slide, and
   this element slides on every hover - it was measured at ~6 layouts per move.
   `height` is intentionally absent from the transition list: it changes only
   when moveRange spans a different number of rows, and snapping it during a
   slide is imperceptible.

   No backdrop-filter either. A blur on a MOVING element is re-run over its
   whole backdrop region every frame, which is the most expensive thing this
   codebase can ask a GPU to do. The translucent background below carries the
   look on its own. */
.pill {
  position: absolute;
  top: 0;
  left: 4px;
  right: 4px;
  border-radius: 4px;
  pointer-events: none;
  z-index: 0;
  transition: translate 0.15s ease, opacity 0.1s ease;
  border: 1px solid rgba(100, 116, 139, 0.4);
  opacity: 0;
}

[data-bs-theme="dark"] .pill {
  border-color: rgba(148, 163, 184, 0.25);
}

/* Selection pill (selected row/item). menu-pill-selected is the same
   component for the tab-hover-menu's "this file/session is already open"
   row (tab_hover_menu.ts) - declared here rather than invented locally, same
   reasoning as menu-pill above. */
.pill.ft-pill-selected,
.pill.menu-pill-selected {
  background: rgba(148, 163, 184, 0.15);
  z-index: 0;
}

[data-bs-theme="dark"] .pill.ft-pill-selected,
[data-bs-theme="dark"] .pill.menu-pill-selected {
  background: rgba(148, 163, 184, 0.15);
  border-color: rgba(148, 163, 184, 0.5);
}

/* Hover pill (currently hovered row/item) */
/* Hover fill, shared by the file tree and by menus so the two look identical.
   Declared once: a menu pill that invented its own inset, border and alpha was
   visibly a different component to the one in the project tree. Everything else
   (4px inset, radius, border) comes from base .pill above. */
.pill.ft-pill-hover,
.pill.menu-pill {
  background: rgba(156, 163, 175, 0.08);
}

[data-bs-theme="dark"] .pill.ft-pill-hover,
[data-bs-theme="dark"] .pill.menu-pill {
  background: rgba(148, 163, 184, 0.1);
  border-color: rgba(148, 163, 184, 0.25);
}

/* The one difference, and it is structural rather than cosmetic: the tree's
   hover pill sits behind its selection pill, while a menu has no selection pill
   and a negative index would hide it behind the menu's own background. */
.pill.ft-pill-hover {
  z-index: -1;
}

.pill.menu-pill {
  z-index: 0;
}

/* Horizontal pill (icon-button strips - the left/right pane tab switchers).
   Geometry only: fill/border come from .ft-pill-hover / .ft-pill-selected,
   combined on the same element (e.g. 'tab-pill ft-pill-selected'), so these
   strips paint identically to the file tree's pills.

   The base .pill above assumes a vertical list: it insets left/right and lets
   JS drive `translate`+`height` down a column. A tab strip is a row, so this
   flips the axis - insets go top/bottom, and pill_highlighter.ts's horizontal
   mode drives `translate`+`width` instead (see createPillHighlighter's
   `direction` param). */
.pill.tab-pill {
  top: 4px;
  bottom: 4px;
  left: 0;
  right: auto;
  height: auto;
  border-radius: 6px;
}

/* Buttons must out-rank the pill (z-index 0, or -1 while hovering behind an
   active selection) to sit visibly on top of it, and the strip's own hover/
   active fills must be neutralized so the pill is the only highlight - same
   reasoning as the .dropdown-item rules below. */
.left-pane-tabs .btn-textonly-large,
.right-pane-tabs .btn-textonly-large {
  position: relative;
  z-index: 1;
}

.left-pane-tabs .btn-textonly-large:hover,
.right-pane-tabs .btn-textonly-large:hover,
.left-pane-tabs .btn-textonly-large:active,
.right-pane-tabs .btn-textonly-large:active,
.left-pane-tabs .btn-textonly-large.active,
.right-pane-tabs .btn-textonly-large.active {
  background: transparent;
}

/* The file tree never recolours a selected row's own label - only the pill
   behind it changes - so the generic .btn-textonly-large.active rule's blue
   icon (style.css) is overridden back to normal here for the same reason. */
.left-pane-tabs .btn-textonly-large.active,
.right-pane-tabs .btn-textonly-large.active {
  color: var(--text-color);
}

/* Bootstrap's own `.dropdown-item:hover/:active` fills, straight from the CDN
   build, would still paint a blue row under the pill on click. motion.css loads
   after bootstrap.min.css (see the <link> order in editor.html), so matching its
   specificity is enough to win — no !important needed.

   Keyboard focus is untouched: `:focus-visible` keeps its fill in style.css,
   because the pill follows the mouse and would leave keyboard users with no
   indicator at all. */
.dropdown-item:hover,
.dropdown-item:active,
.dropdown-item.active {
  background-color: transparent;
  color: inherit;
}

/* The rows the pill slides behind must establish their own stacking context,
   or the pill paints over them. */
.dropdown-menu .dropdown-item,
.ft-context-menu .dropdown-item,
.action-palette-dropdown .action-item {
  position: relative;
  z-index: 1;
}

/* Legacy ft-pill classes for backwards compatibility. Same translate/no-blur
   rules as .pill above - they share the same moving element. */
.ft-pill {
  position: absolute;
  top: 0;
  left: 4px;
  right: 4px;
  border-radius: 4px;
  pointer-events: none;
  z-index: 0;
  transition: translate 0.15s ease, opacity 0.1s ease;
  border: 1px solid rgba(100, 116, 139, 0.4);
}

.ft-pill-selected {
  background: rgba(148, 163, 184, 0.15);
  opacity: 0;
}

[data-bs-theme="dark"] .ft-pill-selected {
  background: rgba(148, 163, 184, 0.15);
  border-color: rgba(148, 163, 184, 0.5);
}

.ft-pill-hover {
  background: rgba(156, 163, 175, 0.08);
  opacity: 0;
}

[data-bs-theme="dark"] .ft-pill-hover {
  background: rgba(148, 163, 184, 0.1);
  border-color: rgba(148, 163, 184, 0.25);
}
