/*
 * QS Group login theme for ERPNext.
 *
 * Reproduces the Qimah ERP login layout (see LOGIN-DESIGN-SPEC.md) on top of
 * Frappe's stock login markup, so `bench update` cannot revert it.
 *
 * Everything is scoped to `body[data-path="login"]`, which Frappe sets in
 * templates/base.html. Nothing here can leak into the desk or other website
 * pages.
 *
 * The dark panel is a fixed pseudo-element rather than a grid column: the login
 * body carries navbar, noscript, footer and script nodes as siblings, and
 * making it a grid container would lay all of those out as tracks.
 */

body[data-path='login'] {
  --qs-ink: #2490ef;
  --qs-muted: #737373;
  --qs-footer: #525252;
  --qs-line: #e5e5e5;
  --qs-tile: #f5f5f5;

  background: #fff;
  color: var(--qs-ink);
}

/* ── Right-hand brand panel ────────────────────────────────────────────── */

body[data-path='login']::after {
  position: fixed;
  z-index: 0;
  top: 8px;
  right: 8px;
  bottom: 8px;
  width: calc(50% - 12px);
  content: '';
  background: var(--qs-ink);
  border-radius: 24px;
}

@media (max-width: 900px) {
  body[data-path='login']::after {
    display: none;
  }
}

/* ── Left-hand form column ─────────────────────────────────────────────── */

body[data-path='login'] .page_content,
body[data-path='login'] main,
body[data-path='login'] .main-section {
  position: relative;
  z-index: 1;
  display: flex;
  width: 50%;
  min-height: 100vh;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
}

@media (max-width: 900px) {
  body[data-path='login'] .page_content,
  body[data-path='login'] main,
  body[data-path='login'] .main-section {
    width: 100%;
  }
}

/* The stock card is a bordered, shadowed box. Here the panel itself is the
   surface, so the card becomes a plain 350px column. */
body[data-path='login'] .login-content.page-card {
  width: 350px;
  max-width: calc(100vw - 48px);
  padding: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
}

body[data-path='login'] .page-card-head {
  align-items: center;
  text-align: center;
}

body[data-path='login'] .page-card-head .page-card-title,
body[data-path='login'] .page-card-head h4 {
  font-size: 30px;
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 1.15;
}

body[data-path='login'] .page-card-subtitle {
  margin-top: 8px;
  color: var(--qs-muted);
  font-size: 13px;
}

/* ── Fields ────────────────────────────────────────────────────────────── */

body[data-path='login'] .form-control {
  height: 40px;
  color: var(--qs-ink);
  background: #fff;
  border: 1px solid var(--qs-line);
  border-radius: 8px;
  box-shadow: none;
  font-size: 13px;
}

body[data-path='login'] .form-control:focus {
  border-color: #a3a3a3;
  box-shadow: 0 0 0 3px rgb(23 23 23 / 0.06);
}

body[data-path='login'] label {
  color: var(--qs-footer);
  font-size: 12px;
  font-weight: 500;
}

/* ── Submit ────────────────────────────────────────────────────────────── */

body[data-path='login'] .es-button[data-variant='solid'],
body[data-path='login'] button[type='submit'] {
  width: 100%;
  height: 40px;
  color: #fff;
  background: var(--qs-ink);
  border: 0;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 500;
  transition: background 0.15s ease;
}

body[data-path='login'] .es-button[data-variant='solid']:hover,
body[data-path='login'] button[type='submit']:hover {
  background: #1579d0;
}

/* ── Injected chrome (see qs_login.js) ─────────────────────────────────── */

.qs-authorized {
  position: absolute;
  z-index: 2;
  top: 20px;
  left: 40px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--qs-muted, #737373);
  font-size: 11px;
}

