/* ===========================================================================
   GitGraph — visual styles + delta animations for the SVG commit-graph widget.
   Used by both the interactive tutorial (js/tutorial-code.js) and the
   standalone SEBook command-lab / diagram embeds (js/git-command-lab.js,
   js/git-graph.js).

   Keep this file free of tutorial-layout-specific styles (tvm-*); see
   tutorial.css for those.
   =========================================================================== */

/* Empty state */
.git-graph-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #888;
  font-family: system-ui, sans-serif;
  font-size: 14px;
}
.git-graph-empty code {
  background: #2a2a3a;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 13px;
}

/* SVG styling */
.git-graph-svg {
  display: block;
  /* Bootstrap sets svg:not(:root) { overflow: hidden } which clips the HEAD
     glow and drop-shadow burst at the SVG boundary. Override it explicitly. */
  overflow: visible !important;
}
.git-graph-svg .git-graph-node {
  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.git-graph-svg .git-graph-head-glow {
  animation: git-graph-pulse 2s ease-in-out infinite;
}
/* Pulse owns stroke-opacity exclusively. Other HEAD-glow animations
   (head-pulse burst, glow-enter/exit cross-fade, node-enter pop-in)
   animate DIFFERENT properties (transform, stroke-width, opacity), so
   pulse can be layered with any of them without property conflicts —
   which is why every compound animation declaration on .head-glow
   includes `git-graph-pulse 2s infinite` alongside the one-shots. This
   keeps the pulse visibly alive at all times, instead of having the
   infinite rule silently replaced by a shorter one-shot the way it
   would be if multiple animations claimed stroke-opacity. */
@keyframes git-graph-pulse {
  0%, 100% { stroke-opacity: 0.3; }
  50% { stroke-opacity: 0.7; }
}

/* Offscreen pausing intentionally removed. IntersectionObserver doesn't
   reliably fire for SVGs inside nested scroll containers (SEBook's
   `main` has its own overflow-y), leaving graphs stuck with the
   `--offscreen` class and their pulse frozen even after they scroll into
   view. The pulse itself only animates stroke-opacity on a single ring
   per graph, so running all 28 on SEBook/tools/git is cheap — a tiny
   per-frame compositor cost that's not worth a fragile visibility hack. */

/* === Git graph delta animations =========================================== */

/* Synchronised motion: nodes, branch labels, HEAD, edges, and chip colors
   all share the exact same duration + ease-in-out curve so the commit circle,
   its branch label, the HEAD chip, and the connecting edges glide together
   as one cohesive unit when the layout shifts.

   `will-change: transform` is NOT set statically here — it's scoped to the
   actual transition window by js/git-graph.js (_setTransformAnimated), which
   sets it inline just before mutating `transform` and clears it after the
   720ms transition completes. Pages with many cards otherwise accumulate
   hundreds of idle compositor layers, at which point Chrome starts evicting
   them and each animation pays a re-promote cost on its first frame. */
.git-graph-svg .git-graph-node-g,
.git-graph-svg .git-graph-label-g,
.git-graph-svg .git-graph-label-g--head {
  transition: transform 720ms cubic-bezier(.7,0,.3,1);
}

.git-graph-svg .git-graph-head-chip {
  transition: fill 720ms cubic-bezier(.7,0,.3,1),
              stroke 720ms cubic-bezier(.7,0,.3,1),
              fill-opacity 720ms cubic-bezier(.7,0,.3,1);
}
.git-graph-svg .git-graph-head-text {
  transition: fill 720ms cubic-bezier(.7,0,.3,1);
}

/* Edge shafts morph via `d`; explicit arrowheads are separate paths whose
   transform follows the shaft endpoint. This avoids SVG marker jitter while
   keeping edge motion in sync with nodes and labels. */
.git-graph-svg .git-graph-edge-shaft {
  transition: stroke-dashoffset 720ms cubic-bezier(.7,0,.3,1),
              stroke 720ms cubic-bezier(.7,0,.3,1),
              d 720ms cubic-bezier(.7,0,.3,1);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.git-graph-svg .git-graph-edge-head {
  transition: transform 720ms cubic-bezier(.7,0,.3,1),
              fill 720ms cubic-bezier(.7,0,.3,1),
              opacity 720ms cubic-bezier(.7,0,.3,1);
}

/* Mobile/low-power fast path: keep semantics intact, drop expensive visual
   effects and shorten transitions so interactions stay responsive. Enabled by
   js/git-graph.js via `.git-graph-svg--perf-lite` / `.git-workbench--perf-lite`. */
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node {
  filter: none !important;
}
.git-graph-svg.git-graph-svg--perf-lite .git-graph-head-glow {
  animation: none !important;
  stroke-opacity: 0.45;
}
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-label-g,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-label-g--head {
  transition: transform 220ms cubic-bezier(.2,0,.2,1) !important;
}
.git-graph-svg.git-graph-svg--perf-lite .git-graph-edge-shaft {
  transition: opacity 180ms linear, stroke 180ms linear !important;
}
.git-graph-svg.git-graph-svg--perf-lite .git-graph-edge-head {
  transition: transform 220ms cubic-bezier(.2,0,.2,1),
              fill 180ms linear,
              opacity 180ms linear !important;
}
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g.entering > .git-graph-node,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g.entering > .git-graph-hash,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g.entering > .git-graph-head-glow,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g.entering > .git-graph-message,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-label-g.entering,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g.head-pulse > .git-graph-head-glow,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-node-g.git-graph-merge-burst > .git-graph-node,
.git-graph-svg.git-graph-svg--perf-lite .git-graph-label-g.git-graph-label-burst {
  animation: none !important;
}

.git-workbench--perf-lite .git-workbench-row,
.git-workbench--perf-lite .git-workbench-stash-entry {
  transition: transform 220ms cubic-bezier(.2,0,.2,1),
              color 160ms linear,
              background-color 160ms linear !important;
}
.git-workbench--perf-lite .git-workbench-row.git-workbench-row-flip,
.git-workbench--perf-lite .git-workbench-stash-entry.git-workbench-row-flip,
.git-workbench--perf-lite .git-workbench-row.git-workbench-row-fly,
.git-workbench--perf-lite .git-workbench-stash-entry.git-workbench-row-fly,
.git-workbench--perf-lite .git-workbench-row.git-workbench-row-burst,
.git-workbench--perf-lite .git-workbench-stash-entry.git-workbench-row-burst,
.git-workbench--perf-lite .git-workbench-row.entering,
.git-workbench--perf-lite .git-workbench-stash-entry.entering,
.git-workbench--perf-lite .git-workbench-row.exiting,
.git-workbench--perf-lite .git-workbench-stash-entry.exiting {
  animation: none !important;
  filter: none !important;
}

/* New commit pop-in: scale + opacity bounce.
   transform-origin 0 0 because each commit's content is centered on (0,0)
   in its wrapper-local coordinate system. */
@keyframes git-graph-node-enter {
  0%   { transform: scale(0);    opacity: 0; }
  60%  { transform: scale(1.18); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}
.git-graph-svg .git-graph-node-g.entering > .git-graph-node,
.git-graph-svg .git-graph-node-g.entering > .git-graph-hash {
  transform-origin: 0 0;
  animation: git-graph-node-enter 480ms cubic-bezier(.34,1.56,.64,1) backwards;
}
/* The glow keeps the same pop-in, but also carries the infinite pulse so
   it starts ringing from the first frame — layered on a different
   property (scale + opacity vs. stroke-opacity) so neither overrides
   the other. */
.git-graph-svg .git-graph-node-g.entering > .git-graph-head-glow {
  transform-origin: 0 0;
  animation: git-graph-node-enter 480ms cubic-bezier(.34,1.56,.64,1) backwards,
             git-graph-pulse 2s ease-in-out infinite;
}
.git-graph-svg .git-graph-node-g.entering > .git-graph-message {
  animation: git-graph-fade-in 480ms ease-out 120ms backwards;
}

/* Label fade-in (also used by HEAD chip) */
@keyframes git-graph-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.git-graph-svg .git-graph-label-g.entering {
  animation: git-graph-fade-in 380ms ease-out;
}

/* Removed commit / label / edge: fade and shrink */
@keyframes git-graph-node-exit {
  0%   { transform: scale(1);   opacity: 1; }
  100% { transform: scale(0.4); opacity: 0; }
}
.git-graph-svg .git-graph-node-g.exiting > .git-graph-node,
.git-graph-svg .git-graph-node-g.exiting > .git-graph-hash,
.git-graph-svg .git-graph-node-g.exiting > .git-graph-head-glow {
  transform-origin: 0 0;
  animation: git-graph-node-exit 420ms ease-in forwards;
}
.git-graph-svg .git-graph-node-g.exiting > .git-graph-message,
.git-graph-svg .git-graph-label-g.exiting {
  animation: git-graph-fade-out 350ms ease-in forwards;
}
.git-graph-svg .git-graph-edge.exiting {
  opacity: 0;
}
@keyframes git-graph-fade-out {
  to { opacity: 0; }
}

/* HEAD glow cross-fade. When HEAD moves from one commit to another, the
   label wrapper slides over 720ms; these animations delay the new glow's
   appearance so it shows up as HEAD arrives (second half of the slide)
   and fades the old glow as HEAD leaves (first half). Pairs with the
   JS-added .glow-entering / .glow-exiting classes. */
@keyframes git-graph-glow-enter {
  0%, 50% { opacity: 0; }
  100%    { opacity: 1; }
}
@keyframes git-graph-glow-exit {
  0%        { opacity: 1; }
  50%, 100% { opacity: 0; }
}
.git-graph-svg .git-graph-head-glow.glow-entering {
  animation: git-graph-glow-enter 720ms cubic-bezier(.7,0,.3,1) forwards,
             git-graph-pulse 2s ease-in-out infinite;
}
.git-graph-svg .git-graph-head-glow.glow-exiting {
  animation: git-graph-glow-exit 720ms cubic-bezier(.7,0,.3,1) forwards,
             git-graph-pulse 2s ease-in-out infinite;
}

/* HEAD pulse burst — the glow's scale + stroke-width pop. Shares duration
   + peak timing (750ms @ 35%) with the yellow commit/label burst so when
   HEAD lands and the commit/label flashes, the halo rings out at the
   exact same beat. Deliberately does NOT animate stroke-opacity — that
   property is owned by git-graph-pulse (infinite), which layers on this
   via the compound animation declaration below. */
@keyframes git-graph-head-pulse {
  0%, 100% { transform: scale(1);   stroke-width: 2; }
  35%      { transform: scale(1.5); stroke-width: 5; }
}
.git-graph-svg .git-graph-node-g.head-pulse > .git-graph-head-glow:not(.glow-entering):not(.glow-exiting) {
  transform-origin: 0 0;
  animation: git-graph-head-pulse 750ms cubic-bezier(.4,0,.2,1),
             git-graph-pulse 2s ease-in-out infinite;
}

/* When the glow is both entering (HEAD just arrived at a new commit) AND
   the parent has head-pulse, stack all three effects. Since each targets
   a different property (opacity / transform+stroke-width / stroke-opacity)
   they run in parallel without overriding each other — no 900ms delay on
   the pulse is needed, which was a workaround for the old keyframes where
   head-pulse also claimed stroke-opacity and masked the infinite pulse. */
.git-graph-svg .git-graph-node-g.head-pulse > .git-graph-head-glow.glow-entering {
  transform-origin: 0 0;
  animation: git-graph-glow-enter 720ms cubic-bezier(.7,0,.3,1) forwards,
             git-graph-head-pulse 750ms cubic-bezier(.4,0,.2,1),
             git-graph-pulse 2s ease-in-out infinite;
}

html.dark-mode .git-graph-svg .git-graph-head-glow {
  stroke: #FFD100 !important;
  stroke-opacity: 0.85 !important;
}

/* Commit burst — yellow/white flash when a node is freshly created (any
   commit, not just merges). Fires during the entry scale animation so the
   commit appears in a little burst of light. */
@keyframes git-graph-merge-burst {
  0%   { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); }
  35%  { filter: drop-shadow(0 0 14px rgba(255,255,255,0.95))
                 drop-shadow(0 0 24px rgba(255,200,28,0.85)); }
  100% { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); }
}
.git-graph-svg .git-graph-node-g.git-graph-merge-burst > .git-graph-node {
  animation: git-graph-merge-burst 750ms ease-out;
}

/* Pointer burst — same yellow flash on branch labels (and HEAD) whose
   target commit hash changed. Identical duration + peak timing as the
   commit-creation burst so when a command both creates a commit and
   moves a pointer (e.g. git commit, merge --no-ff), both flashes ignite
   at the exact same moment. */
@keyframes git-graph-label-burst {
  0%   { filter: none; }
  35%  { filter: drop-shadow(0 0 6px rgba(255,255,255,0.95))
                 drop-shadow(0 0 10px rgba(255,200,28,0.9)); }
  100% { filter: none; }
}
.git-graph-svg .git-graph-label-g.git-graph-label-burst {
  animation: git-graph-label-burst 750ms ease-out;
}

/* Light-mode tweaks for the shadow */
html:not(.dark-mode) .git-graph-svg .git-graph-node {
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.15));
}
html:not(.dark-mode) .git-graph-empty {
  color: #666;
}
html:not(.dark-mode) .git-graph-empty code {
  background: #e8eaed;
  color: #333;
}

