/*
  Palette and metrics are read from live Netcare CSS, not guessed — see
  docs/brand/netcare-ci.md §1.2/§1.4. INFERRED uses are marked as such.
*/
:root {
  /* VERIFIED: the three colours that actually carry the brand online. */
  --navy: #1d3443;        /* 361 uses; h1-h6 and `.navy` buttons */
  --accent: #3da9d1;      /* primary CTA fill, live SVG icons */
  --accent-hover: #7ec6e0;
  --gold: #d5bfac;        /* decorative echo only — never a UI fill */

  /* VERIFIED grey ramp + surfaces + borders. */
  --grey: #80898f;
  --slate: #61717b;
  --slate-deep: #57656e;
  --line: #bec2c6;
  --rule: #e8e9ea;
  --page: #f7f7f7;
  --surface: #ffffff;
  --page-top: #eaf5fa;    /* VERIFIED pale blue tint, used as the page wash */
  --tint: #eaf5fa;
  --tint-2: #d8eef6;
  --focus: #bee2f0;       /* VERIFIED pale blue tint */

  /* VERIFIED alert red. There is no semantic green anywhere in Netcare's CSS,
     so the answer/end-call hues below are ordinary telephony convention, not
     brand colour, and are kept dark rather than neon. */
  --alert: #e7393f;
  --call: #1a7f37;
  --end: #c8302b;

  --fg: #1d3443;
  --radius: 10px;         /* VERIFIED: dominant radius in homeStyles.min.css */
  --gap: 20px;            /* VERIFIED: their row-gap grid unit */
}
* { box-sizing: border-box; }
html, body { height: 100%; }

/* The panels a caller must not see ship `hidden` in the markup. `.card`,
   `#service-health` and `#settings` all set `display: flex`, which beats the UA
   `[hidden]` rule, so the attribute alone would not hide them. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--page);
  background-image: linear-gradient(180deg, var(--page-top) 0%, var(--page) 42%);
  /* Open Sans is Netcare's declared workhorse face; the tail is the fallback
     their own skin.css declares. No webfont is bundled here (no build step), so
     this degrades to the system stack rather than fetching a font. */
  font-family: 'Open Sans', 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  color: var(--fg);
  height: 100dvh;
  overflow: hidden;
  font-size: 15px;
  line-height: 1.45;
}
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* ---- two-column card layout ---------------------------------------------- */
.layout {
  display: flex;
  gap: var(--gap);
  height: 100%;
  padding: 16px;
}
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(29, 52, 67, 0.06);
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}
.call-card { flex: 0 0 clamp(320px, 34vw, 420px); }
.right-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
}
/* With every right-hand card hidden (the product default) the empty flex track
   would still eat half the viewport. `:has()` collapses it without any JS, so
   the call screen is centred before a script has run. Chrome-only is fine: the
   app is WebRTC/Chrome-only already. */
.right-col:not(:has(.card:not([hidden]))) { display: none; }
.layout:not(:has(.right-col .card:not([hidden]))) { justify-content: center; }
.card-header {
  background: var(--navy);
  color: #fff;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 12px 18px;
  /* The transcript header carries the service readout next to its title, so the
     strip needs to be a row. Headers that are a bare text node still work: the
     text becomes an anonymous flex item. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}
.card-body { flex: 1; min-height: 0; overflow: auto; }
.transcription-card { flex: 2 1 0; }
.diagnostics-card { flex: 1 1 0; }

/* ---- call card: brand lock-up + service dots ----------------------------- */
/* A text lock-up, not the Netcare emblem: no logo licence was found, so the
   official mark must not be reproduced (docs/brand/netcare-ci.md §3). */
header#topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--rule);
}
#brand { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.brand-mark {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--navy);
  color: #fff;
  flex: 0 0 auto;
}
.brand-mark svg { width: 16px; height: 16px; fill: currentColor; display: block; }
.brand-name {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.03em;   /* VERIFIED: their headings use -.03em */
  color: var(--navy);
}
.brand-rule {
  width: 1px;
  height: 18px;
  background: var(--gold);
  flex: 0 0 auto;
}
.brand-sub {
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--slate);
}

/* ---- transcript card: service status dots --------------------------------- */
/* Product surface, not a dev panel: the readout sits on the transcript header,
   where "is the service up" is the question someone reading a missing line
   actually asks. Clicking the strip re-probes all three. */
#service-health {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 16px;
  padding: 0;
  cursor: pointer;
  user-select: none;
}
.svc {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--slate);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
}
/* On the navy strip the label needs to read as a muted caption rather than
  recolour itself on state — only the dot carries the verdict. */
