
/* ═══════════════════════════════════════════════════════════════════════════
   THE COMPONENT
   ═══════════════════════════════════════════════════════════════════════════ */

.pxc{
  position: fixed; inset: auto 0 0 0; z-index: 10000;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 12px calc(12px + env(safe-area-inset-bottom, 0px));
  font: 400 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink);
  pointer-events: none;
}
.pxc > *{ pointer-events: auto; }
.pxc[hidden]{ display: none; }

/* ── Material world A — ICE (matches the reference) ───────────────────────── */
.pxc[data-skin="ice"]{
  --pxc-ground:  #EFEBEC;
  --ink:         #0F0B0C;
  --ink-2:       #6B5A60;
  --ink-3:       #98868C;
  /* CLEAR glass — the interactive layer. Near-zero in the middle so the
     backdrop reads straight through, exactly like the reference's pill. */
  --g-top:       rgba(255,255,255,.74);
  --g-mid:       rgba(255,255,255,.13);
  --g-bot:       rgba(246,193,210,.42);
  /* MILKY glass — the structural layer, like the reference's card. Heavier so
     text sits on a stable ground instead of on whatever is behind it. */
  --p-top:       rgba(255,255,255,.90);
  --p-mid:       rgba(255,255,255,.70);
  --p-bot:       rgba(247,199,214,.46);
  --rim-top:     rgba(255,255,255,.98);
  --rim-mid:     rgba(255,255,255,.34);
  --rim-bot:     rgba(255,248,250,.90);
  --rim-w:       1.3px;
  --spec:        .62;
  --blur:        16px;
  --sat:         1.75;
  --bloom:       rgba(255,255,255,.95);
  --sh-contact:  rgba(86,32,50,.10);
  --sh-amb-1:    rgba(86,32,50,.20);
  --sh-amb-2:    rgba(86,32,50,.14);
  --solid:       #FFFFFF;
  --solid-ink:   #0B0D0E;
  --accent:      #D93F6B;
  --field:       rgba(255,252,253,.44);
  --bot-bub:     rgba(255,252,253,.64);
  --err:         #C2412F;
}



/* ── Glass primitive ──────────────────────────────────────────────────────────
   Three separate optical layers, because no single box-shadow reads as glass:
     1. BODY   — near-transparent, tint pooling at the bottom + specular hotspot
     2. RIM    — constant-width band wrapping the whole perimeter (padded mask)
     3. INNER  — the far face of the slab, seen through the near one
   Plus a tight contact shadow and two wide ambient shadows. */
.gl{
  position: relative;
  isolation: isolate;
  background:
    radial-gradient(128% 74% at 26% -2%, rgba(255,255,255,var(--spec)) 0%, rgba(255,255,255,0) 52%),
    linear-gradient(180deg, var(--g-top) 0%, var(--g-mid) 44%, var(--g-mid) 56%, var(--g-bot) 100%);
  backdrop-filter: blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter: blur(var(--blur)) saturate(var(--sat));
  box-shadow:
    var(--focus-ring, 0 0 0 0 rgba(0,0,0,0)),
    0 1px 2px var(--sh-contact),
    0 18px 34px -16px var(--sh-amb-1),
    0 48px 78px -40px var(--sh-amb-2);
}
.gl::before,
.gl::after{ content: ""; position: absolute; border-radius: inherit; pointer-events: none; }

/* RIM — inset box-shadows cannot do this: they light one side and die at the
   corners. The padded-box mask keeps the band a constant width all the way
   round, including through the radius. */
.gl::before{
  inset: 0; z-index: 3;
  padding: var(--rim-w);
  background: linear-gradient(177deg, var(--rim-top) 0%, var(--rim-mid) 38%, var(--rim-mid) 62%, var(--rim-bot) 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;
  filter: blur(.4px);
}
/* INNER EDGE — the back face of the slab. Very faint; it is what gives the
   material thickness rather than reading as a flat tinted rectangle. */
.gl::after{
  inset: var(--inner-edge, 5px); z-index: 2;
  border-radius: calc(var(--rr, 26px) - var(--inner-edge, 5px));
  padding: .7px;
  background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.30));
  -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;
  opacity: .5;
}
/* BLOOM — the rim geometry again, blurred and pushed outward. This is the
   single detail that makes the reference's pill read as lit rather than
   outlined. */
.bloom{
  position: absolute; inset: -1px; z-index: -1; border-radius: inherit;
  padding: 4px;
  background: linear-gradient(180deg, var(--bloom), rgba(255,255,255,.22) 46%, var(--bloom));
  -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;
  filter: blur(5px);
  opacity: .78;
  pointer-events: none;
  transition: opacity .28s ease;
}

/* ── 1 · Call + WhatsApp row ──────────────────────────────────────────────── */
/* Icon-only now, so the row stops being a full-width split and becomes three
   compact circles aligned to the panel's right edge — the same edge the dock
   sits on, so the whole cluster reads as one object. */
.pxc__row{ display: flex; gap: 12px; margin-bottom: 12px; justify-content: center; }

.cta{
  --rr: 29px; --inner-edge: 4px;
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 58px; height: 58px; padding: 0;
  border: 0; border-radius: 50%;
  color: #000;
  cursor: pointer;
  text-decoration: none;
  will-change: transform;
  -webkit-tap-highlight-color: transparent;
}

/* THE CENTRING FIX. .cta is display:grid, and the sheen overlay was only given
   position:absolute inside @media (hover:hover) and (pointer:fine). On a touch
   device that query never matches, so the sheen stayed in flow as a second grid
   item: the grid built two rows (17.5px + 40.5px) and the icon centred inside
   the lower one — measured 8.75px below the circle's centre. Hence "especially
   on mobile". Decoration must never occupy layout, so it is positioned here,
   unconditionally; the hover block adds only the moving light. */
.cta > .cta__sheen{
  position: absolute; inset: 0;
  border-radius: inherit; z-index: 1;
  pointer-events: none; overflow: hidden;
}
/* And a structural guarantee: the icon claims the single cell outright, so no
   future child can push it into a row of its own again. */
.cta > .cta__ico{ grid-area: 1 / 1; }
.cta__ico{
  display: block;
  display: block;
  width: 23px; height: 23px;
  flex: 0 0 auto; position: relative; z-index: 4;
  will-change: transform;
}
.cta__ico path{ fill: currentColor; }

