
/* Vy drawer — the one Vy surface (SON-4012). Inlined by tools/build-vy-drawer.py; edit here.

   TOKENS ONLY, NO FALLBACKS. The site is a dark theme (--bg #080A0C, --text #F4F5F2, --mineral a
   light cream). An earlier version invented token names with LIGHT fallbacks and produced dark-on-
   dark bubbles and a white composer. A fallback that is wrong for the only theme the site has hides
   breakage behind a plausible value; if a token is missing the page is already broken, let it show.

   This is a Sonsoto surface built from Sonsoto tokens. It deliberately borrows no geometry, colour,
   wording or iconography from any third-party assistant UI. */

.vyd-scrim {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(0, 0, 0, .5);
  opacity: 0; pointer-events: none;
  transition: opacity .18s ease;
}
.vyd-scrim[data-open] { opacity: 1; pointer-events: auto; }

.vyd {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 201;
  width: min(440px, 100vw);
  display: flex; flex-direction: column; min-height: 0;
  background: var(--bg-2);
  border-left: 1px solid var(--line);
  transform: translateX(100%);
  transition: transform .22s ease;
  /* The drawer is its own scroll context: the page behind it must not move when the thread does. */
  overscroll-behavior: contain;
}
.vyd[data-open] { transform: none; }

/* Opening heartbeat (SON-5277) ---------------------------------------------
   A WebGL canvas that swells behind the drawer for ~5.5 s when it opens, then is hidden; the
   conversation sits on the ordinary flat ground. All timing, pause and fallback rules are in
   vy-drawer.js above ambient(). CSS only places the canvas behind the content. */
.vyd[data-ambient] { overflow: hidden; isolation: isolate; }   /* own stacking context: canvas above the
                                                                  drawer's ground, below its content */
.vyd-amb {
  position: absolute; inset: 0; z-index: -1;
  display: block; width: 100%; height: 100%;
  pointer-events: none;                                         /* never takes a click, a scroll or focus */
}
.vyd-amb[hidden] { display: none; }
/* Opening content after the heartbeat (SON-5340). Held while the beat plays, then each item rises into
   place in order: --i is its position (header 0, welcome 1, chips 2…). Rhythm knobs: the 150ms step between
   items and the 620ms arrival; the curve settles without overshoot. vy-drawer.js owns data-intro. */
.vyd[data-intro="wait"] .vyd-intro { opacity: 0; transform: translateY(12px); pointer-events: none; }
/* The close button stays operable while held: nobody is ever kept in the drawer (Escape and the scrim
   close it too). */
.vyd[data-intro="wait"] .vyd-x.vyd-intro { pointer-events: auto; }
.vyd[data-intro="play"] .vyd-intro {
  animation: vyd-intro-in 620ms cubic-bezier(.22, .8, .24, 1) both;
  animation-delay: calc(var(--i, 0) * 150ms);
}
@keyframes vyd-intro-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .vyd[data-intro] .vyd-intro { animation: none; opacity: 1; transform: none; pointer-events: auto; }
}
/* The composer band keeps its solid ground: the waves never sit under what the visitor is typing. */
.vyd[data-ambient] .vyd-foot { position: relative; }
.vyd[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) {
  .vyd, .vyd-scrim { transition: none; }
}

/* Type scale (2026-09-15 visual refinement, owner brief: mobile-first, calmer, more premium):
   title 17 · subtitle 13.5 · context line 13 · intro 15 · message + input 16 (line-height 1.5) · chips 14.5 · status 13.
   Every value is a token or a plain px on this scale; nothing sits below 13px. */

/* Header ------------------------------------------------------------------ */
.vyd-head {
  flex: none;
  display: flex; align-items: flex-start; gap: 14px;
  padding: 20px 20px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.vyd-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; padding-top: 2px; }
/* SON-6049: Vy's face. With it, the name centres on the 36px circle instead of hanging at the top. */
.vyd-avatar {
  flex: none; width: 36px; height: 36px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--line-soft); margin-top: -4px;
}
.vyd-avatar + .vyd-id { align-self: center; padding-top: 0; margin-top: -4px; }
.vyd-name { font-size: 17px; font-weight: 600; letter-spacing: -.01em; color: var(--text); line-height: 1.25; }
.vyd-role { font-size: 13.5px; color: var(--muted); line-height: 1.4; }
/* The page the visitor came from, so the conversation is visibly about something. */
.vyd-ctx {
  margin-top: 6px; font-size: 13px; color: var(--faint); line-height: 1.4;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vyd-x {
  flex: none; display: grid; place-items: center;
  width: 40px; height: 40px; padding: 0; margin: -6px -8px 0 0;
  border: 0; border-radius: 10px; cursor: pointer;
  background: none; color: var(--muted);
}
.vyd-x svg { width: 18px; height: 18px; }
.vyd-x:hover { color: var(--text); background: var(--control-surface); }
.vyd-x:focus-visible, .vyd-chip:focus-visible, .vyd-send:focus-visible, .vyd-retry:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}