.qs-page-footer {
  position: absolute;
  z-index: 2;
  bottom: 20px;
  left: 40px;
  display: flex;
  width: calc(50% - 80px);
  align-items: center;
  justify-content: space-between;
  color: var(--qs-footer, #525252);
  font-size: 10px;
}

.qs-page-footer span:last-child {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.qs-brand {
  position: fixed;
  z-index: 1;
  top: 8px;
  right: 8px;
  bottom: 8px;
  width: calc(50% - 12px);
  overflow: hidden;
  border-radius: 24px;
  color: #fff;
  pointer-events: none;
}

/* Faint 36px grid, fading out toward the bottom. */
.qs-brand::before {
  position: absolute;
  inset: 0;
  content: '';
  background-image:
    linear-gradient(rgb(255 255 255 / 0.09) 1px, transparent 1px),
    linear-gradient(90deg, rgb(255 255 255 / 0.09) 1px, transparent 1px);
  background-size: 36px 36px;
  mask-image: linear-gradient(to bottom, black, transparent 92%);
}

/* Concentric rings: one hairline circle plus two very wide, very faint
   spread shadows. Cheaper and crisper than three stacked elements. */
.qs-brand::after {
  position: absolute;
  top: 17%;
  right: -20%;
  width: 70%;
  aspect-ratio: 1;
  content: '';
  border: 1px solid rgb(255 255 255 / 0.08);
  border-radius: 50%;
  box-shadow:
    0 0 0 90px rgb(255 255 255 / 0.025),
    0 0 0 180px rgb(255 255 255 / 0.018);
}

.qs-brand-heading {
  position: absolute;
  z-index: 1;
  top: 40px;
  left: 40px;
}

.qs-brand-heading img {
  width: 68px;
  height: auto;
  max-width: none;
}

.qs-brand-heading h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -0.025em;
}

.qs-brand-heading p {
  margin: 4px 0 0;
  color: rgb(255 255 255 / 0.7);
  font-size: 13px;
}

.qs-brand-footer {
  position: absolute;
  z-index: 1;
  right: 40px;
  bottom: 40px;
  left: 40px;
  display: grid;
  align-items: stretch;
  gap: 12px;
  grid-template-columns: 1fr 1px 1fr;
}

.qs-brand-footer > i {
  width: 1px;
  background: rgb(255 255 255 / 0.25);
}

.qs-brand-footer h2 {
  margin: 0;
  font-size: 14px;
  font-weight: 500;
}

.qs-brand-footer p {
  margin: 4px 0 0;
  color: rgb(255 255 255 / 0.67);
  font-size: 11px;
  line-height: 1.55;
}

@media (max-width: 900px) {
  .qs-brand,
  .qs-page-footer {
    display: none;
  }
}

/* ── De-branding ───────────────────────────────────────────────────────────
 * The stock login card renders an application logo above the heading. This
 * design carries the mark on the dark panel instead, so the card's copy is
 * hidden rather than swapped — one logo per screen, not two.
 */
body[data-path='login'] .app-logo,
body[data-path='login'] .page-card-head img,
body[data-path='login'] .navbar-brand img {
  display: none !important;
}

/* Framework attribution and the language picker in the page chrome. */
body[data-path='login'] .footer-powered,
body[data-path='login'] .powered-by,
body[data-path='login'] .powered-by-frappe,
body[data-path='login'] footer .text-muted a[href*='frappe'],
body[data-path='login'] a[href*='frappe.io'],
body[data-path='login'] a[href*='erpnext.com'] {
  display: none !important;
}

/* The stock navbar is empty on this page but still reserves height, which
   pushes the centred column off vertical centre. */
body[data-path='login'] .navbar,
body[data-path='login'] header.navbar {
  display: none !important;
}

/* "Don't have an account? Sign up" — this is an internal system, so account
   self-registration is not offered. */
body[data-path='login'] .sign-up-message {
  display: none !important;
}

/* ── Brand panel text colour ───────────────────────────────────────────────
 * The framework styles headings with an explicit colour (`h1..h6 { color: … }`),
 * and an element selector beats the colour inherited from `.qs-brand`. Every
 * text node on the dark panel therefore needs its colour stated outright,
 * otherwise headings render near-black on near-black.
 */
.qs-brand,
.qs-brand h1,
.qs-brand h2,
.qs-brand h3,
.qs-brand p,
.qs-brand span,
.qs-brand div {
  color: #fff;
}

.qs-brand-heading p {
  color: rgb(255 255 255 / 0.7);
}

.qs-brand-footer p {
  color: rgb(255 255 255 / 0.67);
}

/* ── Submit button loading state ───────────────────────────────────────────
 * The label is swapped for a spinner in place rather than the button being
 * replaced, so its width does not jump and the click target stays put.
 */
body[data-path='login'] button[type='submit'] {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

body[data-path='login'] button[type='submit'][data-loading='true'] {
  cursor: progress;
  opacity: 0.85;
}

body[data-path='login'] button[type='submit'][data-loading='true'] .qs-btn-label {
  opacity: 0.9;
}

.qs-spinner {
  display: inline-block;
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  border: 2px solid rgb(255 255 255 / 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: qs-spin 0.6s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
  .qs-spinner {
    animation-duration: 2s;
  }
}

/* ── Brand panel text, forced ──────────────────────────────────────────────
 * Stated with `!important` because this panel is injected into a page whose
 * stylesheet we do not control, and any future framework rule touching
 * headings would otherwise render this text black on black.
 * The muted variants follow so they win over the blanket rule.
 */
.qs-brand,
.qs-brand * {
  color: #fff !important;
}

.qs-brand-heading p {
  color: rgb(255 255 255 / 0.7) !important;
}

.qs-brand-footer p {
  color: rgb(255 255 255 / 0.67) !important;
}

.qs-brand-footer > i {
  background: rgb(255 255 255 / 0.25) !important;
}

/* ── Sign-in overlay ───────────────────────────────────────────────────────
 * On success the browser navigates to the desk, and for that moment the login
 * page is still painted — the dark panel stays on screen and then vanishes,
 * which reads as a glitch. This covers the whole page the instant the form is
 * submitted, so the transition is one clean surface.
 *
 * The logo is white-on-transparent, so `brightness(0)` renders it solid black
 * against the light overlay rather than requiring a second asset.
 */
.qs-signing-overlay {
  position: fixed;
  z-index: 9999;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  background: #fff;
  opacity: 0;
  animation: qs-fade-in 0.18s ease forwards;
}

.qs-signing-overlay img {
  width: 132px;
  height: auto;
  filter: brightness(0);
  opacity: 0.9;
}

.qs-signing-overlay .qs-signing-spinner {
  width: 22px;
  height: 22px;
  border: 2px solid rgb(23 23 23 / 0.15);
  border-top-color: #171717;
  border-radius: 50%;
  animation: qs-spin 0.6s linear infinite;
}

.qs-signing-overlay p {
  margin: 0;
  color: #737373;
  font-size: 12px;
}

@keyframes qs-fade-in {
  to {
    opacity: 1;
  }
}

/* ── Signing-in state ──────────────────────────────────────────────────────
 * Hides the split layout outright while the overlay is up. Belt and braces:
 * even if the overlay node were removed by a framework re-render, nothing
 * underneath is revealed, so the dark panel cannot flash back before the
 * browser navigates.
 */
html.qs-signing .qs-brand,
html.qs-signing .qs-authorized,
html.qs-signing .qs-page-footer,
html.qs-signing body > *:not(.qs-signing-overlay) {
  visibility: hidden !important;
}

html.qs-signing body,
html.qs-signing {
  background: #fff !important;
}

html.qs-signing body::after {
  display: none !important;
}

/* The overlay itself must stay visible inside a hidden subtree. */
.qs-signing-overlay {
  visibility: visible !important;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * PHONES
 *
 * The 900px rule above collapses the two-column layout, which is the tablet
 * case. Phones need more than a narrower column — these are the things that
 * actually break a login on a handset.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* `100vh` on a phone is the viewport *without* the browser chrome, so the
 * panel is taller than the visible area and the form sits under the URL bar
 * until the user scrolls. `dvh` tracks the chrome as it hides and shows.
 * The vh line stays first as a fallback for older engines. */
body[data-path='login'] .page_content,
body[data-path='login'] main,
body[data-path='login'] .main-section {
  min-height: 100vh;
  min-height: 100dvh;
}

@media (max-width: 600px) {
  /* Room for the notch and the home indicator. Without this the heading can
     sit under the status bar on an iPhone. */
  body[data-path='login'] .page_content,
  body[data-path='login'] main,
  body[data-path='login'] .main-section {
    padding: max(20px, env(safe-area-inset-top)) 20px
             max(20px, env(safe-area-inset-bottom)) 20px;
    align-items: flex-start;
    padding-top: 12vh;
  }

  body[data-path='login'] .login-content.page-card {
    width: 100%;
    max-width: 420px;
  }

  /* iOS zooms the whole page when a focused input is under 16px, which leaves
     the user zoomed in on a field with the rest of the form off-screen. */
  body[data-path='login'] input,
  body[data-path='login'] .form-control,
  body[data-path='login'] input[type='email'],
  body[data-path='login'] input[type='password'],
  body[data-path='login'] input[type='text'] {
    font-size: 16px;
    height: 48px;
    border-radius: 10px;
  }

  /* A comfortable thumb target. 48px is the usual floor for a primary action. */
  body[data-path='login'] .btn-login,
  body[data-path='login'] .btn-primary,
  body[data-path='login'] button[type='submit'] {
    min-height: 48px;
    font-size: 15px;
    border-radius: 10px;
  }

  body[data-path='login'] .page-card-head h4,
  body[data-path='login'] h4 {
    font-size: 20px;
  }

  /* The brand mark is decoration on a small screen; the form is the point. */
  body[data-path='login'] .qs-login-rings,
  body[data-path='login'] .qs-login-tagline {
    display: none;
  }
}

/* A phone held sideways has very little height — centring vertically pushes
   the fields off the bottom. Let it scroll from the top instead. */
@media (max-height: 480px) and (orientation: landscape) {
  body[data-path='login'] .page_content,
  body[data-path='login'] main,
  body[data-path='login'] .main-section {
    align-items: flex-start;
    padding-top: 16px;
    padding-bottom: 16px;
    min-height: auto;
  }

  body[data-path='login'] .qs-login-logo {
    width: 44px;
  }
}

/* Very small handsets — 320px wide and similar. */
@media (max-width: 360px) {
  body[data-path='login'] .page_content,
  body[data-path='login'] main,
  body[data-path='login'] .main-section {
    padding-left: 14px;
    padding-right: 14px;
  }
}

/* ═══ Login: monogram, glass panel, blue action ═══════════════════════════
 *
 * The large ring in the brand panel read as a stray letter O rather than
 * decoration. Replaced with the company's own initials, sized to sit behind
 * the copy rather than compete with it.
 */

/* The ring becomes a monogram. Still the same single pseudo-element, so no
   extra markup and nothing new for the JS to build. */
.qs-brand::after {
  top: 14%;
  right: -6%;
  width: auto;
  aspect-ratio: auto;
  content: 'QS';
  border: 0;
  border-radius: 0;
  box-shadow: none;
  font-size: clamp(140px, 22vw, 300px);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1;
  color: rgb(255 255 255 / 0.07);
  user-select: none;
}

/* A soft sheen across the top of the panel, the same gesture as the app
   icons — light falling on a surface rather than a flat block of colour. */
.qs-brand {
  box-shadow: 0 18px 50px rgb(0 0 0 / 0.18), inset 0 1px 0 rgb(255 255 255 / 0.10);
}

.qs-brand-heading::before {
  position: absolute;
  z-index: -1;
  top: -40%;
  left: -30%;
  width: 160%;
  height: 120%;
  content: '';
  background: radial-gradient(
    ellipse at 30% 0%,
    rgb(255 255 255 / 0.14),
    transparent 60%
  );
  pointer-events: none;
}

/* ── Panel: stronger sheen, sitting higher ───────────────────────────────
 *
 * The previous highlight was centred too low and too faint to read as glass.
 * Raised and strengthened, with a second, tighter band along the very top
 * edge — that hard line is what makes a surface look polished rather than
 * merely lit.
 */
.qs-brand-heading::before {
  top: -55%;
  left: -25%;
  width: 150%;
  height: 130%;
  background: radial-gradient(
    ellipse at 32% 8%,
    rgb(255 255 255 / 0.26),
    rgb(255 255 255 / 0.08) 42%,
    transparent 66%
  );
}

.qs-brand {
  box-shadow:
    0 18px 50px rgb(0 0 0 / 0.20),
    inset 0 1px 0 rgb(255 255 255 / 0.28),
    inset 0 -1px 0 rgb(255 255 255 / 0.06);
}

/* A broad diagonal sheen across the upper half of the panel itself. */
.qs-brand-heading::after {
  position: absolute;
  z-index: -1;
  top: -30%;
  left: -40%;
  width: 180%;
  height: 90%;
  content: '';
  background: linear-gradient(
    118deg,
    transparent 30%,
    rgb(255 255 255 / 0.13) 48%,
    transparent 64%
  );
  pointer-events: none;
}

/* ── The page must not scroll ────────────────────────────────────────────
 * The panel is fixed to the viewport, so any scrolling only ever revealed
 * empty page beneath the form. */
body[data-path='login'],
body[data-path='login'] html {
  height: 100vh;
  max-height: 100vh;
  overflow: hidden;
}

/* ── The monogram: bigger, centred ───────────────────────────────────────
 * It was tucked into the upper right, half off the panel, which read as a
 * crop rather than a mark. Centred and larger, it sits behind the copy as a
 * watermark should. */
.qs-brand::after {
  top: 50%;
  right: auto;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: clamp(200px, 30vw, 420px);
  color: rgb(255 255 255 / 0.06);
}

/* ── Slow motion ─────────────────────────────────────────────────────────
 *
 * The grid drifts and the sheen crosses, both very slowly. On a sign-in
 * screen the movement should be at the edge of noticing — enough that the
 * surface feels alive, never enough to pull the eye off the form.
 *
 * Both are transform or background-position only, so they stay on the
 * compositor and cost nothing while someone types.
 */
@keyframes qs-grid-drift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 36px 36px, 36px 36px; }
}

.qs-brand::before {
  animation: qs-grid-drift 24s linear infinite;
}

@keyframes qs-sheen-pass {
  0%   { transform: translate3d(-30%, 0, 0); opacity: 0.55; }
  50%  { transform: translate3d(30%, 0, 0);  opacity: 1; }
  100% { transform: translate3d(-30%, 0, 0); opacity: 0.55; }
}

.qs-brand-heading::after {
  animation: qs-sheen-pass 18s ease-in-out infinite;
}

/* The monogram breathes, very faintly. */
@keyframes qs-mark-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

.qs-brand::after {
  animation: qs-mark-breathe 14s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .qs-brand::before,
  .qs-brand::after,
  .qs-brand-heading::after {
    animation: none !important;
  }
}

/* ── Sheen on the panel, not on the heading ──────────────────────────────
 *
 * The previous sheen lived on .qs-brand-heading, which is a small absolutely
 * positioned box holding the logo and one line of text. So the highlight was
 * confined to that corner instead of crossing the panel — which is why it
 * looked like nothing was happening.
 *
 * It now rides on the panel's own background layer, above the base colour and
 * below everything else, and the animation moves the gradient across the full
 * width.
 */
.qs-brand-heading::before,
.qs-brand-heading::after {
  display: none;
}

.qs-brand {
  background-image: linear-gradient(
    115deg,
    transparent 34%,
    rgb(255 255 255 / 0.20) 47%,
    rgb(255 255 255 / 0.28) 50%,
    rgb(255 255 255 / 0.20) 53%,
    transparent 66%
  );
  background-size: 260% 100%;
  background-repeat: no-repeat;
  animation: qs-panel-sheen 11s ease-in-out infinite;
}

@keyframes qs-panel-sheen {
  0%   { background-position: 130% 0; }
  50%  { background-position: -30% 0; }
  100% { background-position: 130% 0; }
}

/* ── The monogram at half the panel ──────────────────────────────────────
 * Sized off the panel width rather than the viewport, so it keeps the same
 * proportion whatever the screen. */
.qs-brand::after {
  font-size: clamp(260px, 26vw, 620px);
  color: rgb(255 255 255 / 0.10);
}

/* Grid lines: stronger again, and the drift made larger so it is visible. */
.qs-brand::before {
  background-size: 44px 44px;
  animation: qs-grid-drift 18s linear infinite;
}

@keyframes qs-grid-drift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 44px 44px, 44px 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .qs-brand { animation: none !important; }
}

/* The monogram, much larger — spanning most of the panel.
 *
 * Sized in vw and allowed to overflow: the panel already clips with
 * overflow:hidden, so the letterforms run off the edges rather than shrinking
 * to fit. A watermark that fills its field reads as deliberate; one that fits
 * neatly inside reads as a logo placed too large. */
.qs-brand::after {
  font-size: min(46vw, 70vh);
  line-height: 0.78;
  color: rgb(255 255 255 / 0.11);
  white-space: nowrap;
}

/* ── Monogram, oversized and cropped ─────────────────────────────────────
 * Large enough that only part of each letter is on the panel — the Q's right
 * side and the S's left. A mark cropped by its frame reads as scale; one that
 * fits reads as a logo. */
.qs-brand::after {
  font-size: min(105vw, 150vh);
  line-height: 0.72;
  letter-spacing: -0.06em;
  color: rgb(255 255 255 / 0.09);
}

/* ── The grid in perspective ─────────────────────────────────────────────
 *
 * Tilted away from the viewer and drifting toward the horizon, so it reads as
 * a plane receding rather than a flat pattern sliding. The mask fades it out
 * where it meets the vanishing point — without that it turns into a hard band
 * of moiré at the top.
 *
 * transform and background-position only, so it stays on the compositor.
 */
.qs-brand::before {
  top: -30%;
  height: 170%;
  transform: perspective(520px) rotateX(56deg) scale(1.9);
  transform-origin: 50% 100%;
  background-size: 56px 56px;
  background-image:
    linear-gradient(rgb(255 255 255 / 0.16) 1px, transparent 1px),
    linear-gradient(90deg, rgb(255 255 255 / 0.16) 1px, transparent 1px);
  mask-image: linear-gradient(to top, black 8%, rgb(0 0 0 / 0.5) 45%, transparent 78%);
  animation: qs-grid-fly 7s linear infinite;
}

/* One full cell per cycle, so the loop is seamless — anything else visibly
   jumps at the restart. */
@keyframes qs-grid-fly {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 0 56px, 0 56px; }
}

