/* ──────────────────────────────────────────────────────────────
   iVP — Page Styles
   Auth pages + shared chrome. Extends tokens.css.
   ────────────────────────────────────────────────────────────── */

/* ─── Scroll root ─── */
html:has(body.auth-body) { overflow-y: auto; }

/* ─── Tooltip base (data-tip) ─────────────────────────────────
   Two-layer pseudo: `::after` for the body, `::before` for the
   pointing arrow. Same vocabulary as iVE's `.chart-tip` —
   inverted-contrast surface (dark on light page, light on dark)
   with an arrow that aligns the tooltip back to the trigger
   element so the relationship is unambiguous.

   Font-family chains `var(--font-ui), var(--font-ar)`: any Arabic
   glyph embedded in an LTR-context tooltip (e.g. the "switch to
   العربية" hint on the language toggle) falls through to
   IBM Plex Sans Arabic via per-glyph fallback rather than dropping
   to the browser's default Arabic font. RTL pages still rewrite
   --font-ui itself, so both directions stay on-system. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 10px;
  font-family: var(--font-ui), var(--font-ar);
  font-size: var(--fsz-caption);
  font-weight: 500;
  line-height: 1.4;
  color: var(--tooltip-fg);
  background: var(--tooltip-bg);
  border: 1px solid var(--tooltip-border);
  border-radius: var(--r-sm);
  box-shadow: var(--tooltip-shadow);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease;
  z-index: var(--z-tooltip);
}
/* Arrow — sits in the 8 px gap between trigger and tooltip. With
   `border-top-color`, the visible triangle has its wide base at the
   pseudo's top edge and its apex at the pseudo's position point,
   so the arrow points DOWN at the trigger when the tooltip is
   above it. Auth-controls variant below flips the orientation. */
[data-tip]::before {
  content: '';
  position: absolute;
  bottom: calc(100% + 3px);
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border: 5px solid transparent;
  border-top-color: var(--tooltip-bg);
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease;
  z-index: var(--z-tooltip);
}
[data-tip]:hover::after,
[data-tip]:hover::before { opacity: 1; }

/* ─── Tenant logo ─── */
.tenant-icon {
  width: 128px;
  height: 32px;
  padding: 0;
  background-image: url('assets/tenant/PL-logo-light.svg');
  background-repeat: no-repeat;
  background-size: contain;
  background-position: left center;
}
[dir="rtl"] .tenant-icon { background-position: right center; }
[data-theme="dark"] .tenant-icon {
  background-image: url('assets/tenant/PL-logo-dark.svg');
}

/* ─── Utility ─── */
.is-hidden { display: none; }

/* Inline lang-tagged text (like "عربي" inside the English UI or
   "EN" inside the Arabic UI) should render in the proper font for
   that language. Uses the design-system Arabic stack --font-ar
   (IBM Plex Sans Arabic) when lang="ar".
   Global (not scoped to any one component): any element marked
   with lang="ar" anywhere in the app — login page lang switch,
   future sidenav lang pill, breadcrumb chips, footer labels —
   picks up the Arabic font automatically. Add `lang="ar"` to the
   wrapping span and the typography stays on-system. Mirrors iVE
   so the two products share one typographic ecosystem. */
[lang="ar"] { font-family: var(--font-ar); }

/* ══════════════════════════════════════════════════════════════
   AUTH PAGE
   ══════════════════════════════════════════════════════════════ */

.auth-body {
  min-height: 100vh;
  background: var(--bg-sunken);
  background-image:
    radial-gradient(1200px 600px at 110% -10%, color-mix(in oklab, var(--brand-500) 18%, transparent), transparent 60%),
    radial-gradient(900px 600px at -10% 110%, color-mix(in oklab, var(--brand-500) 14%, transparent), transparent 60%);
  color: var(--ink);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
}
[dir="rtl"] .auth-body { font-family: var(--font-ui-ar, var(--font-ui)); }
[data-theme="dark"] .auth-body {
  background-image:
    radial-gradient(1200px 600px at 110% -10%, color-mix(in oklab, var(--brand-500) 24%, transparent), transparent 60%),
    radial-gradient(900px 600px at -10% 110%, color-mix(in oklab, var(--brand-500) 18%, transparent), transparent 60%);
}

.auth-page {
  flex: 1;
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 16px;
  padding: 20px 28px 16px;
  min-height: 100vh;
  width: 100%;
  max-width: 1600px;
  margin-inline: auto;
  box-sizing: border-box;
}

