/* ==========================================================================
   auth.css — the ONE stylesheet for every full-screen authentication page:

       /login          (login.ejs)          /forgot-password (forgot-password.ejs)
       /client-login   (client-login.ejs)
       /reset-password (reset-password.ejs)
       /complete-google-registration        (complete-google-registration.ejs)

   WHY THIS FILE EXISTS
   Every one of those pages used to carry its own inline <style> copy of this
   same design — the backdrop, the header block, the card, the inputs, the
   buttons. They were copies, so they drifted: the fold-fitting work (100dvh
   instead of 100vh, the width AND height density breakpoints, the trimmed
   wrapper padding) was done on login.ejs only, and every other auth page kept
   the old 100vh / clamp(1.5rem, 9vh, 4rem) / padding-bottom: 5rem layout that
   overflows a phone viewport — which is exactly why those pages scrolled while
   /login did not, and why they slowly stopped looking like each other.

   So: the shared system lives here and nowhere else. A page's own <style>
   block is only for what is genuinely unique to that page (the login page's
   reCAPTCHA scaling and offline mode, the registration progress chips, and so
   on). Anything edited here changes every auth page at once — which is the
   point.

   THE ONE RULE FOR THIS FILE: these pages must fit the fold without scrolling,
   on a phone and on a short desktop window alike. The breakpoints at the
   bottom are a density ladder, not decoration — each step trims padding and
   type, and deliberately never trims the input/button tap heights or the 16px
   input font (below 16px, iOS Safari auto-zooms the whole page on focus).

   Every page using this file provides the backdrop photo as an inline custom
   property, since the URL comes from the server:
       <div class="bg-blurred" style="--auth-hero-image: url('<%- heroImageUrl %>')"></div>
   ========================================================================== */

/* ==========================================================================
   TYPE SYSTEM
   Every size, weight and tracking on these five pages resolves to something in
   this block. It is here because the login page's type was the loudest thing
   wrong with it, and none of it was a matter of taste:

   - The subtitle was set in italic on a family loaded WITHOUT an italic face,
     so the browser drew a faux oblique: the upright letters mechanically
     sheared over. That is the "cheap" look — a real italic is a differently
     drawn alphabet, not a slanted one. views/layout/header.ejs now requests
     the italic axis, so `font-style: italic` gets the real face.

   - The headline was set in the same sans as the form under it, so the page
     had no typographic hierarchy: one family, one texture, everything
     competing. The institutional headline now uses the serif the landing page
     already uses for its section titles, which both creates that hierarchy
     and makes /login and / read as the same product.

   - The sizes were 13 unrelated values (0.68, 0.7, 0.72, 0.74, 0.75, 0.76,
     0.8, 0.82, 0.85, 0.88, 0.9, 0.95, 1.05rem) with no relationship between
     any two of them, which is what "inconsistent" actually looks like on a
     screen. They are now a scale, and a size that isn't on it is a bug.

   - Tracking was set in PIXELS (0.2px, 0.3px, 0.4px, 0.5px, 1.2px, 2.2px).
     A fixed pixel offset is a different amount of tracking at every step of
     the density ladder below, so the gold badge's letters spread further
     apart the smaller the phone got. Tracking is in `em` here, so it is the
     same proportion at every size.

   NOTE on the ladder at the bottom of this file: its font sizes are
   deliberately NOT tokens. Those are fold-fitting measurements — each one was
   arrived at by checking that the card still fits a specific viewport without
   scrolling — so they are steps DOWN from this scale, not part of it. Change
   one and re-check the fold; don't "tidy" it onto a token.
   ========================================================================== */
:root {
    --font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
    --font-ui: 'Plus Jakarta Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

    /* Seven steps. Small because these cards are small: the top of it is the
       headline and the bottom is a field hint, with everything else in
       between — there is nothing on an auth page that wants 2rem type. The
       gap between --fs-lg and --fs-2xl is deliberate; nothing on these pages
       sits between the card's title and the institutional headline, and
       leaving the step out is what stops something drifting into it. */
    --fs-2xs: 0.7rem;     /* 11.2px — field labels, badges, the OR rule */
    --fs-xs: 0.75rem;     /* 12px   — hints, secondary links */
    --fs-sm: 0.8125rem;   /* 13px   — notices, step copy */
    --fs-base: 0.875rem;  /* 14px   — input text, subtitle */
    --fs-md: 0.9375rem;   /* 15px   — buttons */
    --fs-lg: 1.0625rem;   /* 17px   — the card's own title */
    --fs-2xl: 1.5rem;     /* 24px   — the institutional headline */

    /* The hero block's own three sizes, held apart from the scale above.
       The headline, the subtitle under it and the gold portal badge were read
       as too small on the real page, and they are the only type on these pages
       whose job is to be read at a glance from arm's length rather than while
       filling a field in. Pulling them out of --fs-2xl / --fs-base / --fs-2xs
       means the hero can be set larger WITHOUT dragging the input text, the
       buttons and the field labels up with it — the 16px input size in
       particular is deliberate (it is what stops iOS zooming the page on
       focus) and must not move. Every density tier below overrides these three
       selectors directly, and each of those values went up by the same ~20%,
       so the hero is uniformly larger on every device rather than only on the
       widths that happen to fall back to this block. The extra height is paid
       for out of .login-wrapper::before and .auth-gap, the two shrinkable
       spacers, so the fold rule still holds with nothing clipped. */
    --fs-hero-title: 1.8rem;  /* 28.8px — the institutional headline */
    --fs-hero-sub:   1.05rem; /* 16.8px — the survey subtitle */
    --fs-hero-badge: 0.85rem; /* 13.6px — the gold portal badge */

    /* Tracking, in em so it holds at every size (see the note above). */
    --tr-tight: -0.01em;  /* large display type — closes up the word shape */
    --tr-wide: 0.04em;    /* small sentence-case UI text */
    --tr-caps: 0.09em;    /* ALL CAPS labels, which need the air to be legible */
    --tr-badge: 0.16em;   /* the gold portal badge — caps with room around it */
}

/* ---------- Reset / page shell ---------- */
html, body {
    height: 100vh;
    /* dvh follows the phone browser's collapsing address bar. With plain 100vh
       the box is sized to the LARGEST viewport (bar hidden), so while the bar
       is showing the page is taller than the visible area — which is what made
       these pages scroll on a phone even though nothing here is meant to. */
    height: 100dvh;
    margin: 0;
    padding: 0;
    overflow: hidden;
    font-family: var(--font-ui);
    font-size: var(--fs-base);
    line-height: 1.5;
    /* Both smoothing hints, not just the WebKit one: without the Firefox/macOS
       counterpart the same page renders noticeably heavier there, which is one
       of the ways "the font looks off" happens without anyone changing a font.
       optical-sizing lets Playfair use its display cut at the headline size. */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-optical-sizing: auto;
    text-rendering: optimizeLegibility;
}

/* ---------- 1. Backdrop ----------
   Same photo + treatment as the public landing page (index.ejs .bg-image):
   full-bleed, no blur, so every auth page reads as one consistent backdrop. */
