/* ==========================================================================
   login.css — the standalone authentication page.

   The login screen has no application shell (no sidebar, topbar or status
   bar), so it carries its own layout layer instead of app.css/responsive.css.
   Everything else is the existing Tournate design system: the same tokens,
   the same control radius, focus treatment, button hierarchy and semantic
   colours as the operational screens.

   Breakpoints match the application's bands:
     >= 992px  split composition (brand | form)
     <  992px  stacked: compact branded header, then the form
     <  768px  mobile spacing
   ========================================================================== */

/* --------------------------------------------------------------------------
   Page
   -------------------------------------------------------------------------- */
.login-body {
  display: block;
  height: auto;
  min-height: 100%;
  background: var(--tn-login-page-bg);
  overflow-x: hidden;
  overflow-y: auto;
}

.login-layout {
  display: flex;
  min-height: 100vh;
  min-height: 100dvh;
}

/* --------------------------------------------------------------------------
   Brand panel
   An abstract composition in the Tournate blue family: a diagonal gradient,
   soft lighting, and three very large off-canvas arcs. No imagery and no
   motifs — the only asset on the page is the logo itself.
   -------------------------------------------------------------------------- */
.login-brand {
  position: relative;
  display: flex;
  flex: 1 1 50%;
  flex-direction: column;
  min-width: 0;
  padding: 56px 64px 48px;
  /* The deep end of the family is the base colour and sits under the logo;
     the panel brightens toward the bottom-right, where the light sources are.
     background-color doubles as the fallback and as the contrast reference.

     FOUR layers, painted top-down in this order:
       1  the ground     weight at the foot, held off centre to the left
       2  the mid light  the open, brighter quarter on the right
       3  the shade      the deep left edge the logo and the copy sit on
       4  the diagonal   the Tournate family itself

     The ground is listed FIRST because it has to settle everything under
     it, the mid light's lower spill included. Without it the panel ran out
     of tone below its own midpoint and finished on a pale wash — exactly
     where the footer line sits, which is why that line measured 2.2:1. It
     is anchored off centre (18%) and below the bottom edge (106%) so it
     reads as weight rather than as a vignette, and never draws an edge. */
  background-color: var(--tn-login-brand-from);
  background-image:
    radial-gradient(88% 68% at 18% 106%,
      var(--tn-login-ground) 0%, var(--tn-login-ground-fade) 72%),
    radial-gradient(58% 52% at 84% 42%, var(--tn-login-glow) 0%, transparent 64%),
    radial-gradient(64% 108% at -8% 20%, var(--tn-login-shade) 0%, transparent 98%),
    linear-gradient(128deg,
      var(--tn-login-brand-from) 0%,
      var(--tn-login-brand-mid) 54%,
      var(--tn-login-brand-to) 100%);
  /* A micro-grain layer was built and measured here and then taken out: with
     the profile averaged across the panel the gradient already steps by one
     8-bit level at a time, so there was no contouring for a dither to fix.
     It was invisible rather than dirty — but invisible and load-bearing are
     different things, and it was not earning the extra layer. */
  overflow: hidden;
}

/* Three very large arcs — circles positioned mostly outside the panel, so
   only a long, calm curve is visible. This is the whole decorative language:
   abstract geometry, no imagery. They are part of the base composition (not
   of the optional motion layer), so the background is complete on its own. */
.login-brand::before,
.login-brand::after {
  content: "";
  position: absolute;
  border: 1px solid var(--tn-login-ring);
  border-radius: 50%;
  pointer-events: none;
}

/* The long sweeping arc that reads as the edge of the light. */
.login-brand::before {
  width: 1500px;
  height: 1500px;
  top: -26%;
  left: 18%;
  border-color: var(--tn-login-ring-strong);
}

.login-brand::after {
  width: 1180px;
  height: 1180px;
  left: -46%;
  top: -52%;
}

/* The third arc. Decorative geometry only - it carries no light of its own,
   so the moving illumination can never read as a glowing ring. */
.login-brand__deco {
  position: absolute;
  top: 50%;
  left: 10%;
  width: 1900px;
  height: 1900px;
  margin: -950px 0 0 -950px;
  border: 1px solid var(--tn-login-ring-strong);
  border-radius: 50%;
  pointer-events: none;
}

.login-brand__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--tn-sp-9);
}

/* The logo keeps its native 200x52 proportions; width drives the size. */
.login-brand__logo {
  display: block;
  width: 190px;
  max-width: 58%;
  height: auto;
}

.login-brand__message {
  max-width: none;
}

/* ~15ch holds the statement to the reference's three lines without a
   hard-coded <br>, which would fight the smaller breakpoints. */
.login-brand__headline {
  max-width: 22ch;
  margin: 0;
  color: var(--tn-login-brand-text);
  font-size: 46px;
  font-weight: var(--tn-fw-semibold);
  letter-spacing: -0.02em;
  line-height: 1.14;
}

/* The short accent bar under the statement. */
.login-brand__rule {
  display: block;
  width: 64px;
  height: 3px;
  margin: var(--tn-sp-7) 0 var(--tn-sp-7);
  border-radius: var(--tn-radius-pill);
  background: var(--tn-login-accent-rule);
}

.login-brand__text {
  margin: 0;
  max-width: 30ch;
  color: var(--tn-login-brand-muted);
  font-size: 17px;
  line-height: 1.6;
}

.login-brand__foot {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  color: var(--tn-login-brand-faint);
  font-size: var(--tn-fs-sm);
}

.login-brand__dot {
  width: 4px;
  height: 4px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-login-brand-faint);
}

