/* =========================================================================
   liveverify — verification flow UI
   =========================================================================

   Deliberately BRAND-NEUTRAL. This flow is intended to be offered to third
   parties as an embeddable identity check (see LIVEVERIFY_OPTICAL_BETA_SCOPE.md's
   "Agreed order of work", item 6), so it must not read as SignedBy's own
   product the way the dashboard does. Everything a relying party would want
   to change is one token: --brand. Nothing else in this file hard-codes an
   accent, so re-skinning is a single line, not a hunt.

   The default --brand (#3b5bdb) and --ink (#12203f) are lifted from the
   SignedBy transactional-email wordmark ("Signed" in ink, "By" in blue), which
   is already the most customer-facing surface the product has -- so the
   default skin is consistent with what a verifying user has just been emailed,
   without the dashboard's yellow badge making it look like an internal tool.

   Light throughout, with ONE deliberate exception: the camera stage is
   near-black (--stage). That isn't a style choice -- a light panel behind live
   video washes out the preview and reflects off the user's face during the
   liveness step. Stripe Identity does the same thing for the same reason.
   ========================================================================= */

:root {
  /* The one token a third party would swap -- see LIVEVERIFY_API_DESIGN.md
     §8 step 6 (2026-09-07): app.js's applySessionBranding() sets exactly
     this one custom property from an org's brand_color, and relies on
     --brand-strong/--brand-soft being DERIVED from it rather than
     separate literals, so a tenant override actually reaches every place
     --brand is used, not just the two spots that reference the base
     token directly. The three literal values below (used until a session
     carries a brand override, and always for the default skin) are the
     resolved color-mix() outputs at the default --brand, kept in sync by
     hand -- see that file header's own note on why this stays a literal
     default. */
  --brand: #3b5bdb;
  --brand-strong: color-mix(in srgb, var(--brand) 82%, black);
  --brand-soft: color-mix(in srgb, var(--brand) 10%, white);

  --ink: #12203f;
  --body: #3c4658;
  --muted: #8a93a6;

  --surface: #ffffff;
  --canvas: #f6f8fb;
  --line: #e4e9f0;
  --line-soft: #eef1f6;

  --good: #0e9f6e;
  --good-soft: #e8f6f0;
  --bad: #d92d20;
  --bad-soft: #fdeceb;
  --warn: #b54708;

  /* Camera stage -- see file header. */
  --stage: #0b0f17;

  --radius: 12px;
  --radius-lg: 18px;
  --shadow-card: 0 1px 2px rgba(18, 32, 63, 0.04), 0 8px 24px rgba(18, 32, 63, 0.06);

  /* System stack, matching signedby-app's own deliberate no-webfont choice
     (see its globals.css) -- renders instantly, no loading flash, one less
     network dependency for a flow that runs on strangers' phones. */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

  font-family: var(--font-sans);
  color-scheme: light;
}

* { box-sizing: border-box; }

html, body { min-height: 100%; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
}

a { color: var(--brand); }

/* Dev-only chrome. The harness and the real deployment serve the SAME
   index.html (see server.mjs / build-public.mjs and the deployment-mode meta
   tag); app.js stamps data-mode on <html> so production can hide the
   developer-facing header without a second template to keep in sync. */
[data-mode="serverless"] .dev-only { display: none !important; }

/* ---------------------------------------------------------------- shell -- */

#harness { min-height: 100vh; }

main {
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  padding: 0 20px 48px;
}

main[hidden] { display: none; }

/* The product header: wordmark only. No navigation, no marketing -- a person
   mid-verification has exactly one job, and anything else here is a way to
   leave. */