/* COLOURLESS GLASS — deliberately no brand tint on either button. A green
   WhatsApp wash and a blue Call wash made them read as two coloured plastic
   chips; with the colour gone the material has to carry them, so all four
   optical events below are doing real work:

     1  specular   — a soft window reflection lying on the top face
     2  caustic    — light that has crossed the slab and pooled along the base
     3  graze      — the sides curve away from the viewer, so the last ~8% of
                     each edge brightens: this is what gives the pill thickness
     4  refraction — a wider blur + higher saturation than the panel, because a
                     small clear body bends more of what is behind it

   Both buttons are now identical material; they are told apart by icon and
   label alone, which is also the honest hierarchy — neither is the primary. */
/* Optically identical to the send button, which is the reference for what a
   small round control looks like here.

   The thick ring was a 90deg gradient painting bright bands down the left and
   right edges. On a pill that reads as a graze; on a circle it wraps into a
   heavy band that fights the actual rim — two borders where the material
   should have one. Removed, along with the 2.0 saturation and the 6px inner
   edge, which together drew a second visible ring inside the first.

   Only the plate is kept, because these sit over photographs and the black
   glyphs need something to hold onto. */
.cta{
  --g-top: rgba(255,255,255,.72);
  --g-mid: rgba(255,255,255,.30);
  --g-bot: rgba(255,255,255,.52);
  --blur: 20px;
  --sat: .95;
  background:
    radial-gradient(64% 58% at 32% 16%, rgba(255,255,255,calc(var(--spec) * 1.6)) 0%, rgba(255,255,255,0) 68%),
    radial-gradient(90% 60% at 50% 112%, rgba(255,255,255,calc(var(--spec) * 1.1)) 0%, rgba(255,255,255,0) 62%),
    linear-gradient(180deg, var(--g-top) 0%, var(--g-mid) 52%, var(--g-bot) 100%),
    /* the plate — bottom layer, keeps black glyphs legible over any backdrop */
    linear-gradient(rgba(255,255,255,.20), rgba(255,255,255,.20));
}
.cta:hover .bloom{ opacity: 1; }
.cta:focus-visible{ outline: 2.5px solid var(--accent); outline-offset: 3px; }

/* ── 2 · The chat panel ───────────────────────────────────────────────────── */
.panel{
  --rr: 30px; --inner-edge: 7px;
  --g-top: var(--p-top); --g-mid: var(--p-mid); --g-bot: var(--p-bot);
  --blur: 26px;
  border-radius: var(--rr);
  padding: 7px 14px 13px;
  will-change: transform;
  transform-origin: 50% 100%;
}

.grab{
  width: 38px; height: 4.5px; margin: 3px auto 8px;
  border-radius: 999px;
  background: var(--ink-3); opacity: .5;
  cursor: grab; touch-action: none;
}
.panel.is-dragging .grab{ cursor: grabbing; }