/* Thread ------------------------------------------------------------------ */
.vyd-log {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; gap: 12px;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 20px 20px 22px;
}
/* The intro is supporting copy: one step below the messages, in the muted ink. */
.vyd-welcome { font-size: 15px; line-height: 1.55; color: var(--muted); margin: 0; max-width: 40ch; }
/* SON-5228 (Spec 0077 WP 4.3): a thread recalled from an earlier visit, with its way out */
.vyd-recall { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin: 10px 0 0; font-size: 14px; color: var(--muted); }
.vyd-recall-lead { margin: 0; }
.vyd-recall-fresh { background: none; border: 0; padding: 0; font: inherit; color: var(--text); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.vyd-suggest { display: flex; flex-direction: column; gap: 8px; margin: 6px 0 4px; }
.vyd-chip {
  text-align: left; width: 100%;
  padding: 12px 14px;
  font: inherit; font-size: 14.5px; line-height: 1.4;
  color: var(--text);
  background: var(--control-surface);
  border: 1px solid var(--line); border-radius: 12px;
  cursor: pointer;
}
.vyd-chip:hover { border-color: var(--line-strong); }

.vyd-turn { display: flex; flex-wrap: wrap; }   /* wraps so a reaction row sits under its bubble (SON-7475) */
.vyd-turn + .vyd-turn { margin-top: 2px; }
.vyd-turn[data-role="user"] { justify-content: flex-end; }
.vyd-bubble {
  max-width: 92%;
  padding: 12px 16px; border-radius: 16px;
  font-size: 16px; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* Vy's turn: a quiet card one step lighter than the drawer, no shadow, a short tail. */
.vyd-turn[data-role="assistant"] .vyd-bubble {
  background: var(--surface-2); border: 1px solid var(--line-soft);
  color: var(--text); border-bottom-left-radius: 5px;
}
/* The visitor's turn carries the page's primary voice (the .btn-primary pairing): the mineral tone with the page
   ground as its ink, so it inverts correctly in both themes — cream with dark ink on the dark theme, the olive with
   light ink on the light theme (the light theme used to set dark ink on the olive: muddy and under contrast). */
.vyd-turn[data-role="user"] .vyd-bubble {
  max-width: 84%;
  background: var(--mineral); color: var(--bg); border-bottom-right-radius: 5px;
}
:root[data-theme="light"] .vyd-turn[data-role="user"] .vyd-bubble { background: var(--mineral-hi); }
/* A failure is a statement about the request, not a message from Vy — it must not look like one. */
.vyd-turn[data-error] .vyd-bubble {
  max-width: 100%; background: transparent;
  border: 1px solid var(--line-strong); color: var(--muted); border-radius: 12px; font-size: 15px;
}
.vyd-retry {
  margin-top: 8px; display: block;
  font: inherit; font-size: 14px; font-weight: 600;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--text); text-decoration: underline; text-underline-offset: 3px;
}
.vyd-bubble[data-streaming]::after {
  content: ""; display: inline-block;
  width: 6px; height: .95em; margin-left: 2px; vertical-align: -2px;
  background: currentColor; opacity: .45;
  animation: vyd-blink 1s steps(2, start) infinite;
}
@keyframes vyd-blink { to { visibility: hidden; } }
@media (prefers-reduced-motion: reduce) { .vyd-bubble[data-streaming]::after { animation: none; } }
/* Vy is thinking (SON-5281) — the turn is sent and no reply text has arrived: four dots that orbit
   and squeeze, in place of a cursor blinking in an empty bubble. One pseudo-element draws all four
   (a dot plus three box-shadows), so there is no extra DOM and nothing for a screen reader to read;
   the "thinking" status line stays the accessible signal. The first streamed character fills the
   bubble, :empty stops matching, and the ordinary cursor takes over.
   Knobs: --vyd-think-size (dot), --vyd-think-gap (orbit radius), --vyd-think-speed (one turn). */
.vyd-bubble[data-streaming]:empty {
  --vyd-think-size: 5px; --vyd-think-gap: 5px; --vyd-think-speed: 1.4s;
  display: grid; place-items: center;
  min-width: 52px; min-height: 44px;
}
.vyd-bubble[data-streaming]:empty::after {
  width: var(--vyd-think-size); height: var(--vyd-think-size); margin: 0;
  border-radius: 50%; vertical-align: 0;
  background: transparent; opacity: 1; color: var(--mineral);
  box-shadow:
    calc(var(--vyd-think-gap) * -1) calc(var(--vyd-think-gap) * -1) 0 0 currentColor,
    var(--vyd-think-gap) calc(var(--vyd-think-gap) * -1) 0 0 currentColor,
    var(--vyd-think-gap) var(--vyd-think-gap) 0 0 currentColor,
    calc(var(--vyd-think-gap) * -1) var(--vyd-think-gap) 0 0 currentColor;
  animation: vyd-think var(--vyd-think-speed) cubic-bezier(.45, 0, .55, 1) infinite;
}
@keyframes vyd-think {
  0%   { rotate: 0deg;   scale: 1; }
  25%  { rotate: 70deg;  scale: .72; }   /* the four pull in as they turn… */
  50%  { rotate: 180deg; scale: 1.08; }  /* …and open out again */
  75%  { rotate: 250deg; scale: .8; }
  100% { rotate: 360deg; scale: 1; }
}
@media (prefers-reduced-motion: reduce) { .vyd-bubble[data-streaming]:empty::after { animation: none; } }
/* owner 2026-10-07 — the "thinking" words sit in the log, not under the composer; owner 2026-10-10 (SON-6049) — to
   the RIGHT of the dots, on the same line. The label follows the bubble in the DOM (so the sibling selector can see
   the bubble is still empty); the first streamed character ends :empty and the label goes with the dots. */
.vyd-turn[data-live] { align-items: center; }
.vyd-thinking { display: none; margin: 0 0 0 12px; font-size: 13px; line-height: 1.4; color: var(--muted); }
.vyd-bubble[data-streaming]:empty + .vyd-thinking { display: block; }
/* SON-6564 — the platform tenant's chosen thinking mark (SON-5430), when one is saved. A PORT of the block
   headed "SON-5430 — Vy's thinking mark" in apps/web/src/app/globals.css: same rules, same keyframes, same
   reduced-motion block, with two deliberate differences and no others —
     · every name carries the drawer's prefix (vy-mark -> vyd-mark, vy-grain -> vyd-grain, --vy-mark-* ->
       --vyd-mark-*): `.vy-mark` is already taken on this site by the root hero's ring-and-core mark
       (_shared/vy-hero.css, SON-5437), and this file is inlined on the same pages;
     · colour is this site's own token, var(--mineral), where the app reads --color-mineral.
   packages/shared/src/__tests__/site-vy-mark-parity.test.ts fails if anything else diverges. With NO saved
   mark the drawer never emits these classes and the SON-5281 .vyd-think mark above is what renders.
   The bubble holding a mark carries [data-marked] instead of being :empty, so the cursor and the old dots
   stay off and the "thinking" label still sits beside it. */
.vyd-bubble[data-streaming][data-marked] { display: grid; place-items: center; min-width: 52px; min-height: 44px; }
.vyd-bubble[data-streaming][data-marked]::after { content: none; animation: none; }
.vyd-bubble[data-streaming][data-marked] + .vyd-thinking { display: block; }
.vyd-mark {
  --vyd-mark-dot: calc(var(--vyd-mark-size, 16px) * 0.26);
  --vyd-mark-travel: calc(var(--vyd-mark-size, 16px) - var(--vyd-mark-dot));
  --vyd-mark-q: calc(var(--vyd-mark-speed, 2s) / 4);
  --vyd-mark-stagger: 70ms;
  --vyd-mark-ease: cubic-bezier(0.65, 0, 0.35, 1);
  position: relative;
  display: block;
  width: var(--vyd-mark-size, 16px);
  height: var(--vyd-mark-size, 16px);
  color: var(--mineral);
}
.vyd-mark__ring {
  position: absolute;
  inset: 0;
}
.vyd-mark__dot {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--vyd-mark-dot);
  height: var(--vyd-mark-dot);
  border-radius: 50%;
  background: currentColor;
}
/* Lead + trail. --phase puts each dot one corner behind the one ahead; the still corner (--x/--y)
   is where it rests in reduced motion and in orbit/tumble. */