/* Theme tokens — default to light-mode colors. Every direct container of a
   rendered .git-graph-svg sets these so the commit-message halo renders
   correctly even when the SVG is embedded outside the tutorial layout
   (static SEBook diagrams, command-lab cards, popup modal). The tutorial's
   .tvm-git-graph-container sets the same vars itself in tutorial.css. */
.git-graph-canvas,
.git-command-lab__graph,
.git-command-lab-modal,
.uml-gitgraph-diagram-container {
  --git-graph-text: #000000;
  --git-graph-bg: #fafbfc;
}
html.dark-mode .git-graph-canvas,
html.dark-mode .git-command-lab__graph,
html.dark-mode .git-command-lab-modal,
html.dark-mode .uml-gitgraph-diagram-container {
  --git-graph-text: #ffffff;
  --git-graph-bg: #1a1a2e;
}

/* portfolio.css applies `filter: invert(1) hue-rotate(180deg)` to every SVG
   under .post-content in dark mode so generic SVG icons/diagrams flip to
   light-on-dark. The git-graph already themes itself via CSS variables
   (--git-graph-text/--git-graph-bg) and its branch colors are chosen to
   work in both modes, so we must opt out of the global inversion to avoid
   double-flipping colors (black-on-white messages instead of white-on-dark).*/
