/* =====================================================================
   LeadsNimble Chat Demo — "watch the AI close a sale".

   A Messenger-accurate phone mockup that plays a real sales conversation
   end to end: a photo of the product -> "is this available?" -> price ->
   colourways -> order details -> confirmed sale. It is the PROOF of the
   hero's own headline ("Closed by AI ... closes right in the chat"), which
   until now the page only ever asserted.

   WHERE IT LIVES. Nested inside .lnew-hero2, between the channel strip and
   the "See it work" showcase, exactly the way that showcase is itself
   nested. The narrative order is deliberate: this block is the CUSTOMER's
   side of the conversation, the showcase that follows is the OPERATOR's
   console. Customer first, because that is the half a visitor recognises.

   ---------------------------------------------------------------------
   MODE LOCK. Dark-locked, one palette, same contract as .lnew-hero2 and
   .lnew-nav2 — every token a descendant reads is declared here with a dark
   literal, so the block ignores whatever [data-mode] the nav toggle writes
   on <html>. There is deliberately no [data-mode="dark"] pair: a light
   variant of a black Messenger screen is not a design that exists.

   TWO PALETTES, ON PURPOSE. The SECTION (eyebrow, title, beats rail) runs
   on the site's purple aurora accent so the block belongs to the landing
   page. The PHONE runs on Messenger's own palette — pure black canvas,
   #303030 incoming bubbles, #0084ff outgoing — because the mockup only
   works as proof if it is instantly recognisable as Messenger. Mixing the
   two would cost the recognition and buy nothing. Both are writable from
   the Style tab if that trade is ever revisited.

   ANIMATION CONTRACT (the JS half is lnew-chatdemo.js).
   Every row is rendered server-side and hidden with [hidden]. The script
   plays them in order: for an incoming message it first shows the typing
   pill, holds, then swaps the pill for the bubble — so the row's height is
   already claimed by the pill and the swap reads as the message landing,
   not as the thread jumping. Outgoing rows pop straight in. The thread
   scrolls itself after each row, and takes NO user scroll (see the note on
   .lnew-chatdemo__thread). At the end it holds, cross-fades out, and
   restarts. prefers-reduced-motion gets the finished thread, no loop, no
   typing pill.

   IMAGES. Product photos come from the widget's repeater rows. Any row
   whose image is unset falls back to .lnew-chatdemo__ph, a self-drawn
   placeholder, so the block is never broken while art is pending.
   ===================================================================== */

/* Local reset. :where() so it carries ZERO specificity — a bare
   `.lnew-chatdemo p` would be (0,1,1) and would outrank every (0,1,0)
   component class below it. Same contract as the hero's own reset; do not
   unwrap. See [[nav-widget-feature]]. */
.lnew-chatdemo, .lnew-chatdemo *, .lnew-chatdemo *::before, .lnew-chatdemo *::after { box-sizing: border-box; }
:where(.lnew-chatdemo) :where(p) { margin: 0; }
:where(.lnew-chatdemo) :where(h2, h3) { margin: 0; }
:where(.lnew-chatdemo) :where(ul, ol) { margin: 0; padding: 0; list-style: none; }
:where(.lnew-chatdemo) :where(svg) { display: block; }
:where(.lnew-chatdemo) :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; }

