/* ── Kabiri auth design system ─────────────────────────────────────────
   Shared by the five static auth pages: login, signup, verify-email,
   reset-password and verify-2fa. One file instead of five inline copies
   because the inline convention already drifted (reset-password grew a
   black button and an 8-char rule, verify-2fa went dark indigo).

   The visual language mirrors the landing hero (index.html): the black
   liquid photograph (assets/hero-bg.jpg) on the image pane, Space
   Grotesk uppercase display type, Kabiri-blue CTA cards with a circled
   arrow, and the dashboard's graph-paper gridline on the form pane. The
   photograph is constant - it used to inverse-flip to white marble on
   state changes, which read as a flicker across a signup -> login ->
   onboarding run rather than as one signature moment.

   The formless onboarding modal (services/FormlessOnboarding.js) keeps
   its own injected stylesheet because it must render on index.html AND
   the deploy-copied onboarding.html without touching either page's
   head - its token values are copied from here; keep them in sync. */

:root {
    /* Brand literals - BaseStyles.js is app-side CSS-in-JS and cannot
       be imported by public pages; these mirror its values. */
    --kb-blue: #1433d2;
    --kb-blue-hover: #4a66e6;
    --kb-navy: #0e2399;
    --kb-light: #eef1ff;
    --kb-ring: rgb(20 51 210 / 28%);
    --kb-error: #dc2626;

    /* 5.5:1 on the white form pane. The old #059669 was 3.8:1, and it is
       used for the "requirement met" state, which is information. */
    --kb-success: #047857;
    --kb-text: #111827;
    --kb-text-secondary: #6b7280;

    /* Tertiary is the quietest text on the pane, but it still carries
       placeholders, the OTP separator and the password rules, so it sits
       above 4.5:1 (4.7:1 on white) rather than below it - #9ca3af was
       2.5:1. */
    --kb-text-tertiary: #6e7482;
    --kb-border: #e5e7eb;

    /* The app's --app-grid-line literal (BaseStyles.js). */
    --kb-grid-line: rgb(0 0 0 / 4.5%);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family:
        Inter,
        system-ui,
        -apple-system,
        sans-serif;
    background: #fff;
    color: var(--kb-text);
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
}

/* ── Skip link ──
   Duplicated from site-chrome.css rather than shared: the auth pages load
   this file and nothing else, so a link styled only over there would render
   as a stray visible row above the logo. Keep the two in sync. Parked
   off-screen by transform (not display:none) so it stays focusable - being
   the first tab stop is the entire point. */
.skip-link {
    position: fixed;
    top: 8px;
    left: 8px;
    z-index: 100;
    padding: 10px 16px;
    border-radius: 10px;
    background: #fff;
    color: var(--kb-blue);
    font-size: 13px;
    font-weight: 650;
    line-height: 1;
    text-decoration: none;
    box-shadow:
        0 1px 3px rgb(0 0 0 / 12%),
        0 8px 24px rgb(3 10 60 / 25%);
    transform: translateY(calc(-100% - 16px));
    transition: transform 0.15s ease;
}

.skip-link:focus {
    transform: translateY(0);
    outline: 3px solid var(--kb-blue);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .skip-link {
        transition: none;
    }
}

/* ── Keyboard focus ──
   One visible ring for everything on the five auth pages. The controls
   here are brand-blue cards and white inputs on white, where the UA's
   own thin outline disappears; the fields below re-state it because they
   clear the outline for their own resting treatment. */
:focus-visible {
    outline: 3px solid var(--kb-blue);
    outline-offset: 2px;
}

/* The photo pane is the one dark surface on the page. */
.auth-image-frame :focus-visible {
    outline-color: #fff;
    box-shadow: 0 0 0 6px rgb(6 14 70 / 55%);
}

/* ── Split-view shell ───────────────────────────────────────────────
   Form pane flush against the left edge, photo pane on the right with
   a sliver of padding (top, right, bottom) and curved corners. Same
   geometry the formless onboarding shell uses. */
.auth-shell {
    position: fixed;
    inset: 0;
    background: #fff;
    display: flex;
    align-items: stretch;
    z-index: 1;
}

/* The dashboard tab's graph-paper gridline (24px tile), so every auth
   page reads as a piece of the app. It's drawn on the SHELL, not the
   form pane, so the paper runs edge to edge - it carries on behind the
   photo pane and shows through the sliver of padding around it, rather
   than stopping dead at the halfway seam. Masked to fade out towards the
   bottom of the page so the ruling is strongest where the headline sits
   and dissolves into plain white by the footer. */
