/* Same palette as Say to Paste: green is the resting colour, red only while the mic
   is open, amber only while a cold mic is still opening. One addition — the second
   speaker needs a colour of their own, so the two sides of the conversation can be
   told apart at a glance from across a table. */
:root {
  --bg: #ffffff;
  --fg: #111b21;
  --muted: #667781;
  --faint: #9aa6ad;
  --line: #e3e8ea;
  --surface: #ffffff;
  --sunken: #f1f3f2;
  --green: #00a884;
  --green-press: #008f72;
  --green-soft: #25d366;
  --green-tint: rgba(37, 211, 102, 0.14);
  --indigo: #5b7cf0;
  --indigo-tint: rgba(91, 124, 240, 0.12);
  /* The mic being open is NOT an alarm. Red reads as "recording" and makes a
     stranger across the table wary, which is the opposite of what this app is for:
     listening is a lighter, livelier green than resting. --rec stays, but only for
     things that genuinely went wrong. */
  --live: #25d366;
  --live-press: #1fb85a;
  --live-tint: rgba(37, 211, 102, 0.26);
  --rec: #f15c6d;
  --rec-press: #d94456;
  --rec-tint: rgba(241, 92, 109, 0.2);
  --wait: #f5a524;
  --wait-tint: rgba(245, 165, 36, 0.28);
  /* Driven by the A−/A+ control. The defaults are already generous: the person
     reading the translation is usually a stranger, at arm's length or further. */
  --scale: 1;
  /* Light, always. The dark palette this used to carry was switched on by the
     phone, not by us, and an Android in night mode turned the whole screen
     black — on a screen whose entire job is being read by a stranger across a
     table, that is not a decision to leave to the operating system. */
  color-scheme: light;
}

* { box-sizing: border-box; }

html {
  overflow-x: hidden;
  overflow-x: clip;
}

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

/* The first screen, and the whole app: brand, the two languages, the conversation,
   and the microphone on a bar at the bottom. Twelve pixels short of a full screen on
   purpose — the sliver of the settings strip peeking underneath is the only hint
   anybody needs that there is something below. */
.app {
  box-sizing: border-box;
  /* A full screen. No arithmetic to leave a sliver of the next thing showing:
     Safari and Chrome disagree about how tall the viewport is — on iOS the answer
     depends on whether the toolbars happen to be out — and a few pixels of
     disagreement put the settings on screen. The strip below is simply transparent
     until the page is scrolled, so being wrong about the height costs nothing. */
  min-height: 100vh;   /* anything older */
  min-height: 100svh;  /* the viewport with the toolbars showing */
  min-height: 100dvh;  /* and, where supported, the live one */
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: env(safe-area-inset-top) 14px 0;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
  border: 0;
  background: none;
  touch-action: manipulation;
}

/* ---------- header ---------- */

.top {
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0 0;
}

[data-state="loading"] .top { visibility: hidden; }


.brand {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -0.035em;
  white-space: nowrap;
  min-width: 0;
}

.brand > span:last-child { overflow: hidden; text-overflow: ellipsis; }
.brand em { font-style: normal; color: var(--green); }

.brand-mark {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--green);
  color: #fff;
  flex-shrink: 0;
}

.brand-mark svg { width: 100%; height: 100%; }

/* The set-once controls, down in the footer: same pill as the language pair, quieter. */
.pill-pick { position: relative; flex-shrink: 1; min-width: 0; }

.pill {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  min-height: 38px;
  padding: 0 8px 0 11px;
  border-radius: 999px;
  background: var(--surface);
  border: 1.5px solid var(--line);
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
  transition: border-color 0.15s, box-shadow 0.15s, color 0.15s;
}

.pill-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (hover: hover) { .pill:hover { border-color: var(--green-soft); color: var(--fg); } }

.pill:focus-visible, .pill[aria-expanded="true"] {
  outline: none;
  border-color: var(--green);
  color: var(--fg);
  box-shadow: 0 0 0 3px var(--green-tint);
}

.pill-icon { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; color: var(--green); flex-shrink: 0; }
.chev { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; color: var(--muted); flex-shrink: 0; transition: transform 0.2s; }
[aria-expanded="true"] .chev { transform: rotate(180deg); }

/* ---------- the shared language listbox ---------- */

.lang-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  width: max-content;
  min-width: 100%;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--surface);
  border: 1.5px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.28);
  animation: menu-in 0.14s ease-out;
  outline: none;
}

.chip-menu { left: 0; transform-origin: top left; }
.pair .chip-pick:last-of-type .chip-menu { left: auto; right: 0; transform-origin: top right; }