/* Top bar */
.auth-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.auth-brand {
  display: flex;
  align-items: center;
  gap: 12px;
}
.auth-brand .tenant-icon {
  width: 144px;
  height: 36px;
}
.auth-controls {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.auth-ctl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 36px;
  min-width: 36px;
  padding: 0 12px;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-2);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.auth-ctl:hover { background: var(--brand-tint); color: var(--brand-700); border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line)); }
.auth-ctl > svg { color: currentColor; }
.auth-controls [data-tip]::after {
  bottom: auto;
  top: calc(100% + 8px);
  left: auto;
  right: auto;
  inset-inline-end: 0;
  transform: none;
}
/* Auth-controls tooltips drop BELOW the trigger (so they don't clip
   the viewport top edge) and are right-edge anchored (so they don't
   overflow the page right edge). Arrow flips: now points UP at the
   trigger, positioned near the right side of the tooltip so it
   roughly centers over the 36-52 px wide auth-ctl buttons. */
.auth-controls [data-tip]::before {
  bottom: auto;
  top: calc(100% + 3px);
  left: auto;
  right: auto;
  inset-inline-end: 14px;
  transform: none;
  border-top-color: transparent;
  border-bottom-color: var(--tooltip-bg);
}

/* Centered card */
.auth-main {
  display: grid;
  place-items: center;
  padding: 24px 0;
  min-height: 0;
}
.auth-card {
  width: 100%;
  max-width: 420px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 28px 28px 24px;
  box-shadow: var(--sh-lg);
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.auth-head {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* 22px matches iVE — the subtitle paragraph below the title row
     occupies most of this rhythm; the residual padding preserves
     the visual gap to the first form field. */
  padding-bottom: 22px;
}
.auth-head-top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
}

/* Platform brand mark inside the auth card */
.auth-product-logo {
  width: 66px;
  height: 24px;
  flex-shrink: 0;
  background-image: url('assets/ivp-light.svg');
  background-repeat: no-repeat;
  background-size: contain;
  background-position: center;
}
[data-theme="dark"] .auth-product-logo {
  background-image: url('assets/ivp-dark.svg');
}

.auth-title {
  margin: 0;
  font-size: var(--fsz-h1);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--ink);
}
[dir="rtl"] .auth-title { letter-spacing: 0; }
.auth-sub {
  margin: 0;
  font-size: var(--fsz-body);
  color: var(--ink-3);
  line-height: 1.4;
}
[dir="rtl"] .auth-sub { font-size: var(--fsz-body); }

/* Form fields */
.auth-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.auth-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink-2);
}
[dir="rtl"] .auth-label { font-size: var(--fsz-label); }
.auth-input {
  display: block;
  width: 100%;
  padding: 10px 12px;
  font-family: inherit;
  font-size: var(--fsz-body);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--t-fast), box-shadow var(--t-fast), background var(--t-fast);
  box-sizing: border-box;
}
.auth-input::placeholder { color: var(--ink-4); }
.auth-input:hover { border-color: var(--line-strong); }
.auth-input:focus {
  outline: 0;
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--brand-500) 35%, var(--line)),
    0 0 0 4px var(--brand-tint);
}

.auth-link {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--brand-700);
  text-decoration: none;
}
.auth-link:hover { text-decoration: underline; }

/* Password toggle */
.auth-pwd-wrap { position: relative; }
.auth-pwd-toggle {
  position: absolute;
  top: 1px;
  bottom: 1px;
  inset-inline-end: 1px;
  width: 40px;
  display: inline-grid;
  place-items: center;
  background: transparent;
  border: 0;
  color: var(--ink-3);
  cursor: pointer;
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  border-start-end-radius: calc(var(--r-md) - 1px);
  border-end-end-radius: calc(var(--r-md) - 1px);
  transition: background var(--t-fast), color var(--t-fast);
}
.auth-pwd-toggle:hover { background: var(--bg-sunken); color: var(--ink); }
.auth-pwd-wrap .auth-input { padding-inline-end: 44px; }

/* Remember me */
.auth-remember {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-label);
  color: var(--ink-2);
  cursor: pointer;
  user-select: none;
}
.auth-remember input { accent-color: var(--brand-600); }

