/* ============================================================
   LOGIN — split panel.

   The page used to be a single centred card on a near-black
   radial ground, dark in both themes, with the form itself
   carrying four gradients, a white 2px border, a flickering
   monospace title, an outlined wordmark and a field of shooting
   stars. Six of those animations ran forever behind a form whose
   whole job is two inputs.

   What replaced it:

   ── Two panels, one card ────────────────────────────────────
   LEFT is the brand plate — seal, wordmark, what the system
   does. It stays deep slate in BOTH themes, for the same reason
   theme.css keeps --plate white: it is a brand ground, not a
   surface. RIGHT is the form, on --surface, and it follows the
   theme like every other page in the app.

   Which is the change worth knowing about: index.php now
   includes includes/theme_head.php, so the login page is no
   longer dark-only. reg.php has since followed.

   ── Everything is a token ───────────────────────────────────
   No hardcoded neutral anywhere below. The two accents that ARE
   literal — #667eea (the sidebar/dashboard indigo) and #38bdf8
   (already the logo ring here) — are brand colours and are
   deliberately theme-independent, per the note in theme.css.

   ── The class names are load-bearing ────────────────────────
   .login-tab[data-method], .login-method, .password-toggle,
   .face-login-modal/.show, .face-login-status-modal +
   .detecting/.success/.error, .flashlight-toggle.active and the
   #ids are all read by assets/js/{faceLogin,faceModal,
   flashlight,showPassword}.js. They are kept exactly; only what
   they look like changed.
   ============================================================ */

.login-page {
    --plate-1: #0f172a;
    --plate-2: #1e293b;
    --plate-ink: #ffffff;
    --plate-ink-2: rgba(255, 255, 255, .68);
    --plate-ink-3: rgba(255, 255, 255, .42);
    --plate-line: rgba(255, 255, 255, .12);

    --accent: #667eea;
    --accent-2: #38bdf8;

    /* Field wells sit just below the card in light and just above
       it in dark — an inset well reads as an empty hole on a
       near-black card. */
    --well: #f8fafc;
    --well-focus: #ffffff;

    --mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas,
        "Liberation Mono", monospace;

    --r-card: 20px;
    --r-field: 12px;
}

:root[data-theme="dark"] .login-page {
    --well: rgba(255, 255, 255, .04);
    --well-focus: rgba(255, 255, 255, .07);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .login-page {
        --well: rgba(255, 255, 255, .04);
        --well-focus: rgba(255, 255, 255, .07);
    }
}

* {
    box-sizing: border-box;
}

/* main.css is not loaded here, so the font stack is repeated —
   same stack, so the login page and the admin pages match. */
body.login-page {
    margin: 0;
    min-height: 100vh;
    min-height: 100dvh;
    padding: clamp(16px, 4vw, 48px);
    padding-top: max(clamp(16px, 4vw, 48px), env(safe-area-inset-top));
    padding-bottom: max(clamp(16px, 4vw, 48px), env(safe-area-inset-bottom));
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    color: var(--ink-2);
    font-family: system-ui, -apple-system, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, "Noto Sans", sans-serif;
    font-size: 15px;
    line-height: 1.55;
    overflow-x: hidden;
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    -webkit-tap-highlight-color: transparent;
}

/* The particle field stays — it is the same ambient texture the QR
   generator, scanner and tracker carry. Its background was a
   hardcoded #2c2e43, which in light mode painted a dark slab over
   the page; transparent lets the token ground through and leaves
   only the particles. theme.css dims the whole layer in light. */
#particles-js {
    position: fixed;
    inset: 0;
    background-color: transparent;
    z-index: 0;
}

canvas {
    display: block;
    vertical-align: bottom;
}

/* ─── The card ───────────────────────────────────────────── */