.vyd-mark__dot:nth-child(1) {
  --i: 0;
  --phase: 3;
  --x: 0px;
  --y: var(--vyd-mark-travel);
  opacity: 1;
}
.vyd-mark__dot:nth-child(2) {
  --i: 1;
  --phase: 2;
  --x: var(--vyd-mark-travel);
  --y: var(--vyd-mark-travel);
  opacity: 0.72;
}
.vyd-mark__dot:nth-child(3) {
  --i: 2;
  --phase: 1;
  --x: var(--vyd-mark-travel);
  --y: 0px;
  opacity: 0.46;
}
.vyd-mark__dot:nth-child(4) {
  --i: 0;
  --phase: 0;
  --x: 0px;
  --y: 0px;
  opacity: 0.28;
}

/* relay (default) — the lead moves, the trail follows a beat apart, then all four rest. */
.vyd-mark[data-motion='relay'] .vyd-mark__dot,
.vyd-mark[data-motion='step'] .vyd-mark__dot {
  animation: vyd-mark-relay var(--vyd-mark-speed, 2s) var(--vyd-mark-ease) infinite;
  animation-delay: calc(var(--vyd-mark-q) * var(--phase) * -1 + var(--vyd-mark-stagger) * var(--i));
}
@keyframes vyd-mark-relay {
  0%,
  10% {
    transform: translate(0, 0);
  }
  25%,
  35% {
    transform: translate(var(--vyd-mark-travel), 0);
  }
  50%,
  60% {
    transform: translate(var(--vyd-mark-travel), var(--vyd-mark-travel));
  }
  75%,
  85% {
    transform: translate(0, var(--vyd-mark-travel));
  }
  100% {
    transform: translate(0, 0);
  }
}

/* step — squarer: all four together, longer rests, firmer easing, rounded-square dots. */
.vyd-mark[data-motion='step'] {
  --vyd-mark-stagger: 0ms;
  --vyd-mark-ease: cubic-bezier(0.8, 0, 0.2, 1);
}
.vyd-mark[data-motion='step'] .vyd-mark__dot {
  border-radius: 28%;
  animation-name: vyd-mark-step;
}
@keyframes vyd-mark-step {
  0%,
  16% {
    transform: translate(0, 0);
  }
  25%,
  41% {
    transform: translate(var(--vyd-mark-travel), 0);
  }
  50%,
  66% {
    transform: translate(var(--vyd-mark-travel), var(--vyd-mark-travel));
  }
  75%,
  91% {
    transform: translate(0, var(--vyd-mark-travel));
  }
  100% {
    transform: translate(0, 0);
  }
}

