/* ══════════════════════════════════════════════════════════════════════════
   Dough — presentation layer for the mascot and the surfaces he speaks from.
   ──────────────────────────────────────────────────────────────────────────
   Two parts:

     1. Motion. Every animation is idle-loop scale — a few pixels, a few
        degrees, several seconds long. Dough has to survive being on screen
        the entire time the user is reading a balance, and anything livelier
        than this becomes something you want to close. All of it is switched
        off wholesale under prefers-reduced-motion at the bottom of the file.

     2. Components. The card / bubble / empty-state / avatar shells that carry
        Dough's voice. They derive from design-system.css tokens, so they
        follow every theme without a per-theme rule.

   There is deliberately no third part for *colour*. Dough's artwork is a
   photograph of a finished brand asset (static/img/dough_V2.jpg) and nothing
   here paints him. This file used to derive ~12 --dough-* colour tokens that
   the vector redraw filled itself with, so that his fur tracked each theme's
   accent. That layer went when the redraw did: you cannot re-tint a raster
   without altering the artwork, and altering the artwork is the one thing
   AGENTS.md forbids outright. Dough is the same golden puppy on all 16
   themes, and the theme shows up in the disc, the bubble and the panel he
   sits on — the chrome around him, never him.

   The two structural pieces, because the selectors only make sense together:

     [data-dough]   the slot. Owns the entrance and the hover, and draws the
                    ground shadow.
     .dough         the <img> dough.js puts inside it. Owns the idle motion.

   They are separate because both animate `transform`, and one element cannot
   run an entrance and an idle loop on the same property without the second
   cancelling the first.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Two easing curves do most of the work here:
       --dough-ease    a soft symmetric curve for idle loops. Gentler in and
                       out than ease-in-out, whose linear-feeling middle makes
                       slow loops look mechanical.
       --dough-spring  a mild overshoot for anything that lands. */
  --dough-ease:   cubic-bezier(.45, .05, .35, .95);
  --dough-spring: cubic-bezier(.34, 1.42, .64, 1);
}