/* Footer menus open upwards: there is nothing below them but the edge of the screen. */
.lang-menu.up { top: auto; bottom: calc(100% + 6px); animation-name: menu-up; }
/* Each menu hangs from its own side, so neither opens off the edge of the screen. */
#formality-menu.up { left: 0; transform-origin: bottom left; }
#ui-lang-menu.up { left: auto; right: 0; transform-origin: bottom right; }

@keyframes menu-up { from { opacity: 0; transform: scale(0.96) translateY(4px); } }

.lang-menu[hidden] { display: none; }

@keyframes menu-in { from { opacity: 0; transform: scale(0.96) translateY(-4px); } }

.lang-menu li {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 0 12px;
  border-radius: 12px;
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  outline: none;
}

.lang-menu li:hover, .lang-menu li:focus-visible { background: var(--green-tint); }
.lang-menu li[aria-disabled="true"] { opacity: 0.38; cursor: default; }
.lang-menu li[aria-disabled="true"]:hover { background: none; }

.lang-menu .code { flex-shrink: 0; width: 26px; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; color: var(--muted); }
.lang-menu .name { flex: 1; padding-right: 12px; }
.lang-menu .check { flex-shrink: 0; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; color: var(--green); visibility: hidden; }
.lang-menu [aria-selected="true"] { font-weight: 700; }
.lang-menu [aria-selected="true"] .check { visibility: visible; }

/* ---------- state visibility ---------- */

.login, .pair, .log, .bar, #settings { display: none; }
[data-state="login"] .login { display: flex; }
body:not([data-state="login"]):not([data-state="loading"]) .pair,
body:not([data-state="login"]):not([data-state="loading"]) .log,
body:not([data-state="login"]):not([data-state="loading"]) #settings:not([hidden]) { display: flex; }
/* Its own rule: the shared one above would re-declare this grid as a flex row and
   the microphone would drift off centre. */
body:not([data-state="login"]):not([data-state="loading"]) .bar { display: grid; }

/* While the mic is open the languages are fixed: changing them mid-sentence would
   re-point a translation that is already streaming. */
[data-state="listening"] .pair { opacity: 0.45; pointer-events: none; }

/* ---------- login ---------- */

.login {
  flex-direction: column;
  gap: 10px;
  width: 100%;
  max-width: 300px;
  margin: auto;
}

.login input {
  font: inherit;
  font-size: 16px; /* prevents iOS zoom */
  padding: 12px 14px;
  border-radius: 12px;
  border: 1.5px solid var(--line);
  background: var(--surface);
  color: var(--fg);
  outline: none;
}