/* orbit — the dots hold their corners; the square turns a quarter at a time and draws in mid-turn. */
.vyd-mark[data-motion='orbit'] .vyd-mark__dot,
.vyd-mark[data-motion='tumble'] .vyd-mark__dot {
  transform: translate(var(--x), var(--y));
}
.vyd-mark[data-motion='orbit'] .vyd-mark__ring {
  animation: vyd-mark-orbit var(--vyd-mark-speed, 2s) cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@keyframes vyd-mark-orbit {
  0% {
    transform: rotate(0deg) scale(1);
  }
  12.5% {
    transform: rotate(45deg) scale(0.84);
  }
  25% {
    transform: rotate(90deg) scale(1);
  }
  37.5% {
    transform: rotate(135deg) scale(0.84);
  }
  50% {
    transform: rotate(180deg) scale(1);
  }
  62.5% {
    transform: rotate(225deg) scale(0.84);
  }
  75% {
    transform: rotate(270deg) scale(1);
  }
  87.5% {
    transform: rotate(315deg) scale(0.84);
  }
  100% {
    transform: rotate(360deg) scale(1);
  }
}

/* tumble — the square spins through X, Y and Z in perspective. Each dot runs the exact inverse
   rotation, in reverse order, on the same clock, so it always faces the viewer (round, never a
   disc seen edge-on). 1.5× the lap: three axes at 2s reads as frantic. */
.vyd-mark[data-motion='tumble'] {
  perspective: calc(var(--vyd-mark-size, 16px) * 4);
}
.vyd-mark[data-motion='tumble'] .vyd-mark__ring {
  transform-style: preserve-3d;
  animation: vyd-mark-tumble calc(var(--vyd-mark-speed, 2s) * 1.5) linear infinite;
}
.vyd-mark[data-motion='tumble'] .vyd-mark__dot {
  animation: vyd-mark-tumble-dot calc(var(--vyd-mark-speed, 2s) * 1.5) linear infinite;
}
@keyframes vyd-mark-tumble {
  from {
    transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg);
  }
  to {
    transform: rotateX(360deg) rotateY(360deg) rotateZ(360deg);
  }
}
@keyframes vyd-mark-tumble-dot {
  from {
    transform: translate(var(--x), var(--y)) rotateZ(0deg) rotateY(0deg) rotateX(0deg);
  }
  to {
    transform: translate(var(--x), var(--y)) rotateZ(-360deg) rotateY(-360deg) rotateX(-360deg);
  }
}

/* The grain mark (canvas, the drawer's grain painter): its grains paint in `currentColor`, the same token as
   the dots. It handles reduced motion itself — the engine draws the formed shape once and stops. */
.vyd-grain {
  display: block;
  color: var(--mineral);
}

/* `[data-motion]` matches the specificity of every motion rule above, and this block comes last,
   so it wins for all four — a plain `.vyd-mark .vyd-mark__dot` would lose to them and keep moving. */
/* SON-6562 — a still mark (the status row's resting state). Mirrors the app globals.css SON-5430 still block. */
.vyd-mark[data-motion][data-still] .vyd-mark__ring,
.vyd-mark[data-motion][data-still] .vyd-mark__dot {
  animation: none;
}
.vyd-mark[data-motion][data-still] .vyd-mark__ring {
  transform: none;
}
.vyd-mark[data-motion][data-still] .vyd-mark__dot {
  transform: translate(var(--x), var(--y));
}

@media (prefers-reduced-motion: reduce) {
  .vyd-mark[data-motion] .vyd-mark__ring,
  .vyd-mark[data-motion] .vyd-mark__dot {
    animation: none;
  }
  .vyd-mark[data-motion] .vyd-mark__ring {
    transform: none;
  }
  .vyd-mark[data-motion] .vyd-mark__dot {
    transform: translate(var(--x), var(--y));
  }
}

/* Composer ---------------------------------------------------------------- */
.vyd-foot {
  flex: none;
  padding: 14px 20px calc(16px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line-soft);
  background: var(--bg-2);
}
.vyd-form { display: flex; align-items: flex-end; gap: 10px; }
.vyd-in {
  flex: 1; min-width: 0; resize: none; max-height: 132px; min-height: 48px;
  font: inherit; font-size: 16px; line-height: 1.45;
  padding: 12px 16px;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--control-surface); color: var(--text);
  overflow-y: hidden;   /* the script lifts this once the field reaches its ceiling */
}
/* The placeholder is built from the page title and can run long: one line, cut with an ellipsis, never a wrapped
   second line hiding under the field's edge. */
.vyd-in::placeholder { color: var(--muted); opacity: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vyd-in:focus { outline: none; border-color: var(--line-strong); }
.vyd-in:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }   /* the site's focus ring token; --mineral-30 exists only in the light theme */
.vyd-send {
  flex: none; display: grid; place-items: center;
  width: 48px; height: 48px; padding: 0;
  border: 0; border-radius: 14px; cursor: pointer;
  background: var(--mineral); color: var(--bg);
}
.vyd-send svg { width: 20px; height: 20px; }
.vyd-send:disabled { opacity: .4; cursor: default; }
/* Voice (SON-5151): the same footprint as Send, outlined so the primary action stays Send. Its
   phase is on data-phase; listening breathes so a visitor can see the microphone is open. */