.login-shell {
    position: relative;
    z-index: 2;
    width: 100%;
    max-width: 1040px;
    display: grid;
    grid-template-columns: 1.04fr 1fr;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-card);
    box-shadow: 0 24px 60px -20px var(--shadow-lg), 0 2px 6px var(--shadow);
    overflow: hidden;
    animation: cardIn .55s cubic-bezier(.22, 1, .36, 1) both;
}

@keyframes cardIn {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: none; }
}

/* ─── Left: brand plate ──────────────────────────────────── */

.login-plate {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 28px;
    padding: clamp(28px, 3.4vw, 44px);
    color: var(--plate-ink);
    background:
        radial-gradient(circle at 8% 0%, rgba(102, 126, 234, .34), transparent 58%),
        radial-gradient(circle at 100% 100%, rgba(56, 189, 248, .18), transparent 55%),
        linear-gradient(160deg, var(--plate-2), var(--plate-1));
    isolation: isolate;
    overflow: hidden;
}

/* The QR module field, drawn by assets/js/loginPlate.js. The
   product reads QR codes, so the plate's texture is a module grid
   with a real finder pattern rather than abstract noise. */
.plate-modules {
    position: absolute;
    inset: 0;
    z-index: 0;
    opacity: .5;
    -webkit-mask-image: radial-gradient(circle at 78% 24%, #000 0%, transparent 68%);
    mask-image: radial-gradient(circle at 78% 24%, #000 0%, transparent 68%);
    pointer-events: none;
}

.plate-modules canvas {
    width: 100%;
    height: 100%;
}

.plate-scan {
    position: absolute;
    left: 0;
    right: 0;
    height: 140px;
    z-index: 0;
    background: linear-gradient(to bottom,
            transparent,
            rgba(56, 189, 248, .13) 45%,
            rgba(56, 189, 248, .3) 50%,
            rgba(56, 189, 248, .13) 55%,
            transparent);
    animation: plateSweep 7s cubic-bezier(.5, 0, .5, 1) infinite;
    pointer-events: none;
}

@keyframes plateSweep {
    0% { transform: translateY(-160px); opacity: 0; }
    12% { opacity: 1; }
    88% { opacity: 1; }
    100% { transform: translateY(100%); opacity: 0; }
}

.login-plate > *:not(.plate-modules):not(.plate-scan) {
    position: relative;
    z-index: 1;
}

.plate-crest {
    display: flex;
    align-items: center;
    gap: 16px;
}

/* The seal is dark ink and a yellow sun on transparency; it needs
   a genuinely white plate or it vanishes into the slate — the same
   problem --plate exists for in theme.css. */
.plate-crest .logo-img {
    width: 72px;
    height: 72px;
    flex-shrink: 0;
    border-radius: 50%;
    object-fit: contain;
    background: var(--plate);
    padding: 4px;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .5), 0 8px 24px rgba(0, 0, 0, .35);
}

.plate-crest h1 {
    margin: 0;
    font-size: 1.06rem;
    font-weight: 700;
    line-height: 1.28;
    letter-spacing: -.01em;
    text-wrap: balance;
}

.plate-crest p {
    margin: 3px 0 0;
    font-size: .82rem;
    color: var(--plate-ink-2);
}

.plate-pitch {
    margin-top: auto;
}

/* Holds the system_name straight from Settings, which is a sentence
   and not a word — "BCC Student Attendance System Using QR" in the
   current database. It is allowed to wrap to a second line rather
   than being truncated; the rule is a max-width, so a shorter name
   still sits on one. */
.plate-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    max-width: 40ch;
    font-family: var(--mono);
    font-size: .68rem;
    font-weight: 600;
    line-height: 1.5;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--accent-2);
}

.plate-eyebrow::before {
    content: "";
    width: 22px;
    height: 1px;
    flex-shrink: 0;
    background: currentColor;
    opacity: .55;
}

.plate-wordmark {
    margin: 12px 0 0;
    font-size: clamp(2.3rem, 4.6vw, 3.1rem);
    font-weight: 800;
    letter-spacing: -.035em;
    line-height: 1;
    color: var(--plate-ink);
}