/* Submit CTA */
.auth-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 16px;
  font-family: inherit;
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--brand-ink);
  background: var(--brand-600);
  border: 1px solid var(--brand-600);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), transform 80ms ease;
}
.auth-submit:hover { background: var(--brand-700); border-color: var(--brand-700); }
.auth-submit:active { transform: translateY(1px); }
.auth-submit:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* Error states */
.auth-error {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: color-mix(in oklab, var(--err) 10%, var(--bg-sunken));
  border: 1px solid color-mix(in oklab, var(--err) 35%, var(--line));
  border-radius: var(--r-md);
  font-size: var(--fsz-label);
  color: color-mix(in oklab, var(--err) 70%, var(--ink));
  line-height: 1.4;
}
[dir="rtl"] .auth-error { font-size: var(--fsz-body); }
.auth-error.is-hidden { display: none; }
.auth-error-mark {
  width: 20px; height: 20px;
  flex-shrink: 0;
  border-radius: 50%;
  background: color-mix(in oklab, var(--err) 18%, transparent);
  color: var(--err);
  display: inline-grid;
  place-items: center;
}
.auth-input.is-error {
  border-color: var(--err);
}
.auth-input.is-error:focus {
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--err) 50%, var(--line)),
    0 0 0 4px color-mix(in oklab, var(--err) 8%, transparent);
}

.auth-field-error {
  margin-top: 4px;
  min-height: 1.35em;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--err);
  line-height: 1.35;
}
[dir="rtl"] .auth-field-error { font-size: var(--fsz-label); }
.auth-field-error.is-hidden { visibility: hidden; }

/* Help line */
.auth-help {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: var(--fsz-label);
  color: var(--ink-3);
  padding-top: 2px;
}
[dir="rtl"] .auth-help { font-size: var(--fsz-label); }

/* Footer */
.auth-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  padding: 4px 0 8px;
}
[dir="rtl"] .auth-foot { font-size: var(--fsz-label); }
.auth-foot a {
  color: var(--brand-700);
  text-decoration: none;
}
.auth-foot a:hover { text-decoration: underline; }
.auth-foot-sep { color: var(--ink-4); user-select: none; }

/* Mobile */
@media (max-width: 480px) {
  .auth-page { padding: 14px 16px; }
  .auth-card { padding: 24px 22px 22px; max-width: 100%; }
  .auth-title { font-size: var(--fsz-h2); }
  .auth-brand .tenant-icon { width: 120px; height: 30px; }
}

/* ══════════════════════════════════════════════════════════════
   APP SHELL (sidenav + main)
   Used by home.html and every authenticated page going forward.
   ══════════════════════════════════════════════════════════════ */

:root {
  --sidenav-w-full: 256px;
  --sidenav-w-rail: 64px;
  --mobile-strip-h: 48px;
  --sidenav-row-gap: 4px;
  --sidenav-header-gap: 12px;
  --sidenav-footer-gap: 10px;
}

.app {
  min-height: 100vh;
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--bg);
  overflow-x: clip;
}

.app--sidenav {
  display: grid;
  grid-template-columns: var(--sidenav-w-full) 1fr;
  min-block-size: 100vh;
  min-block-size: 100dvh;
  background: var(--bg);
}

/* ─── Sidenav chrome (left rail) ─── */
.sidenav {
  grid-column: 1;
  grid-row: 1;
  position: sticky;
  top: 0;
  block-size: 100vh;
  block-size: 100dvh;
  background: var(--bg-raised);
  border-inline-end: 1px solid var(--line);
  display: grid;
  grid-template-rows: auto 1fr auto;
  z-index: calc(var(--z-sticky) - 10);
  overflow: visible;
}
.sidenav-header,
.sidenav-content,
.sidenav-footer { min-inline-size: 0; }

.sidenav-header {
  display: flex;
  flex-direction: column;
  padding-block-start: 16px;
  padding-block-end: 12px;
  padding-inline: 12px;
  border-block-end: 1px solid var(--line);
  gap: var(--sidenav-header-gap);
}
.sidenav-brand-mark {
  display: block;
  inline-size: 100%;
  block-size: 32px;
  background-image: url('assets/tenant/PL-logo-light.svg');
  background-repeat: no-repeat;
  background-size: contain;
  background-position: left center;
}
[data-theme="dark"] .sidenav-brand-mark {
  background-image: url('assets/tenant/PL-logo-dark.svg');
}
[dir="rtl"] .sidenav-brand-mark { background-position: right center; }