/* THE META CORNER — two lines, and not a footer.

   Login is the only standalone screen in the application, so it is the only
   place a copyright notice belongs; nothing behind the sign-in has a footer
   and this does not become one. No bar, no ground, no rule across the page,
   no links. Two quiet lines in the corner the brand line already occupied.

   A column with its own tight gap rather than two children of
   .login-brand__inner, whose gap is the large one that separates the mark,
   the statement, the partner strip and this corner from each other. */
.login-brand__meta {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-2);
}

/* One step quieter than the line above it: the same faint ink, at the
   caption size, so it reads as a notice rather than as a second statement.
   The ink is a brand token defined in both themes — the panel behind it is
   the same blue in Light and Dark, so one value serves both. */
.login-brand__legal {
  margin: 0;
  color: var(--tn-login-brand-faint);
  font-size: var(--tn-fs-xs);
}

/* --------------------------------------------------------------------------
   Form panel
   -------------------------------------------------------------------------- */
/* The form block is centred in its half — never pushed against the outer
   edge — and the half itself is a calm near-white ground. */
/* The ambient layer is painted on the panel itself rather than on an extra
   element, so there is nothing to delete when it is not wanted: in the light
   theme both stops are fully transparent tokens and the browser paints a
   flat ground, exactly as the approved light composition always did. In dark
   they become two very wide, very low atmospheric lifts. */
.login-panel {
  position: relative;
  display: flex;
  flex: 1 1 50%;
  align-items: center;
  justify-content: center;
  min-width: 0;
  padding: 48px 56px;
  background-color: var(--tn-login-panel-bg);
  background-image:
    radial-gradient(120% 84% at 100% 0%,
      var(--tn-login-ambient-a) 0%, var(--tn-login-ambient-fade) 62%),
    radial-gradient(96% 72% at 0% 100%,
      var(--tn-login-ambient-b) 0%, var(--tn-login-ambient-fade) 64%);
}

/* Optically centred, not mathematically. The block is light at the top (a
   line of display type and a caption) and heavy at the bottom (the card is a
   solid mass), so its centre of gravity sits below its geometric centre and
   dead-centring reads as very slightly low. The extra bottom padding lifts it
   ~14px, which is the amount that stops it looking like it fell.

   Gated on viewport HEIGHT, not width. The card composition is ~825px tall,
   so a 768px laptop already scrolls a little and has no free space to
   redistribute — spending 28px of it on an optical nicety there would just
   deepen the scroll. Above 880px the block genuinely floats in spare room,
   which is the only place the adjustment is about anything. */
@media (min-height: 880px) {
  .login-panel {
    padding-bottom: 76px;
  }
}

/* Above the ambient wash, so no glow can ever wander across form content. */
.login-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 480px;
}

/* Heading and version are centred over the surface; everything INSIDE the
   surface stays left-aligned. */
.login-head {
  margin-bottom: var(--tn-sp-7);
  text-align: center;
}

.login-title {
  margin: 0 0 var(--tn-sp-4);
  color: var(--tn-login-text);
  font-size: 40px;
  font-weight: var(--tn-fw-semibold);
  letter-spacing: -0.01em;
  line-height: 1.2;
}

.login-subtitle {
  margin: 0;
  color: var(--tn-login-text-secondary);
  font-size: var(--tn-fs-lg);
}

/* A quiet lifted surface, not a floating SaaS card: hairline border, one
   soft layered shadow, no glass. */
.login-surface {
  padding: 36px;
  background: var(--tn-login-form-bg);
  border: 1px solid var(--tn-login-surface-border);
  /* the dialog step of the shared radius family; it was a one-off 14px */
  border-radius: var(--tn-radius-lg);
  box-shadow: var(--tn-login-surface-shadow);
}

/* --------------------------------------------------------------------------
   Fields — comfortable, but still recognisably the same controls as the app
   -------------------------------------------------------------------------- */
.login-form {
  --tn-login-control: 50px;
}

.login-field {
  margin-bottom: var(--tn-sp-6);
}

.login-label {
  display: block;
  margin-bottom: var(--tn-sp-3);
  color: var(--tn-login-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
}

.login-control {
  position: relative;
  display: block;
}

.login-control__icon {
  position: absolute;
  top: 50%;
  left: var(--tn-sp-5);
  transform: translateY(-50%);
  color: var(--tn-login-input-icon);
  font-size: var(--tn-fs-lg);
  line-height: 1;
  pointer-events: none;
}

.login-input {
  width: 100%;
  height: var(--tn-login-control);
  padding: 0 var(--tn-sp-5) 0 44px;
  background: var(--tn-login-input-bg);
  border: 1px solid var(--tn-login-input-border);
  border-radius: var(--tn-radius-md);
  color: var(--tn-login-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-base);
  /* Matches .login-pref__select, which already faded: the three inputs and
     the select are one control family and should settle at one rate. Only
     the two properties that actually change are named — never `all`, which
     would also animate the invalid state's background arriving. */
  transition: border-color var(--tn-dur-fast) var(--tn-ease),
              box-shadow var(--tn-dur-fast) var(--tn-ease);
}

.login-input::placeholder {
  color: var(--tn-login-input-placeholder);
}

.login-input:hover {
  border-color: var(--tn-login-input-border-hover);
}

.login-input:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

.login-input:focus + .login-control__icon,
.login-control:focus-within .login-control__icon {
  color: var(--tn-accent);
}

/* AUTOFILL
   Chrome paints an autofilled field with its own ground and ink — pale
   yellow on Light, a flat near-black on Dark. It was the one moment the
   card stopped being Tournate, and on Dark it was severe enough to look
   broken. The background cannot simply be set (the UA rule wins), so the
   ground is driven through an inset shadow the depth of the field and the
   ink through -webkit-text-fill-color; the absurd transition delay is the
   documented way to stop Chrome repainting its own colour a frame later.

   The field keeps its own tokens either way, so a remembered username
   looks exactly like a typed one, in both themes. */
.login-input:-webkit-autofill,
.login-input:-webkit-autofill:hover,
.login-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--tn-login-input-text);
  -webkit-box-shadow: 0 0 0 100px var(--tn-login-input-bg) inset;
  caret-color: var(--tn-login-input-text);
  transition: background-color 100000s ease 0s;
}

