/* ============================================================================
 * Deckard — motion.css
 * DYNAMICS = STATUS-BEARING MOTION ONLY (spec §11, §18).
 *
 * Every animation in this file encodes a state. If an animation does not tell
 * you something about what an agent or a tool is DOING, it does not belong here.
 *   running lane   -> pulsing amber dot
 *   streaming text -> blinking block cursor
 *   tool card      -> animates in; pulses cyan while running;
 *                     flashes amber on success, red on error
 *   state changes  -> ~200ms transitions (var(--t))
 *
 * GUARDRAIL (§11): readability over spectacle. Glow stays subtle. There are NO
 * scanlines and NO CRT warp/curvature here — the spec explicitly rejects them.
 * Everything degrades to static under prefers-reduced-motion.
 * ==========================================================================*/

/* ------------------------------------------------------------- keyframes - */

/* Status dot for a running lane (§9/§11). Amber, breathing. */
@keyframes dk-pulse-amber {
  /* PERF (user, 2026-07-16 "random hitches"): this used to animate box-shadow, which repaints
     the element's region EVERY FRAME, FOREVER, on every pulsing dot/badge on screen. Opacity
     animates on the compositor — the glow is painted once (static shadow on the users that
     need it) and the whole layer breathes. */
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

/* "In progress" / thinking / tool running — cyan, per the §11 color semantics. */
@keyframes dk-pulse-cyan {
  /* The busiest pulse in the app (anything thinking/running). Opacity only — the box-shadow
     version repainted every pulsing dot's region every frame (see dk-pulse-amber note). */
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* Blocked on the user — red, urgent, faster. This one is meant to nag. */
@keyframes dk-pulse-red {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* Streaming cursor — a solid amber block that blinks, terminal-style. */
@keyframes dk-blink {
  0%,
  49% { opacity: 1; }
  50%,
  100% { opacity: 0; }
}

/* Tool card / permission prompt / lane entering the DOM. */
@keyframes dk-slide-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* A running tool card: the whole card breathes cyan at its border. */
@keyframes dk-border-cyan {
  /* Same perf rule: no border-color/box-shadow in infinite keyframes. Static cyan border on
     the running card (compensation block at end of file); the breathing is opacity. */
  0%, 100% { opacity: 1; }
  50% { opacity: 0.75; }
}

/* One-shot completion flashes. Amber = ok, red = failed. */
@keyframes dk-flash-amber {
  0%   { background: rgb(var(--amber-rgb) / 22%); border-color: var(--amber); }
  100% { background: var(--surface); border-color: var(--border); }
}
@keyframes dk-flash-red {
  0%   { background: rgb(var(--red-rgb) / 22%); border-color: var(--red); }
  100% { background: var(--surface); border-color: var(--border-danger); }
}

/* Attention badge on a collapsed rail (§9: a rail must never hide a block). */
@keyframes dk-attn {
  0%,
  100% { transform: scale(1);    opacity: 1; }
  50%  { transform: scale(1.25); opacity: 0.55; }
}

/* Indeterminate work sweep — used on lane headers while a task is live. */
@keyframes dk-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

/* Conflict row in the file map (§10) — red, unmissable, but not a strobe. */
@keyframes dk-conflict {
  0%,
  100% { background: rgb(var(--red-rgb) / 6%); }
  50%  { background: rgb(var(--red-rgb) / 18%); }
}

/* ------------------------------------------- baseline state transitions - */
/* §11: state changes transition ~200ms. These are the elements whose COLOR is
   the state, so a color change must read as a transition, not a jump cut. */
.dk-lane,
.dk-rail,
.dk-tool-card,
.dk-status-dot,
.dk-tab,
.dk-file-row,
.dk-perm,
.dk-thinking,
.dk-drawer {
  transition:
    background-color var(--t) var(--ease),
    border-color var(--t) var(--ease),
    color var(--t) var(--ease),
    box-shadow var(--t) var(--ease),
    opacity var(--t) var(--ease),
    flex-grow var(--t) var(--ease),
    width var(--t) var(--ease);
}

/* ---------------------------------------------------- status dot (§9) --- */
/* The lane status dot. State classes are set from the AGENT_STATE frame; the
   color IS the state (§11) and the motion says whether it is live. */
.dk-status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--amber-dim);
  flex: 0 0 auto;
}
.dk-status-dot[data-state="idle"],
.dk-status-dot[data-state="done"],
.dk-status-dot[data-state="cancelled"] {
  background: var(--amber-dim);
  animation: none;
}
.dk-status-dot[data-state="streaming"] {
  background: var(--amber);
  animation: dk-pulse-amber 1.1s var(--ease) infinite;
}
.dk-status-dot[data-state="thinking"],
.dk-status-dot[data-state="tool_running"] {
  background: var(--cyan);
  animation: dk-pulse-cyan 1.1s var(--ease) infinite;
}
.dk-status-dot[data-state="awaiting_permission"] {
  background: var(--red);
  animation: dk-pulse-red 0.7s var(--ease) infinite;
}
.dk-status-dot[data-state="error"] {
  background: var(--red);
  animation: none;
  box-shadow: 0 0 calc(6px * var(--glow-strength)) rgb(var(--red-rgb) / 60%);
}

/* A lane that is actively working gets a thin amber sweep under its header. */
.dk-lane-progress {
  position: relative;
  height: 1px;
  overflow: hidden;
  background: transparent;
}
.dk-lane[data-state="streaming"] .dk-lane-progress::after,
.dk-lane[data-state="thinking"] .dk-lane-progress::after,
.dk-lane[data-state="tool_running"] .dk-lane-progress::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent,
    rgb(var(--amber-rgb) / 70%),
    transparent
  );
  animation: dk-sweep 1.6s linear infinite;
}
.dk-lane[data-state="tool_running"] .dk-lane-progress::after,
.dk-lane[data-state="thinking"] .dk-lane-progress::after {
  background: linear-gradient(
    90deg,
    transparent,
    rgb(var(--cyan-rgb) / 70%),
    transparent
  );
}

