/* ============================================================================
   GLEE CANONICAL MOTION PRIMITIVES — v1  (2026-08-06)
   ----------------------------------------------------------------------------
   Companion to glee-tokens.css. Tokens name the *values*; this file names the
   *motions*. An agent handed only tokens still invents the composition on every
   pass, which is how one estate ended up expressing "hover lift" six ways:
   translateY(-1px) / -2px / -3px+scale(1.012) / -4px+scale(1.012) /
   -4px+scale(1.015) / scale(1.04).

   THE LAW (doctrine/GLEE_LIVING_WORKSHOP_UI.md § Motion Primitive Canon):
   an implementation agent SELECTS a primitive from this file. It may not
   invent motion inline. If a genuinely new interaction is needed, prototype it
   on /motion/, get it judged, add it here, and only then use it.

   PRIMITIVES DESCRIBE MEANING, NOT TRANSFORMS. Select by interaction intent —
   "what is this telling the user?" — never by "which transform looks nice?".
   The transform is the current implementation of the meaning and may change; the
   meaning is the contract:

     gm-hover-lift    this surface is interactable and comes toward you
     gm-hover-object  this is a discrete thing you can pick up and move
     gm-hover-tint    this is interactable but stays put — you are in a list
     gm-press         your input was received, at the instant it was received
     gm-focus-ring    the keyboard is here now (position unchanged, on purpose)
     gm-expand        there is more of this, in place, without leaving
     gm-enter         this arrived — a new state, not a new position
     gm-exit          this is gone — and it left faster than it came
     gm-selectable    you have chosen this one; it has not moved
     gm-drag-pickup   you are now holding it and the surface has let go
     gm-dragging      it is following your hand exactly
     gm-drop-settle   it has landed and committed
     gm-route-in      you are somewhere new
     gm-pending       work is happening and the outcome is not known yet
     gm-progress      work is happening and this much of it is done
     gm-camera        the world moved, you did not

   Two primitives with the same transform and different meanings stay two
   primitives. Magnitude registers (.gm-subtle / .gm-emphatic) change how loudly
   a meaning is said, never which meaning it is.

   COMPOSITION RULES — these are what stop the drift, not the values:
     1. ONE TRANSFORM OWNER. A primitive sets `transform` wholesale. Never put
        two motion primitives on the same element, and never add a bare
        `transform:` to an element that already carries one.
     2. THE ANCESTOR OWNS THE LIFT. If a card lifts on hover, nothing inside it
        may also lift, scale, or zoom on that same hover. Stacked scales are the
        "slight enlargement then excessive zoom" bug.
     3. HOVER NEVER CHANGES OPACITY. Fading on hover reads as a state change.
        Use --gm-* colour/border shifts instead.
     4. FOCUS NEVER MOVES ANYTHING. Keyboard focus draws a ring; it does not
        translate, scale, or lift. Moving on focus breaks keyboard tracking.
     5. EXITS ARE FASTER THAN ENTRANCES. Entering explains; leaving gets out of
        the way.

   Reversible: remove this stylesheet link and elements lose their motion but
   keep their layout — no primitive here is load-bearing for position.
   ============================================================================ */