html.dark-mode .post-content svg.git-graph-svg {
  filter: none !important;
}

/* ===========================================================================
   Workbench — HTML panel above the commit graph that mimics `git status`
   output. Three zones (Untracked / Not staged / Staged) + an optional stash
   shelf. Only rendered when a lab's spec declares a `files` object, so labs
   about pure-graph operations (merge/rebase/switch/cherry-pick) are visually
   unchanged.
   =========================================================================== */

.git-workbench {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 8px;
  font-family: 'Fira Code', 'Cascadia Code', Menlo, monospace;
  font-size: 12.5px;
  line-height: 1.35;
  color: #1f2d3d;
  /* `min-height` is managed by js/git-graph.js (`_lockWorkbenchHeight`),
     which pins it to the largest natural height the panel has ever had
     for this lab. A `git commit` / `git add` that flies a row into the
     commit no longer shrinks the panel when the row is removed, so the
     SVG below it doesn't get pulled up — the only motion the user sees
     is the row itself flying, not the whole graph jumping. An empty
     zone reads exactly like `git status` would ("nothing staged"), so
     the reserved space is meaningful rather than wasted. */
}
/* The workbench is shown whenever a `files` spec is declared, even if all
   zones are empty — an "all clean" strip after `git reset --hard` or
   `git stash` is pedagogically meaningful. */

