/* Theme tokens (light defaults). Dark mode overrides via:
   - prefers-color-scheme when no explicit choice
   - [data-theme="dark"] when user toggles */
:root {
    --page-text-color:   #333;
    --page-card-bg:      #fff;
    --page-card-border:  #ececec;
    /* Secondary text (page-description, form hints, footer links, etc).
       Previous #cdd0d6 was ~1.43:1 against the card background — too
       faint to read on a glance. #6c757d lands at ~5.4:1, clearing
       WCAG AA for body copy without becoming heavy enough to compete
       with the primary text colour. */
    --page-muted:        #6c757d;
    --page-input-bg:     #fff;
    --page-input-border: #ddd;
    --progress-track:    #e9ecef;
    --page-error-color:   #dc3545;
    --page-warning-color: #f59e0b;
    --page-tooltip-bg:    #1f1f1f;
    --page-tooltip-fg:    #fff;
    /* Face capture surface — always darker than the page background so
       the live camera frame and the masked moldura have enough contrast
       to read like a video viewport (iOS Camera / FaceTime treat the
       chrome the same way regardless of system theme). The two tokens
       below let dark-mode pull the surface down a notch so it
       integrates with --page-card-bg instead of looking like a brighter
       island. */
    --face-surface-bg:   #1f2024;                 /* solid (recognition view + loading bg) */
    --face-surface-dim:  rgba(31, 32, 36, 0.85);  /* semi-transparent overlay around moldura */
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --page-bg-color:     #121212;
        --page-text-color:   #e8e8e8;
        --page-card-bg:      #1e1e1e;
        --page-card-border:  #2a2a2a;
        --page-muted:        #9aa0a6;
        --page-input-bg:     #262626;
        --page-input-border: #333;
        --progress-track:    #2a2a2a;
        --face-surface-bg:   #181818;                 /* deepens to merge with #1e1e1e card */
        --face-surface-dim:  rgba(24, 24, 24, 0.85);
    }
}

[data-theme="dark"] {
    --page-bg-color:     #121212;
    --page-text-color:   #e8e8e8;
    --page-card-bg:      #1e1e1e;
    --page-card-border:  #2a2a2a;
    --page-muted:        #9aa0a6;
    --page-input-bg:     #262626;
    --page-input-border: #333;
    --progress-track:    #2a2a2a;
    --face-surface-bg:   #181818;
    --face-surface-dim:  rgba(24, 24, 24, 0.85);
}