.vyd-mic {
  flex: none; display: grid; place-items: center;
  width: 48px; height: 48px; padding: 0;
  border: 1px solid var(--line); border-radius: 14px; cursor: pointer;
  background: var(--control-surface); color: var(--text);
}
.vyd-mic[hidden] { display: none; }
.vyd-mic svg { width: 20px; height: 20px; }
.vyd-mic[aria-pressed="true"] { background: var(--mineral); color: var(--bg); border-color: transparent; }
.vyd-mic[data-phase="listening"] { animation: vyd-breathe 1.6s ease-in-out infinite; }
.vyd-mic:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-mic[disabled] { opacity: .45; cursor: not-allowed; animation: none; }   /* SON-5461: voice unavailable here — typing still works */
/* SON-7471 — the emoji shortcut: the microphone's footprint, borderless so Send and voice keep the weight. The
   panel opens upward over the log, the app picker's 6×2 grid. A phone's own keyboard is the picker there. */
.vyd-emoji { position: relative; flex: none; }
.vyd-emoji-btn {
  display: grid; place-items: center; width: 48px; height: 48px; padding: 0;
  border: 1px solid transparent; border-radius: 14px; cursor: pointer;
  background: transparent; color: var(--muted);
}
.vyd-emoji-btn:hover, .vyd-emoji-btn[aria-expanded="true"] { color: var(--text); background: var(--control-surface); }
.vyd-emoji-btn svg { width: 20px; height: 20px; }
.vyd-emoji-btn:focus-visible, .vyd-emoji-opt:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-emoji-pop {
  position: absolute; bottom: calc(100% + 8px); right: 0; z-index: 2;
  display: grid; grid-template-columns: repeat(6, 36px); gap: 2px; padding: 6px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--bg-2);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
}
.vyd-emoji-pop[hidden] { display: none; }
.vyd-emoji-opt {
  display: grid; place-items: center; width: 36px; height: 36px; padding: 0;
  border: 0; border-radius: 8px; background: transparent; cursor: pointer;
  font-size: 18px; line-height: 1;
}
.vyd-emoji-opt:hover { background: var(--control-surface); }
@media (max-width: 640px) and (pointer: coarse) { .vyd-emoji { display: none; } }
/* Rating (SON-5156): a quiet row above the composer, never a modal. */
.vyd-rate { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 0 0 10px; font-size: 13px; line-height: 1.4; color: var(--text); }
.vyd-rate[hidden] { display: none; }
.vyd-rate-q { color: var(--muted); }
.vyd-rate-opt { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border: 1px solid var(--line); border-radius: 10px; background: var(--control-surface); color: var(--text); font: inherit; cursor: pointer; }
.vyd-rate-opt:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-rate-acts { display: inline-flex; align-items: center; gap: 2px; }
/* SON-6294 — like / dislike / dismiss as borderless glyphs, so the bar is one quiet line. */
.vyd-rate-icon { width: 30px; height: 30px; padding: 0; justify-content: center; border-color: transparent; background: transparent; color: var(--muted); }
.vyd-rate-icon:hover { color: var(--text); background: var(--control-surface); }
.vyd-rate-icon svg { width: 16px; height: 16px; }
.vyd-rate-in { flex: 1 1 100%; min-height: 44px; resize: vertical; font: inherit; font-size: 16px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--control-surface); color: var(--text); }
@keyframes vyd-breathe { 0%, 100% { box-shadow: 0 0 0 0 transparent; } 50% { box-shadow: 0 0 0 6px var(--line); } }
@media (prefers-reduced-motion: reduce) { .vyd-mic[data-phase="listening"] { animation: none; } }
.vyd-status { margin: 8px 0 0; font-size: 13px; line-height: 1.4; color: var(--muted); min-height: 1.4em; }
.vyd-status[data-over] { color: var(--mineral-hi); font-weight: 600; }
.vyd-status[data-thinking]:not([data-over]) { color: transparent; user-select: none; }   /* owner 2026-10-07: read by screen readers, shown in the log */

/* Phone — a near-full-height sheet. The composer must clear the virtual keyboard, so the drawer is
   sized against the small viewport unit where it is supported (100vh is the WRONG unit here: it
   measures the viewport WITHOUT the keyboard and pushes the composer under it). */
@media (max-width: 640px) {
  .vyd {
    width: 100vw; top: auto; height: 92vh; border-left: 0;
    border-top: 1px solid var(--line); border-radius: 18px 18px 0 0;
    transform: translateY(100%);
  }
  .vyd[data-open] { transform: none; }
}
@supports (height: 100svh) {
  @media (max-width: 640px) { .vyd { height: 92svh; } }
}