.git-workbench-strip {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  flex: 1 1 360px;
  min-width: 0;
  background: #f7f8fa;
  border: 1px solid #dde2e8;
  border-radius: 6px;
  padding: 5px 0;
}
.git-workbench-zone {
  padding: 0 10px;
  min-width: 0;
  border-right: 1px dashed #dde2e8;
}
.git-workbench-zone:last-child { border-right: none; }
.git-workbench-zone-header {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #5a6573;
  margin: 0 0 3px;
  padding: 0;
  line-height: 1.2;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: transparent;
  border: none;
}
.git-workbench-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  min-height: 0;
}

.git-workbench-row,
.git-workbench-stash-entry {
  list-style: none;
  display: flex;
  gap: 6px;
  padding: 1px 4px;
  margin: 0;
  border-radius: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Shared 720ms cubic-bezier curve so row movement reads as the same
     cohesive motion as the commit/label transitions below. */
  transition: transform 720ms cubic-bezier(.7,0,.3,1),
              color 720ms cubic-bezier(.7,0,.3,1),
              background-color 720ms cubic-bezier(.7,0,.3,1),
              filter 720ms cubic-bezier(.7,0,.3,1);
}
/* Scope `will-change: transform` to the transient classes that actually
   animate rows. Keeping it off idle rows prevents pages with many workbench
   instances from creating dozens of always-on compositor layers. */