.auth-shell::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-image:
        repeating-linear-gradient(
            0deg,
            transparent,
            transparent 23px,
            var(--kb-grid-line) 23px,
            var(--kb-grid-line) 24px
        ),
        repeating-linear-gradient(
            90deg,
            transparent,
            transparent 23px,
            var(--kb-grid-line) 23px,
            var(--kb-grid-line) 24px
        );
    mask-image: linear-gradient(to bottom, #000 0%, rgb(0 0 0 / 55%) 45%, transparent 100%);
}

/* Both panes sit above the ruled paper. The form pane is deliberately
   transparent - an opaque white fill here would paint the grid out. */
.auth-form-pane {
    flex: 1 1 50%;
    min-width: 0;
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background-color: transparent;
}

.auth-form-scroll {
    flex: 1 1 auto;

    /* Scrollable on short viewports, but the scrollbar is hidden so
       the pane reads as a clean surface on desktop. */
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding: 28px 36px 48px;
    display: flex;
    flex-direction: column;
}

.auth-form-scroll::-webkit-scrollbar {
    display: none;
}

@media (width >= 768px) {
    .auth-form-scroll {
        padding: 36px 56px 64px;
    }
}

@media (width >= 1280px) {
    .auth-form-scroll {
        padding: 44px 80px 80px;
    }
}

/* Header floats in the top-left corner of the pane, independent of
   the scroll padding. */
.auth-form-header {
    position: absolute;
    top: 16px;
    left: 16px;
    right: 16px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.auth-form-logo img {
    height: 14px;
    width: auto;
    display: block;
}

.back-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    color: var(--kb-text-secondary);
    background: transparent;
    border: none;
    font-family: inherit;
    cursor: pointer;
    text-decoration: none;
    transition:
        color 0.15s ease,
        background 0.15s ease;
}

.back-link:hover {
    color: var(--kb-text);
    background: rgb(0 0 0 / 4%);
}

.auth-form-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    width: 100%;
    max-width: 420px;
    margin: 0 auto;
    position: relative;
    z-index: 1;
}

/* ── Display type ───────────────────────────────────────────────────
   The landing hero's Space Grotesk uppercase voice, scaled for a form
   pane. Pages must include Space Grotesk in their Google Fonts URL. */
.auth-headline {
    font-family: 'Space Grotesk', Inter, system-ui, sans-serif;
    font-size: clamp(30px, 3.4vw, 44px);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: -0.02em;
    line-height: 0.98;
    color: var(--kb-text);
    text-align: left;
    margin: 0 0 12px;
    transition: color 1s ease;
}

.auth-subtext {
    font-size: 15px;
    font-weight: 400;
    color: var(--kb-text-secondary);
    text-align: left;
    margin: 0 0 28px;
    line-height: 1.5;
}

/* ── Coming back from a provider ────────────────────────────────────
   A full-page OAuth redirect hands the user back to the page it left
   from, so this form is the last thing between the provider's consent
   screen and the app - painted in full, buttons and all, for as long as
   the credential and the routing behind it take. Users read that as
   being bounced back to the start: the sign-in they just finished looks
   to have failed until the page finally moves.

   js/oauth-redirect.js (loaded from <head> with data-splash) puts
   data-oauth-return on <html> before the first paint when this load is a
   return leg, and these rules show the panel in the form's place. The
   form is hidden, never removed - a sign-in that did not take needs it
   back, which is what KabiriOAuth.endSplash() is for. */
.auth-return {
    display: none;
    flex-direction: column;
    align-items: flex-start;
    gap: 16px;
}

html[data-oauth-return] .auth-form-body > :not(.auth-return) {
    display: none;
}

html[data-oauth-return] .auth-return {
    display: flex;
}

/* The panel is spaced by its own flex gap, so the shared display type's
   bottom margins would double it. */
.auth-return .auth-headline,
.auth-return .auth-subtext {
    margin-bottom: 0;
}

.auth-return-spinner {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 2px solid var(--kb-border);
    border-top-color: var(--kb-blue);
    animation: authReturnSpin 0.7s linear infinite;
}

@keyframes authReturnSpin {
    to {
        transform: rotate(360deg);
    }
}

/* Reduced motion keeps the mark and drops the rotation - it says
   "something is happening", and the headline already says what. */