.head{ display: flex; align-items: flex-start; gap: 10px; padding: 12px 2px 20px; }
.head__txt{ flex: 1 1 auto; min-width: 0; }
.head h2{
  margin: 0;
  font-family: ui-serif, "New York", Georgia, serif;
  font-size: 26px; font-weight: 500; letter-spacing: -.02em; line-height: 1.1;
  color: var(--ink);
}
.head p{ margin: 7px 0 0; font-size: 12.5px; font-weight: 460; line-height: 1.42; color: var(--ink-2); letter-spacing: .004em; }
.head__live{
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; letter-spacing: .04em; color: var(--ink-2);
}
.head__live i{ width: 6px; height: 6px; border-radius: 50%; background: #2FBF71; box-shadow: 0 0 0 3px rgba(47,191,113,.20); }

.xbtn{
  --rr: 15px; --inner-edge: 4px;
  flex: 0 0 auto; width: 30px; height: 30px; border: 0; border-radius: var(--rr);
  display: grid; place-items: center; cursor: pointer; color: var(--ink-2);
  -webkit-tap-highlight-color: transparent;
}
.xbtn svg{ width: 12px; height: 12px; }
.xbtn:focus-visible{ outline: 2px solid var(--accent); outline-offset: 2px; }

/* Chat log */
.log{
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 16px);
  mask-image: linear-gradient(180deg, transparent 0, #000 16px);
  scrollbar-width: none;
}
.log::-webkit-scrollbar{ display: none; }
.log__in{ display: flex; flex-direction: column; gap: 7px; padding: 12px 2px 2px; }

.msg{ display: flex; }
.msg--bot{ justify-content: flex-start; }
.msg--me{ justify-content: flex-end; }
.bub{
  max-width: 84%;
  padding: 9px 13px;
  border-radius: 19px;
  font-size: 14.5px; line-height: 1.36; letter-spacing: -.005em;
  will-change: transform;
  word-break: break-word;
}
.msg--bot .bub{
  background: var(--bot-bub);
  border: 1px solid var(--rim-mid);
  border-top-color: var(--rim-top);
  border-bottom-left-radius: 7px;
  color: var(--ink);
  transform-origin: 0% 100%;
  box-shadow: 0 1px 2px var(--sh-contact);
}
.msg--me .bub{
  background: var(--solid);
  color: var(--solid-ink);
  border-bottom-right-radius: 7px;
  font-weight: 560;
  transform-origin: 100% 100%;
  box-shadow: 0 1px 2px var(--sh-contact), 0 8px 18px -10px var(--sh-amb-1);
}
.msg--err .bub{ color: var(--err); }
.msg--skip .bub{ background: none; color: var(--ink-3); font-weight: 500; font-size: 13px; padding: 4px 6px; box-shadow: none; }
.bub b{ font-weight: 660; }

/* Typing indicator */
.typing{ display: flex; gap: 4px; align-items: center; padding: 3px 2px; }
.typing i{
  width: 6.5px; height: 6.5px; border-radius: 50%; background: var(--ink-3);
  animation: blip 1.15s ease-in-out infinite;
}
.typing i:nth-child(2){ animation-delay: .14s; }
.typing i:nth-child(3){ animation-delay: .28s; }
@keyframes blip{ 0%,60%,100%{ opacity:.32; transform: translateY(0) } 30%{ opacity:1; transform: translateY(-3px) } }

/* Progress chips */
/* The chips carry the full question now, not a two-word label, so the rail has
   to scroll. It is the progress bar and the question list in one object: the
   active chip is the question being asked, so it is always pulled to the left
   edge, and everything behind it stays scrolled-past as a visible record. The
   mask fades both ends so a half-cut chip reads as "more this way" rather than
   as a clipped bug. */
.chips{
  display: flex; gap: 7px; padding: 40px 2px 12px;
  overflow-x: auto; overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 14px, #000 calc(100% - 22px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 14px, #000 calc(100% - 22px), transparent 100%);
}
.chips::-webkit-scrollbar{ display: none; }

/* ── Select-question options ──────────────────────────────────────────────────
   A select step swaps the text field for a set of tappable pills. Choosing one
   submits immediately — the extra "confirm" tap buys nothing when the choice is
   the answer. "Others" is the exception: it hands the text field back. */
.opts{
  display: none; flex-wrap: wrap; gap: 7px;
  padding: 0 2px 11px;
  max-height: 31vh; overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: none;
}
.opts::-webkit-scrollbar{ display: none; }
.opts.is-on{ display: flex; }
.opts.is-wait, .skiprow.is-wait{ pointer-events: none; opacity: .45; }
.opt{
  --rr: 15px; --inner-edge: 4px;
  flex: 0 0 auto;
  padding: 9px 14px; border: 0; border-radius: var(--rr);
  font: 560 13.5px/1 system-ui, -apple-system, sans-serif;
  letter-spacing: -.006em; color: var(--ink);
  cursor: pointer; white-space: nowrap;
  will-change: transform;
  -webkit-tap-highlight-color: transparent;
}
.opt:hover .bloom{ opacity: 1; }
.opt:focus-visible{ outline: 2.5px solid var(--accent); outline-offset: 3px; }

/* SKIP — the lead is already banked after the phone number, so every question
   past it is enrichment, not a toll gate. Without this the popup becomes a
   nine-question wall guarding a callback the person has already earned, and the
   only way out is the one the idle timer was built to catch. Quiet by design:
   it is an exit, not an invitation. */
.skiprow{ display: none; justify-content: flex-end; padding: 0 6px 10px; margin-top: -2px; }
.skiprow.is-on{ display: flex; }
.skip{
  border: 0; background: none; cursor: pointer;
  padding: 4px 2px;
  font: 560 12px/1 system-ui, -apple-system, sans-serif;
  letter-spacing: .01em; color: var(--ink-2);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
  -webkit-tap-highlight-color: transparent;
}
.skip:hover{ color: var(--ink); }
.skip:focus-visible{ outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.chip.is-skip{ color: var(--ink-3); opacity: .62; }
.chip.is-skip .chip__dot{ opacity: 1; background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }
.chip{
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 6px;
  height: 29px; padding: 0 12px;
  border-radius: 999px; border: 1px solid var(--rim-mid);
  background: var(--field);
  font: 560 12px/1 system-ui, sans-serif; letter-spacing: -.005em;
  color: var(--ink-3);
  white-space: nowrap; overflow: hidden;
  transition: color .3s ease, background .3s ease, border-color .3s ease;
}
.chip__dot{
  width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto;
  background: currentColor; opacity: .45;
  transition: opacity .3s ease, transform .3s ease;
}
.chip.is-active{
  color: var(--ink);
  background: var(--solid);
  border-color: transparent;
  box-shadow: 0 1px 2px var(--sh-contact), 0 6px 14px -8px var(--sh-amb-1);
}
.chip.is-active .chip__dot{ opacity: 1; background: #2FBF71; transform: scale(1.15); }
.chip.is-done{ color: var(--ink-2); background: var(--field); border-color: var(--rim-mid); }
.chip.is-done .chip__dot{ opacity: 1; background: transparent; }
.chip.is-done .chip__dot::after{
  content: ""; display: block; width: 8px; height: 8px; margin: -1px 0 0 -1px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.6 6.3l2.9 3L10.4 2.6' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.6 6.3l2.9 3L10.4 2.6' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
}

/* Composer */
.composer{ display: flex; align-items: center; gap: 8px; will-change: transform; }
/* THE WELL — the input is the one piece of glass that is *recessed*, and it is
   lit upside-down to say so: dim along the top lip, bright along the bottom.
   A raised body catches light on its top face; a trough catches it on the far
   inside wall. Flipping the rim gradient is what sells the difference — with
   both lit the same way the input read as another button sitting in a row of
   buttons. The inset shadow under the top lip is the contact shadow of the
   surrounding surface falling into the well. */
.field{
  --rr: 24px; --inner-edge: 5px;
  --g-top: rgba(255,255,255,.10);
  --g-mid: rgba(255,255,255,.26);
  --g-bot: rgba(255,255,255,.60);
  --blur: 20px;
  /* The one piece of glass that DESATURATES what is behind it (--sat below 1).
     Everything else here is clear glass and takes the colour of the backdrop;
     over the Ice panel that turned the input into a solid blue lozenge. Frosting
     it neutral separates the well from the panel it is cut into, and it is the
     honest material anyway — a deep trough scatters light, it does not transmit
     it cleanly. */
  --sat: .45;
  --rim-w: 1.5px;
  flex: 1 1 auto; min-width: 0; position: relative;
  display: flex; align-items: center;
  height: 48px; border-radius: var(--rr);
  background:
    radial-gradient(122% 62% at 50% 120%, rgba(255,255,255,calc(var(--spec) * .9)) 0%, rgba(255,255,255,0) 66%),
    linear-gradient(180deg, var(--g-top) 0%, var(--g-mid) 44%, var(--g-bot) 100%);
  box-shadow:
    var(--focus-ring, 0 0 0 0 rgba(0,0,0,0)),
    inset 0 2px 4px -1px var(--sh-contact),
    inset 0 -1px 0 rgba(255,255,255,.42);
  transition: box-shadow .28s ease, backdrop-filter .28s ease, -webkit-backdrop-filter .28s ease;
}
/* Rim flipped: bright edge at the bottom, dim at the top. */
.field::before{
  background: linear-gradient(177deg, var(--rim-mid) 0%, var(--rim-mid) 42%, var(--rim-bot) 78%, var(--rim-top) 100%);
  transition: filter .28s ease;
}
/* On dark, a .60 white well glows almost as hard as the send button and the row
   loses its hierarchy. The trough stays lighter than the panel — that is the
   dark-mode convention and it is legible — just not competitively so. */
/* ── The well's optics ───────────────────────────────────────────────────────
   A ring — coloured OR white — is paint applied on top of the material. Glass
   does not signal focus by growing an outline; it signals by moving light.
   Everything below is light and shadow inside the trough: a specular band that
   travels across the surface, a caustic pool where light gathers at the base,
   and a deeper occlusion shadow under the top lip. No outline anywhere.

   This needs its own layer because ::before is already the rim and ::after the
   inner edge; the lens sits under the text (z-index 1) so glyphs stay crisp. */
.field__lens{
  position: absolute; inset: 0; z-index: 1;
  border-radius: inherit; overflow: hidden;
  pointer-events: none;
}
/* SPECULAR — a soft raking band. Parked off to the left at rest; focus slides
   it into the surface, and typing sweeps it across. Rotated slightly so it
   reads as light raking a curved surface rather than a wipe transition. */
.field__lens::before{
  content: ""; position: absolute;
  top: -60%; bottom: -60%; left: -30%; width: 42%;
  background: linear-gradient(90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,calc(var(--spec) * .38)) 38%,
    rgba(255,255,255,calc(var(--spec) * .62)) 52%,
    rgba(255,255,255,calc(var(--spec) * .30)) 66%,
    rgba(255,255,255,0) 100%);
  filter: blur(6px);
  transform: translateX(-40%) rotate(9deg);
  opacity: 0;
  transition: opacity .42s ease, transform .75s cubic-bezier(.22,.61,.36,1);
}
/* CAUSTIC — light pooling along the floor of the trough, brightest at centre.
   This is what gives the well a bottom; without it the field reads as a flat
   tinted rectangle. */
.field__lens::after{
  content: ""; position: absolute; left: 8%; right: 8%; bottom: -80%;
  height: 150%; border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(255,255,255,calc(var(--spec) * .85)) 0%,
    rgba(255,255,255,calc(var(--spec) * .22)) 52%,
    rgba(255,255,255,0) 100%);
  opacity: .35;
  transition: opacity .42s ease, transform .5s cubic-bezier(.22,.61,.36,1);
}

/* FOCUS — the trough deepens and catches more light. Refraction rises, the cut
   under the top lip darkens, the bottom lip brightens, and the specular slides
   in. Same three optical layers the rest of the component uses, turned up. */
.field:focus-within{
  --blur: 30px;
  --sat: 1.15;
  --spec: .92;
  box-shadow:
    inset 0 3.5px 7px -2px var(--sh-contact),
    inset 0 -1.5px 0 rgba(255,255,255,.70),
    inset 0 0 20px -7px rgba(255,255,255,.50),
    0 3px 12px -9px var(--sh-amb-1);
}
.field:focus-within::before{ filter: blur(.4px) brightness(1.35); }
.field:focus-within .field__lens::before{ opacity: .9; transform: translateX(46%) rotate(9deg); }
.field:focus-within .field__lens::after{ opacity: .72; transform: translateY(-3px); }

/* TYPING — a second, travelling highlight, driven by a spring in JS rather
   than a keyframe.

   A keyframe animation was the obvious way to do this and it was wrong: when
   the class came off, the browser cancelled it mid-sweep and the band snapped
   back to its resting transform. Nothing that moves in this component stops
   like that. As a spring the sweep simply completes — it eases out as it
   leaves the far edge and parks off-screen, so "stopped typing" is a follow
   -through rather than a cut. Starting again reseeds it outside the clip, so
   the reset is never visible.

   It is a real element, not a pseudo, because JS has to write its transform
   every frame and pseudo-elements are unreachable from script. */
.field__rake{
  position: absolute;
  top: -60%; bottom: -60%; left: -30%; width: 38%;
  background: linear-gradient(90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,calc(var(--spec) * .30)) 36%,
    rgba(255,255,255,calc(var(--spec) * .58)) 52%,
    rgba(255,255,255,calc(var(--spec) * .26)) 68%,
    rgba(255,255,255,0) 100%);
  filter: blur(7px);
  transform: translateX(-60%) rotate(9deg);
  will-change: transform;
}
/* The pool leans brighter while they type and eases back — a transition, so it
   too has nowhere to snap to. */