.bg-blurred {
    position: fixed;
    top: 0; left: 0;
    width: 100vw;
    height: 100vh;
    height: 100dvh;
    background-image: var(--auth-hero-image);
    background-size: cover;
    /* The CSPC monument (seal/torch/globe) sits in the upper half of this
       photo. On a tall mobile viewport "cover" scales the image up enough that
       centering it vertically cropped the monument almost entirely out of
       frame, leaving mostly sky and pavement — anchoring to the top keeps it
       in view. */
    background-position: top center;
    background-repeat: no-repeat;
    z-index: -2;
}

/* ---------- 2. Overlay ----------
   The same navy gradient as the landing page's .bg-overlay, rather than a flat
   tint, so the two read as one site. */
.bg-overlay {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: linear-gradient(180deg, rgba(37, 29, 58, 0.92) 0%, rgba(37, 29, 58, 0.85) 100%);
    z-index: -1;
}

/* ---------- 3. Header-anchored wrapper ----------
   The header sits at a FIXED offset from the top of the viewport — it is not
   vertically centred as one block with the card below it. Centring the whole
   [header + card] group made the header land at a different height on every
   auth page depending on how tall that page's card happened to be (login's
   card with its reCAPTCHA vs. forgot-password's short one), so the heading
   jumped as you moved between them. Pinning it is what keeps it in the exact
   same spot across all six pages. */
.login-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    height: 100dvh;
    max-height: 100dvh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    padding: 1rem;
    /* Was clamp(1.5rem, 9vh, 4rem) — on a phone the 9vh term alone burned
       ~70px of the fold before the title even started, and together with the
       5rem bottom reserve it pushed the card past the bottom edge, which is
       what forced these pages to scroll. Both ends pulled in. */
    padding-top: clamp(0.5rem, 1.8vh, 1.25rem);
    /* Clearance for the floating home button, and NOTHING more. The button is
       position: fixed, so it is not in this column and cannot push the card off
       it — this reserve is the only thing keeping the two apart, which is why it
       is derived from the button's own measurements rather than typed in as a
       round number beside them. It used to be: every tier had a hand-picked
       padding-bottom that had drifted away from the button size in the same
       tier, all four of them short (this one by 16px, the three short-desktop
       tiers by 5-8px each), so at 1440x820 the card finished 5px BELOW the top
       of the button and sat on it. --auth-foot is now set wherever .btn-home is
       resized, right next to it, and it is always height + offset + a little
       air. */
    --auth-foot: calc(3rem + 1.25rem + 0.35rem);
    padding-bottom: calc(var(--auth-foot) + env(safe-area-inset-bottom));
    z-index: 1;
    /* Safety fallback ONLY: on a normal phone portrait viewport the content
       now fits and this never engages. It stays `auto` (not `hidden`) so a
       genuinely short viewport — a landscape phone, a desktop window dragged
       short — can still reach the submit button rather than have it clipped
       off with no way back. */
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* ---------- The drop ----------
   How far below the top of the viewport the HEADER starts — and, since the
   card now centres itself in what is left (see .card-custom's auto margins),
   that is ALL it decides. It is a header offset, not a column offset.

   That split is why every tier below got smaller. While the column was packed
   flex-start, this one number also set where the card came to rest, so it was
   being paid to solve the wrong problem: the card finishing high on a roomy
   screen was fixed by dropping the whole column ~9vh, which cured the card and
   bought a slab of empty backdrop above the title — about 85px of it on the
   412x915 phone this was reported on, before the title's first line. The card
   holds its own middle now, so the drop is free to be what the header actually
   wants: a small, steady margin that keeps the title clear of the status bar
   and no larger.

   It is a SHRINKABLE SPACER, not padding, and that distinction is the whole
   design. .header-container and .card-custom are both flex-shrink: 0, so this
   pseudo-element is the only thing in the column that can give: the moment the
   content would overflow the fold, the browser takes the space back from here
   first, down to zero, before anything else is touched. So a generous drop on a
   roomy screen cannot push the submit button off a cramped one — the file's one
   rule still holds at every size, with no media query needed to enforce it.
   The tiers below only set --auth-drop; they never have to prove it fits. */
.login-wrapper::before {
    content: "";
    display: block;
    width: 0;
    flex: 0 1 auto;
    /* max() of the two, and deliberately NOT min-height. min-height is a floor
       the flex algorithm may not cross, so expressing the offline chip's
       clearance that way reintroduced the very fault it was meant to replace:
       on a 1366x650 window the column was pinned 46px down with no room to pin
       it, and the page scrolled. Folded into the height, the clearance is a
       preference like the drop itself and the spacer still collapses to zero
       under real pressure. */
    height: max(var(--auth-drop, 0px), var(--auth-drop-min, 0px));
    min-height: 0;
}

/* ---------- The gap ----------
   The space between the header block and the top of the card — and the second
   half of the same split as the drop above. The drop decides where the header
   starts; this decides how far under it the card hangs. Neither one is allowed
   to answer the other's question any more.

   It is a spacer that GROWS but cannot grow past a cap, and both halves of that
   matter:

   - It grows, so on a page with space to spare the card is not tucked up under
     the heading with all the emptiness dumped below it. With free space to
     take, this is the only item in the column that grows (.login-wrapper::before
     is flex-grow: 0), so it takes the lot — the same generous gap the card used
     to get from an auto margin, and on /login, which has little to spare, it
     lands in the same place it did.

   - It cannot pass --auth-gap-max, so a page whose card is SHORT cannot push
     that card down into the middle of the screen. Past the cap the leftover
     space stops being a gap, stays at the foot of the column where
     justify-content: flex-start leaves it, and the card holds its distance from
     the heading. That is what makes /forgot-password, /reset-password and
     /login sit at the same height instead of each finding its own — the gap is
     now a property of the DESIGN rather than of the card's contents.

   flex-shrink: 1 with min-height: 0, so on a viewport with nothing to spare it
   collapses to nothing like the drop does, before either fixed-size box is
   touched. The file's one rule is unaffected: this can only ever consume space
   that was already free. */
.login-wrapper .auth-gap {
    flex: 1 1 auto;
    width: 0;
    min-height: 0;
    /* The floor is .header-container's own margin-bottom, set per tier; this is
       purely the ceiling on top of it. */
    max-height: var(--auth-gap-max, 3.5rem);
}

/* Tall desktop / large tablet — the roomiest case, and the one where the old
   9vh drop was most visible as dead sky above the title. The cap matters more
   than the vh term here: on a 1440x900 window 9vh was 81px of nothing, and the
   card was going to centre itself below the header regardless. */
.login-wrapper {
    --auth-drop: clamp(0.75rem, 2.5vh, 2rem);
    /* Roomy screens can afford a real gap; the vh term keeps it proportional so
       it does not read as a hairline on a 1440-tall monitor. */
    --auth-gap-max: clamp(1.5rem, 4vh, 3.5rem);
}

/* The chip is a pop-up and reserves no space, so the column keeps its resting
   position and only has to get out of the way while the chip is actually up.
   It asks the spacer for a MINIMUM rather than adding to it, which is the
   difference between this and the hard padding-top it replaces: that one shifted
   the column down by its full 3.1rem even where there was no room to give, and
   on a tall screen it stacked on top of the drop and pushed the card down twice
   over. Asked as a minimum it does neither — where the drop is already bigger,
   nothing moves at all, and where nothing fits, it gives way like everything
   else here. */
.login-wrapper:has(.offline-pill.is-visible) {
    --auth-drop-min: calc(2.9rem + env(safe-area-inset-top, 0px));
}

/* ---------- Entrance animation ---------- */
@keyframes fadeSlideUp {
    0%   { opacity: 0; transform: translateY(30px); }
    100% { opacity: 1; transform: translateY(0); }
}

/* ---------- Hero text block ----------
   margin-bottom is THE gap between the hero and the card, and every tier of
   the density ladder sets its own. It was tuned when the card was a plain
   rounded box; with the travelling border lit around it the card needs to sit
   clear of the header rather than tucked right under it, so each tier's value
   went up. Where there was room, the increase was taken back out of
   .login-wrapper's top padding in the same tier — the column starts a little
   higher and the gap gets what it gave up, so the fold budget is unchanged. On
   the shortest viewports there was nothing to take, and those tiers get a
   deliberately smaller bump. */
.header-container {
    text-align: center;
    margin-bottom: 1.75rem;
    max-width: 800px;
    flex-shrink: 0;
}

.main-title {
    color: #ffffff;
    /* The serif, at 700 rather than 800: Playfair's heaviest weight has very
       thin hairlines next to very thick stems, and over a photograph that
       contrast breaks up at small sizes. 700 keeps the stroke solid. */
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--fs-hero-title);
    text-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
    margin-bottom: 0.35rem;
    /* Negative, where it used to be +0.3px. A display serif is drawn with its
       own spacing already built in; adding more pulls the words apart and the
       line stops reading as one title. */
    letter-spacing: var(--tr-tight);
    line-height: 1.2;
    text-wrap: balance;
    opacity: 0;
    animation: fadeSlideUp 0.8s ease-out forwards;
}