.flow-header {
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  padding: 22px 20px 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.flow-brand {
  display: flex;
  align-items: center;
  min-width: 0;
}
.wordmark {
  font-size: 21px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0;
}
.wordmark .id { color: var(--brand); }
/* Org logo, swapped in for #flowWordmark once branding resolves with one
   -- same slot, same vertical rhythm as the wordmark it replaces (21px
   text line-height), not the smaller 22px badge size the old in-card
   logo used. */
.flow-brand-logo {
  height: 26px;
  max-width: 160px;
  object-fit: contain;
}

.flow-secure {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
}
.flow-secure svg { width: 13px; height: 13px; flex: none; }

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

header.dev-only {
  padding: 18px 20px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
header h1 {
  margin: 0 0 4px;
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
}
header p {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.55;
  max-width: 640px;
}
header p code {
  color: var(--brand);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
}
header p.build-tag {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  color: var(--muted);
  margin-bottom: 8px;
}

/* -------------------------------------------------------------- progress -- */

/* Segmented bar rather than labelled chips: on a phone the step title is
   already stated directly below in .step-title, so repeating it here spent a
   whole line to say the same thing twice. The segments answer only "how far
   through am I", which is the one thing the title can't say. Pill text is kept
   in the DOM (zeroed, not removed) so it still reaches assistive tech. */
.steps {
  display: flex;
  gap: 6px;
  padding: 4px 0 18px;
}
.step-pill {
  flex: 1 1 0;
  height: 4px;
  border-radius: 999px;
  background: var(--line);
  font-size: 0;
  line-height: 0;
  color: transparent;
  overflow: hidden;
  transition: background-color 0.25s ease;
}
.step-pill.active { background: var(--brand); }
.step-pill.done { background: rgba(59, 91, 219, 0.45); }

/* ----------------------------------------------------------------- panel -- */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 20px;
  box-shadow: var(--shadow-card);
}

.step-title {
  margin: 0 0 6px;
  font-size: 21px;
  font-weight: 650;
  letter-spacing: -0.02em;
  line-height: 1.25;
  color: var(--ink);
  text-wrap: balance;
}
.step-desc {
  margin: 0 0 18px;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--body);
}

/* --------------------------------------------------- camera stage (fn) --- */

.video-wrap {
  position: relative;
  width: 100%;
  /* Fixed 1:1 square, deliberately -- not matched to the camera's native
     resolution (an earlier version of this rule did that instead, see git
     history). The <video> element's own object-fit:cover crops the live
     preview to this square reliably (well-supported natively for <video>);
     the <canvas class="overlay"> element instead gets its OWN width/height
     attributes set to that same square crop size in ensureCamera(), so its
     intrinsic aspect ratio always exactly matches this 1:1 box regardless of
     how reliable object-fit support for <canvas> specifically turns out to be
     on a given browser -- see getVideoCropRect() in app.js, which every piece
     of landmark/alignment/crop math in this file is threaded through so it
     stays correct against this square crop rather than the full native
     frame. */
  aspect-ratio: 1 / 1;
  background: var(--stage);
  border-radius: var(--radius);
  overflow: hidden;
}
video, canvas.overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
canvas.overlay { pointer-events: none; }
/* Mirror the preview selfie-style ONLY for the front camera (face steps).
   The rear camera (card steps) must NOT be mirrored -- a real rear-camera
   viewfinder never is, and mirroring it makes the card appear to move
   opposite to how you're actually moving it, which is exactly backwards for
   something you're trying to align by hand. .video-wrap.mirrored is toggled in
   app.js based on which camera is actually active. Capture logic in app.js
   reads the raw (always unmirrored) frame data either way, so this only
   affects the on-screen preview, never what's saved. */
.video-wrap.mirrored video, .video-wrap.mirrored canvas.overlay { transform: scaleX(-1); }

/* The review still shown after a capture (auto or manual), before the step
   actually advances -- see showReview()/hideReview() in app.js. Deliberately
   never mirrored, whatever the live preview was: this is the actual saved
   frame (captureFrame() always reads unmirrored data), so what you review here
   is exactly what gets submitted. */
.review-img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Card steps get the cropped-to-the-card image (see app.js's
   captureCardReviewCrop()) shown with "contain" rather than "cover" -- "cover"
   would fill the box by cropping the longer side, which for an ~1.586:1 card
   image inside a square box would slice back into the card itself, defeating
   the point of zooming in to review it. "contain" letterboxes instead, so the
   whole card is always visible. */
.review-img.contain-fit {
  object-fit: contain;
  background: var(--stage);
}

/* Card-alignment guide: a box at a real ID card's aspect ratio (ISO/IEC 7810
   ID-1, ~85.6mm x 54mm = 1.586:1) so there's something concrete to line the
   card up against, instead of an unguided video feed. Drawn as 4 corner-only
   brackets rather than a full rectangle -- reads less like it's trying to
   frame the WHOLE video. .card-guide positions the (invisible) box the corners
   sit on and holds the shared --card-guide-color custom property that
   setCardGuideScore() drives in app.js (amber -> green as alignment improves;
   that's live feedback, not branding, so it stays independent of --brand). */