.git-workbench-row.entering,
.git-workbench-stash-entry.entering,
.git-workbench-row.exiting,
.git-workbench-stash-entry.exiting,
.git-workbench-row.git-workbench-row-flip,
.git-workbench-stash-entry.git-workbench-row-flip,
.git-workbench-row.git-workbench-row-fly,
.git-workbench-stash-entry.git-workbench-row-fly {
  will-change: transform;
}
.git-workbench-status {
  flex: 0 0 auto;
  font-weight: 600;
  width: 5.5em;
  text-align: left;
}
.git-workbench-path {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Per-zone colors match git's own terminal output: red for unstaged/untracked,
   green for staged. */
.git-workbench-row--untracked {
  color: #b03a2e;
}
.git-workbench-row--untracked .git-workbench-path::before {
  content: '? ';
  color: #b03a2e;
  font-weight: 700;
  opacity: 0.65;
}
.git-workbench-row--unstaged {
  color: #b03a2e;
}
.git-workbench-row--staged {
  color: #1a7d5a;
}
.git-workbench-stash-entry {
  color: #6b3fa0;
}

/* Stash shelf — standalone aside to the right of the strip. Rendered only
   when the spec declares stash entries (the DOM is removed entirely when
   empty). */
.git-workbench-shelf {
  flex: 0 1 280px;
  min-width: 0;
  background: rgba(107, 63, 160, 0.06);
  border: 1px solid rgba(107, 63, 160, 0.35);
  border-radius: 6px;
  padding: 5px 10px;
  align-self: stretch;
}
.git-workbench-shelf-header {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #6b3fa0;
  margin: 0 0 3px;
  padding: 0;
  line-height: 1.2;
  display: block;
  background: transparent;
  border: none;
}

/* ----- Animations — matching the graph's motion language ----- */

/* Row enter: scale bounce + fade, same 480ms ease as commit-entry. */
@keyframes git-workbench-row-enter {
  0%   { opacity: 0; transform: scale(0.85) translateX(-8px); }
  60%  { opacity: 1; transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}
.git-workbench-row.entering,
.git-workbench-stash-entry.entering {
  animation: git-workbench-row-enter 480ms cubic-bezier(.34,1.56,.64,1);
}

/* FLIP zone-change: the JS measures the row's old → new delta and feeds it
   back as `--fx`/`--fy`. The keyframe starts the row at its old position
   (from the delta) and tweens to its new position (0,0) — reads as the
   row sliding smoothly between zones rather than snapping. Uses the same
   720ms cubic-bezier as the commit/label slides below. */
@keyframes git-workbench-row-flip {
  from { transform: translate(var(--fx, 0), var(--fy, 0)); }
  to   { transform: translate(0, 0); }
}
.git-workbench-row.git-workbench-row-flip,
.git-workbench-stash-entry.git-workbench-row-flip {
  animation: git-workbench-row-flip 720ms cubic-bezier(.7,0,.3,1);
}

/* Fly-into-commit: when a staged or unstaged row is committed, it translates
   from its current position down into the new commit node while shrinking
   and fading. `--fx`/`--fy` carry the delta to the node's center. Runs
   alongside the same yellow-gold burst flash used by zone-change moves, so
   the "into the commit" moment glows with the same vocabulary as the rest
   of the graph. */
@keyframes git-workbench-row-fly {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(var(--fx, 0), var(--fy, 0)) scale(0.25); opacity: 0; }
}
.git-workbench-row.git-workbench-row-fly,
.git-workbench-stash-entry.git-workbench-row-fly {
  animation-name: git-workbench-row-fly, git-workbench-row-burst;
  animation-duration: 480ms, 480ms;
  animation-timing-function: cubic-bezier(.55,.08,.68,.53), ease-out;
  animation-fill-mode: forwards, none;
  pointer-events: none;
}

/* Row exit: fade + shrink. */
@keyframes git-workbench-row-exit {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.8); }
}
.git-workbench-row.exiting,
.git-workbench-stash-entry.exiting {
  animation: git-workbench-row-exit 380ms ease-in forwards;
  pointer-events: none;
}