/* Main Styles from index.html */
body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    line-height: 1.6;
    color: var(--page-text-color, #333);
    margin: 0;
    padding: 0;
    background-color: var(--page-bg-color, #ffffff);
    background-image: var(--page-bg-image, none);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    text-align: var(--page-align, center);
}

/* Single wrapper that controls the column width for header, progress bar,
   main and footer. Width responds to device size. Children inherit width;
   they no longer carry their own max-width. */
.page-wrapper {
    max-width: 460px;
    margin: 0 auto;
    padding: 16px;
    box-sizing: border-box;
}

@media (min-width: 768px) {
    .page-wrapper {
        max-width: 460px;
        padding: 24px 16px;
    }
}

@media (max-width: 480px) {
    .page-wrapper {
        padding: 12px;
    }
}

/* Embedded mode (activated client-side via `?embed=true` → sessionStorage,
   see the bootstrap script in partials/index.html). Strip every piece of
   chrome so the signup blends into whatever iframe the host site puts
   it in — no border, no rounded corners, no card background, no
   external padding. The host owns the layout; we own only the form.

   Header (brand logo + progress bar) and the legal/theme/build options
   in the footer are hidden as well — those affordances belong to the
   host. The "Protegido por zarv" badge in the footer stays as the
   single piece of attribution.

   `html.embed-pending` is set inline in <head> before paint to bridge
   the gap until DOMContentLoaded moves the class onto <body> — avoids
   a one-frame flash of the card chrome in embed mode. */
html.embed-pending body,
body.embed {
    max-width: none;
    margin: 0;
    padding: 0;
    background: transparent;
}
html.embed-pending .card,
body.embed .card {
    border: 0;
    box-shadow: none;
    border-radius: 0;
    background: transparent;
}
html.embed-pending header,
body.embed header { display: none; }
html.embed-pending .progress-container,
body.embed .progress-container { display: none; }
body.embed footer .footer-options { display: none; }
/* In embed mode the SDK centers the iframe and the page reports its content
   height for auto-resize, so the standalone full-viewport centering aid would
   only inflate the reported height and stop the modal fitting the screen. */
body.embed .centered-container { min-height: 0; }

/* Single-screen SDK embed (?single=<screen>): one isolated screen, not the full
   funnel. Hide the chrome that only belongs in the standalone flow — the page
   title/subtitle and the "continue on another device" handoff trigger. Scoped
   to single-screen (NOT all of body.embed) so whole-flow embeds keep their UI. */
html.single-screen-pending .page-title,
html.single-screen-pending .page-description,
body.single-screen .page-title,
body.single-screen .page-description,
body.single-screen [data-handoff-open] { display: none; }

/* Theme color on text links */
a {
    color: var(--page-brand-color, #5d2a7c);
}

/* Inlined SVGs use class="svg-customer" on paths that should
   pick up the campaign's brand color. */
.svg-customer {
    fill: var(--page-brand-color, #5d2a7c);
}

.subtitle {
    text-align: center;
    color: #bbb;
    font-size: 1.1em;
    margin-top: -10px;
    margin-bottom: 20px;
}

header {
    text-align: center;
    margin: 0 0 20px;
    padding: 0;
}

/* Brand mark in the page header — anchor with a background-image,
   following the id-signup pattern (see frontend/src/components/AppHeader.vue
   + main.scss #brand). Why a background instead of an <img>?
     - background-size: contain caps both dimensions to this fixed slot,
       so logos with very different aspect ratios all fit without
       overflowing or pushing the layout around;
     - empty / missing image paints nothing instead of a broken-image
       glyph;
     - the anchor stays a real link (clickable, focusable, has a title /
       aria-label) — text-indent shoves the &nbsp; placeholder offscreen
       so screen readers see the accessible name, not the whitespace.
   The fixed dimensions match the upstream pattern (160×50). */
header .header-brand {
    display: block;
    width: 160px;
    height: 50px;
    margin: 0 auto;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: contain;
    text-indent: -9999px;
    overflow: hidden;
}

/* The --zarv modifier marks the Zarv fallback (used by header.html when
   the campaign has no .custom.Logo: unknown-hostname 404, inactive
   campaign, or unconfigured campaign). The asset is a dark-on-transparent
   PNG, so invert it in dark mode for legibility. We must NEVER invert a
   partner's brand asset — hence the modifier class, scoped to the
   fallback only. */
[data-theme="dark"] header .header-brand--zarv {
    filter: invert(1) hue-rotate(180deg);
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) header .header-brand--zarv {
        filter: invert(1) hue-rotate(180deg);
    }
}

footer {
    text-align: center;
    /* Visibly narrower than main: extra horizontal padding insets the content
       within the wrapper's column. */
    margin: 16px 0;
    padding: 0 20px;
    color: var(--page-muted);
    font-size: 14px;
}

footer .footer-text a,
footer .footer-text a:visited {
    color: inherit;
}

footer .footer-options {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    margin-bottom: 28px;
    flex-wrap: wrap;
}

footer .footer-links {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

footer .footer-links a,
footer .locale-label {
    color: var(--page-muted);
    font-size: 14px;
    font-weight: 400;
    line-height: 1.4;
    text-decoration: none;
}

footer .footer-links a:hover {
    color: var(--page-brand-color);
    text-decoration: underline;
}

footer .footer-controls {
    display: flex;
    align-items: center;
    gap: 8px;
}

footer .footer-protected {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: 28px auto 24px;
}

footer .footer-logo {
    max-width: 160px;
    height: auto;
    opacity: 0.7;
}

/* Embed-mode footer: keeps only the "Protegido por zarv" badge — every
   other affordance (legal links, theme toggle) is dropped because the
   host iframe owns those. The badge sits flush with the content above
   (no 28px margin row) but keeps the same visual size as standalone so
   the attribution is equally legible inside the iframe. */
footer.footer-embed {
    margin: 12px 0 8px;
    padding: 0;
}
footer.footer-embed .footer-protected {
    margin: 0 auto;
}

.theme-toggle {
    background: transparent;
    border: none;
    color: var(--page-muted);
    cursor: pointer;
    width: 28px;
    height: 28px;
    line-height: 0;
    padding: 4px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
}

.theme-toggle:hover {
    color: var(--page-brand-color);
}

.theme-toggle svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
    display: block;
}

.theme-toggle .theme-icon-light,
.theme-toggle .theme-icon-dark {
    display: inline-flex;
    width: 100%;
    height: 100%;
}

/* Show moon (icons/dark.svg) in light mode, sun (icons/light.svg) in dark mode */
.theme-toggle .theme-icon-dark { display: none; }
[data-theme="dark"] .theme-toggle .theme-icon-light { display: none; }
[data-theme="dark"] .theme-toggle .theme-icon-dark { display: inline-flex; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-toggle .theme-icon-light { display: none; }
    :root:not([data-theme="light"]) .theme-toggle .theme-icon-dark { display: inline-flex; }
}

/* Protected by Zarv: swap image variant based on theme */
.protected-by-dark { display: none; }
[data-theme="dark"] .protected-by-light { display: none; }
[data-theme="dark"] .protected-by-dark { display: inline-block; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .protected-by-light { display: none; }
    :root:not([data-theme="light"]) .protected-by-dark { display: inline-block; }
}

.card {
    background: transparent;
    padding: 24px;
    border-radius: 16px;
    box-shadow: none;
    border: 1px solid var(--page-card-border, #eee);
    width: 100%;
    text-align: center;
    margin: 0 auto;
    box-sizing: border-box;
}

/* Page section blocks (consistent across all views):
   .hero    — title + description + illustration, INSET via padding
   .content — page-specific content (forms, lists), edge-to-edge (no padding)
   .actions — button(s) wrapper, INSET (matches .hero) */
.hero {
    padding: 0 40px;
    margin-bottom: 32px;
}

/* === 404 hero — Apple-style dramatic numerals ========================== */
.hero-404 {
    padding: 8px 16px 16px;
    margin-bottom: 24px;
}

/* === Inactive-campaign hero — softer than 404 ========================== */
/* Same structural padding as .hero-404 so the surrounding chrome
   (header, footer) doesn't shift between rendering paths, but
   without the dramatic numeric mark. Owner is KNOWN — visual tone
   is "back soon", not "page is gone". */
.hero-inactive {
    padding: 8px 16px 16px;
    margin-bottom: 24px;
    text-align: center;
}

.inactive-icon {
    display: flex;
    justify-content: center;
    margin: 8px auto 18px;
}

.inactive-icon svg {
    width: 96px;
    height: 96px;
}

.error-code {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    margin: 12px auto 28px;
    line-height: 1;
    user-select: none;
}

.error-code-digit {
    font-size: clamp(96px, 22vw, 160px);
    font-weight: 800;
    letter-spacing: -0.06em;
    color: var(--page-text-color);
    background: linear-gradient(180deg,
        var(--page-text-color) 0%,
        color-mix(in srgb, var(--page-text-color) 70%, transparent) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    text-shadow: 0 12px 30px color-mix(in srgb, var(--page-text-color) 18%, transparent);
    animation: error-float 5.5s ease-in-out infinite;
}

.error-code-digit--4   { animation-delay: 0s; }
.error-code-digit--0   { animation-delay: -1.8s; }
.error-code-digit--4-r { animation-delay: -3.6s; }

.error-code-digit--0 {
    /* The center zero is rendered as an SVG ring so we can paint it with a
       branded gradient — punches color into an otherwise monochrome 4-0-4. */
    width: clamp(96px, 22vw, 160px);
    height: clamp(96px, 22vw, 160px);
    background: none;
    -webkit-text-fill-color: initial;
    text-shadow: none;
    filter: drop-shadow(0 14px 28px color-mix(in srgb, var(--page-brand-color) 35%, transparent));
}

.error-code-digit--0 svg {
    width: 100%;
    height: 100%;
    display: block;
}

@keyframes error-float {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-6px); }
}

@media (prefers-reduced-motion: reduce) {
    .error-code-digit { animation: none; }
}

/* Big circular status icon (error/success/warning) used at the top of the
   hero in feedback views (error, success, sorry, not_found). */
.status-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    margin: 8px auto 24px;
    color: var(--page-text-color);
}

.status-icon svg { display: block; }

.status-icon-error   { color: var(--page-error-color); }
.status-icon-success { color: var(--page-brand-color); }

.content {
    padding: 0 16px;
    margin-bottom: 32px;
    display: flex;
    flex-direction: column;
    gap: 20px;
    text-align: left;
}

/* Modifier for single-input or sparse content blocks that benefit from
   extra vertical breathing room (e.g., email entry page). Top padding
   removed — the page hero above already provides the headroom, and the
   extra spacing pushed the content too far from the title in flows
   where they need to read as a single visual unit. */
.content-airy {
    padding-bottom: 24px;
}

.actions {
    padding: 0 40px;
}
/* Vertical spacing between stacked CTAs (e.g. primary + secondary
   "Voltar"). Block-level .btn-wide buttons would otherwise touch. */
.actions > * + * {
    margin-top: 12px;
}

/* Page-level content elements rendered inside the card.
   Title + description form a single visual pairing — tight 8px gap
   between them (Apple style), with the full 24px breathing room kept
   below the description before the form/content starts. */
.page-title {
    font-size: 1.5rem;
    font-weight: 400;
    color: var(--page-text-color, #333);
    margin: 0 0 8px;
    line-height: 1.25;
}

.page-description {
    color: var(--page-muted, #9aa0a6);
    font-size: 0.95rem;
    line-height: 1.5;
    margin: 0 auto 24px;
}

.page-illustration {
    display: block;
    width: 70%;
    max-width: 240px;
    height: auto;
    margin: 40px auto;
    /* Inlined SVGs use `fill="currentColor"` on paths that should pick up
       the campaign's brand colour. Setting `color` here propagates into
       every child SVG via inheritance — works even when class-based
       rules (.svg-customer) lose specificity to a presentation attribute
       like `fill="..."` on the SVG root. */
    color: var(--page-brand-color, #5d2a7c);
}

.page-illustration svg {
    width: 100%;
    height: auto;
}

/* Body copy on the status page — sits between the illustration and the
   action button, slightly more present than .page-description (it's the
   actual message, not subtitle chrome). Centered to match the hero
   above. */
.status-body {
    text-align: center;
    color: var(--page-text-color, #333);
    font-size: 0.95rem;
    line-height: 1.5;
    margin: 0 auto 24px;
    max-width: 360px;
}

/* "Redirecionando..." line shown when Integrations.SuccessUrl /
   RejectionUrl is configured. The auto-redirect is the primary path; the
   inline "clique aqui" link is a fallback for users whose browser blocks
   programmatic navigation (popup blockers, sandboxed iframes, etc). */
.status-redirect {
    text-align: center;
    color: var(--page-muted, #6c757d);
    font-size: 0.95rem;
    line-height: 1.5;
    margin: 0 auto;
    max-width: 360px;
}
.status-redirect strong {
    color: var(--page-text-color, #333);
    font-variant-numeric: tabular-nums; /* keep the countdown width stable as the digit drops */
}
.status-redirect a {
    color: var(--page-brand-color, #5d2a7c);
    text-decoration: underline;
}

.btn.btn-wide,
a.btn.btn-wide {
    display: block;
    width: 100%;
    padding: 14px 20px;
    /* Corner radius is driven by --btn-shape-radius (set in
       partials/index.html from .custom.ButtonShape). Falls back to
       12px so older campaigns and any consumer that hasn't migrated
       to the variable keep their existing look. */
    border-radius: var(--btn-shape-radius, 12px);
    font-weight: 600;
    font-size: 1rem;
    box-sizing: border-box;
    text-align: center;
}

.card-images {
    max-width: 35%;
    height: auto;
    display: block;
    margin: 20px auto;
}

.progress-container {
    width: auto;
    margin: 0 16px 10px;
    background-color: var(--progress-track);
    height: 5px;
    border-radius: 5px;
    overflow: hidden;
}

.progress-fill {
    background-color: #22c55e;
    height: 100%;
    width: 0;
    transition: width 0.3s ease;
}

.icon {
    font-size: 64px;
    line-height: 1;
    margin-bottom: 20px;
}

/* === Button component =====================================================
   Reusable across all views. Markup:
     <a   class="btn"           href="...">Text</a>
     <button class="btn"        type="submit">Text</button>
     <button class="btn btn-secondary">Outlined</button>
     <a   class="btn btn-wide" href="...">Full width</a>
     <button class="btn"        disabled>Disabled</button>
   States: :hover (slight brighten), :active (press), :disabled, :focus-visible.
   Color follows the campaign's --page-brand-color automatically. */
.btn {
    display: inline-block;
    background-color: var(--page-brand-color);
    color: #fff;
    padding: 10px 20px;
    text-decoration: none;
    border-radius: 8px;
    border: 1px solid transparent;
    font-weight: 500;
    font-size: 16px;
    line-height: 1.4;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    transition: filter 0.15s ease, transform 0.05s ease,
                background-color 0.15s ease, color 0.15s ease,
                border-color 0.15s ease;
}

.btn:hover:not(:disabled):not([disabled]) {
    filter: brightness(1.08);
}

.btn:active:not(:disabled):not([disabled]) {
    filter: brightness(0.92);
    transform: translateY(1px);
}

.btn:focus-visible {
    outline: 2px solid var(--page-brand-color);
    outline-offset: 2px;
}

.btn:disabled,
.btn[disabled],
.btn.is-disabled {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
}

/* Variants */
.btn.btn-secondary {
    background-color: transparent;
    color: var(--page-brand-color);
    border-color: var(--page-brand-color);
}

.btn.btn-secondary:hover:not(:disabled):not([disabled]) {
    background-color: var(--page-brand-color);
    color: #fff;
    filter: none;
}

.centered-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 80vh;
}

h1 {
    color: #333;
    margin-top: 10px;
    margin-bottom: 20px;
    font-size: 1.5rem; /* Reduced global size */
}

h2 {
    color: #333;
    margin-top: 0px;
    margin-bottom: 10px;
    font-size: 1.2rem; /* Smaller than h1 */
}

/* Common Form Styles */
.form-container {
    max-width: 500px; /* Standard card width for single column */
    margin: 0 auto;
}

/* Table Styles */
table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 20px;
    text-align: left;
}

th, td {
    padding: 12px;
    border-bottom: 1px solid #eee;
    font-size: 14px;
}

th {
    background-color: #f8f9fa;
    font-weight: 600;
}

.section-header {
    background-color: #e9ecef;
    font-weight: bold;
    text-align: center;
    font-size: 15px;
}

/* `.form-grid` was the legacy wrapper on custom_data_form when it didn't
   follow the hero/content/actions convention. The page now uses the same
   structure as every other form (`.content` already provides the flex
   column + 20px gap), so nothing references this class — kept commented
   here only for the next ripgrep that wonders where it went.
   .form-grid { display: flex; flex-direction: column; gap: 20px; text-align: left; }
*/

.form-group {
    display: flex;
    flex-direction: column;
    min-width: 0;
    position: relative;
}

/* Stripe-style horizontal grouping for related fields (Number+Complement,
   City+State, etc). Collapses to single column on mobile. */
.form-row {
    display: flex;
    gap: 12px;
}

.form-row .form-group {
    margin: 0;
}

.form-group-narrow { flex: 0 0 110px; }
.form-group-uf     { flex: 0 0 90px; }
.form-group-grow   { flex: 1 1 auto; min-width: 0; }

/* === Custom-data stepper (paginated form, 3 fields per step) ============
   The stepper is wired entirely in JS (App.Forms.initCustomDataStepper).
   It chunks the .form-group(s) inside .content into <div class="cd-step">
   wrappers, toggling them via `hidden`. These rules just polish the
   transitions and place the step indicator. */
.cd-step {
    display: flex;
    flex-direction: column;
    gap: 20px;
    /* Tiny entrance animation per step swap — feels coordinated with the
       smooth scroll-to-top the JS does on advance/back. */
    animation: cd-step-in 240ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
.cd-step[hidden] { display: none; }
@keyframes cd-step-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Pill above the fields: "Etapa 1 de 3". Muted on purpose — the focus
   should stay on the field labels, not on the meta chrome. */
.cd-step-indicator {
    align-self: flex-start;
    color: var(--page-muted, #9aa0a6);
    font-size: 0.78rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin-bottom: 4px;
}

/* "Voltar" sits above "Próximo/Avançar" because .actions stacks via the
   `.actions > * + *` margin rule. Hidden on the first step via JS. */
.cd-step-back[hidden] { display: none; }

@media (max-width: 480px) {
    .form-row {
        flex-direction: column;
        gap: 20px;
    }
    .form-group-narrow,
    .form-group-uf {
        flex: 1 1 auto;
    }
}

/* Error state: red border on the input/select and a (!) button next to it
   that reveals the error in a tooltip on hover/focus. */
.form-group.has-error input,
.form-group.has-error select,
.form-group.has-error textarea {
    border-color: var(--page-error-color);
}

/* Hide the legacy inline error text — tooltip now carries the message. */
.form-group.has-error .error-message {
    display: none !important;
}

.field-error-button {
    position: absolute;
    right: 10px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--page-error-color);
    color: #fff;
    border: none;
    cursor: help;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transform: translateY(-50%);
    transition: transform 0.1s ease;
}

.field-error-button:hover,
.field-error-button:focus-visible {
    transform: translateY(-50%) scale(1.06);
    outline: none;
}

.field-error-button:hover::after,
.field-error-button:focus-visible::after,
.field-error-button:hover::before,
.field-error-button:focus-visible::before {
    pointer-events: none;
    z-index: 100;
}

.field-error-button:hover::after,
.field-error-button:focus-visible::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 8px);
    right: -6px;
    width: max-content;
    min-width: 200px;
    max-width: 260px;
    background: var(--page-tooltip-bg);
    color: var(--page-tooltip-fg);
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 0.78rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: normal;
    text-align: left;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}

.field-error-button:hover::before,
.field-error-button:focus-visible::before {
    content: "";
    position: absolute;
    bottom: calc(100% + 2px);
    right: 4px;
    border: 6px solid transparent;
    border-top-color: var(--page-tooltip-bg);
}

/* === Warning state (soft, non-blocking) =============================== */
.form-group.has-warning input,
.form-group.has-warning select,
.form-group.has-warning textarea {
    border-color: var(--page-warning-color);
}

.field-warning-button {
    position: absolute;
    right: 10px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--page-warning-color);
    color: #fff;
    border: none;
    cursor: help;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transform: translateY(-50%);
    transition: transform 0.1s ease;
}

.field-warning-button:hover,
.field-warning-button:focus-visible {
    transform: translateY(-50%) scale(1.06);
    outline: none;
}

.field-warning-button:hover::after,
.field-warning-button:focus-visible::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 8px);
    right: -6px;
    width: max-content;
    min-width: 200px;
    max-width: 260px;
    background: var(--page-tooltip-bg);
    color: var(--page-tooltip-fg);
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 0.78rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: normal;
    text-align: left;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
    pointer-events: none;
    z-index: 100;
}

.field-warning-button:hover::before,
.field-warning-button:focus-visible::before {
    content: "";
    position: absolute;
    bottom: calc(100% + 2px);
    right: 4px;
    border: 6px solid transparent;
    border-top-color: var(--page-tooltip-bg);
    pointer-events: none;
    z-index: 100;
}

.form-group-checkbox {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    gap: 12px;
}

.form-group-checkbox label {
    margin-bottom: 0;
    font-weight: 500;
    font-size: 0.9rem;
    color: var(--page-text-color);
    line-height: 1.45;
    cursor: pointer;
}

.form-group-checkbox input[type="checkbox"] {
    width: 20px;
    height: 20px;
    flex: 0 0 20px;
    margin: 1px 0 0;
    accent-color: var(--page-brand-color);
    cursor: pointer;
}

/* Choice fields (single_choice = radios, multiple_choice = checkboxes).
   Reuses the .form-group flex column so it slots into the existing
   stepper layout without bespoke spacing rules. The <legend> takes the
   place of a regular field label — fieldset is the semantically correct
   wrapper when one form control spans many inputs (radio group / multi
   checkboxes). Defaults are reset (no native fieldset border, legend
   not floated) so it reads as a flat block matching the other rows. */
.form-group-options {
    border: 0;
    padding: 0;
    margin: 0;
}

.form-group-options legend {
    padding: 0;
    margin-bottom: 8px;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--page-text-color);
}

.form-group-options .form-option {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 1px solid var(--page-input-border);
    border-radius: 8px;
    cursor: pointer;
    font-weight: 500;
    font-size: 0.9rem;
    color: var(--page-text-color);
    transition: border-color 120ms ease, background-color 120ms ease;
}

.form-group-options .form-option + .form-option {
    margin-top: 8px;
}

.form-group-options .form-option:hover {
    border-color: var(--page-brand-color);
}

.form-group-options .form-option input[type="radio"],
.form-group-options .form-option input[type="checkbox"] {
    width: 18px;
    height: 18px;
    flex: 0 0 18px;
    margin: 0;
    accent-color: var(--page-brand-color);
    cursor: pointer;
}

.form-group-options .form-option:has(input:checked) {
    border-color: var(--page-brand-color);
    background-color: color-mix(in srgb, var(--page-brand-color) 6%, transparent);
}

label {
    font-weight: 600;
    color: var(--page-text-color);
    font-size: 0.85rem;
    margin-bottom: 4px;
    display: block;
}

/* Required marker (red asterisk) inside labels */
.required {
    color: #e64949;
    margin-left: 2px;
    font-weight: 700;
}

/* Helper text under inputs */
.form-help {
    display: block;
    color: var(--page-muted);
    font-size: 0.8rem;
    line-height: 1.4;
    margin-top: 8px;
}

/* Inline icon next to button label (e.g., "Usar minha localização") */
.btn .geo-icon,
.btn .btn-icon {
    display: inline-flex;
    align-items: center;
    margin-right: 6px;
    vertical-align: -3px;
}

/* === Address search — iOS-style search field + ghost geo link ========= */
.address-search-group {
    position: relative;
    margin-bottom: 6px;
}

.address-search-field {
    position: relative;
    display: flex;
    align-items: center;
    background: var(--page-search-bg, #f0f0f3);
    border-radius: 12px;
    padding: 0 14px;
    height: 48px;
    transition: background 0.15s ease, box-shadow 0.15s ease;
}

[data-theme="dark"] .address-search-field {
    --page-search-bg: #2a2a2e;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .address-search-field {
        --page-search-bg: #2a2a2e;
    }
}

.address-search-field:focus-within {
    background: var(--page-card-bg);
    box-shadow: 0 0 0 2px var(--page-brand-color);
}

.address-search-icon {
    color: var(--page-muted);
    margin-right: 10px;
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    pointer-events: none;
}

.address-search-icon svg {
    display: block;
}

/* Reset global input chrome inside the search shell — the parent renders the
   visual frame, the input becomes a transparent text layer that grows to fill. */
.address-search-field input,
.address-search-field input[type="text"] {
    flex: 1;
    width: 100%;
    height: 100%;
    border: none;
    background: transparent;
    padding: 0;
    margin: 0;
    font-size: 1rem;
    line-height: 1;
    color: var(--page-text-color);
    outline: none;
    box-shadow: none;
    border-radius: 0;
    appearance: none;
    -webkit-appearance: none;
}

.address-search-field input:focus,
.address-search-field input[type="text"]:focus {
    border: none;
    outline: none;
    box-shadow: none;
}

.address-search-field input::placeholder {
    color: var(--page-muted);
}

/* Ghost text-link button under the search — small, brand-colored, no border. */
.address-nearby-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: transparent;
    border: none;
    color: var(--page-brand-color);
    font-size: 0.9rem;
    font-weight: 500;
    cursor: pointer;
    padding: 8px 4px;
    align-self: flex-start;
    text-decoration: none;
    transition: opacity 0.12s ease;
}