@media (prefers-reduced-motion: reduce) {
  .qs-brand::before { animation: none !important; }
}

/* ── The grid, rebuilt as a receding floor ───────────────────────────────
 *
 * The previous attempt tilted the layer and scaled it 1.9x, which pushed most
 * of it outside the panel's overflow:hidden — and the mask faded what little
 * remained. Nothing was left to see.
 *
 * This keeps the element inside the panel and tilts it from its own bottom
 * edge, so the plane lies down and runs away toward the middle of the panel.
 * The lines are twice as bright as before, because perspective compresses
 * them toward the horizon and a faint line simply vanishes there.
 */
.qs-brand::before {
  inset: auto 0 0 0;
  top: auto;
  height: 78%;
  transform: perspective(420px) rotateX(62deg);
  transform-origin: 50% 100%;
  background-size: 62px 62px;
  background-image:
    linear-gradient(rgb(255 255 255 / 0.30) 1px, transparent 1px),
    linear-gradient(90deg, rgb(255 255 255 / 0.30) 1px, transparent 1px);
  /* Fade only at the horizon, keeping the foreground crisp. */
  mask-image: linear-gradient(to top, black 55%, transparent 96%);
  animation: qs-grid-fly 6s linear infinite;
  will-change: background-position;
}

/* Moving the pattern down the tilted plane reads as travelling forward. One
   whole cell per cycle keeps the loop seamless. */