/* Yellow burst — same gold glow + peak timing as the commit/label bursts
   (750ms total, peak at 35%). Fires on row zone-change and on fresh inserts,
   so "git add" / "git commit" / "git stash" feel visually identical to the
   graph's own commit animations. */
@keyframes git-workbench-row-burst {
  0%   { filter: none; background-color: transparent; }
  35%  { filter: drop-shadow(0 0 8px rgba(255,255,255,0.9))
                 drop-shadow(0 0 14px rgba(255,200,28,0.85));
         background-color: rgba(255, 220, 70, 0.35); }
  100% { filter: none; background-color: transparent; }
}

/* When a row is FLIP-ing and bursting at the same time (zone change): run
   both keyframes simultaneously. This is the common case for git add /
   git restore / git stash / stash pop — the row slides into its new zone
   while flashing gold. */
.git-workbench-row.git-workbench-row-flip.git-workbench-row-burst,
.git-workbench-stash-entry.git-workbench-row-flip.git-workbench-row-burst {
  animation-name: git-workbench-row-flip, git-workbench-row-burst;
  animation-duration: 720ms, 750ms;
  animation-timing-function: cubic-bezier(.7,0,.3,1), ease-out;
  animation-fill-mode: both, none;
}

/* Burst only (no flip) — a row freshly appearing or marked for emphasis
   without a position change. */
.git-workbench-row.git-workbench-row-burst:not(.git-workbench-row-flip):not(.entering),
.git-workbench-stash-entry.git-workbench-row-burst:not(.git-workbench-row-flip):not(.entering) {
  animation: git-workbench-row-burst 750ms ease-out;
}