.sidenav-content {
  overflow-y: auto;
  overflow-x: hidden;
  padding-block: 12px;
  padding-inline: 12px;
  display: flex;
  flex-direction: column;
  gap: var(--sidenav-row-gap);
}

.sidenav-footer {
  padding: 12px;
  border-block-start: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--sidenav-footer-gap);
}

/* Sidenav skeleton primitives — sized to match the future real rows
   so the layout doesn't jump when the navbar mounts. */
.sn-skel-item    { block-size: 36px; border-radius: 8px; }
.sn-skel-section { block-size: 14px; border-radius: 4px; width: 60%; margin-block: 6px 2px; }
.sn-skel-tools {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}
.sn-skel-tool { block-size: 36px; border-radius: 8px; }

/* ─── Main content area ─── */
.main {
  grid-column: 2;
  grid-row: 1;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
}
.main > * {
  inline-size: 100%;
  max-inline-size: 1600px;
  margin-inline: auto;
}
.main.main--home {
  padding: 18px 24px 48px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* ─── Home page skeleton shell ─── */
.hp-shell {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.hp-body-2col {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: 16px;
}
.hp-body-left,
.hp-body-right {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-inline-size: 0;
}
.hp-kpis {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.hp-kpis > .skeleton {
  flex: 1 1 0;
  min-inline-size: 120px;
}

/* ─── Mobile top strip placeholder ─── */
.mobile-top-strip {
  display: none;
}

@media (max-width: 1100px) {
  .hp-body-2col { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 720px) {
  .app--sidenav {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: var(--mobile-strip-h) 1fr;
  }
  .mobile-top-strip {
    display: block;
    grid-column: 1;
    grid-row: 1;
    block-size: var(--mobile-strip-h);
    background: var(--bg-raised);
    border-block-end: 1px solid var(--line);
  }
  .sidenav { display: none; }
  .main { grid-column: 1; grid-row: 2; }
  .main.main--home { padding: 12px 12px 32px; gap: 14px; }
}

/* ══════════════════════════════════════════════════════════════
   FACE VERIFICATION (step 2 + 3 + 4 of the login flow)
   ══════════════════════════════════════════════════════════════ */

/* Step-switcher — keeps the same auth-card frame, swaps inner body. */
.auth-step { display: none; flex-direction: column; gap: 16px; }
.auth-step.is-active { display: flex; }

/* ── STEP 2 — Intro / camera permission gate ─────────────────── */
.fv-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 8px 0 4px;
  text-align: center;
}
.fv-intro-icon {
  inline-size: 72px;
  block-size: 72px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--brand-700);
  background: var(--brand-tint);
  border: 1px solid color-mix(in oklab, var(--brand-500) 18%, transparent);
}
.fv-intro-icon svg { inline-size: 32px; block-size: 32px; }
.fv-intro-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
}
.fv-intro-body {
  margin: 0;
  font-size: var(--fsz-body);
  color: var(--ink-3);
  line-height: 1.5;
  max-inline-size: 320px;
}
.fv-chips {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin-block-start: 2px;
}
.fv-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-2);
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
}
.fv-chip svg { inline-size: 12px; block-size: 12px; color: var(--brand-700); }

/* ── STEP 3 — Live capture (Face-ID-style ring + tick dial) ──── */
.fv-capture {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  padding: 4px 0 0;
}

/* Hidden by default — only surfaces if a check stalls. Brand-tinted
   informational nudge (not an error). */
.fv-prompt {
  display: none;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  background: var(--brand-tint);
  border: 1px solid color-mix(in oklab, var(--brand-500) 24%, var(--line));
  border-radius: var(--r-pill);
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--brand-700);
  line-height: 1.4;
}
.fv-prompt.is-visible { display: inline-flex; }
.fv-prompt svg { inline-size: 14px; block-size: 14px; flex-shrink: 0; }

/* ── Ring (the hero element) ──
   240px circular face frame wrapped by a tick-mark dial. The dial
   ticks are SVG <line>s generated in JS — 80 of them at 4.5° steps.
   Color states cascade through CSS classes on .fv-ring. */