/* ------------------------------------------------ streaming cursor (§11) - */
/* Append <span class="dk-cursor"></span> to the live text block; remove it when
   the block completes (done: true). Blinking block, amber, terminal. */
/* The caret sits INLINE, immediately after the last character being typed (message.js keeps it as
   the body's last child). Sized in `ch` so it is exactly one monospace cell — a block that matches
   the grid, like a real terminal — and given a soft fade rather than a hard step, so a caret that
   moves every few milliseconds during a stream reads as a glow rather than a strobe. */
.dk-cursor {
  display: inline-block;
  width: 1ch;
  height: 1.05em;
  margin-left: 1px;
  vertical-align: text-bottom;
  background: var(--amber);
  box-shadow: var(--glow-box);
  animation: dk-blink-soft 1.05s ease-in-out infinite;
}
@keyframes dk-blink-soft {
  0%, 45% { opacity: 1; }
  55%, 95% { opacity: 0.12; }
  100% { opacity: 1; }
}
/* While tokens are actually arriving the caret stops blinking and just glows — a caret that blinks
   AND jumps forward at the same time is visual noise. It resumes blinking when the stream pauses. */
.dk-msg[data-streaming="true"] .dk-cursor { animation-duration: 1.4s; }
.dk-cursor.dk-cursor-cyan { background: var(--cyan); box-shadow: var(--glow-box-cyan); }

/* ------------------------------------------------------- tool cards (§8) - */
/* Lifecycle: .dk-tool-card enters -> [data-state="running"] while executing ->
   [data-state="ok"|"error"] once the tool_result lands. */
.dk-tool-card,
.dk-perm,
.dk-lane,
.dk-msg {
  animation: dk-slide-in var(--t) var(--ease) both;
}
.dk-tool-card[data-state="running"] {
  animation: dk-border-cyan 1.4s var(--ease) infinite;
}
.dk-tool-card[data-state="ok"] {
  animation: dk-flash-amber 600ms var(--ease) 1;
}
.dk-tool-card[data-state="error"] {
  animation: dk-flash-red 600ms var(--ease) 1;
  border-color: var(--border-danger);
}
/* Destructive tools (Bash/Write/Edit/p4 mutations) are red BEFORE they run. */
.dk-tool-card[data-risky="true"] { border-color: var(--border-danger); }

