/**
 * CargoMatrix Site Agent — floating widget
 *
 * Collapsed state reproduces the kiosk app's floating control: a bare avatar with no
 * circle, container or shadow (the cubes *are* the button), plus an iMessage-style
 * tailed caption bubble offset up-left of it.
 *
 * Expanded state is a minimal panel. Colors come from the site's :root custom
 * properties, with fallbacks so pages that don't define the full token set still
 * render correctly. Every class is prefixed `cmx-agent-` on purpose: site-config.js
 * hides `.hero, .hero-fulfillment` for the admin's hero toggle and would otherwise
 * catch a widget that reused those names.
 *
 * z-index: launcher 99997, panel 99998 — both below the sticky nav (99999) so the
 * header always wins, and far below the mobile nav drawer (999999).
 */

.cmx-agent-root {
  /* Chrome colours are the widget's own rather than the page's. There is no shared
     stylesheet — every page carries its own copy of the site token block, and those
     copies have drifted into four different palettes. On 13 of the 20 pages the widget
     appears on, --color-border is the *same colour* as --color-bg-elevated (#1E293B on
     both), so a border drawn in it is invisible: the panel outline and the rule under
     the header simply vanished on every product page. The five solutions pages define
     no --color-bg-elevated at all, so the panel changed background as you moved around
     the site. A floating overlay is one surface and should look the same everywhere,
     so it brings its own — with the border as a translucent overlay, which cannot
     collide with whatever it is drawn on. Values are index.html's, the palette the
     widget was designed against. Fonts stay inherited: those agree across pages. */
  --cmx-agent-accent: #E85A25;
  --cmx-agent-accent-text: #FB8A50;
  --cmx-agent-surface: #101c30;
  --cmx-agent-surface-2: #1a2740;
  --cmx-agent-border: rgba(255, 255, 255, 0.13);
  --cmx-agent-focus-ring: rgba(232, 90, 37, 0.22);
  --cmx-agent-text: #FFFFFF;
  --cmx-agent-text-2: #CBD5E1;
  --cmx-agent-text-3: #A0AEC0;
  --cmx-agent-font: var(--font-body, 'DM Sans', sans-serif);
  --cmx-agent-font-display: var(--font-display, 'Instrument Sans', sans-serif);
  --cmx-agent-bottom: 20px;

  /* The canvas is much larger than the cubes drawn inside it (the resting cluster fills
     ~36% of the frame), so the button and the canvas are sized separately:
       --cmx-agent-hit      the clickable square, and the anchor everything positions to
       --cmx-agent-canvas   the drawing surface, centred on that anchor
     Growing the canvas therefore scales the cubes around a fixed point instead of
     dragging them away from the corner, and never inflates the hit area. */
  --cmx-agent-hit: 96px;
  --cmx-agent-canvas: 260px;
  /* Inset of the anchor from the right edge. Has to clear the widest pose the launcher
     ever shows — IDLE, during the flight into the panel — whose cluster is ~0.27 x the
     canvas wide on each side. At a 260px canvas that is 70px, so the anchor sits 80px
     in and the cubes keep ~10px of margin. */
  --cmx-agent-right: 32px;
  /* How far the caption bubble sits above the anchor, as a fraction of the canvas.
     0.18 (the cluster's half-height) floated it clear above the cubes; 0.05 dropped it
     level with their middle. 0.13 is the midpoint — just below the top of the cluster,
     tail angling into it. */
  --cmx-agent-bubble-lift: 0.13;
  /* Upward nudge that puts the CUBES on the anchor rather than the canvas — see the
     launcher canvas rule. Declared here because the flight animation in cmx-agent.js
     reads it too, and the two must not drift apart. */
  --cmx-agent-canvas-lift: 0.146;
}

/* Light theme. The site's toggle sets data-theme on <html> (see the per-page script),
   so this is the same switch the rest of the page uses — the widget just no longer
   depends on the page having defined the tokens correctly to follow it. */
[data-theme="light"] .cmx-agent-root {
  --cmx-agent-accent-text: #C94D17;
  --cmx-agent-surface: #FFFFFF;
  --cmx-agent-surface-2: #F1F5F9;
  --cmx-agent-border: rgba(15, 23, 42, 0.14);
  --cmx-agent-focus-ring: rgba(232, 90, 37, 0.18);
  --cmx-agent-text: #1E3A5F;
  --cmx-agent-text-2: #475569;
  --cmx-agent-text-3: #64748B;
}

/* Nudged up on the homepage while the cookie banner owns the bottom edge. */
.cmx-agent-root.cmx-agent--raised {
  --cmx-agent-bottom: 104px;
}

/* ─── Collapsed launcher ─────────────────────────────────────────────────── */

.cmx-agent-launcher {
  position: fixed;
  right: var(--cmx-agent-right);
  bottom: var(--cmx-agent-bottom);
  width: var(--cmx-agent-hit);
  height: var(--cmx-agent-hit);
  z-index: 99997;
  border: 0;
  padding: 0;
  margin: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity 0.25s ease, transform 0.25s ease, bottom 0.3s ease;
}