:root,
.dough-avatar,
.dough-bubble,
.dough-empty,
.dough-working {
  /* The accent the *chrome* wears — the avatar disc, the bubble fill and its
     tail. Declared on the components rather than only on :root so a card or a
     branded section that sets its own --red is followed, instead of every
     placement freezing to whatever the accent was at the document root. */
  --dough-accent: var(--red, #7c3aed);

  /* The contact shadow under a full-body Dough, replacing the one that was
     baked into the reference and removed with its background — a fixed grey
     ellipse cannot follow a theme, and on the dark schemes it read as a smear
     of lighter paint under his paws. */
  --dough-ground: color-mix(in srgb, #000000 14%, transparent);

  /* Confetti spot colours. Mixed toward the accent so a celebration reads as
     this product celebrating rather than as generic party stock. */
  --dough-confetti-b: color-mix(in oklch, #14b8a6, var(--dough-accent) 22%);
  --dough-confetti-c: color-mix(in oklch, #f5b301, var(--dough-accent) 22%);
}

[data-scheme="dark"] {
  --dough-ground: color-mix(in srgb, #000000 40%, transparent);
}

/* ── The slot ─────────────────────────────────────────────────────────────
   inline-block so it can be transformed, line-height 0 so it does not carry
   a text baseline's worth of dead space under the image. */
[data-dough] {
  display: inline-block;
  position: relative;
  line-height: 0;
}

.dough {
  display: block;
  /* A soft lift off the panel. Cheap: one drop-shadow on a composited layer,
     and it replaces what the removed background shading was doing. */
  filter: drop-shadow(0 2px 3px color-mix(in srgb, #000000 10%, transparent));
}

/* The ground shadow, on the slot's own ::before so it stays on the floor while
   he bounces. A shadow that leaves the ground with the dog is what makes a
   bounce read as "the whole picture moved" instead of "he jumped".

   ::before rather than ::after: both are children of the slot, and ::before
   paints beneath the <img> without needing a z-index that a stacking context
   somewhere up the tree could break. Full-body only — an avatar disc is a
   portrait and has no floor in it. */
[data-dough]:not([data-dough-tight])::before {
  content: '';
  position: absolute;
  left: 14%;
  right: 14%;
  bottom: 1%;
  height: 5%;
  border-radius: 50%;
  background: var(--dough-ground);
  filter: blur(4px);
}

/* ══════════════════════════════════════════════════════════════════════════
   Motion
   ──────────────────────────────────────────────────────────────────────────
   Everything animates transform or opacity only, so every loop stays on the
   compositor. There are placements with a dozen Doughs on screen at once.

   The artwork is one pose, so motion is now the whole of the difference
   between one mood and another. It is also the *only* thing allowed to
   differ: these move the image, they never modify it.
   ══════════════════════════════════════════════════════════════════════════ */

/* Breathe — near-invisible on purpose. Dough present but not acting:
   sleeping, concerned, proud. */
@keyframes dough-breathe {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-1.2%) scale(1.012); }
}
.dough-anim-breathe {
  transform-origin: 50% 100%;
  animation: dough-breathe 3.8s var(--dough-ease) infinite;
}

/* Float — the resting idle, and what `blink` became. The old blink squashed
   the eye group vertically, which needed an eye group to squash; a raster has
   no parts. A slow vertical drift is the equivalent cheap sign of life. */
@keyframes dough-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-2.5%); }
}
.dough-anim-blink {
  transform-origin: 50% 100%;
  animation: dough-float 4.6s var(--dough-ease) infinite;
}

/* Head tilt — the "I'm working on it" pose, for thinking and curious. It
   rotates the whole image now rather than the head on the neck, so the angle
   is smaller: what reads as a cocked head at 4.5 degrees reads as a picture
   hung crooked at the same angle applied to the body too. */
@keyframes dough-tilt {
  0%, 100% { transform: rotate(-2.2deg); }
  50%      { transform: rotate(1.4deg); }
}
.dough-anim-tilt {
  transform-origin: 50% 90%;
  animation: dough-tilt 3.4s var(--dough-ease) infinite;
}

/* Sweep — Dough casting about while he digs through transactions. The
   magnifier prop that used to do this was drawn artwork and went with the
   redraw; the searching *motion* survives it. */
@keyframes dough-sweep {
  0%, 100% { transform: translateX(0) rotate(0deg); }
  35%      { transform: translateX(-2.5%) rotate(-2deg); }
  70%      { transform: translateX(2%) rotate(1.5deg); }
}
.dough-anim-sweep {
  transform-origin: 50% 90%;
  animation: dough-sweep 3.1s var(--dough-ease) infinite;
}

/* Bounce — reserved for genuine good news (goal met, streak held), never for
   routine state changes, or the celebration stops meaning anything. The
   squash-and-stretch is asymmetric: he compresses on landing, not in the air. */
@keyframes dough-bounce {
  0%, 100% { transform: translateY(0) scale(1, 1); }
  28%      { transform: translateY(-5.5%) scale(.982, 1.024); }
  55%      { transform: translateY(0) scale(1.028, .974); }
  72%      { transform: translateY(-1.6%) scale(.995, 1.006); }
  88%      { transform: translateY(0) scale(1.006, .994); }
}
.dough-anim-bounce {
  transform-origin: 50% 100%;
  animation: dough-bounce 1.9s var(--dough-spring) infinite;
}

/* The ground shadow answers the bounce — it tightens and fades as he lifts. */
@keyframes dough-ground-bounce {
  0%, 100% { transform: scaleX(1);    opacity: 1; }
  28%      { transform: scaleX(.86);  opacity: .62; }
  55%      { transform: scaleX(1.04); opacity: 1; }
  72%      { transform: scaleX(.95);  opacity: .85; }
}
[data-dough]:has(.dough-anim-bounce)::before {
  animation: dough-ground-bounce 1.9s var(--dough-spring) infinite;
}

/* There is no wag. It was a rotation of the tail path, and a raster has no
   parts to rotate — the tail cannot move without the dog moving with it.
   Rather than keep a `.dough-wag` class that composes a second animation onto
   the mood's own (which needs one cascade rule per mood, and had `wag-fast`
   silently losing to `wag + bounce` on the only two moods that used it), the
   wag is simply gone. The moods it marked already run the liveliest idles. */

/* ══════════════════════════════════════════════════════════════════════════
   State effects — the props, as UI
   ──────────────────────────────────────────────────────────────────────────
   The magnifier, the thought dots, the sleep z's and the confetti used to be
   SVG drawn into the mascot. They went with the redraw, and putting them back
   as artwork would be drawing on Dough again.

   They are better here anyway. These sit *beside* him in the page, follow the
   theme accent (they annotate the UI, so they should look like the UI), and
   the thing they used to communicate — "something is generating" — is now
   carried by a real text label that a screen reader can read and a translator
   can translate.
   ══════════════════════════════════════════════════════════════════════════ */

/* Thinking dots. Staggered so the eye reads a direction, the way typing
   indicators do. */
@keyframes dough-dot {
  0%, 100% { opacity: .25; transform: translateY(2px); }
  50%      { opacity: 1;   transform: translateY(-2px); }
}
.dough-dots {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  vertical-align: middle;
}
.dough-dots i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--dough-accent);
  animation: dough-dot 1.4s var(--dough-ease) infinite;
}
.dough-dots i:nth-child(2) { animation-delay: .16s; }
.dough-dots i:nth-child(3) { animation-delay: .32s; }