/* Guided fallback card (owner brief, 2026-09-16) — a failed turn is an advisory next step, not a system error ---- */
.vyd-turn[data-error] .vyd-bubble { border: 1px solid var(--line); background: var(--surface-tinted); }
.vyd-fail-lead { margin: 8px 0 0; font-size: 14px; line-height: 1.5; color: var(--muted); }
.vyd-fail-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.vyd-fail-acts .vyd-retry, .vyd-fail-act {
  display: inline-flex; align-items: center; min-height: 40px; padding: 0 14px;
  font: inherit; font-size: 14px; font-weight: 600; line-height: 1; text-decoration: none; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: 999px; background: var(--control-surface); color: var(--text);
}
.vyd-fail-acts .vyd-retry { background: var(--mineral); color: var(--bg); border-color: transparent; }
.vyd-fail-act:hover, .vyd-fail-acts .vyd-retry:hover { border-color: var(--text); }
.vyd-fail-act:focus-visible, .vyd-fail-acts .vyd-retry:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-suggest--after { margin-top: 16px; }
/* consent ask (SON-4840): rendered in the log before the first turn; mirrors the failure card's controls */
.vyd-ask { margin: 10px 0 0; padding: 12px 14px; border: 1px solid var(--line-strong); border-radius: 14px; background: var(--control-surface); }
.vyd-ask-lead { margin: 0; font-size: 14px; line-height: 1.45; color: var(--text); }
.vyd-ask-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.vyd-ask-allow, .vyd-ask-act {
  display: inline-flex; align-items: center; min-height: 40px; padding: 0 14px;
  font: inherit; font-size: 14px; font-weight: 600; line-height: 1; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: 999px; background: var(--control-surface); color: var(--text);
}
.vyd-ask-allow { background: var(--mineral); color: var(--bg); border-color: transparent; }
.vyd-ask-allow:hover, .vyd-ask-act:hover { border-color: var(--text); }
.vyd-ask-allow:focus-visible, .vyd-ask-act:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-perms { align-self: flex-start; margin: 2px 0 0; padding: 4px 0; border: 0; background: none; font: inherit; font-size: 12.5px; color: var(--muted); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }   /* SON-5516; SON-5646: the always-visible link to the privacy choices */
.vyd-perms[hidden] { display: none; }
.vyd-perms:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-ask-act--link { display: inline-flex; align-items: center; text-decoration: underline; text-underline-offset: 2px; }
.vyd-suggest-lead { flex-basis: 100%; margin: 0 0 4px; font-size: 13px; color: var(--muted); }

/* Phone composer (owner brief, 2026-09-16): more room for the field, a lighter send control, nothing clipped ---- */
@media (max-width: 640px) {
  .vyd-foot { padding: 10px 12px calc(12px + env(safe-area-inset-bottom)); }
  .vyd-form { gap: 8px; }
  .vyd-in { min-height: 44px; padding: 10px 14px; font-size: 16px; }
  .vyd-send, .vyd-mic { width: 44px; height: 44px; border-radius: 12px; }
  .vyd-mic svg { width: 18px; height: 18px; }
  .vyd-send svg { width: 18px; height: 18px; }
  .vyd-status { font-size: 12.5px; }
}

/* ─── Vy voice orb + voice panel (SON-5907) ───────────────────────────────────
   PORTED FROM apps/web/src/app/globals.css §6b, and the values below are the
   SAME FACTS as there, not an interpretation of them: the four colour tokens,
   the keyframes and the per-state tuning are copied verbatim. The site is static
   inlined JS and cannot import the app's stylesheet, so the copy is unavoidable
   — what IS avoidable is the copy drifting silently, which is how the public
   site ended up a whole voice generation behind. `site-voice-parity.test.ts` in
   packages/shared reds when these stop matching the app's source.

   The blue is a scoped exception to Variant C, exactly as it is in the app: the
   four --vy-orb-* tokens exist for this object only and nothing else may read
   them. Alan asked for blue with stronger motion against a reference (2026-10-06).

   HOW IT IS BUILT. Four overlapping blobs, not one circle. Each morphs its own
   border-radius on its own period and spins in its own direction, so the
   silhouette never repeats exactly and the lobes drift across one another; that
   interference is what reads as a wave rather than a spinner. Only transform,
   opacity and border-radius animate, so the compositor owns every frame, and the
   single blur sits on a static layer that is scaled, never re-blurred. */

:root {
  --color-vy-orb-rim: #6fb4ff;
  --color-vy-orb-mid: #2f6fe0;
  --color-vy-orb-deep: #16307a;
  --color-vy-orb-glow: #3d7dff;
}

@keyframes vyOrbDrift {
  0%   { border-radius: 62% 38% 44% 56% / 54% 46% 54% 46%; transform: rotate(0deg) scale(1); }
  25%  { border-radius: 38% 62% 68% 32% / 38% 66% 34% 62%; transform: rotate(90deg) scale(1.06); }
  50%  { border-radius: 70% 30% 34% 66% / 64% 36% 64% 36%; transform: rotate(180deg) scale(0.95); }
  75%  { border-radius: 34% 66% 58% 42% / 44% 58% 42% 56%; transform: rotate(270deg) scale(1.05); }
  100% { border-radius: 62% 38% 44% 56% / 54% 46% 54% 46%; transform: rotate(360deg) scale(1); }
}

/* The counter-rotating twin. A second blob spinning the SAME way reads as one rigid object
   turning; opposing directions are what make the surface look like it flows over itself. */