.cmx-agent-launcher:focus-visible {
  outline: 2px solid var(--cmx-agent-accent-text);
  outline-offset: 6px;
  border-radius: 12px;
}

/* Centred on the button, free to overflow it. pointer-events:none keeps the generous
   canvas from stealing clicks from the page behind its empty margins.

   The extra upward nudge is not arbitrary: the resting pose settles low inside its own
   canvas (SLEEP_BIAS + SLEEP_DROP), measured at +0.146 x size below the canvas centre,
   while the awake poses are centred. Aligning the canvas alone therefore left the
   resting cubes hanging off the bottom of the viewport. Shifting by that exact amount
   puts the CUBES on the anchor rather than the canvas, at any size. */
.cmx-agent-launcher canvas {
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--cmx-agent-canvas);
  height: var(--cmx-agent-canvas);
  transform: translate(-50%, calc(-50% - var(--cmx-agent-canvas) * var(--cmx-agent-canvas-lift)));
  pointer-events: none;
}

.cmx-agent-launcher:hover {
  transform: scale(1.06);
}

/* Hidden while the panel is open, or while the mobile nav drawer covers the page.
   The :not(--flying) guard lets the launcher stay on screen while it travels into the
   panel header; without it the hide rule would cancel the flight mid-air. */
.cmx-agent-root.cmx-agent--open:not(.cmx-agent--flying) .cmx-agent-launcher,
.cmx-agent-root.cmx-agent--nav-open .cmx-agent-launcher,
.cmx-agent-root.cmx-agent--nav-open .cmx-agent-bubble {
  opacity: 0;
  transform: scale(0.85);
  pointer-events: none;
}

/* In flight: ride above the panel, and let the header slot stay empty until landing.
   The transform is applied to the canvas (see flyAvatar), so the button must not carry
   its own — hence transform:none overriding the hover scale. */
.cmx-agent-root.cmx-agent--flying .cmx-agent-launcher {
  opacity: 1;
  z-index: 99999;
  pointer-events: none;
  transform: none;
}

.cmx-agent-root.cmx-agent--flying .cmx-agent-launcher canvas {
  will-change: transform;
}

.cmx-agent-root.cmx-agent--flying .cmx-agent-head-avatar {
  opacity: 0;
}


/* ─── Caption bubble (the "text bar") ───────────────────────────────────────
   Not a transcript — a <=5 word cue. Sits behind the avatar so the cubes
   overlap it, exactly as the kiosk emits them. */

/* Positioned off the same anchor as the launcher, and scaled by the canvas size, so
   resizing the cubes keeps the bubble clear of them instead of stranding it. */
.cmx-agent-bubble {
  position: fixed;
  right: calc(var(--cmx-agent-right) + var(--cmx-agent-hit) / 2 + var(--cmx-agent-canvas) * 0.17);
  bottom: calc(var(--cmx-agent-bottom) + var(--cmx-agent-hit) / 2 +
    var(--cmx-agent-canvas) * var(--cmx-agent-bubble-lift));
  z-index: 99996;
  max-width: 210px;
  padding: 8px 14px;
  /* No background or radius here — .cmx-agent-bubble-shape draws the whole outline,
     body and tail together, as one generated path. */
  background: none;
  /* Primary text, not secondary: the bubble floats over page content and the muted
     tokens washed out against the surface in both themes. */
  color: var(--cmx-agent-text);
  font-family: var(--cmx-agent-font);
  /* The kiosk uses 13sp, but the cube cluster here is larger than its 120dp widget, so
     the caption needs a touch more weight to hold its own beside it. */
  font-size: 14.5px;
  line-height: 1.35;
  opacity: 0;
  transform: translateX(24px);
  pointer-events: none;
  transition: opacity 0.28s ease, transform 0.28s ease, bottom 0.3s ease, right 0.3s ease;
}

/* The generated outline. Sits behind the text and overflows the box by the tail's
   width and height, which is why the bubble itself paints no background. */
.cmx-agent-bubble-shape {
  position: absolute;
  left: 0;
  top: 0;
  overflow: visible;
  pointer-events: none;
  /* The shadow follows the real silhouette, tail included — a box-shadow on the
     element would trace a rectangle around it. */
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.32));
}

.cmx-agent-bubble-shape path {
  fill: var(--cmx-agent-surface-2);
  /* Same hairline the panel and its internal rules use. It has to be a stroke rather
     than a CSS border: the tail is part of this path, and a border would trace the
     element box instead of the silhouette. Centred on the outline, so the ~0.5px that
     falls outside relies on the SVG's overflow: visible. */
  stroke: var(--cmx-agent-border);
  stroke-width: 1;
  stroke-linejoin: round;
}

.cmx-agent-bubble-text {
  position: relative;
  z-index: 1;
}

.cmx-agent-bubble.cmx-agent-bubble--show {
  opacity: 1;
  transform: translateX(0);
}