.fv-ring {
  position: relative;
  inline-size: 248px;
  block-size: 248px;
  /* Subtle outer glow — gives the assembly a "floating" feel that
     reads premium. Pulled from --brand-500 so it picks up the tenant
     hue. Doubles in strength when capture is mid-flight (via
     .is-analysing class set by JS) and shifts hue to --ok on success. */
  filter: drop-shadow(0 18px 40px color-mix(in oklab, var(--brand-500) 14%, transparent));
  transition: filter 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.fv-ring.is-analysing {
  filter: drop-shadow(0 18px 50px color-mix(in oklab, var(--brand-500) 26%, transparent));
}
.fv-ring.is-ok {
  filter: drop-shadow(0 18px 50px color-mix(in oklab, var(--ok) 30%, transparent));
}

/* SVG layer hosts the tick dial. Sits behind/around the video. */
.fv-ring-svg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  pointer-events: none;
}
.fv-ring-svg line {
  stroke: var(--line-2);
  stroke-width: 2.2;
  stroke-linecap: round;
  /* Per-tick stagger via inline custom-property set in JS. Each tick
     transitions to its lit color with a small index-driven delay,
     producing the wave-fill effect across the dial. */
  transition: stroke 280ms cubic-bezier(0.4, 0, 0.2, 1) var(--fv-d, 0ms);
}
.fv-ring-svg line.is-lit-brand { stroke: var(--brand-600); }
.fv-ring-svg line.is-lit-ok    { stroke: var(--ok); }

/* Idle breathe — subtle perimeter pulse before any check has run.
   Compresses to "off" once the dial starts filling. */
.fv-ring-svg line.is-breathe { animation: fv-breathe 2.4s ease-in-out infinite; }
@keyframes fv-breathe {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .fv-ring-svg line.is-breathe { animation: none; opacity: 0.8; }
  .fv-ring-svg line { transition-duration: 0ms !important; transition-delay: 0ms !important; }
}

/* Inner circular video frame — sits inside the dial with breathing
   room for the ticks. Subtle inner shadow + brand-tinted halo
   behind the video gives the "scanning" depth. */
.fv-video-wrap {
  position: absolute;
  inset: 18px;
  border-radius: 50%;
  overflow: hidden;
  background:
    radial-gradient(circle at center,
      color-mix(in oklab, var(--brand-500) 12%, var(--bg-sunken)) 0%,
      var(--bg-sunken) 70%);
  /* Inner ring: thin 1px halo at the video edge defines the cut
     cleanly. Outer ring (negative offset shadow) carves a very subtle
     inset that gives the video edge depth. */
  box-shadow:
    inset 0 0 0 1px color-mix(in oklab, var(--ink) 6%, transparent),
    inset 0 0 24px color-mix(in oklab, var(--ink) 5%, transparent);
}
.fv-video-wrap video {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
  /* Mirror — natural selfie-view. */
  transform: scaleX(-1);
  opacity: 0;
  transition: opacity 360ms cubic-bezier(0.16, 1, 0.3, 1);
}
.fv-video-wrap video.is-ready { opacity: 1; }

/* On success: ring crowns the moment with a brief scale-up + the
   green halo (via .is-ok above). */
.fv-ring.is-ok { animation: fv-ring-pop 520ms cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes fv-ring-pop {
  0%   { transform: scale(1);    }
  50%  { transform: scale(1.04); }
  100% { transform: scale(1);    }
}
@media (prefers-reduced-motion: reduce) {
  .fv-ring.is-ok { animation: none; }
}

/* LIVE pulse — anchored to the top of the ring's perimeter. */
.fv-live {
  position: absolute;
  inset-block-start: -10px;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  background: var(--bg-raised);
  color: var(--ink-2);
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--sh-sm);
}
[dir="rtl"] .fv-live { transform: translateX(50%); }
.fv-live::before {
  content: '';
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 0 var(--ok);
  animation: fv-pulse 1.6s ease-out infinite;
}
.fv-ring.is-ok .fv-live::before { animation: none; }
@keyframes fv-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklab, var(--ok) 50%, transparent); }
  100% { box-shadow: 0 0 0 8px color-mix(in oklab, var(--ok) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .fv-live::before { animation: none; }
}

/* Centered check-icon inside the ring on success. Only visible
   briefly before the step transitions to step-success. */
.fv-ring-check {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--ok);
  pointer-events: none;
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 280ms ease, transform 480ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.fv-ring-check svg { inline-size: 56px; block-size: 56px;
  filter: drop-shadow(0 4px 14px color-mix(in oklab, var(--ok) 40%, transparent));
}
.fv-ring.is-ok .fv-ring-check {
  opacity: 1;
  transform: scale(1);
}
.fv-ring.is-ok .fv-video-wrap video { opacity: 0.55; }

/* ── Instruction block below the ring ──
   Slot order: animated gesture icon → title → sub. min-block-size
   locks vertical space so swapping content doesn't shift the layout
   underneath. */
.fv-instruction {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-block-size: 110px;
}
.fv-instruction-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
  transition: opacity 220ms ease, transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
}
.fv-instruction-sub {
  margin: 0;
  font-size: var(--fsz-label);
  color: var(--ink-3);
  transition: opacity 220ms ease;
}
.fv-instruction.is-swapping .fv-instruction-title,
.fv-instruction.is-swapping .fv-instruction-sub,
.fv-instruction.is-swapping .fv-gesture-frame.is-active {
  opacity: 0;
  transform: translateY(4px);
}
@media (prefers-reduced-motion: reduce) {
  .fv-instruction-title, .fv-instruction-sub { transition: none; }
}