/* ------------------------------------- permission-required attention (§12) */
.dk-perm[data-pending="true"] {
  border-color: var(--red);
  animation: dk-slide-in var(--t) var(--ease) both,
    dk-pulse-red 1.4s var(--ease) infinite;
}
/* The attention badge on a lane header / rail (§9: "a collapsed agent must never hide that it's
   blocked"). js/lanes.js puts a GLYPH in it — '!' when blocked or failed, '✓' when done — so it
   is content-sized, not a bare 6px dot; a fixed 6px box would clip the glyph away entirely.
   Red + pulsing, because §11 reserves red for exactly this. */
.dk-attn-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 12px;
  height: 12px;
  padding: 0 2px;
  border-radius: 6px;
  background: var(--red);
  color: var(--bg);
  font-size: var(--fs-micro);
  line-height: 1;
  font-weight: 700;
  flex: 0 0 auto;
  animation: dk-attn 0.9s var(--ease) infinite;
}
/* 'done' is not an alarm — flag it, but in amber, and stop nagging (§9 flag-only). */
.dk-attn-badge[data-attention="done"] {
  background: var(--amber-dim);
  animation: none;
}

/* ------------------------------------------------------ conflicts (§10) -- */
.dk-file-row[data-conflict="true"] {
  animation: dk-conflict 1.8s var(--ease) infinite;
  color: var(--red);
}
/* ---------------------------------------------- thinking / in-progress --- */
/* thinking.js emits <summary class="dk-thinking-summary">, not a div.dk-thinking-head. */
.dk-thinking[data-streaming="true"] > .dk-thinking-summary::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--cyan);
  animation: dk-pulse-cyan 1.1s var(--ease) infinite;
}

/* ---------------------------------------------------------- reduced motion */
/* Honor the OS setting. Motion is status-bearing, so we do not simply delete
   the signal — we keep the COLOR (which already carries the state) and drop the
   movement. Nothing that only motion communicated is lost, because in this
   design motion is always redundant with color/label. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .dk-cursor {
    animation: none !important;
    opacity: 1;
  }
  .dk-status-dot[data-state="streaming"] { background: var(--amber); }
  .dk-status-dot[data-state="thinking"],
  .dk-status-dot[data-state="tool_running"] { background: var(--cyan); }
  .dk-status-dot[data-state="awaiting_permission"],
  .dk-status-dot[data-state="error"] { background: var(--red); }
  .dk-tool-card[data-state="running"] { border-color: var(--cyan); }
  .dk-file-row[data-conflict="true"] { background: rgb(var(--red-rgb) / 14%); }
}

/* Users can also kill motion from the §19 glow/motion config: js/theme.js sets
   data-motion="off" on <html> when glowIntensity is 0 (spectacle floor). */
:root[data-motion="off"] .dk-status-dot,
:root[data-motion="off"] .dk-cursor,
:root[data-motion="off"] .dk-tool-card,
:root[data-motion="off"] .dk-perm,
:root[data-motion="off"] .dk-file-row,
:root[data-motion="off"] .dk-lane-progress::after,
:root[data-motion="off"] .dk-attn-badge {
  animation: none !important;
}

/* ------------------------------------------------- the machine room (§9) - */
/* The little machines DANCE while they work. steps(1) keeps every move on a
   whole pixel — chunky sprite animation, not smooth tweening. */
