/* ==========================================================================
   SYS assistant — the floating chat widget.

   WHERE IT SITS, AND WHAT IT REPLACED
   The bottom-right corner already held two 56px circles: .totop and
   .wa-float, stacked. A third would have made a 188px tower down the edge of
   every screen. So the assistant TAKES the WhatsApp float's slot rather than
   joining the queue, and WhatsApp moves inside the panel as a permanent bar
   along its foot — which is the arrangement every serious chat widget uses,
   and it is one tap either way. On phones .wa-float was already hidden and
   the mobile bar still carries WhatsApp as one of three targets, so nothing
   was lost there; the launcher takes .totop's old slot above the bar and
   .totop moves up one.

   Geometry is shared with those two on purpose — 56px, same right inset,
   same show/stow transition — so the corner reads as one system rather than
   as a widget pasted on. --fab-b is the single source of truth for the
   launcher's height off the bottom; the panel, the teaser and .totop are all
   positioned from it.
   ========================================================================== */

.cb{
  --fab: 56px;
  --fab-b: calc(clamp(16px,4.5vh,46px) + 68px + var(--consent-h));   /* the old .wa-float slot */
  --gap: 14px;
}

/* ---------- launcher ---------- */
.cb-fab{
  position:fixed; right:clamp(16px,5vw,52px); bottom:var(--fab-b); z-index:71;
  width:var(--fab); height:var(--fab); display:grid; place-items:center;
  padding:0; border:none; border-radius:50%;
  background:var(--accent); color:var(--on-accent);
  box-shadow:0 12px 34px -12px rgba(0,0,0,.55);
  opacity:0; visibility:hidden; transform:translateY(14px) scale(.86);
  /* `bottom` is in the transition list so the lift over the consent bar
     glides with it rather than teleporting. It is a layout-triggering
     property, which would matter if this ran every frame — it runs twice a
     session, when the banner appears and when it is answered. */
  transition:opacity .5s var(--ease-out), transform .5s var(--ease-out), visibility .5s,
             bottom .5s var(--ease-out);
}
.cb-fab.show{ opacity:1; visibility:visible; transform:none; }
/* There is deliberately no .cb-fab.stow rule. The back-to-top button and the
   mobile contact bar both stand down over content — they are chrome, and
   chrome yields — and this launcher used to be stowed on the same rule, which
   took the assistant off screen across the whole enquiry form and anything
   marked data-keep-clear. Those are the moments a visitor is most likely to
   have a question, so it now stays put everywhere, overlapping the corner the
   way a floating launcher does on every site that has one. See the note in
   nav.js initContactChrome(). */
/* The launcher stacks two children in one grid cell: the robot and the close
   cross. The robot is 30px, not the 25 the old speech-bubble glyph used — it
   has eyes, an antenna and a mouth to read, and at 25 those became texture. */
.cb-fab > *{ grid-area:1/1; transition:opacity .3s var(--ease-out), transform .4s var(--ease-out); }
.cb-fab .i-chat{ display:grid; place-items:center; }
.cb-fab .i-chat .rb{ width:30px; height:30px; }
.cb-fab .i-close{ width:25px; height:25px; justify-self:center; align-self:center; }
.cb-fab .cb-ring{ transition:none; }
.cb-fab .i-close{ opacity:0; transform:rotate(-45deg) scale(.7); }
.cb.open .cb-fab .i-chat{ opacity:0; transform:rotate(45deg) scale(.7); }
.cb.open .cb-fab .i-close{ opacity:1; transform:none; }
@media (hover:hover){ .cb-fab:hover{ filter:brightness(1.08); } }
.cb-fab:focus-visible{ outline:2px solid var(--accent); outline-offset:4px; }
/* An unread pip, shown only when the teaser was dismissed without opening —
   a reminder that something was said, not a badge that nags from page load. */
.cb-pip{
  position:absolute; top:2px; right:2px; width:11px; height:11px; border-radius:50%;
  background:var(--amber); border:2px solid var(--bg); opacity:0; transform:scale(.4);
  transition:opacity .3s var(--ease-out), transform .3s var(--ease-out);
}
.cb.has-unread .cb-pip{ opacity:1; transform:none; }

/* ---------- teaser ---------- */
/* Fires once, on the first real scroll, and never again in the session. A
   greeting that reappears on every page view is the single most disliked
   thing about widgets like this. */