.field.is-typing .field__lens::after{ opacity: .9; }
.field.is-err{ --focus-ring: 0 0 0 3.5px color-mix(in srgb, var(--err) 20%, transparent); }
.field.is-err::before{ background: linear-gradient(177deg, var(--err) 0%, var(--err) 100%); opacity: .8; }
.field input{
  /* Above the lens, so a blurred specular band never hazes the glyphs. */
  position: relative; z-index: 2;
  flex: 1 1 auto; min-width: 0;
  appearance: none; background: none; border: 0; outline: 0;
  padding: 0 6px 0 17px;
  font: 500 16px/1 system-ui, -apple-system, sans-serif;
  letter-spacing: -.008em;
  color: var(--ink);
}
.field input::placeholder{ color: var(--ink-3); font-weight: 450; }
.field__count{
  position: relative; z-index: 2;
  padding-right: 13px; font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--ink-3); font-variant-numeric: tabular-nums; opacity: 0; transition: opacity .2s ease;
}
.field__count.is-on{ opacity: 1; }

/* THE SEND — glass, but the brightest and densest body in the whole stack.
   That is how it stays the primary action without falling back to a flat white
   fill: it is not a different *kind* of thing, it is the most lit thing. A
   sphere rather than a slab, so the specular is a tight round hotspot up and
   left, and the bloom is wider and hotter than any other element's.
   Disabled drains the light instead of just lowering opacity — the glass goes
   flat and the bloom dies, which reads as "not yet" rather than "broken". */
.send{
  --rr: 24px; --inner-edge: 4px;
  --g-top: rgba(255,255,255,.72);
  --g-mid: rgba(255,255,255,.30);
  --g-bot: rgba(255,255,255,.52);
  --blur: 20px;
  /* Held at ~1: a saturation boost here amplified the panel's cyan pooling and
     turned the send blue next to a neutral input. The two controls share a row,
     so they have to agree on colour temperature. */
  --sat: .95;
  flex: 0 0 auto; width: 48px; height: 48px; border: 0; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  color: var(--ink);
  background:
    radial-gradient(64% 58% at 32% 16%, rgba(255,255,255,calc(var(--spec) * 1.6)) 0%, rgba(255,255,255,0) 68%),
    radial-gradient(90% 60% at 50% 112%, rgba(255,255,255,calc(var(--spec) * 1.1)) 0%, rgba(255,255,255,0) 62%),
    linear-gradient(180deg, var(--g-top) 0%, var(--g-mid) 52%, var(--g-bot) 100%);
  will-change: transform;
  -webkit-tap-highlight-color: transparent;
  transition: background .3s ease, box-shadow .3s ease;
}
/* A circle has no corners, so the inner edge has to be told it is round. */
.send::after{ border-radius: 50%; }
.cta::after{ border-radius: 50%; }
.cta > .cta__sheen{ border-radius: 50%; }
.send .bloom{ padding: 5px; filter: blur(7px); opacity: .95; }
.send:hover .bloom{ opacity: 1; }
.send svg{ width: 18px; height: 18px; position: relative; z-index: 4; will-change: transform; }
/* Shockwave — a ring thrown off the button at the moment of send. It lives
   outside the button's own bounds, so it must not be clipped by anything. */