@keyframes vyOrbDriftReverse {
  0%   { border-radius: 45% 55% 62% 38% / 60% 40% 60% 40%; transform: rotate(0deg) scale(1.02); }
  33%  { border-radius: 68% 32% 36% 64% / 35% 65% 35% 65%; transform: rotate(-120deg) scale(0.94); }
  66%  { border-radius: 32% 68% 64% 36% / 66% 34% 66% 34%; transform: rotate(-240deg) scale(1.07); }
  100% { border-radius: 45% 55% 62% 38% / 60% 40% 60% 40%; transform: rotate(-360deg) scale(1.02); }
}

@keyframes vyOrbBreathe {
  0%, 100% { transform: scale(calc(1 + var(--vy-orb-lift) - var(--vy-orb-amp))); }
  50%      { transform: scale(calc(1 + var(--vy-orb-lift) + var(--vy-orb-amp))); }
}

@keyframes vyOrbGlowPulse {
  0%, 100% { opacity: calc(var(--vy-orb-glow) * 0.6); transform: scale(0.94); }
  50%      { opacity: var(--vy-orb-glow); transform: scale(1.1); }
}

.vy-orb {
  --vy-orb-spin: 14s;
  --vy-orb-breathe: 5s;
  --vy-orb-amp: 0.05;
  --vy-orb-glow: 0.42;
  --vy-orb-level: 0;
  --vy-orb-lift: calc(var(--vy-orb-level) * 0.12);
  position: relative;
  display: grid;
  place-items: center;
  width: var(--vy-orb-size, 8.5rem);
  height: var(--vy-orb-size, 8.5rem);
  isolation: isolate;                  /* the glow's blur must not bleed into the drawer */
}
.vy-orb-glow {
  position: absolute;
  inset: -28%;
  border-radius: 9999px;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in oklab, var(--color-vy-orb-glow) 85%, transparent) 0%,
    color-mix(in oklab, var(--color-vy-orb-mid) 45%, transparent) 42%, transparent 72%);
  filter: blur(22px);
  opacity: var(--vy-orb-glow);
  animation: vyOrbGlowPulse var(--vy-orb-breathe) ease-in-out infinite;
}
.vy-orb-body { position: absolute; inset: 0; animation: vyOrbBreathe var(--vy-orb-breathe) ease-in-out infinite; }
.vy-orb-shell {
  position: absolute;
  inset: 0;
  animation: vyOrbDrift var(--vy-orb-spin) ease-in-out infinite;
  mix-blend-mode: screen;              /* overlapping lobes BRIGHTEN where they cross, not muddy */
}
.vy-orb-shell-a {
  background: radial-gradient(circle at 38% 30%,
    color-mix(in oklab, var(--color-vy-orb-rim) 95%, transparent) 0%,
    color-mix(in oklab, var(--color-vy-orb-mid) 55%, transparent) 46%, transparent 74%);
}
.vy-orb-shell-b {
  animation-name: vyOrbDriftReverse;
  animation-duration: calc(var(--vy-orb-spin) * 1.35);
  background: radial-gradient(circle at 68% 64%,
    color-mix(in oklab, var(--color-vy-orb-mid) 90%, transparent) 0%,
    color-mix(in oklab, var(--color-vy-orb-deep) 60%, transparent) 52%, transparent 76%);
}
.vy-orb-shell-c {
  animation-duration: calc(var(--vy-orb-spin) * 0.72);
  animation-direction: reverse;
  background: radial-gradient(circle at 50% 76%,
    color-mix(in oklab, var(--color-vy-orb-rim) 70%, transparent) 0%,
    color-mix(in oklab, var(--color-vy-orb-mid) 30%, transparent) 44%, transparent 70%);
  opacity: 0.8;
}
/* The fourth lobe exists for the CROSSING, not for its own shape: a prime-ish period against the
   other three so the pattern takes a long time to repeat. */
.vy-orb-shell-d {
  animation-name: vyOrbDriftReverse;
  animation-duration: calc(var(--vy-orb-spin) * 1.9);
  background: radial-gradient(circle at 24% 66%,
    color-mix(in oklab, var(--color-vy-orb-rim) 60%, transparent) 0%, transparent 58%);
  opacity: 0.65;
}
/* The darker centre. Without it the lobes sum to a flat bright disc; the hollow is what gives the
   thing depth and a readable silhouette. */
.vy-orb-core {
  position: absolute;
  inset: 30%;
  border-radius: 9999px;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in oklab, var(--bg) 94%, transparent) 0%,
    color-mix(in oklab, var(--color-vy-orb-deep) 40%, transparent) 62%, transparent 80%);
}

/* Per-state timing — the same four numbers per state the app sets. */
.vy-orb[data-state="idle"]         { --vy-orb-spin: 22s; --vy-orb-breathe: 7s;   --vy-orb-amp: 0.03;  --vy-orb-glow: 0.24; }
.vy-orb[data-state="listening"]    { --vy-orb-spin: 13s; --vy-orb-breathe: 3s;   --vy-orb-amp: 0.07;  --vy-orb-glow: 0.5; }
.vy-orb[data-state="transcribing"],
.vy-orb[data-state="thinking"]     { --vy-orb-spin: 7s;  --vy-orb-breathe: 2.2s; --vy-orb-amp: 0.05;  --vy-orb-glow: 0.4; }
.vy-orb[data-state="speaking"]     { --vy-orb-spin: 5s;  --vy-orb-breathe: 1.2s; --vy-orb-amp: 0.11;  --vy-orb-glow: 0.68; }
.vy-orb[data-state="paused"]       { --vy-orb-spin: 30s; --vy-orb-breathe: 9s;   --vy-orb-amp: 0.01;  --vy-orb-glow: 0.08; }
.vy-orb[data-state="error"]        { --vy-orb-spin: 30s; --vy-orb-breathe: 9s;   --vy-orb-amp: 0.012; --vy-orb-glow: 0.14; }