@media (prefers-reduced-motion: reduce) {
    .auth-return-spinner {
        animation: none;
        border-color: var(--kb-blue);
        background: var(--kb-light);
    }
}

/* ── Provider buttons ───────────────────────────────────────────────
   Labeled full-width rows (Linear/Notion style) replacing the old
   72px icon-only squares - the email tile especially needs its label.
   The .auth-btn class name and per-provider ids are unchanged so the
   inline scripts' bindings and disabled states keep working. */
.auth-buttons {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 22px;
}

.auth-buttons.hidden {
    display: none;
}

.auth-btn {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    min-height: 52px;
    padding: 0 18px;
    background: #fff;
    border: 1px solid var(--kb-border);
    border-radius: 14px;
    font-family: inherit;
    font-size: 14.5px;
    font-weight: 550;
    color: var(--kb-text);
    cursor: pointer;
    transition:
        border-color 0.15s ease,
        box-shadow 0.15s ease,
        transform 0.15s ease;
}

.auth-btn:hover {
    border-color: var(--kb-blue);
    box-shadow: 0 4px 14px rgb(20 51 210 / 10%);
    transform: translateY(-1px);
}

.auth-btn:active {
    transform: scale(0.99);
}

.auth-btn[disabled] {
    opacity: 0.6;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

.auth-btn .auth-btn-icon {
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;

    /* The mail glyph inside draws with currentColor, so its colour is set
       here rather than baked into the markup as a hex - the brand icons
       beside it keep their own fixed colours, being logos. */
    color: #374151;
}

.auth-btn .auth-btn-icon svg {
    max-width: 100%;
    max-height: 100%;
}

.auth-btn .auth-btn-label {
    /* Was visually hidden when the buttons were icon-only squares;
       now the visible row label. */
    flex: 1 1 auto;
    text-align: left;
}

/* ── Email form ─────────────────────────────────────────────────────
   Hidden under the provider list, revealed by the email provider
   button (`.active` toggled by the inline scripts). */
.email-form {
    display: none;
    margin-top: 14px;
}

.email-form.active {
    display: block;
}

.email-form label,
.auth-field-label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: var(--kb-text);
    margin: 14px 0 6px;
}

.email-form input,
.auth-input {
    width: 100%;
    padding: 13px 14px;
    background: #fff;
    border: 1px solid var(--kb-border);
    border-radius: 12px;
    font-family: inherit;
    font-size: 15px;
    color: var(--kb-text);
    margin-bottom: 6px;
    transition:
        border-color 0.15s ease,
        box-shadow 0.15s ease;
}

/* Keep the text-input chrome off checkboxes. */
.email-form input[type='checkbox'] {
    width: auto;
    padding: 0;
    margin: 0;
    border-radius: 4px;
    flex-shrink: 0;
    accent-color: var(--kb-blue);
}

.email-form input:focus,
.auth-input:focus {
    outline: none;
    border-color: var(--kb-blue);
    box-shadow: 0 0 0 3px var(--kb-ring);
}

.email-form input:focus-visible,
.auth-input:focus-visible {
    outline: 3px solid var(--kb-blue);
    outline-offset: 2px;
}

.email-form input::placeholder,
.auth-input::placeholder {
    color: var(--kb-text-tertiary);
}

.email-form-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
    font-size: 13px;
}

.email-form-row label {
    margin-bottom: 0;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--kb-text-secondary);
    font-weight: 400;
}

.email-form-row a {
    color: var(--kb-blue);
    text-decoration: none;
    font-weight: 500;
}

.email-form-row a:hover {
    text-decoration: underline;
}

/* ── Primary CTA ────────────────────────────────────────────────────
   The landing hero's blue card: uppercase label left, circled arrow
   right. Submit buttons carry an .auth-cta-label span the inline
   scripts write status text into. */
.email-submit,
.auth-cta-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    width: 100%;
    padding: 14px 14px 14px 20px;
    margin-top: 18px;
    background: var(--kb-blue);
    color: #fff;
    border: none;
    border-radius: 14px;
    font-family: inherit;
    font-size: 13px;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    cursor: pointer;
    box-shadow: 0 10px 28px rgb(4 10 50 / 28%);
    transition:
        background 0.2s ease,
        box-shadow 0.2s ease,
        transform 0.2s ease;
}

.email-submit:hover,
.auth-cta-card:hover {
    background: var(--kb-blue-hover);
    box-shadow: 0 14px 34px rgb(4 10 50 / 34%);
    transform: translateY(-2px);
}