.login input:focus { border-color: var(--green); }
.login button { padding: 12px 18px; border-radius: 12px; background: var(--green); color: #fff; font-weight: 600; }
.login button:active { background: var(--green-press); }
.error { margin: 0; color: var(--rec); font-size: 14px; text-align: center; }

/* ---------- the language pair ---------- */

.pair {
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.chip-pick { position: relative; flex: 1 1 0; min-width: 0; display: flex; }
.pair .chip-pick:last-of-type { justify-content: flex-end; }

.chip {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  min-height: 40px;
  padding: 0 8px 0 10px;
  border-radius: 999px;
  border: 1.5px solid var(--line);
  background: var(--surface);
  font-size: 14px;
  font-weight: 600;
  transition: border-color 0.15s, background-color 0.15s;
}

.chip-code {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.06em;
  padding: 2px 6px;
  border-radius: 6px;
  color: #fff;
}

[data-pick="a"] .chip-code { background: var(--green); }
[data-pick="b"] .chip-code { background: var(--indigo); }

.chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (hover: hover) { .chip:hover { border-color: var(--green-soft); } }
.chip:focus-visible, .chip[aria-expanded="true"] { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px var(--green-tint); }

.swap {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--muted);
  transition: transform 0.25s cubic-bezier(.2, .8, .25, 1), color 0.15s, background-color 0.15s;
}

.swap svg { width: 20px; height: 20px; }
.swap:active { transform: rotate(180deg) scale(0.94); }
.swap.spun { animation: spun 0.35s cubic-bezier(.2, .8, .25, 1); }
@keyframes spun { from { transform: rotate(-180deg); } }

@media (hover: hover) { .swap:hover { background: var(--sunken); color: var(--green); } }
.swap:focus-visible { outline: none; color: var(--green); box-shadow: 0 0 0 3px var(--green-tint); }

/* ---------- the conversation log ---------- */

.log {
  flex: 1;
  min-height: 0;
  flex-direction: column;
  gap: 10px;
  overflow-y: auto;
  /* Deliberately not "contain": dragging past the end of the conversation should
     carry on into the page and bring the settings up. That pull is the only way
     anyone discovers they are there. */
  overscroll-behavior: auto;
  /* Deliberately not smooth: the log is re-pinned to the bottom on every delta of a
     streaming translation, and a smooth scroll restarted dozens of times a second
     never arrives. Instant scrolling, applied continuously, is what looks smooth. */
  scroll-behavior: auto;
  padding: 4px 2px 6px;
  /* Keeps the newest turn pinned to the bottom while content is appended above it. */
  overflow-anchor: none;
}

.empty {
  margin: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-align: center;
  padding: 16px 20px;
  max-width: 320px;
}

.empty[hidden] { display: none; }
.empty-title { font-size: calc(20px * var(--scale)); font-weight: 800; letter-spacing: -0.02em; }
.empty-hint { font-size: calc(15px * var(--scale)); color: var(--muted); }

/* One turn: what was said, small and quiet, and underneath it the translation, which
   is the thing a stranger actually has to read. Side A hangs left, side B right, so
   who said what is obvious without reading a word. */
.turn {
  flex-shrink: 0;
  max-width: 94%;
  margin: 0;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  animation: turn-in 0.22s cubic-bezier(.2, .8, .25, 1);
}

@keyframes turn-in { from { opacity: 0; transform: translateY(8px); } }

.turn[data-side="a"] { align-self: flex-start; border-left: 4px solid var(--green); border-top-left-radius: 7px; }
.turn[data-side="b"] { align-self: flex-end; border-right: 4px solid var(--indigo); border-top-right-radius: 7px; }

.turn-source {
  margin: 0;
  font-size: calc(15px * var(--scale));
  line-height: 1.45;
  color: var(--muted);
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* The translation. 21px at scale 1 reads comfortably at arm's length without
   shouting; the A−/A+ control moves it between 18 and 28. */
.turn-target {
  margin: 5px 0 0;
  font-size: calc(21px * var(--scale));
  line-height: 1.4;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--fg);
  white-space: pre-wrap;
  word-wrap: break-word;
}

.turn-target:empty { display: none; }

/* A turn still being spoken: only the transcript exists, and it is provisional. */
.turn.live { border-style: dashed; }
.turn.live .turn-source { color: var(--faint); }

.turn.live .turn-source::after,
.turn.streaming .turn-target::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1.05em;
  margin-left: 3px;
  vertical-align: -0.16em;
  background: var(--live);
  animation: blink 1s steps(1) infinite;
}

.turn.streaming .turn-target::after { background: var(--green); }

@keyframes blink { 50% { opacity: 0; } }

/* Waiting for the translation to start arriving. The bubble exists from the moment
   the utterance ended, so an empty target still has to show itself. */
.turn.pending .turn-target:empty { display: block; }

.turn.pending .turn-target::before {
  content: attr(data-translating);
  color: var(--faint);
  font-size: calc(16px * var(--scale));
  font-weight: 400;
}

.turn.failed { border-color: var(--rec); }
.turn.failed .turn-target { color: var(--rec); font-size: calc(16px * var(--scale)); font-weight: 400; }

/* ---------- timer + controls ---------- */

.timer {
  flex-shrink: 0;
  margin: 0;
  height: 22px;
  text-align: center;
  font-size: 16px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--green);
  visibility: hidden;
}

[data-state="listening"] .timer { visibility: visible; }

.timer::before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: 7px;
  vertical-align: 0.12em;
  border-radius: 50%;
  background: var(--live);
  animation: blink 1.2s steps(1) infinite;
}

/* The bar along the bottom of the screen. The microphone is the only thing on it
   that matters, so it sits dead centre and everything else stays quiet at the edges. */
.bar {
  flex-shrink: 0;
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
  padding-bottom: calc(10px + env(safe-area-inset-bottom));
}

/* The one button: it opens and closes the mic for the whole conversation, not for a
   single sentence. Green with a mic when closed, red with a stop square when open. */