/* ── Gesture icon — stack of frames, only the active one is shown.
   Each frame is a stylized 56×56 line-drawing face with internal
   parts that animate to demonstrate the requested gesture. */
.fv-gesture {
  position: relative;
  inline-size: 56px;
  block-size: 56px;
  color: var(--brand-700);
}
.fv-gesture-frame {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  transition: opacity 220ms ease, transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
}
.fv-gesture-frame.is-active { display: flex; }
.fv-gesture-frame svg {
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}
.fv-gesture-frame svg path,
.fv-gesture-frame svg ellipse,
.fv-gesture-frame svg circle {
  vector-effect: non-scaling-stroke;
}

/* `.gst-features` wraps the inner features (eyes + mouth) so they
   shift as a unit while the head ellipse stays in place — reads as
   "the user is looking off to the side", not "the head is tilting".
   That matches the actual gesture the user performs. */
.gst-features { transform-origin: center; transform-box: fill-box; }
.gst-eye      { transform-origin: center; transform-box: fill-box; }
.gst-mouth-curve, .gst-mouth-line { transition: opacity 220ms ease; }

/* — look_left / look_right —
   Features (eyes + mouth) translate ±7px and back, holding the
   shifted pose so the direction reads cleanly. 2.0s loop with
   neutral rest beats — quick enough to feel responsive, slow
   enough to perceive. The motion arcs (below) sync to this loop. */
.gst-look-left  .gst-features { animation: gst-look-left  2.0s ease-in-out infinite; }
.gst-look-right .gst-features { animation: gst-look-right 2.0s ease-in-out infinite; }
@keyframes gst-look-left {
  0%, 18%, 78%, 100% { transform: translateX(0); }
  38%, 58%           { transform: translateX(-7px); }
}
@keyframes gst-look-right {
  0%, 18%, 78%, 100% { transform: translateX(0); }
  38%, 58%           { transform: translateX(7px); }
}

/* Motion arcs — three staggered "C"-shaped strokes on the side of
   the face that fade in, slide outward, and fade out. Reads as a
   wave of motion in the look direction (like a comic-style speed
   line). All three arcs sit clear of the head ellipse with a ~4 px
   gap; the third arc is intentionally smaller (8 px vs 12/14 px
   tall) and a touch thinner stroke so it reads as the tail of the
   wave. Synced to the 2.0 s look loop so the arcs peak while
   features are at their extreme. */
.gst-motion-arc {
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}
.gst-motion-arc[data-i="3"] { stroke-width: 1.6; }

.gst-look-left  .gst-motion-arc[data-i="1"] { animation: gst-flow-left-1  2.0s ease-in-out infinite; }
.gst-look-left  .gst-motion-arc[data-i="2"] { animation: gst-flow-left-2  2.0s ease-in-out infinite; }
.gst-look-left  .gst-motion-arc[data-i="3"] { animation: gst-flow-left-3  2.0s ease-in-out infinite; }
.gst-look-right .gst-motion-arc[data-i="1"] { animation: gst-flow-right-1 2.0s ease-in-out infinite; }
.gst-look-right .gst-motion-arc[data-i="2"] { animation: gst-flow-right-2 2.0s ease-in-out infinite; }
.gst-look-right .gst-motion-arc[data-i="3"] { animation: gst-flow-right-3 2.0s ease-in-out infinite; }

/* Arc 1: closest to the face — appears first as the features start
   shifting, fades out as they hit the apex. Brightest peak. */