.lnew-chatdemo {
  /* ---- motion / geometry ---- */
  --lncd-ease: cubic-bezier(.22,1,.36,1);
  /* The bubble's landing curve: ease-out-expo. What sells a message as
     "landing" rather than "appearing" is the steep early rise and the long
     settle, NOT an overshoot — an earlier cut used a 1.15 control point and
     measured a 0.64% peak, i.e. 0.05px on this animation's 8px travel.
     Invisible, and 14x smaller than a classic bouncy curve. So the overshoot
     was paying nothing and is gone; the two curves differ by <6% of progress
     at their widest and are sub-pixel apart here. */
  --lncd-pop: cubic-bezier(.16,1,.3,1);
  /* Things LEAVING accelerate away; only things arriving decelerate. Using
     the arrival curve for an exit is what makes a dismissal feel reluctant. */
  --lncd-ease-exit: cubic-bezier(.4,0,1,1);
  --lncd-pill-out: 170ms;
  --lncd-maxw: 980px;
  --lncd-gap: clamp(40px, 5vw, 76px);
  --lncd-pad-t: 0px;
  --lncd-pad-b: 0px;

  /* ---- phone ---- */
  --lncd-phone-w: 344px;
  --lncd-screen-h: 616px;
  --lncd-phone-r: 54px;
  --lncd-screen-r: 42px;
  --lncd-bezel: 11px;

  /* ---- type ---- */
  --lncd-font-sans: 'DM Sans', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --lncd-font-head: 'Parkinsans', 'DM Sans', sans-serif;
  /* The phone deliberately does NOT use the site's faces. Messenger renders
     in the platform UI face, and borrowing DM Sans here is the single
     fastest way to make a mockup read as "a website pretending", so the
     screen asks for the system stack and nothing else. */
  --lncd-font-ui: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, system-ui, sans-serif;
  --lncd-title-size: clamp(26px, 3vw, 38px);

  /* ---- section palette · DARK-LOCKED (site aurora) ---- */
  --lncd-ink: #f5f6ff;
  --lncd-body: #b8bed6;
  --lncd-muted: #949bbb;
  --lncd-line: rgba(255,255,255,.10);
  --lncd-surface: rgba(255,255,255,.035);
  --lncd-accent: #8b7cff;
  --lncd-accent-2: #56d4ff;
  --lncd-accent-halo: rgba(139,124,255,.16);
  --lncd-eyebrow-bg: rgba(255,255,255,.05);
  --lncd-eyebrow-ink: #cfd3ee;
  --lncd-eyebrow-border: rgba(255,255,255,.10);

  /* ---- phone palette · Messenger's own ---- */
  --lncd-frame-1: #3a3a44;
  --lncd-frame-2: #101018;
  --lncd-frame-edge: rgba(255,255,255,.22);
  --lncd-screen-bg: #000000;
  --lncd-bar-bg: rgba(0,0,0,.86);
  --lncd-ui-ink: #ffffff;
  /* Messenger's real secondary-on-dark. The #8a8d91 this started as is
     Messenger's LIGHT-mode secondary; on a black canvas at 11-13px it fails
     to read at all, which is what "the grey text isn't visible" was. */
  --lncd-ui-dim: #b0b3b8;
  --lncd-ui-blue: #0084ff;
  --lncd-bub-in: #303030;
  --lncd-bub-in-ink: #ffffff;
  --lncd-bub-out: #0084ff;
  --lncd-bub-out-ink: #ffffff;
  --lncd-bub-r: 18px;
  --lncd-bub-r-tight: 6px;
  --lncd-glow: rgba(120,96,255,.30);

  position: relative;
  padding-top: var(--lncd-pad-t);
  padding-bottom: var(--lncd-pad-b);
  font-family: var(--lncd-font-sans);
  color: var(--lncd-body);
  /* TYPE-LEAK GUARD. twentytwentyfive puts `font-weight: 300` and
     `letter-spacing: -0.1px` on body text, and they inherit into every widget
     that does not assert otherwise. Measured on the live page before this
     line existed: the chat BUBBLES computed at weight 300 — a Messenger
     bubble rendered in Light is the giveaway that a mockup is a web page.
     Restate both. See [[wp-theme-inherited-type-leak]]. */
  font-weight: 400;
  letter-spacing: normal;
  -webkit-font-smoothing: antialiased;
  /* Asserted, not inherited. A chat bubble is left-aligned text in every
     messenger ever shipped, and this block's usual host — .lnew-hero2__inner
     — centres everything inside it, which silently centres every bubble and
     the whole mockup stops reading as real. The head opts back in below. */
  text-align: left;
}

/* =====================================================================
   Section head — mirrors the showcase's head so the two blocks in this
   hero introduce themselves the same way.
   ===================================================================== */
.lnew-chatdemo__head { text-align: center; max-width: 760px; margin: 0 auto 44px; }

.lnew-chatdemo__eyebrow {
  display: inline-flex; align-items: center; gap: 9px;
  font-size: 15px; font-weight: 600; letter-spacing: .02em; line-height: 1.6;
  color: var(--lncd-eyebrow-ink);
  background: var(--lncd-eyebrow-bg);
  border: 1px solid var(--lncd-eyebrow-border);
  padding: 8px 18px; border-radius: 100px;
}
/* The lamp is the same status-light cycle as the hero's eyebrow: lit, then
   a long dark hold. A continuous pulse reads as decoration; a gap reads as
   a live indicator, which is the claim being made. */
.lnew-chatdemo__eyebrow-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--lncd-accent);
  box-shadow: 0 0 0 4px var(--lncd-accent-halo);
  opacity: .45;
  animation: lncd-lamp 5.2s ease-in-out infinite;
}
@keyframes lncd-lamp {
  0%        { opacity: .35; box-shadow: 0 0 0 4px var(--lncd-accent-halo), 0 0 0 0 transparent; }
  8%        { opacity: 1;   box-shadow: 0 0 0 4px var(--lncd-accent-halo), 0 0 7px 1px var(--lncd-accent); }
  22%       { opacity: 1;   box-shadow: 0 0 0 4px var(--lncd-accent-halo), 0 0 7px 1px var(--lncd-accent); }
  40%, 100% { opacity: .35; box-shadow: 0 0 0 4px var(--lncd-accent-halo), 0 0 0 0 transparent; }
}