/* Hint variant, shown before the visitor has ever engaged. */
.cmx-agent-bubble.cmx-agent-bubble--hint {
  font-size: 14px;
  padding: 8px 14px;
  color: var(--cmx-agent-text-2);
}

/* When a live session is running, the bubble shifts further clear of the avatar —
   ported from the kiosk's 40dp/20dp active offset. */
.cmx-agent-root.cmx-agent--active .cmx-agent-bubble {
  right: calc(var(--cmx-agent-right) + var(--cmx-agent-hit) / 2 + var(--cmx-agent-canvas) * 0.17 + 22px);
  bottom: calc(var(--cmx-agent-bottom) + var(--cmx-agent-hit) / 2 +
    var(--cmx-agent-canvas) * var(--cmx-agent-bubble-lift) + 18px);
}

/* ─── Panel ──────────────────────────────────────────────────────────────── */

/* The panel is now only a layout box: no background, no border, no clipping. Everything
   you can see belongs to .cmx-agent-surface behind it. What forced that split is the drop
   shadow — see .cmx-agent-panel::before, where the reason is spelled out; it cannot share an
   element with the backdrop-filter and it cannot sit on an ancestor of one either. Keeping
   the paint on its own layer also leaves room for a shaped clip-path later without
   revisiting any of this, and means content is free to sit anywhere in the box without a
   clip cropping it. */
.cmx-agent-panel {
  position: fixed;
  right: 16px;
  bottom: var(--cmx-agent-bottom);
  z-index: 99998;
  display: flex;
  flex-direction: column;
  width: 384px;
  max-width: calc(100vw - 32px);
  /* Sizes to its contents and grows upward from the bottom-right anchor, rather than
     always being 560px tall with dead space in it. An empty panel or a two-line exchange
     no longer reserves half the viewport for a conversation that has not happened yet;
     560px stays the ceiling, and past it the transcript scrolls as before.
     min-height holds enough room for the empty state's mic and prompt to sit comfortably
     instead of collapsing to a strip. */
  height: auto;
  min-height: 300px;
  max-height: min(560px, calc(100vh - 140px));
  font-family: var(--cmx-agent-font);
  opacity: 0;
  transform: translateY(16px) scale(0.97);
  transform-origin: bottom right;
  pointer-events: none;
  /* Slight overshoot on the way in so it arrives with some life rather than easing to a
     stop. Opacity keeps a plain curve — overshooting opacity just looks like a flicker. */
  transition: opacity 0.28s cubic-bezier(0.4, 0, 0.2, 1),
              transform 0.42s cubic-bezier(0.34, 1.36, 0.5, 1),
              bottom 0.3s ease;
}

/* ── Glass surface ─────────────────────────────────────────────────────────────
   One layer carrying the whole shell: glass, rim, glow, shadow.

   The shadow lives on the panel, NOT on the surface, and that split is load-bearing rather
   than stylistic. `filter` makes an element a backdrop root for its descendants, so a
   drop-shadow on the surface silently turned its own backdrop-filter into a no-op: the blur
   stopped sampling the page and the panel became a dim sheet with the hero's text still
   sharply readable straight through it. Keeping every `filter` off the glass's ancestor
   chain is what makes the blur work at all — do not move this back. */
.cmx-agent-panel::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: 22px;
  box-shadow: 0 24px 46px rgba(0, 0, 0, 0.46);
  pointer-events: none;
}

/* No chamfer. A 45 degree cut at the bottom-right was tried and read as a wedge bitten out
   of the corner rather than as deliberate geometry, so the shape is a plain rounded
   rectangle again. The glass, the rim and the accent glow are what carry the design; the
   corner was the one part doing no work.

   Kept as a no-op clip-path rather than deleted: ::after inherits it for the rim, and an
   inherited `none` is what makes that rim follow whatever shape this element has. Set
   --cmx-chamfer back to a length here and the cut returns, rim included. */
.cmx-agent-surface {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: 22px;
  background:
    /* Accent bloom behind the cube cluster, which sits at the top-left. Reads as the
       avatar casting light into the panel rather than sitting on top of it.

       Placed and sized in pixels rather than percentages, and centred inside the panel
       rather than above its top edge. Both matter. A percentage centre drifts with the
       panel's height, so the light source slid around as the transcript grew; and a centre
       at -8% put the bloom's brightest point outside the box, which the top edge then cut
       off in a hard line right where the cluster sits. Anchored at the cluster's own centre
       the falloff is continuous in every direction, so what you see behind the cubes is a
       glow rather than the clipped shoulder of one. */
    radial-gradient(340px 320px at 44px 42px, rgba(232, 90, 37, 0.20) 0%, transparent 62%),
    /* A cool counter-light from the opposite corner, so the surface has a direction. */
    radial-gradient(90% 70% at 100% 100%, rgba(56, 116, 200, 0.14) 0%, transparent 60%),
    linear-gradient(160deg, rgba(20, 34, 58, 0.88) 0%, rgba(10, 19, 33, 0.92) 100%);
  -webkit-backdrop-filter: blur(24px) saturate(150%);
  backdrop-filter: blur(24px) saturate(150%);
  pointer-events: none;
}