.burst{
  position: absolute; inset: -3px; border-radius: 50%;
  border: 2px solid var(--rim-top);
  opacity: 0; pointer-events: none; z-index: 5;
  will-change: transform, opacity;
}
.send:disabled{
  cursor: default;
  --g-top: rgba(255,255,255,.20);
  --g-mid: rgba(255,255,255,.07);
  --g-bot: rgba(255,255,255,.12);
  --spec: .12;
  color: var(--ink-3);
}
.send:disabled .bloom{ opacity: .18; }
.send:focus-visible{ outline: 2.5px solid var(--accent); outline-offset: 3px; }

/* Success */
.done{ display: none; text-align: center; padding: 4px 6px 6px; }
.done.is-on{ display: block; }
.done__ring{
  width: 54px; height: 54px; margin: 0 auto 11px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--solid); color: var(--solid-ink);
  box-shadow: 0 1px 2px var(--sh-contact), 0 12px 26px -12px var(--sh-amb-1);
  will-change: transform;
}
.done__ring svg{ width: 26px; height: 26px; }
.done__ring path{ stroke-dasharray: 30; stroke-dashoffset: 30; }
.done h3{
  margin: 0 0 4px;
  font-family: ui-serif, "New York", Georgia, serif;
  font-size: 19px; font-weight: 500; letter-spacing: -.014em; color: var(--ink);
}
.done p{ margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--ink-2); }
.done p b{ color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ── Desktop: dock bottom-right instead of spanning the viewport ──────────── */
@container (min-width: 620px){
  .pxc{ align-items: flex-end; padding: 0 20px calc(20px + env(safe-area-inset-bottom, 0px)); }
  .pxc__stack{ width: 384px; }
  .panel{ --rr: 26px; }
  .cta{ --rr: 24px; height: 54px; font-size: 15px; }
  .head h2{ font-size: 20px; }
}
.pxc__stack{ will-change: transform; }

/* ── Accessibility ───────────────────────────────────────────────────────── */
@media (prefers-reduced-transparency: reduce){
  .gl{ backdrop-filter: none; -webkit-backdrop-filter: none; }
  .pxc[data-skin="ice"]    .gl{ background: #EDF6F9; }
      .pxc[data-skin="ice"]    .panel{ background: #F6FBFC; }
      .bloom{ display: none; }
}
@media (prefers-contrast: more){
  .gl::before{ --rim-mid: var(--rim-top); }
  .chip{ border-color: var(--ink-2); }
  .field{ border-color: var(--ink-2); }
}
@media (prefers-reduced-motion: reduce){
  .typing i{ animation: none; opacity: .55; }
  .device{ transition: none; }
}

/* ── Webhook readout ─────────────────────────────────────────────────────── */
.side{ flex: 1 1 300px; min-width: 280px; display: flex; flex-direction: column; gap: 16px; }
.card{ background: var(--sh-panel); border: 1px solid var(--sh-line-2); border-radius: 15px; padding: 16px 17px; }
.card h3{
  margin: 0 0 3px; font-size: 11px; font-weight: 680; letter-spacing: .1em;
  text-transform: uppercase; color: var(--sh-mute);
}
.card > p{ margin: 0 0 11px; font-size: 13px; line-height: 1.5; color: var(--sh-mute); }
pre{
  margin: 0; padding: 13px 14px; border-radius: 10px; overflow-x: auto;
  background: var(--sh-code-bg); color: var(--sh-code-ink);
  font: 400 12px/1.62 ui-monospace, SFMono-Regular, Menlo, monospace;
  tab-size: 2;
}
.notes{ margin: 0; padding-left: 17px; font-size: 13px; line-height: 1.62; color: var(--sh-mute); }
.notes li + li{ margin-top: 6px; }
.notes b{ color: var(--sh-ink); font-weight: 600; }
kbd{
  font: 600 11px/1 ui-monospace, Menlo, monospace; padding: 2px 5px;
  border: 1px solid var(--sh-line); border-radius: 5px; background: var(--sh-chip); color: var(--sh-ink);
}

/* ── Width ───────────────────────────────────────────────────────────────────
   The Elementor document is 100vw, which is correct on a phone and wrong
   everywhere else: at 1280 the stack measured 1256px, stretching two buttons
   across the whole screen and turning the chip rail into a news ticker. Cap it.
   Centred on tablet, docked bottom-right on desktop — where a contact
   affordance is expected to sit, and clear of the page's own content. */
@media (min-width: 600px){
  #pxc{ align-items: center; }
  #pxc #stack{ width: 460px; max-width: calc(100vw - 32px); }
}
@media (min-width: 900px){
  #pxc{
    align-items: flex-end;
    padding: 0 28px calc(28px + env(safe-area-inset-bottom, 0px));
  }
  #pxc #stack{ width: 404px; }
}

/* ── Chip window + CTA hover ────────────────────────────────────────────────*/
.chip.is-next{ opacity: .62; }
.chip[hidden]{ display: none !important; }

/* The sweep is a real animated value, not a keyframe on background-position:
   registering it as a <percentage> is what lets it transition smoothly, and it
   means hover-out reverses from wherever the light currently is instead of
   snapping back to the start. */
@property --sheen-x {
  syntax: '<percentage>';
  initial-value: -150%;
  /* MUST inherit: the value is animated on .cta but consumed by the .cta__sheen
     child. With inherits:false the child kept resolving to the initial -150%
     while the parent transitioned to 150%, so the light never moved. */
  inherits: true;
}

@media (hover: hover) and (pointer: fine){
  .cta{
    --sheen-x: -150%;
    transition: --sheen-x .9s cubic-bezier(.22,.61,.36,1);
  }
  /* The sweep rides on top of the existing four optical layers. */
  .cta::before{ z-index: 3; }
  .cta > .cta__sheen{
    background: linear-gradient(104deg,
      rgba(255,255,255,0) 40%,
      rgba(255,255,255,calc(var(--spec) * .55)) 47%,
      rgba(255,255,255,calc(var(--spec) * .95)) 50%,
      rgba(255,255,255,calc(var(--spec) * .55)) 53%,
      rgba(255,255,255,0) 60%) no-repeat;
    background-size: 220% 100%;
    background-position-x: var(--sheen-x, -150%);
  }
  .cta:hover{ --sheen-x: 150%; }

  /* Icons get the personality; the pill itself is lifted by a spring in JS,
     because its transform is already owned there and two writers would fight. */
  @keyframes pxv-ring{
    0%{ transform: rotate(0) }      14%{ transform: rotate(-14deg) }
    28%{ transform: rotate(11deg) } 42%{ transform: rotate(-8deg) }
    56%{ transform: rotate(6deg) }  70%{ transform: rotate(-4deg) }
    85%{ transform: rotate(2deg) }  100%{ transform: rotate(0) }
  }
  @keyframes pxv-pulse{
    0%{ transform: scale(1) }   32%{ transform: scale(1.16) }
    58%{ transform: scale(.96) } 100%{ transform: scale(1) }
  }
  #ctaCall:hover .cta__ico{ animation: pxv-ring .8s cubic-bezier(.36,.07,.19,.97); }
  #ctaWa:hover   .cta__ico{ animation: pxv-pulse .62s cubic-bezier(.34,1.56,.64,1); }
  @keyframes pxv-lift{
    0%{ transform: translateY(0) }   35%{ transform: translateY(-3px) }
    65%{ transform: translateY(1px) } 100%{ transform: translateY(0) }
  }
  #ctaMail:hover .cta__ico{ animation: pxv-lift .58s cubic-bezier(.34,1.4,.64,1); }
}

@media (prefers-reduced-motion: reduce){
  .cta{ transition: none; }
  #ctaCall:hover .cta__ico, #ctaWa:hover .cta__ico{ animation: none; }
}

/* Elementor's popup wrapper carries backdrop-filter, which makes it the
   containing block for this position:fixed root — the box collapsed to 28px on
   the live site. Pin it to the viewport explicitly. */
/* Bottom-anchored, content-sized — never a forced height.
   Elementor's popup wrapper carries backdrop-filter, which makes it the
   containing block for this fixed element. That wrapper is full-screen on
   desktop but a fixed 703px on mobile (the popup's own height_mobile setting).
   Forcing height:100dvh therefore made the component TALLER than its container
   on a phone, and because the stack is bottom-aligned the whole thing hung
   below the visible area — the overlay blur was all that showed.

   `inset: auto 0 0 0` pins it to the bottom of whatever the container is and
   lets content decide the height, so it is correct at both sizes and immune to
   whatever the wrapper's height happens to be. */
/* ── Why this is not just a z-index ──────────────────────────────────────────
   Elementor's .dialog-widget carries backdrop-filter: blur(10px). That does two
   things, and both broke this popup:

     1. it makes .dialog-widget a STACKING CONTEXT, so any z-index we set on a
        descendant only ranks inside it. z-index:10000 could never beat the
        overlay's 9999 — proof: elementFromPoint at the Call icon's centre
        returned .dialog-widget, not our button. The popup was painting
        underneath the blur the whole time.

     2. it makes .dialog-widget the CONTAINING BLOCK for position:fixed
        descendants, so our geometry resolved against .dialog-widget-content —
        which Elementor leaves at height:0, top:900 (collapsed at the very
        bottom edge). getBoundingClientRect therefore reported coordinates that
        looked perfectly on-screen, which is why every geometry assertion passed
        while nothing was visible.

   The fix is to stop fighting the wrapper: neutralise the blur on the element
   that owns it, and re-apply the same blur on a plain overlay child that has no
   filter of its own. The page still dims and blurs exactly as before, but
   .dialog-widget stops being a stacking context and a containing block, so our
   fixed positioning resolves against the viewport and normal painting order
   applies. */
.dialog-widget.elementor-popup-modal{
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
.dialog-widget.elementor-popup-modal > .dialog-widget-content{ z-index: 1; }
.dialog-widget.elementor-popup-modal::before{
  content: "";
  position: fixed;
  inset: 0;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  pointer-events: none;
  z-index: 0;
}

/* ── The actual containing block ─────────────────────────────────────────────
   Elementor's own widget wrapper around this markup was the culprit. A probe of
   the ancestor chain flagged it as a containing block for position:fixed while
   every property I printed came back null — because I had not printed
   `perspective`. Any of transform / perspective / filter / will-change /
   contain on an ancestor does it, and that wrapper is 640x0, which is why the
   popup resolved to a zero-height box instead of the viewport (pxcW measured
   640, never 1440) and why the offset changed with the breakpoint.

   Neutralise every trigger on that one wrapper. `position` is deliberately left
   alone — relative does not create a containing block for fixed, and changing
   it risks Elementor's own layout. */
.elementor-element.elementor-element-pxvhtml1{
  transform: none !important;
  perspective: none !important;
  filter: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  will-change: auto !important;
  contain: none !important;
}

/* Now genuinely at the viewport, and above the blur layer. */
#pxc{
  position: fixed;
  inset: auto 0 0 0;
  top: auto;
  height: auto;
  max-height: 100vh;
  max-height: 100dvh;
  z-index: 10000;
}
#pxc[hidden]{ display: none !important; }

/* The two Elementor form widgets are submitted programmatically and never seen.
   Off-screen rather than display:none so their inputs stay real. */
.pxv-eform-lead, .pxv-eform-full{
  position: absolute !important; left: -9999px !important; top: 0 !important;
  width: 1px !important; height: 1px !important;
  overflow: hidden !important; opacity: 0 !important; pointer-events: none !important;
}

/* ── Resume / restart ───────────────────────────────────────────────────────*/
.restart{
  --rr: 17px; --inner-edge: 4px;
  margin: 14px auto 2px; display: inline-flex; align-items: center; justify-content: center;
  padding: 10px 18px; border: 0; border-radius: var(--rr);
  font: 600 13px/1 system-ui, -apple-system, sans-serif; letter-spacing: -.004em;
  color: var(--ink); cursor: pointer; position: relative;
  -webkit-tap-highlight-color: transparent;
}
.restart:hover .bloom{ opacity: 1; }
.restart:focus-visible{ outline: 2.5px solid var(--accent); outline-offset: 3px; }
/* A resumed conversation says so, quietly — arriving mid-transcript with no
   explanation reads as a bug, not a convenience. */
.resumed{
  align-self: center; margin: 2px auto 4px; padding: 4px 12px;
  border-radius: 999px; background: var(--field); border: 1px solid var(--rim-mid);
  font: 600 10.5px/1.4 system-ui, sans-serif; letter-spacing: .05em; text-transform: uppercase;
  color: var(--ink-3);
}

/* Black label on a 20% white backing.

   Colour first: these are <a> elements and a theme link rule keeps winning —
   raising specificity to `#pxc a.cta` still lost, and a scan of every readable
   stylesheet found no rule of ours matching the anchor at all. !important is
   the honest tool here rather than a fourth specificity guess.

   Backing second: the pills are clear glass, so their legibility depended on
   whatever page content happened to sit behind them — dark hero imagery made
   black text disappear. A 20% white plate underneath the optics guarantees a
   minimum brightness on any background without turning the glass opaque; it is
   the bottom layer of the stack, so specular, caustic and graze all still read
   on top of it. */
#pxc .cta,
#pxc a.cta,
#pxc .cta{ color: #000 !important; }
#pxc .cta .cta__ico path{ fill: #000 !important; }
.pxc[data-skin="obsidian"] #pxc .cta{ color: #fff !important; }
.pxc[data-skin="obsidian"] #pxc .cta .cta__ico path{ fill: #fff !important; }

/* ── Anti-flash, fail-open ───────────────────────────────────────────────────
   The stack starts hidden so no frame paints it at rest before JS parks it.

   But it MUST NOT depend on JS to become visible. Elementor clones the popup
   markup at runtime, so several #pxc can exist; `is-armed` is applied via
   getElementById, which reaches only the first. If the clone actually on screen
   was not the one armed, it stayed hidden forever — the "only the blur shows"
   bug. An animation with a delay reveals it regardless: JS arming is now just
   the fast path, and CSS guarantees the slow one. Nothing can leave it hidden. */
#pxc #stack{
  visibility: hidden;
  animation: pxv-reveal 0s linear 1.1s forwards;
}
@keyframes pxv-reveal{ to{ visibility: visible; } }
#pxc.is-armed #stack{ visibility: visible; animation: none; }
@media (prefers-reduced-motion: reduce){
  #pxc #stack{ visibility: visible; animation: none; }
}

/* ── Collapsed icon cluster ──────────────────────────────────────────────────
   Idle for 3.2s and the three buttons gather into a small overlapping stack at
   the left edge, the way a set of coins slides together. Tapping the stack
   spreads them again.

   Nothing here is a keyframe: position and scale are separate springs per icon
   (§3 decompose 2D motion), so a tap mid-collapse reverses from wherever each
   icon currently is instead of snapping. The overlap is what sells it as
   liquid — the circles pass through each other rather than lining up. */
/* The buttons must paint ABOVE the panel. They enter by rising through the
   same band of screen the panel is climbing into, and .panel comes later in
   the DOM — so without this the panel covered them mid-flight and they only
   reappeared once they had cleared its top edge. Read as: Call enters, the
   chat area enters, and only then WhatsApp and Email pop out from behind it.
   The order was always right; two of the three were simply hidden. */
.pxc__row{ position: relative; z-index: 3; }
.panel{ z-index: 1; }
.cta{ transform-origin: 50% 50%; }
.pxc__row.is-collapsed .cta{ cursor: pointer; }
/* Larger invisible hit area around the collapsed stack — a 35px cluster is
   below the comfortable tap target, so the target is padded, not the art. */
.pxc__row.is-collapsed::before{
  content: ""; position: absolute; left: -8px; top: -7px;
  width: 96px; height: 72px; z-index: 5;
}

/* ── Travelling light on the panel rim ───────────────────────────────────────
   A slow highlight that drifts around the panel's edge, like a reflection
   moving across a curved glass lip. It rides the existing rim geometry (the
   padded-box mask) so it reads as light ON the material, not a border added to
   it. 18s and low contrast on purpose: at the edge of perception, never a
   thing that asks to be watched. */
@property --pxv-rim{
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
@keyframes pxv-rim-travel{ to{ --pxv-rim: 360deg; } }
.panel.gl::before{
  background:
    conic-gradient(from var(--pxv-rim, 0deg),
      rgba(255,255,255,0) 0deg,
      rgba(255,255,255,.85) 26deg,
      rgba(255,255,255,0) 62deg,
      rgba(255,255,255,0) 180deg,
      rgba(255,255,255,.55) 208deg,
      rgba(255,255,255,0) 250deg,
      rgba(255,255,255,0) 360deg),
    linear-gradient(177deg, var(--rim-top) 0%, var(--rim-mid) 38%, var(--rim-mid) 62%, var(--rim-bot) 100%);
  animation: pxv-rim-travel 18s linear infinite;
}

/* ── Send button: liquid fill on first word ─────────────────────────────────
   The button fills from the bottom as soon as there is something to send —
   a level rising in a glass, not a colour swap. The surface has a slight
   curve and drifts, so it reads as liquid rather than a progress bar. */
.send__fill{
  position: absolute; inset: 0; z-index: 2;
  border-radius: 50%; overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
}
.send.is-ready .send__fill{ opacity: 1; }
.send__fill::before{
  content: ""; position: absolute;
  left: -55%; width: 210%; height: 200%;
  bottom: -140%;
  border-radius: 42% 44% 0 0 / 22% 24% 0 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 42%, transparent),
    color-mix(in srgb, var(--accent) 16%, transparent));
  transform: translateY(0) rotate(0deg);
  transition: bottom .62s cubic-bezier(.34,1.28,.64,1);
  animation: pxv-slosh 5.5s ease-in-out infinite;
}
.send.is-ready .send__fill::before{ bottom: -104%; }
@keyframes pxv-slosh{
  0%,100%{ transform: translateX(-3%) rotate(-1.6deg); }
  50%    { transform: translateX(3%)  rotate(1.6deg); }
}