/* Pull the link snug under the search field — the .content flex gap (20px)
   would otherwise push them apart. */
.address-search-group + .address-nearby-link {
    margin-top: -12px;
}

.address-nearby-link:hover { opacity: 0.75; }
.address-nearby-link:disabled { opacity: 0.5; cursor: default; }
.address-nearby-link .geo-icon { display: inline-flex; vertical-align: -2px; }

.place-suggestions {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    margin: 0;
    padding: 4px 0;
    list-style: none;
    background: var(--page-card-bg);
    border: 1px solid var(--page-card-border);
    border-radius: 10px;
    max-height: 260px;
    overflow-y: auto;
    z-index: 50;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.10);
    opacity: 1;
    transition: opacity 0.18s ease, transform 0.18s ease;
    transform: translateY(0);
}

.place-suggestions[hidden] {
    display: block !important;
    opacity: 0;
    transform: translateY(-4px);
    pointer-events: none;
    visibility: hidden;
}

.place-suggestion {
    padding: 10px 14px;
    cursor: pointer;
    text-align: left;
    font-size: 0.9rem;
    color: var(--page-text-color);
    border-bottom: 1px solid var(--page-card-border);
}

.place-suggestion:last-child {
    border-bottom: none;
}

.place-suggestion strong {
    display: block;
    font-weight: 600;
    margin-bottom: 2px;
}

.place-suggestion small {
    display: block;
    color: var(--page-muted);
    font-size: 0.78rem;
}

.place-suggestion[aria-selected="true"],
.place-suggestion:hover {
    background: rgba(0, 0, 0, 0.04);
}

[data-theme="dark"] .place-suggestion[aria-selected="true"],
[data-theme="dark"] .place-suggestion:hover {
    background: rgba(255, 255, 255, 0.05);
}

/* Fade-in/slide reveal for the rest of the address fields. */
.address-fields {
    border: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 20px;
    opacity: 0;
    max-height: 0;
    overflow: hidden;
    transform: translateY(-6px);
    transition:
        opacity 0.28s ease,
        transform 0.28s ease,
        max-height 0.4s ease,
        margin-top 0.28s ease;
    margin-top: 0;
}

.address-fields.is-visible {
    opacity: 1;
    max-height: 1200px;
    transform: translateY(0);
    margin-top: 4px;
    overflow: visible;
}

/* === Nearby addresses modal (map + list side-by-side) =================== */
.nearby-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    animation: handoffFade 0.18s ease;
}

.nearby-modal[hidden] { display: none; }

.nearby-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(20, 20, 22, 0.45);
    backdrop-filter: blur(14px) saturate(150%);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    cursor: pointer;
}

.nearby-content {
    position: relative;
    background: var(--page-card-bg);
    border-radius: 16px;
    width: 100%;
    max-width: 720px;
    /* Up to 5 items (each can wrap to 2 lines for long bairro/cidade) plus
       header. ~500px holds the worst case without showing a scrollbar. */
    height: min(86vh, 500px);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow:
        0 1px 1px rgba(0, 0, 0, 0.05),
        0 22px 70px -10px rgba(0, 0, 0, 0.45);
    animation: handoffPop 0.28s cubic-bezier(0.2, 0.85, 0.3, 1.05);
}

.nearby-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid var(--page-card-border);
    gap: 12px;
}

.nearby-header h2 {
    font-size: 1rem;
    font-weight: 600;
    margin: 0;
    color: var(--page-text-color);
    line-height: 1.3;
    /* Long titles ("Endereços próximos a você em São Paulo, SP") wrap to two
       lines instead of overflowing on narrow screens. */
    overflow-wrap: anywhere;
}

.nearby-close-btn {
    background: transparent;
    border: none;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    color: var(--page-muted);
    cursor: pointer;
    font-size: 1.4rem;
    line-height: 0.8;
    padding: 0;
    transition: background-color 0.12s ease, color 0.12s ease;
}

.nearby-close-btn:hover {
    background-color: rgba(0, 0, 0, 0.05);
    color: var(--page-text-color);
}

[data-theme="dark"] .nearby-close-btn:hover {
    background-color: rgba(255, 255, 255, 0.08);
}

.nearby-body {
    flex: 1;
    display: flex;
    overflow: hidden;
    min-height: 0;
}

.nearby-map {
    flex: 1.4;
    min-height: 0;
    background: #ececec;
}

/* Shrink Google Maps' bundled zoom buttons (default 40×40 — too big for our
   compact modal). Scale from the bottom-right anchor so they stay tucked
   into the corner without overflowing the map area. */
.nearby-map .gm-bundled-control,
.nearby-map .gm-bundled-control-on-bottom {
    transform: scale(0.72);
    transform-origin: bottom right;
}

/* Right-side column on desktop / bottom column on mobile. Holds the list
   (which scrolls) plus the "Vou buscar pelo nome" footer (which doesn't),
   so the action button always sits flush below the list — never under the
   map column. */
.nearby-list-column {
    flex: 1;
    min-width: 220px;
    display: flex;
    flex-direction: column;
    border-left: 1px solid var(--page-card-border);
    min-height: 0;
}

.nearby-list {
    flex: 1;
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    min-height: 0;
    -webkit-overflow-scrolling: touch;
}

.nearby-list-item {
    padding: 12px 16px;
    cursor: pointer;
    border-bottom: 1px solid var(--page-card-border);
    text-align: left;
    transition: background-color 0.12s ease;
    min-height: 48px; /* WCAG-friendly tap target */
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.nearby-list-item:hover,
.nearby-list-item.is-active {
    background-color: rgba(0, 0, 0, 0.04);
}

[data-theme="dark"] .nearby-list-item:hover,
[data-theme="dark"] .nearby-list-item.is-active {
    background-color: rgba(255, 255, 255, 0.06);
}

.nearby-list-item strong {
    display: block;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--page-text-color);
    margin-bottom: 2px;
    line-height: 1.3;
}

.nearby-list-item small {
    display: block;
    color: var(--page-muted);
    font-size: 0.78rem;
    line-height: 1.35;
}

.nearby-list-empty {
    padding: 20px 16px;
    color: var(--page-muted);
    font-size: 0.85rem;
    text-align: center;
}

/* Footer sits OUTSIDE the .nearby-body scroll area so the button stays
   visible while the list scrolls. No divider — the button itself is the
   visual separator. */
.nearby-footer {
    flex: 0 0 auto;
    padding: 12px 16px;
    text-align: center;
    background: var(--page-card-bg);
}

/* Smaller secondary-action button: brand-color outline, compact padding,
   fills with brand on hover. Sits below the list as a "fall back to manual
   search" affordance. */
.nearby-search-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--page-brand-color);
    color: var(--page-brand-color);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    padding: 7px 16px;
    border-radius: 999px;
    line-height: 1.2;
    transition: background-color 0.15s ease, color 0.15s ease;
    -webkit-tap-highlight-color: transparent;
}

.nearby-search-link:hover {
    background-color: var(--page-brand-color);
    color: #fff;
}

.nearby-search-link:active {
    transform: translateY(1px);
}

@media (max-width: 640px) {
    .nearby-modal { padding: 0; }
    .nearby-content {
        max-width: none;
        /* Fill the viewport; 100dvh handles iOS Safari's bottom URL bar. */
        height: 100vh;
        height: 100dvh;
        border-radius: 0;
    }
    .nearby-header {
        padding: 12px 14px;
    }
    .nearby-header h2 {
        font-size: 0.95rem;
    }
    /* Larger close button for thumb reach (≥44×44 per Apple HIG). */
    .nearby-close-btn {
        width: 44px;
        height: 44px;
        font-size: 1.6rem;
    }
    .nearby-body { flex-direction: column; }
    .nearby-map {
        /* Fixed-height map so the list stays predictable below it. */
        flex: 0 0 220px;
        min-height: 220px;
    }
    .nearby-list-column {
        flex: 1;
        border-left: none;
        border-top: 1px solid var(--page-card-border);
    }
    .nearby-list-item {
        padding: 14px 16px;
        min-height: 56px; /* taller tap target on small screens */
    }
}

input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="date"],
input[type="file"],
select {
    width: 100%;
    padding: 14px 16px;
    border: 1px solid var(--page-input-border, #ddd);
    background: var(--page-input-bg, #fff);
    color: var(--page-text-color, #333);
    border-radius: 10px;
    box-sizing: border-box;
    font-size: 1rem;
    font-family: inherit;
    transition: border-color 0.15s ease;
}

input[type="text"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
input[type="file"]:focus,
select:focus {
    outline: none;
    border-color: var(--page-brand-color);
}

input::placeholder {
    color: var(--page-muted);
}

/* Native input[type=submit] is no longer used in views — they all render as
   <button class="btn ...">. The .btn component handles styling and states. */

/* Input Group */
.input-group {
    display: flex;
    gap: 10px;
}

.input-group select {
    width: auto;
    flex-shrink: 0;
    min-width: 100px;
}

.input-group input {
    flex-grow: 1;
}

/* Code Input Style (legacy single field — kept for any view still using it) */
.input-code {
    width: 100%;
    padding: 12px !important;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-sizing: border-box;
    font-size: 18px !important;
    text-align: center;
    letter-spacing: 5px;
    font-family: monospace;
}

/* === Phone form: tighter input ↔ toggle gap + plain label ============= */
/* Wraps phone input + WhatsApp toggle so they sit close together as a
   single channel-selection unit, isolated from the .content's wider gap. */
.phone-channel-block {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Label without the default semi-bold weight — keeps "Telefone"/"WhatsApp"
   inline emphasis as the only bold chunks on the page. */
.label-plain {
    font-weight: 400;
}

/* === Phone field: country select + number input as one visual unit ==== */
.phone-field {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--page-input-border);
    border-radius: 10px;
    background: var(--page-input-bg);
    overflow: hidden;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.phone-field:focus-within {
    border-color: var(--page-brand-color);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--page-brand-color) 25%, transparent);
}

.phone-field-cc {
    border: none !important;
    background: transparent !important;
    padding: 14px 8px 14px 14px !important;
    font-size: 1rem;
    color: var(--page-text-color);
    appearance: none;
    -webkit-appearance: none;
    box-shadow: none !important;
    border-radius: 0 !important;
    width: auto !important;
    flex: 0 0 auto;
    cursor: pointer;
    /* tiny chevron after the code */
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='12' height='12' fill='%23999'><path d='M7 10l5 5 5-5z'/></svg>") !important;
    background-repeat: no-repeat !important;
    background-position: right 4px center !important;
    padding-right: 20px !important;
}

.phone-field-cc:focus { outline: none; }

.phone-field input[type="tel"] {
    border: none !important;
    background: transparent !important;
    padding: 14px 14px 14px 4px !important;
    flex: 1;
    border-radius: 0 !important;
    box-shadow: none !important;
}

.phone-field input[type="tel"]:focus { outline: none; box-shadow: none; }

/* Right-side WhatsApp affordance — JS unhides it once /verify whatsapp
   confirms the number is registered. Stays hidden otherwise. */
.phone-field-wa {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 12px;
    flex: 0 0 auto;
    animation: phone-wa-pop 240ms ease;
}
.phone-field-wa[hidden] { display: none; }

@keyframes phone-wa-pop {
    from { opacity: 0; transform: scale(0.6); }
    to   { opacity: 1; transform: scale(1); }
}

/* === Toggle switch (iOS-style) ======================================== */
.toggle-field {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
    margin-top: 4px;
    user-select: none;
}

.toggle-input {
    position: absolute !important;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.toggle-track {
    position: relative;
    flex: 0 0 44px;
    width: 44px;
    height: 26px;
    background: var(--page-input-border);
    border-radius: 999px;
    transition: background-color 0.18s ease;
}

.toggle-track::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 22px;
    height: 22px;
    background: #fff;
    border-radius: 50%;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
    transition: transform 0.18s ease;
}

.toggle-input:checked + .toggle-track {
    background: var(--page-brand-color);
}

.toggle-input:checked + .toggle-track::after {
    transform: translateX(18px);
}

/* WhatsApp variant: when ON, paint the toggle in WhatsApp green
   (the user just opted into a WhatsApp channel — visual reinforcement). */
.toggle-whatsapp .toggle-input:checked + .toggle-track {
    background: #25D366;
}

.toggle-input:focus-visible + .toggle-track {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--page-brand-color) 35%, transparent);
}

.toggle-label {
    font-size: 0.9rem;
    color: var(--page-text-color);
    line-height: 1.4;
}

/* === OTP code: six independent boxes ================================== */
.otp-input-group {
    display: flex;
    gap: 10px;
    justify-content: center;
    align-items: center;
    margin: 8px 0 4px;
}

.otp-input-group input {
    width: 46px;
    height: 56px;
    text-align: center;
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--page-text-color);
    background: var(--page-card-bg);
    border: 1px solid var(--page-input-border);
    border-radius: 12px;
    padding: 0;
    box-sizing: border-box;
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.otp-input-group input:focus {
    outline: none;
    border-color: var(--page-brand-color);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--page-brand-color) 25%, transparent);
}