/* The inset ground above is a box-shadow, so it would otherwise paint over
   the focus ring — focus has to restate both. */
.login-input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 100px var(--tn-login-input-bg) inset,
                      var(--tn-focus-ring);
}

/* Reserve the toggle's width so showing/hiding never shifts the layout */
.login-input--password {
  padding-right: 48px;
}

.login-input.is-invalid {
  border-color: var(--tn-danger);
  background: var(--tn-danger-soft);
}

/* Same shared state token the modal's invalid controls use, so an empty
   required field reads identically wherever it appears. */
.login-input.is-invalid::placeholder {
  color: var(--tn-input-placeholder-invalid);
}

.login-input.is-invalid:focus {
  box-shadow: var(--tn-focus-ring-danger);
}

/* A remembered value that fails validation keeps the danger tint rather
   than reverting to the neutral ground the autofill rule paints. */
.login-input.is-invalid:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 100px var(--tn-danger-soft) inset;
}

.login-input.is-invalid:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 100px var(--tn-danger-soft) inset,
                      var(--tn-focus-ring-danger);
}

.login-toggle {
  position: absolute;
  top: 50%;
  right: var(--tn-sp-3);
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-lg);
  cursor: pointer;
}

.login-toggle:hover {
  background: var(--tn-login-control-bg);
  color: var(--tn-login-text);
}

.login-toggle:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.login-help {
  display: block;
  margin-top: var(--tn-sp-3);
  color: var(--tn-login-text-muted);
  font-size: var(--tn-fs-sm);
  line-height: 1.45;
}

/* Field-level error — restrained, and it states the problem in words */
.login-error {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  margin: var(--tn-sp-3) 0 0;
  color: var(--tn-danger);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
}

/* Form-level error — a quiet strip, not a large red alert */
.login-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-sp-4);
  margin-bottom: var(--tn-sp-6);
  padding: var(--tn-sp-5);
  background: var(--tn-danger-soft);
  border: 1px solid var(--tn-danger-border);
  border-left: 3px solid var(--tn-danger);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-danger);
  font-size: var(--tn-fs-md);
  line-height: 1.45;
}

.login-alert .bi {
  flex: 0 0 auto;
  font-size: var(--tn-fs-lg);
  line-height: 1.3;
}

/* --------------------------------------------------------------------------
   Remember me + submit
   -------------------------------------------------------------------------- */
/* The 44px touch target already carries ~13px of air above and below the
   17px box, so the field above needed no margin of its own and the margin
   below was doing the work twice: the row sat 20px under the help text and
   30px above the button. Pulling the row up by its own top overhang evens
   that to 20/20 without shrinking the target by a pixel. */
.login-remember {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-4);
  min-height: var(--tn-h-touch);
  margin: 0 0 var(--tn-sp-5);
  color: var(--tn-login-text-secondary);
  font-size: var(--tn-fs-base);
  cursor: pointer;
  user-select: none;
}

.login-submit {
  width: 100%;
  height: 52px;
  /* The shared button radius is 6px, which left the primary action a step
     tighter than the three inputs stacked directly above it. Inside this
     card the geometry is one family: a 12px surface holding 8px controls,
     with nothing at 6px sitting between them. */
  border-radius: var(--tn-radius-md);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
}

/* Pressed reads as the button settling INTO the card rather than as a colour
   swap alone: the lift comes off while the shared :active fill deepens. No
   movement, no glow — the whole cue is one shadow being removed. */
.login-submit:active {
  box-shadow: none;
}

.login-submit[disabled] {
  cursor: progress;
}

/* Spinner reuses the application's, recoloured for the primary button */
.login-submit .spinner-tn {
  width: 17px;
  height: 17px;
  border-color: var(--tn-on-accent-soft);
  border-top-color: var(--tn-text-on-accent);
}

/* Version sits BELOW the surface, quiet and centred. */
.login-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-3);
  margin: var(--tn-sp-7) 0 0;
  color: var(--tn-login-text-muted);
  font-size: var(--tn-fs-sm);
}

.login-foot .bi {
  font-size: var(--tn-fs-md);
  line-height: 1;
}

/* --------------------------------------------------------------------------
   Preferences — Theme and Language

   One row, below the sign-in action and behind a hairline, so the primary
   action keeps the card's only strong weight. The control is the application's
   segmented language (sunken track, lifted selection, accent ink) rebuilt on
   the login surface tokens, because the login card is its own surface in dark.

   Markup contract (see login.html and js/prefs.js):
     .login-pref__choice[role="radiogroup"][data-pref="theme"|"lang"]
       > button.login-pref__option[role="radio"][data-value="…"]
   State lives ONLY in aria-checked — there is no is-active class to keep in
   step, so the control cannot disagree with what a screen reader announces.
   -------------------------------------------------------------------------- */
.login-prefs {
  /* auto-fit rather than a breakpoint: the row becomes one column the moment
     two would be cramped, at every card width, including the tablet band. */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
  gap: var(--tn-sp-5);
  /* Equal air on both sides of the hairline. It sat 20 above and 16 below,
     which read as the rule belonging to the button rather than separating
     two regions of the card. */
  margin-top: var(--tn-sp-7);
  padding-top: var(--tn-sp-7);
  border-top: 1px solid var(--tn-login-divider);
}