@keyframes dk-critter-bob {
  0%, 100% { transform: translateY(0); }
  25% { transform: translateY(-2px) rotate(-2deg); }
  50% { transform: translateY(0) rotate(0deg); }
  75% { transform: translateY(-2px) rotate(2deg); }
}
.dk-critter[data-mood="work"]:not([data-resting="true"]) svg {
  animation: dk-critter-bob 1.15s steps(1, end) infinite;
}
/* Typing is faster, busier — the machine is hammering keys. */
.dk-critter[data-activity="type"] svg {
  animation: dk-critter-bob 0.45s steps(1, end) infinite;
}
@keyframes dk-critter-hand {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-1px); }
}
/* SCOPED to actively-working machines: these part animations used to run UNCONDITIONALLY on
   every critter — including idle tab minis and hidden props — ticking the animation timeline
   forever (perf review, 2026-07-17: 35 constant animations found at rest). */
.dk-critter[data-mood="work"]:not([data-resting="true"]) .crt-hand-l { animation: dk-critter-hand 0.3s steps(1, end) infinite; }
.dk-critter[data-mood="work"]:not([data-resting="true"]) .crt-hand-r { animation: dk-critter-hand 0.3s steps(1, end) 0.15s infinite; }

/* The screen types in dots while working — three pixels, round-robin. */
@keyframes dk-critter-blink { 0%, 100% { opacity: 0.15; } 33% { opacity: 1; } }
.dk-critter[data-activity="type"] .crt-type-1 { animation: dk-critter-blink 0.9s steps(1, end) infinite; }
.dk-critter[data-activity="type"] .crt-type-2 { animation: dk-critter-blink 0.9s steps(1, end) 0.3s infinite; }
.dk-critter[data-activity="type"] .crt-type-3 { animation: dk-critter-blink 0.9s steps(1, end) 0.6s infinite; }

/* Thought bubble pixels breathe upward. */
@keyframes dk-critter-think {
  0%, 100% { opacity: 0.2; transform: translateY(0); }
  50% { opacity: 1; transform: translateY(-1px); }
}
.dk-critter[data-activity="think"] .crt-think-1 { animation: dk-critter-think 1.4s steps(2, end) infinite; }
.dk-critter[data-activity="think"] [data-prop="think"] .crt-think-2 { animation: dk-critter-think 1.4s steps(2, end) 0.5s infinite; }

/* Carried props ride the bob a half-beat behind, like they have real weight. */
.dk-critter[data-activity="doc"] .crt-carry,
.dk-critter[data-activity="mag"] .crt-carry { animation: dk-critter-hand 1.15s steps(1, end) 0.3s infinite; }

/* The gear turns in quarter clicks. */
@keyframes dk-critter-gear { to { transform: rotate(360deg); } }
.dk-critter .crt-gear {
  transform-origin: -3.5px 11.2px;
  transform-box: view-box;
}
.dk-critter[data-activity="gear"] .crt-gear { animation: dk-critter-gear 2.4s steps(8, end) infinite; }

/* Blocked on YOU: the §11 red treatment — an urgent little shake (§9: a
   put-away agent must never hide that it needs you). */
@keyframes dk-critter-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-1px); }
  75% { transform: translateX(1px); }
}
.dk-critter[data-mood="wait"] svg {
  animation: dk-critter-shake 0.4s steps(2, end) infinite;
}

/* Finished: one proud hop, then it fades off and cleans up after itself
   (lanes.js removes the lane when this ends — keep the duration in sync
   with DESPAWN_ANIM_MS). */
@keyframes dk-critter-leave {
  0% { transform: translateY(0); opacity: 1; }
  30% { transform: translateY(-6px); opacity: 1; }
  60% { transform: translateY(0); opacity: 0.8; }
  100% { transform: translateY(0); opacity: 0; }
}
.dk-critter[data-leaving="true"] {
  animation: dk-critter-leave 0.7s var(--ease) forwards;
}

@media (prefers-reduced-motion: reduce) {
  .dk-critter svg,
  .dk-critter .crt-hand-l,
  .dk-critter .crt-hand-r,
  .dk-critter .crt-carry,
  .dk-critter .crt-gear,
  .dk-critter .crt-think-1,
  .dk-critter [data-prop="think"] .crt-think-2 { animation: none !important; }
  .dk-critter[data-leaving="true"] { animation: none; opacity: 0; }
}
:root[data-motion="off"] .dk-critter svg,
:root[data-motion="off"] .dk-critter .crt-hand-l,
:root[data-motion="off"] .dk-critter .crt-hand-r,
:root[data-motion="off"] .dk-critter .crt-carry,
:root[data-motion="off"] .dk-critter .crt-gear,
:root[data-motion="off"] .dk-critter .crt-think-1,
:root[data-motion="off"] .dk-critter [data-prop="think"] .crt-think-2 {
  animation: none !important;
}