.lnew-chatdemo__title {
  margin-top: 18px;
  font-family: var(--lncd-font-head);
  font-size: var(--lncd-title-size);
  font-weight: 700; line-height: 1.16; letter-spacing: -.02em;
  color: var(--lncd-ink);
  text-wrap: balance;
}
.lnew-chatdemo__title em {
  font-style: normal;
  background: linear-gradient(92deg, var(--lncd-accent) 0%, var(--lncd-accent-2) 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.lnew-chatdemo__lead {
  margin-top: 14px;
  font-size: 17px; line-height: 1.6; color: var(--lncd-body);
  text-wrap: pretty;
}

/* =====================================================================
   Layout — beats rail + phone.
   ===================================================================== */
.lnew-chatdemo__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* STRETCH, not center. The rail's natural height is ~144px shorter than the
     phone at 1440, and hard-coding a gap to close that only works at one
     width and one set of line-wraps. Stretching the row and letting the rail
     distribute its own items keeps the two columns the same height at every
     width automatically. */
  align-items: stretch;
  gap: var(--lncd-gap);
  max-width: var(--lncd-maxw);
  margin-inline: auto;
}
/* Rail off => the phone is the whole block and centres itself. */
.lnew-chatdemo--norail .lnew-chatdemo__inner { grid-template-columns: auto; justify-content: center; }

/* =====================================================================
   Beats rail. Each beat lights as the conversation reaches it, so the
   copy is not a list beside the demo — it is a running commentary on it.
   ===================================================================== */
/* space-between spreads the items down the stretched column; the gap is the
   floor for when the text wraps enough to fill it on its own. The roomier
   item padding does half the work, so the free space left to distribute stays
   small enough that the list still reads as a list rather than five things
   drifting apart. */
.lnew-chatdemo__beats {
  display: flex; flex-direction: column;
  justify-content: space-between;
  gap: 12px;
  text-align: left;
}

.lnew-chatdemo__beat {
  position: relative;
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  align-items: start;
  column-gap: 14px; row-gap: 2px;
  /* 25px, not 14px, and this is what keeps the GAPS small. The column has to
     match the phone's height either way; the only question is whether that
     height is paid for inside the items or between them. At 20px padding the
     items totalled 530px of a 638px column and space-between spread the
     remaining 108px as 27px gaps, which read as five things drifting apart.
     Paying it inside instead leaves ~58px to distribute — roughly 14px gaps —
     and the list reads as a list. Measured, not guessed. */
  padding: 25px 16px;
  border: 1px solid transparent;
  border-radius: 16px;
  transition: background .5s var(--lncd-ease), border-color .5s var(--lncd-ease);
}
.lnew-chatdemo__beat.is-on {
  background: var(--lncd-surface);
  border-color: var(--lncd-line);
}

.lnew-chatdemo__beat-ic {
  grid-row: span 2;
  width: 38px; height: 38px; border-radius: 12px;
  display: grid; place-items: center;
  color: var(--lncd-muted);
  background: rgba(255,255,255,.04);
  border: 1px solid var(--lncd-line);
  transition: color .5s var(--lncd-ease), background .5s var(--lncd-ease),
              border-color .5s var(--lncd-ease), box-shadow .5s var(--lncd-ease);
}
.lnew-chatdemo__beat-ic svg { width: 18px; height: 18px; }
.lnew-chatdemo__beat.is-on .lnew-chatdemo__beat-ic {
  color: #fff;
  background: linear-gradient(135deg, var(--lncd-accent) 0%, var(--lncd-accent-2) 140%);
  border-color: transparent;
  box-shadow: 0 0 0 4px var(--lncd-accent-halo);
}
/* A beat already played stays credited, but stops competing with the live
   one — it keeps the panel and loses the halo. */
.lnew-chatdemo__beat.is-done .lnew-chatdemo__beat-ic {
  color: var(--lncd-accent);
  background: rgba(139,124,255,.12);
  border-color: rgba(139,124,255,.28);
  box-shadow: none;
}

.lnew-chatdemo__beat-t {
  font-family: var(--lncd-font-head);
  font-size: 16px; font-weight: 600; line-height: 1.35; letter-spacing: -.01em;
  color: var(--lncd-body);
  transition: color .5s var(--lncd-ease);
}
.lnew-chatdemo__beat.is-on .lnew-chatdemo__beat-t,
.lnew-chatdemo__beat.is-done .lnew-chatdemo__beat-t { color: var(--lncd-ink); }

/* No opacity dimming here. Stacking .82 on an already-muted token is how a
   description ends up effectively invisible, and it is unpredictable because
   the token itself is mapped in from whatever host the block sits in. */
.lnew-chatdemo__beat-d {
  font-size: 15px; font-weight: 400; line-height: 1.5; color: var(--lncd-muted);
}

/* =====================================================================
   Phone.

   The frame is drawn, not an image: a gradient rim (the titanium edge), an
   inset highlight along the top-left, and the screen inset by --lncd-bezel.
   Drawing it keeps it crisp at every DPR and lets the Style tab recolour it.
   ===================================================================== */
.lnew-chatdemo__stage { position: relative; display: grid; place-items: center; }

/* The aurora bloom behind the phone — the same purple the hero's wires run
   on, so the mockup sits IN the section rather than on top of it. */
.lnew-chatdemo__stage::before {
  content: '';
  position: absolute; z-index: 0;
  left: 50%; top: 50%; translate: -50% -50%;
  width: 120%; aspect-ratio: 1;
  background: radial-gradient(circle at 50% 50%, var(--lncd-glow) 0%, transparent 62%);
  filter: blur(18px);
  pointer-events: none;
}

.lnew-chatdemo__phone {
  position: relative; z-index: 1;
  width: var(--lncd-phone-w);
  padding: var(--lncd-bezel);
  border-radius: var(--lncd-phone-r);
  background: linear-gradient(160deg, var(--lncd-frame-1) 0%, var(--lncd-frame-2) 42%, var(--lncd-frame-2) 68%, var(--lncd-frame-1) 100%);
  box-shadow:
    0 0 0 1px rgba(0,0,0,.6),
    inset 0 1px 0 var(--lncd-frame-edge),
    inset 0 -1px 0 rgba(255,255,255,.06),
    0 42px 90px -28px rgba(0,0,0,.85),
    0 18px 40px -20px rgba(90,70,220,.45);
}

/* Side buttons. Purely decorative, so they are pseudo-elements on the frame
   rather than nodes a screen reader has to skip. */
.lnew-chatdemo__phone::before,
.lnew-chatdemo__phone::after {
  content: '';
  position: absolute;
  width: 3px; border-radius: 3px;
  background: linear-gradient(180deg, var(--lncd-frame-1), var(--lncd-frame-2));
  pointer-events: none;
}
/* volume pair, left */
.lnew-chatdemo__phone::before { left: -3px; top: 132px; height: 94px; box-shadow: 0 -58px 0 0 currentColor; color: transparent; }
/* power, right */
.lnew-chatdemo__phone::after  { right: -3px; top: 160px; height: 76px; }

.lnew-chatdemo__screen {
  position: relative;
  height: var(--lncd-screen-h);
  border-radius: var(--lncd-screen-r);
  overflow: hidden;
  background: var(--lncd-screen-bg);
  display: flex; flex-direction: column;
  font-family: var(--lncd-font-ui);
  font-weight: 400;
  letter-spacing: normal;
  -webkit-font-smoothing: antialiased;
}

/* Dynamic island. */
.lnew-chatdemo__island {
  position: absolute; z-index: 6;
  /* Offset from the PHONE's box, so the bezel has to be paid for first. At a
     flat `top: 9px` the island's top edge landed 2px ABOVE the screen and the
     notch appeared to sit on the metal frame — measured: island top 1185,
     screen top 1187. It must always clear --lncd-bezel. */
  left: 50%; translate: -50% 0; top: calc(var(--lncd-bezel) + 8px);
  width: 96px; height: 27px; border-radius: 100px;
  background: #000;
  pointer-events: none;
}
.lnew-chatdemo__island::after {
  content: '';
  position: absolute; right: 9px; top: 50%; translate: 0 -50%;
  width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #24243a 0%, #06060b 68%);
  box-shadow: inset 0 0 2px rgba(120,140,255,.5);
}

/* ---- status bar ---- */
.lnew-chatdemo__status {
  flex: none;
  display: flex; align-items: center; justify-content: space-between;
  padding: 15px 26px 8px;
  color: var(--lncd-ui-ink);
}
.lnew-chatdemo__clock { font-size: 14px; font-weight: 600; letter-spacing: .01em; }
.lnew-chatdemo__sysic { display: flex; align-items: center; gap: 5px; }
.lnew-chatdemo__sysic svg { height: 11px; width: auto; }

/* ---- conversation header ---- */
.lnew-chatdemo__bar {
  flex: none;
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  padding: 6px 14px 10px;
  background: var(--lncd-bar-bg);
  border-bottom: .5px solid rgba(255,255,255,.10);
}
.lnew-chatdemo__back { color: var(--lncd-ui-blue); display: grid; place-items: center; }
.lnew-chatdemo__back svg { width: 11px; height: 18px; }

.lnew-chatdemo__ava {
  width: 32px; height: 32px; border-radius: 50%;
  overflow: hidden; flex: none;
  display: grid; place-items: center;
  background: linear-gradient(150deg, #2b2f52 0%, #14162b 100%);
  border: 1px solid rgba(255,255,255,.14);
}
/* (0,2,1): `.elementor img{height:auto}` is (0,1,1) and would otherwise tie
   or win on enqueue order. The redundant root class is deliberate — same
   reason the hero carries it on its marquee images. */
.lnew-chatdemo .lnew-chatdemo__ava img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lnew-chatdemo__ava-txt { font-size: 13px; font-weight: 700; color: #fff; letter-spacing: -.02em; }

.lnew-chatdemo__who { min-width: 0; display: flex; flex-direction: column; gap: 0; }
.lnew-chatdemo__name {
  font-size: 15px; font-weight: 600; line-height: 1.25; color: var(--lncd-ui-ink);
  letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lnew-chatdemo__sub { font-size: 12px; font-weight: 500; line-height: 1.3; color: var(--lncd-ui-dim); }

.lnew-chatdemo__acts { display: flex; align-items: center; gap: 15px; color: var(--lncd-ui-blue); }
.lnew-chatdemo__acts svg { width: 19px; height: 19px; }

/* =====================================================================
   Thread.

   scrollbar hidden on every engine: the mockup is a phone, and a visible
   desktop scrollbar inside it is the tell that breaks the illusion.
   ===================================================================== */
.lnew-chatdemo__thread {
  flex: 1 1 auto;
  min-height: 0;
  /* HIDDEN, NOT AUTO — deliberately. The demo drives this thread; a visitor
     who can scroll it can scroll away from the message that is landing, or
     back to the top mid-playback, and the section stops being a demo. An
     overflow:hidden box is still a scroll container, so scrollTop and
     scrollTo() keep working for the player — only the user's wheel, drag and
     touch are refused. Do not "fix" this back to auto. */
  overflow: hidden;
  padding: 14px 12px 8px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  overscroll-behavior: contain;
}
.lnew-chatdemo__thread::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ---- business intro block (the "profile card" at the top of a business
        chat: logo, name, reply time, social proof, category, privacy link) ---- */
.lnew-chatdemo__intro { text-align: center; padding: 16px 22px 22px; }
.lnew-chatdemo__intro-ava {
  width: 76px; height: 76px; border-radius: 50%;
  margin: 0 auto 12px; overflow: hidden;
  display: grid; place-items: center;
  background: linear-gradient(150deg, #2b2f52 0%, #14162b 100%);
  border: 1px solid rgba(255,255,255,.14);
}
.lnew-chatdemo .lnew-chatdemo__intro-ava img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lnew-chatdemo__intro-ava .lnew-chatdemo__ava-txt { font-size: 30px; }

.lnew-chatdemo__intro-name {
  font-size: 21px; font-weight: 700; line-height: 1.25; color: var(--lncd-ui-ink);
  letter-spacing: -.02em;
}
.lnew-chatdemo__intro-meta {
  margin-top: 5px;
  font-size: 13px; font-weight: 500; line-height: 1.45; color: var(--lncd-ui-dim);
}
.lnew-chatdemo__intro-link {
  display: inline-block; margin-top: 7px;
  font-size: 13px; font-weight: 500; line-height: 1.45; color: var(--lncd-ui-blue);
}

/* ---- rows ---- */
.lnew-chatdemo__feed { display: flex; flex-direction: column; }

.lnew-chatdemo__row { display: flex; margin-top: 2px; }
/* A new speaker starts a new run — the extra air is what makes runs legible
   as runs without any divider. */
.lnew-chatdemo__row.is-runstart { margin-top: 10px; }
.lnew-chatdemo__row[hidden] { display: none; }

.lnew-chatdemo__row--out { justify-content: flex-end; }
.lnew-chatdemo__row--in  { justify-content: flex-start; align-items: flex-end; gap: 6px; }

/* centred timestamp */
.lnew-chatdemo__row--time { justify-content: center; margin: 14px 0 6px; }
.lnew-chatdemo__time {
  font-size: 12px; font-weight: 600; letter-spacing: .01em;
  color: var(--lncd-ui-dim);
}

/* right-aligned "You replied to their story/image" */
.lnew-chatdemo__row--note { justify-content: flex-end; margin: 12px 0 3px; padding-right: 6px; }
.lnew-chatdemo__note { font-size: 11.5px; line-height: 1.4; color: var(--lncd-ui-dim); }

/* the 24px avatar slot on incoming rows. It is always present so bubbles in
   a run stay on one left edge; only the LAST row of a run paints it. */
.lnew-chatdemo__rowava {
  width: 24px; height: 24px; flex: none; border-radius: 50%;
  overflow: hidden; align-self: flex-end;
  display: grid; place-items: center;
  background: linear-gradient(150deg, #2b2f52 0%, #14162b 100%);
  border: 1px solid rgba(255,255,255,.14);
  margin-bottom: 1px;
}
.lnew-chatdemo .lnew-chatdemo__rowava img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lnew-chatdemo__rowava .lnew-chatdemo__ava-txt { font-size: 10px; }
.lnew-chatdemo__rowava--ghost { visibility: hidden; }

/* ---- bubbles ---- */
.lnew-chatdemo__bub {
  max-width: 78%;
  padding: 9px 13px;
  border-radius: var(--lncd-bub-r);
  font-size: 15px; font-weight: 400; line-height: 1.36; letter-spacing: -.005em;
  white-space: pre-wrap;      /* the address block is authored with newlines */
  overflow-wrap: anywhere;
}
.lnew-chatdemo__row--in  .lnew-chatdemo__bub { background: var(--lncd-bub-in);  color: var(--lncd-bub-in-ink); }
.lnew-chatdemo__row--out .lnew-chatdemo__bub { background: var(--lncd-bub-out); color: var(--lncd-bub-out-ink); }

/* Run shaping: the corner facing the run tightens, so three bubbles from one
   speaker read as one block. Messenger's exact geometry. */
.lnew-chatdemo__row--in.is-runmid   .lnew-chatdemo__bub,
.lnew-chatdemo__row--in.is-runend   .lnew-chatdemo__bub { border-top-left-radius: var(--lncd-bub-r-tight); }
.lnew-chatdemo__row--in.is-runstart .lnew-chatdemo__bub,
.lnew-chatdemo__row--in.is-runmid   .lnew-chatdemo__bub { border-bottom-left-radius: var(--lncd-bub-r-tight); }

.lnew-chatdemo__row--out.is-runmid   .lnew-chatdemo__bub,
.lnew-chatdemo__row--out.is-runend   .lnew-chatdemo__bub { border-top-right-radius: var(--lncd-bub-r-tight); }
.lnew-chatdemo__row--out.is-runstart .lnew-chatdemo__bub,
.lnew-chatdemo__row--out.is-runmid   .lnew-chatdemo__bub { border-bottom-right-radius: var(--lncd-bub-r-tight); }

/* ---- image bubbles ---- */
.lnew-chatdemo__img {
  border-radius: var(--lncd-bub-r);
  overflow: hidden;
  width: 132px;
  /* the supplied product shots are 716x1074 — native 2:3. A 3:4 box would
     centre-crop 11% off each end, and on these the bottom 11% is the base of
     the bag, not background. */
  aspect-ratio: 2 / 3;
  background: #16161f;
}
.lnew-chatdemo .lnew-chatdemo__img img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* the 2-up group the AI sends when it offers variants */
.lnew-chatdemo__imgs { display: flex; gap: 4px; max-width: 78%; }
.lnew-chatdemo__imgs .lnew-chatdemo__img { width: 118px; }

/* A story/image reply carries a hairline on the side it was sent from —
   Messenger's own "this is a reply to media" affordance. */
.lnew-chatdemo__row--out .lnew-chatdemo__img { position: relative; }
.lnew-chatdemo__row--out.is-reply .lnew-chatdemo__img {
  margin-right: 7px;
  box-shadow: 7px 0 0 -3px rgba(255,255,255,.28);
}

/* Placeholder for a row whose photo has not been supplied yet. Deliberately
   legible rather than invisible: a missing product shot should be obvious in
   the editor, not silently render as an empty grey box on the live page. */
.lnew-chatdemo__ph {
  width: 100%; height: 100%;
  display: grid; place-items: center; gap: 6px;
  background:
    linear-gradient(135deg, rgba(139,124,255,.16), rgba(86,212,255,.10)),
    repeating-linear-gradient(45deg, rgba(255,255,255,.04) 0 8px, transparent 8px 16px);
  color: rgba(255,255,255,.55);
}
.lnew-chatdemo__ph svg { width: 26px; height: 26px; }
.lnew-chatdemo__ph span { font-size: 9.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }

/* ---- typing pill ---- */
.lnew-chatdemo__typing { display: flex; align-items: flex-end; gap: 6px; margin-top: 10px; }
.lnew-chatdemo__typing[hidden] { display: none; }
.lnew-chatdemo__dots {
  display: flex; align-items: center; gap: 4px;
  padding: 12px 14px;
  background: var(--lncd-bub-in);
  border-radius: var(--lncd-bub-r);
}
.lnew-chatdemo__dots i {
  width: 7px; height: 7px; border-radius: 50%;
  background: rgba(255,255,255,.62);
  animation: lncd-typing 1.25s ease-in-out infinite;
}
.lnew-chatdemo__dots i:nth-child(2) { animation-delay: .16s; }
.lnew-chatdemo__dots i:nth-child(3) { animation-delay: .32s; }
@keyframes lncd-typing {
  0%, 60%, 100% { opacity: .35; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-4px); }
}

/* =====================================================================
   The landing animation.

   Rows are painted at rest in their FINAL state and animated by classes the
   script adds, so nothing depends on the script to be VISIBLE — if JS never
   runs, [hidden] is simply never removed and the thread shows whatever the
   server marked visible. That contract is unchanged.

   TWO CLASSES, NOT ONE — this is the fix for a measured flash.
   The first cut un-hid the row and added .is-landing two frames later. In
   between, the row painted at its FINAL state. Measured in Chrome against
   this very stylesheet: opacity was 1 on the unhide tick and 1 for both
   following frames, then snapped to 0 when .is-landing landed and animated
   up again. Every message in the thread flashed fully-opaque and restarted.
   So the script now adds .is-arming in the SAME tick as hidden=false — the
   row's first paint is already the pre-state — and swaps to .is-landing on
   the next frame. Never reintroduce a gap between the unhide and the
   pre-state.

   ONE KEYFRAME, TOKENS FOR THE REST — this is the fix for a dead rule.
   There used to be lncd-land-out / lncd-land-in overriding animation-name
   at (0,2,0), while the base rule set the `animation` SHORTHAND at (0,3,0).
   The shorthand carries animation-name, so it won regardless of source
   order and BOTH directional keyframes were dead CSS — verified: computed
   animation-name read `lncd-land` on incoming and outgoing alike. Rather
   than fight that with more specificity, direction is now a custom property
   the single keyframe reads, which cannot collide by construction.
   ===================================================================== */
.lnew-chatdemo__row {
  /* Landing defaults; overridden per row kind below. */
  --lncd-land-dur: 400ms;
  --lncd-land-x: 0px;
  --lncd-land-y: 10px;
  /* 1 = no scale. Text is deliberately NOT scaled: interpolating a 15px
     antialiased glyph run for 400ms resamples it every frame and reads as a
     soft, slightly blurry bubble. Photographs resample cleanly, so they keep
     a little scale and are the only thing that does. */
  --lncd-land-s: 1;
}
.lnew-chatdemo__row--out { --lncd-land-x:  6px; }
.lnew-chatdemo__row--in  { --lncd-land-x: -6px; }

/* A timestamp is a label, not a message: no mass, no direction, quicker. */
.lnew-chatdemo__row--time,
.lnew-chatdemo__row--note { --lncd-land-dur: 300ms; --lncd-land-x: 0px; --lncd-land-y: 4px; }

/* Photos are the heavy objects in the thread and the only scaled ones. */
.lnew-chatdemo__row:has(> .lnew-chatdemo__img),
.lnew-chatdemo__row:has(> .lnew-chatdemo__imgs) { --lncd-land-dur: 460ms; --lncd-land-s: .965; }

/* The pre-state. Applied in the same tick as the un-hide. */
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__bub,
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__img,
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__imgs,
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__note,
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__time,
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__rowava {
  opacity: 0;
  transform: translate3d(var(--lncd-land-x), var(--lncd-land-y), 0) scale(var(--lncd-land-s));
}
.lnew-chatdemo__row.is-arming > .lnew-chatdemo__rowava { transform: none; }

.lnew-chatdemo__row.is-landing > .lnew-chatdemo__bub,
.lnew-chatdemo__row.is-landing > .lnew-chatdemo__img,
.lnew-chatdemo__row.is-landing > .lnew-chatdemo__imgs,
.lnew-chatdemo__row.is-landing > .lnew-chatdemo__note,
.lnew-chatdemo__row.is-landing > .lnew-chatdemo__time {
  animation: lncd-land var(--lncd-land-dur) var(--lncd-pop) both;
  will-change: transform, opacity;
}

@keyframes lncd-land {
  from { opacity: 0; transform: translate3d(var(--lncd-land-x), var(--lncd-land-y), 0) scale(var(--lncd-land-s)); }
  to   { opacity: 1; transform: none; }
}

/* The avatar fades without travelling — it belongs to the run, not the
   bubble, and sliding it looks like a second message arriving. */
.lnew-chatdemo__row.is-landing > .lnew-chatdemo__rowava { animation: lncd-fade var(--lncd-land-dur) var(--lncd-ease) both; }
@keyframes lncd-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---- the typing pill's exit ----
   It used to be snapped away with [hidden] in the same statement pair that
   landed the bubble, so one frame removed ~41px of pill and inserted a
   bubble — two layout mutations the eye reads as a jolt. Now it dissolves
   first, and the time is paid OUT of the think delay rather than added to
   it, so the overall pace is unchanged. Exits accelerate (ease-in); only
   arrivals decelerate. */
.lnew-chatdemo__typing.is-leaving > * {
  animation: lncd-pill-out var(--lncd-pill-out) var(--lncd-ease-exit) both;
}
@keyframes lncd-pill-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(0, 3px, 0) scale(.94); }
}

/* The loop's reset: the whole feed dips out, is re-hidden, and comes back. */
.lnew-chatdemo__feed { transition: opacity .5s var(--lncd-ease); }
.lnew-chatdemo--rewind .lnew-chatdemo__feed { opacity: 0; }

/* =====================================================================
   Composer.
   ===================================================================== */
.lnew-chatdemo__composer {
  flex: none;
  display: grid;
  grid-template-columns: repeat(4, auto) minmax(0, 1fr) auto;
  align-items: center;
  gap: 13px;
  padding: 9px 14px 20px;
  color: var(--lncd-ui-blue);
  background: var(--lncd-screen-bg);
}
.lnew-chatdemo__composer svg { width: 21px; height: 21px; }

.lnew-chatdemo__field {
  display: flex; align-items: center; gap: 6px;
  min-width: 0;
  height: 32px; padding: 0 9px 0 12px;
  border-radius: 100px;
  background: #303030;
}
.lnew-chatdemo__field-t {
  flex: 1 1 auto; min-width: 0;
  font-size: 14px; font-weight: 500; color: var(--lncd-ui-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The caret is the one thing in the composer that moves. It is what keeps a
   paused thread from reading as a screenshot. */
.lnew-chatdemo__caret {
  width: 1.5px; height: 15px; flex: none;
  background: var(--lncd-ui-blue);
  animation: lncd-caret 1.1s steps(1) infinite;
}
@keyframes lncd-caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
.lnew-chatdemo__field svg { width: 18px; height: 18px; flex: none; }

/* home indicator */
.lnew-chatdemo__home {
  position: absolute; z-index: 5;
  left: 50%; translate: -50% 0; bottom: 8px;
  width: 124px; height: 4px; border-radius: 100px;
  background: rgba(255,255,255,.34);
  pointer-events: none;
}

/* =====================================================================
   Responsive.

   The rail drops below the phone at 1040 (where the hero's own layout
   changes), and the phone shrinks in two steps after that. Font sizes
   INSIDE the screen never scale — a Messenger bubble at 12px stops being a
   believable Messenger bubble, and legibility is the whole point of
   choosing the customer's phone over the operator's dashboard.
   ===================================================================== */
@media (max-width: 1040px) {
  .lnew-chatdemo__inner {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 44px;
  }
  /* Phone first: it is the proof, the rail is the caption. */
  .lnew-chatdemo__stage { order: -1; }
  /* Below the breakpoint the rail sits UNDER the phone as a grid, so the
     column-stretching above has nothing to match and is reset. */
  .lnew-chatdemo__inner { align-items: center; }
  .lnew-chatdemo__beats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    justify-content: start;
    gap: 8px;
    width: 100%;
    max-width: 720px;
  }
  .lnew-chatdemo__beat { padding: 14px 16px; }
}

@media (max-width: 700px) {
  .lnew-chatdemo { --lncd-phone-w: 320px; --lncd-screen-h: 580px; }
  .lnew-chatdemo__head { margin-bottom: 34px; }
  .lnew-chatdemo__lead { font-size: 16px; }
  .lnew-chatdemo__beats { grid-template-columns: minmax(0, 1fr); max-width: 460px; }
  .lnew-chatdemo__beat { padding: 12px 14px; }
}

@media (max-width: 420px) {
  .lnew-chatdemo {
    --lncd-phone-w: min(300px, 100%);
    --lncd-screen-h: 548px;
    --lncd-phone-r: 46px;
    --lncd-screen-r: 36px;
    --lncd-bezel: 9px;
  }
  .lnew-chatdemo__intro { padding: 12px 14px 18px; }
  .lnew-chatdemo__intro-ava { width: 64px; height: 64px; }
  .lnew-chatdemo__intro-name { font-size: 19px; }
  .lnew-chatdemo__img { width: 118px; }
  .lnew-chatdemo__imgs .lnew-chatdemo__img { width: 104px; }
  /* the drawn side buttons sit outside the frame and would be the only
     thing causing a horizontal scrollbar at the very smallest widths */
  .lnew-chatdemo__phone::before,
  .lnew-chatdemo__phone::after { display: none; }
}

/* =====================================================================
   Reduced motion — the finished thread, held. Every row is already in the
   DOM, so honouring the preference costs nothing but turning the timers
   off (the script reads the same query and skips straight to the end).
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  .lnew-chatdemo__eyebrow-dot { animation: none; opacity: 1; }
  .lnew-chatdemo__dots i,
  .lnew-chatdemo__caret { animation: none; }
  .lnew-chatdemo__caret { opacity: 1; }
  /* Every class the player can add must be neutralised here, not just
     is-landing — is-arming sets opacity:0, so leaving it live would hide
     rows outright for anyone who asked for less motion. */
  .lnew-chatdemo__row.is-landing > *,
  .lnew-chatdemo__typing.is-leaving > * { animation: none !important; }
  .lnew-chatdemo__row.is-arming > * { opacity: 1 !important; transform: none !important; }
  .lnew-chatdemo__row.is-landing > * { will-change: auto; }
  .lnew-chatdemo__feed { transition: none; }
  .lnew-chatdemo__thread { scroll-behavior: auto; }
}