/* The light-catching hairline, painted as a 1px frame of gradient.

   The padding + mask-composite pair is what limits the gradient to the frame: two masks,
   one clipped to the content box and one to the whole box, composited so only the
   difference — the 1px padding ring — survives. A plain `border` would do the job for a
   rounded rectangle; this survives a shaped clip-path too, which is why it stays. */
.cmx-agent-surface::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 22px;
  clip-path: inherit;
  padding: 1px;
  /* Lit from the top-left and falling away, but never to nothing: the floor keeps the panel
     defined all the way round instead of its lower edges dissolving into the page. The
     accent used to spike here to pick out the chamfer — with the chamfer gone that spike
     was just an orange corner, so the rim is neutral white throughout again. */
  background:
    linear-gradient(145deg,
      rgba(255, 255, 255, 0.40) 0%,
      rgba(255, 255, 255, 0.17) 32%,
      rgba(255, 255, 255, 0.11) 66%,
      rgba(255, 255, 255, 0.15) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* Without backdrop-filter the translucency has nothing to work against and the panel
   becomes a see-through sheet with text on it. Go nearly opaque instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .cmx-agent-surface {
    background:
      radial-gradient(340px 320px at 44px 42px, rgba(232, 90, 37, 0.16) 0%, transparent 62%),
      linear-gradient(160deg, rgba(20, 34, 58, 0.985) 0%, rgba(10, 19, 33, 0.99) 100%);
  }
}

/* Content sits above the surface. */
.cmx-agent-head,
.cmx-agent-log,
.cmx-agent-typing,
.cmx-agent-bar {
  position: relative;
  z-index: 1;
}

.cmx-agent-root.cmx-agent--open .cmx-agent-panel {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

/* Header. No rule under it: the transcript dissolves into the ribbon instead (see the
   mask on .cmx-agent-log), which does the same job of separating them without drawing a
   line across a panel this small. */
/* 14px of top padding rather than 6px. At 6px the clear and close buttons sat hard against
   the panel's top edge with no breathing room, which read as cramped rather than tight. The
   whole row drops together so the title stays aligned with them, and the avatar — whose
   negative margin is measured from this box — comes down with it. Both avatar sizes are
   pinned against this padding rather than against each other, so changing it moves them
   both: see the voice-mode margins. */
.cmx-agent-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 14px 12px 6px 8px;
  flex: 0 0 auto;
}

/* Oversized on purpose — the cubes only fill part of their canvas, so the visible
   cluster is roughly half this box. The negative margins let it overhang the header's
   padding so a big canvas doesn't inflate the ribbon's height. */
/* The cluster sits level with the title rather than riding above it. An earlier version
   pushed it up to break the panel's top edge, which does soften the boundary — but it left
   the logo floating out of relation to the wordmark beside it, and reading as misplaced
   costs more than the softened edge bought.

   Measured, not calculated. Because the row centres its items, an asymmetric margin moves
   the canvas centre by only half the change and the two sides fight each other — chasing it
   that way overshot in both directions. Symmetric negative margins sidestep it entirely:
   the canvas centre lands on the row's centre, which is where the title and the buttons
   already are. All three now sit on 32px.

   A transform would have been the obvious tool and is not usable here: flyAvatar() lands the
   travelling launcher on headWrap.offsetTop, which ignores transforms, so the avatar would
   arrive offset from where it appears and snap into place.

   -12px still pulls the 60px canvas in enough that it only adds 2px to the header's height,
   which is what these margins were always for. */
.cmx-agent-head-avatar {
  width: 60px;
  height: 60px;
  flex: 0 0 auto;
  margin: -12px -6px -12px -8px;
  position: relative;
  z-index: 2;
  /* One list, and the only one on this selector — a second `transition` further down the
     sheet silently replaced the whole shorthand rather than adding to it, which is how the
     flight's opacity fade got lost. It is folded in here instead.

     Deliberately no overshoot. The cubes are drawn, not scaled, so the cluster's own growth
     is a lerp in cmx-agent-avatar.js with no springiness available to it; a bouncing box
     around exponentially-easing contents read as the two coming apart. A steep ease-out is
     the closest CSS shape to that lerp, so the frame and the cluster now travel together. */
  transition: width 0.38s cubic-bezier(0.22, 1, 0.36, 1),
              height 0.38s cubic-bezier(0.22, 1, 0.36, 1),
              margin 0.38s cubic-bezier(0.22, 1, 0.36, 1),
              opacity 0.12s ease;
}

.cmx-agent-head-avatar canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.cmx-agent-head-titles {
  flex: 1 1 auto;
  min-width: 0;
}

.cmx-agent-title {
  font-family: var(--cmx-agent-font-display);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--cmx-agent-text);
  line-height: 1.2;
}

.cmx-agent-iconbtn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  border: 1px solid var(--cmx-agent-border);
  background: transparent;
  color: var(--cmx-agent-text-2);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.cmx-agent-iconbtn:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--cmx-agent-text);
}