/* An idle machine stands still — dancing is earned by doing something. The screen
   dots hold at a dim steady state instead of typing. */
.dk-critter[data-resting="true"] svg { animation: none; }
.dk-critter[data-resting="true"] .crt-type-1,
.dk-critter[data-resting="true"] .crt-type-2,
.dk-critter[data-resting="true"] .crt-type-3 { animation: none; opacity: 0.3; }

/* Idle stand-down: no bow, no hop — the machine just quietly isn't there anymore.
   Duration must match VANISH_ANIM_MS in lanes.js. */
@keyframes dk-critter-vanish { to { opacity: 0; } }
.dk-critter[data-vanishing="true"] { animation: dk-critter-vanish 0.4s var(--ease) forwards; }

/* A new artifact arrived while the dock is closed: the handle pulses amber instead of the
   dock stealing the screen (no auto-popout). */
.dk-artifacts-handle[data-fresh="true"] {
  animation: dk-pulse-amber 1s var(--ease) 3;
  color: var(--amber);
  border-color: var(--amber);
}
:root[data-motion="off"] .dk-artifacts-handle[data-fresh="true"] { animation: none !important; }

/* A workflow just launched while the panel is closed — pulse its handle, don't popout. */
.dk-workflows-handle[data-fresh="true"] {
  animation: dk-pulse-amber 1s var(--ease) 3;
  color: var(--cyan);
  border-color: var(--cyan);
}
:root[data-motion="off"] .dk-workflows-handle[data-fresh="true"] { animation: none !important; }

/* The agent just put a new thread on the board while the panel is closed — pulse, don't popout. */
.dk-threads-handle[data-fresh="true"] {
  animation: dk-pulse-amber 1s var(--ease) 3;
  color: var(--cyan);
  border-color: var(--cyan);
}
:root[data-motion="off"] .dk-threads-handle[data-fresh="true"] { animation: none !important; }

/* Alive pulse — a blinking prompt block, the terminal's own "working" idiom. */
@keyframes dk-alive-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.12; } }
.dk-alive-block { animation: dk-alive-blink 1.1s steps(1, end) infinite; }
@media (prefers-reduced-motion: reduce) { .dk-alive-block { animation: none; opacity: 0.6; } }
:root[data-motion="off"] .dk-alive-block { animation: none !important; opacity: 0.6; }


/* ---- Compositor-friendly pulse compensation (see the dk-pulse-* keyframes) -----------------
   The glow/border these pulses used to ANIMATE is now painted ONCE, statically, on the pulse
   users where it was the point — the opacity breathing carries the motion. */
.dk-tool-card[data-state="running"] {
  border-color: rgb(var(--cyan-rgb) / 70%);
  box-shadow: var(--glow-box-cyan, none);
}
.dk-perm[data-pending="true"] {
  box-shadow: 0 0 calc(5px * var(--glow-strength, 1)) rgb(var(--red-rgb) / 60%);
}



/* Zzz pixels breathe while a machine sleeps — slow, dim, opacity only. */
@keyframes dk-critter-zzz { 0%, 100% { opacity: 0.15; } 50% { opacity: 0.9; } }
.dk-critter[data-mood="sleep"] .crt-zzz-1 { animation: dk-critter-zzz 2.6s ease-in-out infinite; }
.dk-critter[data-mood="sleep"] .crt-zzz-2 { animation: dk-critter-zzz 2.6s ease-in-out 0.9s infinite; }
@media (prefers-reduced-motion: reduce) {
  .dk-critter[data-mood="sleep"] .crt-zzz-1,
  .dk-critter[data-mood="sleep"] .crt-zzz-2 { animation: none; opacity: 0.5; }
}