/* Confetti. Positioned against the slot rather than the dog, and pointer-events
   none so a celebration never eats a click on whatever is underneath. */
@keyframes dough-fall {
  0%   { opacity: 0; transform: translateY(-40%) rotate(0deg); }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(220%) rotate(320deg); }
}
.dough-confetti {
  position: absolute;
  inset: -10% -20% 0;
  pointer-events: none;
  overflow: hidden;
}
.dough-confetti i {
  position: absolute;
  top: 0;
  /* Spread across the width, and give each piece its own timing from --i so
     twelve elements need one rule rather than twelve. */
  left: calc(var(--i) * 8.3%);
  width: 6px;
  height: 9px;
  border-radius: 2px;
  background: var(--dough-accent);
  animation: dough-fall 2.6s linear infinite;
  animation-delay: calc(var(--i) * .17s);
}
.dough-confetti i:nth-child(3n)   { background: var(--dough-confetti-b); border-radius: 50%; }
.dough-confetti i:nth-child(3n+1) { background: var(--dough-confetti-c); }

/* ── Entrance ────────────────────────────────────────────────────────────
   Dough settles in rather than appearing. One pass, 420ms, a little
   overshoot — long enough to be felt on a hero, short enough that a chat
   byline arriving mid-conversation does not draw the eye from the text that
   arrived with it. `backwards` holds the first frame during any delay so he
   never flashes at full size first.

   On the slot, not the image: the image is running an infinite idle on the
   same property, and the last animation declared would win outright. */
@keyframes dough-enter {
  from { opacity: 0; transform: scale(.86) translateY(6%); }
  to   { opacity: 1; transform: none; }
}
[data-dough] {
  transform-origin: 50% 100%;
  animation: dough-enter .42s var(--dough-spring) backwards;
}

/* ── Hover ───────────────────────────────────────────────────────────────
   Only where Dough is inside something the user can actually click, so a
   decorative mascot in an empty state does not react to a passing cursor. */
.dough-avatar [data-dough],
a [data-dough],
button [data-dough] { transition: transform .18s var(--dough-spring); }

a:hover [data-dough],
button:hover [data-dough],
.dough-avatar:hover [data-dough] { transform: scale(1.06); }

/* ══════════════════════════════════════════════════════════════════════════
   Components — the surfaces Dough talks from
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Avatar: Dough's face in a themed disc. Chat replies, card headers,
      toasts. The disc, not the fur, carries the accent. ────────────────── */