/* ---------- the nudge ----------
   Sits to the LEFT of the launcher and stays there. The teaser above is the
   one-time pop; this is the standing invitation, and the two never show at
   once — chat.js hides this while the teaser is up and brings it back when
   the teaser goes.

   Right-anchored rather than left, so it grows leftward away from the screen
   edge and the gap to the launcher is fixed however long the copy gets. */
.cb-nudge{
  position:fixed; z-index:70;
  right:calc(clamp(16px,5vw,52px) + var(--fab) + 11px);
  /* 44, not 38. The site-wide target audit flagged it at 38 and it was the
     only thing this widget failed; it is also a better mouse target beside a
     56px circle. Centred on the launcher by construction rather than by a
     guessed offset, so changing --fab keeps them aligned. */
  bottom:calc(var(--fab-b) + (var(--fab) - 44px) / 2);
  height:44px; display:inline-flex; align-items:center; gap:9px;
  padding:0 15px; white-space:nowrap;
  background:var(--panel); border:1px solid var(--line); border-radius:999px;
  box-shadow:0 14px 34px -20px rgba(0,0,0,.7);
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.14em;
  text-transform:uppercase; color:var(--ink);
  opacity:0; visibility:hidden; transform:translateX(10px);
  /* `bottom` belongs in THIS list, not in a first `transition:` further up:
     the shorthand is not additive, so a second one silently replaces the
     first and the pill used to teleport into place while the launcher it
     sits beside glided. Same reasoning as .cb-fab — it runs twice a session,
     when the consent bar appears and when it is answered. */
  transition:opacity .5s var(--ease-out), transform .5s var(--ease-out),
             visibility .5s, border-color .3s var(--ease-out),
             bottom .5s var(--ease-out);
}
.cb-nudge.in{ opacity:1; visibility:visible; transform:none; }
/* A live dot, the same one .f-status and the panel head use, so the pill
   reads as a channel that is open rather than as an advert. */
.cb-nudge-d{
  width:7px; height:7px; border-radius:50%; background:var(--accent);
  animation:pulse 2.6s var(--ease-out) infinite; flex:0 0 auto;
}
@media (hover:hover){ .cb-nudge:hover{ border-color:var(--accent); color:var(--accent); } }
/* Below ~420px the pill and the launcher together crowd the screen edge, and
   the launcher alone is doing the job by then. */
@media (max-width:420px){ .cb-nudge{ display:none; } }
@media (prefers-reduced-motion:reduce){
  .cb-nudge{ transition:none; transform:none; }
  .cb-nudge.in{ transform:none; }
  .cb-nudge-d{ animation:none; }
}

.cb-teaser{
  position:fixed; right:clamp(16px,5vw,52px); z-index:71;
  bottom:calc(var(--fab-b) + var(--fab) + 12px);
  width:min(292px, calc(100vw - 32px));
  padding:15px 16px 15px 17px;
  background:var(--panel); border:1px solid var(--line); border-radius:2px;
  box-shadow:0 22px 50px -28px rgba(0,0,0,.7);
  opacity:0; transform:translateY(10px) scale(.96); transform-origin:100% 100%;
  transition:opacity .55s var(--ease-out), transform .55s var(--ease-out), visibility .55s;
  visibility:hidden;
}
.cb-teaser.in{ opacity:1; transform:none; visibility:visible; }
.cb-teaser::after{
  content:""; position:absolute; right:22px; bottom:-5px; width:9px; height:9px;
  background:var(--panel); border-right:1px solid var(--line); border-bottom:1px solid var(--line);
  transform:rotate(45deg);
}
.cb-teaser p{ margin:0; font-size:var(--fs-md); line-height:1.6; color:var(--muted); padding-right:26px; }
.cb-teaser p strong{ color:var(--ink); font-weight:600; }
/* 44x44, like every other control on this site, even though the glyph is
   12px — the site-wide target audit caught this at 30 and it was the only
   thing the whole widget failed. Anchored to the corner rather than inset,
   so growing the target moved the X by 3px and nothing else. */
.cb-tx{
  position:absolute; top:0; right:0; width:44px; height:44px;
  display:grid; place-items:center; padding:0; background:none; border:none;
  color:var(--faint); transition:color .3s var(--ease-out);
}
.cb-tx svg{ width:12px; height:12px; }
@media (hover:hover){ .cb-tx:hover{ color:var(--ink); } }