.sub-title {
    color: #e2e8f0;
    /* Stays the sans, so it is a clear second voice under the serif headline
       rather than more of the same. The italic is now a REAL italic — the
       family is loaded with its italic axis (see views/layout/header.ejs), so
       this no longer renders as the upright face sheared over. */
    font-family: var(--font-ui);
    font-weight: 500;
    font-style: italic;
    font-size: var(--fs-hero-sub);
    letter-spacing: var(--tr-wide);
    line-height: 1.45;
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
    margin-bottom: 0.6rem;
    text-wrap: balance;
    opacity: 0;
    animation: fadeSlideUp 0.8s ease-out 0.3s forwards;
}

/* The gold pill that names the portal ("Login Portal", "Office Staff Login",
   "Forgot Password", …). Same badge on every page; only the words differ. */
.portal-text {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: #ffffff;
    font-family: var(--font-ui);
    font-weight: 700;
    font-size: var(--fs-hero-badge);
    line-height: 1;
    text-transform: uppercase;
    /* em, not the old fixed 2.2px: at the 0.6rem this badge shrinks to on the
       tightest tier, 2.2px was a quarter of the letter height and the word
       came apart into separate characters. */
    letter-spacing: var(--tr-badge);
    /* The trailing letter's tracking is drawn AFTER it, so a centred all-caps
       run with wide tracking always sits a little left of centre. Pulling the
       right padding in by one tracking unit puts it back. */
    padding: 0.3rem 0.9rem;
    padding-right: calc(0.9rem - var(--tr-badge));
    background: rgba(255, 204, 0, 0.35);
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 50px;
    backdrop-filter: blur(6px);
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
    margin-bottom: 0;
    opacity: 0;
    animation: fadeSlideUp 0.8s ease-out 0.6s forwards;
}

/* ---------- 4. Card ---------- */
.card-custom {
    /* The travelling border (see below) hangs off this box, so the card has to
       be a positioning context and has to publish its own corner radius — the
       ring sits 2px outside the card and needs to round by exactly that much
       more, at every breakpoint, or the colour cuts across the corners. */
    position: relative;
    --auth-card-radius: 1.75rem;
    background: rgba(255, 255, 255, 0.94);
    backdrop-filter: blur(18px) saturate(160%);
    -webkit-backdrop-filter: blur(18px) saturate(160%);
    border: 1px solid rgba(255, 255, 255, 0.6);
    border-radius: var(--auth-card-radius);
    box-shadow: 0 30px 60px -15px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
    /* Widened from a fixed 400px: the reCAPTCHA widget alone is 304px, so at
       400px minus the card's own side padding the form sat in a ~310px column
       and the helper text wrapped onto extra lines — height these pages can't
       spare on a short desktop window. min() keeps it from ever exceeding a
       narrow viewport. */
    width: min(480px, 92vw);
    height: auto;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 1.5rem 2.25rem;
    /* public/css/style.css gives every .card-custom a 1.5rem bottom margin —
       right for the stacked cards on the dashboard pages it was written for,
       dead space counted against the fold here. No auth page links that file
       any more, but this still overrides it if one ever does.

       The card takes NO auto margin. It briefly did, to centre itself in the
       space below the header, and that is the thing this replaces: auto margins
       split ALL the free space in half, so the gap above the card was a function
       of how tall that page's card happened to be. On /login, whose card carries
       a reCAPTCHA and leaves ~95px spare, half of it was a ~55px gap and the
       page looked right. On /forgot-password, whose card is barely half as tall
       and leaves ~330px spare, half of it was a ~165px gap — the heading at the
       top of the screen, the card stranded near the middle, and a visible band
       of empty backdrop between the two that belonged to neither.

       The gap is .auth-gap now, and it is CAPPED. See it below. */
    margin-bottom: 0;
    opacity: 0;
    animation: fadeSlideUp 0.7s ease-out 0.15s forwards;
}

/* ---------- Travelling border ----------
   A band of light that runs continuously around the edge of the card, on every
   auth page and every device. It is built from ONE conic gradient rotated by an
   animated angle and then masked down to a ring, so what moves is the colour,
   not the card: nothing is laid out or resized on any frame.

   Two layers, both driven by the same --auth-ring-angle so they can never drift
   apart:
     1. the halo (::after) — a wider, blurred, dimmer copy that spills light
        onto the dark backdrop. This is what makes the border READ from across
        the room instead of being a hairline you have to look for.
     2. the edge (::before) — the crisp 3px ring itself, sitting on top.

   The gradient keeps a constant indigo tint all the way round and carries TWO
   bright sweeps (white core into gold) 180deg apart. The tint means the card
   always has a deliberate lit frame rather than a plain border with something
   occasionally passing over it; two sweeps mean one is almost always on a side
   you are looking at, which is what sells the movement.

   Two things make it degrade instead of break:
   - @supports: the mask is what turns the gradient into a RING. Without
     mask-composite the same element would be a solid colour rectangle over the
     whole card, so where it is unsupported neither layer is created at all and
     the card keeps its plain border.
   - @property: only the ANIMATION needs it (a custom property is otherwise
     interpolated as a string, i.e. it jumps rather than sweeps). A browser
     without it still paints the full lit frame, just without the travel.

   Both layers are pointer-events: none and live at the very edge of the card,
   so neither can ever take a tap meant for the field underneath. */
@property --auth-ring-angle {
    syntax: "<angle>";
    inherits: false;
    initial-value: 0deg;
}

@keyframes authRingSpin {
    to { --auth-ring-angle: 360deg; }
}