:root {
  /* ---- duration ladder: four values, one job each ------------------------
     Consolidates the 9 hardcoded durations found across round_zero and the
     13 across platform/worker. `quick` sits at 140ms — the median of the two
     dominant legacy values (120ms, 12 uses; 160ms, 15 uses) — so converting
     legacy rules is a sub-perceptual change in both directions. */
  --gm-instant:  80ms;   /* state flips: colour, border, focus ring          */
  --gm-quick:   140ms;   /* pointer feedback: hover, press                   */
  --gm-standard:280ms;   /* structure: open/close, expand, dock, route       */
  --gm-slow:    450ms;   /* space: camera, semantic zoom, lock-in settle     */

  /* ---- easing: three curves ---------------------------------------------
     precise + alive are the two ratified 2026-06-29 curves, unchanged.
     exit is new — the estate had no accelerating curve, so every "leave"
     used the same decelerating curve as its "arrive" and felt sticky. */
  --gm-ease-precise: cubic-bezier(0.2, 0, 0.1, 1);    /* mechanical, default */
  --gm-ease-alive:   cubic-bezier(0.23, 1, 0.32, 1);  /* decelerating reveal */
  --gm-ease-exit:    cubic-bezier(0.4, 0, 1, 1);      /* accelerating leave  */

  /* ---- displacement -------------------------------------------------------
     MEANING is the primitive; MAGNITUDE is a register on top of it. A −3px lift
     that reads well on a 142px card is >10% of a 28px control's height (jumpy)
     and <1% of a 400px scene object (invisible). One number cannot serve all
     three, and the alternative — letting each component pick — is the drift this
     file exists to stop. So: one semantic primitive, three registers.

       subtle   — controls and dense rows under ~48px
       standard — cards, posts, panels, tiles (the default; no class needed)
       emphatic — media and scene objects over ~280px

     Judge them at all three extremes on /motion/ before treating any value here
     as settled. */
  --gm-lift-subtle:    -2px;
  --gm-lift:           -3px;   /* standard — the default hover lift          */
  --gm-lift-emphatic:  -6px;
  --gm-scale-up-subtle:  1.006;
  --gm-scale-up:         1.015; /* standard — grabbable objects only          */
  --gm-scale-up-emphatic:1.022;

  --gm-nudge:     2px;   /* directional hints, list-row shift                */
  --gm-rise:      6px;   /* entrance travel                                  */
  --gm-scale-down:0.985; /* press                                            */
  --gm-scale-pickup:1.02;/* held for drag — reads bigger than hover on purpose*/

  /* ---- elevation paired with lift ---------------------------------------- */
  --gm-shadow-rest: 0 0 0 rgba(0,0,0,0);
  --gm-shadow-subtle: 0 4px 12px rgba(0,0,0,0.45);
  --gm-shadow-lift: 0 10px 28px rgba(0,0,0,0.55);
  --gm-shadow-emphatic: 0 20px 52px rgba(0,0,0,0.6);
  --gm-shadow-drag: 0 18px 44px rgba(0,0,0,0.65);
}

/* ==========================================================================
   MAGNITUDE REGISTERS — `.gm-subtle` / `.gm-emphatic` alongside a hover
   primitive. They change HOW MUCH, never WHAT IT MEANS. A control and a scene
   object both say "interactable, comes toward you"; they just say it at the
   volume their size can carry.
   ========================================================================== */

.gm-subtle   { --gm-lift: var(--gm-lift-subtle);
               --gm-scale-up: var(--gm-scale-up-subtle);
               --gm-shadow-lift: var(--gm-shadow-subtle); }
.gm-emphatic { --gm-lift: var(--gm-lift-emphatic);
               --gm-scale-up: var(--gm-scale-up-emphatic);
               --gm-shadow-lift: var(--gm-shadow-emphatic); }

/* ==========================================================================
   HOVER — three primitives. Pick exactly one per element.
   ========================================================================== */

/* gm-hover-lift — the default for anything card-shaped: posts, tiles, panels,
   result rows, project cards. Moves in Y only. NO SCALE: a card is a surface,
   not an object you can pick up, and scaling text-bearing surfaces resamples
   the glyphs. This is the primitive that replaces the six divergent lifts. */
.gm-hover-lift {
  transition: transform var(--gm-quick) var(--gm-ease-alive),
              box-shadow var(--gm-quick) var(--gm-ease-alive),
              border-color var(--gm-instant) var(--gm-ease-precise);
}
.gm-hover-lift:hover,
.gm-hover-lift.is-lifted {
  transform: translateY(var(--gm-lift));
  box-shadow: var(--gm-shadow-lift);
}

/* gm-hover-object — for canvas objects the user can actually grab and move.
   Scale is the affordance that says "this is a thing, not a region"; spend it
   only where dragging is real. */