.dough-avatar {
  /* One knob. Every placement — nav, tab bar, chat byline, card header, error
     page — sets this and nothing else, so Dough is the same size relative to
     his disc everywhere and there is a single place to retune him. */
  --dough-size: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--dough-size);
  height: var(--dough-size);
  border-radius: var(--r-full, 999px);
  overflow: hidden;
  background: linear-gradient(150deg,
              color-mix(in srgb, var(--dough-accent) 26%, var(--surface, #fff)),
              color-mix(in srgb, var(--dough-accent) 9%, var(--surface, #fff)));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dough-accent) 22%, transparent);
}
/* Exactly 100%, never more. The disc is a hard clip, so any overscale here
   comes straight off Dough's ears — an earlier 112% did that at every size.
   The head crop is already sized to the disc (see head_box in
   tools/build_dough_assets.py): it is the square around the portrait plus 6%
   air, so at 100% he lands centred, whole, and just short of the rim. */
.dough-avatar [data-dough] { width: 100%; height: 100%; }
.dough-avatar .dough {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: none;   /* the disc already casts the shadow */
}

/* The scale. */
.dough-avatar-xs { --dough-size: 28px; }   /* chat bylines, dense rows */
.dough-avatar-sm { --dough-size: 36px; }   /* nav brand, card headers */
.dough-avatar-md { --dough-size: 48px; }
.dough-avatar-lg { --dough-size: 72px; }   /* section heroes */

/* ── Speech bubble: Dough addressing the user directly, with a tail. Used
      where the copy is a sentence from Dough rather than a labelled panel. */
.dough-bubble {
  position: relative;
  border: 1px solid color-mix(in srgb, var(--dough-accent) 20%, var(--hairline-firm, #e5e7eb));
  border-radius: var(--r-md, 13px);
  background: color-mix(in srgb, var(--dough-accent) 6%, var(--surface, #fff));
  padding: var(--sp-3, .75rem) var(--sp-4, 1rem);
  font-size: var(--f-md, .9375rem);
  line-height: var(--lh-snug, 1.32);
  color: var(--ink-body, inherit);
}
.dough-bubble::after,
.dough-bubble::before {
  content: '';
  position: absolute;
  left: -9px;
  top: 18px;
  border: 9px solid transparent;
  border-left: 0;
}
.dough-bubble::before { border-right-color: color-mix(in srgb, var(--dough-accent) 20%, var(--hairline-firm, #e5e7eb)); }
.dough-bubble::after  { left: -8px; border-right-color: color-mix(in srgb, var(--dough-accent) 6%, var(--surface, #fff)); }

/* ── Empty state: the single component every "you have nothing here yet"
      screen uses, so Dough greets the user consistently instead of each page
      inventing its own placeholder. ──────────────────────────────────────  */
.dough-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-3, .75rem);
  padding: var(--sp-10, 2.5rem) var(--sp-5, 1.25rem);
  max-width: 30rem;
  margin: 0 auto;
}
.dough-empty-title {
  font-size: var(--f-lg, 1.075rem);
  font-weight: var(--w-semi, 620);
  color: var(--ink-strong, inherit);
}
.dough-empty-body {
  font-size: var(--f-md, .9375rem);
  line-height: var(--lh-body, 1.55);
  color: var(--ink-muted, inherit);
}
.dough-empty .dough { opacity: .95; }

/* ── Working: Dough visibly doing the job while a model call is in flight.
      Used anywhere the product would otherwise show a bare spinner or
      skeleton — the wait is where a companion earns its keep. The label is
      required, not optional: it is what carries the state to a screen reader
      and to anyone who has turned motion off. ──────────────────────────── */
.dough-working {
  display: flex;
  align-items: center;
  gap: var(--sp-2, .5rem);
  margin-bottom: var(--sp-3, .75rem);
}
.dough-working__label {
  font-size: var(--f-sm, .8125rem);
  font-weight: var(--w-medium, 530);
  color: var(--ink-muted, inherit);
}

/* ══════════════════════════════════════════════════════════════════════════
   Reduced motion
   ──────────────────────────────────────────────────────────────────────────
   Dough is decorative by contract — nothing he does is the only carrier of
   any message — so the honest response to a reduced-motion preference is to
   stop moving entirely rather than to soften the timing. The mascot stays,
   the animation does not. That includes the entrance and the hover, which are
   the two easiest to forget.
   ══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  [data-dough],
  [data-dough]::before,
  .dough,
  .dough-dots i {
    animation: none !important;
    transition: none !important;
  }

  /* Confetti is motion and nothing else — a static pile of rectangles beside
     the mascot communicates nothing, so it is removed rather than frozen. */
  .dough-confetti { display: none !important; }

  a:hover [data-dough],
  button:hover [data-dough],
  .dough-avatar:hover [data-dough] { transform: none !important; }

  /* The entrance starts at opacity 0; without this he never appears. */
  [data-dough],
  .dough { opacity: 1 !important; transform: none !important; }
}