@keyframes gst-flow-left-1 {
  0%, 100%  { opacity: 0; transform: translateX(2px); }
  22%, 42%  { opacity: 0.9;  transform: translateX(0); }
  60%       { opacity: 0;    transform: translateX(-3px); }
}
@keyframes gst-flow-right-1 {
  0%, 100%  { opacity: 0; transform: translateX(-2px); }
  22%, 42%  { opacity: 0.9;  transform: translateX(0); }
  60%       { opacity: 0;    transform: translateX(3px); }
}
/* Arc 2: middle — appears slightly later, peaks a touch dimmer. */
@keyframes gst-flow-left-2 {
  0%, 100%  { opacity: 0; transform: translateX(2px); }
  35%, 55%  { opacity: 0.7;  transform: translateX(0); }
  75%       { opacity: 0;    transform: translateX(-4px); }
}
@keyframes gst-flow-right-2 {
  0%, 100%  { opacity: 0; transform: translateX(-2px); }
  35%, 55%  { opacity: 0.7;  transform: translateX(0); }
  75%       { opacity: 0;    transform: translateX(4px); }
}
/* Arc 3: tail — smallest, dimmest, latest. Reinforces the
   "wave outward" feel as a fading trail. */
@keyframes gst-flow-left-3 {
  0%, 100%  { opacity: 0; transform: translateX(2px); }
  50%, 68%  { opacity: 0.5;  transform: translateX(0); }
  88%       { opacity: 0;    transform: translateX(-5px); }
}
@keyframes gst-flow-right-3 {
  0%, 100%  { opacity: 0; transform: translateX(-2px); }
  50%, 68%  { opacity: 0.5;  transform: translateX(0); }
  88%       { opacity: 0;    transform: translateX(5px); }
}

/* — blink — eyes scale vertically to 0.08 twice per loop, with
   spacing between blinks. */
.gst-blink .gst-eye { animation: gst-blink 1.8s ease-in-out infinite; }
@keyframes gst-blink {
  0%, 32%, 50%, 68%, 100% { transform: scaleY(1); }
  40%, 60%                { transform: scaleY(0.08); }
}

/* — smile — neutral mouth-line cross-fades with the smile-curve.
   Longer 2.4s loop with the smile held from 32% to 68% (~0.86s on
   screen) so the user can clearly see it appear, hold, and relax. */
.gst-smile .gst-mouth-line  { animation: gst-mouth-out 2.4s ease-in-out infinite; }
.gst-smile .gst-mouth-curve { animation: gst-mouth-in  2.4s ease-in-out infinite; opacity: 0; }
@keyframes gst-mouth-out { 0%, 18%, 82%, 100% { opacity: 1; } 32%, 68% { opacity: 0; } }
@keyframes gst-mouth-in  { 0%, 18%, 82%, 100% { opacity: 0; } 32%, 68% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .gst-look-left .gst-features,
  .gst-look-right .gst-features,
  .gst-blink .gst-eye,
  .gst-smile .gst-mouth-line,
  .gst-smile .gst-mouth-curve,
  .gst-motion-arc {
    animation: none;
  }
  /* Static shifted pose for look gestures so the direction still
     reads even without motion. Motion arcs visible at rest. */
  .gst-look-left  .gst-features    { transform: translateX(-4px); }
  .gst-look-right .gst-features    { transform: translateX(4px); }
  .gst-look-left  .gst-motion-arc,
  .gst-look-right .gst-motion-arc  { opacity: 0.6; }
  .gst-smile .gst-mouth-line  { opacity: 0; }
  .gst-smile .gst-mouth-curve { opacity: 1; }
}

/* ── STEP 3b — Failure ──────────────────────────────────────────
   Triggered when the real liveness / identity / quality check
   fails. Three reasons, three messages, one card shape. Reuses
   the success-step composition with status tokens swapped to
   --err for the icon disc + halo. */
.fv-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 8px 0 4px;
  text-align: center;
}
.fv-error-icon {
  inline-size: 72px;
  block-size: 72px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--err);
  background: var(--err-soft);
  border: 1px solid color-mix(in oklab, var(--err) 28%, transparent);
  animation: fv-pop 360ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.fv-error-icon svg { inline-size: 32px; block-size: 32px; }

/* Identity-error icon — "Do Not Enter" road-sign treatment. The
   disc itself flips to solid --err red; the inner SVG provides the
   white horizontal bar. Visually breaks from the other failure
   cards (which all share the soft-disc + bold-stroke icon pattern)
   to signal that identity is the security boundary, not just
   another retryable miss. */