.cmx-agent-iconbtn svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cmx-agent-iconbtn.cmx-agent-iconbtn--on {
  background: var(--cmx-agent-accent);
  border-color: var(--cmx-agent-accent);
  color: #fff;
}

.cmx-agent-iconbtn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Clear. Disabled means "there is no conversation yet", which is not a control to grey
   out and explain — it is one to keep out of the way, so it goes fully transparent and
   drops out of the tab order rather than sitting at 0.45 next to Close. It holds its
   width either way, so the header does not shift when the first message lands. */
.cmx-agent-clear {
  transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease,
    border-color 0.2s ease;
}

.cmx-agent-clear[disabled] {
  opacity: 0;
  pointer-events: none;
}

/* Destructive, so it says so on hover instead of taking the neutral treatment. */
.cmx-agent-clear:hover:not([disabled]) {
  background: rgba(232, 90, 37, 0.14);
  border-color: rgba(232, 90, 37, 0.5);
  color: var(--cmx-agent-accent-text);
}

/* Transcript */
.cmx-agent-log {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 24px 16px 10px;
  display: flex;
  flex-direction: column;
  /* Messages stack from the bottom while the thread is short. `safe` is what makes that
     survive a long one: with plain flex-end an overflowing column spills past the START
     edge, and the overflow is unreachable — the browser reports scrollHeight ===
     clientHeight, so the earliest messages were simply gone rather than scrolled off.
     `safe` falls back to start alignment the moment content overflows, which is exactly
     when the bottom-stacking is no longer doing anything. The plain value stays first as
     the fallback for anything that doesn't parse the two-value form. */
  justify-content: flex-end;
  justify-content: safe flex-end;
  gap: 10px;
  scroll-behavior: smooth;
  /* Messages dissolve into the header rather than clipping against it — this replaces the
     rule that used to sit there. The fade is exactly as deep as the padding above the
     first message on purpose: scrolled to the top, the text starts where the fade ends
     and reads at full strength, and only content actually passing under the ribbon is
     ever washed out. Lengthening one without the other greys the first line permanently.
     The bottom fade is much shallower than the top's 24px and deliberately so: the newest
     message lands there and has to stay legible. 10px is enough to dissolve the transcript
     into the input area now that the border-top above it is gone, without washing out the
     line the visitor is actually reading. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 24px,
                                      black calc(100% - 10px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, black 24px,
                              black calc(100% - 10px), transparent 100%);
}

.cmx-agent-log::-webkit-scrollbar {
  width: 0;
}

.cmx-agent-msg {
  font-size: 0.875rem;
  line-height: 1.55;
  max-width: 92%;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  /* Arrives from slightly below and out of focus, rather than appearing. The blur is what
     sells it as depth instead of a slide — it reads as the line coming forward. */
  animation: cmx-agent-msg-in 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes cmx-agent-msg-in {
  from { opacity: 0; transform: translateY(7px); filter: blur(3px); }
  to   { opacity: 1; transform: none;            filter: blur(0); }
}

/* A thread replayed from sessionStorage is not "arriving" — it was already said. Animating
   it would run the whole transcript at once on every page load. */
.cmx-agent-msg--restored {
  animation: none;
}

.cmx-agent-msg--agent {
  align-self: flex-start;
  color: var(--cmx-agent-text-2);
}

.cmx-agent-msg--user {
  align-self: flex-end;
  text-align: right;
  color: var(--cmx-agent-accent-text);
}

.cmx-agent-msg--system {
  align-self: center;
  text-align: center;
  font-size: 0.75rem;
  color: var(--cmx-agent-text-3);
}

.cmx-agent-msg strong { color: var(--cmx-agent-text); font-weight: 600; }
.cmx-agent-msg em { font-style: italic; }
.cmx-agent-msg code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8em;
  padding: 1px 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.08);
}

.cmx-agent-msg a {
  color: var(--cmx-agent-accent-text);
  text-decoration: underline;
}

/* Intro block. Lives inside the log and sits centred in the empty space, rather than
   hugging the input bar. Removed outright once the conversation starts so it can't
   affect the transcript's layout. */
.cmx-agent-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  align-self: center;
  gap: 18px;
  max-width: 15rem;
  padding: 0 8px;
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--cmx-agent-text-3);
  text-align: center;
}

.cmx-agent-root.cmx-agent--started .cmx-agent-intro {
  display: none;
}

/* ── Big mic ───────────────────────────────────────────────────────────────────
   The primary call to action while the panel is empty. Voice is what makes this
   widget worth opening, so before the first message it gets the middle of the panel
   rather than a 16px glyph in the corner of the text field. Once a message exists the
   whole intro is gone and the in-field mic takes over — see the rule below it. */
.cmx-agent-bigmic {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--cmx-agent-accent);
  color: #fff;
  cursor: pointer;
  transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.2s ease;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
  -webkit-tap-highlight-color: transparent;
}

.cmx-agent-bigmic-glyph {
  display: inline-flex;
  position: relative;   /* above the ring */
}