/* The wave keeps waving — it is the only motion in an otherwise still bubble,
   so it stays small and slow enough not to nag. */
@keyframes pxv-wave{
  0%,60%,100%{ transform: rotate(0deg); }
  10%,30%{ transform: rotate(14deg); }
  20%,40%{ transform: rotate(-8deg); }
  50%{ transform: rotate(6deg); }
}
.wave{
  display: inline-block;
  transform-origin: 72% 78%;
  animation: pxv-wave 2.9s ease-in-out infinite;
}
.heart{ display: inline-block; }

@media (prefers-reduced-motion: reduce){
  .panel.gl::before{ animation: none; }
  .wave{ animation: none; }
  .send__fill::before{ animation: none; transition: none; }
}

/* ── The name moment ─────────────────────────────────────────────────────────
   When someone gives their name the heading becomes theirs, and that deserves
   to be marked. Canvas rather than DOM nodes: eighty particles as elements
   would mean eighty style recalcs a frame on a panel that is already running
   springs, a backdrop-filter and a travelling rim light. One canvas is one
   composited layer.

   It sits behind the words (z-index 0 against the h2's 1) so the sparkle reads
   as coming off the text rather than covering it. */
.head__txt{ position: relative; }
.head__spark{
  position: absolute;
  /* Generous, and mostly ABOVE the text: a canvas clips its own drawing, and
     the first version was 82px tall with the particles launching upward out of
     it within a few frames — technically bursting, visibly nothing. */
  /* An explicit width, because a canvas is a REPLACED element: with width:auto
     it takes its intrinsic 300x150 and simply ignores `right`, so the layer was
     300px wide inside a 404px panel and most of the burst fell outside it. */
  display: block;
  left: -22px; top: -46px;
  width: calc(100% + 44px);
  height: 150px;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
}
.head__spark.is-live{ opacity: 1; }
.head h2{ position: relative; z-index: 1; }