/* Reduce does NOT mean "show nothing". The orb is the only state display on this surface, so
   freezing every state into an identical disc would delete the information the words used to
   carry. Motion stops; the per-state amplitude and glow stay. */
@media (prefers-reduced-motion: reduce) {
  .vy-orb-glow, .vy-orb-body, .vy-orb-shell { animation: none; }
  .vy-orb-glow { opacity: var(--vy-orb-glow); transform: none; }
  .vy-orb-body { transform: scale(calc(1 + var(--vy-orb-amp) * 2)); }
}

/* The panel the orb sits in. It REPLACES the composer while a session runs, for the two reasons
   the app gives: the surface needs the width to carry the state, and a live text field under an
   open microphone invites typing a turn into a conversation already taking one. */
/* The state words live here and nowhere visible: the orb carries the state for anyone who can see
   it, and deleting the words outright would take the state away from anyone who cannot. */
.vyd-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.vyd-voice { display: none; flex-direction: column; align-items: center; gap: 14px; padding: 10px 0 2px; }
.vyd[data-voice] .vyd-voice { display: flex; }
.vyd[data-voice] .vyd-form { display: none; }
.vyd-voice .vy-orb { --vy-orb-size: 7rem; }
.vyd-voice-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
.vyd-voice-btn {
  width: 48px; height: 48px; border-radius: 9999px; border: 1px solid var(--line);
  background: none; color: var(--text); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.vyd-voice-btn:hover { border-color: var(--muted); }
.vyd-voice-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vyd-voice-btn svg { width: 20px; height: 20px; }
.vyd-voice-end { width: 40px; height: 40px; border-radius: 12px; color: var(--muted); }
.vyd-voice-end:hover { color: var(--text); }
@media (max-width: 640px) {
  .vyd-voice .vy-orb { --vy-orb-size: 6rem; }
}

/* The peek and the continuity badge (SON-6790) -------------------------------
   Bottom-right, just above the launcher (.chat-fab); vy-drawer.js measures the launcher and sets
   --vyd-peek-right/--vyd-peek-bottom, with a fallback for a page without one. Below the drawer's own
   layers (200/201) so an open drawer always covers it. Tokens only, as the drawer above. */
.vyd-peek {
  position: fixed; z-index: 190;
  right: var(--vyd-peek-right, 16px); bottom: var(--vyd-peek-bottom, 88px);
  display: flex; align-items: flex-start; gap: 4px;
  max-width: min(320px, calc(100vw - 32px));
  padding: 6px 6px 6px 14px;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: 14px; border-bottom-right-radius: 5px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .35);
  animation: vyd-peek-in .22s ease both;
}
.vyd-peek-q {
  flex: 1; min-width: 0; padding: 8px 0; margin: 0;
  background: none; border: 0; color: var(--text); text-align: left;
  font: inherit; font-size: 14.5px; line-height: 1.45; cursor: pointer;
}
.vyd-peek-q:hover { text-decoration: underline; text-underline-offset: 3px; }
.vyd-peek-x {
  flex: none; display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
  background: none; border: 0; border-radius: 8px; color: var(--muted); cursor: pointer;
}
.vyd-peek-x svg { width: 16px; height: 16px; }
.vyd-peek-x:hover { color: var(--text); background: var(--control-surface); }
.vyd-peek-q:focus-visible, .vyd-peek-x:focus-visible, .vyd-continue:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
.vyd-peek-mark { position: absolute; left: 0; width: 1px; height: 1px; pointer-events: none; visibility: hidden; }

.vyd-continue {
  position: fixed; z-index: 191;
  right: var(--vyd-badge-right, 12px); bottom: var(--vyd-badge-bottom, 70px);
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px 3px 7px;
  background: var(--mineral); color: var(--bg);
  border: 1px solid var(--line-strong); border-radius: 999px;
  font: inherit; font-size: 12px; font-weight: 600; line-height: 1.3; cursor: pointer;
  animation: vyd-peek-in .22s ease both;
}
.vyd-continue-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }

@keyframes vyd-peek-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .vyd-peek, .vyd-continue { animation: none; }
}

/* SON-7475 — the read-only reaction row under the visitor's own message (vy-drawer.js reactBar). SON-7571 took the
   control off Vy's replies, so every chip is a span. */
/* Its own line under the bubble (flex-basis 100% in the wrapping turn row), not squeezed beside it (SON-7552). */
.vyd-react { flex-basis: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px; }
.vyd-react--mine { justify-content: flex-end; }   /* under the visitor's own (right-hand) message */
.vyd-react-chip {
  display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 9px; border-radius: 999px;
  border: 1px solid var(--line); background: transparent; color: var(--text); font: inherit; font-size: 12px;
  line-height: 1; cursor: default;
}
    