.plate-wordmark span {
    color: var(--accent-2);
}

/* Its own class, NOT `.plate-pitch p`. That element selector scores
   higher than .plate-wordmark and silently won the font-size — the
   wordmark rendered at .94rem instead of 3rem. */
.plate-lede {
    margin: 12px 0 0;
    max-width: 34ch;
    font-size: .94rem;
    color: var(--plate-ink-2);
}

/* Three capabilities that run in parallel, so they take icons and
   not 01/02/03 — the numbering would claim an order that is not
   there. */
.plate-caps {
    display: grid;
    gap: 12px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.plate-caps li {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: .88rem;
    color: var(--plate-ink-2);
}

.plate-caps i {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: 9px;
    border: 1px solid var(--plate-line);
    background: rgba(255, 255, 255, .06);
    color: var(--accent-2);
    font-size: 1rem;
}

.plate-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding-top: 18px;
    border-top: 1px solid var(--plate-line);
    font-family: var(--mono);
    font-size: .7rem;
    letter-spacing: .04em;
    color: var(--plate-ink-3);
}

.plate-status {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--plate-ink-2);
}

.plate-status i {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #10b981;
    box-shadow: 0 0 0 3px rgba(16, 185, 129, .2);
    animation: plateBeat 2.4s ease-in-out infinite;
}

@keyframes plateBeat {
    0%, 100% { box-shadow: 0 0 0 3px rgba(16, 185, 129, .2); }
    50% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
}

/* ─── Right: the form ────────────────────────────────────── */

.login-panel {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 20px;
    padding: clamp(28px, 3.4vw, 46px);
    background: var(--surface);
}

.login-head h2 {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--ink);
}

.login-head p {
    margin: 5px 0 0;
    font-size: .9rem;
    color: var(--ink-4);
}

/* Segmented control. The two tabs are not equals: "Password"
   selects a panel, "Face login" opens the recognition modal and
   the password tab stays selected underneath (faceModal.js). The
   styling says so — one is a selected segment, the other reads as
   a control you press. */
.login-tabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
    padding: 4px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--surface-2);
}

.login-tab {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-width: 0;
    min-height: 42px;
    padding: 8px 10px;
    border: 0;
    border-radius: 10px;
    background: transparent;
    color: var(--ink-4);
    font: inherit;
    font-size: .88rem;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    transition: background .18s ease, color .18s ease, box-shadow .18s ease;
}

.login-tab svg {
    flex-shrink: 0;
}

.login-tab:hover {
    color: var(--ink-2);
}

.login-tab.active {
    background: var(--surface);
    color: var(--accent);
    box-shadow: 0 1px 3px var(--shadow), 0 0 0 1px var(--line);
}

.login-tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.login-method {
    display: none;
}

.login-method.active {
    display: block;
}

.form {
    display: grid;
    gap: 16px;
}

.input-container {
    position: relative;
    display: grid;
    gap: 7px;
}

.input-container label {
    font-family: var(--mono);
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-4);
}

.input-field {
    position: relative;
    display: flex;
    align-items: center;
}

.input-field > .field-icon {
    position: absolute;
    left: 14px;
    color: var(--ink-4);
    font-size: 1rem;
    line-height: 1;
    pointer-events: none;
    transition: color .18s ease;
}

.input-container input {
    width: 100%;
    min-height: 50px;
    padding: 12px 14px 12px 42px;
    border: 1px solid var(--line);
    border-radius: var(--r-field);
    background: var(--well);
    color: var(--ink);
    font: inherit;
    /* 16px floor: anything smaller and iOS Safari zooms the page
       every time the field is tapped. */
    font-size: 16px;
    transition: border-color .18s ease, background .18s ease, box-shadow .18s ease;
}

.input-container input::placeholder {
    color: var(--ink-4);
    opacity: .75;
}