.login-pref {
  min-width: 0;
}

/* The quiet eyebrow register the top bar's Market label uses. */
.login-pref__label {
  display: block;
  margin-bottom: var(--tn-sp-3);
  color: var(--tn-login-text-muted);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

/* 38px option + 2px padding + 1px border on each side = a 44px group, the
   shared minimum touch target. */
.login-pref__choice {
  display: flex;
  padding: 2px;
  background: var(--tn-login-control-bg);
  border: 1px solid var(--tn-login-control-border);
  border-radius: var(--tn-radius-md);
}

.login-pref__option {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-3);
  min-width: 0;
  height: 38px;
  padding: 0 var(--tn-sp-4);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-login-control-text);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              color var(--tn-dur-fast) var(--tn-ease);
}

.login-pref__option .bi {
  flex: 0 0 auto;
  font-size: var(--tn-fs-md);
  line-height: 1;
}

.login-pref__option:hover {
  background: var(--tn-login-control-hover-bg);
  color: var(--tn-login-text);
}

/* Selected reads by value and weight, never by a glowing border. */
.login-pref__option[aria-checked="true"] {
  background: var(--tn-login-control-selected-bg);
  border-color: var(--tn-login-control-selected-border);
  box-shadow: var(--tn-login-control-selected-shadow);
  color: var(--tn-login-control-selected-text);
  font-weight: var(--tn-fw-semibold);
}

.login-pref__option[aria-checked="true"]:hover {
  background: var(--tn-login-control-selected-bg);
  color: var(--tn-login-control-selected-text);
}

/* The same ring the shared check control uses, so focus is one idea across
   the prototype. It sits in the track's 2px padding and is never clipped. */
.login-pref__option:focus-visible {
  outline: 2px solid var(--tn-focus-outline-color);
  outline-offset: 1px;
}

/* --------------------------------------------------------------------------
   LANGUAGE — a native select

   44px to the pixel, so it sits level with the Theme control's track beside
   it. Styled from the login card's own input tokens rather than the
   application's, because in Dark the card is a navy surface with its own
   input treatment. The browser's arrow is kept: the application's other
   selects do the same, and replacing it costs the native control.
   -------------------------------------------------------------------------- */
