/* KAI — the whole stylesheet. Values are recorded in DESIGN.md, and the two
   files change in the same commit. */

@font-face {
  font-family: "Archivo";
  src: url("/static/fonts/archivo-1.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: optional;
}

@font-face {
  font-family: "Azeret Mono";
  src: url("/static/fonts/azeret-mono-1.woff2") format("woff2");
  font-weight: 400 600;
  font-style: normal;
  font-display: optional;
}

@layer reset, tokens, base, layout, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box }

  body, h1, h2, h3, p, ol, ul, dl, dd, figure {
    margin: 0;
  }

  ol[role="list"] { list-style: none; padding: 0 }
}

@layer tokens {
  :root {
    /* KAI is an instrument panel: it is dark in every scheme, so there is
       no light theme to fork to and no toggle to build. */
    color-scheme: dark;

    /* Grounds. Two per screen: the app background and one raised surface —
       plus the input well, which needs to read as a place to type. */
    --color-bg: #05070A;
    --color-surface: #0F151C;
    --color-raised: #151D26;

    /* Text. */
    --color-text: #C3CED7;
    --color-text-strong: #EAF2F6;
    --color-text-muted: #8695A3;

    /* The two voices. Mint is KAI, amber is you. This mapping is absolute:
       mint never marks what you said, amber never marks a reply. */
    --color-accent: #5FF0C2;
    --color-user: #FFB33F;

    /* The other two states a turn can be in. */
    --color-thinking: #7EA6FF;
    --color-error: #FF5C63;

    /* Boundaries. border is load-bearing and measured; hairline and
       accent-line are decoration and carry no information. */
    --color-border: #687684;
    --color-hairline: rgba(147, 163, 177, 0.14);
    --color-accent-line: rgba(95, 240, 194, 0.28);
    --color-on-accent: #04120D;

    /* Type. */
    --font-body: "Archivo", system-ui, sans-serif;
    --font-mono: "Azeret Mono", ui-monospace, monospace;

    /* Shape. Four radii, because four kinds of thing are rounded differently:
       controls, panels, bubbles, and anything circular. */
    --radius: 0.625rem;
    --radius-panel: 1rem;
    --radius-bubble: 1.25rem;
    --radius-pill: 999px;

    /* Space. */
    --space: clamp(1rem, 0.5rem + 2vw, 1.5rem);
    --space-xs: calc(var(--space) / 4);
    --space-sm: calc(var(--space) / 2);
    --space-lg: calc(var(--space) * 2);

    /* Widths. The rail is the phone, and it stays the phone on a desktop. */
    --rail: 430px;
    --measure: 34ch;

    /* Motion. Two speeds, plus the one ambient cycle the orb breathes at. */
    --motion-fast: 160ms;
    --motion-slow: 260ms;
    --motion-ambient: 2400ms;

    /* Set by voice.js from the microphone, 0 to 1. It is declared here so the
       orb has a resting size before any script runs. */
    --level: 0;
  }
}

@layer base {
  html {
    font-family: var(--font-body);
    line-height: 1.5;
    /* No font-size: the browser default is the size the reader chose. */
  }

  body {
    background-color: var(--color-bg);
    color: var(--color-text);
    -webkit-font-smoothing: antialiased;
    /* A long unbroken string — a URL read out of a transcript — must wrap
       rather than scroll the page sideways. */
    overflow-wrap: anywhere;
  }

  img { max-inline-size: 100%; block-size: auto }
  input, select, textarea { max-inline-size: 100%; font: inherit }
  button { font: inherit }
  fieldset { min-inline-size: 0 }

  h1, h2 { line-height: 1.2; text-wrap: balance }

  a {
    color: var(--color-accent);
    text-decoration: none;
  }

  code {
    font-family: var(--font-mono);
    font-size: 0.9375em;
  }

  :focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  ::selection {
    background: var(--color-accent-line);
    color: var(--color-text-strong);
  }

  @media (prefers-reduced-motion: no-preference) {
    a, button, input {
      transition:
        color var(--motion-fast) ease-out,
        background-color var(--motion-fast) ease-out,
        border-color var(--motion-fast) ease-out,
        box-shadow var(--motion-fast) ease-out;
    }
  }
}