.input-container input:focus {
    outline: none;
    border-color: var(--accent);
    background: var(--well-focus);
    box-shadow: 0 0 0 4px rgba(102, 126, 234, .18);
}

.input-field:focus-within > .field-icon {
    color: var(--accent);
}

/* Room for the eye so it never sits on the text. */
.password-container input {
    padding-right: 52px;
}

.password-container .password-toggle {
    position: absolute;
    right: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--ink-4);
    font-size: 18px;
    cursor: pointer;
    z-index: 2;
    transition: color .18s ease, background .18s ease;
}

.password-container .password-toggle:hover {
    color: var(--ink-2);
    background: rgba(var(--tint), .06);
}

.password-container .password-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* No "keep me signed in" and no "forgot password" here: neither has
   anything behind it — crud/login_process.php does not read a
   remember flag and there is no reset page. A control that does
   nothing is worse than one that is missing. */

.login-panel a {
    color: var(--accent);
    font-weight: 600;
    text-decoration: none;
}

.login-panel a:hover {
    text-decoration: underline;
}

.login-panel a:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

.submit {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    min-height: 52px;
    padding: 14px 18px;
    border: 0;
    border-radius: var(--r-field);
    background: linear-gradient(135deg, #667eea, #5566d8);
    color: var(--on-accent);
    font: inherit;
    font-size: .95rem;
    font-weight: 600;
    letter-spacing: .01em;
    cursor: pointer;
    box-shadow: 0 8px 20px -8px rgba(102, 126, 234, .8);
    transition: transform .16s ease, box-shadow .16s ease, filter .16s ease;
}

.submit:hover {
    filter: brightness(1.06);
    transform: translateY(-1px);
    box-shadow: 0 12px 26px -10px rgba(102, 126, 234, .9);
}

.submit:active {
    transform: translateY(0);
}

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

/* components/footer.php carries `margin-top: 30px` as an inline
   style, which no stylesheet can outrank without !important. The
   panel already spaces its children with gap, so the extra 30px
   only pushes the copyright line away from its own rule. */
.login-foot {
    padding-top: 16px;
    border-top: 1px solid var(--line);
    font-size: .84rem;
    color: var(--ink-4);
}

.login-foot footer {
    margin-top: 0 !important;
    padding: 0 !important;
    text-align: left !important;
    font-size: .78rem !important;
}

/* ─── Face recognition modal ─────────────────────────────── */

.face-login-modal {
    display: none;
    position: fixed;
    /* `inset: 0` alone stretches this to the viewport. The old rule
       also set `height: 100dvh`, and an explicit height beats the
       bottom offset — where dvh resolved short (any browser mid
       address-bar transition, and headless Chrome) the backdrop
       ended above the bottom of the screen and left a bright strip. */
    inset: 0;
    z-index: 1040;
    padding: 16px;
    padding-top: max(16px, env(safe-area-inset-top));
    padding-bottom: max(16px, env(safe-area-inset-bottom));
    background-color: rgba(2, 6, 23, .72);
    backdrop-filter: blur(6px);
    overscroll-behavior: contain;
}

.face-login-modal.show {
    display: flex;
    align-items: center;
    justify-content: center;
    animation: modalFade .25s ease;
}

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

.modal-content-face {
    position: relative;
    width: 100%;
    max-width: 500px;
    max-height: 100%;
    padding: 26px;
    border: 1px solid var(--line);
    border-radius: var(--r-card);
    background: var(--surface);
    box-shadow: 0 30px 70px -20px var(--shadow-lg);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    animation: modalRise .3s cubic-bezier(.22, 1, .36, 1);
}

@keyframes modalRise {
    from { transform: translateY(24px); opacity: 0; }
    to { transform: none; opacity: 1; }
}

.modal-header-face {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 18px;
}

.modal-header-face h3 {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -.01em;
    color: var(--ink);
}

.modal-header-face h3 i {
    color: var(--accent);
}

.close-modal {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    border: 1px solid var(--line);
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--ink-3);
    font-size: 20px;
    cursor: pointer;
    transition: background .2s ease, color .2s ease, transform .2s ease;
}