.login-pref__select {
  width: 100%;
  height: 44px;
  padding: 0 var(--tn-sp-5);
  background: var(--tn-login-input-bg);
  border: 1px solid var(--tn-login-input-border);
  border-radius: var(--tn-radius-md);
  color: var(--tn-login-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  cursor: pointer;
  transition: border-color var(--tn-dur-fast) var(--tn-ease);
}

.login-pref__select:hover {
  border-color: var(--tn-login-input-border-hover);
}

.login-pref__select:focus {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

/* (the select's reduced-motion opt-out now sits with the card's other
   transitions, in the single block further down) */

/* ==========================================================================
   992 - 1199px — TABLET LANDSCAPE
   The split survives, but the brand narrows so the form gets real width.
   ========================================================================== */
@media (max-width: 1199.98px) and (min-width: 992px) {
  .login-brand {
    flex: 0 0 42%;
    padding: 40px 44px 36px;
  }

  .login-panel {
    flex: 1 1 58%;
    padding: 40px 44px;
  }

  .login-brand__logo {
    width: 168px;
  }

  .login-brand__headline {
    font-size: 34px;
  }

  .login-brand__rule {
    margin: var(--tn-sp-6) 0;
  }

  .login-brand__text {
    font-size: var(--tn-fs-lg);
  }

  .login-brand__deco {
    left: 8%;
    width: 1500px;
    height: 1500px;
    margin: -750px 0 0 -750px;
  }

  .login-title {
    font-size: 34px;
  }
}

/* ==========================================================================
   < 992px — stacked: a compact branded header, then the form
   ========================================================================== */
@media (max-width: 991.98px) {
  .login-layout {
    flex-direction: column;
  }

  /* A band, not a panel: tall enough to carry the mark and the light, short
     enough that the form stays the priority. */
  .login-brand {
    flex: 0 0 auto;
    min-height: 220px;
    padding: var(--tn-sp-8) var(--tn-sp-8) var(--tn-sp-7);
    justify-content: center;
  }

  .login-brand__inner {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: var(--tn-sp-6);
  }

  .login-brand__logo {
    width: 178px;
    max-width: 52%;
  }

  .login-brand__message {
    max-width: 46ch;
  }

  .login-brand__headline {
    max-width: none;
    font-size: 27px;
  }

  .login-brand__rule {
    margin: var(--tn-sp-5) 0 0;
  }

  /* Supporting copy and the brand line belong to the desktop composition.

     The copyright does NOT stand down with them: it is a notice rather than
     brand copy, and the page it belongs to is the same page at every width.
     It stays where it already was — last in the band, under the partner
     strip — so it needs no second home and no duplicate markup. */
  .login-brand__text,
  .login-brand__foot {
    display: none;
  }

  .login-brand__meta {
    gap: 0;
  }

  .login-brand__deco {
    top: 46%;
    left: 14%;
    width: 1080px;
    height: 1080px;
    margin: -540px 0 0 -540px;
  }

  .login-brand::before {
    width: 760px;
    height: 760px;
    top: -120%;
    left: 32%;
  }

  .login-brand::after {
    width: 620px;
    height: 620px;
    left: -32%;
    top: -110%;
  }

  .login-panel {
    flex: 1 1 auto;
    align-items: flex-start;
    padding: var(--tn-sp-9) var(--tn-sp-8);
  }

  .login-card {
    max-width: 460px;
    margin: 0 auto;
  }
}

/* ==========================================================================
   < 768px — mobile: single column, the form is the priority
   ========================================================================== */
@media (max-width: 767.98px) {
  .login-brand {
    min-height: 180px;
    padding: var(--tn-sp-7) var(--tn-sp-6);
  }

  .login-brand__inner {
    gap: var(--tn-sp-5);
  }

  .login-brand__logo {
    width: 156px;
    max-width: 62%;
  }

  .login-brand__headline {
    font-size: 22px;
  }

  .login-brand__rule {
    width: 48px;
    height: 2px;
    margin-top: var(--tn-sp-4);
  }

  .login-brand__deco {
    top: 44%;
    left: 10%;
    width: 820px;
    height: 820px;
    margin: -410px 0 0 -410px;
  }

  .login-panel {
    padding: var(--tn-sp-7) var(--tn-sp-5) var(--tn-sp-8);
  }

  .login-surface {
    padding: 22px;
  }

  .login-title {
    font-size: 27px;
  }

  .login-subtitle {
    font-size: var(--tn-fs-base);
  }

  .login-field {
    margin-bottom: var(--tn-sp-5);
  }

  /* The whole label is the hit area; the box itself also grows a little
     so it is comfortable to aim at with a thumb. */
  .login-remember .tn-check {
    width: 20px;
    height: 20px;
  }

  /* 16px avoids the iOS zoom-on-focus jump */
  .login-input,
  .login-pref__select {
    font-size: 16px;
  }

  /* The preference row keeps its 44px targets; only the air above it is
     trimmed, so the card does not grow taller than the phone. */
  .login-prefs {
    margin-top: var(--tn-sp-6);
    padding-top: var(--tn-sp-5);
    gap: var(--tn-sp-4);
  }
}

/* Very short viewports (landscape phones): trim the brand further */
@media (max-height: 560px) and (max-width: 991.98px) {
  .login-brand__message {
    display: none;
  }

  .login-brand {
    padding: var(--tn-sp-5) var(--tn-sp-6);
  }
}

@media (prefers-reduced-motion: reduce) {
  .login-submit .spinner-tn {
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
  }

  /* Belt and braces. components.css section 18 already flattens every
     transition on the page with `* { transition-duration: 0.01ms !important }`
     and that !important outranks the shorthand below, so these three are
     what the card would fall back to if that global rule ever narrowed —
     not what is doing the work today. Named rather than assumed. */
  .login-pref__option,
  .login-input,
  .login-pref__select {
    transition: none;
  }
}

/* ==========================================================================
   ▼▼▼ OPTIONAL MOTION LAYER — the brand panel's atmosphere ▼▼▼

   THREE VEILS, TWO ATMOSPHERES.

   The layer is three stacked fields. What they PAINT is a theme token, so
   the same three elements are sunlight in Light and a star field in Dark —
   there is no per-theme markup, no hidden duplicate, and login.css still
   names no colour of its own.

     Light   .tn-login-veil--far / --mid / --near are broad, very soft
             luminous fields: a warm-white wash, a cooler white glint and one
             small warm highlight. No dots, no rays, no radial burst — the
             read is sunlight moving across a blue surface, not an effect
             placed on top of it.

     Dark    the same three become a depth-sorted particle field: far is
             many tiny dim points, near is a few larger brighter ones, each
             with a faint wash behind it for atmosphere. Points are placed
             individually rather than tiled, so there is no repeat grid to
             notice, and none of them is a shape — they are soft glows that
             fade out through their own hue.

   The panel's colour, its arcs and its base lighting all live OUTSIDE this
   fence, so the background is complete and finished with the motion switched
   off — or with this layer deleted entirely.

   To remove the feature: delete this section, the
   <div class="tn-login-hero-ornaments"> block in login.html, and the
   TN.loginOrnaments module in login.js. To switch it off without deleting
   anything: set ENABLED = false in that module, or open
   login.html?ornaments=off.

   COST: three elements. No canvas, no images, no per-particle DOM. Each veil
   rasterises once and then only transform and opacity animate, so the whole
   atmosphere stays on the compositor.
   ========================================================================== */
.tn-login-hero-ornaments {
  position: absolute;
  inset: 0;
  z-index: 0;                 /* under .login-brand__inner (z-index 1) */
  pointer-events: none;
  overflow: hidden;
}

/* Each veil is oversized so it can drift without ever bringing an edge into
   view. background-repeat is off on purpose: the dark field's points are
   placed one by one, and tiling them would produce exactly the regular grid
   this is trying not to look like. */
.tn-login-veil {
  position: absolute;
  inset: -8%;
  display: block;
  background-repeat: no-repeat;
  will-change: transform, opacity;
}

/* ---- FAR: the deep layer. Slowest, smallest travel, lowest contrast. ---- */
.tn-login-veil--far {
  background-image: var(--tn-login-veil-far);
  opacity: 0.85;
  animation: tn-veil-drift-far 79s ease-in-out infinite alternate,
             tn-veil-breathe-far 61s ease-in-out infinite alternate;
}

/* ---- MID: the working layer. Carries most of the character. ---- */
.tn-login-veil--mid {
  background-image: var(--tn-login-veil-mid);
  opacity: 0.9;
  animation: tn-veil-drift-mid 53s ease-in-out infinite alternate,
             tn-veil-breathe-mid 37s ease-in-out infinite alternate;
}

/* ---- NEAR: fewest elements, largest travel. This is what makes the layer
        read as having depth rather than as one flat wash. ---- */
.tn-login-veil--near {
  background-image: var(--tn-login-veil-near);
  opacity: 0.95;
  animation: tn-veil-drift-near 34s ease-in-out infinite alternate,
             tn-veil-breathe-near 47s ease-in-out infinite alternate;
}

/* ---- ACCENT: four anchor lights in Dark, the tail of the wash in Light.
        Its own pair of periods, so in Dark the bright points vary against
        the field underneath instead of with it. ---- */
.tn-login-veil--accent {
  background-image: var(--tn-login-veil-accent);
  opacity: 0.92;
  animation: tn-veil-drift-accent 41s ease-in-out infinite alternate,
             tn-veil-breathe-accent 29s ease-in-out infinite alternate;
}

/* Six periods — 79/61, 53/37, 34/47 — and no two share a factor, so drift
   and brightness never line up twice and the field does not visibly loop.
   Travel is in percent of the veil, which is oversized, so the largest
   movement here is a few dozen pixels over half a minute: perceptible when
   you look, invisible when you are reading. */
@keyframes tn-veil-drift-far {
  from { transform: translate3d(-1.2%,  0.8%, 0) scale(1.00); }
  to   { transform: translate3d( 1.4%, -1.0%, 0) scale(1.03); }
}
@keyframes tn-veil-drift-mid {
  from { transform: translate3d( 1.8%, -1.4%, 0) scale(1.02); }
  to   { transform: translate3d(-2.2%,  1.6%, 0) scale(1.00); }
}
@keyframes tn-veil-drift-near {
  from { transform: translate3d(-1.9%,  1.2%, 0) scale(1.00); }
  to   { transform: translate3d( 2.2%, -1.5%, 0) scale(1.035); }
}

/* The anchor lights move least of anything here — a star that travels is a
   satellite. This is parallax only, a few pixels over most of a minute. */
@keyframes tn-veil-drift-accent {
  from { transform: translate3d( 0.6%, -0.4%, 0) scale(1.00); }
  to   { transform: translate3d(-0.8%,  0.5%, 0) scale(1.02); }
}

/* Brightness breathing, not twinkling: the range is deliberately narrow so
   nothing ever blinks. In Dark the three layers running at different periods
   is what makes individual points appear to vary independently, without a
   per-particle animation anywhere. */
@keyframes tn-veil-breathe-far  { from { opacity: 0.62; } to { opacity: 0.92; } }
@keyframes tn-veil-breathe-mid  { from { opacity: 0.95; } to { opacity: 0.68; } }
@keyframes tn-veil-breathe-near { from { opacity: 0.72; } to { opacity: 1.00; } }
/* The widest swing of the four, and the slowest to come back: this is the
   twinkle. It never reaches zero, so nothing ever blinks out. */
@keyframes tn-veil-breathe-accent { from { opacity: 0.55; } to { opacity: 1.00; } }

/* --------------------------------------------------------------------------
   Responsive: the brand becomes a short band below 992px, so the field is
   thinned rather than scaled — a phone header showing the same number of
   points as a 1080px panel would read as busy.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .tn-login-veil--far {
    display: none;
  }

  .tn-login-veil {
    inset: -6%;
  }
}

@media (max-width: 767.98px) {
  /* A phone gets a placed atmosphere, not a moving one: the band is only
     ~180px tall, so drift there would read as restlessness. */
  .tn-login-veil {
    animation: none;
    transform: translate3d(0, 0, 0) scale(1.02);
  }

  /* Down to ONE layer here. The veils place their content by percentage, so
     on a 180px band the same points that read as a sparse field on a 1080px
     panel land on top of each other — measured at sixteen in the phone
     header, which is a particle field rather than an ambience. The anchor
     lights go too: at 4-5px with a halo they are sized for a 1080px panel,
     and on a band this short they stop being distant stars and start being
     orbs. Keeping only the middle layer thins it to about eight. Light
     barely changes — that layer carries its wash, and the small glints it
     loses were never what made the band read. */
  .tn-login-veil--near,
  .tn-login-veil--accent {
    display: none;
  }

  .tn-login-veil--mid {
    opacity: 0.75;
  }
}

/* --------------------------------------------------------------------------
   Reduced motion: the veils stop where the moving panel spends most of its
   time, not at an end stop, so the still composition is the designed one.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .tn-login-veil {
    animation: none;
  }

  .tn-login-veil--far  { transform: translate3d( 0.1%, -0.1%, 0) scale(1.015); opacity: 0.80; }
  .tn-login-veil--mid  { transform: translate3d(-0.2%,  0.1%, 0) scale(1.01);  opacity: 0.85; }
  .tn-login-veil--near   { transform: translate3d( 0.2%, -0.2%, 0) scale(1.025); opacity: 0.90; }
  .tn-login-veil--accent { transform: translate3d(-0.1%,  0.1%, 0) scale(1.01);  opacity: 0.92; }
}
/* ==========================================================================
   ▲▲▲ END OPTIONAL MOTION LAYER ▲▲▲
   ========================================================================== */

/* ==========================================================================
   ▼▼▼ INTEGRATION LOGOS — the brand panel's partner strip ▼▼▼

   Six partner logos in the lower half of the brand panel — three columns
   by two rows — cycling through a list of twenty in groups of six.

   WHY THERE IS A TILE UNDER EACH LOGO

   Nine of the twenty marks are drawn in black or deep navy. On a blue panel
   those are not quiet, they are absent — so the choice is a light surface
   under every logo, or recolouring the logos, and recolouring somebody
   else's trademark is not a choice. The tile is therefore the entire theming
   surface of this feature: the logos are never tinted, filtered, inverted or
   stretched, in either theme. See --tn-login-integration-* in themes.css.

   WHY THE ASSETS ARE PREPARED RATHER THAN LINKED

   The published sources sit at very different content sizes inside their own
   canvases, and one of them arrives opaque on a white background. They are
   normalised once, offline, into assets/integrations/ — trimmed to their own
   ink, scaled to equal optical AREA (not equal width, which would make a 6:1
   logotype tower over a 1.5:1 stacked mark, and not equal height, which does
   the reverse) and centred on one shared 200x72 canvas. Because every file
   is that same canvas, the page draws all twenty at one fixed box with no
   object-fit rule and no per-logo CSS. assets/integrations/normalise-logos.js
   documents and reproduces the whole step.

   HOW THE TRANSITION WORKS

   Each of the six tiles is a fixed frame holding TWO stacked images. A
   set change paints the incoming logo onto the hidden layer and swaps which
   layer carries .is-visible; the outgoing layer leaves and the incoming one
   arrives behind it, and nothing in the panel ever reflows. Only opacity
   animates, and the layout is settled from the first paint because the
   frame's height comes from aspect-ratio rather than from whichever image
   happens to have loaded.

   To remove the feature: delete this section, the .login-integrations block
   in login.html, and the TN.loginIntegrations module in login.js. To switch
   it off without deleting anything: set ENABLED = false in that module, or
   open login.html?integrations=off.
   ========================================================================== */
.login-integrations {
  /* THE HANDOVER, and the one number that is not obvious.

     A logo is a dense, high-contrast mark, and two of them overlaid at half
     opacity is not a dissolve, it is a double exposure — the single thing
     that makes a crossfade of LOGOS look cheap where the same crossfade of
     photographs looks expensive. So the two layers hand over instead of
     blending: the outgoing one leaves, and the incoming one starts as it is
     finishing. They share 60ms, which is short enough that nothing ever
     reads as a blink and nothing ever reads as a double.

     380 + 320 + 420 is a 740ms handover inside a 4000ms set, so a set is
     completely still for more than three quarters of its time on screen.

     These grew with the interval rather than staying put when it went from
     1900ms to 4000ms. A transition keeps its old speed only if you want a
     long, calm dwell to end in a snap; the pace of the movement has to read
     as belonging to the pace of the sequence. They did NOT grow in
     proportion — 2.1x on the interval against 1.3x here — because past
     roughly half a second a fade stops reading as calm and starts reading
     as slow. Named here rather than as shared tokens: nothing else in the
     prototype does this, and the --tn-dur family tops out at 260ms, which
     is a control answering a click, not an ambience turning over. */
  --tn-login-integration-out: 380ms;   /* the outgoing layer leaving   */
  --tn-login-integration-hold: 320ms;  /* how long the incoming waits  */
  --tn-login-integration-in: 420ms;    /* the incoming layer arriving  */

  /* THE TILE'S CEILING, in one place.

     126 - 12 padding - 1 border on each side = a 100px logo box, which is
     the size the artwork in assets/integrations was normalised against. The
     breakpoints below lower this one number rather than restating the tile
     rule three times, and the grid track reads it directly — so the block
     is always three columns of at most this, never a stretched row. */
  --tn-login-tile-max: 126px;
  flex: 0 0 auto;
  /* Below 992 the panel's column switches to align-items: flex-start, and a
     row of flex-basis:0 tiles has no content width to be sized from — the
     strip collapsed to 89px and the tiles to 24 apiece. Everything else in
     that column is text and sizes itself, so the fix belongs here rather
     than in the panel: this is the one child that needs the full measure. */
  align-self: stretch;
}

/* The same quiet eyebrow register as the card's Theme / Language labels, in
   the panel's existing faint ink. Six logos with no caption read as an
   unexplained decoration; one word is what turns them into a statement. */
.login-integrations__label {
  display: block;
  margin-bottom: var(--tn-sp-4);
  color: var(--tn-login-brand-faint);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

/* SIX TILES, AS THREE COLUMNS BY TWO ROWS.

   A grid, not a flex row, and that is the whole reason six fits where three
   did. One row of six would take each tile to about 60px on a 1440 panel and
   to nothing on the narrow band — half the size the artwork was normalised
   for — and six marks spread edge to edge stop reading as a group at all.
   Three columns keeps the tile at exactly the size it was before, and gives
   the block a shape rather than a line.

   minmax(0, ceiling) rather than 1fr is what keeps the block from spreading
   across a 1920 panel: the tracks grow to the ceiling and stop, and they
   shrink below it when the panel is narrower than 3 x ceiling + 2 gaps, so
   there is no width at which this either stretches or overflows. The row
   itself is still never given a width.

   Nothing in here is interactive, and it is aria-hidden in the markup for a
   related reason: a caption that re-announced itself every 4 seconds would
   be the loudest thing on the page for a screen reader. The names are
   carried once, statically, by the .sr-only line underneath. */
.login-integrations__row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, var(--tn-login-tile-max)));
  gap: var(--tn-sp-5);
  justify-content: start;
  pointer-events: none;
}