.card-header .svc { color: rgba(255, 255, 255, 0.72); }
.svc i {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--line);
  display: inline-block;
  transition: background 0.3s;
}
.svc.ok i { background: var(--call); }
.svc.bad i, .svc.err i { background: var(--alert); }
/* On the dark strip the grey "not probed yet" dot would read as an alert, so
   brighten it to a neutral that clearly means "unknown". */
.card-header .svc i { background: rgba(255, 255, 255, 0.35); }
.card-header .svc.ok i { background: var(--call); }
.card-header .svc.bad i, .card-header .svc.err i { background: var(--alert); }

/* ---- call card: avatar, clock, status, spectrum --------------------------- */
main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 20px 16px;
  /* Clipping beats spilling: with `?debug=1` the spectrum and settings row eat
     into the avatar's space, and centered overflow would paint over the brand
     bar. Shrinking below covers it; this guarantees it. */
  overflow: hidden;
}
#avatar {
  /* Sized by height, not width: in a column flex track the main axis is the
     height, so when `?debug=1` crowds the card this is the item that gives way —
     and because the width follows `aspect-ratio`, it stays a circle while it
     does. A width-set avatar would shrink to an ellipse or not at all. */
  height: min(38vh, 260px);
  aspect-ratio: 1;
  flex: 0 1 auto;
  min-height: 0;
  border-radius: 50%;
  background: var(--navy);
  display: grid;
  place-items: center;
  color: #fff;
  font-size: clamp(28px, 8vh, 42px);
  font-weight: 300;          /* VERIFIED: Light 300 is a live weight */
  letter-spacing: 0.06em;
  box-shadow: 0 0 0 1px var(--navy), 0 10px 30px rgba(29, 52, 67, 0.16);
  transition: box-shadow 0.35s;
}
#avatar.state-speaking { animation: breathe 1.4s ease-in-out infinite; }
#avatar.state-listening { box-shadow: 0 0 0 8px var(--tint-2), 0 10px 30px rgba(29, 52, 67, 0.16); }
#avatar.state-thinking { box-shadow: 0 0 0 8px var(--focus), 0 10px 30px rgba(29, 52, 67, 0.16); }
@keyframes breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.04); }
}
#session-clock {
  font-size: clamp(22px, 5vh, 32px);
  font-weight: 300;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--navy);
  /* Blank before a call is dialed, so without this the element collapses and
     the avatar and controls jump together when the timer appears. 1.45em is what
     the line box measures with digits in it (46px at the 32px top of the clamp),
     so the row is the same height either way. Mirrors the `min-height` that
     keeps `#call-status` on its own row. */
  min-height: 1.45em;
}
#call-status {
  margin: 0;
  min-height: 1.45em;
  font-size: 15px;
  text-align: center;
  max-width: 32ch;
  color: var(--slate-deep);
}
#call-status.is-error { color: var(--alert); font-weight: 600; }
/* Shrinks with the avatar when `?debug=1` crowds the card. `min-height: 0` is
   required: a canvas's content-based minimum is its own bitmap height, which on
   a HiDPI screen is twice its CSS height and would refuse to shrink. The
   ResizeObserver in app.js re-fits the bitmap after the box changes. */
#spectrum {
  width: 100%;
  max-width: 420px;
  height: min(20vh, 130px);
  flex: 0 1 auto;
  min-height: 0;
}

/* ---- call card: controls -------------------------------------------------- */
#controls {
  margin: 0 auto 22px;
  background: var(--navy);
  border-radius: 40px;
  padding: 12px 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  box-shadow: 0 12px 28px rgba(29, 52, 67, 0.22);
}
.ctl {
  width: 56px; height: 56px;
  border: 0;
  border-radius: 50%;
  background: #fff;
  color: var(--navy);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: transform 0.1s, background 0.2s, color 0.2s, opacity 0.2s;
}
.ctl:active { transform: scale(0.94); }
/* A control that is not available yet should read as waiting, not as broken: an
   idle call screen with two washed-out grey discs looks faulty. Ghost them
   instead — the icon stays legible on the navy bar without pretending to be a
   white button. `#hangup-btn`/`#mute-btn` outrank `.ctl`, so their colour needs
   an id-level rule to be overridden while disabled. */
.ctl:disabled {
  background: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.38);
  cursor: default;
}
#mute-btn:disabled, #hangup-btn:disabled { color: rgba(255, 255, 255, 0.38); }
.ctl svg { width: 24px; height: 24px; display: block; }
/* `fill: currentColor` makes the icon follow the button's text colour, so the
   per-button rules below are the only place these hues are declared. */
