:root {
  --ink: #160a38;
  --blue: #250e62;
  --purple: #7734ac;
  --pink: #E31E79;
  --paper: #f6f4fa;
  --line: rgba(37, 14, 98, 0.14);
  --muted: #6d6879;
  --white: #fff;
  /* DIGITAL Brand Kit 2026 p.10: Aptos for headings, Arial for body.
     Aptos ships with Microsoft 365 / Windows 11 and is not a web font, so
     Arial — the kit's own body face — is the fallback. */
  --font-head: "Aptos Display", "Aptos", Arial, Helvetica, sans-serif;
  --font: Arial, Helvetica, sans-serif;
  --voice-level: 0;
  /* Height of the site footer. Every bottom offset is measured from it. */
  --foot-h: 44px;
  /* How much of the stage the avatar fills. She grows on a large display and
     pulls back on a small one, so she reads at the same relative size rather
     than dominating a laptop or vanishing on a wall screen. */
  --avatar-scale: 1.26;
  /* Width the transcript claims when open. The stage gives up exactly this. */
  --transcript-w: 366px;
}

* { box-sizing: border-box; }

html,
body {
  min-width: 280px;
  min-height: 100%;
}

html { background: var(--blue); }

body {
  margin: 0;
  overflow: hidden;
  color: var(--ink);
  background: var(--blue);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
}

button,
input { font: inherit; }

button { color: inherit; }
a { color: inherit; text-decoration: none; }
h1, h2, p { margin: 0; }
/* Kit p.10: Aptos carries headings, Arial the body copy. */
h1, h2, h3 { font-family: var(--font-head); }

button,
a,
input { -webkit-tap-highlight-color: transparent; }

button:focus-visible,
a:focus-visible,
input:focus-visible {
  outline: 3px solid rgba(227, 28, 121, 0.6);
  outline-offset: 3px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.app {
  position: relative;
  width: 100vw;
  height: 100vh;
  height: 100svh;
  min-height: 500px;
  overflow: hidden;
  background: var(--blue);
}

/* ---------- Site footer ---------- */

/* Matches the atlas footer so the two pages read as one product. It is a real
   bar at the foot of the stage, so everything anchored to the bottom is offset
   by --foot-h rather than overlapping it. */
.site-foot {
  position: fixed;
  z-index: 36;
  right: 0;
  bottom: 0;
  left: 0;
  height: var(--foot-h);
  min-height: var(--foot-h);
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 0 clamp(16px, 2vw, 30px);
  color: var(--muted);
  border-top: 1px solid var(--line);
  background: var(--white);
  font-size: 10px;
}

.site-foot strong { color: var(--blue); }

/* ---------- Floating masthead ---------- */

.masthead {
  position: absolute;
  z-index: 40;
  top: 0;
  right: 0;
  left: 0;
  height: 104px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 28px clamp(26px, 4vw, 64px);
  border: 0;
  background: transparent;
}

.brand {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 13px;
  color: #fff;
}

.brand svg {
  width: 112px;
  height: auto;
  flex: none;
}

.brand svg path { fill: #fff; }

.brand-rule {
  width: 1px;
  height: 25px;
  flex: none;
  background: rgba(255, 255, 255, 0.34);
}

.brand-name {
  overflow: hidden;
  color: rgba(255, 255, 255, 0.86);
  font-size: 10px;
  line-height: 1.15;
  font-weight: 750;
  letter-spacing: 0.065em;
  text-transform: uppercase;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.mast-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(37, 14, 98, 0.9);
}

.status-pill {
  min-height: 38px;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 0 14px;
  color: rgba(255, 255, 255, 0.84);
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.09);
  font-size: 11px;
  font-weight: 700;
}

.status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #c8c3d8;
  box-shadow: 0 0 0 4px rgba(200, 195, 216, 0.13);
}

.status-pill[data-state="connecting"] .status-dot {
  background: #e6a33e;
  animation: pulse 1.2s ease-in-out infinite;
}

.status-pill[data-state="live"] .status-dot {
  background: #25aa6c;
  box-shadow: 0 0 0 4px rgba(37, 170, 108, 0.13);
}

.status-pill[data-state="error"] .status-dot { background: #d94f67; }

.transcript-toggle {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.09);
  cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}

.transcript-toggle:hover,
.transcript-toggle[aria-pressed="true"] {
  color: #fff;
  background: var(--purple);
  transform: translateY(-1px);
}

/* ---------- Immersive stage ---------- */

.stage,
.atlas {
  width: 100%;
  height: 100%;
  min-height: 0;
}