/* The swap itself: the old title drops and dims, the new one rises into place.
   Short, because it is punctuation and not an event of its own. */
/* Unhurried on purpose. This is the one moment the panel makes a fuss, and a
   quick swap reads as a glitch where a slow one reads as intent. */
.head h2.is-swapping{
  animation: pxv-title-out .42s cubic-bezier(.4,0,1,1) forwards;
}
.head h2.is-new{
  animation: pxv-title-in 1.05s cubic-bezier(.22,1.22,.36,1) forwards;
}

/* GRADIENT TEXT — the personalised title is painted with the brand's colours
   rather than flat ink, and the gradient sweeps through once as it arrives.
   background-size is oversized so there is somewhere to sweep to; the sweep
   ends on a position that is itself a good static gradient, so nothing flips
   when the animation finishes. It stays for the rest of the session — the
   title is theirs now, and it should keep looking like it. */
.head h2.is-grad{
  background-image: linear-gradient(100deg,
    #E34A74 0%, #C86FA8 18%, #B98CD6 34%,
    #D9A79C 52%, #E8B4B8 66%, #E34A74 84%, #B98CD6 100%);
  background-size: 260% 100%;
  background-position: 0% 50%;
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* Two animations: the sweep, then the settle. The colour is an ARRIVAL, not
     a permanent state — left on, it reads as a stuck effect rather than a
     flourish. Fading -webkit-text-fill-color from transparent to the ink is
     what makes the return smooth: the solid fill rises in front of the
     gradient instead of replacing it in one frame. JS drops the class once the
     settle has finished, by which point the two are pixel-identical. */
  animation:
    pxv-title-grad 1.6s ease-in-out 0s 3 alternate,
    pxv-title-settle 1.2s ease 4.4s forwards;
}
/* Pulsing rather than a single pass: the colour travels out and back three
   times, so the name reads as alight rather than as something being wiped
   across once. `alternate` is what makes it breathe — the return leg is the
   same motion reversed, so there is no seam where a loop restarts. */
@keyframes pxv-title-grad{
  from{ background-position: 6% 50%; }
  to  { background-position: 118% 50%; }
}
@keyframes pxv-title-settle{
  from{ -webkit-text-fill-color: transparent; }
  to  { -webkit-text-fill-color: var(--ink); }
}
@keyframes pxv-title-out{
  to{ opacity: 0; transform: translateY(6px) scale(.97); filter: blur(2px); }
}
@keyframes pxv-title-in{
  from{ opacity: 0; transform: translateY(-10px) scale(.96); filter: blur(3px); }
  to{ opacity: 1; transform: none; filter: none; }
}
/* A warm wash that blooms behind the new name and fades — the gradient the
   particles are drawn from, held for a moment so the colour registers. */
.head h2.is-new::after{
  content: "";
  position: absolute; inset: -18px -22px;
  z-index: -1; pointer-events: none;
  border-radius: 20px;
  background: radial-gradient(60% 120% at 30% 50%,
      rgba(227,74,116,.20) 0%, rgba(185,140,214,.16) 38%,
      rgba(240,217,181,.14) 62%, rgba(255,255,255,0) 100%);
  animation: pxv-title-wash 2.3s ease-out forwards;
}
@keyframes pxv-title-wash{
  0%  { opacity: 0; transform: scale(.86); }
  20% { opacity: 1; transform: scale(1); }
  100%{ opacity: 0; transform: scale(1.08); }
}
@media (prefers-reduced-motion: reduce){
  .head h2.is-swapping, .head h2.is-new, .head h2.is-grad{ animation: none; }
  .head h2.is-new::after{ animation: none; opacity: 0; }
}

/* ── Hold-to-restart ─────────────────────────────────────────────────────────
   A press-and-hold on the close button starts the conversation over. A hidden
   gesture needs to announce itself the moment it begins, or the first 600ms
   just feel like a button that is not responding — so the ring fills as you
   hold, and letting go early cancels it visibly.

   SVG stroke rather than a conic-gradient: dashoffset animates everywhere,
   where an animated angle needs a registered custom property to move at all. */
.xbtn{ position: relative; }
.xbtn__ring{
  position: absolute; inset: -4px;
  width: calc(100% + 8px); height: calc(100% + 8px);
  transform: rotate(-90deg);
  pointer-events: none;
  opacity: 0;
  transition: opacity .18s ease;
}
.xbtn__ring circle{
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.4;
  stroke-linecap: round;
  /* 2πr for r=20 */
  stroke-dasharray: 125.66;
  stroke-dashoffset: 125.66;
}
.xbtn.is-holding .xbtn__ring{ opacity: 1; }
.xbtn.is-holding .xbtn__ring circle{
  animation: pxv-hold 620ms linear forwards;
}
@keyframes pxv-hold{ to{ stroke-dashoffset: 0; } }