.ctl svg { fill: currentColor; }
#connect-btn { color: var(--call); }
#hangup-btn { color: var(--end); }
#mute-btn { border-radius: 18px; }
#mute-btn.muted { background: var(--accent); color: #fff; }
/* The one control that stays on screen for a caller. */
#transcript-toggle.active { background: var(--accent); color: #fff; }
#transcript-toggle:not(.active) { background: rgba(255, 255, 255, 0.14); color: #fff; }
#connect-btn.dim svg { opacity: 0.35; }

/* ---- call card: voice / speed / STT-model settings (dev-only) ------------- */
#settings {
  margin: 0 18px 6px;
  background: var(--tint);
  border: 1px solid var(--tint-2);
  border-radius: var(--radius);
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.setting {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  color: var(--slate-deep);
  font-size: 13px;
  font-weight: 600;
}
.setting-label { display: inline-flex; align-items: baseline; gap: 8px; white-space: nowrap; }
#speed-value { font-variant-numeric: tabular-nums; min-width: 3.4ch; }
/* Shared by both dropdowns (voice + STT model); the ids stay for the JS. */
#settings select {
  flex: 1 1 0;
  min-width: 0;
  max-width: 200px;
  font: inherit;
  color: var(--fg);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 6px 10px;
  cursor: pointer;
}
#speed-slider {
  flex: 1 1 0;
  min-width: 0;
  max-width: 200px;
  accent-color: var(--accent);
  cursor: pointer;
}
#settings :disabled { opacity: 0.5; cursor: default; }

/* ---- transcription card: chat bubbles ------------------------------------- */
#transcription { display: flex; flex-direction: column; gap: 14px; padding: 18px; }
/* An empty transcript card looks like a broken panel. Naming what fills it costs
   no markup and no JS: `:empty` stops matching the moment the first bubble is
   appended, and hiding the card keeps the whole call's bubbles for the reopen. */
#transcription:empty::before {
  content: "Nothing to show yet. What you and the agent say appears here during a call.";
  color: var(--grey);
  font-size: 14px;
}
.msg { display: flex; flex-direction: column; max-width: 85%; }
.msg.agent { align-items: flex-start; align-self: flex-start; }
.msg.user { align-items: flex-end; align-self: flex-end; }
.msg .who {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--slate);
  margin-bottom: 4px;
}
.msg .bubble {
  background: var(--tint);
  border: 1px solid var(--tint-2);
  color: var(--fg);
  font-size: 15px;
  line-height: 1.45;
  padding: 10px 14px;
  border-radius: var(--radius);
  white-space: pre-wrap;
  word-break: break-word;
}
.msg.user .bubble { background: var(--surface); border-color: var(--line); }
/* The line that closes a call, so two calls in one feed read as two calls. It
   sits in the column track rather than inside a bubble, and `align-self:
   stretch` is what makes it a rule across the card instead of a short
   left-aligned caption: the flex track sizes its items to their content. */
.call-divider {
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--grey);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.call-divider::before,
.call-divider::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--line);
}

/* ---- transcript card: text composer --------------------------------------- */
/* Under the feed, on the pale page wash rather than the navy strip, so it reads
   as "say something here" and not as another control bar. The button is a plain
   accent disc: one obvious action, no labels competing with the transcript. */
.composer {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-top: 1px solid var(--rule);
  background: var(--page);
}
.composer-input {
  flex: 1 1 0;
  min-width: 0;
  font: inherit;
  color: var(--fg);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 9px 14px;
}
.composer-input:focus-visible { outline: none; border-color: var(--accent); }
.composer-input:disabled { background: var(--page); color: var(--grey); cursor: default; }
.composer-input::placeholder { color: var(--grey); }
.composer-send {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 36px; height: 36px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
  transition: background 0.2s, opacity 0.2s, transform 0.1s;
}
.composer-send svg { width: 17px; height: 17px; fill: currentColor; display: block; }
.composer-send:hover:not(:disabled) { background: var(--navy); }
.composer-send:active:not(:disabled) { transform: scale(0.94); }
/* Ghosted while there is no call to speak into — same "waiting, not broken"
   read as the call controls. */
.composer-send:disabled { background: var(--tint-2); color: #fff; cursor: default; }

/* ---- diagnostics card: event/error feed (dev-only) ------------------------ */
#diagnostics {
  padding: 14px 16px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 1.55;
  color: var(--slate-deep);
}
.diag-line { margin-bottom: 2px; word-break: break-word; }
.diag-line .kind { font-weight: 700; color: var(--accent); }
.diag-line.error { color: var(--alert); }
.diag-line.error .kind { color: var(--alert); }
.diag-line .t { color: var(--grey); margin-right: 6px; }

/* ---- connection settings: gear + modal ------------------------------------- */
/* Gear next to the status dots, deliberately quiet: the strip says "services",
   the gear says "there is a door here" without competing with the call. */