.otp-input-group input.is-filled {
    border-color: var(--page-brand-color);
}

.otp-input-group input.has-error,
.otp-input-group input.has-error:focus {
    border-color: var(--page-error-color, #dc3545);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--page-error-color, #dc3545) 22%, transparent);
    color: var(--page-error-color, #dc3545);
}

/* --- OTP state machine: hero image + title/description swap.
   States: waiting (default) | error | success.
   The screen wrapper carries `data-otp-state`, and slots below show only
   the variant matching that state. */
.otp-screen {
    display: flex;
    flex-direction: column;
}
/* The OTP hero image already gives breathing room above the form, so the
   shared 32px gap below the title/description is redundant here. */
.otp-screen > .hero { margin-bottom: 0; }

.otp-hero-image {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 180px;
    margin: 16px auto 8px;
}

.otp-state-img { display: none; }

.otp-state-img > svg {
    display: block;
    width: 220px;
    max-width: 100%;
    height: auto;
}

.otp-screen[data-otp-state="waiting"] .otp-state-img[data-state-img="waiting"],
.otp-screen[data-otp-state="expired"] .otp-state-img[data-state-img="expired"],
.otp-screen[data-otp-state="error"]   .otp-state-img[data-state-img="error"],
.otp-screen[data-otp-state="success"] .otp-state-img[data-state-img="success"] {
    display: block;
    animation: otp-img-pop 320ms ease;
}

@keyframes otp-img-pop {
    from { opacity: 0; transform: scale(0.94); }
    to   { opacity: 1; transform: scale(1); }
}

[data-state-title], [data-state-desc] { display: none; }

.otp-screen[data-otp-state="waiting"] [data-state-title="waiting"],
.otp-screen[data-otp-state="waiting"] [data-state-desc="waiting"],
.otp-screen[data-otp-state="expired"] [data-state-title="expired"],
.otp-screen[data-otp-state="expired"] [data-state-desc="expired"],
.otp-screen[data-otp-state="error"]   [data-state-title="error"],
.otp-screen[data-otp-state="error"]   [data-state-desc="error"],
.otp-screen[data-otp-state="success"] [data-state-title="success"],
.otp-screen[data-otp-state="success"] [data-state-desc="success"] {
    display: block;
}

/* Success state: collapse the form scaffolding so only the verified
   illustration + heading remain on screen during the brief redirect window. */
.otp-screen[data-otp-state="success"] form .content > .otp-input-group,
.otp-screen[data-otp-state="success"] form .content > .otp-timer,
.otp-screen[data-otp-state="success"] form .content > #error-code,
.otp-screen[data-otp-state="success"] form .actions {
    display: none;
}

/* Expired state: code has aged out of its TTL window. Hide the inputs and
   the countdown pill — there's nothing to type into — and turn the "Reenviar
   código" link into the primary action by promoting it to a full-width
   button. "Alterar" stays visible as a secondary escape hatch. */
.otp-screen[data-otp-state="expired"] form .content > .otp-input-group,
.otp-screen[data-otp-state="expired"] form .content > .otp-timer,
.otp-screen[data-otp-state="expired"] form .content > #error-code {
    display: none;
}
.otp-screen[data-otp-state="expired"] .otp-actions {
    flex-direction: column-reverse;
    align-items: stretch;
    gap: 12px;
}
.otp-screen[data-otp-state="expired"] [data-resend-form] {
    /* Inherit the .btn / .btn-wide visual without the markup change — the
       button is already in the DOM as an .otp-link, just re-skin it. */
    background: var(--page-brand-color);
    color: #fff;
    padding: 14px 20px;
    border-radius: 12px;
    font-size: 1rem;
    font-weight: 600;
    justify-content: center;
    text-decoration: none;
}
.otp-screen[data-otp-state="expired"] [data-resend-form] svg {
    color: #fff;
}
.otp-screen[data-otp-state="expired"] [data-resend-form][disabled] {
    opacity: 0.6;
}

@media (max-width: 380px) {
    .otp-input-group { gap: 6px; }
    .otp-input-group input {
        width: 40px;
        height: 50px;
        font-size: 1.3rem;
    }
}

/* Pill banner with the countdown to code expiration. */
.otp-timer {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 14px;
    margin: 16px auto 0;
    background: color-mix(in srgb, var(--page-brand-color) 8%, transparent);
    color: var(--page-brand-color);
    border-radius: 999px;
    font-size: 0.85rem;
    line-height: 1;
}

.otp-timer-icon { display: inline-flex; }
.otp-timer[data-expired] {
    background: color-mix(in srgb, var(--page-error-color) 10%, transparent);
    color: var(--page-error-color);
}

/* Footer-style actions row: text-link "Alterar" + "Reenviar código". */
.otp-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    padding: 0 8px;
}

.otp-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: transparent;
    border: none;
    color: var(--page-text-color);
    font-size: 0.95rem;
    font-weight: 500;
    cursor: pointer;
    padding: 10px 4px;
    text-decoration: none;
    transition: color 0.12s ease, opacity 0.12s ease;
}

.otp-link:hover { color: var(--page-brand-color); }

.otp-link:disabled,
.otp-link[disabled] {
    color: var(--page-muted);
    cursor: default;
    opacity: 0.6;
}

.instruction {
    color: #666;
    margin-bottom: 20px;
    text-align: center;
}

/* === Continue elsewhere component =========================================
   Apple Handoff-style banner: lets the user continue the signup on another
   device by scanning a QR code. Used via {{template "partials/qrcode" .}}
   inside any view that benefits from device-switch (forms, disclaimers). */
.continue-elsewhere {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    margin-top: 12px;
    padding: 14px 16px;
    border: 1px solid var(--page-card-border);
    border-radius: 12px;
    background: transparent;
    color: var(--page-text-color);
    text-decoration: none;
    box-sizing: border-box;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

.continue-elsewhere:hover {
    background-color: rgba(0, 0, 0, 0.02);
    border-color: var(--page-brand-color);
}

[data-theme="dark"] .continue-elsewhere:hover {
    background-color: rgba(255, 255, 255, 0.04);
}

.continue-elsewhere:focus-visible {
    outline: 2px solid var(--page-brand-color);
    outline-offset: 2px;
}

.continue-elsewhere-icon {
    flex: 0 0 36px;
    width: 36px;
    height: 36px;
    color: var(--page-brand-color);
    display: flex;
    align-items: center;
    justify-content: center;
}

.continue-elsewhere-icon svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
}

.continue-elsewhere-text {
    flex: 1 1 auto;
    text-align: left;
    line-height: 1.3;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.continue-elsewhere-text strong {
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--page-text-color);
}

.continue-elsewhere-text small {
    font-size: 0.78rem;
    color: var(--page-muted);
}

.continue-elsewhere-arrow {
    flex: 0 0 16px;
    width: 16px;
    height: 16px;
    color: var(--page-muted);
    transition: transform 0.15s ease, color 0.15s ease;
    display: flex;
    align-items: center;
    justify-content: center;
}

.continue-elsewhere-arrow svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
}

.continue-elsewhere:hover .continue-elsewhere-arrow {
    transform: translateX(2px);
    color: var(--page-brand-color);
}

/* === Handoff modal — Apple-style alert sheet ============================
   Frosted-glass backdrop, soft scale-in pop, divider above cancel button. */
.handoff-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    animation: handoffFade 0.18s ease;
}

.handoff-modal[hidden] { display: none; }

.handoff-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(20, 20, 22, 0.4);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    cursor: pointer;
}

.handoff-content {
    position: relative;
    background: var(--page-card-bg);
    border-radius: 22px;
    width: 100%;
    max-width: 360px;
    overflow: hidden;
    box-sizing: border-box;
    text-align: center;
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.04) inset,
        0 1px 1px rgba(0, 0, 0, 0.05),
        0 22px 70px -10px rgba(0, 0, 0, 0.45);
    animation: handoffPop 0.28s cubic-bezier(0.2, 0.85, 0.3, 1.05);
    transform-origin: center;
}

@keyframes handoffFade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes handoffPop {
    from { opacity: 0; transform: scale(0.94); }
    to   { opacity: 1; transform: scale(1); }
}

.handoff-body {
    padding: 26px 24px 22px;
}

.handoff-title {
    font-size: 1.05rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--page-text-color);
    margin: 0 0 6px;
}

.handoff-description {
    color: var(--page-muted);
    font-size: 0.82rem;
    line-height: 1.4;
    margin: 0 auto 18px;
    max-width: 240px;
}