.close-modal:hover {
    background: var(--surface-3);
    color: var(--ink);
    transform: rotate(90deg);
}

.close-modal:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.face-video-wrapper-modal {
    position: relative;
    width: 100%;
    height: 330px;
    max-height: 46dvh;
    margin-bottom: 16px;
    border: 1px solid var(--line);
    border-radius: 16px;
    /* Deep in both themes on purpose: this is a camera preview, and
       a white frame around a live face is glare, not a surface. */
    background: #0b1220;
    overflow: hidden;

    /* Every colour in the scan effect below is mixed from this one
       channel triple, so a state change is a single custom-property
       swap rather than a dozen rules. Sky is the resting hue — the
       same 56,189,248 the brand plate sweeps with, the status chip
       tints with and the canvas draws its brackets in. */
    --scan-rgb: 56, 189, 248;
    --scan-speed: 2.4s;
}

.face-video-wrapper-modal.is-success { --scan-rgb: 16, 185, 129; }
.face-video-wrapper-modal.is-error   { --scan-rgb: 239, 68, 68; }

/* Verifying is the one state that should feel like it is working:
   the beam speeds up as the consecutive-match count climbs. */
.face-video-wrapper-modal.is-detecting { --scan-speed: 1.35s; }

/* ─── Scan effect over the camera preview ────────────────────
   Decorative only. It lives UNDER the detection canvas (z-index 10)
   so the brackets and the name label faceLogin.js draws always win,
   and it is invisible until .is-live goes on — before the camera
   opens there is nothing to scan, and a beam sweeping over a black
   rectangle just looks broken.

   The whole thing is four spans rather than pseudo-elements on one,
   because they animate on different clocks and .is-success has to
   freeze the beam while the flash still plays. */

.face-scan-fx {
    position: absolute;
    inset: 0;
    z-index: 5;
    border-radius: inherit;
    overflow: hidden;
    opacity: 0;
    transition: opacity .45s ease;
    pointer-events: none;
}

.face-video-wrapper-modal.is-live .face-scan-fx {
    opacity: 1;
}

/* Faint sensor grid — reads as "the camera is measuring", and gives
   the beam something to travel across on an otherwise flat image. */