.card-guide {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 84%;
  aspect-ratio: 1.586 / 1;
  pointer-events: none;
  display: block;
  --card-guide-color: rgba(232, 179, 57, 0.9);
}
.card-guide-corner {
  position: absolute;
  width: 32px;
  height: 32px;
  border: 4px solid var(--card-guide-color);
  transition: border-color 0.15s linear;
}
.card-guide-corner.tl { top: -5px; left: -5px; border-right: none; border-bottom: none; border-top-left-radius: 10px; }
.card-guide-corner.tr { top: -5px; right: -5px; border-left: none; border-bottom: none; border-top-right-radius: 10px; }
.card-guide-corner.bl { bottom: -5px; left: -5px; border-right: none; border-top: none; border-bottom-left-radius: 10px; }
.card-guide-corner.br { bottom: -5px; right: -5px; border-left: none; border-top: none; border-bottom-right-radius: 10px; }

/* The face steps' progress indicator is drawn entirely on the
   <canvas class="overlay"> element -- see app.js's drawFaceOvalGuide() -- not
   built from DOM/CSS the way the card guide above is, so there's no
   corresponding style block here. */

/* The pre-selfie explainer's illustration -- see app.js's
   renderSelfieIntro(). Sits where the camera would be, at roughly the same
   visual weight, so the step doesn't lurch when the video replaces it. */
.selfie-intro {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 0 40px;
}
.selfie-intro[hidden] { display: none; }
.selfie-intro svg {
  width: 96px;
  height: 96px;
  color: var(--ink);
}

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

.status-line {
  margin-top: 12px;
  margin-bottom: 0;
  font-size: 14px;
  min-height: 20px;
  color: var(--muted);
  text-align: center;
}
.status-line.good { color: var(--good); }
.status-line.warn { color: var(--warn); }
.status-line.bad { color: var(--bad); }
/* The min-height above reserves space so text appearing mid-capture doesn't
   shift the layout -- but on steps with no camera (the submit/result screen)
   it's just a gap. renderStep() already hides this line outright during
   camera-active steps, so collapsing it when empty costs no stability. */
.status-line:empty { min-height: 0; margin-top: 0; }

/* Same status text as .status-line (see app.js's setStatus(), which sets both
   at once), but overlaid directly on the video as a floating pill -- keeps the
   feedback where a user's eyes actually are while framing a shot rather than
   only below the video. renderStep() hides .status-line during camera-active
   steps so the two never show the same text twice; this element hides itself
   the rest of the time via :empty (never a bare floating pill with nothing in
   it). */