.gm-hover-object {
  transition: transform var(--gm-quick) var(--gm-ease-alive),
              box-shadow var(--gm-quick) var(--gm-ease-alive),
              border-color var(--gm-instant) var(--gm-ease-precise);
  will-change: transform;
}
.gm-hover-object:hover,
.gm-hover-object.is-lifted {
  transform: translateY(var(--gm-lift)) scale(var(--gm-scale-up));
  box-shadow: var(--gm-shadow-lift);
}

/* gm-hover-tint — no geometry at all. Links, nav items, dense list rows,
   toolbar buttons, anything in a scrolling column where lifting would make
   the column shimmer under the cursor. */
.gm-hover-tint {
  transition: color var(--gm-instant) var(--gm-ease-precise),
              background-color var(--gm-instant) var(--gm-ease-precise),
              border-color var(--gm-instant) var(--gm-ease-precise);
}

/* ==========================================================================
   PRESS — one primitive. Pairs with any hover primitive; press owns the
   transform during :active because it is the more specific state.
   ========================================================================== */

.gm-press {
  transition: transform var(--gm-instant) var(--gm-ease-precise);
}
.gm-press:active {
  transform: scale(var(--gm-scale-down));
}
/* When an element carries both a lift and a press, the press wins and the
   lift is held — the object dips from where it was, it does not snap to
   origin first. */
.gm-hover-lift.gm-press:active {
  transform: translateY(var(--gm-lift)) scale(var(--gm-scale-down));
}
.gm-hover-object.gm-press:active {
  transform: translateY(var(--gm-lift)) scale(var(--gm-scale-down));
}

/* ==========================================================================
   FOCUS — ring only. Rule 4: focus never moves anything.
   ========================================================================== */

.gm-focus-ring {
  transition: box-shadow var(--gm-instant) var(--gm-ease-precise),
              border-color var(--gm-instant) var(--gm-ease-precise);
}
.gm-focus-ring:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg, #000), 0 0 0 4px var(--sand, #d8c08a);
}

/* ==========================================================================
   OPEN / CLOSE — height-agnostic expand. Uses grid-template-rows so the
   panel animates to its true content height without a measured pixel value.
   Markup: <div class="gm-expand"><div class="gm-expand-inner">…</div></div>
   ========================================================================== */

.gm-expand {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--gm-standard) var(--gm-ease-alive),
              opacity var(--gm-standard) var(--gm-ease-alive);
  opacity: 0;
}
.gm-expand > .gm-expand-inner { overflow: hidden; min-height: 0; }
.gm-expand.is-open,
[data-gm-open="true"] > .gm-expand {
  grid-template-rows: 1fr;
  opacity: 1;
}

/* ==========================================================================
   ENTER / EXIT — for content arriving in place (streamed rows, new receipts,
   agent handoff cards). Rule 5: the exit is quicker and accelerates.
   ========================================================================== */