.cmx-agent-bigmic svg {
  width: 30px;
  height: 30px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cmx-agent-bigmic:hover {
  transform: translateY(-1px) scale(1.03);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.34);
}

.cmx-agent-bigmic:active {
  transform: scale(0.97);
}

.cmx-agent-bigmic:focus-visible {
  outline: none;
  box-shadow: 0 0 0 4px var(--cmx-agent-focus-ring), 0 6px 18px rgba(0, 0, 0, 0.28);
}

/* A slow halo that says "this is the thing to press" without becoming a distraction.
   Sits behind the glyph and is purely decorative. */
.cmx-agent-bigmic-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--cmx-agent-accent);
  opacity: 0.45;
  animation: cmx-agent-bigmic-pulse 2.6s ease-out infinite;
  pointer-events: none;
}

@keyframes cmx-agent-bigmic-pulse {
  0%   { transform: scale(1);    opacity: 0.4; }
  70%  { transform: scale(1.55); opacity: 0; }
  100% { transform: scale(1.55); opacity: 0; }
}

/* Live: stop advertising and start indicating. The halo becomes a steady breath. */
.cmx-agent-bigmic.cmx-agent-bigmic--on .cmx-agent-bigmic-ring {
  animation: cmx-agent-bigmic-breathe 1.6s ease-in-out infinite;
}

@keyframes cmx-agent-bigmic-breathe {
  0%, 100% { transform: scale(1.12); opacity: 0.5; }
  50%      { transform: scale(1.3);  opacity: 0.18; }
}

.cmx-agent-bigmic.cmx-agent-bigmic--busy .cmx-agent-bigmic-ring {
  animation: none;
  opacity: 0;
}

.cmx-agent-bigmic.cmx-agent-bigmic--busy svg {
  animation: cmx-agent-spin 0.9s linear infinite;
}

/* Two mics on screen at once would be one too many. Before the first message the big
   one owns voice; after it, the in-field one does. */
.cmx-agent-root:not(.cmx-agent--started) .cmx-agent-mic {
  display: none;
}

/* With the mic gone there is nothing occupying the right of the field, so reclaim the
   space the input was leaving for it. */
.cmx-agent-root:not(.cmx-agent--started) .cmx-agent-input {
  padding-right: 12px;   /* matches the left, since nothing sits in the field now */
}

@media (prefers-reduced-motion: reduce) {
  .cmx-agent-bigmic,
  .cmx-agent-bigmic-ring,
  .cmx-agent-bigmic svg {
    animation: none;
    transition: none;
  }
  .cmx-agent-bigmic-ring { opacity: 0.28; }
}

/* Centre the intro vertically while the transcript is empty; once messages exist the
   log goes back to stacking them from the bottom. */
.cmx-agent-root:not(.cmx-agent--started) .cmx-agent-log {
  justify-content: center;
}

/* ── Voice takeover ────────────────────────────────────────────────────────────
   While a call is live this stops being a chat window. Nobody reads a scrolling
   transcript while talking — they glance at it. So the widget commits to being a
   presence: the cluster grows, the last two turns get room and size, and the chrome
   steps back. All of it hangs off .cmx-agent--voice, which applyMode() already toggles
   for connecting and live, so there is no extra state to keep in sync. */

/* The margins stop exactly at the panel's edges — top 0 given the header's 14px of top
   padding, left 0 given its 8px on the left — rather than lifting the cluster out through
   the corner. The breakout softened the boundary, but the glass, the rim and the accent
   bloom all end at that boundary, so whatever crosses it has the panel's top edge running
   through it as a hard horizontal line. Nothing paints behind the overhanging part, and no
   amount of gradient work on the surface can reach outside its own box. Sitting inside the
   glass, the cluster has the bloom behind it the whole way. */
.cmx-agent-root.cmx-agent--voice .cmx-agent-head-avatar {
  width: 104px;
  height: 104px;
  margin: -14px -14px -14px -8px;
}

/* Everything except the last exchange steps out of the way. :nth-last-child rather than a
   JS pass, so it re-resolves by itself as turns arrive and needs no unwinding when the
   call ends. */
.cmx-agent-root.cmx-agent--voice .cmx-agent-msg:not(:nth-last-child(-n+2)) {
  display: none;
}

.cmx-agent-root.cmx-agent--voice .cmx-agent-msg {
  font-size: 1.06rem;
  line-height: 1.5;
  max-width: 100%;
  transition: font-size 0.3s ease;
}

.cmx-agent-root.cmx-agent--voice .cmx-agent-msg--agent {
  color: var(--cmx-agent-text);
}

/* Centre the pair in the space the hidden turns left behind, instead of stranding them at
   the bottom above a large gap. */
.cmx-agent-root.cmx-agent--voice .cmx-agent-log {
  justify-content: center;
  gap: 16px;
}

/* The title stays. It used to fade out here on the grounds that it was redundant beside a
   cluster this prominent — which is true of the title on its own, and false of the header as
   a whole: the one piece of permanent furniture in the row disappearing and coming back is a
   bigger event than the words are worth, and it made starting a call read as the panel
   rebuilding itself rather than leaning in. Only the clear button still steps back, and for a
   different reason: wiping the thread mid-call is not something to leave a target for. */