.email-submit:active,
.auth-cta-card:active {
    transform: translateY(0) scale(0.995);
}

.email-submit[disabled],
.auth-cta-card[disabled] {
    background: #d1d5db;
    color: var(--kb-text-secondary);
    box-shadow: none;
    cursor: not-allowed;
    transform: none;
}

.auth-cta-arrow {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: #fff;
    color: var(--kb-blue);
    flex: 0 0 auto;
}

.auth-cta-arrow svg {
    width: 14px;
    height: 14px;
}

.email-submit[disabled] .auth-cta-arrow,
.auth-cta-card[disabled] .auth-cta-arrow {
    background: #f3f4f6;
    color: var(--kb-text-tertiary);
}

/* Centred single-label variant for buttons whose textContent is
   rewritten wholesale by JS (e.g. verify-email's Verify button). */
.auth-cta-plain {
    display: block;
    width: 100%;
    padding: 15px;
    margin-top: 18px;
    background: var(--kb-blue);
    color: #fff;
    border: none;
    border-radius: 14px;
    font-family: inherit;
    font-size: 13px;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    text-align: center;
    cursor: pointer;
    box-shadow: 0 10px 28px rgb(4 10 50 / 28%);
    transition:
        background 0.2s ease,
        box-shadow 0.2s ease,
        transform 0.2s ease;
}

.auth-cta-plain:hover {
    background: var(--kb-blue-hover);
    box-shadow: 0 14px 34px rgb(4 10 50 / 34%);
    transform: translateY(-2px);
}

.auth-cta-plain[disabled] {
    background: #d1d5db;
    color: var(--kb-text-secondary);
    box-shadow: none;
    cursor: not-allowed;
    transform: none;
}

.email-back-btn {
    width: 100%;
    padding: 12px;
    margin-top: 10px;
    background: transparent;
    color: var(--kb-text-secondary);
    border: none;
    border-radius: 10px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: color 0.15s ease;
}

.email-back-btn:hover {
    color: var(--kb-text);
}

/* ── Messages ──────────────────────────────────────────────────────
   These three containers hold a role="alert" / role="status", so they
   have to stay rendered: a live region that is display:none until the
   moment it has something to say is not in the accessibility tree when
   the text arrives, and screen readers announce nothing. "Wrong
   password" was silent for exactly that reason.

   So instead of toggling display, an empty container simply has no box -
   no padding, no border, no background, no margin - which collapses it
   to zero height while leaving it live. The .active class is kept
   working for the callers that still set it, but presence of text is
   what decides whether anything is visible. */
.error-message,
.success-message,
.auth-status {
    display: block;
    margin-top: 12px;
    padding: 10px 14px;
    border-radius: 10px;
    font-size: 13px;
    line-height: 1.4;
}

.error-message {
    background: rgb(239 68 68 / 8%);
    border: 1px solid rgb(239 68 68 / 25%);
    color: var(--kb-error);
}

.error-message a {
    color: var(--kb-blue);
    text-decoration: underline;
    font-weight: 500;
}

.success-message {
    background: #f0fdf4;
    border: 1px solid #bbf7d0;
    color: #15803d;
}

/* Status pill (verify pages): neutral by default, tinted by state. */
.auth-status {
    background: #f3f4f6;
    border: 1px solid var(--kb-border);
    color: var(--kb-text-secondary);
}

.auth-status.is-error {
    background: rgb(239 68 68 / 8%);
    border-color: rgb(239 68 68 / 25%);
    color: var(--kb-error);
}

.auth-status.is-success {
    background: #f0fdf4;
    border-color: #bbf7d0;
    color: #15803d;
}

/* Last, so it beats every tint above it: nothing to say, nothing to see. */
.error-message:empty,
.success-message:empty,
.auth-status:empty {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
}

/* These boxes are permanent fixtures that fill with text rather than being
   created when something goes wrong, so there is no new element to animate
   and no class toggle to hang this on. Going from :empty to :not(:empty) is
   the state change, and a selector that starts matching starts its animation
   - so filling one in plays this, and nothing plays on load while they are
   still blank. Failed sign-ins are the one moment on these pages where the
   answer appears somewhere the user was not looking. */