.login-integrations__tile {
  display: block;
  min-width: 0;
  padding: var(--tn-sp-5);
  background: var(--tn-login-integration-bg);
  border: 1px solid var(--tn-login-integration-border);
  /* The card family on this page is a 12px surface holding 8px controls.
     A tile this small belongs at the control step. */
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-login-integration-shadow);
}

/* The frame owns the height. Taking it from aspect-ratio rather than from an
   image means the row is at its final size before a single logo has loaded,
   so arriving on the page never shifts the statement above it. */
.login-integrations__frame {
  position: relative;
  display: block;
  aspect-ratio: 200 / 72;
}

.login-integrations__logo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* The assets are already exactly this ratio, so contain neither crops nor
     letterboxes anything today. It is here so that a replaced asset can only
     ever be the wrong size, never the wrong shape. */
  object-fit: contain;
  /* LEAVING. A layer that loses .is-visible transitions with the property
     declared here, and one that gains it with the property declared on the
     rule below — which is the whole mechanism for giving the two halves of
     the handover different timings without JavaScript having to know which
     of the two layers is which. */
  opacity: 0;
  transition: opacity var(--tn-login-integration-out) var(--tn-ease)
              var(--tn-login-integration-lead, 0ms);
}

/* ARRIVING — after the outgoing layer has had --hold to get out of the way. */
.login-integrations__logo.is-visible {
  opacity: 1;
  transition: opacity var(--tn-login-integration-in) var(--tn-ease)
              calc(var(--tn-login-integration-lead, 0ms) +
                   var(--tn-login-integration-hold));
}