/* Burst + enter — a fresh row that's also worth flashing (e.g. a first-time
   render). Runs both in parallel. */
.git-workbench-row.entering.git-workbench-row-burst,
.git-workbench-stash-entry.entering.git-workbench-row-burst {
  animation-name: git-workbench-row-enter, git-workbench-row-burst;
  animation-duration: 480ms, 750ms;
  animation-timing-function: cubic-bezier(.34,1.56,.64,1), ease-out;
}

/* ----- Dark-mode theming ----- */
html.dark-mode .git-workbench {
  color: #e6edf3;
}
html.dark-mode .git-workbench-strip {
  background: #141a24;
  border-color: #2f3a4a;
}
html.dark-mode .git-workbench-zone {
  border-right-color: #2f3a4a;
}
html.dark-mode .git-workbench-zone-header,
html.dark-mode .git-workbench-shelf-header {
  color: #9aa6b2;
}
html.dark-mode .git-workbench-row--untracked,
html.dark-mode .git-workbench-row--unstaged {
  color: #f1948a;
}
html.dark-mode .git-workbench-row--untracked .git-workbench-path::before {
  color: #f1948a;
}
html.dark-mode .git-workbench-row--staged {
  color: #4ade80;
}
html.dark-mode .git-workbench-stash-entry {
  color: #c29bf0;
}
html.dark-mode .git-workbench-shelf {
  background: rgba(194, 155, 240, 0.08);
  border-color: rgba(194, 155, 240, 0.35);
}

/* ----- Print styling: keep the workbench visible above each graph ----- */
@media print {
  .git-workbench {
    font-size: 8pt;
    line-height: 1.3;
    margin-bottom: 6px;
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .git-workbench-strip {
    padding: 6px 0;
  }
  .git-workbench-zone-header,
  .git-workbench-shelf-header {
    font-size: 7.5pt;
  }
  .git-workbench-row,
  .git-workbench-stash-entry {
    transition: none;
  }
}

/* ----- Responsive: on narrow viewports let zones stack ----- */
@media (max-width: 560px) {
  .git-workbench-strip {
    grid-template-columns: 1fr;
    grid-auto-rows: min-content;
    gap: 2px 0;
    padding: 4px 0;
  }
  .git-workbench-zone {
    border-right: none;
    border-bottom: 1px dashed #dde2e8;
    padding: 2px 10px 4px;
  }
  .git-workbench-zone:last-child { border-bottom: none; padding-bottom: 2px; }
  html.dark-mode .git-workbench-zone { border-bottom-color: #2f3a4a; }
  .git-workbench-rows { min-height: 0; }
  .git-workbench-shelf { flex: 1 1 100%; padding: 4px 10px; }
  /* On mobile, collapse empty zones to just their header + tiny padding so
     students still see that the zone exists but it doesn't waste space. */
  .git-workbench-zone:has(.git-workbench-rows:empty) {
    opacity: 0.55;
    padding-bottom: 2px;
  }
  .git-workbench-zone:has(.git-workbench-rows:empty) .git-workbench-zone-header {
    margin-bottom: 0;
  }
}

/* Respect OS-level "reduce motion" — disable every animation and
   transition inside git graphs and workbenches. State still changes
   correctly, just without the glide/pulse/burst effects. The graphs
   remain fully interactive; nothing functional depends on animation
   completion. Uses attribute-prefix selectors so it matches the actual
   flat class names (`.git-graph-svg`, `.git-workbench-row`, …) rather
   than expecting a nested `.git-graph > *` structure. */
@media (prefers-reduced-motion: reduce) {
  [class^="git-graph-"],
  [class*=" git-graph-"],
  [class^="git-workbench-"],
  [class*=" git-workbench-"] {
    animation: none !important;
    transition: none !important;
  }
}
