/* Kiosk layout — tuned for a large portrait/landscape touchscreen. */

.stage {
  position: relative; z-index: 1; height: 100%; width: 100%; overflow: hidden;
  /* How far the virtual keyboard sits off the bottom edge. Declared here
     because the input bar has to stack on top of the keyboard and therefore
     needs the same number; two copies of it would drift apart. */
  --vkb-inset: clamp(12px, 2.2vh, 20px);
}

/* ---------- brand ---------- */
.brand {
  position: absolute; top: 42px; left: 38px; z-index: 6;
  display: flex; flex-direction: column; line-height: 1;
}
/* Width is matched to the tagline below it by fitLogo() in kiosk.js. */
.brand-logo { width: 172px; height: auto; display: block; opacity: 0.94; }
.brand-sub { margin-top: 10px; font-size: clamp(11px, 1.3vmin, 15px); letter-spacing: 4px; text-transform: uppercase; color: var(--accent-soft); white-space: nowrap; }

/* Back button — shown once she's engaged, sits where the logo was. */
.back-btn {
  position: absolute; top: 38px; left: 38px; z-index: 8;
  display: inline-flex; align-items: center; justify-content: center;
  width: clamp(46px, 6vmin, 64px); height: clamp(46px, 6vmin, 64px);
  border-radius: 50%; cursor: pointer;
  background: var(--glass); border: 1px solid var(--glass-brd); color: var(--lilac);
  backdrop-filter: blur(12px);
  opacity: 0; transform: translateX(-12px); pointer-events: none;
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease), background 0.3s var(--ease), color 0.3s var(--ease);
}
.back-btn svg { width: clamp(20px, 2.6vmin, 28px); height: clamp(20px, 2.6vmin, 28px); }
.back-btn:active { background: var(--accent); color: #2a1206; border-color: transparent; }
.stage.engaged .back-btn { opacity: 1; transform: none; pointer-events: auto; }
/* The brand steps aside for the back button whenever she's engaged. */
.stage.engaged .brand { opacity: 0; pointer-events: none; transition: opacity 0.4s var(--ease); }

/* ---------- character ---------- */
.character-zone {
  position: absolute; z-index: 2;
  left: 50%; bottom: 0;
  transform: translateX(-50%);
  height: 82vh; width: 46vh; max-width: 620px;
  display: flex; align-items: flex-end; justify-content: center;
  transition: left 1.05s var(--ease), transform 1.05s var(--ease),
              height 0.8s var(--ease), top 1.05s var(--ease), bottom 0.8s var(--ease);
  --speak: 0; --listen: 0;
}
.stage.engaged .character-zone {
  left: 23%;
  transform: translateX(-50%);
}
.character {
  position: relative; z-index: 3;
  height: 100%; aspect-ratio: 887 / 1774;
  filter: drop-shadow(0 30px 44px rgba(0,0,0,0.55));
  transform-origin: 50% 92%;
  animation: breathe 6.5s ease-in-out infinite;
  will-change: transform;
}
.stage.engaged .character { animation: breathe 5.5s ease-in-out infinite; }

/* Layered pose images crossfade between Klaire's expressions. */
.pose {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; object-position: bottom center;
  opacity: 0; transition: opacity 0.55s var(--ease); pointer-events: none;
}
.pose-idle { opacity: 1; }
.stage[data-state="thinking"] .pose-idle,
.stage[data-state="speaking"] .pose-idle,
.stage.delight .pose-idle { opacity: 0; }
.stage[data-state="thinking"] .pose-thinking { opacity: 1; }
.stage[data-state="speaking"] .pose-talking { opacity: 1; }
.stage.delight .pose-laughing { opacity: 1; }

@keyframes breathe {
  0%, 100% { transform: translateY(0) scale(1); }
  50% { transform: translateY(-1.1%) scale(1.006); }
}

/* speaking pulse driven by --speak (0..1) */
.character { transform: translateY(calc(var(--speak) * -0.6%)) scale(calc(1 + var(--speak) * 0.014)); }

.aura {
  position: absolute; z-index: 1; bottom: 6%; left: 50%;
  width: 118%; height: 62%;
  transform: translateX(-50%) scale(calc(1 + var(--speak) * 0.16 + var(--listen) * 0.1));
  background: radial-gradient(ellipse at center,
     rgba(154,108,255, calc(0.42 + var(--speak) * 0.4)) 0%,
     rgba(124,77,188,0.2) 42%, transparent 68%);
  filter: blur(26px);
  opacity: calc(0.55 + var(--speak) * 0.45);
  transition: opacity 0.12s linear;
  pointer-events: none;
}
.pedestal {
  position: absolute; z-index: 1; bottom: 2%; left: 50%;
  width: 96%; height: 40px; transform: translateX(-50%);
  background: radial-gradient(ellipse at center, rgba(255,138,61,0.28), transparent 70%);
  filter: blur(10px);
}

/* Gold glow that blooms from the centre of the background while Klaire speaks.
   Intensity + size track her voice amplitude (--speak, set on the stage). */
/* Gold speaking glow — sits INSIDE the character zone (behind the character
   image, z<3) so it follows Klaire wherever she is on screen, not the centre. */
.speak-glow {
  position: absolute; z-index: 2; left: 50%; bottom: 8%;
  width: 56%; height: 82%; pointer-events: none;   /* smaller, portrait */
  opacity: 0; mix-blend-mode: screen;
  transform: translateX(-50%) scale(calc(0.94 + var(--speak, 0) * 0.2));
  /* small bright core that feathers all the way out to fully transparent */
  background: radial-gradient(ellipse 50% 50% at center,
     rgba(255, 194, 82, calc(0.30 + var(--speak, 0) * 0.5)) 0%,
     rgba(255, 150, 40, calc(0.12 + var(--speak, 0) * 0.26)) 30%,
     transparent 100%);
  filter: blur(30px);
  transition: opacity 0.55s var(--ease), transform 0.12s ease-out;
}
.stage[data-state="speaking"] .speak-glow { opacity: 1; }

/* Thinking is signalled by the cooling, pulsing ambient background (see
   base.css, body.is-thinking) — the old bouncing-dots indicator is retired. */
.thinking { display: none !important; }

/* ---------- greeting + rotating prompt buttons ---------- */
.speech {
  position: absolute; z-index: 5; left: 52px; right: 214px; bottom: 150px;
  transition: opacity 0.5s var(--ease), transform 0.6s var(--ease);
}
.speech-line {
  font-family: var(--font-display);
  font-size: clamp(24px, 3.2vw, 44px); font-weight: 700; line-height: 1.14;
  letter-spacing: -0.2px; color: var(--ink); margin-bottom: clamp(12px, 1.8vmin, 24px);
  text-wrap: balance;
}
.prompts { display: flex; flex-direction: column; align-items: flex-start; gap: clamp(8px, 1.2vmin, 15px); }
.prompt-btn {
  display: inline-flex; align-items: center; gap: 12px; width: auto; max-width: 100%; text-align: left;
  padding: clamp(12px, 1.9vmin, 22px) clamp(16px, 2.3vmin, 28px);
  border-radius: 18px; cursor: pointer;
  background: rgba(201, 182, 242, 0.10); border: 1px solid var(--glass-brd);
  color: var(--lilac); font-family: inherit; font-weight: 600;
  font-size: clamp(15px, 2.05vmin, 23px); backdrop-filter: blur(12px);
  box-shadow: 0 10px 30px -18px rgba(0, 0, 0, 0.7);
  transition: opacity 0.95s var(--ease), transform 0.35s var(--ease),
              background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.prompt-btn svg { width: clamp(18px, 2.5vmin, 28px); height: clamp(18px, 2.5vmin, 28px);
  color: var(--accent-soft); flex: 0 0 auto; transition: color 0.3s var(--ease); }
.prompt-btn:hover, .prompt-btn:active {
  background: var(--accent); color: #2a1206; border-color: transparent; transform: translateY(-2px);
}
.prompt-btn:hover svg, .prompt-btn:active svg { color: #2a1206; }
.prompt-btn.fading { opacity: 0; }   /* slow cross-fade as prompts rotate */
.speech.hide { opacity: 0; transform: translateY(10px); pointer-events: none; }

/* ---------- transient notice ---------- */
/* Its own element, so a passing message cannot drag the attract screen's
   prompts back over a live conversation. */
.toast {
  position: fixed; z-index: 20; left: 50%; bottom: 6%;
  transform: translate(-50%, 12px);
  max-width: min(560px, 86vw); padding: 14px 22px; border-radius: 16px;
  background: rgba(28, 15, 52, 0.95); border: 1px solid var(--glass-brd);
  color: var(--lilac); font-size: 15px; line-height: 1.4; text-align: center;
  box-shadow: var(--shadow); backdrop-filter: blur(18px);
  opacity: 0; pointer-events: none;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
body.mobile-mode .toast { bottom: calc(var(--m-bar) + var(--m-safe) + var(--kb) + 12px); }

/* ---------- transcript ---------- */
.transcript {
  position: absolute; z-index: 5; right: 214px; top: 3.5%; bottom: 150px;
  width: min(36vw, 520px);
  overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column; gap: 14px;
  padding: 8px 6px 8px 2px;
  opacity: 0; pointer-events: none;
  transition: opacity 0.6s var(--ease);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 3%, #000 94%, transparent);
  mask-image: linear-gradient(180deg, transparent 0, #000 3%, #000 94%, transparent);
}
.stage.engaged .transcript { opacity: 1; pointer-events: auto; }
/* Keep the transcript stacked cleanly above the input bar / card.
   Kiosk-scoped: the phone lays the transcript out from the header down, and
   these compound selectors would otherwise outrank the phone rules. */
body:not(.mobile-mode) .stage.kbmode .transcript { bottom: 128px; }
body:not(.mobile-mode) .stage.card-open .transcript { bottom: calc(44vh + 28px); }
body:not(.mobile-mode) .stage.kbmode.card-open .transcript { bottom: calc(44vh + 104px); }
.transcript::-webkit-scrollbar { width: 6px; }
.transcript::-webkit-scrollbar-thumb { background: rgba(201,182,242,0.25); border-radius: 999px; }

.bubble {
  max-width: 88%; padding: 11px 15px; border-radius: 18px; font-size: 15px; line-height: 1.42;
  animation: rise 0.5s var(--ease) both;
}
.bubble.user {
  align-self: flex-end; background: linear-gradient(135deg, var(--purple), var(--purple-bright));
  color: #fff; border-bottom-right-radius: 6px; box-shadow: 0 10px 26px -10px rgba(124,77,188,0.7);
}
.bubble.klaire {
  align-self: flex-start; background: rgba(28, 15, 52, 0.88); border: 1px solid var(--glass-brd);
  color: var(--lilac); border-bottom-left-radius: 6px; backdrop-filter: blur(20px);
  box-shadow: 0 10px 30px -16px rgba(0, 0, 0, 0.75);
}
/* Blinking cursor while a reply is being revealed letter by letter. */
.bubble.klaire.typing::after {
  content: "▌"; margin-left: 1px; color: var(--accent-soft);
  animation: caret 0.9s steps(1) infinite;
}
@keyframes caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
/* Contextual follow-up chips under the latest reply. */
.followups {
  align-self: flex-start; display: flex; flex-wrap: wrap; gap: 8px;
  max-width: 92%; margin: -4px 0 2px; animation: rise 0.5s var(--ease) both;
}
.followup-chip {
  font-family: inherit; cursor: pointer; text-align: left;
  padding: 8px 14px; border-radius: 999px; font-size: 13.5px; font-weight: 600;
  color: var(--accent-soft); background: rgba(255, 138, 61, 0.1);
  border: 1px solid rgba(255, 138, 61, 0.35);
  transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}
.followup-chip::before { content: "↳ "; opacity: 0.7; }
.followup-chip:hover, .followup-chip:active { background: rgba(255, 138, 61, 0.22); transform: translateY(-1px); }
/* "Was this helpful?" prompt */
.followup-chip { }
.feedback {
  align-self: flex-start; display: inline-flex; align-items: center; gap: 10px;
  margin: 2px 0 2px; padding: 8px 14px; border-radius: 999px;
  background: rgba(201,182,242,0.06); border: 1px solid var(--glass-brd);
  animation: rise 0.5s var(--ease) both;
}
.feedback .fb-q { font-size: 13.5px; color: var(--ink-dim); }
.fb-btn {
  border: 1px solid var(--glass-brd); background: rgba(201,182,242,0.08); cursor: pointer;
  width: 34px; height: 34px; border-radius: 50%; font-size: 16px; line-height: 1;
  transition: transform 0.15s var(--ease), background 0.2s var(--ease);
}
.fb-btn:hover, .fb-btn:active { transform: scale(1.12); background: rgba(255,138,61,0.2); }
.bubble.voice {
  align-self: flex-start; display: inline-flex; align-items: center; gap: 10px;
  background: rgba(201,182,242,0.08); border: 1px solid var(--glass-brd); color: var(--ink-dim);
  font-size: 15px; padding: 10px 15px;
}
.bubble.voice .spk { color: var(--accent-soft); }
.bubble.voice button {
  background: none; border: none; color: var(--lavender); cursor: pointer; font-size: 13px;
  text-decoration: underline; padding: 0;
}
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }

/* ---------- right controls ---------- */
.controls {
  position: absolute; z-index: 7; right: 46px; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: center; gap: clamp(16px, 2.6vmin, 34px);
}
.mic-btn {
  position: relative; width: clamp(96px, 14vmin, 176px); height: clamp(96px, 14vmin, 176px);
  border-radius: 50%; border: none; cursor: pointer;
  background: radial-gradient(circle at 35% 30%, var(--purple-bright), var(--purple) 60%, #4a2a86);
  color: #fff; box-shadow: 0 18px 44px -12px rgba(124,77,188,0.85), inset 0 2px 6px rgba(255,255,255,0.25);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.mic-btn:active { transform: scale(0.95); }
.mic-ico { width: clamp(34px, 5vmin, 60px); height: clamp(34px, 5vmin, 60px); fill: currentColor; }
.mic-label { font-size: clamp(13px, 1.6vmin, 19px); font-weight: 700; letter-spacing: 0.5px; }
.mic-ring {
  position: absolute; inset: -6px; border-radius: 50%; border: 3px solid var(--accent);
  opacity: 0; transform: scale(1);
}
.stage[data-state="listening"] .mic-btn {
  background: radial-gradient(circle at 35% 30%, var(--accent-soft), var(--accent) 65%, #d9631f);
  box-shadow: 0 0 0 calc(6px + var(--listen, 0) * 26px) rgba(255,138,61,0.18),
              0 18px 44px -10px rgba(255,138,61,0.7);
}
.stage[data-state="listening"] .mic-ring { animation: ripple 1.6s ease-out infinite; }
@keyframes ripple { 0%{opacity:.7;transform:scale(1)} 100%{opacity:0;transform:scale(1.5)} }

.key-btn {
  width: clamp(64px, 9.6vmin, 120px); height: clamp(64px, 9.6vmin, 120px);
  border-radius: 24%; cursor: pointer;
  background: var(--glass); border: 1px solid var(--glass-brd); color: var(--lavender);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  backdrop-filter: blur(12px);
  transition: transform 0.3s var(--ease), background 0.3s var(--ease), color 0.3s var(--ease);
}
.key-btn svg { width: clamp(26px, 3.8vmin, 44px); height: clamp(26px, 3.8vmin, 44px); }
.key-btn span { font-size: clamp(11px, 1.35vmin, 16px); font-weight: 700; letter-spacing: 0.5px; }
.key-btn:active { transform: scale(0.94); }
.stage.kbmode .key-btn { background: var(--accent); color: #2a1206; border-color: transparent; }

/* ---------- keyboard bar ---------- */
.keybar {
  position: absolute; z-index: 8; left: 50%; bottom: -120px; transform: translateX(-50%);
  width: min(760px, 80vw); display: flex; gap: 12px; align-items: center;
  padding: 12px; border-radius: 26px; background: var(--glass); border: 1px solid var(--glass-brd);
  backdrop-filter: blur(18px); box-shadow: var(--shadow);
  opacity: 0; transition: bottom 0.6s var(--ease), opacity 0.5s var(--ease);
}
/* Kiosk-scoped — on a phone the bar is pinned to the bottom edge instead. */
body:not(.mobile-mode) .stage:not(.vkb-open).kbmode .keybar { bottom: 34px; opacity: 1; }
body:not(.mobile-mode) .stage.kbmode.card-open .keybar { bottom: calc(44vh + 22px); }
/*
 * With the virtual keyboard up, the measured stack WINS over the card-open
 * position. The card-open rule above outranked the vkb rule on specificity, so
 * typing a question that opened a card flung the input bar to mid-screen —
 * hundreds of pixels from the keys the visitor was typing on.
 */
body:not(.mobile-mode) .stage.vkb-open.kbmode .keybar {
  left: auto; right: 46px; top: auto; transform: none;
  bottom: calc(var(--vkb-h, 320px) + var(--vkb-inset) + clamp(10px, 1.5vh, 16px));
  width: min(540px, 72vw);
  opacity: 1;
}
.keybar input {
  flex: 1; background: transparent; border: none; outline: none; color: var(--ink);
  font-size: clamp(15px, 2.3vmin, 18px); font-family: inherit; padding: 12px 14px;
}
.keybar input::placeholder { color: var(--ink-faint); }
.send {
  width: 56px; height: 56px; border-radius: 18px; border: none; cursor: pointer; color: #2a1206;
  background: linear-gradient(135deg, var(--accent-soft), var(--accent));
  display: flex; align-items: center; justify-content: center;
  transition: transform 0.25s var(--ease);
}
.send svg { width: 26px; height: 26px; }
.send:active { transform: scale(0.9); }

/* ---------- info card ---------- */
.card-dock {
  position: absolute; z-index: 9; left: 50%; bottom: 0; transform: translate(-50%, 110%);
  width: min(1080px, 92vw); height: 44vh; max-height: 520px;
  overflow: hidden;
  background: linear-gradient(160deg, rgba(42,21,80,0.92), rgba(20,10,36,0.94));
  border: 1px solid var(--glass-brd); border-bottom: none;
  border-radius: 30px 30px 0 0; box-shadow: var(--shadow); backdrop-filter: blur(22px);
  transition: transform 0.72s var(--ease);
}
.stage.card-open .card-dock { transform: translate(-50%, 0); }
.card-close {
  position: absolute; top: 16px; right: 18px; z-index: 4; width: 46px; height: 46px; border-radius: 50%;
  background: rgba(20,10,36,0.5); border: 1px solid var(--glass-brd); color: var(--ink);
  font-size: 26px; cursor: pointer; line-height: 1; backdrop-filter: blur(6px);
  transition: background 0.25s var(--ease), transform 0.25s var(--ease);
}
.card-close:hover, .card-close:active { background: var(--accent); color: #2a1206; transform: scale(1.05); }
/* Swipe-down handle (portrait sheet). */
.card-grab {
  display: none; position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 4;
  width: 52px; height: 5px; border-radius: 999px; background: rgba(201,182,242,0.4);
}
/* The dock holds two swappable views; the mode class picks which is visible. */
.card-deck, .card-detail { position: absolute; inset: 0; }
.card-dock.mode-deck .card-detail { display: none; }
.card-dock.mode-detail .card-deck { display: none; }

/* ---- detail view (the full single card) ---- */
.card-detail { display: flex; }

/* ---- deck view (grid of summary tiles) ---- */
.card-deck { display: flex; flex-direction: column; padding: 26px 30px 20px; }
.deck-head { font-family: var(--font-display); font-weight: 700; color: var(--ink);
  font-size: clamp(18px, 2vw, 24px); margin: 0 0 14px; padding-right: 56px; }
/* Discipline filter chips above the results grid. */
.deck-filters { display: flex; gap: 8px; overflow-x: auto; padding: 0 2px 16px; flex: 0 0 auto; scrollbar-width: none; }
.deck-filters[hidden] { display: none; }
.deck-filters::-webkit-scrollbar { display: none; }
.deck-chip {
  flex: 0 0 auto; font-family: inherit; cursor: pointer; white-space: nowrap;
  padding: 8px 15px; border-radius: 999px; font-size: 13.5px; font-weight: 700;
  background: rgba(201,182,242,0.08); border: 1px solid var(--glass-brd); color: var(--lavender);
  transition: background 0.2s var(--ease), color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.deck-chip.active { background: var(--accent); color: #2a1206; border-color: transparent; }
.deck-chip:active { transform: scale(0.96); }
/* Results shown as an image-forward grid of portfolio cards. */
.deck-grid { flex: 1; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  grid-auto-rows: clamp(170px, 26vmin, 240px);
  gap: 16px; overflow-y: auto; padding: 6px 4px 0 0; align-content: start; }
.deck-grid::-webkit-scrollbar { width: 6px; }
.deck-grid::-webkit-scrollbar-thumb { background: rgba(201,182,242,0.25); border-radius: 999px; }
/* Each result is a big image tile with the title overlaid at the bottom, so the
   artwork is the focus — the summary lives on the expanded card after a tap. */
/* Card fills its grid row (fixed height via grid-auto-rows), and the image fills
   the card — no ratio-based height, so grid row-tracks size correctly and the
   cards never overlap. */
.deck-card {
  position: relative; height: 100%; text-align: left; cursor: pointer;
  border-radius: 16px; overflow: hidden;
  background: linear-gradient(135deg, var(--plum), var(--bg-1));
  border: 1px solid var(--glass-brd); color: var(--ink);
  transition: transform 0.25s var(--ease), border-color 0.25s var(--ease);
}
.deck-card:hover, .deck-card:active { transform: translateY(-4px); border-color: var(--purple-bright); }
.deck-thumb { position: absolute; inset: 0; overflow: hidden; }
.deck-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center 30%;
  transition: transform 0.4s var(--ease); }
.deck-card:hover .deck-thumb img { transform: scale(1.05); }
/* Legibility scrim under the title. */
.deck-card::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to top, rgba(12,6,24,0.92) 3%, rgba(12,6,24,0.35) 38%, transparent 64%); }
.deck-info { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; flex-direction: column; gap: 3px; padding: 14px 16px 15px; }
.deck-kicker { font-size: 10.5px; letter-spacing: 2.5px; text-transform: uppercase; color: var(--accent-soft); font-weight: 700; }
.deck-title { font-family: var(--font-display); font-size: clamp(15px, 2vmin, 20px); font-weight: 700;
  color: #fff; line-height: 1.15; margin: 0; text-shadow: 0 1px 10px rgba(0,0,0,0.55);
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.deck-sum { display: none; }
.deck-arrow { display: none; }

/* Back-to-results button on an expanded card. */
.detail-back {
  position: absolute; top: 16px; left: 18px; z-index: 4;
  display: none; align-items: center; gap: 4px; cursor: pointer;
  padding: 9px 16px; border-radius: 999px; font-family: inherit; font-weight: 700; font-size: 14px;
  background: rgba(20,10,36,0.5); border: 1px solid var(--glass-brd); color: var(--ink); backdrop-filter: blur(6px);
}
.card-dock.has-deck.mode-detail .detail-back { display: inline-flex; }

.card-media { flex: 0 0 52%; overflow: hidden; position: relative; background: var(--bg-1); }
/* Blurred fill behind the image, in case its aspect ratio leaves any gap. */
.card-media::before {
  content: ""; position: absolute; inset: -8%; z-index: 0;
  background-image: var(--card-bg, none); background-size: cover; background-position: center;
  filter: blur(30px) brightness(0.5) saturate(1.15);
}
/* Image fills the panel edge-to-edge so it's the hero of the card; positioned
   slightly high so faces/subjects stay in frame. */
.card-media img { position: relative; z-index: 1; width: 100%; height: 100%; object-fit: cover; object-position: center 32%; animation: kenburns 12s ease-out both; }
.card-media::after { content: ""; position: absolute; inset: 0; z-index: 2; background: linear-gradient(90deg, transparent 62%, rgba(20,10,36,0.85)); }
@keyframes kenburns { from { transform: scale(1.08); } to { transform: scale(1); } }
/* Interactive location map — occupies the same panel as the media image. */
.card-map { flex: 0 0 52%; position: relative; overflow: hidden; background: var(--bg-1); }
.card-map iframe { width: 100%; height: 100%; border: 0; display: block; }
/* Showreel — poster + play button that loads the embedded player on tap. */
.card-video { flex: 0 0 52%; position: relative; overflow: hidden; background: #000; }
.card-video-poster { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.card-video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.card-video-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 84px; height: 84px; border-radius: 50%; border: none; cursor: pointer;
  background: rgba(255, 138, 61, 0.94); color: #2a1206;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 12px 34px -8px rgba(0, 0, 0, 0.7);
  transition: transform 0.22s var(--ease), background 0.22s var(--ease);
}
.card-video-play svg { width: 38px; height: 38px; margin-left: 4px; }
.card-video-play:hover, .card-video-play:active { transform: translate(-50%, -50%) scale(1.07); background: var(--accent); }
/* Lead-capture QR — scannable code on a white plate + caption. */
.card-qr { flex: 0 0 42%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; padding: 28px; background: var(--bg-1); }
.card-qr[hidden] { display: none; }  /* explicit display:flex would otherwise beat [hidden] */
.card-qr-img { width: min(66%, 260px); aspect-ratio: 1; background: #fff; border-radius: 20px; padding: 16px; box-shadow: var(--shadow); }
.card-qr-cap { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--lavender); text-align: center; }
/* "Send this to my phone" — button + revealed QR on the detail card. */
.send-phone {
  display: inline-flex; align-items: center; gap: 9px; align-self: flex-start;
  margin-top: 14px; padding: 11px 18px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-weight: 700; font-size: 14px; color: var(--lilac);
  background: rgba(201,182,242,0.1); border: 1px solid var(--glass-brd);
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease);
}
.send-phone[hidden] { display: none; }
.send-phone svg { width: 20px; height: 20px; }
.send-phone:hover, .send-phone:active { background: rgba(201,182,242,0.18); border-color: var(--purple-bright); }
.send-phone-qr { display: flex; align-items: center; gap: 16px; margin-top: 14px; }
.send-phone-qr[hidden] { display: none; }
.send-phone-img { width: 128px; height: 128px; background: #fff; border-radius: 14px; padding: 10px; flex: 0 0 auto; }
.send-phone-cap { font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--lavender); }

/* Awards badge list */
.card-extra:empty { display: none; }
.card-extra { margin: 6px 0 4px; }
.award-list { list-style: none; display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; }
.award-item {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px 16px; border-radius: 14px;
  background: rgba(201, 182, 242, 0.08); border: 1px solid var(--glass-brd);
  border-left: 3px solid var(--accent);
}
.award-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--ink); }
.award-detail { font-size: 13px; color: var(--ink-dim); }
/* Client logo wall (typographic) */
.logo-wall { display: flex; flex-wrap: wrap; gap: 8px; }
.logo-tile {
  padding: 8px 14px; border-radius: 999px; font-size: 14px; font-weight: 600;
  color: var(--lilac); background: rgba(201, 182, 242, 0.08); border: 1px solid var(--glass-brd);
}
.card-body { flex: 1; padding: 40px 46px; overflow-y: auto; display: flex; flex-direction: column; }
.card-body::-webkit-scrollbar { width: 6px; }
.card-body::-webkit-scrollbar-thumb { background: rgba(201,182,242,0.25); border-radius: 999px; }
.card-kicker { font-size: 12px; letter-spacing: 3px; text-transform: uppercase; color: var(--accent-soft); font-weight: 700; }
.card-title { font-family: var(--font-display); font-size: clamp(28px, 3vw, 40px); font-weight: 700; margin: 10px 0 16px; line-height: 1.1; letter-spacing: -0.2px; }
.card-text { font-size: clamp(14px, 1.6vw, 16px); line-height: 1.55; color: var(--lilac); white-space: pre-line; }
.card-related { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.card-link { margin-top: 20px; color: var(--accent-soft); font-weight: 600; text-decoration: none; font-size: 15px; }
.card-link:hover { text-decoration: underline; }

/* ---------- QR — vertical, top-right, sized to tuck in beside the chat.
   Same size in idle and while chatting (stays visible throughout). ---------- */
.qr-dock {
  position: absolute; z-index: 6; top: 22px; right: 20px; left: auto; bottom: auto;
  display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center;
  width: calc(clamp(72px, 9.6vmin, 120px) + 20px);
  padding: 10px; border-radius: 18px;
  background: var(--glass); border: 1px solid var(--glass-brd); backdrop-filter: blur(16px);
  box-shadow: 0 14px 36px -18px rgba(0,0,0,0.7);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
  /* Hidden until there is a conversation to hand off. Without this the dock sat
     on the attract screen showing an empty white square, since the image has no
     src until the first exchange mints a token. The transition above was already
     written for exactly this fade. */
  opacity: 0; pointer-events: none;
}
.qr-dock.ready { opacity: 1; pointer-events: auto; }
.qr-frame { width: clamp(72px, 9.6vmin, 120px); height: clamp(72px, 9.6vmin, 120px); border-radius: 12px; background: #fff; padding: 6px; }
.qr-frame img { width: 100%; height: 100%; display: block; }
.qr-copy { display: flex; flex-direction: column; gap: 2px; align-items: center; }
.qr-copy strong { font-family: var(--font-display); font-size: clamp(12px, 1.5vmin, 16px); font-weight: 700; color: var(--ink); line-height: 1.15; }
.qr-copy span { font-size: clamp(10px, 1.2vmin, 13px); color: var(--ink-dim); line-height: 1.25; }

/* auto-reset countdown ring (bottom-right) */
.idle-timer {
  position: absolute; z-index: 30; right: 26px; bottom: 26px;
  width: clamp(50px, 7.4vmin, 78px); height: clamp(50px, 7.4vmin, 78px);
  display: grid; place-items: center;
  opacity: 0; transform: scale(0.85); pointer-events: none;
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}
.idle-timer.show { opacity: 1; transform: none; }
.idle-timer svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.it-track { fill: rgba(20, 10, 36, 0.7); stroke: var(--glass-brd); stroke-width: 2; }
.it-prog {
  fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: 100; stroke-dashoffset: 0; transition: stroke-dashoffset 1s linear;
}
.idle-timer.urgent .it-prog { stroke: #ff5a5a; }
.it-num { position: relative; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(19px, 3vmin, 32px); color: var(--ink); }

/* listening veil */
.listen-veil {
  position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0;
  background: radial-gradient(60% 60% at 26% 60%, rgba(255,138,61,0.10), transparent 70%);
  transition: opacity 0.5s var(--ease);
}
.stage[data-state="listening"] .listen-veil { opacity: 1; }

/* ---------- virtual keyboard (kiosk mode) ---------- */

/*
 * Keybar: sits directly on top of the keyboard, measured rather than guessed.
 *
 * --vkb-h is set by showKeyboard() from the real height. The fallback matters:
 * it is what the bar uses for the frame before the measurement lands, and on
 * the rare path where the keyboard never opens.
 */
.stage.vkb-open.kbmode .keybar {
  left: auto; right: 46px; top: auto; transform: none;
  bottom: calc(var(--vkb-h, 320px) + var(--vkb-inset) + clamp(10px, 1.5vh, 16px));
  width: min(540px, 72vw);
  opacity: 1;
}

/*
 * VKB: sized by its own keys, anchored to the bottom, never scrolled.
 *
 * It used to be stretched between a fixed `top` — tied to where the keybar sat
 * at 50% of the stage — and `bottom: 16px`, with `overflow-y: auto` to cope.
 * On any screen shorter than about 900px that band is smaller than the keys
 * need, so the keyboard became a little scrolling window showing two rows out
 * of five. A keyboard you have to scroll to reach the letters is not a
 * keyboard.
 *
 * Now the height comes from the content and the keys themselves shrink with
 * the viewport (see .vkb-key), so the whole thing is always fully on screen.
 * The keybar is positioned from the measured height — see --vkb-h below.
 */
.vkb {
  position: absolute; z-index: 10;
  right: 46px; bottom: var(--vkb-inset);
  top: auto; height: auto;
  transform: translateY(12px);
  width: min(540px, 72vw);
  padding: clamp(10px, 1.6vh, 14px) clamp(12px, 1.6vw, 16px) clamp(12px, 2vh, 18px);
  border-radius: 20px;
  background: rgba(22, 11, 42, 0.92);
  border: 1px solid var(--glass-brd);
  backdrop-filter: blur(22px);
  box-shadow: 0 12px 50px -16px rgba(0, 0, 0, 0.7);
  transition: transform 0.4s var(--ease), opacity 0.35s var(--ease);
  opacity: 0; pointer-events: none;
  touch-action: manipulation;
  /* Nothing may ever be hidden behind a scroll edge. */
  overflow: hidden;
}
.vkb.vkb-visible { transform: translateY(0); opacity: 1; pointer-events: auto; }

/* ---- prediction bar ---- */
.vkb-predict {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: clamp(7px, 1.3vh, 12px);
  min-height: clamp(32px, 5.2vh, 44px);
}
.vkb-nav {
  flex: 0 0 auto;
  width: clamp(30px, 4.8vh, 40px); height: clamp(30px, 4.8vh, 40px);
  border-radius: 12px;
  background: rgba(201, 182, 242, 0.08); border: 1px solid rgba(154, 108, 255, 0.35);
  color: var(--lavender); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.2s var(--ease);
}
.vkb-nav svg { width: 20px; height: 20px; }
.vkb-nav:active { background: rgba(201, 182, 242, 0.2); }
.vkb-chips-scroll {
  flex: 1; display: flex; gap: 8px; overflow-x: auto; scroll-behavior: smooth;
  scrollbar-width: none; -ms-overflow-style: none;
  padding: 2px 0;
}
.vkb-chips-scroll::-webkit-scrollbar { display: none; }
.vkb-chip {
  flex: 0 0 auto; font-family: inherit; cursor: pointer; white-space: nowrap;
  padding: clamp(6px, 1.1vh, 9px) clamp(12px, 1.8vw, 18px); border-radius: 14px;
  font-size: clamp(12px, 1.9vh, 15px); font-weight: 700;
  color: var(--ink);
  background: linear-gradient(135deg, #845AD6, #7648B5);
  border: 1px solid rgba(154, 108, 255, 0.35);
  transition: background 0.2s var(--ease), transform 0.15s var(--ease);
}
.vkb-chip:active { transform: scale(0.96); background: linear-gradient(135deg, #9a6cff, #845AD6); }

/* ---- key rows ---- */
.vkb-row {
  display: flex; justify-content: center; gap: clamp(4px, 0.8vh, 6px);
  margin-bottom: clamp(4px, 0.8vh, 6px);
}
.vkb-row:last-child { margin-bottom: 0; }
/*
 * The key height is what makes the whole keyboard fit.
 *
 * Six rows of keys plus the prediction bar is roughly nine times this value,
 * so 5.4vh keeps the keyboard near half the screen and lets it shrink on a
 * short one rather than overflow. The 34px floor is the smallest comfortable
 * touch target; below that the answer is a smaller layout, not smaller keys.
 */
.vkb-key {
  flex: 1 1 0; min-width: 0; max-width: 56px;
  height: clamp(34px, 5.4vh, 52px);
  border-radius: 12px; cursor: pointer;
  border: 1px solid rgba(154, 108, 255, 0.35);
  background: rgba(58, 30, 102, 0.7);
  color: var(--ink); font-family: inherit;
  font-size: clamp(14px, 2.2vh, 18px); font-weight: 600;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.12s var(--ease), transform 0.1s var(--ease);
}
.vkb-key:active { transform: scale(0.92); background: rgba(124, 77, 188, 0.7); color: #fff; }
.vkb-key:active .vkb-ico { filter: brightness(0) invert(1); }

/* Wide keys */
.vkb-key.vkb-wide { max-width: none; }
.vkb-key.vkb-shift { flex: 0 0 60px; font-size: 22px; }
.vkb-key.vkb-shift.active { background: var(--purple-bright); color: #fff; }
.vkb-key.vkb-shift.active .vkb-ico { filter: brightness(0) invert(1); }
.vkb-key.vkb-bksp { flex: 0 0 60px; font-size: 22px; }
.vkb-key.vkb-space { flex: 3 1 0; max-width: none; }
.vkb-key.vkb-close {
  flex: 0 0 46px; font-size: 22px; color: var(--ink-dim);
}
.vkb-key.vkb-close .vkb-ico { width: 17.6px; height: 17.6px; }
.vkb-key.vkb-clear {
  flex: 0 0 auto; padding: 0 16px; max-width: none;
  font-size: 14px; font-weight: 700;
}
.vkb-key.vkb-send {
  flex: 0 0 auto; padding: 0 16px; max-width: none;
  font-size: 14px; font-weight: 700;
  background: var(--accent); color: #2a1206;
  border-color: var(--accent);
}
.vkb-key.vkb-send:active { background: var(--accent-soft); }

/* Key icons */
.vkb-ico { width: 22px; height: 22px; object-fit: contain; pointer-events: none; }
.vkb-ico.vkb-ico-on { display: none; }

/* =====================  PORTRAIT KIOSK  ===================== */
@media (max-aspect-ratio: 3/4) {
  /* Large, near life-size character standing on the floor glow, positioned a
     touch left of centre to leave a clear right-hand control rail. */
  .character-zone {
    height: 86vh; bottom: 0; top: auto; left: 41%;
  }
  /* When chatting she simply slides to the left — SAME size, not resized —
     freeing the right side for the conversation. */
  .stage.engaged .character-zone {
    height: 86vh; bottom: 0; top: auto; left: 18%;
  }
  .stage.engaged .brand { opacity: 0; transition: opacity 0.5s var(--ease); }
  /* With a card open she shrinks a little and rises to stay visible above it. */
  .stage.card-open .character-zone { height: 42vh; bottom: calc(52vh - 5vh); left: 20%; }

  /* Controls sit RIGHT-MIDDLE (inherited base rail). When a card is up they lift
     to just above it, so voice/type follow-ups stay one tap away. */
  .controls { right: clamp(20px, 2.8vmin, 40px); }
  .stage.card-open .controls { top: auto; bottom: calc(52vh + 18px); transform: none; }
  /* In keyboard mode the input bar is the follow-up affordance, so hide the rail. */
  .stage.kbmode.card-open .controls { opacity: 0; pointer-events: none; }

  /* Prompts span nearly the full width at the bottom (control rail is mid-right,
     QR sits below). */
  .speech {
    left: 28px; right: 28px; max-width: none; bottom: 188px; top: auto;
  }
  .speech-line { text-shadow: 0 2px 16px rgba(10, 4, 22, 0.75); }

  /* Transcript: a tall block that starts near the top edge and runs down to a
     third from the bottom. Its right edge is kept LEFT of the control rail so
     the bubbles never sit under Talk/Type. The top fade is trimmed so upper
     bubbles reach close to the top. */
  /* Wide chat that slightly overlaps the character on the left. Its right edge
     stays clear of the Talk/Type rail (which is mid-right in this state). Starts
     near the top edge with only a whisper of a top fade. */
  .transcript {
    left: 30%; right: clamp(124px, 19vmin, 224px); width: auto; top: 2%; bottom: 33%;
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 1.5%, #000 96%, transparent);
    mask-image: linear-gradient(180deg, transparent 0, #000 1.5%, #000 96%, transparent);
  }
  /* Card open — keep the SAME left/right as the no-card chat so the alignment
     never shifts, and run the chat DOWN to just above the card. In voice it
     passes to the LEFT of the lifted control rail (its right edge already clears
     it). This MUST override the base landscape rules. */
  /* body:not(.mobile-mode) matches the base rules' specificity — a media
     query gates applicability but adds no weight, so the shorter selectors
     silently LOST to lines ~191-192 and the newest bubbles (which autoscroll
     to the bottom) sat behind the 52vh card sheet. */
  body:not(.mobile-mode) .stage.card-open .transcript { top: 2%; bottom: calc(52vh + 16px); }
  /* Keyboard: the input bar spans full width above the card, so stop above it. */
  body:not(.mobile-mode) .stage.kbmode.card-open .transcript { top: 2%; bottom: calc(52vh + 100px); }
  .bubble { max-width: 100%; font-size: clamp(13px, 2.7vmin, 16px); }

  /* QR keeps the compact top-right position from the base (visible throughout). */
  .qr-dock { top: 20px; right: 18px; }

  /* Keyboard bar: full-width along the bottom (the control rail is mid-right,
     so nothing here needs to dodge it). */
  /* Kiosk-scoped: a phone is portrait too, and it pins the bar to the bottom
     edge instead. :not(.vkb-open).kbmode is a 4-class selector, so without
     this it would outrank the phone rules. */
  body:not(.mobile-mode) .keybar {
    width: auto; left: 24px; right: 24px; transform: none; bottom: 40px;
  }
  body:not(.mobile-mode) .stage:not(.vkb-open).kbmode .keybar { bottom: 40px; }
  body:not(.mobile-mode) .stage.kbmode.card-open .keybar { bottom: calc(52vh + 16px); }
  /* Same fight, portrait: the measured keyboard stack must win over card-open. */
  body:not(.mobile-mode) .stage.vkb-open.kbmode .keybar {
    width: min(540px, calc(100vw - 48px));
    right: clamp(20px, 2.8vmin, 40px); left: auto; transform: none;
    top: auto;
    bottom: calc(var(--vkb-h, 320px) + var(--vkb-inset) + clamp(10px, 1.5vh, 16px));
  }

  /* Card: full-width sheet, image stacked on top, with a swipe-down handle. */
  .card-dock {
    width: 100vw; height: 52vh; max-height: none;
    border-radius: 28px 28px 0 0;
  }
  .card-grab { display: block; }
  .card-detail { flex-direction: column; }
  .card-media { flex: 0 0 46%; }
  .card-map { flex: 0 0 46%; }
  .card-video { flex: 0 0 46%; }
  .card-qr { flex: 0 0 auto; padding: 22px; }
  .card-qr-img { width: min(48%, 200px); }
  .card-media::after { background: linear-gradient(0deg, rgba(20,10,36,0.92), transparent 55%); }
  .card-body { padding: 26px 30px; }
  .card-title { font-size: clamp(26px, 6vw, 34px); }
  .card-deck { padding: 24px 22px 18px; }
  .deck-head { font-size: clamp(17px, 4.8vw, 22px); }

  /* Virtual keyboard in portrait — right-aligned under the Type button */
  .vkb { width: min(540px, calc(100vw - 48px)); right: clamp(20px, 2.8vmin, 40px); }
  .stage.vkb-open.kbmode .keybar {
    width: min(540px, calc(100vw - 48px));
    right: clamp(20px, 2.8vmin, 40px); left: auto; transform: none;
    top: auto;
  }
  /* Heights stay fluid here too — a fixed 48px was the portrait half of the
     same overflow, since portrait is exactly where vertical room is scarce. */
  .vkb-key { height: clamp(34px, 5.4vh, 48px); font-size: clamp(14px, 2.1vh, 16px); }
  .vkb-chip { font-size: clamp(12px, 1.8vh, 14px); }
}

/* ---------------------------------------------------------------------------
   Phone mode
   ---------------------------------------------------------------------------
   Set by isMobileDevice() in config.js, not by a media query, because the
   distinction is about the DEVICE (touch-only and small = no microphone, no
   speech) rather than the current window size. A narrow desktop window is
   still a desktop: it has a mic and should keep its voice.

   The mic and Type buttons are removed from the DOM in mobile mode, so the
   control rail has nothing left in it. Hidden rather than left as an empty
   floating panel.

   The kiosk layout is a stage: a life-size Klaire with the conversation beside
   her. That does not shrink onto a phone — at 375px the portrait eats the
   screen and leaves a letterbox for the text. So the phone is not a scaled
   kiosk, it is a chat: a header strip carrying her face, the conversation
   filling everything, and the input pinned to the thumb.

   Everything below is layout only. It reuses the same elements, so there is
   one transcript, one input and one set of quick replies to keep working.
--------------------------------------------------------------------------- */

/* Height/inset bookkeeping, so the transcript and the chip strip can be told
   where the furniture is without three copies of the same magic number. */
body.mobile-mode {
  --m-head: 60px;
  --m-bar: 69px;        /* input row */
  --m-chips: 52px;      /* quick-reply strip */
  --m-safe: env(safe-area-inset-bottom, 0px);
  /* Set from visualViewport by kiosk.js when the on-screen keyboard opens. */
  --kb: 0px;
}

body.mobile-mode .controls { display: none; }

/* Already on a phone — an invitation to continue on your phone is nonsense,
   and it occupies the most valuable corner of a small screen. */
body.mobile-mode .qr-dock { display: none; }

/* No kiosk-only furniture: nobody is waiting behind you for the screen. */
body.mobile-mode .idle-timer,
body.mobile-mode .listen-veil,
body.mobile-mode .aura,
body.mobile-mode .pedestal,
body.mobile-mode .speak-glow,
body.mobile-mode .back-btn { display: none !important; }

/* ---------- header ---------- */
.m-head { display: none; }
body.mobile-mode .m-head {
  position: fixed; z-index: 10; top: 0; left: 0; right: 0;
  height: calc(var(--m-head) + env(safe-area-inset-top, 0px));
  padding: env(safe-area-inset-top, 0px) 130px 0 14px;
  display: flex; flex-direction: row; align-items: center; gap: 10px;
  background: rgba(20, 10, 36, 0.82);
  border-bottom: 1px solid var(--glass-brd);
  backdrop-filter: blur(18px);
}
body.mobile-mode .m-id {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
}

/* The avatar is a purpose-cropped head shot, not the full-body portrait scaled
   down. Squeezing a 887×1774 standing figure into a 44px circle gives a face
   about six pixels tall; these are cropped from the same four poses at build
   time, so she still visibly thinks while a search runs. */
body.mobile-mode .m-avatar {
  position: relative; flex: 0 0 auto;
  width: 44px; height: 44px; border-radius: 50%; overflow: hidden;
  border: 2px solid rgba(201, 182, 242, 0.45);
  background: #2a164e;
}
body.mobile-mode .m-pose {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0;
  transition: opacity 0.45s var(--ease);
}
body.mobile-mode .m-pose-idle { opacity: 1; }
body.mobile-mode .stage[data-state="thinking"] .m-pose-idle,
body.mobile-mode .stage[data-state="speaking"] .m-pose-idle,
body.mobile-mode .stage.delight .m-pose-idle { opacity: 0; }
body.mobile-mode .stage[data-state="thinking"] .m-pose-thinking { opacity: 1; }
body.mobile-mode .stage[data-state="speaking"] .m-pose-talking { opacity: 1; }
body.mobile-mode .stage.delight .m-pose-laughing { opacity: 1; }
body.mobile-mode .m-name {
  font-family: var(--font-display); font-weight: 700; font-size: 19px;
  line-height: 1.1; color: var(--ink);
}
body.mobile-mode .m-role {
  font-size: 12.5px; line-height: 1.1; color: var(--accent-soft);
}

/*
 * The full-body portrait has no place on a phone — the header avatar is the
 * whole of her presence here.
 *
 * Removed outright rather than resized. The kiosk moves and resizes her with
 * compound selectors like `.stage.engaged .character-zone`, which is three
 * classes and outranks anything prefixed with `body.mobile-mode` — she sized
 * correctly on load and then stretched down the whole screen the moment the
 * conversation started. Fighting that with `!important` on every geometry
 * property is a losing game; not rendering her is one rule that cannot lose.
 */
body.mobile-mode .character-zone { display: none !important; }

/* Logo to the top right. fitLogo() is skipped on mobile — it sizes the logo to
   the tagline, and the tagline is hidden here. */
body.mobile-mode .brand {
  position: fixed; z-index: 12;
  top: calc(env(safe-area-inset-top, 0px) + 18px); right: 14px; left: auto;
  opacity: 1 !important; pointer-events: none;
}
body.mobile-mode .brand-sub { display: none; }
body.mobile-mode .brand-logo { width: 112px !important; }

/* ---------- conversation ---------- */
/* Visible from the first frame: the greeting is the first bubble, so there is
   no attract screen to engage out of. */
body.mobile-mode .transcript,
body.mobile-mode .stage.engaged .transcript {
  position: fixed; left: 0; right: 0; width: auto;
  top: calc(var(--m-head) + env(safe-area-inset-top, 0px));
  bottom: calc(var(--m-bar) + var(--m-chips) + var(--m-safe) + var(--kb));
  padding: 14px 14px 4px;
  opacity: 1; pointer-events: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
/* The card-open case is handled further down, against the sheet's measured
   height — a second rule here only survived because it lost to the later one. */
body.mobile-mode .bubble { max-width: 84%; font-size: 15.5px; }

/* ---------- quick replies ---------- */
/* The kiosk stacks three big prompts in the empty space beside her. There is
   no empty space here, so they become a scrolling chip strip above the input —
   still the same buttons, still rotating while she is idle. */
body.mobile-mode .speech {
  position: fixed; z-index: 9; left: 0; right: 0;
  top: auto; bottom: calc(var(--m-bar) + var(--m-safe) + var(--kb));
  max-width: none; height: var(--m-chips);
}
/* The greeting moved into the transcript as a bubble. */
body.mobile-mode .speech-line { display: none; }
/* Engaging hides the prompts on the kiosk to clear the stage. Here they are
   the fastest way to ask the next thing, so they stay. */
body.mobile-mode .speech.hide {
  opacity: 1; transform: none; pointer-events: auto;
}
body.mobile-mode .prompts {
  flex-direction: row; align-items: center; gap: 8px;
  height: 100%; padding: 0 14px;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
body.mobile-mode .prompts::-webkit-scrollbar { display: none; }
body.mobile-mode .prompt-btn {
  flex: 0 0 auto; max-width: 76vw;
  padding: 9px 16px; border-radius: 999px;
  font-size: 14px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  box-shadow: none;
}
body.mobile-mode .prompt-btn svg { display: none; }
/* Slots 3–7 exist only for the chip strip. */
.prompt-btn.m-only { display: none; }
body.mobile-mode .prompt-btn.m-only { display: inline-flex; }

/* ---------- input ---------- */
/* Full width, flat to the bottom edge, riding above the on-screen keyboard. */
body.mobile-mode .keybar {
  position: fixed; z-index: 10;
  left: 0; right: 0; width: auto; transform: none;
  bottom: calc(var(--kb) + var(--m-safe));
  opacity: 1 !important;
  padding: 10px 12px; gap: 10px;
  border-radius: 0; border-left: none; border-right: none; border-bottom: none;
  background: rgba(20, 10, 36, 0.92);
  transition: bottom 0.18s var(--ease);
}
body.mobile-mode .stage.card-open .keybar { bottom: calc(var(--kb) + var(--m-safe)); }
body.mobile-mode .keybar input {
  padding: 12px 16px; font-size: 16px;   /* 16px or iOS zooms the page on focus */
  border-radius: 999px;
  background: rgba(201, 182, 242, 0.07);
  border: 1px solid rgba(160, 120, 255, 0.55);
}
body.mobile-mode .send {
  width: 48px; height: 48px; border-radius: 50%; flex: 0 0 auto;
}
body.mobile-mode .send svg { width: 22px; height: 22px; }

/* ---------- cards ---------- */
/*
 * A showreel on a phone deserves the screen, not a 44vh sliver — but it stops
 * short of the input bar, so the next question is always one tap away rather
 * than behind a sheet that has to be dismissed first.
 *
 * The sheet is sized by its CONTENT, up to a ceiling. A fixed height made
 * every card the tallest card: asking for the location produced a panel with
 * 254px of body for 89px of address — two-thirds empty — while squeezing the
 * conversation above it into a 212px scrolling sliver. Short card, short
 * sheet; long card, capped and scrollable as before.
 */
body.mobile-mode .card-dock {
  /*
   * FIXED, not absolute — this is the whole bug on a real phone.
   *
   * Every other piece of phone furniture (header, conversation, chips, input)
   * is position: fixed. The dock inherited `absolute` from the kiosk layout,
   * and on a phone those two anchor to different things: a fixed element sits
   * against the VISIBLE viewport, an absolute one against the layout viewport,
   * which extends behind the browser's address bar. So the sheet hung roughly
   * an address-bar's height below everything else — under the input bar, with
   * its last line of text clipped off the bottom of the screen.
   *
   * It looked correct in every desktop-sized test because a desktop browser has
   * no retracting toolbar, so the two viewports are identical there.
   */
  position: fixed;
  width: 100%; left: 0; transform: translate(0, 130%);
  bottom: calc(var(--m-bar) + var(--m-safe) + var(--kb));
  /* dvh, not vh: vh is the height with the address bar HIDDEN, so a sheet
     capped in vh is taller than the screen whenever the bar is showing. */
  height: auto; max-height: 62dvh; border-radius: 24px 24px 0 0;
}
/* inset:0 stretches the panes to the dock; on an auto-height dock they have to
   size themselves instead, or there is nothing for the dock to be as tall as. */
body.mobile-mode .card-detail,
body.mobile-mode .card-deck { position: relative; inset: auto; }

/*
 * A list of results behaves differently from a single card.
 *
 * A detail card is sized by its content — a short address should not reserve
 * two-thirds of the screen. A DECK is a list, and a list wants the space: it
 * gets a definite height so the results inside can scroll, rather than the
 * sheet growing to fit five faces and being clipped at the screen edge, which
 * is what was happening — the third result sat behind the input bar with no
 * way to reach it.
 *
 * min-height: 0 is what actually makes the scroll work. A flex child defaults
 * to min-height: auto, which refuses to shrink below its content, so
 * `overflow-y: auto` on it never has a bounded height to scroll within.
 */
body.mobile-mode .card-dock.mode-deck { height: 62dvh; }
body.mobile-mode .card-deck {
  display: flex; flex-direction: column; min-height: 0; height: 100%;
  padding: 18px 16px 12px;
}
body.mobile-mode .deck-head {
  flex: 0 0 auto; font-size: 19px; margin-bottom: 10px; padding-right: 52px;
}
body.mobile-mode .deck-grid {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  grid-template-columns: 1fr;            /* one per row: a phone is not a wall */
  grid-auto-rows: 150px; gap: 12px;
  padding-right: 2px;
  overscroll-behavior: contain;          /* scrolling the list stops at the list */
  -webkit-overflow-scrolling: touch;
}
/* A percentage of an auto height is circular, so the media panes take a fixed
   share of the screen instead. */
body.mobile-mode .card-media,
body.mobile-mode .card-map,
body.mobile-mode .card-video { flex: 0 0 auto; height: 26dvh; }

/*
 * The single card scrolls too.
 *
 * The deck above got min-height: 0 and a scroller; the DETAIL view never did,
 * so anything past 62dvh — most of the text on a project card — was clipped by
 * the dock's overflow:hidden with no way to reach it.
 *
 * Making the dock a flex column is what bounds the card: the dock is height:auto
 * so it still shrinks to a short address rather than reserving two thirds of the
 * screen, and once content passes max-height the child is bounded by it instead
 * of overflowing past it. Repeating `62dvh` on the child would have worked too
 * and would have been a second copy of the number to keep in step.
 *
 * There is a second bug hiding in the first. The swipe-to-dismiss gesture only
 * takes over when the scroller is already at the top — but a pane that cannot
 * scroll has scrollTop 0 forever, so every upward drag was read as a dismiss.
 * Give the body a real scroll and the gesture stands down by itself.
 */
body.mobile-mode .card-dock.mode-detail { display: flex; flex-direction: column; }
body.mobile-mode .card-detail { flex: 1 1 auto; min-height: 0; }
body.mobile-mode .card-body {
  flex: 0 1 auto; min-height: 0; padding: 20px 22px 24px;
  overflow-y: auto;
  overscroll-behavior: contain;         /* scrolling the card stops at the card */
  -webkit-overflow-scrolling: touch;
}
body.mobile-mode .stage.card-open .card-dock { transform: translate(0, 0); }
/* The chip strip is behind the sheet while it is open. */
body.mobile-mode .stage.card-open .speech { display: none; }
/* --card-h is the sheet's measured height, set by kiosk.js when a card opens.
   The fallback only applies for the frame before the measurement lands. */
body.mobile-mode .stage.card-open .transcript {
  bottom: calc(var(--card-h, 58dvh) + var(--m-bar) + var(--m-safe) + var(--kb));
}

/* ── Frame rate readout (opt-in, ?fps=1) ──────────────────────────────────
 *
 * Deliberately plain: no blur, no shadow, no animation. It is the instrument
 * measuring the scene, so it must not be part of what the scene costs.
 * pointer-events:none keeps it from ever swallowing a touch on a kiosk.
 */
.fps-hud {
  position: fixed; top: 8px; left: 8px; z-index: 9999;
  display: flex; align-items: baseline; gap: 7px;
  padding: 5px 10px; border-radius: 7px;
  background: rgba(0, 0, 0, 0.72); border: 1px solid rgba(255, 255, 255, 0.16);
  font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: #e8e0dc; pointer-events: none; user-select: none;
}
.fps-hud b { font-size: 15px; font-weight: 700; }
.fps-hud span { font-weight: 500; opacity: 0.78; font-size: 11px; }
.fps-hud[data-health='ok'] b { color: #6ee7a8; }
.fps-hud[data-health='warn'] b { color: #ffc861; }
.fps-hud[data-health='bad'] b { color: #ff6b6b; }

/* ── Admin gate ───────────────────────────────────────────────────────────
 *
 * An invisible corner and the login it opens. The corner sits ABOVE the QR
 * dock (z-index 6) so the gesture works whether or not the dock is showing,
 * and the dock has nothing clickable of its own to shadow.
 */
.admin-corner {
  position: absolute; z-index: 7; top: 0; right: 0;
  width: clamp(90px, 14vmin, 170px); height: clamp(90px, 14vmin, 170px);
  background: transparent; cursor: default;
}
/*
 * The backdrop stops BELOW the keyboard's z-index 10 on purpose: the whole
 * point of the gesture is a login you can type into, so the keyboard has to
 * stay on top of the sheet it is typing into.
 */
.admin-gate {
  position: absolute; z-index: 9; inset: 0;
  background: rgba(8, 4, 16, 0.82); backdrop-filter: blur(6px);
  display: flex; justify-content: center; align-items: flex-start;
  padding: clamp(16px, 6vh, 64px) 20px 0;
}
.admin-gate[hidden] { display: none; }
.admin-card {
  width: min(430px, 92vw);
  padding: clamp(18px, 3vh, 26px) clamp(18px, 2.4vw, 26px);
  border-radius: 20px;
  background: rgba(26, 13, 48, 0.96); border: 1px solid var(--glass-brd);
  box-shadow: 0 24px 70px -20px rgba(0, 0, 0, 0.8);
  display: flex; flex-direction: column; gap: 8px;
}
.admin-card h2 { margin: 0; font-size: clamp(18px, 2.6vh, 22px); color: var(--ink); }
.admin-card > p { margin: 0 0 6px; font-size: 13px; color: var(--ink-faint); line-height: 1.5; }
.admin-card label {
  font-size: 10.5px; letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--ink-faint); margin-top: 4px;
}
.admin-card input {
  font: inherit; font-size: 16px; color: var(--ink);
  background: rgba(12, 6, 24, 0.9); border: 1px solid var(--glass-brd);
  border-radius: 12px; padding: 12px 14px; outline: none;
}
.admin-card input:focus { border-color: var(--purple-bright); }
.admin-msg { min-height: 18px; font-size: 12.5px; color: #ff8f8f; }
.admin-msg.working { color: var(--ink-faint); }
.admin-actions { display: flex; gap: 10px; margin-top: 10px; }
.admin-btn {
  flex: 1; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  padding: 12px 16px; border-radius: 14px;
  background: rgba(58, 30, 102, 0.7); border: 1px solid rgba(154, 108, 255, 0.35);
  color: var(--ink);
}
.admin-btn.go { background: var(--accent); border-color: transparent; color: #2a1206; }
.admin-btn:active { transform: scale(0.97); }

/* ── Live 3D avatar ───────────────────────────────────────────────────────
 *
 * The canvas sits exactly where the pose images sit and is display:none until
 * body.avatar-3d says the model is genuinely ready — the class is added after
 * the first frame exists, so there is no moment with neither character.
 */
.avatar3d-canvas {
  display: none;
  position: absolute; inset: 0;
  width: 100%; height: 100%;
}
body.avatar-3d .avatar3d-canvas { display: block; }
body.avatar-3d .character .pose { opacity: 0 !important; }
/*
 * Both of these are 2D-era effects that turn expensive or wrong on a canvas:
 * breathe would sway the whole render on top of the idle clip's own breathing,
 * and drop-shadow re-runs the filter on every one of the canvas's frames.
 */
body.avatar-3d .character { animation: none; filter: none; }

/* ── Recall a dismissed card ──────────────────────────────────────────────
 *
 * A small upward tab at the foot of the stage, shown only while a card is
 * owed. Bottom-centre because the card it brings back rises from there, and
 * because the control rail (right) and brand (left) are already spoken for.
 */
.card-recall {
  position: absolute; z-index: 8;
  bottom: clamp(16px, 3vh, 30px); left: 50%; transform: translateX(-50%);
  width: 70px; height: 36px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; cursor: pointer; color: var(--ink);
  background: var(--glass); border: 1px solid var(--glass-brd);
  backdrop-filter: blur(16px);
  box-shadow: 0 10px 28px -14px rgba(0, 0, 0, 0.7);
  transition: background 0.2s var(--ease), transform 0.15s var(--ease);
}
/* After the display rule above, or [hidden] loses to it. */
.card-recall[hidden] { display: none; }
.card-recall svg { width: 28px; height: 28px; }
.card-recall:active { transform: translateX(-50%) scale(0.94); background: rgba(154, 108, 255, 0.28); }
/*
 * Never while the on-screen keyboard is up: on a narrower screen the keyboard
 * reaches past the centre line, and an undo tab is not something anyone needs
 * mid-word. Dismissing the keyboard brings it back.
 */
.stage.vkb-open .card-recall { display: none; }
/* Phone: above the fixed input bar, and above the OS keyboard when it opens. */
body.mobile-mode .card-recall {
  bottom: calc(var(--m-bar) + var(--m-safe) + var(--kb) + 12px);
}