.error-message:not(:empty),
.success-message:not(:empty) {
    animation: auth-message-in 180ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes auth-message-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* No app-level clamp out here - the public pages answer to the OS alone. */
@media (prefers-reduced-motion: reduce) {
    .error-message:not(:empty),
    .success-message:not(:empty) {
        animation: none;
    }
}

/* The lines below the provider buttons ("New user? Sign up", the terms
   note, "Already have an account?") share the headline's left edge - the
   whole column reads as one flush-left stack rather than a left-aligned
   headline over centred small print. */
.signup-link {
    margin-top: 18px;
    text-align: left;
    font-size: 13px;
    color: var(--kb-text-secondary);
}

.signup-link a {
    color: var(--kb-text);
    text-decoration: underline;
    font-weight: 500;
    margin-left: 4px;
}

.legal-text {
    margin-top: 24px;

    /* No side padding: it would inset this line from the headline's edge. */
    padding: 0;
    text-align: left;
    font-size: 12px;
    line-height: 1.5;
    color: var(--kb-text-tertiary);
}

.legal-text a {
    color: var(--kb-text-secondary);
    text-decoration: underline;
}

.legal-text a:hover {
    color: var(--kb-text);
}

/* ── OTP inputs (verify-email, verify-2fa) ──────────────────────────── */
.otp-grid {
    display: flex;
    gap: 10px;
    justify-content: flex-start;
    margin: 18px 0 6px;
}

.otp-input,
.code-input {
    width: 52px;
    height: 60px;
    text-align: center;
    font-family: 'Space Grotesk', Inter, monospace;
    font-size: 24px;
    font-weight: 700;
    color: var(--kb-text);
    background: #fff;
    border: 1px solid var(--kb-border);
    border-radius: 12px;
    transition:
        border-color 0.15s ease,
        box-shadow 0.15s ease;
}

.otp-input:focus,
.code-input:focus {
    outline: none;
    border-color: var(--kb-blue);
    box-shadow: 0 0 0 3px var(--kb-ring);
}

.otp-input:focus-visible,
.code-input:focus-visible {
    outline: 3px solid var(--kb-blue);
    outline-offset: 2px;
}

.otp-input.is-error,
.code-input.is-error {
    border-color: var(--kb-error);
}

.otp-separator {
    align-self: center;
    color: var(--kb-text-tertiary);
    font-weight: 700;
}

@media (width <= 420px) {
    .otp-input,
    .code-input {
        width: 44px;
        height: 54px;
        font-size: 20px;
    }

    .otp-grid {
        gap: 7px;
    }
}

/* ── Show/hide password toggle (signup, login, reset-password) ─────
   Wrap the input in .password-row and drop a .toggle-pw button next to
   it; js/password-toggle.js binds any button carrying data-toggle-pw.
   The row owns the 6px gap the input normally carries so the eye stays
   centred on the field rather than on field-plus-margin. */
.password-row {
    position: relative;
    margin-bottom: 6px;
}

.password-row input {
    /* Room for the eye, and no double gap under the row. */
    padding-right: 46px;
    margin-bottom: 0;
}

.password-row .toggle-pw {
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    color: var(--kb-text-tertiary);
    transition: color 0.15s ease;
}

.password-row .toggle-pw:hover {
    color: var(--kb-text-secondary);
}

.password-row .toggle-pw:focus-visible {
    outline: 3px solid var(--kb-blue);
    outline-offset: 2px;
}

/* ── Password strength meter (signup, reset-password) ─────────────── */
.password-strength {
    height: 4px;
    background: var(--kb-border);
    border-radius: 2px;
    overflow: hidden;
    margin: 8px 0 4px;
}

.password-strength-fill {
    /* display: block because the fill is a <span> - inline elements
       ignore height, so the bar would never paint. */
    display: block;
    height: 100%;
    width: 0;
    border-radius: 2px;
    transition:
        width 0.25s ease,
        background 0.25s ease;
}

/* "Already have an account?" line (signup) - same look as .signup-link. */
.login-link {
    margin-top: 18px;
    text-align: left;
    font-size: 13px;
    color: var(--kb-text-secondary);
}

.login-link a {
    color: var(--kb-text);
    text-decoration: underline;
    font-weight: 500;
    margin-left: 4px;
}

.password-strength-label {
    font-size: 12px;
    color: var(--kb-text-secondary);
    min-height: 16px;
    display: block;
}

.password-requirements {
    list-style: none;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px 14px;
    margin: 8px 0 2px;
    padding: 0;
    font-size: 12px;
    color: var(--kb-text-tertiary);
}

.password-requirements li {
    display: flex;
    align-items: center;
    gap: 6px;
    transition: color 0.2s ease;
}

.password-requirements li::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #d1d5db;
    flex-shrink: 0;
    transition: background 0.2s ease;
}