.header-tools {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
#settings-gear {
  display: inline-flex;
  padding: 3px 4px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: rgba(255, 255, 255, 0.55);
  cursor: pointer;
}
#settings-gear svg { width: 15px; height: 15px; fill: currentColor; display: block; }
#settings-gear:hover { color: #fff; background: rgba(255, 255, 255, 0.12); }

/* Overlay + card. The card reuses the navy strip the cards already have, so
   the modal reads as the same product wearing a dialog, not a new one. */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(29, 52, 67, 0.45);
}
.modal-card {
  display: flex;
  flex-direction: column;
  width: min(640px, 92vw);
  max-height: min(85vh, calc(100dvh - 32px));
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(29, 52, 67, 0.18);
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 11px 16px;
  background: var(--navy);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}
.modal-x {
  padding: 2px 6px;
  border: 0;
  background: none;
  color: rgba(255, 255, 255, 0.7);
  font-size: 13px;
  cursor: pointer;
}
.modal-x:hover { color: #fff; }
.modal-body {
  padding: 14px 16px;
  overflow: auto;
}
/* One boxed row per surface, tinted like the header's own accents so the
   three sections read as siblings of the STT/LLM/TTS strip above. */
.msec {
  margin-bottom: 10px;
  padding: 9px 12px 10px;
  background: var(--tint);
  border: 1px solid var(--tint-2);
  border-radius: var(--radius);
}
.msech {
  margin: 0 0 7px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--slate-deep);
}
.mrow {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}
.mrow:last-child { margin-bottom: 0; }
.mrow > span {
  flex: 0 0 74px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--slate-deep);
}
.mrow input,
.mrow select {
  flex: 1 1 0;
  min-width: 0;
  padding: 4px 8px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 6px;
  font: inherit;
  font-size: 12.5px;
  color: var(--fg);
}
.mrow input:focus-visible,
.mrow select:focus-visible { outline: none; border-color: var(--accent); }
/* Italic grey placeholders carry the "what blank means" copy — the field is
   not empty, it is inheriting, and that has to be readable at a glance. */
.modal-body ::placeholder { color: var(--grey); font-style: italic; }
/* A censored key is real content, not a hint: render it in field ink so it
   reads as the value in the box (it is), unlike the grey placeholder copy. */
.mrow input.masked { color: var(--slate-deep); letter-spacing: 0.03em; }
/* Test verdicts live in the card, next to the fields they describe, in the
   same dot language as the header strip. */
.mtest {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 2px;
  padding-top: 9px;
  border-top: 1px solid var(--rule);
  font-size: 11px;
  font-weight: 600;
  color: var(--slate-deep);
}
.mtest-item { display: inline-flex; align-items: center; gap: 6px; }
.mtest-item::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--line);
}
.mtest-item.ok::before { background: var(--call); }
.mtest-item.bad::before { background: var(--alert); }
.modal-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 16px;
  background: var(--page);
  border-top: 1px solid var(--rule);
  border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
}
.mfoot-note { font-size: 11px; font-style: italic; color: var(--grey); }
.mbtns { display: inline-flex; gap: 10px; }
.mbtn {
  padding: 7px 14px;
  border: 1px solid transparent;
  border-radius: 8px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
}
.mbtn:disabled { opacity: 0.6; cursor: default; }
.mbtn.ghost { background: #fff; border-color: var(--line); color: var(--slate-deep); }
.mbtn.test { background: #fff; border-color: var(--accent); color: #1d7ba4; }
.mbtn.save { background: var(--navy); color: #fff; }

/* ---- small screens: the call screen becomes the screen -------------------- */
/* On a handset a framed card in a grey gutter reads as a widget, while the
   thing it imitates — a phone call — has no frame. So the media query drops the
   `.card` chrome from the call screen and hands it the whole viewport. Kept
   inside the query on purpose: on a desk the card is correct, and
   `tests/test_mobile_call_screen.py` fails if the base `.card` rule is edited
   instead of this one. The transcript is not stripped — it keeps its card look,
   just below the fold. */
@media (max-width: 760px) {
  body { overflow: auto; }
  .layout { flex-direction: column; height: auto; min-height: 100%; padding: 0; gap: 0; }
  .call-card {
    flex: 0 0 auto;
    /* svh, not vh: with the browser's own bar on screen, `vh` is the height
       without it, which pushes the call buttons under the bar. */
    min-height: 100vh;
    min-height: 100svh;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  /* The gutter moves off the call screen and onto the column under it, so the
     transcript is still an inset card rather than edge-to-edge text. */
  .right-col { flex: 0 0 auto; padding: 16px; gap: var(--gap); }
  .transcription-card { min-height: 320px; }
  .diagnostics-card { min-height: 200px; }
  header#topbar { flex-wrap: wrap; }
}