.status-overlay {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  max-width: 88%;
  background: rgba(9, 13, 20, 0.78);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  padding: 10px 18px;
  border-radius: 999px;
  text-align: center;
  line-height: 1.4;
  pointer-events: none;
}
.status-overlay:empty { display: none; }
.status-overlay.good { color: #6ee7b7; }
.status-overlay.warn { color: #fcd34d; }
.status-overlay.bad { color: #fca5a5; }

/* --------------------------------------------------------------- buttons -- */

.controls {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 18px;
}

button {
  appearance: none;
  width: 100%;
  min-height: 52px;
  padding: 14px 20px;
  font-family: inherit;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
button.primary {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
button:disabled { opacity: 0.45; cursor: not-allowed; }
button:not(:disabled):hover { background: var(--canvas); }
button.primary:not(:disabled):hover { background: var(--brand-strong); border-color: var(--brand-strong); }

:where(button, a, input):focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- thumbs -- */

.thumbs { display: flex; gap: 8px; margin-top: 18px; flex-wrap: wrap; }
.thumb {
  width: 52px;
  height: 52px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background-color: var(--canvas);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

/* ---------------------------------------------------------------- result -- */

.verify-result {
  width: 100%;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  padding: 18px;
  background: var(--surface);
  font-size: 14.5px;
}
.verify-result.pass { border-color: rgba(14, 159, 110, 0.35); background: var(--good-soft); }
.verify-result.fail { border-color: rgba(217, 45, 32, 0.3); background: var(--bad-soft); }
.verify-banner {
  font-size: 19px;
  font-weight: 680;
  letter-spacing: -0.02em;
  margin-bottom: 10px;
}
.verify-result.pass .verify-banner { color: var(--good); }
.verify-result.fail .verify-banner { color: var(--bad); }
.verify-reasons { margin: 0 0 12px; padding-left: 20px; color: var(--body); }
.verify-reasons li { margin-bottom: 4px; }
.verify-facematch { margin-top: 10px; font-size: 13px; color: var(--muted); line-height: 1.5; }
.verify-facematch strong { color: var(--body); }
.verify-recourse-link {
  display: inline-block;
  margin-top: 14px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.85em;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.verify-recourse-link:hover { color: var(--brand); }

.verify-recourse-dialog {
  max-width: 30rem;
  padding: 20px 22px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  color: var(--body);
}
.verify-recourse-dialog::backdrop { background: rgba(18, 32, 63, 0.45); }
.verify-recourse-dialog h2 {
  margin: 0 0 10px;
  font-size: 1.05rem;
  color: var(--ink);
}
.verify-recourse-dialog p { margin: 0 0 10px; line-height: 1.5; }
.verify-recourse-note { font-size: 0.9em; color: var(--muted); }
.verify-recourse-close {
  margin-top: 6px;
  padding: 8px 16px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--canvas);
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}

.verify-checks { margin-top: 12px; }
.verify-checks ul { margin: 0; padding-left: 20px; }
.verify-identity {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid rgba(18, 32, 63, 0.1);
  line-height: 1.6;
}
.verify-identity strong { color: var(--ink); display: block; margin-bottom: 2px; }

/* ----------------------------------------------------------------- notes -- */

pre.result {
  margin-top: 16px;
  padding: 14px;
  background: var(--ink);
  color: #dbe3f0;
  border-radius: var(--radius);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.5;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.note {
  margin-top: 14px;
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
}

.session-note {
  margin-top: 14px;
  padding: 12px 14px;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
}

/* ------------------------------------------- landing / consent screens --- */

/* Shared by #landing and #consent: a single centred card, nothing else on
   screen. Both are decision points -- start, or agree -- and a decision screen
   with one card and one primary button is read correctly at a glance. */
.center-card-screen {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px 20px 40px;
}
/* display:flex above would otherwise beat the user-agent's [hidden] rule and
   leave these screens impossible to hide by attribute -- which is exactly how
   app.js and landing.js toggle them. Caught by rendering the flow headlessly
   rather than on a device, which is the cheap place to catch it. */
.center-card-screen[hidden] { display: none; }

.landing-card {
  width: 100%;
  max-width: 440px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 28px 24px;
  text-align: center;
}

.landing-lockup {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
  margin: 0 0 20px;
}

.landing-eyebrow {
  margin: 0;
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.1;
}
.landing-eyebrow .id { color: var(--brand); }

.landing-tag {
  margin: 3px 0 0;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--muted);
}

/* Tenant attribution on the consent gate -- LIVEVERIFY_API_DESIGN.md §8
   step 6 (2026-09-07). Deliberately small and above the fold's actual
   headline (.landing-title keeps top billing): this is "who's asking",
   the same supporting role Stripe Identity gives the requesting
   merchant's name on its own hosted consent screen, not a replacement
   for LiveVerify's own identity. Hidden by default in the markup;
   app.js's applySessionBranding() only reveals it once branding data
   for the session's org has actually resolved, so there is never a
   flash of an empty row. */
.consent-branding {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 0 16px;
}
.consent-branding-line {
  margin: 0;
  font-size: 13px;
  font-weight: 550;
  color: var(--muted);
}

.landing-title {
  margin: 0 0 10px;
  font-size: 25px;
  line-height: 1.2;
  font-weight: 680;
  letter-spacing: -0.025em;
  color: var(--ink);
  text-wrap: balance;
}
.landing-copy {
  margin: 0 0 22px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--body);
}

.landing-state { display: block; }
.landing-state[hidden] { display: none; }

.landing-status { font-size: 14.5px; color: var(--muted); margin: 0; }
.landing-heading { margin: 0 0 8px; font-size: 17px; font-weight: 650; color: var(--ink); }
.landing-sub { margin: 0 0 18px; font-size: 14.5px; line-height: 1.55; color: var(--body); }

.landing-primary-btn { background: var(--brand); border-color: var(--brand); color: #fff; }
.landing-primary-btn:not(:disabled):hover { background: var(--brand-strong); border-color: var(--brand-strong); }
.landing-secondary-btn { background: var(--surface); }

.consent-actions { display: flex; flex-direction: column; gap: 10px; }

/* What the consent screen actually promises, itemised -- a wall of prose gets
   skimmed, and consent that was skimmed isn't really consent. */
.consent-points {
  list-style: none;
  margin: 0 0 22px;
  padding: 0;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.consent-points li {
  position: relative;
  padding-left: 26px;
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--body);
}
.consent-points li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  top: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

.waitlist-form { display: flex; flex-direction: column; gap: 10px; }
.waitlist-form input[type="email"] {
  width: 100%;
  min-height: 52px;
  padding: 14px 16px;
  font-family: inherit;
  font-size: 16px; /* 16px min, or iOS Safari zooms the whole page on focus */
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.waitlist-form input[type="email"]::placeholder { color: var(--muted); }
.waitlist-form input[type="email"]:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.waitlist-form button { background: var(--brand); border-color: var(--brand); color: #fff; }
.waitlist-form button:not(:disabled):hover { background: var(--brand-strong); border-color: var(--brand-strong); }
.waitlist-form button:disabled { opacity: 0.45; }

.landing-note { margin: 14px 0 0; font-size: 13px; line-height: 1.55; color: var(--muted); }
.landing-login { margin: 16px 0 0; font-size: 13.5px; color: var(--muted); }
.landing-login a { color: var(--brand); font-weight: 550; }

.landing-qr-img {
  width: 220px;
  height: 220px;
  display: block;
  margin: 0 auto 4px;
  padding: 12px;
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  animation: qr-pulse 2.4s ease-in-out infinite;
}

/* Brand-blue breathing glow -- signals "this code is live, waiting for a
   scan," not just decoration. Neutralized by the prefers-reduced-motion
   rule below (it zeroes every animation-duration). */
@keyframes qr-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(59, 91, 219, 0.22), 0 0 16px 2px rgba(59, 91, 219, 0.12); }
  50% { box-shadow: 0 0 0 0 rgba(59, 91, 219, 0.4), 0 0 26px 6px rgba(59, 91, 219, 0.3); }
}

.landing-error-text { margin: 0 0 16px; font-size: 14.5px; color: var(--bad); line-height: 1.55; }

/* Below the card on the landing/consent screens: says who is running the
   check without making the check itself look like an ad for them. */
.flow-footer {
  margin: 18px auto 0;
  max-width: 440px;
  text-align: center;
  font-size: 12.5px;
  color: var(--muted);
}

/* -------------------------------------------------- language + privacy --
   LIVEVERIFY_I18N_SCOPE.md §5 (2026-09-07). Two peer links, equal weight --
   see §1's note on Stripe giving its language switcher the same billing
   as its privacy link, not tucking it in a corner. */
.consent-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 20px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
  font-size: 12.5px;
}
.consent-footer-sep { color: var(--line); }
/* Overrides the base `button` rule (block, 52px, bordered) -- both the
   privacy <a> and the language <button> read as plain inline links, not
   as a second pair of action buttons competing with Agree/Decline. */
.consent-footer-link {
  appearance: none;
  width: auto;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  text-decoration: none;
  cursor: pointer;
}
.consent-footer-link:hover { color: var(--brand); background: none; }
.consent-footer-link:focus-visible { outline-offset: 3px; }

/* ------------------------------------------------------- language picker --
   A full-height screen, not a card -- see language-picker.js's own note on
   why it's a sibling of #consent rather than nested in .landing-card. */
.language-picker {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.language-picker[hidden] { display: none; }

.language-picker-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 20px;
  border-bottom: 1px solid var(--line-soft);
}
.language-picker-back {
  appearance: none;
  width: 36px;
  min-height: 36px;
  height: 36px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  flex-shrink: 0;
}
.language-picker-back svg { width: 18px; height: 18px; }
.language-picker-title {
  margin: 0;
  font-size: 16px;
  font-weight: 650;
  color: var(--ink);
}

.language-picker-list {
  flex: 1 1 auto;
  overflow-y: auto;
  max-width: 480px;
  width: 100%;
  margin: 0 auto;
  padding: 8px 20px 32px;
}
.language-picker-heading {
  margin: 18px 0 6px;
  font-size: 12px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.language-picker-heading:first-child { margin-top: 8px; }
.language-picker-item {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 48px;
  padding: 12px 4px;
  border: 0;
  border-bottom: 1px solid var(--line-soft);
  border-radius: 0;
  background: none;
  font-size: 15px;
  font-weight: 550;
  color: var(--ink);
  text-align: left;
}
.language-picker-item:hover { background: var(--canvas); }
.language-picker-item[aria-selected="true"] { color: var(--brand); }
.language-picker-check {
  color: var(--brand);
  font-size: 14px;
  font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

@media (min-width: 560px) {
  .panel { padding: 24px; }
  .landing-card { padding: 32px 28px; }
  .step-title { font-size: 23px; }
  .controls { flex-direction: row; flex-wrap: wrap; }
  .controls button { width: auto; flex: 1 1 auto; min-width: 160px; }
  .controls .verify-result { flex: 1 1 100%; }
}

/* ============================================================
   #landing — Design System G ("Capital Desk") treatment
   ------------------------------------------------------------
   Scoped entirely to the #landing ID so #consent (same markup,
   same .landing-card/.center-card-screen classes, but carries a
   real org's session-branding via applySessionBranding()) is
   completely unaffected -- it keeps this file's brand-neutral,
   single-token (--brand) system exactly as documented at the top
   of this file. #landing is the one screen that's ours alone
   (LiveVerify's own pre-signup waitlist gate, no org session yet),
   so it's the one screen that can carry a fixed design system
   instead of the shared brand-neutral tokens.

   Colors/type lifted from the Option G ("Capital Desk") mockup --
   see design/liveverify-design-system-options-abc.md in the
   LiveVerifyCo project. Fonts loaded in index.html <head>, scoped
   to this block only; every other screen keeps --font-sans.
   ============================================================ */

#landing {
  background:
    radial-gradient(circle at 12% -15%, rgba(108, 71, 232, 0.28), transparent 55%),
    radial-gradient(circle at 92% 115%, rgba(74, 108, 245, 0.18), transparent 55%),
    #0a0a0d;
}

#landing .landing-card {
  background: #16161c;
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 20px 40px -24px rgba(0, 0, 0, 0.6);
  font-family: 'Sora', var(--font-sans);
}

#landing .landing-eyebrow {
  font-family: 'Sora', var(--font-sans);
  color: #f5f4f8;
}
#landing .landing-eyebrow .id { color: #a89cdb; }
#landing .landing-tag { color: #6f6d7c; }

#landing .landing-title,
#landing .landing-heading {
  font-family: 'Sora', var(--font-sans);
  color: #f5f4f8;
}
#landing .landing-copy,
#landing .landing-sub,
#landing .landing-status {
  color: #a6a4b3;
}

/* The "Coming soon" label -- the one spot on this card that maps directly
   to Option G's eyebrow-chip device (mono, uppercase, pill, lavender). Only
   this heading (#landingWaitlistHeading), not the generic .landing-heading
   rule above (also used by the QR-code state's "Scan with your phone",
   which reads as a real heading, not a badge). */
#landingWaitlistHeading {
  display: inline-flex;
  font-family: 'Fira Code', ui-monospace, monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #a89cdb;
  background: rgba(168, 156, 219, 0.14);
  border: 1px solid rgba(168, 156, 219, 0.25);
  padding: 6px 14px;
  border-radius: 999px;
  margin: 0 0 14px;
}

#landing .waitlist-form input[type="email"] {
  background: #0e0e12;
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: #f5f4f8;
  font-family: 'Sora', var(--font-sans);
}
#landing .waitlist-form input[type="email"]::placeholder { color: #6f6d7c; }
#landing .waitlist-form input[type="email"]:focus-visible { outline-color: #a89cdb; }

#landing .waitlist-form button,
#landing .landing-primary-btn {
  background: linear-gradient(135deg, #6c47e8 0%, #4a6cf5 100%);
  border-color: transparent;
  box-shadow: 0 8px 24px -8px rgba(108, 71, 232, 0.55);
  font-family: 'Sora', var(--font-sans);
}
#landing .waitlist-form button:not(:disabled):hover,
#landing .landing-primary-btn:not(:disabled):hover {
  background: linear-gradient(135deg, #7c59ec 0%, #5c7bf6 100%);
  border-color: transparent;
}

#landing .landing-secondary-btn {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.16);
  color: #f5f4f8;
}
#landing .landing-secondary-btn:not(:disabled):hover { background: rgba(255, 255, 255, 0.06); }

#landing .landing-note { color: #6f6d7c; }
#landing .landing-error-text { color: #fca5a5; }

#landing .landing-qr-img { border-color: rgba(255, 255, 255, 0.18); }

#landing :where(button, a, input):focus-visible { outline-color: #a89cdb; }