.mic {
  --size: clamp(84px, 23vw, 100px);
  position: relative;
  grid-column: 2;
  justify-self: center;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  background: var(--green);
  color: #fff;
  display: grid;
  place-items: center;
  transform: scale(calc(1 + var(--v, 0) * 0.07));
  transition: transform 0.12s ease-out, background-color 0.2s;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.mic.pressed { transform: scale(0.93); }
.mic:focus-visible { outline: 3px solid var(--green-soft); outline-offset: 5px; }

@media (hover: hover) {
  [data-state="idle"] .mic:hover, [data-state="error"] .mic:hover { background: var(--green-press); }
}

.icon { position: relative; grid-area: 1 / 1; width: 62%; height: 62%; }
.icon-stop { width: 32%; height: 32%; display: none; }

[data-state="listening"] .mic { background: var(--live); }
[data-state="listening"] .mic:active { background: var(--live-press); }
[data-state="listening"] .icon-mic { display: none; }
[data-state="listening"] .icon-stop { display: block; }

/* Cold iOS mic: amber 3-2-1 means "wait"; red means "talk". */
.count { grid-area: 1 / 1; display: none; font-size: calc(var(--size) * 0.42); font-weight: 700; line-height: 1; color: #fff; font-variant-numeric: tabular-nums; }
.count.pop { animation: pop-in 0.3s ease-out; }
@keyframes pop-in { from { transform: scale(1.5); opacity: 0.3; } to { transform: scale(1); opacity: 1; } }
[data-mic="warming"] .mic, [data-mic="warming"] .mic:active { background: var(--wait); }
[data-mic="warming"] .count { display: block; }
[data-mic="warming"] .icon-mic, [data-mic="warming"] .icon-stop { display: none; }
[data-mic="warming"] .level { background: var(--wait-tint); }

/* Flat voice halo: grows with the voice level (--v, 0..1) set from JS. */
.level {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--live-tint);
  transform: scale(calc(1 + var(--v, 0) * 0.32));
  opacity: 0;
  transition: transform 0.07s linear, opacity 0.2s;
}

[data-state="listening"] .level { opacity: 1; }

/* A ring while an utterance is in flight, so the open mic still shows activity. */
.spinner {
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 3px solid transparent;
  border-top-color: var(--green);
  opacity: 0;
}

body.working .spinner { opacity: 1; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- text size + clear ---------- */

.sizer {
  grid-column: 1;
  justify-self: start;
  display: inline-flex;
  border-radius: 999px;
  border: 1.5px solid var(--line);
  background: var(--surface);
  overflow: hidden;
}

.size-btn {
  min-width: 44px;
  min-height: 42px;
  font-size: 15px;
  font-weight: 700;
  color: var(--fg);
  transition: background-color 0.15s;
}

.size-btn:first-child { border-right: 1.5px solid var(--line); }
.size-btn .minus, .size-btn .plus { font-size: 12px; vertical-align: 0.1em; color: var(--muted); }
.size-btn:active { background: var(--sunken); }
.size-btn:disabled { opacity: 0.35; cursor: default; }
.size-btn:focus-visible { outline: none; background: var(--green-tint); }

@media (hover: hover) { .size-btn:not(:disabled):hover { background: var(--sunken); } }

.clear {
  grid-column: 3;
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 42px;
  padding: 0 15px 0 11px;
  border-radius: 999px;
  background: var(--surface);
  border: 1.5px solid var(--line);
  font-size: 14px;
  font-weight: 600;
  animation: fade-in 0.2s ease-out;
  transition: border-color 0.15s, transform 0.08s;
}

.clear svg { width: 19px; height: 19px; color: var(--muted); transition: color 0.15s; }
.clear:active { transform: scale(0.95); }
.clear:focus-visible { outline: none; border-color: var(--rec); box-shadow: 0 0 0 3px var(--rec-tint); }
.clear[hidden] { display: none; }

@media (hover: hover) {
  .clear:hover { border-color: color-mix(in srgb, var(--rec) 55%, var(--line)); }
  .clear:hover svg { color: var(--rec); }
}

@keyframes fade-in { from { opacity: 0; transform: translateY(3px); } }

/* Narrow phones: the label goes, the icon stays. */
@media (max-width: 380px) {
  .clear span { display: none; }
  .clear { padding: 0 12px; }
}

/* Normally empty and gone. It speaks only when something needs saying — an error,
   a mic that will not open — never to narrate what the screen already shows. */
.status {
  flex-shrink: 0;
  margin: 0;
  padding: 2px 0;
  font-size: 14px;
  color: var(--muted);
  text-align: center;
}

.status:empty { display: none; }
[data-state="error"] .status { color: var(--rec); }

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

/* Invisible until the page moves. Scrolling is the whole gesture: nothing of this
   belongs on the first screen, and fading it in is what makes it feel deliberate
   rather than hidden. */
#settings {
  box-sizing: border-box;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease-out;
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 28px 14px calc(22px + env(safe-area-inset-bottom));
}

.pick { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.pick[hidden] { display: none; }

.pick select {
  font: inherit;
  font-weight: 500;
  color: var(--fg);
  background: transparent;
  border: 0;
  padding: 5px 2px;
  cursor: pointer;
  outline: none;
}

.pick select:focus-visible { outline: 2px solid var(--green-soft); border-radius: 6px; }



/* ---------- wide screens: the log gets the room, the controls stay put ---------- */

@media (min-width: 700px) {
  .turn { max-width: 78%; }
  .turn-target { font-size: calc(23px * var(--scale)); }
}

body.peek #settings { opacity: 1; pointer-events: auto; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