/* ---------- panel ----------
   Two locks, the same pair .cs-modal needed: [hidden] for the frames before
   scripting runs, and :not(.open) for everything after. One alone leaves a
   full-height fixed box sitting over the page eating clicks. */
.cb-panel[hidden], .cb-panel:not(.open){ display:none; }
.cb-panel{
  position:fixed; right:clamp(16px,5vw,52px); z-index:72;
  bottom:calc(var(--fab-b) + var(--fab) + var(--gap));
  /* 392x700, matching the proportions of the support widgets this is
     modelled on. Narrower than the 430 it was, and considerably taller: with
     answers now arriving as three or four separate bubbles, HEIGHT is what
     decides whether the whole exchange stays on screen, and width past about
     390px only stretches each bubble into fewer, longer lines. */
  width:min(392px, calc(100vw - 32px));
  max-height:min(700px, calc(100dvh - var(--fab-b) - var(--fab) - 34px));
  display:flex; flex-direction:column; overflow:hidden;
  background:var(--panel); border:1px solid var(--line); border-radius:2px;
  box-shadow:0 34px 80px -40px rgba(0,0,0,.78);
  transform-origin:100% 100%;
  animation:cb-in .42s var(--ease-out) both;
}
.cb-panel.closing{ animation:cb-out .26s var(--ease-out) both; }
@keyframes cb-in{ from{ opacity:0; transform:translateY(12px) scale(.94); } to{ opacity:1; transform:none; } }
@keyframes cb-out{ from{ opacity:1; transform:none; } to{ opacity:0; transform:translateY(8px) scale(.96); } }

/* head — the same three-dot title bar the hero terminal uses, so the panel
   reads as another instrument on this page rather than a third-party box. */
.cb-bar{
  flex:0 0 auto; display:flex; align-items:center; gap:11px;
  padding:12px 8px 12px 14px; border-bottom:1px solid var(--line); background:var(--elev);
}
/* The robot sits in an accent disc, so the panel opens on a face rather than
   on a row of labels. The three window dots it replaced were a nice nod to
   the hero terminal and said nothing to a visitor. */