@layer layout {
  body {
    min-block-size: 100dvh;
    display: grid;
    /* The implicit column has an auto minimum, which a wide code block would
       push past the viewport. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    /* The HUD grid fills the whole surface; the rail floats on top of it. */
    background-image:
      linear-gradient(rgba(95, 240, 194, 0.06) 1px, transparent 1px),
      linear-gradient(90deg, rgba(95, 240, 194, 0.06) 1px, transparent 1px);
    background-size: 32px 32px;
  }

  .chrome, main > *, .dock {
    inline-size: min(100% - 2 * var(--space), var(--rail));
    margin-inline: auto;
  }

  .chrome {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-block-size: 3.5rem;
    padding-block-start: env(safe-area-inset-top);
  }

  main {
    display: grid;
    align-content: center;
    justify-items: center;
  }

  footer {
    position: sticky;
    inset-block-end: 0;
    background: rgba(10, 14, 19, 0.82);
    -webkit-backdrop-filter: blur(18px) saturate(1.2);
    backdrop-filter: blur(18px) saturate(1.2);
    border-block-start: 1px solid var(--color-hairline);
    box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.6);
  }

  footer:empty {
    background: none;
    border: 0;
    box-shadow: none;
  }

  .dock {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding-block: var(--space-sm);
    /* Without the inset the phone's home indicator sits on the microphone. */
    padding-block-end: calc(var(--space-sm) + env(safe-area-inset-bottom));
  }
}