.cmx-agent-root.cmx-agent--voice .cmx-agent-clear {
  opacity: 0;
  pointer-events: none;
}

.cmx-agent-title,
.cmx-agent-clear {
  transition: opacity 0.25s ease;
}

/* Warm the surface while the call is live — the one state cue on the shell itself.
   Identical bloom geometry to the resting state, differing only in strength, so entering a
   call warms the panel where it already glowed instead of also sliding the light source
   across it. That keeps the whole mode change to changes of degree. */
.cmx-agent-root.cmx-agent--voice .cmx-agent-surface {
  background:
    radial-gradient(340px 320px at 44px 42px, rgba(232, 90, 37, 0.32) 0%, transparent 62%),
    radial-gradient(90% 70% at 100% 100%, rgba(56, 116, 200, 0.14) 0%, transparent 60%),
    linear-gradient(160deg, rgba(24, 38, 62, 0.88) 0%, rgba(10, 19, 33, 0.92) 100%);
}

.cmx-agent-surface {
  transition: background 0.45s ease;
}

/* ── Light theme glass ─────────────────────────────────────────────────────────
   Not a tweak of the dark recipe. In dark mode the rim is white light on a dark body; in
   light mode a white rim on a white body is invisible, so the frame has to darken instead,
   and the accent bloom has to drop right back or it turns the panel pink. */
[data-theme="light"] .cmx-agent-panel::before {
  box-shadow: 0 22px 40px rgba(15, 23, 42, 0.20);
}

[data-theme="light"] .cmx-agent-surface {
  background:
    radial-gradient(340px 320px at 44px 42px, rgba(232, 90, 37, 0.10) 0%, transparent 62%),
    radial-gradient(90% 70% at 100% 100%, rgba(56, 116, 200, 0.07) 0%, transparent 60%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.88) 0%, rgba(242, 247, 253, 0.92) 100%);
}

/* The rim inverts: white light on a white body is invisible, so in light mode the frame
   darkens instead of brightening. */
[data-theme="light"] .cmx-agent-surface::after {
  background:
    linear-gradient(145deg,
      rgba(15, 23, 42, 0.22) 0%,
      rgba(15, 23, 42, 0.14) 32%,
      rgba(15, 23, 42, 0.10) 66%,
      rgba(15, 23, 42, 0.14) 100%);
}

[data-theme="light"] .cmx-agent-root.cmx-agent--voice .cmx-agent-surface {
  background:
    radial-gradient(340px 320px at 44px 42px, rgba(232, 90, 37, 0.18) 0%, transparent 62%),
    radial-gradient(90% 70% at 100% 100%, rgba(56, 116, 200, 0.07) 0%, transparent 60%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.88) 0%, rgba(244, 248, 253, 0.92) 100%);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  [data-theme="light"] .cmx-agent-surface {
    background:
      radial-gradient(340px 320px at 44px 42px, rgba(232, 90, 37, 0.08) 0%, transparent 62%),
      linear-gradient(160deg, rgba(255, 255, 255, 0.99) 0%, rgba(246, 249, 253, 0.99) 100%);
  }
}

/* A visitor turn whose transcription has not landed yet. The slot is already in the log
   holding its place in the order; this just stops it reading as an empty gap. */
.cmx-agent-msg--pending::after {
  content: '…';
  opacity: 0.45;
}

/* Typing indicator */
.cmx-agent-typing {
  display: none;
  align-items: center;
  gap: 4px;
  padding: 0 16px 8px;
  flex: 0 0 auto;
}

.cmx-agent-typing.cmx-agent-typing--on {
  display: flex;
}

.cmx-agent-typing span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--cmx-agent-text-3);
  animation: cmx-agent-bounce 1.2s infinite ease-in-out;
}

.cmx-agent-typing span:nth-child(2) { animation-delay: 0.15s; }
.cmx-agent-typing span:nth-child(3) { animation-delay: 0.3s; }

@keyframes cmx-agent-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-4px); opacity: 1; }
}

/* ─── Bottom bar: [mode toggle] [input | voice readout] [send] ───────────────
   One row, so the toggle never moves between modes and the transcript keeps the
   vertical space a full-width segmented control was eating. */

/* No border-top. A rule across a panel this small is exactly the kind of line that makes it
   read as a stack of boxes; the log's mask fades into this area instead, which separates
   them without drawing anything. (The right padding was 34px while a chamfer lived in that
   corner and the send button had to clear it — back to symmetrical now.) */
.cmx-agent-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px 12px;
}

.cmx-agent-bar-main {
  position: relative;   /* anchors the mic button inside the field */
  flex: 1 1 auto;
  min-width: 0;
}

.cmx-agent-input {
  display: block;
  width: 100%;
  min-width: 0;
  padding: 10px 42px 10px 12px;   /* right padding clears the mic */
  border-radius: 10px;
  border: 1px solid var(--cmx-agent-border);
  background: var(--cmx-agent-surface-2);
  color: var(--cmx-agent-text);
  font-family: var(--cmx-agent-font);
  font-size: 0.875rem;
  outline: none;
  transition: border-color 0.2s ease;
}