.atlas {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: linear-gradient(90deg, var(--blue) 0 49%, var(--lavender, #e9e6ef) 49% 100%);
}

.scene-backdrop {
  position: absolute;
  z-index: -2;
  inset: 0;
  overflow: hidden;
}

/* A soft studio wash rather than concentric rings. The rings drew hard-edged
   circles centred behind the avatar, which read as a target with her standing
   on the bullseye. This lights her from behind and falls away toward the
   floor, so the panel has depth without any shape competing with her. */
.scene-backdrop::before {
  content: "";
  position: absolute;
  inset: 0 0 0 49%;
  background:
    radial-gradient(
      118% 78% at 50% 26%,
      #fff 0%,
      rgba(255, 255, 255, 0.72) 34%,
      rgba(255, 255, 255, 0) 68%
    ),
    linear-gradient(
      180deg,
      #f4f2f8 0%,
      #ebe8f1 54%,
      #e2dded 86%,
      #d8d2e6 100%
    );
}

/* The grid stays as quiet texture, faded out behind her so it never crosses
   the figure, and faded in toward the floor to suggest a surface. */
.scene-backdrop::after {
  content: "";
  position: absolute;
  inset: 0 0 0 49%;
  background:
    linear-gradient(rgba(37, 14, 98, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(37, 14, 98, 0.045) 1px, transparent 1px);
  background-size: 34px 34px;
  -webkit-mask-image: radial-gradient(72% 58% at 50% 34%, transparent 0%, #000 74%);
  mask-image: radial-gradient(72% 58% at 50% 34%, transparent 0%, #000 74%);
}

.window-frame,
.wall-seam { display: none; }

.scene-heading {
  position: absolute;
  z-index: 1;
  inset: 0;
  pointer-events: none;
}

.scene-kicker {
  position: absolute;
  top: clamp(142px, 17vh, 190px);
  left: clamp(28px, 5vw, 76px);
  color: #c8c3d8;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.scene-heading h1 {
  position: absolute;
  top: clamp(174px, 21vh, 228px);
  right: auto;
  left: clamp(28px, 5vw, 76px);
  display: block;
  width: 40%;
  color: #fff;
  font-size: clamp(60px, 7vw, 108px);
  line-height: 0.88;
  font-weight: 780;
  letter-spacing: -0.065em;
}

.scene-heading h1 span { display: block; }
.scene-heading h1 span:last-child { color: #fff; text-align: left; }

.scene-lede {
  position: absolute;
  top: clamp(340px, 45vh, 470px);
  left: clamp(28px, 5vw, 76px);
  width: min(35vw, 480px);
  color: rgba(255, 255, 255, 0.78);
  font-size: clamp(14px, 1.25vw, 18px);
  line-height: 1.5;
}

.stage-col {
  position: absolute;
  z-index: 4;
  inset: 104px 0 calc(var(--foot-h) + 78px) 49%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
}

/* ---------- Avatar ---------- */

.avatar-frame {
  position: relative;
  width: min(49vw, 700px, calc((100svh - 182px - var(--foot-h)) * 1.185));
  aspect-ratio: 32 / 27;
  overflow: visible;
  isolation: isolate;
}

/* The floor shadow sits directly under her, tracking the centred figure. */
.avatar-ground {
  position: absolute;
  z-index: 0;
  bottom: 1.5%;
  left: 50%;
  width: min(38vw, 520px);
  height: 42px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(28, 20, 38, 0.25), transparent 68%);
  filter: blur(7px);
}

/* A soft pool of light she stands in. */
.avatar-glow {
  position: absolute;
  z-index: -1;
  right: 3%;
  bottom: 3%;
  left: 3%;
  height: 80%;
  opacity: 0;
  border-radius: 48%;
  background: radial-gradient(ellipse at 50% 52%, rgba(255, 255, 255, 0.28), transparent 64%);
  filter: blur(16px);
  transition: opacity 0.5s ease, background 0.5s ease;
}

.avatar-frame:has(.has-video) .avatar-glow { opacity: 1; }

/* Charlie is a full-body 3D character: she stands on the ground line and owns
   the stage, so the container is the whole panel. The oval portrait frame that
   used to live here existed to explain the crop on a head-and-shoulders photo
   avatar. A whole figure needs no such explanation. */
.avatar-video {
  position: absolute;
  z-index: 2;
  inset: 0;
}

/* Speaking brightens the light she stands in, rather than a badge over her. */
.avatar-frame[data-speaking="true"] .avatar-glow {
  background: radial-gradient(ellipse at 50% 52%, rgba(227, 28, 121, 0.16), transparent 66%);
}

/* Contain, bottom-anchored and scaled up: she stands on the ground line and
   fills the panel height, with her hands kept inside the frame. */
.avatar-video video,
.avatar-video canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  transform: scale(var(--avatar-scale));
  transform-origin: center bottom;
  filter: drop-shadow(0 18px 24px rgba(21, 15, 29, 0.16));
}

.avatar-video audio { display: none; }

.avatar-source {
  width: 1px !important;
  height: 1px !important;
  opacity: 0;
  pointer-events: none;
}

.avatar-source--fallback {
  width: 100% !important;
  height: 100% !important;
  opacity: 1;
  object-fit: contain !important;
}

.avatar-composite {
  animation: avatar-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.avatar-placeholder {
  position: absolute;
  z-index: 3;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--blue);
  text-align: center;
  transition: opacity 0.45s ease, transform 0.45s ease;
  pointer-events: auto;
}

.avatar-placeholder.has-video {
  opacity: 0;
  transform: scale(0.97);
  pointer-events: none;
}

.avatar-mark {
  position: relative;
  width: 132px;
  height: 132px;
  margin-bottom: 34px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: 50%;
  background:
    radial-gradient(circle at 38% 30%, rgba(119, 52, 172, 0.75), transparent 42%),
    var(--blue);
  box-shadow:
    0 24px 55px rgba(37, 14, 98, 0.34),
    0 0 48px rgba(227, 30, 121, 0.15),
    inset 0 0 30px rgba(255, 255, 255, 0.08);
}

.avatar-mark::before,
.avatar-mark::after {
  content: "";
  position: absolute;
  pointer-events: none;
}

.avatar-mark::before {
  inset: -19px;
  border: 2px solid transparent;
  border-top-color: var(--pink);
  border-right-color: var(--purple);
  border-radius: 50%;
  box-shadow: 0 0 24px rgba(227, 30, 121, 0.14);
}

.avatar-mark::after {
  inset: -38px;
  border: 1px dashed rgba(119, 52, 172, 0.42);
  border-radius: 50%;
  opacity: 0.72;
}

.avatar-mark span {
  position: relative;
  z-index: 1;
  width: 70px;
  height: 84px;
  display: block;
  filter: drop-shadow(0 0 14px rgba(255, 255, 255, 0.38));
}

.avatar-mark svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.avatar-mark svg path {
  fill: white;
}

/* Audio-only mode keeps Charlie present without calling Azure's avatar
   renderer. The orb rests gently while connected, then switches to the more
   expressive speaking animation below when audio starts. */
.avatar-frame[data-state="live"]:not([data-speaking="true"]) .avatar-mark {
  animation: avatar-logo-idle 3.2s ease-in-out infinite;
}

@keyframes avatar-logo-idle {
  0%, 100% { transform: translateY(0) scale(1); opacity: 0.94; }
  50% { transform: translateY(-5px) scale(1.035); opacity: 1; }
}

.avatar-frame[data-state="live"]:not([data-speaking="true"]) .avatar-mark::before {
  animation: avatar-ring-spin 7s linear infinite;
}

.avatar-frame[data-state="live"]:not([data-speaking="true"]) .avatar-mark::after {
  animation: avatar-ring-spin 14s linear infinite reverse;
}

/* Connecting: the core pulses and the two rings spin at different speeds, so
   the orb reads as "working" rather than idle while the session negotiates. */
.avatar-frame[data-state="connecting"] .avatar-mark {
  animation: avatar-mark-pulse 1.4s ease-in-out infinite;
}

.avatar-frame[data-state="connecting"] .avatar-mark::before {
  animation: avatar-ring-spin 1.2s linear infinite;
}

.avatar-frame[data-state="connecting"] .avatar-mark::after {
  animation: avatar-ring-spin 2s linear infinite reverse;
}

.avatar-frame[data-state="connecting"] .avatar-mark span {
  animation: avatar-dot-fade 1.4s ease-in-out infinite;
}

@keyframes avatar-mark-pulse {
  0%, 100% { transform: scale(0.96); filter: saturate(0.9); }
  50% { transform: scale(1.04); filter: saturate(1.18); }
}

@keyframes avatar-ring-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

@keyframes avatar-dot-fade {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .avatar-frame[data-state="connecting"] .avatar-mark,
  .avatar-frame[data-state="connecting"] .avatar-mark::before,
  .avatar-frame[data-state="connecting"] .avatar-mark::after,
  .avatar-frame[data-state="connecting"] .avatar-mark span {
    animation: none;
  }
}

/* Speaking: only visible while the placeholder itself is visible,
   i.e. before the avatar video track has arrived (or in an audio-only
   fallback) — once video is playing, her own face is the "she's talking"
   signal and .avatar-frame[data-speaking] instead brightens the ground glow
   (see above). A soft breathing pulse plus a slow colour sweep around the
   rings, deliberately calmer than the connecting spin so the two states read
   differently at a glance. */
.avatar-frame[data-speaking="true"] .avatar-mark {
  animation: avatar-logo-speak 0.72s ease-in-out infinite alternate;
}

.avatar-frame[data-speaking="true"] .avatar-mark::before {
  animation: avatar-ring-spin 1.05s linear infinite;
}

.avatar-frame[data-speaking="true"] .avatar-mark::after {
  animation: avatar-ring-spin 2.2s linear infinite reverse;
}

.avatar-frame[data-speaking="true"] .avatar-mark span {
  animation: avatar-logo-glow 0.72s ease-in-out infinite alternate;
}

@keyframes avatar-logo-speak {
  0% {
    transform: scale(0.96);
    box-shadow: 0 20px 48px rgba(37, 14, 98, 0.3), 0 0 32px rgba(119, 52, 172, 0.16);
  }
  100% {
    transform: scale(1.08);
    box-shadow: 0 26px 62px rgba(37, 14, 98, 0.36), 0 0 58px rgba(227, 30, 121, 0.34);
  }
}

@keyframes avatar-logo-glow {
  from { transform: scale(0.94); filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.32)); }
  to { transform: scale(1.06); filter: drop-shadow(0 0 24px rgba(255, 255, 255, 0.76)); }
}

@media (prefers-reduced-motion: reduce) {
  .avatar-frame[data-speaking="true"] .avatar-mark,
  .avatar-frame[data-speaking="true"] .avatar-mark::before,
  .avatar-frame[data-speaking="true"] .avatar-mark::after,
  .avatar-frame[data-speaking="true"] .avatar-mark span {
    animation: none;
  }
}

.avatar-placeholder strong {
  font-size: 27px;
  line-height: 1.1;
}

.avatar-placeholder small {
  margin-top: 6px;
  color: var(--muted);
  font-size: 12px;
}

/* The speaking badge is not drawn. It sat inside the avatar frame at bottom 7%,
   which put a dark pill of animated bars across the avatar's hands whenever she
   spoke. The avatar's own mouth already shows she is speaking, and the status
   pill in the masthead carries the session state, so the badge was redundant as
   well as badly placed.
   The element stays in the DOM and app.js keeps toggling its `hidden` property;
   only the rendering is removed, so no show/hide path can break on a missing
   node. Give it styling again to bring the badge back. */
.speaking-indicator { display: none; }

/* ---------- Conversation glass card ---------- */

.conversation-card {
  position: absolute;
  z-index: 10;
  bottom: calc(var(--foot-h) + 102px);
  left: clamp(28px, 5vw, 76px);
  width: min(40vw, 560px);
  padding: 16px 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  background: transparent;
}

.conversation-intro {
  display: flex;
  align-items: center;
  gap: 12px;
}

.ai-mark {
  width: 43px;
  height: 43px;
  flex: none;
  display: grid;
  place-items: center;
  color: var(--blue);
  border-radius: 13px;
  background: #fff;
  box-shadow: none;
}

.eyebrow {
  margin-bottom: 2px;
  color: #c8c3d8;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.conversation-card h2 {
  color: #fff;
  font-size: 24px;
  line-height: 1.05;
  font-weight: 780;
  letter-spacing: -0.035em;
}

.conversation-copy {
  margin-top: 12px;
  color: rgba(255, 255, 255, 0.74);
  font-size: 13px;
  line-height: 1.42;
}

/* What Charlie is scoped to, in the card where she is introduced. It is the
   longest paragraph on the page and the card is anchored to the bottom of the
   stage, so it is given its own scroller rather than a smaller window being
   allowed to cut the sentence that says what she will not answer. */
.conversation-scope {
  margin-top: 10px;
  max-height: min(30vh, 138px);
  overflow-y: auto;
  overscroll-behavior: contain;
  color: rgba(255, 255, 255, 0.66);
  font-size: 12px;
  line-height: 1.45;
}

/* Chips ride above the control bar, so they are right-aligned and read
   against the light half of the scene rather than the purple panel.

   One row, scrolled sideways, rather than wrapped: there are five approved
   openers and each is the whole question it asks, so wrapping them stacked
   two and three rows deep into the bottom of the panel Charlie presents into
   and covered whatever a visitor had scrolled to. The row keeps a single
   chip's height at every width, and the questions past the edge are reached
   by scrolling the row. */
.suggestions {
  display: flex;
  flex-wrap: nowrap;
  /* `safe` keeps the row against the right edge while it fits and falls back
     to the start edge when it does not: with a plain `flex-end` the overflow
     goes off the left of the scroller, where nothing can scroll to it, and
     the first question was simply gone. */
  justify-content: safe flex-end;
  gap: 6px;
  padding-right: 7px;
  /* A scroller has to be allowed to be narrower than its content, or the row
     simply grows past the dock it sits in and the first question hangs off
     the left of the window. */
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.suggestions::-webkit-scrollbar { display: none; }

.chip {
  /* The row itself stays click-through; only the chips take pointer events. */
  pointer-events: auto;
  flex: 0 0 auto;
  min-height: 30px;
  padding: 0 11px;
  color: var(--blue);
  border: 1px solid rgba(37, 14, 98, 0.14);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.86);
  box-shadow: 0 6px 18px rgba(21, 13, 36, 0.1);
  backdrop-filter: blur(14px);
  font-size: 9px;
  font-weight: 750;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

.chip:hover:not(:disabled) {
  color: var(--pink);
  border-color: rgba(227, 28, 121, 0.42);
  transform: translateY(-1px);
}

.chip:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.disclosure {
  margin-top: 11px;
  color: rgba(255, 255, 255, 0.62);
  font-size: 9px;
}

/* Shown only for visitors who arrived from a project card. Built as a button
   rather than a text link: it is the only way back to the atlas, and it sits
   in a corner of the page where a bare underline reads as decoration. */
.atlas-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  margin-top: 12px;
  padding: 0 14px;
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.09);
  font-size: 10px;
  font-weight: 750;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease,
              background 0.2s ease, transform 0.2s ease;
}

.atlas-link:hover {
  color: #fff;
  border-color: rgba(227, 28, 121, 0.66);
  background: rgba(255, 255, 255, 0.16);
  transform: translateY(-1px);
}

.atlas-link:focus-visible {
  outline: 2px solid var(--pink);
  outline-offset: 2px;
}

/* ---------- Visual panel ---------- */

/* When Charlie shows something, the content takes the stage and she steps
   aside to a corner tile. One region changes, and it is the region the reader
   is already looking at — the presenter moving aside for the slide. */
.visual-panel {
  position: absolute;
  z-index: 20;
  top: 128px;
  right: clamp(20px, 3vw, 44px);
  bottom: calc(var(--foot-h) + 96px);
  left: calc(49% + clamp(20px, 3vw, 44px));
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: #fff;
  box-shadow: 0 26px 64px rgba(23, 7, 71, 0.16);
  animation: panel-in 0.28s ease;
}

.visual-panel[hidden] { display: none; }

.visual-head {
  display: grid;
  gap: 9px;
  padding: 15px 19px 13px;
  border-bottom: 1px solid var(--line);
}

.visual-head-main {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

/* The way back up, from the top of the program down to whatever is on screen.
   Reads as a path rather than a row of buttons: the steps behind you are
   quiet until you go for one, and where you are is simply stated. */
.visual-trail {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 3px 5px;
  min-width: 0;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.06em;
}

.visual-trail[hidden] { display: none; }

.visual-trail-step {
  padding: 3px 7px;
  color: var(--purple);
  border: 0;
  border-radius: 999px;
  background: rgba(119, 52, 172, 0.08);
  cursor: pointer;
  transition: color 0.18s, background 0.18s;
}

.visual-trail-step:hover {
  color: #fff;
  background: var(--purple);
}

.visual-trail-sep { color: var(--line); }

.visual-trail-here {
  min-width: 0;
  padding: 3px 2px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.visual-eyebrow {
  color: var(--purple);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.visual-head h2 {
  margin-top: 3px;
  color: var(--blue);
  font-size: 19px;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.visual-close {
  width: 28px;
  height: 28px;
  flex: none;
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--muted);
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease;
}

.visual-close:hover { color: var(--blue); background: rgba(37, 14, 98, 0.07); }

.visual-body {
  overflow-y: auto;
  padding: 16px 19px;
  font-size: 13px;
  line-height: 1.55;
  color: #4a4458;
}

.visual-tagline {
  color: var(--blue);
  font-size: 15px;
  line-height: 1.4;
  font-weight: 700;
}

.visual-org {
  margin-top: 6px;
  color: var(--muted);
  font-size: 11px;
  line-height: 1.45;
}

.visual-body h3 {
  margin: 18px 0 5px;
  color: var(--purple);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.visual-body ul { margin: 0; padding-left: 17px; }
.visual-body li { margin-bottom: 5px; }

/* ---------- The panel in embedded-content mode ---------- */

/* Focus pages and the four current journey-modal designs use the same frame.
   The legacy project summary stays in the markup as a no-script fallback, but
   is never shown over either embedded surface. */
.visual-panel[data-mode="microsite"] .visual-body,
.visual-panel[data-mode="exhibit"] .visual-body,
.visual-panel[data-mode="gallery"] .visual-body { display: none; }

/* The page carries its own citation, and a more precise one — "2025 Housing
   Mission Brief (page 4 & 11)" rather than the catalogue-level line this
   panel shows for a project card. Two source lines one above the other is
   one too many, and the page's is the one worth keeping. */
.visual-panel[data-mode="microsite"] .visual-source,
.visual-panel[data-mode="exhibit"] .visual-source,
.visual-panel[data-mode="gallery"] .visual-source { display: none; }

/* The reused dossier already has its own identity header. Repeating its title
   in Charlie's panel wastes vertical space and made the outer heading appear
   clipped at narrow canvas edges, so the title, the eyebrow and the presenting
   pill all stand down here.

   The strip itself does not. A dossier is the deepest thing the panel can
   hold, and it is exactly where a visitor most needs to know how to get back
   out of it — so the head keeps its own row and carries the trail across it
   with the close button at the end. Floating the trail over the frame beside
   the close button would cost nothing in height and land a pill of navigation
   on the dossier's own eyebrow, which is the one line of it that says what
   the visitor is looking at. The row is worth its 37px. */
.visual-panel[data-mode="exhibit"] {
  grid-template-rows: auto minmax(0, 1fr);
}

.visual-panel[data-mode="exhibit"] .visual-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
}

.visual-panel[data-mode="exhibit"] .visual-head-main > div,
.visual-panel[data-mode="exhibit"] .visual-live {
  display: none;
}

/* While she is presenting, the introduction strip stands down and the page
   takes its place. "Hi, I'm Charlie" is an opening line, and someone watching
   her present a page has had it — so the strip is spending 68px of a framed
   page's height on nothing. Raising the panel without hiding it was tried
   first and simply put the panel on top of it.

   The way back to the atlas goes with it. That is acceptable here and only
   here: the panel's close button is always present, and closing brings the
   strip and its link straight back.

   This applies to both things the panel can hold. A project card was tried
   sized to its own content instead, on the reasoning that a few paragraphs do
   not need a full page's height — but a short card floating above a large
   empty stage reads as less finished than a full panel does, and the two
   surfaces swapping shape as the conversation moved between them was its own
   distraction. One shape, one position, whatever is inside it. */
.app[data-visual="open"] .conversation-card,
.app[data-visual="open"] .atlas-link { display: none; }

.visual-frame {
  position: relative;
  min-height: 0;
  background: #FBFAFD;
}

.visual-frame[hidden] { display: none; }

.visual-frame iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The page paints itself in about a second on a cold cache, and an empty
   white rectangle beside a talking presenter looks like a failure. */
.visual-frame-veil {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 12px;
  background: #FBFAFD;
  color: var(--muted);
  font-size: 11px;
  letter-spacing: 0.04em;
}

.visual-frame-veil[hidden] { display: none; }

.visual-frame-spinner {
  width: 22px;
  height: 22px;
  border: 2px solid rgba(37, 14, 98, 0.16);
  border-top-color: var(--purple);
  border-radius: 50%;
  animation: visual-spin 0.8s linear infinite;
}

@keyframes visual-spin { to { transform: rotate(360deg); } }

/* Says the page is following her rather than merely sitting there — which is
   otherwise something the visitor has to work out for themselves. */
.visual-live {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  margin-left: auto;
  margin-right: 4px;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(227, 30, 121, 0.1);
  color: var(--pink, #E31E79);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.visual-live[hidden] { display: none; }

.visual-live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: visual-live-pulse 1.9s ease-in-out infinite;
}

@keyframes visual-live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.7); }
}

@media (prefers-reduced-motion: reduce) {
  .visual-frame-spinner,
  .visual-live-dot { animation: none; }
}

/* Naming the source is not decoration: it is what separates an approved panel
   from a picture someone found. */
.visual-source {
  padding: 11px 19px;
  color: var(--muted);
  border-top: 1px solid var(--line);
  font-size: 10px;
}

/* With a visual up, the page becomes a viewer: the split disappears, the
   introduction collapses to a strip along the top, and the panel takes the
   full width. Holding the 49% purple column while a document sits in the
   remaining half wastes the screen on material the reader has already read. */
.app[data-visual="open"] .atlas { background: var(--paper); }

/* The masthead and the intro strip stay on the same dark navy the page opens
   with, rather than turning white for the light document-viewer background
   behind them — Charlie sharing a panel is still Charlie's page, not a
   plain document reader, and it keeps the DIGITAL wordmark reading the same
   whether she's sharing or not. */
.app[data-visual="open"] .masthead { background: var(--blue); }
.app[data-visual="open"] .scene-backdrop,
.app[data-visual="open"] .scene-heading { display: none; }

/* The introduction used to collapse into a dark strip along the top while the
   panel was open. It is hidden outright now, so the rules that shaped that
   strip have been removed rather than left behind to be puzzled over. */

/* 104px, not 172px: the panel starts directly under the masthead now that the
   introduction strip that used to sit between them is hidden. */
/* The panel used to run the full width and the full height of the stage, and
   the two things that sit over it — Charlie's corner tile and the suggestion
   chips — were simply drawn on top of it. At every desktop size the tile
   covered a 168x130 block of the panel's bottom-right corner and the chips
   crossed its bottom edge. On the hub, where the lower half of the frame is
   empty, that looked deliberate; on a scrolling overview or a dossier it hid
   whatever the visitor had scrolled to.

   The panel now ends above the chips and stops short of the tile's lane, so
   the three things share the stage instead of stacking on it. Nothing is
   hidden and nothing is clipped: the panel is a little smaller and everything
   in it stays reachable. */
.app[data-visual="open"] .visual-panel {
  top: 104px;
  right: calc(clamp(26px, 4vw, 56px) + 168px + 18px);
  bottom: calc(var(--foot-h) + 132px);
  left: clamp(20px, 3vw, 44px);
}

/* Charlie shrinks into the corner and keeps talking. She has to sit above the
   panel, not behind it: the stage column is normally the backdrop the panel
   covers, and at this size that would hide her entirely. */
.app[data-visual="open"] .stage-col {
  z-index: 22;
  inset: auto clamp(26px, 4vw, 56px) calc(var(--foot-h) + 132px) auto;
  width: 168px;
  height: 214px;
}

/* The placeholder inside the tile was still drawn at stage size: a 132px orb
   with two rings reaching 38px past it, then the name, then the status line.
   That composition is about 280px tall and 208px wide inside a tile 168px
   wide and 142px tall, so it overflowed on every side — which is why
   "Speaking..." and "Listening..." rendered below the tile and ended up
   behind the suggestion chips. Scaled to the tile it is a corner presence
   with its status line legible in its own space. */
.app[data-visual="open"] .avatar-mark {
  width: 84px;
  height: 84px;
  margin-bottom: 11px;
}

.app[data-visual="open"] .avatar-mark::before { inset: -12px; }
.app[data-visual="open"] .avatar-mark::after { inset: -24px; }
.app[data-visual="open"] .avatar-mark span { width: 45px; height: 54px; }
.app[data-visual="open"] .avatar-placeholder strong { font-size: 16px; }
.app[data-visual="open"] .avatar-placeholder small { margin-top: 3px; font-size: 10px; }

.app[data-visual="open"] .avatar-frame {
  width: 168px;
  filter: drop-shadow(0 14px 30px rgba(21, 15, 29, 0.22));
}

.app[data-visual="open"] .avatar-ground { display: none; }

/* Below the push-aside threshold the transcript overlays the stage, and the
   control dock floats above everything — so the dock lands on top of the panel
   it is supposed to sit beneath. The panel gives way to the bar, and the
   suggestion chips stand down entirely: they are shortcuts for starting a
   conversation, not something needed while reading one back. */
@media (max-width: 1199px) {
  .app[data-transcript="open"] .suggestions { display: none; }

  .app[data-transcript="open"] .transcript {
    bottom: calc(var(--foot-h) + 84px);
  }
}

/* ---------- Transcript ---------- */

/* Docked, not floating. As a floating card it sat on top of Charlie, which
   read as something covering her rather than part of the page. It now takes a
   column of its own between the masthead and the footer, and the stage gives
   up the same width — so opening it rearranges the page instead of obscuring
   it. On small screens there is no width to give up, so it stays an overlay. */
.transcript {
  position: absolute;
  z-index: 30;
  top: 104px;
  right: 0;
  bottom: var(--foot-h);
  width: var(--transcript-w);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  overflow: hidden;
  border: 0;
  border-left: 1px solid var(--line);
  border-radius: 0;
  background: rgba(250, 248, 252, 0.97);
  box-shadow: -18px 0 50px rgba(23, 7, 71, 0.09);
  backdrop-filter: blur(24px);
  /* No fill mode: with `both` the panel holds the animation's opening offset
     whenever the animation does not actually run — a background tab, a paused
     compositor — leaving it parked 14px off the edge of the screen. Without
     it the element always settles at its real position. */
  animation: panel-in 0.25s ease;
}

/* Stepping aside only works if there is room to step into. The stage starts at
   49% of the width, so on anything under ~1200px the remainder is narrower than
   the panel itself and pushing squeezes Charlie into a sliver. Above that the
   page rearranges; below it the panel simply sits over the stage — still docked
   and full height, so it reads as a panel either way. */
@media (min-width: 1200px) {
  .app[data-transcript="open"] .stage-col { right: var(--transcript-w); }

  .app[data-transcript="open"] .controls {
    right: calc(var(--transcript-w) + clamp(20px, 3vw, 44px));
    width: min(920px, calc(100% - var(--transcript-w) - 64px));
  }

  .app[data-transcript="open"] .avatar-frame {
    width: min(34vw, 520px, calc((100svh - 182px - var(--foot-h)) * 1.185));
  }

  /* The panel's own right edge otherwise reaches to clamp(20px, 3vw, 44px),
     which is exactly where the transcript column sits when both are open.
     The corner tile moves left by the same amount, so its lane moves with
     it. */
  .app[data-visual="open"][data-transcript="open"] .visual-panel {
    right: calc(var(--transcript-w) + 168px + 18px);
  }
}

.transcript[hidden] { display: none; }

.transcript-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 17px 19px;
  border-bottom: 1px solid var(--line);
}

.transcript-head h2 {
  color: var(--blue);
  font-size: 16px;
}

.transcript-actions {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Hidden by default (see index.html) — a deployment with saving turned off
   at the operator level has nothing for this control to opt out of, so it
   never appears rather than showing a switch that does nothing. */
.save-toggle {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-right: 6px;
  padding-right: 10px;
  border-right: 1px solid var(--line);
  cursor: pointer;
  user-select: none;
}

.save-toggle[hidden] { display: none; }

.save-toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.save-toggle-track {
  position: relative;
  width: 30px;
  height: 17px;
  flex: none;
  border-radius: 999px;
  background: var(--line);
  transition: background 0.2s ease;
}

.save-toggle-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(37, 14, 98, 0.35);
  transition: transform 0.2s ease;
}

.save-toggle input:checked + .save-toggle-track {
  background: var(--purple);
}

.save-toggle input:checked + .save-toggle-track .save-toggle-thumb {
  transform: translateX(13px);
}

.save-toggle input:focus-visible + .save-toggle-track {
  outline: 2px solid var(--purple);
  outline-offset: 2px;
}

.save-toggle-text {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.3;
  max-width: 84px;
}

/* A panel that can be opened from the masthead should also be closable from
   itself: the toggle that opened it is across the page, and once the panel has
   the reader's attention that is the last place they look. */
.transcript-close {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--muted);
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease;
}