@layer components {
  /* ---- HUD labels: mono, uppercase, widely tracked. The strongest brand
     signal in the system, so every caption gets it — not just some. ---- */
  .hud {
    font-family: var(--font-mono);
    font-size: 0.75em;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  /* ---- Header ---- */
  .mark {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--color-text-strong);
  }

  .mark .icon { color: var(--color-accent) }

  /* Three tracked words on a 430px rail is a tight fit: the mark holds its
     size, the session label gives way first, and the tracking eases off. */
  .mark { flex-shrink: 0 }

  .chrome .hud {
    margin-inline-start: auto;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    letter-spacing: 0.12em;
  }

  .chrome-action {
    font-family: var(--font-mono);
    font-size: 0.75em;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    flex-shrink: 0;
    min-block-size: 2.75rem;
    padding-inline: var(--space-xs);
    letter-spacing: 0.12em;
  }

  .chrome-action:hover { color: var(--color-accent) }

  /* ---- The stage: orb, state, last exchange ---- */
  .stage {
    display: grid;
    justify-items: center;
    gap: var(--space);
    padding-block: var(--space);
    /* The colour every part of the orb reads. At rest it is the mint voice
       mixed down toward the ground: awake, but plainly not listening. The
       state is never carried by colour alone — the dot and the word say it. */
    --orb: color-mix(in oklab, var(--color-accent), var(--color-bg) 55%);
  }

  .stage[data-state="listening"] { --orb: var(--color-user) }
  .stage[data-state="thinking"] { --orb: var(--color-thinking) }
  .stage[data-state="speaking"] { --orb: var(--color-accent) }
  .stage[data-state="error"] { --orb: var(--color-error) }

  /* A request in flight is KAI thinking, whether it was started by the
     microphone or by the text field. */
  body.htmx-request .stage { --orb: var(--color-thinking) }

  .orb {
    position: relative;
    inline-size: clamp(10rem, 46vw, 13.5rem);
    aspect-ratio: 1;
    display: grid;
    place-items: center;
  }

  .orb-halo,
  .orb-ring,
  .orb-sweep {
    position: absolute;
    border-radius: 50%;
  }

  .orb-halo {
    inset: -18%;
    background: radial-gradient(circle at 50% 50%,
      rgba(95, 240, 194, 0.22), rgba(95, 240, 194, 0) 68%);
    opacity: 0.35;
  }

  .orb-ring { border: 1px solid var(--color-hairline) }
  .orb-ring-1 { inset: 0; border-color: var(--orb) }
  .stage[data-state="idle"] .orb-ring-1 { border-color: var(--color-border) }
  .orb-ring-2 { inset: 9%; opacity: 0.75 }
  .orb-ring-3 { inset: 18%; opacity: 0.5 }

  .orb-core {
    inline-size: 46%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(circle at 38% 32%,
      var(--orb), rgba(5, 7, 10, 0.9) 72%);
    /* The one thing the microphone drives directly. */
    transform: scale(calc(0.92 + min(var(--level), 1) * 0.16));
  }

  .orb-sweep {
    inset: 0;
    overflow: hidden;
    pointer-events: none;
  }

  .orb-sweep::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    block-size: 22%;
    background: linear-gradient(rgba(95, 240, 194, 0),
      rgba(95, 240, 194, 0.12), rgba(95, 240, 194, 0));
    translate: 0 -100%;
  }

  .state {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    color: var(--orb);
  }

  .state-dot {
    inline-size: 0.375rem;
    block-size: 0.375rem;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 8px currentColor;
  }

  /* ---- The last exchange, under the orb ---- */
  .exchange {
    display: grid;
    gap: var(--space-xs);
    max-inline-size: var(--measure);
    text-align: center;
  }

  .greeting {
    font-size: clamp(1.75rem, 1.2rem + 2.4vw, 2.5rem);
    font-weight: 700;
    line-height: 1.08;
    letter-spacing: -0.02em;
    color: var(--color-text-strong);
    text-wrap: balance;
  }

  .prompt { color: var(--color-text-muted) }

  .said-user {
    color: var(--color-user);
    font-size: 1.0625rem;
  }

  .said-agent {
    color: var(--color-text-strong);
    font-size: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
    line-height: 1.35;
    text-wrap: balance;
  }

  .notice { color: var(--color-error) }

  .playback {
    inline-size: 100%;
    max-inline-size: var(--measure);
  }

  /* voice.js speaks every reply it can, so the bar is the fallback for a
     reader with no script — it hides itself once the answer has been heard. */
  .playback[data-spoken] { display: none }

  /* ---- Composer ---- */
  .composer {
    flex: 1;
    min-inline-size: 0;
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-block-size: 3rem;
    padding-inline: var(--space-sm) var(--space-xs);
    background: var(--color-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-bubble);
  }

  .composer:focus-within { border-color: var(--color-accent) }

  .composer input {
    flex: 1;
    min-inline-size: 0;
    background: none;
    border: 0;
    outline: none;
    color: var(--color-text-strong);
    font-size: 1.0625rem;
  }

  .composer input::placeholder { color: var(--color-text-muted) }

  .send {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border: 0;
    border-radius: var(--radius-pill);
    background: none;
    color: var(--color-accent);
    cursor: pointer;
  }

  .send:hover { filter: brightness(1.12) }
  .send:active { scale: 0.97 }

  /* ---- Microphone ---- */
  .mic {
    position: relative;
    display: grid;
    place-items: center;
    inline-size: 3.5rem;
    block-size: 3.5rem;
    flex-shrink: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    cursor: pointer;
    /* The face colour and the fill, per state. */
    --mic-face: var(--color-accent);
    --mic-fill: var(--color-raised);
    --mic-edge: var(--color-accent-line);
  }

  .mic[disabled] {
    opacity: 0.4;
    cursor: not-allowed;
  }

  .mic[data-state="listening"] {
    --mic-face: var(--color-on-accent);
    --mic-fill: var(--color-user);
    --mic-edge: var(--color-user);
  }

  .mic[data-state="speaking"] {
    --mic-face: var(--color-on-accent);
    --mic-fill: var(--color-accent);
    --mic-edge: var(--color-accent);
  }

  .mic-face {
    position: relative;
    display: grid;
    place-items: center;
    inline-size: calc(100% - 0.5rem);
    block-size: calc(100% - 0.5rem);
    border-radius: 50%;
    background: var(--mic-fill);
    color: var(--mic-face);
    font-size: 1.25rem;
  }

  .mic-ring {
    position: absolute;
    inset: 0;
    border: 1px solid var(--mic-edge);
    border-radius: 50%;
    opacity: 0;
  }

  .mic:hover .mic-face { filter: brightness(1.12) }
  .mic:active { scale: 0.97 }

  /* One glyph at a time; the button's state decides which. */
  .mic .icon { display: none }
  .mic:not([data-state]) .icon-mic,
  .mic[data-state="idle"] .icon-mic,
  .mic[data-state="thinking"] .icon-mic { display: inline-block }
  .mic[data-state="listening"] .icon-stop { display: inline-block }
  .mic[data-state="speaking"] .icon-lines { display: inline-block }

  /* ---- Transcript ---- */
  .transcript {
    display: grid;
    gap: var(--space);
    padding-block: var(--space) var(--space-lg);
    justify-items: stretch;
  }

  .page-title { color: var(--color-text-muted) }

  .thread {
    display: grid;
    gap: var(--space);
    margin: 0;
  }

  .turn {
    display: grid;
    gap: 0.375rem;
    justify-items: start;
  }

  .turn-user { justify-items: end }

  .bubble {
    max-inline-size: 86%;
    padding: var(--space-sm) var(--space);
    font-size: 1.0625rem;
    border: 1px solid var(--color-hairline);
    border-radius: var(--radius-bubble);
  }

  /* The one asymmetric radius in the system: the corner nearest the speaker
     is cut, so a glance tells you who is talking. */
  .turn-agent .bubble {
    border-end-start-radius: 0.375rem;
    border-color: var(--color-accent-line);
    background: linear-gradient(180deg,
      rgba(95, 240, 194, 0.09), rgba(95, 240, 194, 0.04));
    color: var(--color-text);
  }

  .turn-user .bubble {
    border-end-end-radius: 0.375rem;
    background: var(--color-raised);
    color: var(--color-text-strong);
  }

  .meta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding-inline: 0.25rem;
  }

  .replay {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 2.75rem;
    min-block-size: 2.75rem;
    color: var(--color-text-muted);
  }

  .replay:hover { color: var(--color-accent) }

  /* ---- Panel: the instrument frame ---- */
  .panel {
    position: relative;
    display: grid;
    gap: var(--space-sm);
    padding: var(--space);
    background-color: var(--color-surface);
    border: 1px solid var(--color-hairline);
    border-radius: var(--radius-panel);
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset,
      0 8px 24px rgba(0, 0, 0, 0.55);
  }

  /* Four corner registration ticks — the motif that makes a surface read as
     an instrument rather than a card. Eight hairline gradients rather than
     pseudo-elements, because two pseudo-elements can only reach two corners. */
  .panel {
    background-image:
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0),
      linear-gradient(var(--color-accent-line) 0 0);
    background-repeat: no-repeat;
    background-size:
      9px 1px, 1px 9px,
      9px 1px, 1px 9px,
      9px 1px, 1px 9px,
      9px 1px, 1px 9px;
    background-position:
      left 6px top 6px, left 6px top 6px,
      right 6px top 6px, right 6px top 6px,
      left 6px bottom 6px, left 6px bottom 6px,
      right 6px bottom 6px, right 6px bottom 6px;
  }

  .panel-label { color: var(--color-accent) }

  /* The label sits above its value rather than beside it: a rail is 430px and
     an endpoint is 25 characters of mono, so side by side the port — the part
     worth reading — is the part that gets cut off. */
  .endpoints {
    display: grid;
    gap: var(--space-xs);
    margin: 0;
  }

  .endpoints dt { margin-block-start: var(--space-xs) }
  .endpoints dt:first-of-type { margin-block-start: 0 }

  .endpoints dd {
    margin: 0;
    color: var(--color-text-strong);
    /* An endpoint is the one place numerals belong, and a port split across
       two lines is not an address any more — so it scrolls instead. */
    overflow-x: auto;
  }

  .endpoints dd code {
    overflow-wrap: normal;
    white-space: nowrap;
  }

  .secondary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    min-block-size: 2.75rem;
    padding-inline: var(--space);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: none;
    color: var(--color-text);
    cursor: pointer;
  }

  .secondary:hover { filter: brightness(1.12) }
  .secondary:active { scale: 0.97 }

  .back {
    justify-self: start;
    margin-block-start: var(--space);
  }

  /* ---- Icons: one rule, painted with currentColor ---- */
  .icon {
    display: inline-block;
    inline-size: 1em;
    block-size: 1em;
    flex-shrink: 0;
    vertical-align: -0.125em;
    background-color: currentColor;
    mask: var(--icon) center / contain no-repeat;
  }

  /* Forced colours replaces author background colours with system ones, so
     currentColor never survives to paint the shape. A system keyword does. */
  @media (forced-colors: active) {
    .icon { background-color: CanvasText }
  }

  .icon-mic { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 19v3'/%3E%3Cpath d='M19 10v2a7 7 0 0 1-14 0v-2'/%3E%3Crect x='9' y='2' width='6' height='13' rx='3'/%3E%3C/svg%3E") }
  .icon-stop { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='18' x='3' y='3' rx='2'/%3E%3C/svg%3E") }
  .icon-lines { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 10v3'/%3E%3Cpath d='M6 6v11'/%3E%3Cpath d='M10 3v18'/%3E%3Cpath d='M14 8v7'/%3E%3Cpath d='M18 5v13'/%3E%3Cpath d='M22 10v3'/%3E%3C/svg%3E") }
  .icon-send { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z'/%3E%3Cpath d='M6 12h16'/%3E%3C/svg%3E") }
  .icon-waypoints { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m10.586 5.414-5.172 5.172'/%3E%3Cpath d='m18.586 13.414-5.172 5.172'/%3E%3Cpath d='M6 12h12'/%3E%3Ccircle cx='12' cy='20' r='2'/%3E%3Ccircle cx='12' cy='4' r='2'/%3E%3Ccircle cx='20' cy='12' r='2'/%3E%3Ccircle cx='4' cy='12' r='2'/%3E%3C/svg%3E") }
  .icon-back { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 18-6-6 6-6'/%3E%3C/svg%3E") }
  .icon-volume { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z'/%3E%3Cpath d='M16 9a5 5 0 0 1 0 6'/%3E%3Cpath d='M19.364 18.364a9 9 0 0 0 0-12.728'/%3E%3C/svg%3E") }
  .icon-refresh { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8'/%3E%3Cpath d='M21 3v5h-5'/%3E%3Cpath d='M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16'/%3E%3Cpath d='M8 16H3v5'/%3E%3C/svg%3E") }

  /* ---- Waiting ---- */
  .htmx-indicator { opacity: 0 }

  .htmx-request .htmx-indicator,
  .htmx-request.htmx-indicator { opacity: 1 }

  @media (prefers-reduced-motion: no-preference) {
    .htmx-indicator { transition: opacity var(--motion-fast) ease-out }

    .htmx-request .htmx-indicator,
    .htmx-request.htmx-indicator { transition-delay: 100ms }
  }

  /* ---- Ambient motion ----
     These loops run only while KAI is listening, thinking, or speaking:
     they are the status indicator, and they stop when it does. Under reduced
     motion nothing moves and every state is still legible from colour, the
     glyph, and the words. */
  @media (prefers-reduced-motion: no-preference) {
    .orb-core {
      transition: transform var(--motion-fast) ease-out,
        box-shadow var(--motion-slow) ease-out;
    }

    .orb-halo,
    .orb-ring {
      transition: opacity var(--motion-slow) ease-out,
        border-color var(--motion-slow) ease-out;
    }

    .stage:not([data-state="idle"]) .orb-halo { opacity: 1 }

    .stage:not([data-state="idle"]) .orb-ring-1 {
      animation: breathe var(--motion-ambient) ease-in-out infinite;
    }

    .stage:not([data-state="idle"]) .orb-ring-2 {
      animation: breathe calc(var(--motion-ambient) + 600ms) ease-in-out infinite;
    }

    .stage:not([data-state="idle"]) .orb-ring-3 {
      animation: breathe calc(var(--motion-ambient) + 1200ms) ease-in-out infinite;
    }

    .stage:not([data-state="idle"]) .orb-sweep::after {
      animation: sweep 3.2s linear infinite;
    }

    .stage[data-state="speaking"] .orb-core {
      box-shadow: 0 0 48px rgba(95, 240, 194, 0.5),
        0 0 120px rgba(95, 240, 194, 0.22);
    }

    .stage:not([data-state="idle"]) .state-dot,
    body.htmx-request .state-dot {
      animation: pulse var(--motion-ambient) ease-in-out infinite;
    }

    .mic[data-state="listening"] .mic-ring,
    .mic[data-state="speaking"] .mic-ring {
      animation: ring var(--motion-ambient) ease-out infinite;
    }

    .mic-ring-2 { animation-delay: 1.2s }
  }

  @keyframes breathe {
    0%, 100% { transform: scale(1); opacity: 0.85 }
    50% { transform: scale(1.06); opacity: 1 }
  }

  @keyframes sweep {
    from { translate: 0 -100% }
    to { translate: 0 400% }
  }

  @keyframes pulse {
    0%, 100% { opacity: 1 }
    50% { opacity: 0.35 }
  }

  @keyframes ring {
    from { transform: scale(0.85); opacity: 0.55 }
    to { transform: scale(1.7); opacity: 0 }
  }
}

@layer utilities {
  .visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* The view-transition fades come from the browser's own stylesheet, so a
     media query cannot wrap them — only cancel them. */
  @media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
      animation: none !important;
    }
  }
}
