/* Paint Protection Solutions — browser call page.
   Deliberately plain. One button, one status line, nothing else.

   Design system (from the PPS site pitch, not from ppsdxb.com):
     #0F0E0D near-black · #FFFFFF · #B98D2E gold · Instrument Sans

   Motion budget: the button press, the state crossfade, and the speaking dot. That is
   all. Every one of those exists to make state legible; nothing here is decorative. */

/* One variable file, latin subset, self-hosted. Google serves the same woff2 for both
   400 and 500, so declaring two static faces would download it twice and still
   synthesise nothing. 30 KB covers every weight the page uses. */
@font-face {
  font-family: 'Instrument Sans';
  src: url('/fonts/InstrumentSans-Variable.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --ink: #0f0e0d;
  --paper: #ffffff;
  --gold: #b98d2e;

  --muted: rgba(255, 255, 255, 0.56);
  --faint: rgba(255, 255, 255, 0.14);

  /* Stronger than any built-in easing. The CSS defaults are too weak to read as
     intentional at these short durations. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);

  --press: 160ms;
  --swap: 200ms;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
}

body {
  background: var(--ink);
  color: var(--paper);
  font-family: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Centred column. min-height uses dvh so the iOS toolbar collapsing does not shift the
   button under the user's thumb mid-tap; svh is the fallback for browsers without dvh. */
.stage {
  min-height: 100svh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 56px;
  padding: 32px 24px calc(32px + env(safe-area-inset-bottom));
  text-align: center;
}

.mark {
  margin: 0;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.control {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  width: 100%;
  max-width: 340px;
}

/* ---------------------------------------------------------------- the button */

.action {
  appearance: none;
  border: 1px solid var(--gold);
  border-radius: 999px;
  background: var(--gold);
  color: var(--ink);
  font: inherit;
  font-weight: 500;
  font-size: 17px;
  letter-spacing: 0.01em;
  padding: 17px 40px;
  width: 100%;
  cursor: pointer;

  /* Named properties only. `all` would sweep up colour and border and make the
     disabled transition muddy. */
  transition:
    transform var(--press) var(--ease-out),
    background-color var(--swap) var(--ease-out),
    border-color var(--swap) var(--ease-out),
    color var(--swap) var(--ease-out),
    opacity var(--swap) var(--ease-out);

  /* 44px minimum touch target, comfortably exceeded. */
  min-height: 56px;
}

/* Hover only where a real pointer exists — on touch this fires on tap and sticks. */
@media (hover: hover) and (pointer: fine) {
  .action:hover:not([aria-disabled='true']) {
    background: #c99a35;
    border-color: #c99a35;
  }
}

.action:active:not([aria-disabled='true']) {
  transform: scale(0.97);
}

.action:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

/* In call the button is a quiet outline. The loud gold belongs to the action that starts
   something; ending it should not compete for attention while someone is talking.
   --muted, not --faint: this is an ACTIVE control, so its visual boundary needs 3:1
   under WCAG 1.4.11. --faint is ~1.2:1 and is reserved for genuinely inert states. */
.stage[data-state='in-call'] .action {
  background: transparent;
  border-color: var(--muted);
  color: var(--paper);
}

@media (hover: hover) and (pointer: fine) {
  .stage[data-state='in-call'] .action:hover:not([aria-disabled='true']) {
    background: transparent;
    border-color: var(--muted);
  }
}

/* The label crosses between two words on every state change. Opacity alone, deliberately:
   a blur here would read marginally better but it is the one property in this file that
   forces a per-frame repaint, and the audience is mixed mobile hardware. Not worth the
   jank risk for a 200ms crossfade on a two-word label. */
.action__label {
  display: inline-block;
  transition: opacity var(--swap) var(--ease-out);
}

.action__label[data-swapping='true'] {
  opacity: 0;
}

/* States with nothing to press: the button is removed, not left dead on screen. A
   visible control that ignores taps is the exact failure this page is built to avoid.
   `no-mic` and `mic-busy` are deliberately absent — both are conditions the user can fix
   without reloading, so they keep a "Try again" button. */
.stage[data-state='boot'] .action,
.stage[data-state='blocked'] .action,
.stage[data-state='disabled'] .action,
.stage[data-state='denied'] .action,
.stage[data-state='insecure'] .action,
.stage[data-state='unsupported'] .action,
.stage[data-state='error'] .action {
  display: none;
}

/* aria-disabled, not [disabled] — see app.js. The property would steal focus from a
   keyboard user mid-flow, so the state is communicated without it. */
.action[aria-disabled='true'] {
  cursor: default;
  background: transparent;
  border-color: var(--faint);
  color: var(--muted);
}

/* ---------------------------------------------------------------- status line */

/* Height is reserved for the longest message in the ordinary flow ("Allow microphone
   access when your browser asks." wraps to two lines at this width). Without it, that
   message collapsing to "Connecting…" re-centres the column and slides the button
   downward in the instant after a tap — the same class of problem the dvh note above
   guards against, from a different direction. */
.status {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  min-height: 48px;
  font-size: 14px;
  color: var(--muted);
  transition: color var(--swap) var(--ease-out);
}

/* When the button is gone, or is only a retry, the status line carries the whole
   message — it stops being a caption and becomes the content. */
.stage[data-state='blocked'] .status,
.stage[data-state='denied'] .status,
.stage[data-state='no-mic'] .status,
.stage[data-state='mic-busy'] .status,
.stage[data-state='insecure'] .status,
.stage[data-state='unsupported'] .status,
.stage[data-state='disabled'] .status,
.stage[data-state='error'] .status {
  font-size: 16px;
  color: var(--paper);
  max-width: 32ch;
  text-wrap: balance;
}

.dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--muted);
  opacity: 0;
  transition: opacity var(--swap) var(--ease-out), background-color var(--swap) var(--ease-out);
}

/* The dot only means something once a call is live. */
.stage[data-state='connecting'] .dot,
.stage[data-state='in-call'] .dot {
  opacity: 1;
}

.stage[data-state='in-call'][data-speaking='true'] .dot {
  background: var(--gold);
  animation: breathe 1400ms ease-in-out infinite;
}

/* Continuous, so it eases both ways — an ease-out loop reads as a stutter. */
@keyframes breathe {
  0%,
  100% {
    opacity: 0.35;
  }
  50% {
    opacity: 1;
  }
}

/* ---------------------------------------------------------------- reduced motion */

/* Fewer and gentler, not none: the colour and opacity changes carry meaning, so they
   stay. Only the looping movement goes. */
@media (prefers-reduced-motion: reduce) {
  .action {
    transition-duration: 1ms;
  }

  .action:active:not([aria-disabled='true']) {
    transform: none;
  }

  .action__label {
    transition-duration: 1ms;
  }

  .stage[data-state='in-call'][data-speaking='true'] .dot {
    animation: none;
    opacity: 1;
  }
}