.face-scan-grid {
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(var(--scan-rgb), .16) 1px, transparent 1px),
        linear-gradient(90deg, rgba(var(--scan-rgb), .16) 1px, transparent 1px);
    background-size: 34px 34px;
    /* Strongest at the edges, clear over the middle where the face is. */
    -webkit-mask-image: radial-gradient(ellipse 62% 58% at 50% 48%, transparent 40%, #000 100%);
    mask-image: radial-gradient(ellipse 62% 58% at 50% 48%, transparent 40%, #000 100%);
    opacity: .55;
    transition: opacity .3s ease;
}

/* The sweep itself: a soft band with a bright filament at its centre,
   same shape as .plate-scan on the brand plate. */
.face-scan-beam {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 30%;
    background:
        linear-gradient(to bottom,
            transparent,
            rgba(var(--scan-rgb), .10) 42%,
            rgba(var(--scan-rgb), .30) 50%,
            rgba(var(--scan-rgb), .10) 58%,
            transparent);
    box-shadow: 0 0 22px rgba(var(--scan-rgb), .28);
    animation: faceScanSweep var(--scan-speed) cubic-bezier(.45, 0, .55, 1) infinite;
}

.face-scan-beam::after {
    content: "";
    position: absolute;
    left: 6%;
    right: 6%;
    top: 50%;
    height: 2px;
    border-radius: 2px;
    background: linear-gradient(90deg,
            transparent,
            rgba(var(--scan-rgb), .95) 20%,
            rgba(var(--scan-rgb), .95) 80%,
            transparent);
    box-shadow: 0 0 12px rgba(var(--scan-rgb), .8);
}

@keyframes faceScanSweep {
    0%   { transform: translateY(-100%); opacity: 0; }
    12%  { opacity: 1; }
    88%  { opacity: 1; }
    100% { transform: translateY(333%); opacity: 0; }
}

/* Viewfinder brackets on the frame corners — where to put your face.
   Eight background layers, two per corner, so no extra elements. */
.face-scan-guides {
    position: absolute;
    inset: 16px;
    --g: rgba(var(--scan-rgb), .8);
    --arm: 26px;
    --thick: 2px;
    background:
        linear-gradient(var(--g) 0 0) left    top    / var(--thick) var(--arm) no-repeat,
        linear-gradient(var(--g) 0 0) left    top    / var(--arm) var(--thick) no-repeat,
        linear-gradient(var(--g) 0 0) right   top    / var(--thick) var(--arm) no-repeat,
        linear-gradient(var(--g) 0 0) right   top    / var(--arm) var(--thick) no-repeat,
        linear-gradient(var(--g) 0 0) left    bottom / var(--thick) var(--arm) no-repeat,
        linear-gradient(var(--g) 0 0) left    bottom / var(--arm) var(--thick) no-repeat,
        linear-gradient(var(--g) 0 0) right   bottom / var(--thick) var(--arm) no-repeat,
        linear-gradient(var(--g) 0 0) right   bottom / var(--arm) var(--thick) no-repeat;
    animation: faceScanPulse 2s ease-in-out infinite;
}

@keyframes faceScanPulse {
    0%, 100% { opacity: .55; }
    50%      { opacity: 1; }
}

/* Fires once when a state lands, by restarting the animation on a
   class change. Off (scaled out, transparent) at rest. */
.face-scan-flash {
    position: absolute;
    inset: 0;
    opacity: 0;
    background: radial-gradient(circle at 50% 45%,
            rgba(var(--scan-rgb), .28),
            transparent 62%);
}

/* Locked on: the beam stops travelling and the frame holds steady —
   the stillness is the signal that the scan is over. */
.face-video-wrapper-modal.is-success .face-scan-beam,
.face-video-wrapper-modal.is-error .face-scan-beam {
    animation: none;
    opacity: 0;
}

.face-video-wrapper-modal.is-success .face-scan-guides {
    animation: none;
    opacity: 1;
    --arm: 40px;
    --thick: 3px;
}

.face-video-wrapper-modal.is-success .face-scan-flash {
    animation: faceScanFlash .7s ease-out 1;
}

.face-video-wrapper-modal.is-error .face-scan-guides {
    animation: faceScanShake .4s ease-in-out 1;
    opacity: 1;
}

.face-video-wrapper-modal.is-error .face-scan-flash {
    animation: faceScanFlash .5s ease-out 1;
}

@keyframes faceScanFlash {
    0%   { opacity: 0; transform: scale(1.06); }
    35%  { opacity: 1; transform: scale(1); }
    100% { opacity: 0; transform: scale(1); }
}

@keyframes faceScanShake {
    0%, 100% { transform: translateX(0); }
    25%      { transform: translateX(-5px); }
    75%      { transform: translateX(5px); }
}

/* Grid off once a verdict is in — nothing left to measure. */
.face-video-wrapper-modal.is-success .face-scan-grid,
.face-video-wrapper-modal.is-error .face-scan-grid {
    opacity: .2;
}


.face-video-wrapper-modal video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.face-video-wrapper-modal canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 10;
    pointer-events: none;
}

/* faceLogin.js swaps the whole className between these four, so
   each state has to stand on its own — no shared base class to
   fall back to. */
.face-login-status-modal {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 50px;
    padding: 13px 15px;
    margin-bottom: 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-field);
    background: var(--surface-2);
    color: var(--ink-3);
    font-size: .88rem;
    text-align: center;
}