@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
    .card-custom {
        /* The spin runs HERE, on the card, not on the two pseudo-elements.
           --auth-ring-paint below is substituted at computed-value time on this
           element, so the angle it reads is this element's: an animation on
           ::before would advance that pseudo's own copy of the angle and never
           reach the gradient the pseudos actually inherit, leaving the ring
           frozen. Driving it from the card also means the two layers cannot
           drift apart — there is only one clock. The entrance animation is
           restated because this is a shorthand and would otherwise drop it. */
        animation:
            fadeSlideUp 0.7s ease-out 0.15s forwards,
            authRingSpin 5s linear 0.85s infinite;
        --auth-ring-width: 3px;
        --auth-ring-glow: 9px;
        /* One gradient, used by both layers. The stops are spaced so each
           sweep ramps up over ~30deg and falls away over ~45deg — a hard edge
           reads as a spinning object, a long tail reads as light. */
        --auth-ring-paint: conic-gradient(from var(--auth-ring-angle),
            rgba(129, 140, 248, 0.28) 0deg,
            rgba(129, 140, 248, 0.28) 20deg,
            rgba(167, 139, 250, 0.95) 52deg,
            #ffffff 70deg,
            #ffd84d 88deg,
            rgba(129, 140, 248, 0.28) 135deg,
            rgba(129, 140, 248, 0.28) 200deg,
            rgba(167, 139, 250, 0.95) 232deg,
            #ffffff 250deg,
            #ffd84d 268deg,
            rgba(129, 140, 248, 0.28) 315deg,
            rgba(129, 140, 248, 0.28) 360deg);
    }

    .card-custom::before,
    .card-custom::after {
        content: "";
        position: absolute;
        pointer-events: none;
        background: var(--auth-ring-paint);
        /* Paint the padding box, knock out the content box: what is left is
           the frame between them. */
        -webkit-mask:
            linear-gradient(#000 0 0) content-box,
            linear-gradient(#000 0 0);
        -webkit-mask-composite: xor;
        mask:
            linear-gradient(#000 0 0) content-box,
            linear-gradient(#000 0 0);
        mask-composite: exclude;
    }

    /* The halo. Blurred and held at 0.75, so on the dark backdrop it is a glow
       and on the card's own white it is nothing — which is why it can spill
       inwards over the corner without touching legibility. Deliberately the
       only blurred thing here: it is one small composited layer around a card
       that is already running a backdrop-filter, and widening it is the first
       thing to reconsider if a low-end phone ever struggles. */
    .card-custom::after {
        inset: calc(-1 * var(--auth-ring-glow));
        z-index: 1;
        border-radius: calc(var(--auth-card-radius) + var(--auth-ring-glow));
        padding: var(--auth-ring-glow);
        filter: blur(7px);
        opacity: 0.75;
    }

    /* The edge itself. */
    .card-custom::before {
        inset: calc(-1 * var(--auth-ring-width));
        z-index: 2;
        border-radius: calc(var(--auth-card-radius) + var(--auth-ring-width));
        padding: var(--auth-ring-width);
    }

    /* Phones get the same treatment — this is the one element on the page that
       is supposed to look expensive, and hiding it on the device most people
       will actually sign in from would defeat the point. Only the numbers come
       down, in proportion with the card's smaller corner radius, so a 3px ring
       on a 480px-wide card does not read as a 3px ring on a 360px one. */
    @media (max-width: 480px) {
        .card-custom {
            --auth-ring-width: 2.5px;
            --auth-ring-glow: 7px;
        }
        .card-custom::after { filter: blur(6px); opacity: 0.8; }
    }

    /* Reduced motion: the lit frame stays, the travel stops. The border still
       reads as a deliberate part of the design, it just holds still. The card
       keeps its entrance — that one is a single short movement, not a loop. */
    @media (prefers-reduced-motion: reduce) {
        .card-custom { animation: fadeSlideUp 0.7s ease-out 0.15s forwards; }
    }
}

.card-custom .cqa-logo {
    width: 46px;
    height: auto;
}

.card-custom h4 {
    color: #0f172a;
    /* The card is the working part of the page, so its title is the sans — the
       serif stops at the headline above. */
    font-family: var(--font-ui);
    font-size: var(--fs-lg);
    font-weight: 700;
    letter-spacing: var(--tr-tight);
    line-height: 1.3;
}

.card-custom .divider-soft {
    width: 36px;
    height: 3px;
    background: #ffcc00;
    border-radius: 50px;
    margin: 0.4rem auto 0.55rem;
}

/* ---------- Form fields ---------- */
.field-group {
    margin-bottom: 0.75rem;
}

.field-label {
    display: block;
    font-size: var(--fs-2xs);
    font-weight: 700;
    /* Caps tracking. All-caps text has no ascenders or descenders to tell one
       word from the next, so it needs measurably more air than the 0.4px it
       had — at this size that was very nearly none. */
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: #64748b;
    margin-bottom: 0.3rem;
}

.input-modern {
    position: relative;
    display: flex;
    align-items: center;
    background: #f8fafc;
    border: 1.5px solid #e2e8f0;
    border-radius: 0.75rem;
    transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}

.input-modern:focus-within {
    border-color: #251d3a;
    background: #ffffff;
    box-shadow: 0 0 0 0.2rem rgba(37, 29, 58, 0.15);
}

.input-modern .input-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.6rem;
    flex-shrink: 0;
    color: #94a3b8;
    font-size: var(--fs-md);
    transition: color 0.2s ease;
}

.input-modern:focus-within .input-icon {
    color: #251d3a;
}

.input-modern .form-control {
    border: none;
    background: transparent;
    padding: 0.55rem 0.85rem 0.55rem 0;
    font-family: var(--font-ui);
    font-size: var(--fs-base);
    font-weight: 500;
    box-shadow: none !important;
}

.input-modern .form-control:focus {
    outline: none;
    box-shadow: none;
}

/* A field the server filled in and the user cannot change (the Google name and
   email on the registration page). Reads as "this is already settled", not as
   a broken input. */
.input-modern .form-control[readonly] {
    color: #475569;
    cursor: default;
}

.field-hint {
    font-size: var(--fs-xs);
    font-weight: 500;
    color: #94a3b8;
    margin-top: 0.3rem;
    line-height: 1.4;
}

.forgot-link {
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: var(--tr-wide);
    color: #251d3a;
    text-decoration: none;
    transition: color 0.2s ease;
}

.forgot-link:hover {
    color: #3730a3;
    text-decoration: underline;
}

/* ---------- Buttons ---------- */
.btn-login {
    /* These three exist because .btn-login is NOT one of Bootstrap's own
       variants (.btn-primary and friends), and Bootstrap's disabled rule is
       written against variables that only those variants define:

           .btn.disabled, .btn:disabled { color: var(--bs-btn-disabled-color);
                                          background-color: var(--bs-btn-disabled-bg); }

       With the variables unset those declarations are invalid at
       computed-value time, so `color` fell back to INHERIT — the card's dark
       body text — and painted it over this button's dark navy gradient at 65%
       opacity. The result was a plain grey bar with no readable label, and
       that is the state every single visitor sees on arrival, because the
       submit button starts disabled until the reCAPTCHA is ticked. The rule
       that beat `color: #ffffff` below is `.btn:disabled` (two classes to one),
       which is why the fix is to give Bootstrap the values it is asking for
       rather than to add another colour declaration it would also outrank. */
    --bs-btn-disabled-color: #ffffff;
    --bs-btn-disabled-bg: #251d3a;
    --bs-btn-disabled-border-color: transparent;

    background: linear-gradient(135deg, #251d3a, #2a2550);
    border: none;
    border-radius: 0.75rem;
    padding: 0.6rem 1rem;
    font-family: var(--font-ui);
    font-weight: 700;
    font-size: var(--fs-md);
    /* Both buttons carry the same tracking. They used to differ (0.2px here,
       none on the Google button) for no reason, and two buttons stacked
       directly on top of each other is exactly where that shows. */
    letter-spacing: var(--tr-wide);
    color: #ffffff;
    box-shadow: 0 8px 18px -6px rgba(37, 29, 58, 0.55);
    transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
}

.btn-login:hover {
    filter: brightness(1.06);
    transform: translateY(-1px);
    box-shadow: 0 12px 22px -6px rgba(37, 29, 58, 0.6);
    color: #ffffff;
}

.btn-login:active { transform: translateY(0); }
.btn-login:focus-visible { outline: 2px solid #ffcc00; outline-offset: 2px; }

.btn-google {
    /* Same reason as .btn-login above — Bootstrap's disabled rule needs these
       defined or it resolves `color` to inherit. */
    --bs-btn-disabled-color: #334155;
    --bs-btn-disabled-bg: #ffffff;
    --bs-btn-disabled-border-color: #e2e8f0;

    background-color: #ffffff;
    color: #334155;
    border: 1.5px solid #e2e8f0;
    border-radius: 0.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    font-family: var(--font-ui);
    font-weight: 600;
    font-size: var(--fs-md);
    letter-spacing: var(--tr-wide);
    padding: 0.6rem 1rem;
    transition: all 0.25s ease;
}

.btn-google:hover {
    background-color: #f8fafc;
    border-color: #251d3a;
    box-shadow: 0 6px 16px -4px rgba(37, 29, 58, 0.25);
    transform: translateY(-1px);
}

.btn-google:active { transform: translateY(0); }
.btn-google:focus-visible { outline: 2px solid #ffcc00; outline-offset: 2px; }
.btn-google.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }

.btn-google img {
    width: 18px;
    height: 18px;
}

/* ---------- OR divider ---------- */
.divider-or {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 0.9rem 0;
}

.divider-or hr {
    flex: 1;
    margin: 0;
    border-top: 1px solid #e2e8f0;
    opacity: 1;
}

.divider-or span {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--tr-caps);
    color: #94a3b8;
}

.text-xsmall {
    font-size: var(--fs-xs);
    line-height: 1.45;
}

/* ---------- Home / back button ----------
   A floating circular icon button centred at the bottom of the viewport, so it
   reads as a proper mobile FAB rather than a text pill competing with the
   header above it. */
.btn-home {
    position: fixed;
    bottom: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10;
    width: 3rem;
    height: 3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.12);
    color: #ffffff;
    border: 1px solid rgba(255, 255, 255, 0.4);
    border-radius: 50%;
    font-size: var(--fs-lg);
    backdrop-filter: blur(6px);
    box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.45);
    transition: all 0.3s ease;
    text-decoration: none;
}

.btn-home:hover {
    background: rgba(255, 255, 255, 0.25);
    color: #ffffff;
    transform: translateX(-50%) translateY(-2px);
}

.btn-home:focus-visible { outline: 2px solid #ffcc00; outline-offset: 2px; }

/* ---------- "Viewing Offline" chip ----------
   Exactly the shared component from /css/offline-pill.css — fixed, centred, at
   the top of the screen, popping up OVER the page rather than sitting in the
   column. These pages have no header bar for it to tuck under, so it goes to
   the very top, above the hero header.

   It is a pop-up, so it reserves no space and the header keeps its high resting
   position online. The one thing it must not do is land on the hero title, so
   while it is actually showing the column starts below it — :has() keys that off
   the same .is-visible class the connectivity listener already toggles, with no
   extra class for a page's script to remember to set. */
:root { --offline-pill-top: 0.5rem; }

/* The clearance itself is the spacer's min-height, next to the drop it has to
   cooperate with — see .login-wrapper::before near the top of this file. */

/* ---------- In-card notices ----------
   One notice design for every auth page. The legacy per-page class names are
   kept as aliases on purpose: the JS on those pages toggles them by name
   (setNotice(), the pending-logout guard), so renaming the markup would mean
   touching working script for a purely visual change.
     amber (default) = "hang tight / heads up"
     red   (.is-error) = "something is actually wrong"
     green (.sent-notice, .verified-notice, .is-success) = "that worked" */
.auth-notice,
.offline-login-notice,
.pending-logout-notice,
.sent-notice,
.verified-notice {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: #fef3c7;
    color: #92400e;
    border: 1px solid #fde68a;
    border-radius: 10px;
    padding: 0.65rem 0.85rem;
    font-size: var(--fs-sm);
    font-weight: 600;
    letter-spacing: var(--tr-wide);
    line-height: 1.45;
    margin-bottom: 0.85rem;
}

.auth-notice.is-error,
.offline-login-notice.is-error,
.pending-logout-notice.is-error {
    background: #fee2e2;
    color: #991b1b;
    border-color: #fecaca;
}

.auth-notice.is-success,
.sent-notice,
.verified-notice {
    background: #ecfdf5;
    color: #047857;
    border-color: #a7f3d0;
    align-items: flex-start;
}

.auth-notice span,
.offline-login-notice span,
.pending-logout-notice span,
.sent-notice span,
.verified-notice span {
    flex: 1 1 auto;
}

.sent-notice i,
.verified-notice i {
    margin-top: 2px;
    flex: none;
}

.notice-retry-btn {
    flex: 0 0 auto;
    background: rgba(255, 255, 255, 0.55);
    border: 1px solid currentColor;
    color: inherit;
    border-radius: 50px;
    font-family: var(--font-ui);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: var(--tr-wide);
    padding: 0.25rem 0.7rem;
    min-height: 30px;
}

.notice-retry-btn:hover { background: rgba(255, 255, 255, 0.85); }

/* ---------- Numbered step list ----------
   Used by the login page (registration steps, offline instructions). Shared so
   any page that needs to explain a multi-step flow looks the same doing it. */
.auth-steps {
    border: 1px solid #e2e8f0;
    background: #f8fafc;
    border-radius: 12px;
    padding: 0.75rem 0.85rem;
    margin-bottom: 0.85rem;
}

.auth-steps-title {
    margin: 0 0 0.55rem;
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: var(--tr-caps);
    line-height: 1.3;
    color: #64748b;
}

.auth-step { display: flex; align-items: flex-start; gap: 0.6rem; }
.auth-step + .auth-step { margin-top: 0.55rem; }

.auth-step .step-num {
    flex: none;
    width: 22px;
    height: 22px;
    margin-top: 1px;
    border-radius: 50%;
    background: #251d3a;
    color: #ffffff;
    /* Lining, tabular figures: the step numbers sit in a vertical stack of
       identical circles, so 1 and 2 have to occupy the same width or the
       column reads as crooked. */
    font-size: var(--fs-2xs);
    font-weight: 800;
    font-variant-numeric: tabular-nums lining-nums;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

.auth-step p { margin: 0; font-size: var(--fs-sm); line-height: 1.5; color: #334155; }
.auth-step strong { color: #1e293b; font-weight: 700; }

.auth-steps-foot {
    display: flex;
    align-items: flex-start;
    gap: 0.4rem;
    margin: 0.6rem 0 0;
    padding-top: 0.55rem;
    border-top: 1px dashed #e2e8f0;
    font-size: var(--fs-xs);
    line-height: 1.45;
    color: #64748b;
}

/* ==========================================================================
   DENSITY LADDER
   The fold is tuned to fit EXACTLY, so each step below trims padding, gaps and
   type — spread across the hero block, the card header and the field gaps
   rather than taken out of any one place, so nothing collapses visibly. Tap
   heights (41px inputs / 40px buttons) and the 16px input font are deliberately
   NOT touched at any step.
   ========================================================================== */

/* Hero text on small tablets and phones. */
@media (max-width: 600px) {
    .main-title { font-size: 1.68rem; }
    /* 0.8125rem, not the 0.9rem this used to be. Every other line in this
       tier steps DOWN from the desktop size; the subtitle alone stepped up
       (0.85 -> 0.9rem), so on a tablet it was set larger than the size it is
       given on a full desktop window — and larger relative to the headline
       above it than at any other width. */
    .sub-title { font-size: 0.975rem; margin-bottom: 0.5rem; }
    .portal-text { font-size: 0.82rem; min-height: 36px; padding: 0.4rem 0.9rem; }
}

/* Phones.
   NOTE on the breakpoint: a fixed 400px card (plus the wrapper's 16px side
   padding) needs >= 432px of viewport to avoid overflowing — but common phones
   (375/390/393/412/414/428/430px) are ALL narrower than that, and html/body is
   overflow:hidden, so below this the card used to be clipped off-screen at the
   sides instead of shrinking. 480px covers every phone width including the
   larger Pro Max / Android sizes a 400px cutoff missed. */
@media (max-width: 480px) {
    .header-container { margin-bottom: 0.7rem; }
    .main-title { font-size: 1.42rem; margin-bottom: 0.25rem; line-height: 1.2; }
    .sub-title { font-size: 0.96rem; margin-bottom: 0.4rem; line-height: 1.3; }
    .portal-text { font-size: 0.84rem; min-height: 36px; padding: 0.4rem 0.9rem; }

    .card-custom {
        width: calc(100% - 24px);
        margin-inline: 12px;
        height: auto;
        padding-inline: 0.9rem;
        --auth-card-radius: 22px;
    }
    .card-custom .cqa-logo { width: 40px; }
    .card-custom h4 { font-size: 1.15rem; }
    .card-custom p.text-xsmall { font-size: 0.85rem; }
    .card-custom .text-center.mb-2 { margin-bottom: 0.25rem !important; }
    .card-custom .text-center.mb-3 { margin-bottom: 0.5rem !important; }
    .card-custom .cqa-logo.mb-1 { margin-bottom: 0.1rem !important; }
    .card-custom .divider-soft { margin: 0.3rem auto 0.35rem; }

    /* 16px input text avoids iOS Safari's focus auto-zoom outright — kept at
       16px even though it sits slightly above the general 14-15px mobile
       target, since regressing would reintroduce that bug. Vertical padding is
       trimmed instead, so overall input height lands at ~44px. */
    .input-modern .form-control {
        font-size: 16px;
        padding: 0.5rem 0.85rem 0.5rem 0;
        min-height: 41px;
    }
    .input-modern .input-icon { width: 2.6rem; font-size: 1.1rem; }
    .field-group { margin-bottom: 0.5rem; }
    .field-label { font-size: 0.8rem; margin-bottom: 0.25rem; }
    .field-hint { font-size: 0.74rem; margin-top: 0.3rem; line-height: 1.3; }

    .btn-login, .btn-google {
        padding-inline: 1rem;
        font-size: 0.95rem;
        min-height: 40px;
    }

    /* "Forgot Password?" sits inline next to the Password label — a short link
       with almost no hit area beyond its own text height. Padding it turns it
       into a real tap target and keeps a thumb from clipping the label. */
    .forgot-link { padding: 0.35rem 0; margin: -0.35rem 0; font-size: 0.85rem; }
    .field-group .d-flex.justify-content-between { gap: 0.75rem; }

    .divider-or { margin: 0.5rem 0; }

    .btn-home { width: 2.6rem; height: 2.6rem; font-size: 0.95rem; bottom: 0.8rem; }
}

/* The compact step — for a viewport under 900px tall (a laptop screen, a
   desktop window that is not full height, a landscape phone) OR for any
   phone-width screen at all.

   900px, not 780px: a 1280x800 browser window has ~800px of viewport and was
   overflowing by ~78px, because it cleared the old 780px cutoff and got the
   full desktop rhythm it has no room for. Anything with real room to spare —
   a maximised window on a 1080p display or larger — is above 900 and keeps it.

   That second half is the important one and it was missing. This used to be
   keyed on height alone, on the assumption that a tall screen has room to
   spare. A phone does not: a 412x900 handset is 120px too tall to match
   `max-height: 780px`, so it got the roomy desktop rhythm and the login card
   ran straight off the bottom — while the SAME page fitted fine on a shorter
   phone, which is exactly the confusing part. Width and height each catch a
   case the other misses, so the query asks for either.

   Because it now also applies to narrow screens, every declaration in here is
   VERTICAL. Anything that also set a horizontal value (card and button side
   padding) would otherwise override the phone-width block above it and widen
   the card back out on a 360px screen. */
@media (max-height: 900px), (max-width: 480px) {
    .login-wrapper {
        /* As high as the safe area allows — the header was left floating in the
           middle of a large empty band when this reserved room for a top-corner
           button that no longer exists. */
        padding-top: calc(0.3rem + env(safe-area-inset-top, 0px));
        /* 2.5rem button + 0.7rem from the edge + a little air. */
        --auth-foot: calc(2.5rem + 0.7rem + 0.35rem);
        /* The tier a modern handset lands in (412x915, 393x852). This was
           9vh — about 70px on one of those once the browser chrome is out of
           the viewport — which, on top of the padding-top above it, began the
           title a tenth of the way down the screen with nothing in the band.
           The card no longer depends on this to find its resting place, so what
           is left is just the title's own margin from the status bar. */
        --auth-drop: clamp(0.4rem, 2vh, 1.5rem);
        /* 1.75rem = 28px, which with .header-container's own 0.7rem below it
           puts ~39px between the badge and the top of the card on a 412x800
           handset.

           The cap was first set at 3rem, to reproduce what /login's gap
           happened to measure when the card still centred itself. That was the
           wrong thing to aim at: it was never a chosen value, just whatever
           half of /login's leftover space came to, and once every page was
           pinned to it the card read as sitting low — the heading at the top of
           the screen with a band of backdrop under it, rather than the two
           reading as one block. Tightened to a value picked for how it looks
           instead of inherited from what the old layout produced. */
        --auth-gap-max: clamp(1rem, 3.5vh, 1.75rem);
    }
    .header-container { margin-bottom: 0.55rem; }
    .main-title { font-size: 1.3rem; margin-bottom: 0.2rem; }
    .sub-title { font-size: 0.91rem; margin-bottom: 0.3rem; }
    .portal-text { min-height: 30px; font-size: 0.77rem; padding: 0.25rem 0.8rem; }

    .card-custom { padding-block: 0.9rem; }
    .card-custom .cqa-logo { width: 36px; }
    .card-custom h4 { font-size: 0.95rem; }
    .card-custom p.text-xsmall { font-size: 0.72rem; }
    .card-custom .text-center.mb-2 { margin-bottom: 0.2rem !important; }
    .card-custom .text-center.mb-3 { margin-bottom: 0.35rem !important; }
    .card-custom .cqa-logo.mb-1 { margin-bottom: 0.1rem !important; }
    .card-custom .cqa-logo.mb-2,
    .card-custom .cqa-logo.mb-3 { margin-bottom: 0.3rem !important; }
    .card-custom .divider-soft { margin: 0.25rem auto 0.3rem; }

    .field-group { margin-bottom: 0.4rem; }
    .field-label { margin-bottom: 0.2rem; }
    .field-hint { font-size: 0.66rem; margin-top: 0.2rem; }
    .divider-or { margin: 0.3rem 0; }
    .btn-login, .btn-google { padding-block: 0.5rem; }

    .auth-notice,
    .offline-login-notice,
    .pending-logout-notice,
    .sent-notice,
    .verified-notice { padding-block: 0.5rem; font-size: 0.76rem; margin-bottom: 0.55rem; }

    /* The home button stays at the BOTTOM CENTRE at every size — it is the one
       control that is in the same place on every auth page, and moving it to a
       corner on small screens (which it used to do, to save the ~40px it
       reserves at the foot) both broke that consistency and put it on top of
       the hero title, whose lines run the full width of a phone. The space it
       needs is taken out of the top padding instead: the header sits high, the
       button sits low, and nothing overlaps. */
    .btn-home { width: 2.5rem; height: 2.5rem; font-size: 0.95rem; bottom: 0.7rem; }
}

/* The last squeeze — a very short viewport (a 1024x640-class laptop screen, a
   browser window dragged to half a 1080p display) OR one of the narrowest
   phones still in use (320-360px: an SE-class handset, a folded phone's cover
   screen). Same reasoning as the step above: those small phones are not SHORT,
   they are narrow, so everything on them wraps onto more lines and the card
   ends up taller than it does on a roomier phone. Kept separate from that step
   so the ordinary 700-780px / 361-480px range isn't compressed more than it
   has to be. */
@media (max-height: 680px), (max-width: 360px) {
    /* At this size the secondary explanatory lines are what has to give — the
       same call the compact step already makes for .auth-steps-foot. The
       fields, their labels and every tap target stay exactly as they are; a
       hint that repeats what the placeholder says is worth more as the space
       that keeps the submit button on screen. */
    .field-hint { display: none; }

    .login-wrapper {
        padding-top: 0.25rem;
        --auth-foot: calc(2.2rem + 0.5rem + 0.35rem);
        /* Barely a nudge: at this size the fold is the whole budget. Kept
           non-zero only so the column does not sit flush against the very top
           edge of the screen. */
        --auth-drop: clamp(0.25rem, 1.2vh, 0.75rem);
        --auth-gap-max: clamp(0.5rem, 2.5vh, 1.25rem);
    }
    .btn-home { width: 2.2rem; height: 2.2rem; font-size: 0.85rem; bottom: 0.5rem; }
    .header-container { margin-bottom: 0.38rem; }
    .main-title { font-size: 1.2rem; }
    .sub-title { font-size: 0.86rem; }
    .portal-text { min-height: 26px; }
    .card-custom { padding-block: 0.7rem; }
    .card-custom .cqa-logo { width: 30px; }
    .field-group { margin-bottom: 0.3rem; }
    .field-hint { line-height: 1.25; margin-top: 0.15rem; }
    .divider-or { margin: 0.2rem 0; }
    .btn-login, .btn-google { padding-block: 0.45rem; }
    .sub-title { margin-bottom: 0.2rem; line-height: 1.25; }
    .portal-text { min-height: 25px; font-size: 0.75rem; }
}

/* ---------- Small AND short phones (e.g. 360x640) ----------
   Deliberately an AND, not another OR step: this is the one combination where
   neither of the ladders above is enough on its own — a narrow screen wraps
   every line onto more rows, and a short one has no rows to spare. A tall 360px
   phone and a short desktop window each keep their subtitle; only this corner
   case gives it up. */
@media (max-width: 480px) and (max-height: 700px) {
    .login-wrapper {
        padding-top: 0.25rem;
        --auth-drop: clamp(0.25rem, 1.5vh, 0.9rem);
        --auth-gap-max: clamp(0.5rem, 2.5vh, 1.25rem);
    }
    .header-container { margin-bottom: 0.34rem; }
    .sub-title { display: none; }
    .main-title { font-size: 1.14rem; }
    /* The chip's pop-up clearance, tightened to match the smaller hero. */
    .login-wrapper:has(.offline-pill.is-visible) {
        --auth-drop-min: calc(2.5rem + env(safe-area-inset-top, 0px));
    }
}

/* ==========================================================================
   SHORT DESKTOP WINDOWS — putting the air back where the width says there is
   room for it.

   The ladder above is keyed on height as well as width, so a 1365x650 desktop
   window matched the same steps a 360px phone does: right about the height it
   has, wrong about everything else. The card came out at its absolute tightest
   — hints hidden, subtitle gone, 4px between fields, the reCAPTCHA scaled down
   to 64% beside full-size inputs — because those trims are sized for a phone's
   much narrower column, where every line wraps onto two or three.

   What this cannot do is invent height: at 650px of viewport, a comfortable
   card plus the hero plus the home button genuinely does not fit. So the air
   comes out of the two things that are not the form — the hero block (on a wide
   screen its words fit on one line, so it costs little) and the foot, which now
   reserves only what the button actually needs — and all of it goes to the
   card: first to the reCAPTCHA, which is what looks broken at 64%, then to the
   gaps between fields, which is what reads as cramped.

   Three tiers, each sized for the SHORTEST window it serves, because a 650px
   window and an 810px one have very different budgets. All bounded by
   min-width: 768px so no phone ever sees them, and placed last in the file so
   they land after every step they correct.
   ========================================================================== */

/* Tier 1 — a 1366x768 laptop with browser chrome (~650px of viewport), or a
   window dragged to about that. The tightest desktop case that still has to fit
   without scrolling. */
@media (min-width: 768px) and (max-height: 700px) {
    .login-wrapper {
        /* Zero, not 0.25rem: at ~650px of viewport this tier is 4px away from
           overflowing, and 4px of scroll on the tightest window the file claims
           to fit is not a rounding error, it is the rule being broken. The top
           edge is the cheapest place to find it — the hero is centred text, so
           nothing reads differently for starting 4px higher. */
        padding-top: 0;
        /* Same footprint rule as every other tier, but with the air trimmed to
           the minimum that still reads as a gap rather than a collision, since
           this is the one tier with nothing spare. */
        --auth-foot: calc(2.2rem + 0.4rem + 0.25rem);
        /* The tightest desktop case there is — this tier exists because the
           card only just fits. Nothing to drop by. */
        --auth-drop: 0px;
        --auth-gap-max: clamp(0.4rem, 2vh, 1rem);
    }
    .btn-home { width: 2.2rem; height: 2.2rem; font-size: 0.85rem; bottom: 0.4rem; }

    .header-container { margin-bottom: 0.45rem; }
    .main-title { font-size: 1.1rem; margin-bottom: 0.12rem; }
    .sub-title { display: block; font-size: 0.82rem; margin-bottom: 0.18rem; }
    .portal-text { min-height: 23px; font-size: 0.72rem; padding: 0.18rem 0.75rem; }

    .card-custom { padding-block: 1rem; padding-inline: 1.9rem; }
    .card-custom .cqa-logo { width: 36px; }
    .card-custom h4 { font-size: 1.02rem; }
    .card-custom p.text-xsmall { display: block; font-size: 0.72rem; }
    .card-custom .divider-soft { margin: 0.25rem auto 0.35rem; }
    .card-custom .text-center.mb-2 { margin-bottom: 0.45rem !important; }

    .field-group { margin-bottom: 0.6rem; }
    .field-label { font-size: 0.66rem; margin-bottom: 0.22rem; }
    .field-hint { display: block; font-size: 0.68rem; margin-top: 0.3rem; }
    .btn-login, .btn-google { padding-block: 0.55rem; }
    .divider-or { margin: 0.45rem 0; }
}

/* Tier 2 — roughly 700-780px of viewport. */
@media (min-width: 768px) and (min-height: 701px) and (max-height: 780px) {
    .login-wrapper {
        padding-top: 0.3rem;
        --auth-foot: calc(2.5rem + 0.6rem + 0.35rem);
        --auth-drop: clamp(0px, 1.5vh, 0.75rem);
        --auth-gap-max: clamp(0.75rem, 2.5vh, 1.75rem);
    }
    .btn-home { width: 2.5rem; height: 2.5rem; font-size: 0.92rem; bottom: 0.6rem; }

    .header-container { margin-bottom: 0.75rem; }
    .main-title { font-size: 1.2rem; margin-bottom: 0.18rem; }
    .sub-title { display: block; font-size: 0.89rem; margin-bottom: 0.25rem; }
    .portal-text { min-height: 26px; font-size: 0.77rem; padding: 0.25rem 0.8rem; }

    .card-custom { padding-block: 1.2rem; padding-inline: 2rem; }
    .card-custom .cqa-logo { width: 40px; }
    .card-custom h4 { font-size: 1rem; }
    .card-custom p.text-xsmall { display: block; font-size: 0.76rem; }
    .card-custom .divider-soft { margin: 0.3rem auto 0.45rem; }
    .card-custom .text-center.mb-2 { margin-bottom: 0.3rem !important; }

    .field-group { margin-bottom: 0.7rem; }
    .field-label { font-size: 0.68rem; margin-bottom: 0.26rem; }
    .field-hint { display: block; font-size: 0.7rem; margin-top: 0.35rem; }
    .btn-login, .btn-google { padding-block: 0.6rem; }
    .divider-or { margin: 0.55rem 0; }
}

/* Tier 3 — roughly 780-900px of viewport: nearly the full desktop rhythm.
   It runs all the way to 900 because that is where the compact step above stops:
   without this, a window between 820 and 900 fell back to that phone-shaped step
   and the card visibly SHRANK as the window got taller. */
@media (min-width: 768px) and (min-height: 781px) and (max-height: 900px) {
    .login-wrapper {
        padding-top: 0.35rem;
        --auth-foot: calc(2.6rem + 0.7rem + 0.35rem);
        /* Nearly the full desktop rhythm, so nearly the full drop — but capped
           well under the tall tier's, since this window is up to 140px shorter
           and the card below is very nearly the same height. */
        --auth-drop: clamp(0.4rem, 2vh, 1.25rem);
        --auth-gap-max: clamp(1rem, 3vh, 2.25rem);
    }
    .btn-home { width: 2.6rem; height: 2.6rem; font-size: 0.95rem; bottom: 0.7rem; }

    .header-container { margin-bottom: 1.15rem; }
    .main-title { font-size: 1.56rem; margin-bottom: 0.2rem; }
    .sub-title { display: block; font-size: 0.94rem; margin-bottom: 0.4rem; }
    .portal-text { min-height: 30px; font-size: 0.79rem; padding: 0.3rem 0.85rem; }

    .card-custom { padding-block: 1.4rem; padding-inline: 2.1rem; }
    .card-custom .cqa-logo { width: 46px; }
    .card-custom h4 { font-size: 1.05rem; }
    .card-custom p.text-xsmall { display: block; font-size: 0.78rem; }
    .card-custom .divider-soft { margin: 0.35rem auto 0.5rem; }
    .card-custom .text-center.mb-2 { margin-bottom: 0.7rem !important; }

    .field-group { margin-bottom: 0.85rem; }
    .field-label { font-size: 0.7rem; margin-bottom: 0.3rem; }
    .field-hint { display: block; font-size: 0.7rem; margin-top: 0.35rem; }
    .input-modern .form-control { padding-block: 0.65rem; }
    .btn-login, .btn-google { padding-block: 0.7rem; }
    .divider-or { margin: 0.85rem 0; }

    .auth-notice,
    .offline-login-notice,
    .pending-logout-notice,
    .sent-notice,
    .verified-notice { padding-block: 0.6rem; font-size: 0.8rem; margin-bottom: 0.75rem; }
}

/* One CSPC logo size on every auth card (login, client login, forgot/reset
   password, complete registration), at every breakpoint. The attribute
   selector keeps the Google profile photo on complete-google-registration
   out of it — that is an avatar, not the logo. Specificity is above the
   per-breakpoint `.card-custom .cqa-logo` width ladder, so it wins throughout. */
.card-custom img.cqa-logo[alt="CSPC Logo"] {
    width: 72px;
    height: auto;
    margin-bottom: 0.5rem !important;
}

/* Desktop / tablet: a larger headline and a card that hangs lower.
   Only spacer variables and the title size are set here. The drop and the gap
   are the shrinkable spacers described above, so on a short window they still
   collapse before the fold is touched — the lower position only shows up where
   there is height to spare. Placed last so it wins over the height tiers. */
@media (min-width: 768px) {
    .login-wrapper {
        --auth-drop: clamp(1rem, 5vh, 3.5rem);
        --auth-gap-max: clamp(0.25rem, 1vh, 0.75rem);
    }
    .header-container { margin-bottom: 0.75rem; }
    .main-title { font-size: clamp(1.75rem, 4vh, 2.6rem); }

    /* Fixed, not shrinkable: the header and the card start at the same spot on
       every auth page and in both the Login and Register tabs. The shrinkable
       spacers gave up different amounts for the taller Register card than for
       the Login card, so the header and card jumped when the tab switched. */
    .login-wrapper::before {
        flex: 0 0 auto;
        height: var(--auth-drop);
    }
    .login-wrapper .auth-gap {
        flex: 0 0 auto;
        height: var(--auth-gap-max);
    }
}