.cb-face{
  flex:0 0 auto; width:34px; height:34px; display:grid; place-items:center;
  border-radius:50%; background:color-mix(in oklab, var(--accent) 16%, transparent);
  color:var(--accent);
}
.cb-face .rb{ width:22px; height:22px; }
.cb-head-t{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:1px; }
.cb-title{
  min-width:0; font-family:var(--mono); font-size:var(--fs-2xs);
  letter-spacing:.18em; text-transform:uppercase; color:var(--ink);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.cb-live{
  display:inline-flex; align-items:center; gap:6px; min-width:0;
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.06em; color:var(--faint);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.cb-live i{ flex:0 0 auto; width:6px; height:6px; border-radius:50%; background:var(--accent); animation:pulse 2.4s var(--ease-out) infinite; }
.cb-x{
  flex:0 0 auto; width:36px; height:36px; display:grid; place-items:center;
  padding:0; background:none; border:none; color:var(--faint);
  transition:color .3s var(--ease-out);
}
.cb-x svg{ width:13px; height:13px; }
@media (hover:hover){ .cb-x:hover{ color:var(--ink); } }

/* log */
.cb-log{
  flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  padding:16px 14px 4px; display:flex; flex-direction:column; gap:11px;
  scrollbar-width:thin;
}
.cb-log::-webkit-scrollbar{ width:5px; }
.cb-log::-webkit-scrollbar-thumb{ background:var(--line); }

/* A bot answer is an avatar plus a bubble on one baseline. align-items at
   the end keeps the robot level with the LAST line of a long answer, which
   is where the reader finishes rather than where they started. */
.cb-row{ display:flex; align-items:flex-start; gap:9px; max-width:94%; }
/* A continuation bubble in the same run: no avatar, indented by exactly the
   avatar's width plus the gap so it lines up under the first one. */
.cb-row-cont{ padding-left:37px; margin-top:-5px; }
.cb-bubbles{ display:flex; flex-direction:column; gap:4px; min-width:0; }
.cb-who{
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.1em;
  color:var(--faint); margin-bottom:1px;
}
/* The conversation's opening timestamp, centred like a date separator. */
.cb-time{
  align-self:center; margin:2px 0 6px;
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.08em; color:var(--faint);
}
.cb-av{
  flex:0 0 auto; width:28px; height:28px; display:grid; place-items:center;
  border-radius:50%; background:color-mix(in oklab, var(--accent) 14%, transparent);
  color:var(--accent); margin-top:17px;   /* sits beside the bubble, below the name */
}
.cb-av .rb{ width:18px; height:18px; }
.cb-row .cb-msg{ max-width:100%; }

.cb-msg{
  max-width:88%; padding:11px 13px; border-radius:2px;
  font-size:var(--fs-md); line-height:1.62;
  animation:cb-msg .42s var(--ease-out) both;
}
@keyframes cb-msg{ from{ opacity:0; transform:translateY(7px); } to{ opacity:1; transform:none; } }
.cb-bot{ align-self:flex-start; background:var(--elev); border:1px solid var(--line); color:var(--ink); border-left:2px solid color-mix(in oklab, var(--accent) 60%, transparent); }
.cb-me{ align-self:flex-end; background:var(--accent); color:var(--on-accent); border:1px solid var(--accent); }
.cb-msg strong{ font-weight:600; }
.cb-msg a{ color:inherit; text-decoration:underline; text-underline-offset:3px; }

/* the typing beat before an answer — three dots on the same cadence as the
   hero terminal's spinner, so the wait reads as the machine working */
.cb-typing{ align-self:flex-start; margin-left:37px; margin-top:2px; display:inline-flex; gap:5px; padding:13px; background:var(--elev); border:1px solid var(--line); border-radius:2px; }
.cb-typing i{ width:6px; height:6px; border-radius:50%; background:var(--faint); animation:cb-blink 1.25s ease-in-out infinite; }
.cb-typing i:nth-child(2){ animation-delay:.16s; }
.cb-typing i:nth-child(3){ animation-delay:.32s; }
@keyframes cb-blink{ 0%,60%,100%{ opacity:.28; transform:none } 30%{ opacity:1; transform:translateY(-3px) } }

/* chips */
/* Indented to the bubbles' left edge (14px padding + 28px avatar + 9px gap)
   so the suggestions read as a continuation of what the robot just said,
   not as a separate toolbar stuck to the bottom of the log. */
.cb-chips{ flex:0 0 auto; display:flex; flex-wrap:wrap; align-items:center; gap:7px; padding:4px 14px 13px 51px; }
.cb-chips-k{
  flex:0 0 100%; margin-bottom:2px;
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.2em;
  text-transform:uppercase; color:var(--faint);
}
.cb-chips:empty{ display:none; }
.cb-chip{
  min-height:38px; padding:9px 12px; background:none; border:1px solid var(--line); border-radius:2px;
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.06em; color:var(--muted);
  text-align:left; transition:color .3s var(--ease-out), border-color .3s var(--ease-out), background .3s var(--ease-out);
  animation:cb-msg .4s var(--ease-out) both; animation-delay:calc(var(--i,0) * 45ms);
}
@media (hover:hover){ .cb-chip:hover{ color:var(--accent); border-color:var(--accent); background:color-mix(in oklab, var(--accent) 8%, transparent); } }
.cb-chip:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

/* input */
/* The input's label. There is no site-wide visually-hidden utility — the only
   thing that looked like one is .hp-field, which is the contact form's
   honeypot and must never be reused for something a screen reader SHOULD
   read. */
.cb-sr{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
}
.cb-form{ flex:0 0 auto; display:flex; gap:8px; padding:11px 12px; border-top:1px solid var(--line); }
.cb-in{
  flex:1 1 auto; min-width:0; height:40px; padding:0 12px;
  background:var(--bg); border:1px solid var(--field-line); border-radius:2px;
  color:var(--ink); font-family:inherit; font-size:var(--fs-md);
  transition:border-color .3s var(--ease-out);
}
.cb-in::placeholder{ color:var(--faint); }
.cb-in:focus{ outline:none; border-color:var(--accent); }
.cb-send{
  flex:0 0 auto; width:40px; height:40px; display:grid; place-items:center;
  padding:0; border:1px solid var(--accent); border-radius:2px;
  background:var(--accent); color:var(--on-accent);
  transition:filter .3s var(--ease-out);
}
.cb-send svg{ width:16px; height:16px; }
@media (hover:hover){ .cb-send:hover{ filter:brightness(1.1); } }
.cb-send:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

/* the human handoff — pinned to the foot, present from the first frame and
   never scrolled away, because "talk to a person" must not be something you
   have to find */
.cb-human{
  flex:0 0 auto; display:flex; align-items:center; gap:10px; min-height:44px;
  padding:11px 14px; border-top:1px solid var(--line);
  background:color-mix(in oklab, var(--accent) 9%, transparent);
  font-family:var(--mono); font-size:var(--fs-2xs); letter-spacing:.13em;
  text-transform:uppercase; color:var(--accent);
  transition:background .3s var(--ease-out);
}
.cb-human svg{ width:16px; height:16px; fill:currentColor; flex:0 0 auto; }
.cb-human .cb-arrow{ margin-left:auto; fill:none; stroke:currentColor; stroke-width:1.8; width:13px; height:13px; }
@media (hover:hover){ .cb-human:hover{ background:color-mix(in oklab, var(--accent) 17%, transparent); } }

/* ---------- phones ---------- */
@media (max-width:1023px){
  /* .mobile-bar owns the bottom edge; the launcher takes the slot .totop had
     and .totop steps up one. Both numbers come from --fab-b so they cannot
     drift apart. */
  .cb{ --fab-b: calc(66px + env(safe-area-inset-bottom,0) + 12px + var(--consent-h)); }
  .totop{ bottom:calc(66px + env(safe-area-inset-bottom,0) + 12px + 68px + var(--consent-h)); }
}
@media (max-width:640px){
  /* A 384px card on a 360px screen is a card with no margins. Full-bleed
     sheet instead, which is also the shape a thumb expects down here. */
  .cb-panel{
    left:10px; right:10px; width:auto;
    max-height:min(76dvh, calc(100dvh - var(--fab-b) - var(--fab) - 24px));
  }
  /* The 51px indent costs a third of the line on a 360px screen. */
  .cb-chips{ padding-left:14px; }
  .cb-row{ max-width:100%; }
  .cb-teaser{ width:min(272px, calc(100vw - 24px)); right:12px; }
}
/* Landscape phones: there is no room for a launcher, a panel and a mobile bar
   at 380px of height. The panel takes what is left above the bar and scrolls. */
@media (max-height:520px){
  .cb-panel{ max-height:calc(100dvh - var(--fab-b) - var(--fab) - 16px); }
  .cb-teaser{ display:none; }
}

/* ---------- motion off ---------- */
@media (prefers-reduced-motion:reduce){
  .cb-fab,.cb-teaser,.cb-panel,.cb-msg,.cb-chip{ transition:none; animation:none; }
  .cb-fab{ transform:none; }
  .cb-fab.show{ transform:none; }
  .cb-teaser{ transform:none; }
  .cb-teaser.in{ transform:none; }
  .cb-panel{ opacity:1; transform:none; }
  .cb-panel.closing{ animation:none; }
  .cb-typing i,.cb-live i{ animation:none; }
  .cb-fab svg{ transition:opacity .2s linear; }
}

/* ==========================================================================
   The robot.

   Hand-drawn rather than Lottie: lottie-web is ~250KB against a 230KB total
   JS budget, connect-src is pinned to 'self' so the JSON would be
   self-hosted on top, and a Lottie bakes its own colours — on a two-theme
   site it would be the one mark that does not follow the theme. This is
   about 2KB, in tokens, correct in both themes.

   WHAT WAS WRONG WITH THE FIRST VERSION, because the same mistakes are easy
   to make again:

   - `opacity:.14` on the head. opacity fades the WHOLE element, stroke
     included, so the outline that defines the face was drawn at 14% and the
     robot read as a grey smudge. fill-opacity is the property that was
     wanted; the shape below is a clean outline instead, which holds at 30px
     far better than a translucent fill.
   - A 7-degree snap rotation of the whole head every nine seconds. Rotation
     is the wrong axis for "alive" — it reads as a glitch or a tic. What
     reads as alive is a slow vertical bob, which is why every character
     animator uses one.
   - `d:path()` for the mouth, which is unsupported widely enough that a
     second scaleX animation was stacked on the same element as a fallback.
     Two animations fighting over one property is not a fallback, it is a
     coin toss. The mouth is a rect now and scaleX alone does the job.

   One drawing, three sizes: the launcher, the panel head, and beside each
   answer. Sizes come from the parent, so the geometry is written once.
   ========================================================================== */

.rb{ display:block; overflow:visible; }
.rb *{ transform-box:fill-box; }

.rb-head{ fill:none; stroke:currentColor; stroke-width:2.3; }
.rb-ant{ fill:none; stroke:currentColor; stroke-width:2.3; stroke-linecap:round; }
.rb-tip{ fill:currentColor; transform-origin:center; }
.rb-eye{ fill:currentColor; transform-origin:center; }
.rb-mouth{ fill:currentColor; transform-origin:center; }

/* ---- idle ----------------------------------------------------------------
   A slow vertical bob on the whole body. This is the one that does the work:
   it is continuous, it never snaps, and it reads as breathing rather than as
   a mechanism firing. Everything else is a detail on top of it. */
@keyframes rb-bob{ 0%,100%{ transform:translateY(.9px); } 50%{ transform:translateY(-.9px); } }
.rb-body{ animation:rb-bob 3.6s ease-in-out infinite; }

/* One clean blink every six seconds. The first version blinked twice in
   quick succession every 5.2s, which at this size is a twitch. Real blinks
   are rare and fast: 5% of the cycle, and snapped rather than eased, because
   an eased blink looks like a droopy eyelid. */
@keyframes rb-blink{
  0%, 93%, 100% { transform:scaleY(1); }
  95%           { transform:scaleY(.08); }
  97%           { transform:scaleY(1); }
}
.rb-eye{ animation:rb-blink 6s linear infinite; }

/* The eyes glance around. This is the cheapest possible charm — two pixels
   of translate — and it is the thing that makes a face look like it is
   paying attention rather than staring through you. Off-phase with the blink
   (8s against 6s) so the two never lock into a pattern. */
@keyframes rb-look{
  0%, 26%, 100%      { transform:translateX(0); }
  33%, 43%           { transform:translateX(1.5px); }
  52%, 62%           { transform:translateX(-1.5px); }
  70%                { transform:translateX(0); }
}
.rb-eyes{ animation:rb-look 8s ease-in-out infinite; }

@keyframes rb-tip{ 0%,100%{ opacity:.45; transform:scale(.86); } 50%{ opacity:1; transform:scale(1.06); } }
.rb-tip{ animation:rb-tip 2.8s ease-in-out infinite; }

/* ---- hover ----
   Leans in and perks up: faster bob, brighter antenna, wider mouth. No
   rotation anywhere. */
@media (hover:hover){
  .cb-fab:hover .rb-body{ animation-duration:1.5s; }
  .cb-fab:hover .rb-tip{ animation-duration:.85s; }
  .cb-fab:hover .rb-mouth{ transform:scaleX(1.35); transition:transform .3s var(--ease-out); }
  .cb-fab:hover .rb-eye{ transform:scale(1.12); }
}

/* ---- speaking ----
   Runs while an answer is being composed, on the same beat as the typing
   dots, so the avatar and the bubble are doing one thing together. */
@keyframes rb-talk{ 0%,100%{ transform:scaleX(1) scaleY(1); } 50%{ transform:scaleX(.4) scaleY(1.5); } }
.cb.thinking .rb-mouth{ animation:rb-talk .44s ease-in-out infinite; }
.cb.thinking .rb-eye{ animation-duration:2.6s; }

/* ---- attention ring ----
   Three pulses after the launcher lands, then never again. Three is an
   invitation; a permanent pulse is a nag, and it is the most common reason
   people close these things without reading them. */
.cb-ring{
  position:absolute; inset:0; border-radius:50%;
  border:1.5px solid var(--accent); opacity:0; pointer-events:none;
}
.cb-fab.show .cb-ring{ animation:cb-ring 2.6s var(--ease-out) 3; animation-delay:1.4s; }
@keyframes cb-ring{
  0%   { opacity:.55; transform:scale(1); }
  70%  { opacity:0;   transform:scale(1.7); }
  100% { opacity:0;   transform:scale(1.7); }
}

@media (prefers-reduced-motion:reduce){
  .rb-body,.rb-eye,.rb-eyes,.rb-tip,.cb.thinking .rb-mouth,.cb-fab.show .cb-ring{ animation:none; }
  .cb-fab:hover .rb-mouth,.cb-fab:hover .rb-eye{ transform:none; }
  .rb-tip{ opacity:.9; }
}