.face-login-status-modal.detecting {
    border-color: rgba(56, 189, 248, .45);
    background: rgba(56, 189, 248, .12);
    color: var(--scan-ink, #7dd3fc);
}

.face-login-status-modal.success {
    border-color: rgba(16, 185, 129, .45);
    background: rgba(16, 185, 129, .12);
    color: var(--ok-ink, #34d399);
}

.face-login-status-modal.error {
    border-color: rgba(239, 68, 68, .45);
    background: rgba(239, 68, 68, .12);
    color: var(--bad-ink, #fca5a5);
}

/* Shown by flashlight.js with display:flex once a torch-capable
   track is found, so the rule must not fight that. */
.flashlight-toggle {
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    min-height: 48px;
    padding: 12px 20px;
    border: 1px solid var(--line);
    border-radius: var(--r-field);
    background: var(--surface-2);
    color: var(--ink-2);
    font: inherit;
    font-size: .9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .2s ease, border-color .2s ease, color .2s ease;
}

.flashlight-toggle:hover {
    background: var(--surface-3);
    border-color: var(--line-strong);
}

.flashlight-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.flashlight-toggle.active {
    border-color: rgba(245, 158, 11, .5);
    background: rgba(245, 158, 11, .14);
    color: var(--warn-ink, #fbbf24);
}

.flashlight-toggle.active #flashlightIcon {
    color: #fbbf24;
    filter: drop-shadow(0 0 8px rgba(251, 191, 36, .7));
}

#flashlightIcon {
    font-size: 18px;
}

/* SweetAlert has to clear the modal. */
.swal2-container {
    z-index: 99999 !important;
}

/* ─── Responsive ─────────────────────────────────────────── */

@media (max-width: 900px) {
    .login-shell {
        grid-template-columns: 1fr;
        max-width: 520px;
    }

    /* The plate becomes a header strip. The capability list and the
       session footer do not earn vertical space above a form on a
       phone — the crest and the wordmark do. */
    .login-plate {
        gap: 18px;
        padding: 26px 24px;
    }

    .plate-caps,
    .plate-foot {
        display: none;
    }

    .plate-crest .logo-img {
        width: 56px;
        height: 56px;
    }

    .plate-wordmark {
        font-size: 2.1rem;
    }

    .plate-lede {
        font-size: .88rem;
    }

    .plate-modules {
        -webkit-mask-image: radial-gradient(circle at 82% 50%, #000 0%, transparent 72%);
        mask-image: radial-gradient(circle at 82% 50%, #000 0%, transparent 72%);
    }

    /* Bottom-left is where the AI widget launcher is not. */
    .tb-theme.theme-floating {
        left: auto;
        right: 16px;
        bottom: 16px;
    }

    .face-video-wrapper-modal {
        height: 280px;
    }
}

@media (max-width: 480px) {
    .login-tab {
        font-size: .82rem;
    }

    .modal-content-face {
        padding: 18px;
    }

    .modal-header-face h3 {
        font-size: 1rem;
    }

    .face-video-wrapper-modal {
        height: 240px;
    }

    .login-foot footer {
        text-align: center !important;
    }
}

@media (max-width: 360px) {
    .login-tab svg {
        display: none;
    }
}

/* Short landscape phones: the card must not grow past the screen. */
@media (max-height: 560px) and (orientation: landscape) {
    body.login-page {
        align-items: flex-start;
    }

    .login-plate {
        padding: 20px 22px;
        gap: 14px;
    }

    .plate-crest .logo-img {
        width: 52px;
        height: 52px;
    }

    .face-video-wrapper-modal {
        height: 200px;
    }
}

/* No real hover on touch, so the pressed state is the feedback. */
@media (hover: none) {
    .submit:hover,
    .close-modal:hover {
        transform: none;
        filter: none;
    }

    .submit:active,
    .login-tab:active {
        opacity: .88;
        transform: scale(.985);
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation: none !important;
        transition-duration: .01ms !important;
    }
}