@keyframes qs-grid-fly {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 0 62px, 0 62px; }
}

/* A matching plane above, tilted the other way, so the panel reads as a
   corridor rather than a floor with nothing over it. */
.qs-brand-footer::after {
  position: absolute;
  z-index: -1;
  inset: 0 0 auto 0;
  height: 62%;
  content: '';
  transform: perspective(420px) rotateX(-62deg);
  transform-origin: 50% 0%;
  background-size: 62px 62px;
  background-image:
    linear-gradient(rgb(255 255 255 / 0.16) 1px, transparent 1px),
    linear-gradient(90deg, rgb(255 255 255 / 0.16) 1px, transparent 1px);
  mask-image: linear-gradient(to bottom, black 50%, transparent 96%);
  animation: qs-grid-fly 6s linear infinite;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .qs-brand::before,
  .qs-brand-footer::after {
    animation: none !important;
  }
}

/* ── The grid: flat, full panel, slow ────────────────────────────────────
 *
 * Three attempts at a perspective grid all read as harsh — tilted planes make
 * lines converge into hard moiré bands, and at any speed that is visible the
 * convergence flickers. A flat grid drifting slowly is calmer and reads as
 * texture rather than as an effect, which is what a sign-in screen wants.
 *
 * Both perspective layers are switched off rather than deleted, so the earlier
 * approach stays in the file as a record of what was tried.
 */
.qs-brand-footer::after {
  display: none;
}

.qs-brand::before {
  inset: 0;
  top: 0;
  height: auto;
  transform: none;
  background-size: 52px 52px;
  background-image:
    linear-gradient(rgb(255 255 255 / 0.13) 1px, transparent 1px),
    linear-gradient(90deg, rgb(255 255 255 / 0.13) 1px, transparent 1px);
  /* Full panel, fading only at the very bottom so the footer text stays clean. */
  mask-image: linear-gradient(to bottom, black 70%, transparent 99%);
  animation: qs-grid-drift 40s linear infinite;
}

/* One cell diagonally over forty seconds — movement you notice only if you
   look for it. */
@keyframes qs-grid-drift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 52px 52px, 52px 52px; }
}