.password-requirements li.met {
    color: var(--kb-success);
}

.password-requirements li.met::before {
    background: #10b981;
}

/* ── Photo pane ─────────────────────────────────────────────────────
   The landing hero's liquid photograph. It lives on a ::before layer
   rather than on the frame itself, so the caption above it is never
   affected by anything applied to the artwork. */
.auth-image-pane {
    flex: 1 1 50%;
    min-width: 0;
    padding: 12px 12px 12px 0;
    display: flex;

    /* Above .auth-shell::before, so the photo covers the ruled paper
       while the padding sliver around it lets the grid show. */
    position: relative;
    z-index: 1;
}

.auth-image-frame {
    flex: 1;
    border-radius: 18px;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: flex-end;
    background: var(--kb-blue);
}

.auth-image-frame::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    background: var(--kb-blue) url('../assets/hero-bg.jpg') center / cover no-repeat;
}

/* Display caption over the photo. */
.auth-frame-caption {
    position: relative;
    z-index: 1;
    padding: 40px 44px;
    font-family: 'Space Grotesk', Inter, sans-serif;
    font-weight: 700;
    text-transform: uppercase;
    font-size: clamp(2rem, 3.6vw, 4rem);
    line-height: 0.94;
    letter-spacing: -0.02em;
    color: #fff;
    text-shadow: 0 2px 28px rgb(6 14 70 / 22%);
}

.auth-frame-caption span {
    display: block;
}

/* ── Entrance animation (landing hero's fade-up) ────────────────────── */
.wl-fade-up {
    opacity: 0;
    transform: translateY(10px);
    animation: wlFadeUp 0.6s ease forwards;
}

.wl-fade-d1 {
    animation-delay: 0.1s;
}

.wl-fade-d2 {
    animation-delay: 0.2s;
}

.wl-fade-d3 {
    animation-delay: 0.35s;
}

.wl-fade-d4 {
    animation-delay: 0.5s;
}

@keyframes wlFadeUp {
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* The fade-up parks its elements at opacity 0 and relies on the animation
   to bring them back, so cancelling the animation is not enough - the
   resting state has to be the visible one. */
@media (prefers-reduced-motion: reduce) {
    .wl-fade-up {
        opacity: 1;
        transform: none;
        animation: none;
    }
}

/* ── Ink-splat hover ────────────────────────────────────────────────
   Copy of css/main.css:1342-1386 (auth pages don't load the 32 KB
   main.css) - keep in sync. Driver: js/inline/index-inline-5.js. */
.ink-btn {
    position: relative;
    overflow: hidden;
    z-index: 0;
    transition:
        color 0.35s ease,
        border-color 0.35s ease;
}

.ink-btn .ink-blob {
    position: absolute;
    border-radius: 50%;
    background: #0a0a0a;
    opacity: 0;
    transform: translate(-50%, -50%) scale(0);
    pointer-events: none;
    z-index: -1;
}

.ink-btn.ink-active {
    color: white;
    border-color: #0a0a0a;
}

.ink-blob.ink-animate {
    animation: inkSplat var(--ink-duration) var(--ink-ease) var(--ink-delay) forwards;
}

@keyframes inkSplat {
    0% {
        transform: translate(-50%, -50%) scale(0);
        opacity: 0.85;
    }

    40% {
        transform: translate(-50%, -50%) scale(0.7);
        opacity: 1;
    }

    100% {
        transform: translate(-50%, -50%) scale(1);
        opacity: 1;
    }
}

/* The blob rests at opacity 0, so stopping the animation is all that is
   needed here. Declared after .ink-blob.ink-animate, which it ties with
   on specificity. */
@media (prefers-reduced-motion: reduce) {
    .ink-blob.ink-animate {
        animation: none;
    }

    .ink-btn,
    .password-requirements li,
    .password-requirements li::before {
        transition: none;
    }
}

/* ── Mobile ─────────────────────────────────────────────────────────
   Photo pane hidden; the gridline form pane goes full width. */
@media (width <= 767px) {
    .auth-image-pane {
        display: none;
    }

    .auth-form-pane {
        flex-basis: 100%;
    }

    .auth-form-scroll {
        padding: 56px 20px 32px;
    }
}