@keyframes gm-enter-kf {
  from { opacity: 0; transform: translateY(var(--gm-rise)); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes gm-exit-kf {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(calc(var(--gm-nudge) * -2)); }
}
.gm-enter { animation: gm-enter-kf var(--gm-standard) var(--gm-ease-alive) both; }
.gm-exit  { animation: gm-exit-kf  var(--gm-quick)    var(--gm-ease-exit)  both; }

/* ==========================================================================
   SELECTION — a border and an inset ring. Selection must not move the thing
   selected: the user is pointing at it, and a moving target under a click is
   how you get the wrong row.
   ========================================================================== */

.gm-selectable {
  transition: border-color var(--gm-instant) var(--gm-ease-precise),
              box-shadow var(--gm-instant) var(--gm-ease-precise),
              background-color var(--gm-instant) var(--gm-ease-precise);
}
.gm-selectable.is-selected {
  border-color: var(--sand, #d8c08a);
  box-shadow: inset 0 0 0 1px var(--sand, #d8c08a);
}

/* ==========================================================================
   DRAG / DROP — pickup, carry, settle. The three moments the Reddit thread
   named as the ones agents most reliably reinvent.
   ========================================================================== */

.gm-drag-pickup {
  transform: scale(var(--gm-scale-pickup));
  box-shadow: var(--gm-shadow-drag);
  cursor: grabbing;
  transition: transform var(--gm-instant) var(--gm-ease-precise),
              box-shadow var(--gm-instant) var(--gm-ease-precise);
}
/* Carry: no transition on transform — the element must track the pointer
   exactly. A transition here is the classic "laggy drag" bug. */
.gm-dragging { transition: none !important; will-change: transform; }

@keyframes gm-settle-kf {
  0%   { transform: scale(var(--gm-scale-pickup)); }
  60%  { transform: scale(0.995); }
  100% { transform: scale(1); }
}
.gm-drop-settle { animation: gm-settle-kf var(--gm-standard) var(--gm-ease-alive) both; }

/* ==========================================================================
   NAVIGATION — route arrival. One primitive; a route change is an entrance
   for the whole region, not a per-element cascade.
   ========================================================================== */

@keyframes gm-route-kf {
  from { opacity: 0; transform: translateY(calc(var(--gm-nudge) * 2)); }
  to   { opacity: 1; transform: translateY(0); }
}
.gm-route-in { animation: gm-route-kf var(--gm-standard) var(--gm-ease-alive) both; }

/* ==========================================================================
   LOADING / PROGRESS — indeterminate pulse and determinate sweep. Both are
   opacity/width only: nothing that is still loading may move, because motion
   during token streaming breaks readability (ratified doctrine § Rules).
   ========================================================================== */

@keyframes gm-pulse-kf { 0%,100% { opacity: 0.45; } 50% { opacity: 1; } }
.gm-pending { animation: gm-pulse-kf 1400ms var(--gm-ease-precise) infinite; }

.gm-progress > .gm-progress-fill {
  transition: width var(--gm-standard) var(--gm-ease-precise);
}

/* ==========================================================================
   CAMERA / ZOOM — the signature semantic-zoom move. Applied to a scene
   wrapper whose transform is driven by variables, never by a second class.
   ========================================================================== */

.gm-camera {
  transform: translate3d(var(--gm-cam-x, 0), var(--gm-cam-y, 0), 0)
             scale(var(--gm-cam-scale, 1));
  transform-origin: 0 0;
  transition: transform var(--gm-slow) var(--gm-ease-precise);
}
.gm-camera.gm-camera-live { transition: none; }  /* live pan/wheel: track input */

/* ==========================================================================
   REDUCED MOTION — every primitive's equivalent.

   Both stacks already carry a blanket `transition-duration: 0.001ms !important`
   under this query (round-zero-style.css:4357, style_engine.js), so timing is
   handled: changes land instantly. What a blanket rule CANNOT do is neutralise
   the displacement itself — `transform: translateY(-3px)` still moves the
   element, just instantly, which is a jump rather than a motion and is worse.

   So this block does the one thing the blanket cannot: it removes the
   displacement. State stays fully visible — shadow, border, colour, opacity and
   progress width all still change, they simply no longer travel.

   `!important` is required here to beat the blanket rules, not to win a
   specificity fight with product CSS.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .gm-hover-lift:hover, .gm-hover-lift.is-lifted,
  .gm-hover-object:hover, .gm-hover-object.is-lifted,
  .gm-press:active,
  .gm-hover-lift.gm-press:active, .gm-hover-object.gm-press:active,
  .gm-drag-pickup {
    transform: none !important;
  }
  .gm-enter, .gm-exit, .gm-route-in, .gm-drop-settle {
    animation: none !important;
    opacity: 1;
    transform: none !important;
  }
  .gm-pending { animation: none !important; opacity: 0.7; }
  /* Drag carry is exempt: the element must still track the pointer, or dragging
     stops working for the users who most need it to be predictable. */
  .gm-dragging { transform: revert !important; }
}