/* The six tiles do not turn over in lockstep. Six simultaneous fades read as
   a slide changing; offset by a fraction of their own duration they read as
   a block settling, which is the difference between a component and a
   banner. It is far too small to be seen as a wave.

   The offset runs diagonally, from the top-left tile to the bottom-right:
   90ms per COLUMN and 45ms per ROW, so the six leads are

       0    90   180
       45  135   225

   Column-first was already the character of the three-tile version and it
   is kept; the half-step per row is what stops the second row arriving as a
   second event. 225ms of cascade inside a 740ms handover is still under a
   third of it, which is the ratio that keeps a set completely still for
   about three quarters of its 4s on screen.

   The offset is a proportion of the handover, not a fixed number of
   milliseconds, so it moved with it: 180 of 740 is the same quarter that
   140 of 560 was at the old pace. Hold that ratio and the cascade keeps its
   character at any speed; leave the offset behind and a slower handover
   swallows it until the tiles look simultaneous again.

   One custom property per tile rather than a pile of transition-delay
   rules, because both halves of the handover need the same offset and only
   one of them has a plain delay — the other has to add it to the hold. It
   also means the module can paint all twelve layers in a single pass and
   own no clock but the one between sets. */
.login-integrations__tile:nth-child(2) {
  --tn-login-integration-lead: 90ms;
}