.handoff-qr {
    background: #fff;
    padding: 12px;
    border-radius: 16px;
    margin: 0 auto 20px;
    width: -moz-fit-content;
    width: fit-content;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.handoff-qr img {
    display: block;
    width: 260px;
    height: 260px;
    border-radius: 8px;
}

@media (max-width: 480px) {
    body {
        padding: 12px;
    }

    .card {
        padding: 24px 18px;
        border-radius: 12px;
    }

    .page-title {
        font-size: 1.3rem;
    }

    .page-description {
        font-size: 0.9rem;
        margin-bottom: 20px;
    }

    .page-illustration {
        width: 65%;
        max-width: 200px;
        margin: 28px auto;
    }

    h1 {
        font-size: 1.25rem;
    }

    h2 {
        font-size: 1.0rem;
    }

    .subtitle {
        font-size: 1em;
    }

    .btn {
        width: 100%;
        box-sizing: border-box;
        text-align: center;
    }

    .btn.btn-wide,
    a.btn.btn-wide {
        padding: 12px 16px;
        font-size: 0.95rem;
    }

    /* Footer collapses to a tighter, single-column layout on small screens */
    footer .footer-options {
        flex-direction: column;
        gap: 10px;
        margin-bottom: 14px;
    }

    .qr-code {
        margin-top: 20px;
        padding-top: 20px;
    }
}

p {
    color: #666;
    margin-bottom: 20px;
}

.text-success { color: #28a745; }
.text-danger { color: #dc3545; }
.text-warning { color: #ffc107; }
.text-muted { color: #6c757d; }

.text-center { text-align: center; }
.d-block { display: block; }
.d-none { display: none; }

.error-message {
    color: #dc3545;
    display: none;
    margin-top: 5px;
    font-size: 0.9em;
}

.error-message-checkbox {
    margin-top: -10px;
    margin-bottom: 10px;
}

/* Inline validating hint — floats below the input so it never pushes the
   submit button down. Slides in/out with opacity + translate. */
.field-status {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 4px;
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--page-muted);
    font-size: 0.85em;
    pointer-events: none;
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 180ms ease, transform 220ms ease;
}

.field-status.is-visible {
    opacity: 1;
    transform: translateY(0);
}

.field-status-spinner {
    width: 12px;
    height: 12px;
    border: 1.5px solid var(--page-border, rgba(0,0,0,0.15));
    border-top-color: var(--page-muted);
    border-radius: 50%;
    animation: field-status-spin 0.8s linear infinite;
    flex-shrink: 0;
}

@keyframes field-status-spin {
    to { transform: rotate(360deg); }
}

/* Confirmation Pages Styles */
.confirmation-wrapper {
    width: 100%;
    max-width: 400px;
    margin: 0 auto;
}

.confirmation-image {
    display: block;
    width: 100%;
    border-radius: 8px;
    margin-bottom: 20px;
}

.page-qrcode .qrcode-img {
    margin: 24px 0;
}

.page-qrcode .qrcode-img img {
    display: block;
    width: min(256px, 100%);
    height: auto;
    margin: 0 auto;
}

.action-buttons {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 10px;
}

.btn-action {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    margin-top: 0;
}

.btn-warning {
    background-color: #fff;
    color: var(--custom-primary-color);
    border: 1px solid var(--custom-primary-color);
}

.btn-success {
    background-color: var(--custom-primary-color);
}

/* Disclaimer pages — emoji-led tip list. Uses the same theme tokens as
   the rest of the form pages so it slots into .content without overriding
   spacing or colour. */
.tips-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 16px;
    text-align: left;
}

.tip-item {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    color: var(--page-text-color);
    font-size: 0.95rem;
    line-height: 1.45;
}

.tip-icon {
    font-size: 1.5rem;
    line-height: 1;
    flex: 0 0 28px;
    text-align: center;
}

.tip-text { flex: 1; }
.tip-text strong { color: var(--page-text-color); font-weight: 600; }

/* Face/CNH capture screens render the camera as the page itself: drop the
   card chrome (padding + border) so the video reaches all four edges of
   .page-wrapper, no title, no surrounding frame. */
.card.page-face-recognition,
.card.page-cnh-recognition {
    padding: 0;
    border: 0;
    background: transparent;
    text-align: left;
}

/* Once the CNH capture is done, the camera is gone and the page shows the
   same preview block the upload page renders. Restore the .card chrome so
   the two flows look identical — rounded white card framing the green
   "CNH pronta" title, preview, and action pair. */
.card.page-cnh-recognition.is-done {
    padding: 24px;
    border: 1px solid var(--page-card-border, #eee);
    border-radius: 16px;
    text-align: center;
}

/* Mirror the front-camera feed so the user perceives it like a real mirror —
   "vire para a esquerda" then translates to the user's own left, no mental
   inversion. The MediaPipe detection runs on the raw stream so this is
   purely a display tweak. */
.recognition-video-mirrored {
    transform: scaleX(-1);
}

/* Face-capture page only: also shift the video up so a face that is
   centred in front of the camera (raw video y=50%) appears at the
   mask centre (view y=38%). 12% of view height ≈ the gap between 50%
   and 38%, so the user's face drops cleanly inside the masked circle
   without them needing to crouch. Detection still runs on the raw,
   un-shifted stream so 'isCenter' continues to mean 'centred in
   the camera frame'. */
.page-face-recognition .recognition-video-mirrored {
    transform: scaleX(-1) translateY(-12%);
}

/* Bottom info strip with detected gender / age range. Sits over the video
   so it survives the full-bleed layout. Items hidden by default; the JS
   removes [hidden] once face-api returns a result. */
.user-info {
    position: absolute;
    z-index: 80;
    left: 0;
    right: 0;
    bottom: 12px;
    display: flex;
    flex-direction: row;
    gap: 14px;
    align-items: center;
    justify-content: center;
    font-size: 0.78rem;
    color: rgba(255, 255, 255, 0.85);
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
    pointer-events: none;
}
.user-info-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.user-info-item[hidden] { display: none; }
/* The UA's `[hidden] { display: none }` rule doesn't reliably apply to SVG
   elements in every browser (SVG isn't in the HTML namespace), so the three
   gender glyphs would all show at once even though JS toggled `.hidden = true`
   on the unwanted ones. Force-hide them explicitly. */
.user-info-item svg[hidden] { display: none; }

/* The whole strip slides down + fades together — never hide individual
   chips, because that causes the surviving items to recentre horizontally
   (flexbox redistributes space) and the row "jumps sideways" mid-frame.
   Animating at the row level keeps the layout stable: items just glide
   off-screen as a unit and come back in the same place.
   Asymmetric timing:
   • SHOW (default state): ease-out, decelerating curve, with a small
     delay so the strip doesn't pop in the same instant the camera
     turns ready / face re-enters frame — a beat to "settle softly".
   • HIDE (state classes below): ease-in, accelerating curve, no delay.
     Reads as gravity — the strip starts slow then snaps down. */
.user-info {
    transition: transform 360ms cubic-bezier(0, 0, 0.2, 1) 120ms,
                opacity   320ms cubic-bezier(0, 0, 0.2, 1) 120ms;
}
/* Hide the strip while:
   1. the camera is still loading — nothing meaningful to show yet
      (.recognition-view doesn't have .is-ready until the stream is live)
   2. the face has drifted out of the moldura (data is stale)
   3. the user has no location to anchor the row (.is-no-location is
      added by JS in _setLocation when both city + state are empty)
   .recognition-view.is-finalizing has its own rule below — kept separate
   so the post-capture exit animation can override the timing if needed. */
.recognition-view:not(.is-ready) .user-info,
.recognition-view.is-off-moldura .user-info,
.user-info.is-no-location {
    transform: translateY(40px);
    opacity: 0;
    pointer-events: none;
    /* Faster, accelerating curve on the way out — gravity feel. No delay
       so the user gets immediate feedback that the data is stale. */
    transition: transform 200ms cubic-bezier(0.55, 0.06, 0.68, 0.19),
                opacity   160ms cubic-bezier(0.55, 0.06, 0.68, 0.19);
}

.card.page-face-recognition #image-capture,
.card.page-cnh-recognition #image-capture {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* Shared Recognition Component Styles (Face & CNH) */
.recognition-view {
    position: relative;
    width: 100%;
    aspect-ratio: 3 / 4;
    margin: 0 auto;
    border-radius: 20px;
    overflow: hidden;
    /* Token-driven so the surface matches the active theme — same dark
       neutral in both modes (camera viewport always reads better dark)
       but slightly deeper in dark theme to merge with --page-card-bg
       instead of looking like a brighter island. */
    background: var(--face-surface-bg, #1f2024);
    display: flex;
    justify-content: center;
    align-items: center;
}
/* Hide the ring host until the JS layer flips .is-ready on the
   recognition-view (after the camera stream is live). Prevents a
   flash of bare ticks/guides before the loading overlay paints. */
.page-face-recognition .face-ring-host { visibility: hidden; }
.page-face-recognition .recognition-view.is-ready .face-ring-host { visibility: visible; }

/* When the face leaves the moldura (off-centre, too far/close, or
   no face at all), blur the camera so the ring is unmistakeably the
   target the user has to fill. The mask overlay still lets the
   blurred image show through inside the circle. */
.page-face-recognition .recognition-view.is-off-moldura .recognition-video {
    filter: blur(6px);
    transition: filter 220ms ease;
}
.page-face-recognition .recognition-view .recognition-video {
    /* Fade-in synced to .face-loading's fade-out (460ms ease) so the
       loading overlay dissolves directly into the live camera feed
       instead of cutting hard. Starts at 0 opacity; .is-ready on the
       recognition-view kicks it to 1. */
    opacity: 0;
    transition: filter 220ms ease, opacity 460ms ease;
}
.page-face-recognition .recognition-view.is-ready .recognition-video {
    opacity: 1;
}
/* Finalization phase — JS stops the MediaStream tracks (turning the camera
   indicator off and freeing the device); we also fade the <video> opacity
   to 0 so the existing #333 background of .recognition-view shows through.
   The slideshow stays visible inside the moldura on top of the backdrop. */
.page-face-recognition .recognition-view.is-finalizing .recognition-video {
    opacity: 0;
}

/* === CNH capture page ============================================== */
/* SVG positioning frame uses a cropped viewBox (50 0 346 500) so the
   moldura takes up more of the card. Match this aspect on the
   recognition-view itself so the SVG fills it edge-to-edge with no
   letterbox. Net effect: card is noticeably taller (aspect 0.692 vs the
   old 0.892) and the document slot inside the moldura sits closer to a
   real CNH-card proportion. */
.page-cnh-recognition .recognition-view {
    aspect-ratio: 346 / 500;
}
.page-cnh-recognition .recognition-view .recognition-video {
    /* Use object-fit: cover so the camera fills the view regardless of
       device camera aspect (most laptops are 16:9, mobile rear is 4:3). */
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity 460ms ease;
}
.page-cnh-recognition .recognition-view.is-ready .recognition-video {
    opacity: 1;
}
.page-cnh-recognition .recognition-view.is-finalizing .recognition-video {
    opacity: 0;
}

/* Document positioning SVG — sits on top of the live video, drawn at
   the same scale (446×500 viewBox = recognition-view box). The SVG's
   own dim cut-out + corner markers do all the visual work; CSS just
   colours them by state. */
.page-cnh-recognition .doc-frame-host {
    position: absolute;
    inset: 0;
    z-index: 30;
    pointer-events: none;
    /* Hidden until camera is live — same gate as the ring on the face
       page, so the dim cut-out doesn't flash on top of the loading
       overlay before the video arrives. */
    visibility: hidden;
}
.page-cnh-recognition .recognition-view.is-ready .doc-frame-host {
    visibility: visible;
}
.doc-frame-svg {
    width: 100%;
    height: 100%;
    display: block;
    /* Smooth colour shifts when JS toggles state classes (.is-aligned /
       .is-misaligned / .is-error). */
    transition: filter 240ms ease;
}
/* Default neutral palette — dim around the document slot is dark
   transparent grey, corner markers and inner hints are soft white. */
.doc-frame-dim    { fill: rgba(0, 0, 0, 0.55); }
.doc-frame-corners { fill: rgba(255, 255, 255, 0.85); transition: fill 220ms ease; }
.doc-frame-hints   { fill: rgba(255, 255, 255, 0.55); transition: fill 220ms ease; }

/* Aligned: 4 corners locked onto the document → green. */
.doc-frame-host.is-aligned .doc-frame-corners { fill: #25c97a; }
.doc-frame-host.is-aligned .doc-frame-hints   { fill: rgba(37, 201, 122, 0.65); }

/* Off-centre / wrong size — amber warning. */
.doc-frame-host.is-misaligned .doc-frame-corners { fill: #ffb84a; }

/* Final upload failure (after retries exhausted) — red. */
.doc-frame-host.is-error .doc-frame-corners { fill: #e84747; }
.doc-frame-host.is-error .doc-frame-hints   { fill: rgba(232, 71, 71, 0.5); }

/* Hide the document frame during finalization so the captured photo
   shows clean (the slideshow / preview takes its place). */
.recognition-view.is-finalizing .doc-frame-host {
    opacity: 0;
    transition: opacity 320ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Ring fills the full view width and sits centred on the same point as
   the mask (50%, 38%). With ticks drawn at viewBox y=8..17 (= radii
   83..92 SVG units, i.e. 41.5%..46% of viewBox half), and the ring
   host being 100% wide square = view-width × view-width, the inner
   tick edge falls at 41.5% × view-width. The mask uses 80% of
   closest-side (= 80% of view-width/2 = 40% of view-width). The 1.5%
   gap between mask edge and inner tick is the halo Apple Face ID has.
   Note: `inset: auto` MUST come before top/left or it wipes them. */
.page-face-recognition .face-ring-host {
    inset: auto;
    top: 38%;
    left: 50%;
    aspect-ratio: 1;
    /* Mask = 68% × half-side = 34% of view-width radius. Inner tick at
       41.5% × host-width, so host = 34/41.5 / 2 ≈ 86% of view width
       leaves a small halo gap between mask edge and tick. */
    width: 86%;
    height: auto;
    transform: translate(-50%, -50%);
}
.page-face-recognition .face-ring {
    width: 100%;
    max-width: none;
}

/* === Face ID-style animated overlay ====================================
   The ring lives in .face-ring-host (built by App.FaceRecognition on init).
   Three coordinated layers:
     1. Tick segments around the perimeter — fill with brand colour as
        each liveness check completes.
     2. A breathing pulse circle that slows when the user is steady.
     3. A success checkmark that scale-springs in on completion.
   States are driven by classes on the host element:
     .is-attention  → user needs to recenter (ticks turn amber, pulse faster)
     .is-capturing  → brief white flash to mimic a shutter
     .is-success    → ring fills, pulse stops, check appears
==================================================================== */
.face-ring-host {
    position: absolute;
    inset: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    pointer-events: none;
    z-index: 25;
    color: var(--page-brand-color, #25c97a);
    transition: opacity 0.4s ease;
}
.face-ring {
    width: 78%;
    max-width: 280px;
    height: auto;
    overflow: visible;
}

/* --- ticks (segments) ---
   Each tick gets its angle from --tick-angle (set on the element style by
   _buildRing). The base transform applies the rotation around the SVG
   centre; the .is-filled keyframe composes a scale on top of the same
   rotation so the radial layout survives the pop animation. */
/* Dense ring of thin ticks. Default: WHITE — same baseline as the
   Apple Face ID enrolment screen ('not yet scanned' state). On
   .is-filled they turn solid brand green with a spring pop. No
   continuous sweep — the colour change is the feedback. */
.face-ring-ticks line {
    stroke: rgba(255, 255, 255, 0.85);
    /* Match the loading animation's bar weight (1.6) so the handover
       from the boot loader to the live ring doesn't visually thin out
       the ticks. Filled (green) ticks below get a heavier weight to
       still read as 'thicker = done'. */
    stroke-width: 1.6;
    stroke-linecap: round;
    transform-box: view-box;
    transform-origin: 100px 100px;
    /* Per-tick rotation + a global ring-scale var so the whole halo can
       grow in from a small size during camera loading (JS toggles
       .is-loading on the host). */
    transform: rotate(var(--tick-angle, 0deg)) scale(var(--ring-scale, 1));
    transition: stroke 280ms ease, stroke-width 200ms ease,
                transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}
.face-ring-host.is-loading {
    --ring-scale: 0.45;
}
.face-ring-host.is-loading .face-ring-ticks line {
    opacity: 0.55;
}
.face-ring-host.is-attention .face-ring-ticks line:not(.is-filled) {
    stroke: rgba(255, 196, 84, 0.85);
    stroke-width: 1.6;             /* keep the matched loader weight */
}
.face-ring-ticks line.is-filled {
    stroke: #25c97a;
    stroke-width: 2.4;             /* still visibly thicker than 1.6 white/yellow */
    animation: face-tick-pop 360ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes face-tick-pop {
    0%   { stroke-width: 0.8; transform: rotate(var(--tick-angle, 0deg)) scale(calc(0.6 * var(--ring-scale, 1))); }
    60%  { stroke-width: 3.0; transform: rotate(var(--tick-angle, 0deg)) scale(calc(1.18 * var(--ring-scale, 1))); }
    100% { stroke-width: 2.4; transform: rotate(var(--tick-angle, 0deg)) scale(var(--ring-scale, 1)); }
}

/* === Dev debug panel (?debug in URL) ===============================
   Floats over the right-centre of the recognition view so the
   operator can read every detection variable while the user moves
   their face. Hidden by default; JS removes the `hidden` attribute
   and updates the textContent each frame. */
#faceDebug {
    position: fixed;
    z-index: 999;
    top: 50%;
    right: 12px;
    transform: translateY(-50%);
    margin: 0;
    padding: 12px 14px;
    max-width: 280px;
    max-height: 80vh;
    overflow: auto;
    background: rgba(0, 0, 0, 0.78);
    color: #d6f5d6;
    font: 11px/1.45 ui-monospace, "SF Mono", Menlo, monospace;
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.18);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    pointer-events: none;
    white-space: pre;
}
#faceDebug[hidden] { display: none; }
#faceDebug .ok   { color: #6dffa3; }
#faceDebug .bad  { color: #ff7a7a; }
#faceDebug .warn { color: #ffc07a; }

/* === Camera loading overlay ===========================================
   Smiley face inside a dotted halo. The dots are independent <line>
   elements built by JS (60 of them) sharing a single chase animation:
   each gets a -0.0167s phase offset (1s / 60), creating a comet of
   brand-coloured dots running clockwise. */
.face-loading {
    position: absolute;
    inset: 0;
    z-index: 50;
    background: var(--face-surface-bg, #1f2024);
    /* Mounts faded in (keyframe below); leaves via the .is-hiding
       class which fades both opacity and background to transparent
       at the same rate. The transition handles the way out, the
       keyframe handles the way in. */
    opacity: 1;
    animation: face-loading-fade-in 280ms ease both;
    transition: opacity 460ms ease, background-color 460ms ease;
}
.face-loading.is-hiding {
    opacity: 0;
    background-color: transparent;
    pointer-events: none;
}
@keyframes face-loading-fade-in {
    from { opacity: 0; background-color: transparent; }
    to   { opacity: 1; background-color: var(--face-surface-bg, #1f2024); }
}
/* Belt-and-braces: even without the class, the element is finally
   removed from layout when JS sets `hidden` after the transition
   finishes. */
.face-loading[hidden] { display: none; }
/* Loading art lives at the SAME position + size as the moldura
   (top: 38%, width 86%, square) so the dots line up exactly with
   the main ring's tick positions when the camera goes live. */
.page-face-recognition .face-loading-art {
    position: absolute;
    inset: auto;
    top: 38%;
    left: 50%;
    width: 86%;
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
}
/* CNH page reuses the same smiley + halo loading art, but the
   recognition-view is portrait (446×500) instead of 3:4, so the same
   86% width would spill outside the top edge. Center on the document
   slot midpoint (~y 41% in the SVG) and shrink to ~58% so the smiley
   sits comfortably inside the corner markers without overlapping
   them. Same animation, just sized for the document frame. */
.page-cnh-recognition .face-loading-art {
    position: absolute;
    inset: auto;
    top: 41%;
    left: 50%;
    width: 58%;
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
}
.face-loading-art svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* Loading ticks share geometry with .face-ring-ticks (same viewBox,
   same line coords y=8..17, same per-element rotation). Length, stroke
   width and opacity are written per frame by the JS rAF loop in
   App.FaceRecognition._buildLoadingArt — that drives the wave-sweep
   visual you see during camera bring-up. */
.face-loading-dots line {
    /* Uniform hairline gray — matches the Apple loader exactly: every
       bar is the same thickness and color, only LENGTH varies with the
       wave. No opacity ramp, no width ramp. */
    stroke: #c0c0c0;
    stroke-width: 0.9;
    stroke-linecap: round;
    transform-box: view-box;
    transform-origin: 100px 100px;
    transform: rotate(var(--dot-angle, 0deg));
}

/* The face wrapper is the 3D rotation pivot. perspective() in the inline
   transform set by JS only reads as a true tilt when the wrapper has a
   transform-box pinning the origin to the face's centre — otherwise the
   browser uses the SVG root origin and the head appears to swing in arcs
   instead of pivoting in place. */
.face-loading-face-wrap {
    transform-box: fill-box;
    transform-origin: 50% 50%;
    will-change: transform;
}

.face-loading-face path,
.face-loading-face circle,
.face-loading-face rect,
.face-loading-face line {
    /* Same gray as the bars — Apple's reference uses a single tone for
       the entire loader, both ring and face. White-on-#333 made the face
       pop while the bars receded.
       `rect` covers the CNH card-shaped head outline; without it the
       rectangle would render with the default fill (black) instead of
       a hairline stroke matching the rest of the smiley. */
    stroke: #c0c0c0;
    stroke-width: 2.4;
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
}
/* CNH card head — explicit white stroke (the document outline reads as
   "chrome" of the doc, not as a face feature). Slightly thinner than the
   inner stroke so the rect feels like a frame rather than a heavy box. */
.face-loading-face .face-loading-doc {
    stroke: #ffffff;
    stroke-width: 2.0;
    fill: none;
}

/* Match the orientation prompt exactly so the camera-ready handover
   doesn't shift the text — same position, same size, same weight.
   Default 72% targets the face flow (prompt at top:72%); the CNH
   override below shifts to 84% to align with that page's prompt. */
.face-loading-text {
    position: absolute;
    top: 72%;
    left: 50%;
    transform: translateX(-50%);
    margin: 0;
    color: #fff;
    font-size: 1.1rem;
    font-weight: 600;
    line-height: 1.35;
    padding: 0 24px;
    max-width: 80%;
    text-align: center;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}
.page-cnh-recognition .face-loading-text {
    top: 84%;
}

/* === Camera error overlay =========================================== */
.face-error {
    position: absolute;
    inset: 0;
    z-index: 60;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 24px;
    background: #000;
    color: #fff;
    text-align: center;
}
.face-error[hidden] { display: none; }
.face-error-icon { color: #ff6b6b; }
.face-error-title { font-size: 1.05rem; font-weight: 700; margin: 0; }
.face-error-body  { font-size: 0.9rem;  font-weight: 400; margin: 0; max-width: 320px; line-height: 1.45; color: rgba(255,255,255,0.78); }
.face-error-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    max-width: 320px;
    margin-top: 6px;
}

/* --- dome guides: 3D-ribbon scan effect ---
   9 parallel paths per axis (data-rib=0..8). Centre path (rib=4) is the
   bright leading edge; outer ribs sample older snapshots from the history
   buffer in JS, so they trail behind on movement — Apple's "motion blur"
   sheet. Stack of drop-shadow filters builds the neon halo. */
.face-ring-guides {
    filter: drop-shadow(0 0 1.5px rgba(180, 240, 255, 0.95))
            drop-shadow(0 0 4px   rgba(120, 210, 255, 0.70))
            drop-shadow(0 0 10px  rgba( 90, 190, 255, 0.45))
            drop-shadow(0 0 20px  rgba( 70, 170, 255, 0.30));
}
.face-ring-guides path {
    /* Lighter, almost-white cyan — Apple's ribbon reads more white-aqua
       than blue once the glow stack is on top. */
    stroke: #d6f3ff;
    stroke-linecap: round;
    fill: none;
    transition: stroke 240ms ease;
}
/* Stroke widths and opacities are stepped per rib so the centre line is
   the sharp bright leading edge and outer ribs fade to a soft trail.
   Thinner overall than before (was 1.2-1.6) to match Apple's hairline feel. */
/* Outer ribs bumped — they need to be readable enough to show the trail
   the user is supposed to see when their head turns. Previous values were
   tuned for a tight in-step ribbon; with the slower lerp + bigger age
   multiplier the outer ribs lag much further behind, so dimmer values
   were leaving the trail almost invisible. */
.face-ring-guides path[data-rib="0"],
.face-ring-guides path[data-rib="8"] { opacity: 0.32; stroke-width: 0.55; }
.face-ring-guides path[data-rib="1"],
.face-ring-guides path[data-rib="7"] { opacity: 0.50; stroke-width: 0.65; }
.face-ring-guides path[data-rib="2"],
.face-ring-guides path[data-rib="6"] { opacity: 0.68; stroke-width: 0.75; }
.face-ring-guides path[data-rib="3"],
.face-ring-guides path[data-rib="5"] { opacity: 0.85; stroke-width: 0.85; }
.face-ring-guides path[data-rib="4"] { opacity: 1.00; stroke-width: 1.0; }
.face-ring-host.is-attention .face-ring-guides path {
    stroke: #ffd9b8;
}
.face-ring-host.is-attention .face-ring-guides {
    filter: drop-shadow(0 0 1.5px rgba(255, 217, 184, 0.95))
            drop-shadow(0 0 4px   rgba(255, 183, 132, 0.7))
            drop-shadow(0 0 10px  rgba(255, 165, 110, 0.4));
}
.face-ring-host.is-success .face-ring-guides,
.face-ring-host.is-capturing .face-ring-guides { opacity: 0; }

/* --- capture shutter flash ---
   Covers the WHOLE viewport (not just .recognition-view) so the screen
   flashes white and acts as a fill light bouncing off the user's face.
   JS adds .is-active before each capture; the keyframe ramps up fast,
   plateaus during the canvas-draw window (so the captured selfie picks
   up the brightening), then fades out. pointer-events stay none so the
   live tick loop keeps running underneath. */
.face-flash-overlay {
    position: fixed;
    inset: 0;
    background: #fff;
    z-index: 9999;
    pointer-events: none;
    opacity: 0;
}
.face-flash-overlay.is-active {
    animation: face-screen-flash 480ms cubic-bezier(0.2, 0.6, 0.4, 1) forwards;
}
@keyframes face-screen-flash {
    0%   { opacity: 0; }
    10%  { opacity: 0.95; }   /* ~48ms in — peak brightness */
    30%  { opacity: 0.95; }   /* hold to ~144ms — capture window */
    100% { opacity: 0; }       /* fade by 480ms total */
}

/* --- success checkmark (springs in when the sequence finishes) --- */
.face-ring-check {
    opacity: 0;
    transform-box: view-box;
    transform-origin: 100px 100px;
    transform: scale(0.4);
    color: currentColor;
    transition: opacity 260ms ease, transform 480ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.face-ring-host.is-success .face-ring-check {
    opacity: 1;
    transform: scale(1);
}
.face-ring-host.is-success .face-ring {
    animation: face-success-glow 1200ms ease-out;
}
@keyframes face-success-glow {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.08); }
    100% { transform: scale(1); }
}

/* Captured-frame thumbs sit BELOW the orientation prompt as a centred
   horizontal row — matches the design layout. Empty slots stay hidden
   so the row collapses to just the captures so far. */
.capture-thumbs {
    list-style: none;
    margin: 0;
    padding: 0;
    position: absolute;
    z-index: 70;
    bottom: 8%;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: row;
    gap: 10px;
}
/* Thumbs only render once a frame has been captured. Empty slots stay
   hidden so the ring isn't visually competed with by placeholder boxes. */
.capture-thumb {
    position: relative; /* anchors the ✓ badge */
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 1.5px solid #25c97a;
    background-size: cover;
    background-position: center;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
    transition: transform 0.25s ease;
    display: none;
}
.capture-thumb.is-filled {
    display: block;
    animation: capture-thumb-pop 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.capture-thumb.is-filled::after {
    content: '✓';
    position: absolute;
    right: -4px;
    top: -4px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: #25c97a;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

@keyframes capture-thumb-pop {
    0%   { opacity: 0; transform: scale(0.5); }
    100% { opacity: 1; transform: scale(1); }
}

/* === Finalization slideshow ============================================
   Triggered by the .is-finalizing class on .recognition-view (added when
   the final upload kicks off). The thumbs slide down + fade out, while
   the slideshow inside the moldura cycles through the captured liveness
   frames with a slow ease-in-out crossfade. Two layers stacked, JS
   toggles `is-active` on whichever should be visible — the CSS opacity
   transition produces the crossfade. */
.face-slideshow {
    position: absolute;
    inset: 0;
    z-index: 6;     /* above the camera video, below the ring/guides */
    pointer-events: none;
    opacity: 0;
    transition: opacity 420ms cubic-bezier(0.4, 0, 0.2, 1);
    /* Same circular mask as the moldura — photos only paint inside the
       face hole, not the dim margin. Coordinates mirror the mask in
       .recognition-view::after. */
    -webkit-mask-image: radial-gradient(circle closest-side at 50% 38%,
        #000 68%, transparent 68.5%);
            mask-image: radial-gradient(circle closest-side at 50% 38%,
        #000 68%, transparent 68.5%);
}
.recognition-view.is-finalizing .face-slideshow { opacity: 1; }
.face-slideshow-layer {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    opacity: 0;
    /* Long, soft crossfade — matches the relaxed cadence of the cycle
       interval in JS (~900ms per frame). */
    transition: opacity 700ms cubic-bezier(0.4, 0, 0.2, 1);
    /* The capture frames came from the (mirrored) selfie video, so the
       camera frames are mirrored in the slideshow too — no extra flip. */
}
.face-slideshow-layer.is-active { opacity: 1; }

/* Thumb removal during finalization. JS staggers the .is-leaving class
   across the row left-to-right (130ms apart), so each thumb individually
   slides down + fades on its own beat. Keeping the row container in
   place (no en-bloc transform) lets the per-thumb timing read clearly. */
.capture-thumb {
    transition: transform 0.25s ease, opacity 380ms cubic-bezier(0.4, 0, 0.2, 1);
}
.capture-thumb.is-leaving {
    transform: translateY(60px);
    opacity: 0;
    transition: transform 380ms cubic-bezier(0.4, 0, 0.2, 1),
                opacity 320ms cubic-bezier(0.4, 0, 0.2, 1);
}
.recognition-view.is-finalizing .user-info {
    transition: transform 480ms cubic-bezier(0.4, 0, 0.2, 1),
                opacity 380ms cubic-bezier(0.4, 0, 0.2, 1);
    transform: translateY(40px);
    opacity: 0;
}

/* During the upload phase the dashed ticks + guide ribbons fade out, but
   the host itself stays visible so the new continuous arc (sibling element)
   can take their place. Selectively targeting the children — instead of
   the whole host — lets the arc remain interactive with .is-success. */
.page-face-recognition .face-ring-ticks,
.page-face-recognition .face-ring-guides {
    transition: opacity 420ms cubic-bezier(0.4, 0, 0.2, 1);
}
.page-face-recognition .recognition-view.is-finalizing .face-ring-ticks,
.page-face-recognition .recognition-view.is-finalizing .face-ring-guides {
    opacity: 0;
}

/* Continuous arc that replaces the 60 dashed ticks during finalization.
   Same radius as the dashes' midline (87.5 ≈ midpoint of y=8..17), so the
   line sits at exactly the same distance from the moldura — visually it
   reads as "the dashes merged into one ring". Stroke is thicker (2.6) so
   the line clearly pops against the dim grey background; the ticks are
   60 short segments and read collectively even at 1.6, but a single
   continuous circle disappears at that weight. */
.face-ring-arc {
    fill: none;
    stroke: #ffffff;
    stroke-width: 2.6;
    opacity: 0;
    transition: opacity 420ms cubic-bezier(0.4, 0, 0.2, 1),
                stroke 320ms ease;
}
.recognition-view.is-finalizing .face-ring-arc { opacity: 1; }

/* Green progress overlay — sits on top of the white .face-ring-arc track.
   stroke-dashoffset is animated by JS to grow the green fill clockwise from
   the LEFT (9 o'clock) — same starting point as the loading wave so the
   upload looks like a continuation of the boot animation. The transition on
   stroke-dashoffset smooths the jump to 100% when uploads actually finish. */
.face-ring-arc-progress {
    fill: none;
    stroke: #25c97a;
    stroke-width: 2.6;
    /* butt instead of round — round caps leave a small green dot at the
       path's start point even when stroke-dashoffset hides everything else,
       AND can leave a tiny visible seam at full coverage where the start +
       end caps don't perfectly overlap. butt removes both artifacts. */
    stroke-linecap: butt;
    opacity: 0;
    transition: opacity 420ms cubic-bezier(0.4, 0, 0.2, 1),
                stroke-dashoffset 280ms cubic-bezier(0.22, 1, 0.36, 1);
}
.recognition-view.is-finalizing .face-ring-arc-progress { opacity: 1; }
/* Upload failed (all retries exhausted) — JS resets the dashoffset and
   re-runs the fill, this rule only flips the colour. Track stays white
   underneath so the red fill clearly reads as a progress overlay. */
.face-ring-arc-progress.is-failed { stroke: #e84747; }

/* === Inline confirmation actions ======================================
   Shown after the final upload succeeds. Sits below the moldura where the
   thumbs used to be (those slid out during finalization). Hidden by
   default — the JS removes [hidden] when the green progress arc completes. */
.face-actions {
    position: absolute;
    z-index: 75;
    bottom: 6%;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: row;
    gap: 12px;
    align-items: center;
    justify-content: center;
    padding: 0 16px;
    /* Fade in from below so the appearance feels coordinated with the
       other finalization animations (slideshow + arc fill). */
    opacity: 0;
    transition: opacity 360ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 360ms cubic-bezier(0.4, 0, 0.2, 1);
}
.face-actions:not([hidden]) {
    opacity: 1;
    transform: translate(-50%, -8px);
}
.face-actions .btn {
    min-width: 130px;
}

.recognition-overlay {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    z-index: 5;
}

.detection-canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 6; /* Above overlay, below status bar/text */
}

.orientation-box {
    position: absolute;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    background-color: rgba(0, 0, 0, 0.45);
    color: #fff;
    padding: 12px 22px;
    border-radius: 999px;
    font-size: 0.95rem;
    font-weight: 600;
    text-align: center;
    z-index: 30;
    max-width: 80%;
    pointer-events: none;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.18);
    transition: background-color 280ms ease, color 280ms ease;
    will-change: transform, opacity;
}

/* Apple Face ID: prompt sits just below the face circle. The mask is
   centred at top:38% with radius ~30% of view-width (~22% of view
   height) — its bottom edge falls around 60% of view height, so a
   prompt at top: 64% lands cleanly in the gap. */
.page-face-recognition .orientation-box {
    inset: auto;
    top: 72%;
    left: 50%;
    transform: translateX(-50%);
    background: transparent;
    border: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    color: #fff;
    font-size: 1.1rem;
    font-weight: 600;
    line-height: 1.35;
    padding: 0 24px;
    max-width: 80%;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}
/* During liveness challenges the prompt sits at top: 72% (just
   below the mask). When the capture flow enters finalization
   (is-finalizing on the recognition-view → the inline confirm
   buttons appear at bottom: 6%), the prompt lifts up so it
   doesn't collide with the buttons. The challenge-time position
   stays the same — only the success copy moves. */
.page-face-recognition .recognition-view.is-finalizing .orientation-box {
    top: 58%;
}
.page-face-recognition .orientation-box.is-warning { background: transparent; color: #ffb784; }
.page-face-recognition .orientation-box.is-success { background: transparent; color: #25c97a; }
.orientation-box.is-warning { background-color: rgba(220, 60, 60, 0.85); }
.orientation-box.is-success { background-color: rgba(40, 175, 110, 0.92); }

/* CNH inherits the same prompt treatment as face: prompt sits just below
   the document slot (slot bottom ≈ 77% of view height in the 446×500 SVG,
   so 84% lands cleanly under the L-corner markers without overlapping
   them). Same transparent bubble + text-shadow look as face for visual
   continuity between the two capture flows. */
.page-cnh-recognition .orientation-box {
    inset: auto;
    top: 84%;
    left: 50%;
    transform: translateX(-50%);
    background: transparent;
    border: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    color: #fff;
    font-size: 1.1rem;
    font-weight: 600;
    line-height: 1.35;
    padding: 0 24px;
    max-width: 80%;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}
.page-cnh-recognition .orientation-box.is-warning { background: transparent; color: #ffb784; }
.page-cnh-recognition .orientation-box.is-success { background: transparent; color: #25c97a; }

/* Camera mirror — applied conditionally by JS when the active video track
   is using the user-facing camera (desktop fallback when no rear camera
   exists). On phones using the rear camera the document text reads the
   right way around without a mirror, so we leave it untouched. */
.recognition-video.is-mirrored {
    transform: scaleX(-1);
}

/* Each setPrompt() call removes-then-adds .is-entering, retriggering the
   slide-in animation so the user notices the new instruction. */
.orientation-box.is-entering {
    animation: face-prompt-in 320ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes face-prompt-in {
    0%   { opacity: 0; transform: translateX(-50%) translateY(10px); }
    100% { opacity: 1; transform: translateX(-50%) translateY(0); }
}

.status-bar {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 6px;
    background-color: #d93025; /* Default Red */
    z-index: 10;
    transition: background-color 0.3s ease;
}

.recognition-video {
    height: 100%;
    width: 100%;
    object-fit: cover;
    display: block;
}

/* Apple Face ID framing: the face circle sits in the UPPER portion of
   the view (~38% from the top). The periphery uses #333 with 85%
   alpha — same colour as the canvas + the loading panel, so the
   frame reads as one cohesive grey field with a faint glimpse of the
   room still showing through behind the rim of the moldura. */
.page-face-recognition .recognition-view::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--face-surface-dim, rgba(31, 32, 36, 0.85));
    pointer-events: none;
    z-index: 20;
    -webkit-mask-image: radial-gradient(circle closest-side at 50% 38%,
        transparent 68%, #000 68.5%);
            mask-image: radial-gradient(circle closest-side at 50% 38%,
        transparent 68%, #000 68.5%);
}

.recognition-upload-loader {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 12px;
    background: rgba(0, 0, 0, 0.6);
    z-index: 30;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

.recognition-upload-loader.is-visible {
    opacity: 1;
    pointer-events: auto;
}

.recognition-upload-spinner {
    width: 42px;
    height: 42px;
    border: 4px solid rgba(255, 255, 255, 0.35);
    border-top-color: #fff;
    border-radius: 50%;
    animation: recognition-loader-spin 0.9s linear infinite;
}

.recognition-upload-text {
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    text-align: center;
    padding: 0 16px;
}

@keyframes recognition-loader-spin {
    to {
        transform: rotate(360deg);
    }
}

/* .btn-full is deprecated — use .btn-wide instead. */

.invisible {
    display: none;
}

/* Face Recognition Specifics */
.page-face-recognition .recognition-overlay {
    width: 85%;
    height: 85%;
    opacity: 0.6;
}

.page-face-recognition .recognition-video {
    width: auto;
    transform: rotateY(180deg);
}

/* CNH Recognition Specifics */
.page-cnh-recognition .recognition-overlay {
    width: 92%;
    height: 92%;
    opacity: 0.8;
    border: 2px solid rgba(255, 255, 255, 0.8);
    border-radius: 8px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
}

/* CNH Corner Markers */
.page-cnh-recognition .recognition-overlay::before,
.page-cnh-recognition .recognition-overlay::after,
.page-cnh-recognition .face-overlay-bottom::before,
.page-cnh-recognition .face-overlay-bottom::after {
    content: '';
    position: absolute;
    width: 20px;
    height: 20px;
    border-color: #fff;
    border-style: solid;
    pointer-events: none;
}

.page-cnh-recognition .recognition-overlay::before { top: -2px; left: -2px; border-width: 4px 0 0 4px; border-radius: 4px 0 0 0; }
.page-cnh-recognition .recognition-overlay::after { top: -2px; right: -2px; border-width: 4px 4px 0 0; border-radius: 0 4px 0 0; }

.page-cnh-recognition .face-overlay-bottom {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
}

.page-cnh-recognition .face-overlay-bottom::before { bottom: -2px; left: -2px; border-width: 0 0 4px 4px; border-radius: 0 0 0 4px; }
.page-cnh-recognition .face-overlay-bottom::after { bottom: -2px; right: -2px; border-width: 0 4px 4px 0; border-radius: 0 0 4px 0; }


/* Recognition view stays full-width inside .page-wrapper (max 460px), so the
   single rule above already covers every breakpoint — no overrides needed. */
@media (max-width: 768px) {
    .page-face-recognition .recognition-overlay {
        width: 70% !important;
        height: 70% !important;
    }

    .page-cnh-recognition .recognition-overlay {
        width: 90% !important;
        height: auto !important;
        aspect-ratio: 1/1.45 !important;
        top: 50% !important;
    }
}

/* === CNH choice screen (Tela 1) ====================================== */
/* Two stacked option cards (camera vs PDF upload). Vertical because the
   page is narrow on mobile and the card content reads left-to-right. */
/* `.cnh-choice-card` was originally the picker-screen tile (two cards
   side-by-side: upload vs camera). The picker is gone — the upload page
   is now the entry point and the card is reused there as the "capture
   via camera" alternative below the drop zone. The legacy list wrapper
   was removed; the card style is kept as-is. */
.cnh-choice-card {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 18px 20px;
    background: var(--page-card-bg, #fff);
    border: 1px solid var(--page-card-border, #ececec);
    border-radius: 14px;
    color: var(--page-text-color, #333);
    text-decoration: none;
    transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
    text-align: left;
}
.cnh-choice-card:hover,
.cnh-choice-card:focus-visible {
    transform: translateY(-1px);
    border-color: var(--page-brand-color, #25c97a);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
    outline: none;
}
.cnh-choice-icon {
    font-size: 30px;
    line-height: 1;
    flex-shrink: 0;
}
.cnh-choice-text {
    flex: 1;
    min-width: 0;
}
.cnh-choice-title {
    font-weight: 600;
    font-size: 1rem;
    margin-bottom: 2px;
}
.cnh-choice-desc {
    color: var(--page-muted, #6c757d);
    font-size: 0.85rem;
    line-height: 1.35;
}
.cnh-choice-chevron {
    font-size: 22px;
    color: var(--page-muted, #cdd0d6);
    flex-shrink: 0;
}

/* Capture-via-camera card on the upload page. Reuses the .cnh-choice-card
   look but sits as a sibling of the drop zone, with a small breathing
   room above. Hidden once the upload succeeds — at that point only the
   confirm/retake pair stays visible. */
.cnh-capture-card {
    margin-top: 12px;
}
.page-cnh-upload.is-done .cnh-capture-card { display: none; }

/* CNH pages collapse the default content gutter — the drop zone + capture
   card already provide their own internal padding, and the actions block
   below carries enough breathing room. Removing both the bottom margin
   on .content and the airy variant's padding keeps the layout tight. */
.page-cnh-upload .content,
.page-cnh-upload .content-airy {
    margin-bottom: 0;
    padding-bottom: 0;
}

/* === CNH PDF upload screen (Tela 2b) ================================= */
/* Drop zone shell — kept compact (the page already has a hero above
   explaining what to do, so the zone itself just needs to invite the
   drop, not host instructions). Smooth 240ms cross-state transition. */
/* :not([hidden]) so `display: flex` doesn't override the UA's
   `[hidden] { display: none }` rule (same specificity, mine wins by
   source order — would otherwise leave the camera page's done wrapper
   visible at page load even though the markup sets `hidden`). */
.cnh-drop-zone:not([hidden]) {
    position: relative;
    min-height: 200px;
    border: 1.5px dashed var(--page-card-border, #d0d4d9);
    border-radius: 18px;
    /* Track the theme card surface so the drop zone matches the rest of
       the page in both light and dark (no more hardcoded white island
       on dark backgrounds). The bg deliberately does NOT animate — only
       `border-color` does — so a late theme handoff doesn't fade the
       surface through a wrong colour. */
    background: var(--page-card-bg, #fff);
    padding: 28px 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 10px;
    cursor: pointer;
    transition: border-color 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.cnh-drop-zone.is-dragover {
    border-color: var(--page-brand-color, #25c97a);
    background-color: rgba(37, 201, 122, 0.06);
}
/* State containers fade in with a tiny lift — feels like the content
   "settles" into place rather than popping. The :not([hidden]) guard
   stops `display: flex` from overriding the UA's `[hidden] {
   display:none }` rule (same specificity, later in cascade — would
   leave every state visible at once otherwise). */
.cnh-drop-idle:not([hidden]),
.cnh-drop-busy:not([hidden]),
.cnh-drop-error:not([hidden]),
.cnh-drop-done:not([hidden]) {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    animation: cnh-drop-fade-in 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes cnh-drop-fade-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}

.cnh-drop-icon {
    width: 42px;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--page-muted, #9aa0a6);
}
.cnh-drop-icon svg {
    width: 100%;
    height: 100%;
    stroke-width: 1.5;
    stroke: currentColor;
    fill: none;
}
.cnh-drop-title {
    font-weight: 600;
    font-size: 1.15rem;
    line-height: 1.3;
    letter-spacing: -0.01em;
    color: var(--page-text-color, #333);
}
.cnh-drop-sub {
    color: var(--page-muted, #6c757d);
    font-size: 0.9rem;
    line-height: 1.4;
    max-width: 320px;
}
.cnh-drop-hint {
    color: var(--page-muted, #9aa0a6);
    font-size: 0.78rem;
    margin-top: 8px;
    letter-spacing: 0.01em;
}

/* Busy state — bigger, slower spinner. Apple loaders run at ~1s/rev,
   not the snappy 720ms; the calmness reads as "we're working on it,
   not stuck". */
.cnh-drop-spinner {
    width: 48px;
    height: 48px;
    border: 3.5px solid var(--page-card-border, #d0d4d9);
    border-top-color: var(--page-brand-color, #25c97a);
    border-radius: 50%;
    animation: cnh-drop-spin 980ms linear infinite;
}
@keyframes cnh-drop-spin {
    to { transform: rotate(360deg); }
}

/* Error state — muted yellow icon (warning, not danger) + red title.
   No body line by default; the title carries the message. */
.cnh-drop-error .cnh-drop-icon {
    color: var(--page-warning-color, #f59e0b);
}
.cnh-drop-error .cnh-drop-icon svg {
    stroke-width: 2;
}
.cnh-drop-error .cnh-drop-title {
    color: var(--page-error-color, #dc3545);
}

.cnh-drop-preview {
    /* Use all the horizontal room the drop zone offers (its 28px side
       padding caps us at ~`100% - 56px`); the natural CNH aspect (~0.7)
       keeps the height in check. */
    max-width: 100%;
    max-height: 420px;
    width: auto;
    height: auto;
    display: block;
}
.cnh-drop-done .cnh-drop-title { font-size: 1.05rem; }

/* === Done state — page-level layout shift =============================
   When the upload+crop OR camera-capture+upload succeeds we strip the
   "submission" framing: the page hero (Envie sua CNH digital + drop
   instructions) and the dashed drop-zone chrome are no longer relevant.
   The done content takes their place — large title above the CNH
   preview, with the action buttons immediately below.
   Both pages share the same selectors so the two flows render
   identically (the camera page mirrors the upload page's done block). */
.page-cnh-upload.is-done .hero,
.page-cnh-recognition.is-done .hero { display: none; }
.page-cnh-upload.is-done .cnh-drop-zone,
.page-cnh-recognition.is-done .cnh-drop-zone {
    border: 0;
    background: transparent;
    padding: 0;
    min-height: 0;
    cursor: default;
    /* Centre the column so a tall preview stays narrow on wide viewports. */
    max-width: 480px;
    margin: 0 auto;
}
/* Promote the done title to hero scale and reorder so the title /
   subtitle sit ABOVE the image (matching the rhythm of the original
   page hero). The flex order trick lets us keep the markup unchanged. */
.page-cnh-upload.is-done .cnh-drop-done,
.page-cnh-recognition.is-done .cnh-drop-done {
    gap: 6px;
}
.page-cnh-upload.is-done .cnh-drop-done .cnh-drop-title,
.page-cnh-recognition.is-done .cnh-drop-done .cnh-drop-title {
    order: 1;
    font-size: 1.5rem;
    font-weight: 600;
    letter-spacing: -0.015em;
    margin-bottom: 4px;
    /* Match the face match success colour (orientation-box.is-success
       uses #25c97a) so the "ready to advance" cue is consistent. */
    color: #25c97a;
}
.page-cnh-upload.is-done .cnh-drop-done .cnh-drop-sub,
.page-cnh-recognition.is-done .cnh-drop-done .cnh-drop-sub {
    order: 2;
    font-size: 0.95rem;
    margin-bottom: 14px;
}
.page-cnh-upload.is-done .cnh-drop-done .cnh-drop-preview,
.page-cnh-recognition.is-done .cnh-drop-done .cnh-drop-preview {
    order: 3;
    /* Keep the preview proportional but compact — the title + subtitle
       above already establish the "CNH pronta" cue, the image just needs
       to be legible enough to verify legibility before advancing. ~360px
       leaves the action pair below comfortably above the fold on common
       phone viewports (iPhone SE / 12 mini territory). */
    max-height: 360px;
    max-width: 280px;
}

/* :not([hidden]) so `display: flex` doesn't override the UA's
   `[hidden] { display: none }` (same specificity, my rule would
   otherwise win by source order — leaving the buttons visible at
   page load even though JS set `actions.hidden = true`).
   Side-by-side row matching the face match done state: Quero mudar on
   the left (warning, outlined) + Avançar on the right (success, solid).
   Same shape as `.face-actions .btn { min-width: 130px }` — buttons
   share the row via `.btn-action { flex: 1 }`. */
.cnh-upload-actions:not([hidden]) {
    display: flex;
    flex-direction: row;
    gap: 12px;
    align-items: center;
    justify-content: center;
    margin-top: 20px;
}
.cnh-upload-actions .btn-action {
    flex: 1;
    /* Forbid wrapping on the labels — "Quero mudar" was wrapping to two
       lines when flex-basis pulled the button tighter than the text. The
       buttons stay side-by-side and grow to fit their own labels; if the
       container is narrower than two single-line buttons fit (very rare
       at the screen sizes we target), the larger label still owns its
       row and the layout stays predictable. */
    white-space: nowrap;
    /* Slightly tighter padding so two single-line labels comfortably
       share a narrow viewport. */
    padding: 14px 18px;
    font-size: 1rem;
    border-radius: 12px;
}
/* Hide the Voltar wrapper in the done state — at that point the
   relevant CTAs are Quero refazer / Avançar (revealed by JS via the
   `.cnh-upload-actions` hidden toggle), which already let the user
   reroute through the choice screen via the reset POST. The wrapper
   is otherwise just a layout grouping inside .actions; styling comes
   from the inner .btn.btn-secondary. */
.page-cnh-upload.is-done .cnh-upload-back { display: none; }

/* === CNH camera capture — done state ================================
   The done block (data-cnh-capture-done) is a .content.content-airy
   wrapper containing the same .cnh-drop-zone > .cnh-drop-done + sibling
   .actions structure the upload page renders, so it inherits all
   .page-cnh-upload.is-done rules above via the dual selectors. The
   recognition page needs two extras:
   1. Hide the camera chrome (the image-capture div with the video,
      moldura, manual button, etc) once the capture is done.
   2. Force the wrapper back to `display: none` when `hidden` — `.content`
      sets `display: flex`, which would otherwise win over the UA's
      `[hidden] { display: none }` and leak the preview at page load. */
.page-cnh-recognition.is-done #image-capture { display: none; }
.page-cnh-recognition [data-cnh-capture-done][hidden] { display: none; }

/* === Custom-field file dropzone =====================================
   Drag-and-drop surface used in custom_data_form.html. The native
   <input type="file"> is visually hidden but the wrapping <label>
   acts as the click target (clicking the dropzone opens the file
   picker via the label[for] association). Drag-and-drop handlers in
   main.js set input.files programmatically and re-dispatch `change`,
   so the same validation pipeline (size limit, required check,
   accept= filter) runs whether the visitor clicked or dropped a
   file. The dashed outline + soft fill follow the page's apple
   surface tokens so it sits alongside the other inputs naturally. */
.file-dropzone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 18px 16px;
    border: 1.5px dashed var(--border-color, #d4d4d8);
    border-radius: 12px;
    background-color: var(--surface-soft, #f8fafc);
    color: var(--text-color, #1f2937);
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.05s ease;
    text-align: center;
}
.file-dropzone:hover {
    border-color: var(--primary, #3b82f6);
    background-color: var(--surface-hover, #eff6ff);
}
.file-dropzone:focus-within {
    outline: 2px solid var(--primary, #3b82f6);
    outline-offset: 2px;
}
.file-dropzone.is-dragover {
    border-color: var(--primary, #3b82f6);
    background-color: var(--surface-hover, #eff6ff);
    transform: scale(1.01);
}
/* Visually-hidden input — kept in the DOM so form serialisation +
   the existing main.js `input[type="file"]` selectors still find it.
   Stays focusable for keyboard users; the parent label highlights
   via `:focus-within` so the focus ring is visible. */
.file-dropzone-input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}
.file-dropzone-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}
.file-dropzone-icon {
    color: var(--text-muted, #64748b);
    transition: color 0.15s ease;
}
.file-dropzone:hover .file-dropzone-icon,
.file-dropzone.is-dragover .file-dropzone-icon {
    color: var(--primary, #3b82f6);
}
.file-dropzone-copy {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.file-dropzone-primary {
    font-weight: 600;
    font-size: 0.95rem;
}
.file-dropzone-hint {
    color: var(--text-muted, #64748b);
    font-size: 0.8rem;
}
/* Selected-file pill — JS toggles `hidden` off + populates innerText
   the moment a file is picked / dropped. Shown below the call-to-
   action copy so the visitor can see what's queued without opening
   the picker again. */
.file-dropzone-filename {
    display: inline-flex;
    align-items: center;
    max-width: 100%;
    margin-top: 6px;
    padding: 4px 10px;
    border-radius: 999px;
    background-color: var(--primary-soft, #dbeafe);
    color: var(--primary, #1d4ed8);
    font-size: 0.8rem;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