.transcript-close:hover {
  color: var(--blue);
  background: rgba(37, 14, 98, 0.07);
}

.text-button {
  padding: 4px;
  color: var(--purple);
  border: 0;
  background: transparent;
  font-size: 11px;
  font-weight: 750;
  cursor: pointer;
}

.transcript-body {
  min-height: 0;
  padding: 16px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.transcript-empty {
  padding: 34px 18px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
}

.msg { margin-bottom: 14px; }

.msg-role {
  display: block;
  margin: 0 0 4px 3px;
  color: var(--muted);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.msg-text {
  padding: 10px 12px;
  color: var(--ink);
  border-radius: 13px 13px 13px 4px;
  background: #eae6f0;
  font-size: 13px;
  line-height: 1.48;
  white-space: pre-wrap;
}

.msg-text.interrupted::after {
  content: "Interrupted";
  display: block;
  width: max-content;
  margin-top: 7px;
  color: var(--muted);
  font-size: 9px;
  font-style: italic;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.msg.user .msg-role { text-align: right; }

.msg.user .msg-text {
  color: #fff;
  border-radius: 13px 13px 4px 13px;
  background: var(--purple);
}

.msg.user .msg-text.pending {
  color: rgba(255, 255, 255, 0.72);
  font-style: italic;
}

/* ---------- Floating controls ---------- */

/* The dock is a right-aligned column: chips sit directly above the bar.
   Its box is far wider than the controls inside it and sits above the
   conversation card, so the empty space must let clicks through to the card
   beneath — otherwise it silently swallows them. */
.controls {
  pointer-events: none;
  position: absolute;
  z-index: 35;
  right: clamp(20px, 3vw, 44px);
  bottom: calc(var(--foot-h) + max(14px, env(safe-area-inset-bottom)));
  width: min(920px, calc(100% - 40px));
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

.control-bar {
  pointer-events: auto;
  width: 100%;
  padding: 7px;
  border: 1px solid rgba(255, 255, 255, 0.75);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.84);
  box-shadow: 0 18px 55px rgba(21, 13, 36, 0.2);
  backdrop-filter: blur(20px) saturate(1.2);
}

.control-inner {
  display: grid;
  grid-template-columns: auto minmax(180px, 1fr) auto;
  align-items: center;
  gap: 8px;
}

.mic-button,
.connect-button,
.send-button {
  border: 0;
  cursor: pointer;
}

.mic-button {
  position: relative;
  width: 48px;
  min-width: 48px;
  height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  overflow: hidden;
  color: #fff;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--pink), var(--purple));
  font-size: 12px;
  font-weight: 700;
  box-shadow: 0 8px 22px rgba(119, 52, 172, 0.2);
  transition: transform 0.2s ease, filter 0.2s ease, background 0.2s ease;
}

.mic-button:hover:not(:disabled) {
  transform: translateY(-1px);
  filter: brightness(1.07);
}

.mic-button[aria-pressed="false"]:not(:disabled) {
  color: var(--muted);
  background: #ece9f0;
  box-shadow: none;
}

/* The muted state is now carried entirely by the slash across the icon, so it
   is centred on the circle rather than offset against a vanished label. */
.mic-button[aria-pressed="false"]:not(:disabled)::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 26px;
  height: 2px;
  margin-left: -13px;
  border-radius: 99px;
  background: var(--pink);
  transform: rotate(-45deg);
}

/* The state still has to be announced; it just is not drawn. */
.mic-copy {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.mic-button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  filter: grayscale(0.5);
}

.mic-meter {
  position: absolute;
  right: 13px;
  bottom: 8px;
  left: 13px;
  height: 2px;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
}

.mic-meter i {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: currentColor;
  transition: width 60ms linear;
}

.text-entry {
  min-width: 0;
  height: 48px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 40px;
  align-items: center;
  overflow: hidden;
  border: 1px solid rgba(37, 14, 98, 0.12);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.82);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.text-entry:focus-within {
  border-color: rgba(119, 52, 172, 0.5);
  box-shadow: 0 0 0 4px rgba(119, 52, 172, 0.08);
}

.text-entry input {
  min-width: 0;
  height: 100%;
  padding: 0 8px 0 18px;
  color: var(--ink);
  border: 0;
  outline: 0;
  background: transparent;
  font-size: 14px;
}

.text-entry input::placeholder { color: #968fa4; }

.send-button {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  padding: 0;
  color: #fff;
  border-radius: 50%;
  background: var(--purple);
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.send-button:hover:not(:disabled) { transform: translateX(2px); }
.send-button:disabled { opacity: 0.28; cursor: not-allowed; }

.connect-button {
  width: 48px;
  min-width: 48px;
  height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: #fff;
  border-radius: 50%;
  background: var(--blue);
  font-size: 12px;
  font-weight: 750;
  box-shadow: 0 8px 22px rgba(37, 14, 98, 0.17);
  transition: transform 0.2s ease, background 0.2s ease;
}

/* The label used to say whether this starts or ends a conversation. With only
   the power icon left, colour has to carry that: calm blue to begin, pink to
   end a live session. */
.connect-button[data-connected="true"] {
  background: var(--pink);
  box-shadow: 0 8px 22px rgba(227, 28, 121, 0.26);
}

.connect-button:hover:not(:disabled) {
  transform: translateY(-1px);
  background: var(--purple);
}

.connect-button[data-connected="true"]:hover:not(:disabled) {
  background: #c4155f;
}

.connect-button:disabled { opacity: 0.55; cursor: wait; }

/* Idle shows a voice waveform — an invitation to speak. Once live, it becomes
   a power icon, because the only thing left to do with it is end the session. */
.connect-button .icon-power { display: none; }
.connect-button[data-connected="true"] .icon-voice { display: none; }
.connect-button[data-connected="true"] .icon-power { display: block; }

/* Hidden from view, still announced — the label tracks the session state. */
.connect-button span {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.toast {
  position: fixed;
  z-index: 100;
  right: 20px;
  bottom: 96px;
  max-width: min(390px, calc(100vw - 28px));
  padding: 12px 16px;
  color: #fff;
  border-radius: 12px;
  background: rgba(23, 7, 71, 0.94);
  box-shadow: 0 15px 40px rgba(23, 7, 71, 0.3);
  font-size: 12px;
  line-height: 1.45;
  animation: toast-in 0.24s ease both;
}

.toast[hidden] { display: none; }

/* ---------- Motion ---------- */

@keyframes pulse {
  50% { opacity: 0.42; transform: scale(0.8); }
}

@keyframes wave {
  from { height: 7px; }
  to { height: 19px; }
}

@keyframes avatar-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes panel-in {
  from { opacity: 0; transform: translateX(14px); }
  to { opacity: 1; transform: none; }
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ---------- Responsive ---------- */

@media (min-width: 1500px) {
  :root { --avatar-scale: 1.32; }
}

@media (max-width: 980px) {
  :root { --avatar-scale: 1.38; }

  .masthead { padding-inline: 30px; }

  .scene-kicker { left: 30px; }

  .scene-heading h1 {
    top: clamp(174px, 21vh, 214px);
    right: auto;
    left: 30px;
    width: 41%;
    font-size: clamp(54px, 7vw, 76px);
  }

  .scene-lede {
    top: clamp(310px, 35vh, 350px);
    left: 30px;
    width: 40%;
    font-size: 13px;
  }

  .avatar-frame {
    width: min(51vw, 610px, calc((100svh - 182px - var(--foot-h)) * 1.185));
  }

  .avatar-video video,
  .avatar-video canvas { transform: scale(var(--avatar-scale)); }

  .conversation-card {
    left: 30px;
    width: 41%;
  }

  .conversation-copy { font-size: 12px; }
  .disclosure { display: none; }
}

@media (max-width: 760px) {
  :root { --foot-h: 38px; --avatar-scale: 1.08; }

  /* No room for a panel and a presenter side by side: the panel takes the
     stage and Charlie waits behind it rather than being shrunk to a token. */
  .visual-panel {
    top: 84px;
    right: 10px;
    bottom: calc(var(--foot-h) + 96px);
    left: 10px;
  }

  /* The introduction strip would eat a third of a phone screen, so on small
     screens the panel simply takes over and the card stands down. */
  .app[data-visual="open"] .conversation-card { display: none; }
  /* No corner tile on a phone (below), so the panel keeps the full width; it
     still has to end above the chips, which crossed its bottom edge by a few
     pixels at 390x844. */
  .app[data-visual="open"] .visual-panel {
    top: 78px;
    right: 10px;
    bottom: calc(var(--foot-h) + 116px);
    left: 10px;
  }

  /* The desktop rule buys a framed page height by starting it just under the
     intro strip. There is no strip here, so it would only push the page down
     the screen. */
  .app[data-visual="open"] .visual-panel[data-mode="microsite"] { top: 78px; }

  .app[data-visual="open"] .stage-col { display: none; }

  /* Once she is live, the introduction has done its job and the phone screen
     is needed for her. "Meet Charlie" and the card below it are an invitation
     to start; a visitor mid-conversation has already accepted it, and on a
     phone the two together push her face down behind the control dock.
     They come back the moment the session ends. */
  .app[data-session="live"] .scene-heading,
  .app[data-session="live"] .conversation-card { display: none; }

  /* The studio takes the whole phone screen, in plain white. At rest this
     view is split horizontally — brand purple across the top 38%, a light
     studio wash below — because the top half is carrying the introduction.
     Once that is gone the split has nothing left to divide, and a band of
     empty purple above her head is just a smaller screen.

     White rather than the wash: the wash is a lavender-grey that deepens
     toward the floor (#f4f2f8 down to #d8d2e6), which reads as a lit backdrop
     behind a half-height figure but sits behind her head as a grey cast once
     she fills the screen. The faint grid below stays, and is what keeps the
     white reading as a room rather than as a blank page. */
  .app[data-session="live"] .atlas { background: #fff; }

  .app[data-session="live"] .scene-backdrop::before {
    inset: 0;
    background: #fff;
  }

  .app[data-session="live"] .scene-backdrop::after { inset: 0; }

  /* The masthead keeps its purple. Its wordmark and status pill are white,
     and white on a white studio is an invisible header — this is the same
     treatment the panel view already uses for the same reason. */
  .app[data-session="live"] .masthead { background: var(--blue); }

  /* With the introduction gone the stage is free, so she takes the room it
     was using rather than staying pinned where it left her. Keep the frame
     bounded by both viewport dimensions: height-only sizing made Charlie more
     than 130% of the screen width on tall phones and cropped most of her body. */
  .app[data-session="live"] .stage-col {
    inset: 76px 0 calc(var(--foot-h) + 84px);
    align-items: center;
  }

  .app[data-session="live"] .avatar-frame {
    flex: none;
    width: min(
      96vw,
      620px,
      calc((100svh - 76px - var(--foot-h) - 84px) * 1.185)
    );
  }

  /* Nothing to step aside on a phone: the panel covers the stage instead. */
  .app[data-transcript="open"] .stage-col,
  .app[data-transcript="open"] .controls { right: 0; }

  .app[data-transcript="open"] .avatar-frame { width: min(96vw, 620px); }

  .site-foot { font-size: 9px; }
  .site-foot span:last-child { display: none; }

  .app { min-height: 560px; }

  .masthead {
    top: 0;
    right: 0;
    left: 0;
    height: 70px;
    padding: 14px 16px;
  }

  .brand svg { width: 88px; }
  .brand-rule,
  .brand-name { display: none; }

  .status-pill {
    min-height: 34px;
    max-width: 128px;
    padding: 0 11px;
    font-size: 9px;
  }

  .transcript-toggle {
    width: 36px;
    height: 36px;
  }

  .scene-kicker {
    top: 82px;
    left: 20px;
    font-size: 8px;
  }

  .scene-heading h1 {
    top: 104px;
    right: auto;
    left: 20px;
    display: block;
    width: calc(100% - 40px);
    font-size: clamp(42px, 12vw, 64px);
    line-height: 0.9;
  }

  .scene-heading h1 span { display: inline; }
  .scene-heading h1 span:last-child {
    margin-left: 0.16em;
    text-align: left;
  }

  .scene-lede {
    top: 162px;
    left: 20px;
    width: calc(100% - 40px);
    max-width: 470px;
    font-size: 12px;
    line-height: 1.4;
  }

  .atlas {
    background: linear-gradient(180deg, var(--blue) 0 38%, #e9e6ef 38% 100%);
  }

  .scene-backdrop::before,
  .scene-backdrop::after {
    inset: 38% 0 0;
  }

  .stage-col {
    inset: 196px 0 118px;
    justify-content: center;
  }

  .avatar-frame {
    width: min(96vw, 620px, calc((100svh - 314px - var(--foot-h)) * 1.185));
  }

  .avatar-video video,
  .avatar-video canvas { transform: scale(var(--avatar-scale)); }

  .avatar-ground {
    left: 50%;
    width: 76vw;
  }

  /* The card moves up into the empty half of the purple panel. Anchored to the
     bottom it competed with the chips and the dock for the few pixels above
     the footer, while a third of the screen sat unused under the lede. */
  .conversation-card {
    top: clamp(190px, 27vh, 260px);
    right: 12px;
    bottom: auto;
    left: 12px;
    width: auto;
    padding: 13px 15px;
    /* Opaque rather than tinted: at this position the card crosses the seam
       between the purple panel and the light stage, so it cannot borrow
       contrast from whatever happens to sit behind it. */
    border: 1px solid rgba(255, 255, 255, 0.9);
    border-radius: 17px;
    background: #fff;
    box-shadow: 0 14px 38px rgba(23, 7, 71, 0.18);
    backdrop-filter: none;
  }

  .conversation-intro { gap: 9px; }
  .ai-mark {
    width: 35px;
    height: 35px;
    color: #fff;
    border-radius: 10px;
    background: var(--purple);
  }
  .eyebrow { color: var(--purple); }
  .conversation-card h2 { color: var(--blue); font-size: 19px; }
  .conversation-copy { margin-top: 7px; font-size: 11px; }
  .conversation-copy { color: #514a5e; }
  .conversation-scope {
    margin-top: 6px;
    max-height: 5.6em;
    color: #514a5e;
    font-size: 12px;
  }
  .atlas-link {
    color: var(--blue);
    border-color: rgba(37, 14, 98, 0.18);
    background: rgba(255, 255, 255, 0.8);
  }

  .atlas-link:hover {
    color: var(--pink);
    background: #fff;
  }
  .suggestions {
    flex-wrap: nowrap;
    justify-content: flex-start;
    padding-right: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .suggestions::-webkit-scrollbar { display: none; }
  .chip { flex: 0 0 auto; min-height: 27px; }

  .transcript {
    top: 72px;
    right: 10px;
    bottom: calc(var(--foot-h) + 118px);
    left: 10px;
    width: auto;
  }

  .controls {
    right: 10px;
    bottom: calc(var(--foot-h) + max(8px, env(safe-area-inset-bottom)));
    width: calc(100% - 20px);
    gap: 8px;
  }

  .control-bar { padding: 5px; }

  .control-inner {
    grid-template-columns: 44px minmax(0, 1fr) 44px;
    gap: 6px;
  }

  .mic-button,
  .connect-button {
    width: 44px;
    min-width: 44px;
    height: 44px;
    padding: 0;
    border-radius: 50%;
  }

  .text-entry { height: 44px; }
}

@media (max-width: 420px) {
  :root { --avatar-scale: 1.08; }
  .scene-heading h1 {
    top: 105px;
    font-size: clamp(39px, 11.5vw, 52px);
  }

  .scene-lede { top: 154px; }

  .conversation-copy { display: none; }

  /* The invitation stands down on the narrowest screens; the boundary does
     not, because it is the only place a visitor is told what Charlie is for. */
  .conversation-scope { max-height: 4.6em; font-size: 12px; }

  .conversation-card {
    padding: 9px 11px 10px;
  }

  .conversation-card h2 { font-size: 16px; }

  .text-entry input {
    padding-left: 12px;
    font-size: 16px;
  }

  .toast {
    right: 10px;
    bottom: calc(var(--foot-h) + 72px);
    left: 10px;
    max-width: none;
  }
}

@media (max-height: 600px) and (orientation: landscape) {
  :root { --foot-h: 32px; --avatar-scale: 1.05; }

  .site-foot { font-size: 9px; }

  .app { min-height: 320px; }

  .masthead {
    top: 0;
    right: 0;
    left: 0;
    height: 54px;
    padding: 10px 20px;
  }

  .brand svg { width: 72px; }
  .status-pill { min-height: 30px; }
  .transcript-toggle { width: 32px; height: 32px; }
  .scene-kicker {
    top: 74px;
    left: 24px;
  }

  .scene-heading h1 {
    top: 96px;
    right: auto;
    left: 24px;
    display: block;
    width: 39%;
    font-size: clamp(40px, 6vw, 64px);
  }

  .scene-heading h1 span { display: block; }
  .scene-lede { display: none; }

  .atlas {
    background: linear-gradient(90deg, var(--blue) 0 49%, #e9e6ef 49% 100%);
  }

  .scene-backdrop::before,
  .scene-backdrop::after { inset: 0 0 0 49%; }

  .stage-col { inset: 54px 0 55px 49%; }
  .avatar-frame {
    width: min(49vw, 540px, calc((100svh - 109px - var(--foot-h)) * 1.185));
  }

  .avatar-video video,
  .avatar-video canvas { transform: scale(var(--avatar-scale)); }

  .avatar-ground { left: 50%; }

  .conversation-card {
    right: auto;
    bottom: calc(var(--foot-h) + 104px);
    left: 16px;
    width: min(330px, 42vw);
    padding: 9px 0 0;
    border: 0;
    border-top: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    backdrop-filter: none;
  }

  .conversation-copy,
  .disclosure { display: none; }

  .conversation-scope {
    max-height: 4.2em;
    color: rgba(255, 255, 255, 0.72);
    font-size: 12px;
  }

  .conversation-card h2 { font-size: 17px; }
  .conversation-card h2 { color: #fff; }
  .eyebrow { color: #c8c3d8; }
  .ai-mark { color: var(--blue); background: #fff; }
  .chip { min-height: 25px; font-size: 8px; }

  .controls {
    bottom: calc(var(--foot-h) + 6px);
    width: min(720px, calc(100% - 24px));
    gap: 6px;
  }

  .control-bar { padding: 4px; }

  .mic-button,
  .connect-button,
  .text-entry { height: 40px; }

  .mic-button,
  .connect-button { width: 40px; min-width: 40px; }

  .transcript {
    top: 60px;
    bottom: calc(var(--foot-h) + 56px);
  }

  /* A short landscape window has a shorter masthead and a tighter control
     dock than the desktop offsets assume, so the panel starts higher and
     ends just above the chips instead of reserving desktop-sized margins it
     cannot afford — at 844x390 those margins left it 122px tall. The corner
     tile rides at the same bottom edge, in the lane already reserved for it. */
  .app[data-visual="open"] .visual-panel {
    top: 60px;
    bottom: calc(var(--foot-h) + 92px);
  }

  .app[data-visual="open"] .stage-col { bottom: calc(var(--foot-h) + 92px); }
}

@media (pointer: coarse) {
  .chip:hover:not(:disabled),
  .transcript-toggle:hover,
  .mic-button:hover:not(:disabled),
  .connect-button:hover:not(:disabled) {
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