.fv-error-icon[data-reason="identity"] {
  background: var(--err);
  /* Slightly darker rim than the disc for subtle depth — a thin
     line between the disc and the page bg that reads as the sign's
     metal edge without being heavy. */
  border-color: color-mix(in oklab, var(--err) 70%, oklch(0 0 0));
  color: oklch(0.99 0 0);
}
/* The default `.fv-error-icon svg` size (32 px) leaves the white bar
   looking thin on a 72 px disc. Bump to 64 px so the bar matches
   the proportions of an actual "Do Not Enter" sign — ~66 % disc
   width, ~18 % disc height. */
.fv-error-icon[data-reason="identity"] svg {
  inline-size: 64px;
  block-size: 64px;
}
/* The bar slides in horizontally AFTER the disc pop-in (which runs
   0-360 ms via .fv-error-icon's fv-pop animation), so the user
   reads it as: disc lands → gate-bar locks across. Spring overshoot
   easing gives the bar a small "snap" at the apex. */
.fv-error-icon[data-reason="identity"] svg rect {
  transform-origin: 50% 50%;
  transform-box: fill-box;
  animation: fv-id-bar-in 380ms 200ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}
@keyframes fv-id-bar-in {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .fv-error-icon { animation: none; }
  .fv-error-icon[data-reason="identity"] svg rect { animation: none; }
}
.fv-error-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
}
.fv-error-sub {
  margin: 0;
  font-size: var(--fsz-body);
  color: var(--ink-3);
  line-height: 1.5;
  max-inline-size: 320px;
}

/* ── STEP 4 — Success ────────────────────────────────────────── */
.fv-success {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px 0 8px;
  text-align: center;
}
.fv-success-icon {
  inline-size: 72px;
  block-size: 72px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--ok);
  background: var(--ok-soft);
  border: 1px solid color-mix(in oklab, var(--ok) 28%, transparent);
  animation: fv-pop 360ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.fv-success-icon svg { inline-size: 32px; block-size: 32px; }
@keyframes fv-pop {
  from { opacity: 0; transform: scale(0.7); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .fv-success-icon { animation: none; }
}
.fv-success-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
}
.fv-success-sub {
  margin: 0;
  font-size: var(--fsz-body);
  color: var(--ink-3);
}
.fv-success-spinner {
  inline-size: 22px;
  block-size: 22px;
  border-radius: 50%;
  border: 2px solid var(--line);
  border-top-color: var(--brand-600);
  animation: fv-spin 0.8s linear infinite;
  margin-block-start: 6px;
}
@media (prefers-reduced-motion: reduce) {
  .fv-success-spinner { animation: none; }
}

/* ── Inline back-to-credentials link (steps 2 + 3) ───────────── */
/* Same visual treatment as `.auth-link` (Forgot password? / Contact
   your administrator) — secondary text actions share one style so
   "Use a different account" lives in the same vocabulary. The only
   reason this is a separate selector at all is the chevron icon
   that sits next to the label and the <button> element reset. */
.auth-back-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--brand-700);
  text-decoration: none;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
}
.auth-back-link:hover { text-decoration: underline; }
.auth-back-link svg { inline-size: 14px; block-size: 14px; }
[dir="rtl"] .auth-back-link svg { transform: scaleX(-1); }

/* Camera-error state — surfaces getUserMedia() failures (denied,
   no device, etc). Reuses auth-error visual vocabulary but lives
   inside the capture step. */
.fv-cam-error { display: none; }
.fv-cam-error.is-visible { display: flex; }

/* ─── Entrance animations ─── */
@keyframes auth-fade-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes auth-fade-up-card {
  from { opacity: 0; transform: translateY(20px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.auth-top   { animation: auth-fade-up      420ms cubic-bezier(0.16, 1, 0.3, 1) backwards; animation-delay: 60ms; }
.auth-card  { animation: auth-fade-up-card 540ms cubic-bezier(0.16, 1, 0.3, 1) backwards; animation-delay: 140ms; }
.auth-foot  { animation: auth-fade-up      420ms cubic-bezier(0.16, 1, 0.3, 1) backwards; animation-delay: 320ms; }
@media (prefers-reduced-motion: reduce) {
  .auth-top, .auth-card, .auth-foot { animation: none; }
}
