/* =====================================================================
   Nevo Money — the Assistant demo, rebuilt for the web
   ---------------------------------------------------------------------
   Every measurement here is lifted from the shipping Flutter widgets so
   the demo on the website and the demo inside the app are the same
   object. Where a number looks arbitrary it is because it is the app's
   number; the comment says which widget it came from.

   Sources: assistant_message_bubble.dart, assistant_typing_indicator.dart,
   action_proposal_card.dart, assistant_composer.dart,
   upcoming_payments_sheet_widgets.dart, cash_flow_chart.dart,
   and demo/assistant_demo_player.dart (the script itself).
   ===================================================================== */

.nvx-chat {
  --nvx-c-ink: var(--nvx-ink);
  --nvx-c-dim: var(--nvx-ink-dim);
  /* insurance.auto resolves to CategoryColorToken.violet in the taxonomy. */
  --nvx-cat: #b388ff;

  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  /* The app's chat sits on the scaffold gradient, not on a card. */
  color: var(--nvx-c-ink);
  font-size: 14px;
  line-height: 1.43;
  letter-spacing: 0.25px;
}
.light-mode .nvx-chat,
.theme-light .nvx-chat { --nvx-cat: #7c4dff; }

/* --- header: mirrors NevoAppBar's title + subtitle ------------------- */
.nvx-chat-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 42px 16px 10px;
}
.nvx-chat-bar img { width: 26px; height: 26px; border-radius: 26.8%; flex: none; }
.nvx-chat-bar b { display: block; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.nvx-chat-bar span { display: block; font-size: 11px; color: var(--nvx-c-dim); letter-spacing: 0.2px; }

/* --- the thread ------------------------------------------------------ */
/* ListView padding is fromLTRB(16, 12, 16, 12).
   The column is anchored to the BOTTOM of the clipped box, so appending a
   turn grows the conversation upward and the newest message is always the
   one you are looking at. No scroll maths, and nothing to get out of sync
   with the streaming text. */
.nvx-chat-thread {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
.nvx-chat-scroll {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 12px;
  display: flex;
  flex-direction: column;
}

/* Each turn element eases in the way MountReveal does: fade + 10px rise,
   380ms, Flutter's easeOutCubic. */
.nvx-msg,
.nvx-att,
.nvx-think {
  animation: nvx-mount 380ms cubic-bezier(0.215, 0.61, 0.355, 1) both;
}
@keyframes nvx-mount {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* --- user bubble ----------------------------------------------------- */
/* primary @ .12 fill; radius 16/16/4/16; padding 14x10; margin-start 48. */
.nvx-msg-user {
  align-self: flex-end;
  max-width: calc(100% - 48px);
  margin: 4px 0;
  padding: 10px 14px;
  border-radius: 16px 16px 4px 16px;
  background: color-mix(in srgb, var(--nvx-primary) 12%, transparent);
  color: var(--nvx-c-ink);
}

/* --- assistant turn: NOT a bubble, bare text ------------------------- */
.nvx-msg-nevo {
  align-self: flex-start;
  max-width: calc(100% - 40px);
  margin: 4px 0;
  line-height: 1.4;
  color: var(--nvx-c-ink);
}
.nvx-msg-nevo p { margin: 0; font-size: inherit; line-height: inherit; color: inherit; letter-spacing: inherit; }
.nvx-msg-nevo p + p { margin-top: 16px; }
/* gpt_markdown renders **bold** as w700 with no colour change. */
.nvx-msg-nevo strong { font-weight: 700; color: inherit; }

/* The streaming softener: the app ShaderMasks the last ~28px down to 35%
   opacity while text is still arriving. */
.nvx-msg-nevo.is-streaming {
  mask-image: linear-gradient(180deg, #000 0, #000 calc(100% - 28px), rgba(0, 0, 0, 0.35) 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0, #000 calc(100% - 28px), rgba(0, 0, 0, 0.35) 100%);
}
/* The caret only exists mid-stream. */
.nvx-caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.15em;
  background: var(--nvx-primary);
  animation: nvx-blink 1s steps(1) infinite;
}
@keyframes nvx-blink { 50% { opacity: 0; } }

/* --- "Thinking..." --------------------------------------------------- */
/* 44px paint field, an 18.92px app-icon tile centred, a comet orbiting. */
.nvx-think {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  align-self: flex-start;
}
.nvx-think-glyph { position: relative; width: 44px; height: 44px; flex: none; }
.nvx-think-glyph canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.nvx-think-glyph img {
  position: absolute;
  top: 50%; left: 50%;
  width: 19px; height: 19px;
  margin: -9.5px 0 0 -9.5px;
  border-radius: 28%; /* the app's own tile ratio for this glyph */
}
.nvx-think span { font-size: 12px; letter-spacing: 0.4px; color: var(--nvx-c-dim); }

/* =====================================================================
   Entity cards in the thread
   ---------------------------------------------------------------------
   The demo wraps its attachments in a neutral bordered surface so they
   read as first-class objects rather than embedded chat text.
   ===================================================================== */

.nvx-att { margin: 6px 0; }
.nvx-ent {
  border-radius: 16px;
  border: 1px solid color-mix(in srgb, var(--nvx-outline) 50%, transparent);
  background: color-mix(in srgb, var(--nvx-surface) 88%, transparent);
  overflow: hidden;
}

/* --- UpcomingPaymentTile --------------------------------------------- */
.nvx-sp {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr) auto;
  gap: 12px;
  align-items: start;
  padding: 10px 12px;
}
.nvx-sp-av {
  width: 42px; height: 42px;
  border-radius: 14px; /* size / 3 — the _CategoryAvatar rule */
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--nvx-cat) 18%, transparent);
  border: 1px solid var(--nvx-cat);
  color: var(--nvx-cat);
}
.nvx-sp-name { font-size: 14px; font-weight: 600; letter-spacing: 0.25px; color: var(--nvx-c-ink); }
.nvx-sp-sub {
  margin-top: 2px;
  font-size: 12px;
  letter-spacing: 0.4px;
  color: color-mix(in srgb, var(--nvx-c-ink) 70%, transparent);
}
.nvx-sp-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.nvx-sp-pill {
  padding: 4px 10px;
  border-radius: 10px;
  background: var(--nvx-surface-hi);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: color-mix(in srgb, var(--nvx-c-ink) 80%, transparent);
}
/* "Tomorrow" is the one due-chip with its own palette in the app. */
.nvx-sp-pill.is-soon {
  color: var(--nvx-amber);
  background: color-mix(in srgb, var(--nvx-amber) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--nvx-amber) 50%, transparent);
}
.nvx-sp-amt {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.1px;
  color: var(--nvx-red);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- forecast card ---------------------------------------------------- */
.nvx-fc { padding: 10px 12px 6px; }
.nvx-fc-head { display: flex; align-items: flex-start; gap: 8px; }
.nvx-fc-head svg { color: var(--nvx-primary); flex: none; margin-top: 2px; }
/* The app allows the title two lines before it ellipsises. One line plus a
   right-hand "Low of ..." does not fit a phone width, and a clipped title
   is the one thing on this card that must stay readable. */
.nvx-fc-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.1px;
  color: var(--nvx-c-ink);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.nvx-fc-low {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0.4px;
  color: var(--nvx-c-dim);
  text-align: end;
}
.nvx-fc-low b { font-weight: 700; color: var(--nvx-c-ink); }
.nvx-fc-low.is-unsafe b { color: var(--nvx-red); }
.nvx-fc-chart { margin-top: 6px; height: 180px; }
.nvx-fc-chart svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* Chart ink. The balance line carries a glow at 50% of its own colour,
   blur 8 — suppressed under reduced motion along with everything else. */
.nvx-ch-line { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.nvx-ch-line.is-pos { stroke: var(--nvx-green); filter: drop-shadow(0 0 8px color-mix(in srgb, var(--nvx-green) 50%, transparent)); }
.nvx-ch-line.is-neg { stroke: var(--nvx-red); filter: drop-shadow(0 0 8px color-mix(in srgb, var(--nvx-red) 50%, transparent)); }
.nvx-ch-grid { stroke: color-mix(in srgb, var(--nvx-ink) 5%, transparent); stroke-width: 1; stroke-dasharray: 4 4; }
.nvx-ch-zero { stroke: color-mix(in srgb, var(--nvx-ink) 20%, transparent); stroke-width: 1.5; }
.nvx-ch-today { stroke: color-mix(in srgb, var(--nvx-primary) 30%, transparent); stroke-width: 2; stroke-dasharray: 2 2; }
.nvx-ch-todaylbl { fill: var(--nvx-primary); font-size: 9px; font-weight: 700; letter-spacing: 1px; }
/* 72% ink at 10px, up from 40% at 9px: measured 2.39:1 in light and 3.54:1
   in dark, and these are the only marks that turn the curve into money. */
.nvx-ch-ylbl { fill: color-mix(in srgb, var(--nvx-ink) 72%, transparent); font-size: 10px; font-weight: 600; text-anchor: end; }
.nvx-ch-xlbl { fill: color-mix(in srgb, var(--nvx-ink) 72%, transparent); font-size: 10px; font-weight: 700; letter-spacing: 0.6px; }

/* The line draws itself in when the card lands. */
.nvx-ch-line { stroke-dasharray: var(--nvx-len); stroke-dashoffset: var(--nvx-len); animation: nvx-draw 900ms cubic-bezier(0.215, 0.61, 0.355, 1) 120ms forwards; }
.nvx-ch-line.is-pos, .nvx-ch-line.is-neg { stroke-dasharray: var(--nvx-len); }
@keyframes nvx-draw { to { stroke-dashoffset: 0; } }
.nvx-ch-area { animation: nvx-fade 700ms cubic-bezier(0.215, 0.61, 0.355, 1) 420ms both; }
@keyframes nvx-fade { from { opacity: 0; } to { opacity: 1; } }

/* =====================================================================
   Proposal card — the beat the whole demo exists for
   ===================================================================== */

.nvx-prop {
  border-radius: 16px;
  border: 1px solid color-mix(in srgb, var(--nvx-outline) 50%, transparent);
  background: color-mix(in srgb, var(--nvx-surface) 88%, transparent);
  overflow: hidden;
}
.nvx-prop-div {
  height: 1px;
  background: color-mix(in srgb, var(--nvx-outline) 40%, transparent);
}
.nvx-prop-body { padding: 12px 14px 14px; }
.nvx-prop-change {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.25px;
  color: var(--nvx-c-ink);
}
.nvx-prop-change svg { color: var(--nvx-primary); flex: none; }

.nvx-prop-actions { display: flex; gap: 10px; margin-top: 14px; }
.nvx-prop-btn {
  flex: 1 1 0;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.1px;
  border: 1px solid transparent;
  cursor: default;
}
/* Dismiss: surfaceContainerHighest @ .72 with the 3-tone ramp. */
.nvx-prop-btn.is-dismiss {
  color: var(--nvx-c-ink);
  background: color-mix(in srgb, var(--nvx-surface-hi) 72%, transparent);
  border-color: color-mix(in srgb, var(--nvx-outline) 50%, transparent);
}
/* Approve: primaryButton.fill, brand navy, top-to-bottom ramp. */
.nvx-prop-btn.is-approve {
  color: #d8e6ff;
  background: linear-gradient(180deg, #18479b, #11336f 50%, #0a1f43);
  border-color: color-mix(in srgb, var(--nvx-primary) 18%, transparent);
}
.light-mode .nvx-prop-btn.is-approve,
.theme-light .nvx-prop-btn.is-approve {
  color: #fff;
  background: linear-gradient(180deg, #1d4396, #14306b 50%, #0c1d40);
}
/* The approve tap: a quick press-in, because a demo that resolves with no
   physical beat reads as a screenshot rather than a tap. */
.nvx-prop-btn.is-pressed { transform: scale(0.97); transition: transform 140ms cubic-bezier(0.215, 0.61, 0.355, 1); }

/* Resolved state: the button row is replaced by the Done pill + Undo. */
.nvx-prop-done { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.nvx-prop-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border-radius: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  /* "Done" is a STATUS, not an amount, so it takes the brand green rather
     than the money green the figures use. */
  color: var(--nvx-secondary);
  background: color-mix(in srgb, var(--nvx-secondary) 12%, transparent);
}
.nvx-prop-donetext { font-size: 14px; font-weight: 600; letter-spacing: 0.25px; color: var(--nvx-c-ink); }

/* =====================================================================
   Composer
   ===================================================================== */

.nvx-comp {
  flex: none;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 12px 14px;
}
.nvx-comp-field {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  display: flex;
  align-items: center;
  padding: 12px 18px;
  border-radius: 24px;
  background: color-mix(in srgb, var(--nvx-c-ink) 6%, transparent);
  font-size: 14px;
  line-height: 1.43;
  color: var(--nvx-c-ink);
  overflow: hidden;
}
.nvx-comp-field.is-hint { color: var(--nvx-c-dim); }
.nvx-comp-btn {
  flex: none;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--nvx-c-dim);
}
.nvx-comp-send {
  flex: none;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  border: 1px solid color-mix(in srgb, var(--nvx-primary) 18%, transparent);
  background: linear-gradient(180deg, #18479b, #11336f 50%, #0a1f43);
  color: #d8e6ff;
  transition: opacity var(--nvx-dur-med) var(--nvx-ease-emph), filter var(--nvx-dur-med) var(--nvx-ease-emph);
}
.light-mode .nvx-comp-send,
.theme-light .nvx-comp-send { background: linear-gradient(180deg, #1d4396, #14306b 50%, #0c1d40); color: #fff; }
/* Disabled = fill drops to surfaceContainerHighest at alpha .30. */
.nvx-comp-send.is-off {
  background: color-mix(in srgb, var(--nvx-surface-hi) 30%, transparent);
  border-color: color-mix(in srgb, var(--nvx-outline) 60%, transparent);
  color: color-mix(in srgb, var(--nvx-primary) 30%, transparent);
}

/* =====================================================================
   Demo chrome — replay + progress, which belong to the website only
   ===================================================================== */

/* Sits ABOVE the composer, and the thread reserves room for it, so it can
   never cover the last line of the reply. That line is the compliance
   disclaimer ("you'll still need to move the payment with your bank"), and
   the settled frame is what every late arrival reads. */
.nvx-chat:has(.nvx-demo-ctl.is-on) .nvx-chat-scroll { padding-bottom: 52px; }
.nvx-demo-ctl {
  position: absolute;
  left: 50%;
  bottom: 76px;
  transform: translateX(-50%);
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--nvx-primary) 35%, transparent);
  background: color-mix(in srgb, var(--nvx-primary) 12%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--nvx-primary);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  /* visibility, not just opacity. At opacity:0 alone the button stayed in
     the tab order: a keyboard user hit an invisible control (zero changed
     pixels on focus, WCAG 2.4.7) and Enter fired it. Under reduced motion
     it never gains .is-on at all, so that was permanent, not transient.
     visibility:hidden removes it from the tab order and still transitions. */
  visibility: hidden;
  transition:
    opacity var(--nvx-dur-long) var(--nvx-ease-emph),
    visibility 0s linear var(--nvx-dur-long);
}
.nvx-demo-ctl.is-on {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition:
    opacity var(--nvx-dur-long) var(--nvx-ease-emph),
    visibility 0s linear 0s;
}
.nvx-demo-ctl:focus-visible {
  outline: 2px solid var(--nvx-primary);
  outline-offset: 3px;
}
.nvx-demo-ctl:hover { background: color-mix(in srgb, var(--nvx-primary) 20%, transparent); }

/* =====================================================================
   The "Newborn Star" cinematic
   ---------------------------------------------------------------------
   Plays inside the phone the first time the assistant section comes into
   view, then hands the screen to the demo — the same arc the app runs on
   first open of the feature. Beat timings live in js/nevo-cinematic.js;
   this file owns only how each beat LOOKS.

   Type sizes follow the app's `s = clamp(shortestSide / 390, 0.82, 1.7)`
   scale, resolved against the phone frame's own width via container-ish
   clamps so the lockup keeps its proportions at every breakpoint.
   ===================================================================== */

.nvx-cine {
  position: absolute;
  inset: 0;
  z-index: 7;
  overflow: hidden;
  border-radius: inherit;
  opacity: 1;
  transition: opacity 620ms cubic-bezier(0.42, 0, 0.58, 1);
}
.nvx-cine.is-out { opacity: 0; }
.nvx-cine.is-gone { display: none; }

/* Opaque from frame 0, OUTSIDE the stage fade, so the navy never flashes
   through to the chat behind it. */
.nvx-cine-bg {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, #0a1128, #050810);
}

.nvx-cine-stage {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 600ms cubic-bezier(0, 0, 0.58, 1);
}
.nvx-cine.is-up .nvx-cine-stage { opacity: 1; }

/* --- the pre-reveal copy -------------------------------------------- */
/* Envelope: 800ms in easeOut, hold, 600ms out easeIn, each line rising
   14 * s px as it arrives. */
.nvx-cine-line,
.nvx-cine-kicker,
.nvx-cine-tag {
  position: absolute;
  left: 8%;
  right: 8%;
  margin: 0;
  text-align: center;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 600ms cubic-bezier(0.42, 0, 1, 1), transform 600ms cubic-bezier(0.42, 0, 1, 1);
  pointer-events: none;
}
.nvx-cine-line.is-in,
.nvx-cine-kicker.is-in,
.nvx-cine-tag.is-in {
  opacity: 1;
  transform: none;
  transition-duration: 800ms;
  transition-timing-function: cubic-bezier(0, 0, 0.58, 1);
}

.nvx-cine-line {
  top: 40%;
  font-size: clamp(20px, 8.4vw, 33px);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.5px;
  color: #edf3fa;
}
.nvx-cine-kicker {
  top: 45%;
  font-size: clamp(11px, 4vw, 16px);
  font-weight: 600;
  letter-spacing: 2.4px;
  text-transform: uppercase;
  color: rgba(100, 175, 255, 0.92);
}

/* --- the star ------------------------------------------------------- */
.nvx-cine-star {
  position: absolute;
  left: 50%;
  top: 4%;
  width: 88%;
  aspect-ratio: 1 / 1.15;
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity 400ms cubic-bezier(0, 0, 0.58, 1);
}
.nvx-cine-star.is-in { opacity: 1; }
/* The canvas BLEEDS past this box so the nebula has room to fall off
   rather than hitting an edge. js/nevo-star.js owns those insets:
   it sets them inline, and its scene scale and centre anchor are
   derived to match. Do not add overflow:hidden to the host. */
/* The engine reveals the mark from the ignition beat; no float loop here,
   because the wordmark below is what the eye should travel to next. */
.nvx-cine-star .nvx-star-mark {
  width: clamp(72px, 26vw, 112px);
  height: clamp(72px, 26vw, 112px);
  /* radius inherits the 26.8% cinematic ratio from nevo2.css */
  animation: none;
  box-shadow: 0 0 66px 2px rgba(100, 175, 255, 0.35);
}

/* --- the "Nevo 2.0" lockup ------------------------------------------ */
.nvx-cine-lockup {
  position: absolute;
  left: 0;
  right: 0;
  top: 58%;
  display: flex;
  align-items: baseline;
  justify-content: center;
  /* The app inserts an explicit 0.26em box between the words rather than
     a space, so the numerals have somewhere to land. */
  gap: 0.26em;
  font-size: clamp(30px, 13vw, 52px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -1.2px;
  color: #edf3fa;
}
.nvx-cine-word,
.nvx-cine-two { display: inline-block; opacity: 0; }

/* "Nevo" arrives on the gentle spring, overshooting ~2%. */
.nvx-cine-word.is-in {
  opacity: 1;
  animation: nvx-cine-word 800ms var(--nvx-spring, cubic-bezier(0.34, 1.56, 0.64, 1)) both;
}
@keyframes nvx-cine-word { from { transform: scale(0.88); } to { transform: scale(1); } }

/* THE SLAM. easeInCubic is the whole trick: the numerals barely move, then
   cover almost all the distance in the last few frames, so they land
   instead of arriving. */
.nvx-cine-two.is-in {
  opacity: 1;
  color: #64afff;
  animation:
    nvx-cine-slam 340ms cubic-bezier(0.55, 0.055, 0.675, 0.19) both,
    nvx-cine-flare 900ms cubic-bezier(0.215, 0.61, 0.355, 1) 340ms both;
}
@keyframes nvx-cine-slam { from { transform: scale(3.4); opacity: 0; } 12% { opacity: 1; } to { transform: scale(1); opacity: 1; } }
@keyframes nvx-cine-flare {
  from { text-shadow: 0 0 82px rgba(100, 175, 255, 0.95); }
  to { text-shadow: 0 0 30px rgba(100, 175, 255, 0.35); }
}

/* The light sweep, once, across the settled lockup — then the mask is
   dropped entirely so there is no shader cost at rest. */
.nvx-cine-lockup.is-swept {
  animation: nvx-cine-sweep 1200ms cubic-bezier(0.42, 0, 0.58, 1) 1 both;
}
@keyframes nvx-cine-sweep {
  from {
    -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 100%);
    mask-image: linear-gradient(90deg, #000 0%, #000 100%);
    filter: brightness(1);
  }
  35% { filter: brightness(1.5); }
  to { filter: brightness(1); }
}

/* --- impact --------------------------------------------------------- */
.nvx-cine-flash {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% 57%, rgba(255, 255, 255, 0.55) 0%, rgba(100, 175, 255, 0.22) 35%, rgba(100, 175, 255, 0) 100%);
}
.nvx-cine-flash.is-on { animation: nvx-cine-flash 560ms cubic-bezier(0.42, 0, 1, 1) both; }
@keyframes nvx-cine-flash { 0% { opacity: 0; } 14% { opacity: 1; } 100% { opacity: 0; } }

/* Two shockwave rings, the second lagging 16%. Ellipses at 0.42 height,
   anchored at 57% — the app's numbers, and the flattening is what keeps
   them reading as ground-level rather than bubbles. */
.nvx-cine-rings { position: absolute; inset: 0; pointer-events: none; }
.nvx-cine-rings i {
  position: absolute;
  left: 50%;
  top: 57%;
  width: 20%;
  height: 8.4%;
  border-radius: 50%;
  border: 3px solid rgba(100, 175, 255, 0.5);
  transform: translate(-50%, -50%) scale(0.2);
  opacity: 0;
}
.nvx-cine-rings.is-on i:nth-child(1) { animation: nvx-cine-ring 900ms cubic-bezier(0.215, 0.61, 0.355, 1) both; }
.nvx-cine-rings.is-on i:nth-child(2) { animation: nvx-cine-ring 900ms cubic-bezier(0.215, 0.61, 0.355, 1) 140ms both; }
@keyframes nvx-cine-ring {
  from { transform: translate(-50%, -50%) scale(0.2); opacity: 0.5; border-width: 3.4px; }
  to { transform: translate(-50%, -50%) scale(5.6); opacity: 0; border-width: 1px; }
}

/* The shake is applied to the WHOLE stage, not the wordmark: the app
   shakes the scene, which is why the hit feels like it landed on the
   screen rather than inside it. ~3.5 damped cycles. */
.nvx-cine-stage.is-shaking { animation: nvx-cine-shake 620ms cubic-bezier(0, 0, 0.58, 1) both; }
@keyframes nvx-cine-shake {
  0% { transform: translateY(0); }
  14% { transform: translateY(-9px); }
  30% { transform: translateY(6px); }
  46% { transform: translateY(-4px); }
  62% { transform: translateY(2.4px); }
  80% { transform: translateY(-1px); }
  100% { transform: translateY(0); }
}

.nvx-cine-tag {
  top: 72%;
  font-size: clamp(13px, 4.4vw, 17px);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0.1px;
  color: rgba(237, 243, 250, 0.76);
}

/* TOP-right, not bottom-right. At the bottom it landed under the cookie
   banner on every phone size tested: elementFromPoint at the button's own
   centre returned the banner, so the tap was swallowed rather than merely
   obscured, and there was no way out of the 9.4s intro on a phone. It was
   also 53x34 — below any touch target minimum — and sitting in the bezel
   where it read as part of the app rather than as page chrome. */
.nvx-cine-skip {
  position: absolute;
  right: 12px;
  top: 12px;
  z-index: 5;
  min-height: 44px;
  min-width: 44px;
  padding: 11px 16px;
  border-radius: 999px;
  border: 1px solid rgba(237, 243, 250, 0.22);
  background: rgba(10, 17, 40, 0.55);
  color: rgba(237, 243, 250, 0.72);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  opacity: 0;
  transition: opacity 300ms cubic-bezier(0.215, 0.61, 0.355, 1), color 200ms ease;
}
.nvx-cine.is-playing .nvx-cine-skip { opacity: 1; }
.nvx-cine-skip:hover { color: #edf3fa; border-color: rgba(237, 243, 250, 0.4); }

/* Replaying the intro is a deliberate act, so it gets a quiet control
   under the phone rather than a second button competing with the demo's. */
.nvx-replay-intro {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 18px auto 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--nvx-ink-dim);
  font: inherit;
  font-size: 0.86rem;
  cursor: pointer;
  transition: color var(--nvx-dur-med) var(--nvx-ease-emph);
}
.nvx-replay-intro:hover { color: var(--nvx-primary); }

@media (prefers-reduced-motion: reduce) {
  .nvx-cine { display: none !important; }
  .nvx-replay-intro { display: none !important; }
  .nvx-msg, .nvx-att, .nvx-think, .nvx-ch-area { animation: none !important; }
  .nvx-ch-line { animation: none !important; stroke-dashoffset: 0 !important; }
  .nvx-caret { display: none; }
  .nvx-msg-nevo.is-streaming { mask-image: none; -webkit-mask-image: none; }
  .nvx-ch-line.is-pos, .nvx-ch-line.is-neg { filter: none; }
  .nvx-demo-ctl { backdrop-filter: none; -webkit-backdrop-filter: none; }
}