.cmx-agent-input::placeholder { color: var(--cmx-agent-text-3); }

/* A 1px saturated orange line on a dark navy field has too little area to read as
   orange — antialiasing blends it toward pink. Same colour, but paired with a soft ring
   that carries the hue over enough pixels to be recognisable, so focus reads as
   deliberate brand orange rather than an odd stray colour. */
.cmx-agent-input:focus {
  border-color: var(--cmx-agent-accent);
  box-shadow: 0 0 0 3px var(--cmx-agent-focus-ring);
}

/* Mic button, inside the field on the right. Click to talk, click again to type. */
.cmx-agent-mic {
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--cmx-agent-text-3);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}

.cmx-agent-mic svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cmx-agent-mic:hover:not(.cmx-agent-mic--on):not([disabled]) {
  background: rgba(255, 255, 255, 0.07);
  color: var(--cmx-agent-text);
}

.cmx-agent-mic.cmx-agent-mic--on {
  background: var(--cmx-agent-accent);
  color: #fff;
}

.cmx-agent-mic[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Connecting spinner */
.cmx-agent-mic.cmx-agent-mic--busy svg {
  animation: cmx-agent-spin 0.9s linear infinite;
}

@keyframes cmx-agent-spin {
  to { transform: rotate(360deg); }
}

/* Live mic level — a line along the bottom edge of the input field. Voice mode keeps
   the field, the mic and the send button exactly where text mode has them, so the only
   visible differences are the lit mic, this line, and the placeholder. */
.cmx-agent-meter {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 3px;
  height: 2px;
  border-radius: 999px;
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
}

.cmx-agent-root.cmx-agent--voice .cmx-agent-meter { opacity: 1; }

/* Voice mode keeps the input, but it is the agent's turn to listen. */
.cmx-agent-root.cmx-agent--voice .cmx-agent-input {
  border-color: var(--cmx-agent-accent);
}

.cmx-agent-level {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: 999px;
  background: var(--cmx-agent-accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.08s linear;
}

/* Highlight pulse applied to a section the agent scrolls to. */
.cmx-agent-highlight {
  animation: cmx-agent-pulse 1.6s ease-out 1;
  border-radius: 12px;
}

@keyframes cmx-agent-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(232, 90, 37, 0); }
  25%  { box-shadow: 0 0 0 5px rgba(232, 90, 37, 0.45); }
  100% { box-shadow: 0 0 0 14px rgba(232, 90, 37, 0); }
}

/* ─── Mobile ─────────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
  .cmx-agent-panel {
    right: 8px;
    left: 8px;
    width: auto;
    max-width: none;
    /* Same grow-to-fit behaviour as desktop, with the phone ceiling kept as the max.
       72vh swallowed roughly two thirds of a phone screen, which felt like a takeover
       rather than a helper. Just under half leaves the page visible behind it — the
       point of the widget is that you can still see what you are being shown.
       dvh where supported: on mobile Safari, vh is the tall viewport, so a vh-sized
       panel grows past the visible area once the URL bar is showing. */
    height: auto;
    min-height: 264px;
    max-height: min(46vh, 470px);
    max-height: min(46dvh, 470px);
  }

  .cmx-agent-root {
    --cmx-agent-hit: 76px;
    --cmx-agent-canvas: 200px;
    --cmx-agent-right: 24px;
  }

  .cmx-agent-bubble {
    max-width: 160px;
  }

  /* A blurred surface is the expensive case on phones, and this one is now nearly
     full-width. Halve the radius: the effect still reads at this size and it costs the
     compositor far less per frame. */
  .cmx-agent-surface {
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    backdrop-filter: blur(12px) saturate(140%);
  }

  /* Same title alignment as desktop; the header padding is unchanged at this width, so the
     desktop value already holds. Left explicit as the place to adjust if the mobile header
     ever diverges. */
  .cmx-agent-head-avatar {
    margin-top: -12px;
  }

  /* Same reasoning as desktop: the box stops at the panel's edges. */
  .cmx-agent-root.cmx-agent--voice .cmx-agent-head-avatar {
    width: 88px;
    height: 88px;
    margin: -14px -12px -14px -8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cmx-agent-launcher,
  .cmx-agent-bubble,
  .cmx-agent-panel,
  .cmx-agent-intro {
    transition-duration: 0.01ms;
  }

  .cmx-agent-highlight { animation: none; outline: 2px solid var(--cmx-agent-accent); }
  .cmx-agent-typing span { animation: none; }

  /* Messages appear rather than rising and focusing, and the avatar resizes instantly
     between chat and voice. The glass itself stays — it is not motion. */
  .cmx-agent-msg { animation: none; }
  .cmx-agent-head-avatar,
  .cmx-agent-surface,
  .cmx-agent-title,
  .cmx-agent-clear { transition-duration: 0.01ms; }
}