.login-integrations__tile:nth-child(3) {
  --tn-login-integration-lead: 180ms;
}

.login-integrations__tile:nth-child(4) {
  --tn-login-integration-lead: 45ms;
}

.login-integrations__tile:nth-child(5) {
  --tn-login-integration-lead: 135ms;
}

.login-integrations__tile:nth-child(6) {
  --tn-login-integration-lead: 225ms;
}

/* ==========================================================================
   Responsive

   The strip is given no breakpoints of its own — it reuses the panel's, so
   it changes at the same moments the composition around it does.
   ========================================================================== */

/* 992-1199 — the brand panel narrows to 42%, so the tiles give up padding
   before they give up logo. */
@media (max-width: 1199.98px) and (min-width: 992px) {
  .login-integrations__tile {
    padding: var(--tn-sp-4);
  }
}

/* < 992px — the panel is a short header band above the form. The block stays,
   because it is the one piece of the lower composition that still has
   something to say; it just stops being generous about it. */
@media (max-width: 991.98px) {
  .login-integrations {
    --tn-login-tile-max: 108px;
  }

  .login-integrations__row {
    gap: var(--tn-sp-4);
  }

  .login-integrations__tile {
    padding: var(--tn-sp-4);
  }
}

/* < 768px — phones. Below this the block is width-bound rather than
   ceiling-bound, so every pixel spent on padding is a pixel taken off the
   logo; at 320 the tiles are ~92px wide and there is nothing spare. Three
   columns is still the right count there: two would leave a hole in the
   last row of a six-logo set, and six in a column would be a list. */
@media (max-width: 767.98px) {
  .login-integrations {
    --tn-login-tile-max: 100px;
  }

  .login-integrations__label {
    margin-bottom: var(--tn-sp-3);
  }

  .login-integrations__row {
    gap: var(--tn-sp-3);
  }

  .login-integrations__tile {
    padding: var(--tn-sp-4) var(--tn-sp-3);
  }
}

/* Short viewports, in both layouts.

   Below 992 this is the landscape-phone rule that already removes
   .login-brand__message, and the block goes with it for the same reason.

   At 992 and up it is arithmetic, and the arithmetic moved with the second
   row. A tile is its logo box plus 24px of padding and 2px of border, so a
   126px tile stands 62px tall and two rows plus a 12px gap come to 136px
   against the single row's 62px — 74px more, with the eyebrow unchanged.

   The desktop column needs about 500px of inner height to seat logo,
   statement, strip and footer at their minimum 32px gaps, and the panel
   clips rather than scrolls. With the taller block that threshold moves the
   same 74px it grew by: under roughly 700px of viewport the strip is what
   comes out, exactly as the supporting copy is what comes out of the narrow
   band. The narrow rule moves by the same amount, from 560 to 634. */
@media (max-height: 634px) and (max-width: 991.98px) {
  .login-integrations {
    display: none;
  }
}

@media (min-width: 992px) and (max-height: 700px) {
  .login-integrations {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   Reduced motion

   The rotation itself is stopped in JavaScript — the timer never starts, so
   the first six logos are simply the six logos. What is left here is the
   one paint that still happens, and the delays that would otherwise survive
   it: components.css section 18 flattens every transition-DURATION on the
   page with an !important, but it says nothing about delay, so without this
   rule the opening set would still arrive in three steps, the last of them
   half a second after the first. Both selectors are named because the
   arriving layer carries its own transition, and the shorthand here is what
   resets it.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .login-integrations__logo,
  .login-integrations__logo.is-visible {
    transition: none;
  }
}
/* ==========================================================================
   ▲▲▲ END INTEGRATION LOGOS ▲▲▲
   ========================================================================== */
