/* Dispatch Pro — auth screens (login, 2FA, password reset).
 Ported from designs/login/login.html. The track-results dashboard, the
 Command Center mock and the Microsoft passwordless button were not
 ported (see the design notes in that file); the authenticator app is a
 2FA method instead. Sections marked 'island additions' are ours. */

:root{
  --primary: #2d88ff;
  --primary-dark: #1667ca;
  --text-color: #616376;
  --heading: #000000;
  --border: #ececee;
  --bg: #ffffff;
  --surface: #ffffff;
  --text-strong: #000000;
  --text-faint: #aaaaaa;
  --text-placeholder: #b0b0b8;
  --icon-border: #dfdfdf;
  --toggle-shadow: rgba(20,25,40,0.1);
}

/* Not from Figma — geometry of assets/login-logo-horizontal-dark.svg
   (its own width/height attributes: 168.6 x 31.4747). The icon mark
   ("dp") occupies roughly x:[0, 34.2] of that box; the "DISPATCHPRO"
   wordmark text starts at x≈39.8 and runs to 168.6. 37 sits in the gap
   between them and is used as the crop split point for .dp-icon-window
   below — lets the single asset be cropped down to just its icon
   region with plain CSS calc(), no image editing and no second asset. */
:root{
  --dp-logo-w: 168.6;
  --dp-logo-h: 31.4747;
  --dp-icon-w: 37;
  /* Left edge of the "DISPATCHPRO" text in the same asset (measured
     off the SVG source directly — the leftmost x in the first letter's
     own path data). Used by .dp-text-window/.dp-text-crop to crop the
     same single asset down to just the text, the same way --dp-icon-w
     crops it down to just the icon. */
  --dp-text-x: 39.7923;
  /* Where the "d"/"p" seam sits, and how wide a soft blend zone
     straddles it — both as a % of .dp-icon-crop's own (oversized,
     full-asset-width) box. See .dp-icon-crop-top/-bottom's own
     comment for why a plain 50% doesn't work here. */
  --dp-icon-seam: calc(100% * var(--dp-icon-w) / var(--dp-logo-w) / 2);
  --dp-icon-feather: calc(100% * var(--dp-icon-w) / var(--dp-logo-w) * 0.06);
}

/* Dark theme — navy surfaces matching the product's own dashboard chrome,
   toggled via the button in the corner (persisted in localStorage). */
[data-theme="dark"]{
  --text-color: #a6b0c3;
  --heading: #f3f5f9;
  --border: #263248;
  --bg: #0b1220;
  --surface: #121b2e;
  --text-strong: #f3f5f9;
  --text-faint: #7c879b;
  --text-placeholder: #6b7590;
  --icon-border: #33405c;
  --toggle-shadow: rgba(0,0,0,0.35);
}

*{ box-sizing: border-box; }

html,body{
  margin:0;
  padding:0;
  overflow-x:hidden;
  font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  color:var(--text-color);
  background:var(--bg);
}

body, .otp-card, .otp-option, .field input, .email-chip, .btn-secondary, .theme-toggle{
  transition: background-color .3s ease, color .3s ease, border-color .3s ease;
}

a{ color:inherit; text-decoration:none; }
button{ font-family:inherit; }

.split{
  display:flex;
  min-height:100vh;
  width:100%;
}

/* ---------- Left brand panel ---------- */

.panel-left{
  flex:0 0 60.8%;
  max-width:60.8%;
  position:relative;
  background-image: linear-gradient(134.47deg, #2d88ff 1.5%, #1667ca 98.5%);
  display:flex;
  align-items:center;
  /* 17.08% was written as a percentage, but padding percentages always
     resolve against the CONTAINING block's width (.split, ~100vw here)
     — not this element's own 60.8%-of-that width. That silently over-
     padded the panel down to a ~380px content column regardless of
     screen size, which is what was squeezing the feature row into a
     vertical stack. 10.385vw is the same 17.08% inset, computed against
     vw directly so it actually scales with panel-left's own width
     (17.08% × 60.8vw). */
  padding:64px 10.385vw;
  color:#fff;
  overflow:hidden;
  isolation:isolate;
}

/* Two soft light/dark pools breaking up the flat gradient — a light
   glow top-left, a deeper pool bottom-right — so the panel reads as
   lit from one side instead of a single uniform blue fill. */
.panel-left::before{
  content:"";
  position:absolute;
  inset:0;
  background:
    radial-gradient(620px circle at 6% 8%, rgba(255,255,255,0.18), transparent 58%),
    radial-gradient(680px circle at 96% 94%, rgba(6,30,72,0.4), transparent 55%);
  pointer-events:none;
  z-index:0;
}

/* A faint diagonal line field — the same "routes" idea behind the logo
   mark, kept subtle enough to read as texture rather than decoration. */
.panel-left::after{
  content:"";
  position:absolute;
  inset:0;
  background-image: repeating-linear-gradient(115deg, rgba(255,255,255,0.06) 0px, rgba(255,255,255,0.06) 1px, transparent 1px, transparent 68px);
  opacity:0.6;
  pointer-events:none;
  z-index:0;
}

.brand-block{
  position:relative;
  z-index:1;
  width:100%;
  max-width:640px;
}

/* Not from Figma — reveal timing added at the user's direct request,
   traced from a reference recording of the original full-page boot
   screen's own icon-merge-then-crossfade: the icon mark below merges
   first, holds, THEN crossfades — icon fades out while the complete
   "DISPATCHPRO" lockup fades in already 30% bigger than resting and
   grows the rest of the way in (see .dp-full-lockup below) — THEN the
   badges further down stagger in one at a time. Built with plain CSS
   transitions triggered by .brand-in (added by a tiny script right
   after paint — see it near the bottom of the page), NOT keyframe
   animation-delay chains: a screen recording confirmed the
   animation-delay approach never visually ran in the user's browser
   across Edge/Chrome/Firefox alike, while this exact transition +
   class-toggle mechanism (used for this same reveal before) did. */

/* Holds .dp-icon-window (small icon-only merge) and .dp-full-lockup
   (icon+text together, crossfading in over it — see the "Boot
   animation" CSS section for both). Sized at its final, already-30%-
   enlarged width from the start — nothing here changes size over time;
   the "enlarge" reads entirely through .dp-full-lockup's own scale
   transform, same as the reference recording (the wordmark itself
   grows in place, the surrounding layout doesn't reflow). */
.brand-wordmark-wrap{
  position:relative;
  width:calc(min(460px, 92%) * 1.3);
  margin:16px 0 0;
}

/* Not from Figma — added at the user's direct request: the row starts
   under the "DISPATCHPRO" text specifically (indented past the icon by
   the text's own left offset within the lockup asset, 39.7923/168.6 of
   the wrap width), not centered under the whole lockup, and spans
   EXACTLY the text region's width (the remaining fraction of the wrap)
   with space-between so its right edge lines up with the "O" of the
   text at any screen size. Negative top margin pulls the row up into
   the lockup's own bottom whitespace, halving the visual gap to the
   text; the enlarged icon (see .brand-block .dp-icon-window) fills the
   column beside it. */
.badges{
  /* Restored to designs/login/login.html. The deployed copy had drifted to
     flex-wrap:nowrap, justify-content:space-between, a negative top margin
     and a width cut down to just the wordmark's text region - three pills in
     that narrow a box overflowed and collided with each other and with the
     logo above. The design centres them across the full lockup width with a
     positive margin, which cannot overlap by construction. */
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  justify-content:center;
  row-gap:14px;
  column-gap:18px;
  margin:8px 0 0;
  width:calc(min(460px, 92%) * 1.3);
}

/* The lockup reserves its height through .dp-lockup's aspect-ratio. If that
   is ever unsupported or overridden the box collapses to zero and the
   absolutely-positioned wordmark straddles the badges below it, which is
   exactly the overlap this guards against. Belt and braces: reserve the same
   height the aspect-ratio would, only where aspect-ratio is unavailable. */
@supports not (aspect-ratio: 1 / 1) {
  .brand-wordmark-wrap::before{
    content:"";
    display:block;
    padding-top: calc(100% * var(--dp-logo-h) / var(--dp-logo-w));
  }
}

/* Not from Figma — staggered one-at-a-time reveal added at the user's
   direct request, continuing the chain from the logo above (merge →
   crossfade/enlarge → badges). nth-child transition-delays, same idea
   the original full-page boot screen used for its own feature row,
   just transition-based rather than animation-based (see the .badges
   comment above for why). */
.badge{
  display:flex;
  align-items:center;
  gap:8px;
  opacity:0;
  transform:translateY(14px);
  transition: opacity 1.5s cubic-bezier(.16,1,.3,1), transform 1.5s cubic-bezier(.16,1,.3,1);
}

.brand-block.brand-in .badge{
  opacity:1;
  transform:translateY(0);
}

/* Sequential chain per request: badge 1 starts when the text's 3s fade
   (running .125s → ~3.1s) is 50% done (~1.6s); each later badge starts
   .75s after the previous one — half the previous 1.5s spacing, so the
   1.5s fades overlap their neighbors halfway. */
.brand-block.brand-in .badge:nth-child(1){ transition-delay:1.6s; }
.brand-block.brand-in .badge:nth-child(2){ transition-delay:2.35s; }
.brand-block.brand-in .badge:nth-child(3){ transition-delay:3.1s; }

/* Hover needs to respond instantly, not wait out whichever entrance
   delay above happens to still be in scope on this element — reset it
   explicitly rather than relying on selector order/specificity. */
.badge:hover{
  transition-delay:0s !important;
  transition-duration:.25s;
  transform:translateY(-2px);
}

.badge .icon{
  display:flex;
  align-items:center;
  justify-content:center;
  flex:none;
  width:22px;
  height:22px;
  border-radius:6px;
  background:rgba(255,255,255,0.14);
}

.badge .icon svg{
  width:12px;
  height:12px;
  stroke:#fff;
  opacity:0.9;
}

.badge .label{
  font-size:13px;
  font-weight:500;
  letter-spacing:0.1px;
  color:rgba(255,255,255,0.95);
  white-space:nowrap;
}

/* ---------- Right form panel ---------- */

/* Used to stay hidden (opacity:0, slid right) until the old full-page
   boot overlay dissolved, revealed by JS adding .panel-in. Now that the
   boot animation is contained to .panel-left only (at the user's direct
   request), the login form has nothing to wait on — it just renders
   normally from the start. */
.panel-right{
  flex:1;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:flex-start;
  position:relative;
  padding:48px 24px;
}

/* Two-way segmented switch (Figma node 1:215) — one pill split into two
   equal halves, each half rounded only on its outer corner so together
   they read as a single control. */
.auth-tabs{
  flex:none;
  display:flex;
  width:100%;
  max-width:364px;
  margin:0 0 32px;
}

/* Holds whichever auth-step is currently visible and centers it in
   whatever vertical space is left below the tabs — the tabs stay
   pinned near the top of the panel (in normal flow, so there's no
   overlap risk at any viewport height) while the form itself keeps
   its previous centered feel underneath. */
.auth-views{
  flex:1;
  width:100%;
  display:flex;
  align-items:center;
  justify-content:center;
}

.auth-tab{
  flex:1;
  height:35px;
  font-family:inherit;
  font-size:12px;
  font-weight:400;
  color:var(--text-color);
  background:var(--surface);
  border:1px solid var(--border);
  cursor:pointer;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease;
}

.auth-tab:first-child{
  border-radius:8px 0 0 8px;
  border-right:none;
}

.auth-tab:last-child{
  border-radius:0 8px 8px 0;
}

.auth-tab.active{
  background:var(--primary);
  border-color:var(--primary);
  color:#fff;
}

.auth-tab:not(.active):hover{
  color:var(--primary);
}

.form-block{
  width:100%;
  max-width:487px;
}

.form-block h1{
  font-size:26px;
  font-weight:600;
  line-height:1.42;
  color:var(--heading);
  margin:0 0 12px;
}

.signup-note{
  font-size:14px;
  line-height:1.42;
  color:var(--text-color);
  margin:0 0 32px;
}

.signup-note a{
  color:var(--primary);
}

.field{
  margin-bottom:12px;
}

.field input{
  width:100%;
  height:35px;
  padding:0 15px;
  font-family:inherit;
  font-size:12px;
  font-weight:300;
  color:var(--text-color);
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:6px;
  outline:none;
  transition: border-color .2s ease, box-shadow .2s ease;
}

.field input::placeholder{
  color:var(--text-color);
}

/* Soft blue backlight on hover — a preview of the focus state so the
   field reads as interactive before you even click into it. */
.field input:hover{
  border-color:var(--primary);
  box-shadow: 0 0 0 3px rgba(45,136,255,0.12);
}

.field input:focus{
  border-color:var(--primary);
  box-shadow: 0 0 0 4px rgba(45,136,255,0.16), 0 0 18px rgba(45,136,255,0.2);
}

[data-theme="dark"] .field input:hover{
  box-shadow: 0 0 0 3px rgba(45,136,255,0.22);
}

[data-theme="dark"] .field input:focus{
  box-shadow: 0 0 0 4px rgba(45,136,255,0.28), 0 0 20px rgba(45,136,255,0.32);
}

/* Wrong-password / wrong-code / missing-field states — the backlight
   turns red instead of blue, and the field shakes once when a wrong
   value is actively rejected (not on every empty-field check). */
.field input.input-error{
  border-color:#d92d2d;
}

.field input.input-error:hover,
.field input.input-error:focus{
  box-shadow: 0 0 0 4px rgba(217,45,45,0.16);
}

.field.shake{
  animation: fieldShake .4s cubic-bezier(.36,.07,.19,.97) both;
}

@keyframes fieldShake{
  10%, 90% { transform:translateX(-1px); }
  20%, 80% { transform:translateX(2px); }
  30%, 50%, 70% { transform:translateX(-4px); }
  40%, 60% { transform:translateX(4px); }
}

@media (prefers-reduced-motion: reduce){
  .field.shake{ animation:none; }
}

.field-error{
  display:none;
  align-items:flex-start;
  gap:6px;
  margin:-6px 0 12px;
  padding:8px 10px;
  border-radius:6px;
  background:rgba(217,45,45,0.08);
  border:1px solid rgba(217,45,45,0.25);
  color:#c62828;
  font-size:12px;
  line-height:1.4;
}

.field-error.show{
  display:flex;
}

.field-error svg{
  flex:none;
  width:14px;
  height:14px;
  margin-top:1px;
}

[data-theme="dark"] .field-error{
  background:rgba(255,99,99,0.14);
  border-color:rgba(255,99,99,0.35);
  color:#ff9b9b;
}

.btn-continue{
  width:100%;
  height:39px;
  margin-top:8px;
  border:none;
  border-radius:6px;
  background:var(--primary);
  color:#fff;
  font-size:14px;
  font-weight:500;
  cursor:pointer;
}

.btn-continue:hover{
  background:var(--primary-dark);
}

/* ---------- Multi-step auth views (login / signup / password / device-trust)
   all share .form-block/.field/.btn-continue; exactly one is ever visible,
   swapped in via JS — no page navigation between steps. ---------- */

.auth-step{ display:none; }
.auth-step.view-active{ display:block; }

.field label{
  display:block;
  font-size:12px;
  font-weight:300;
  line-height:18px;
  color:var(--text-color);
  margin-bottom:4px;
}

.form-signup .field input::placeholder{
  color:var(--text-placeholder);
}

.field-row{
  display:flex;
  gap:16px;
  margin-bottom:12px;
}

.field-row .field{
  flex:1;
  margin-bottom:0;
  min-width:0;
}

/* Figma node 1:9332 — the "Dispatchpro#" prefix field is narrower than
   its "Tracking ID" partner (roughly a third of the row) rather than
   splitting the row evenly like the signup form's field-rows do. */
.field-row .field-narrow{
  flex:0 0 34%;
}

.terms-note{
  font-size:12px;
  line-height:1.5;
  color:var(--text-faint);
  text-align:center;
  margin:16px 0 0;
}

@media (max-width: 520px){
  .field-row{
    flex-direction:column;
    gap:12px;
  }
}

/* ---------- Password step (step 2 — email already captured, shown
   read-only, password entry with a show/hide toggle) ---------- */

.email-chip{
  width:100%;
  height:35px;
  padding:0 15px;
  display:flex;
  align-items:center;
  font-size:12px;
  font-weight:300;
  color:var(--text-strong);
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:6px;
  margin-bottom:12px;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.password-field{
  position:relative;
}

.password-field input{
  padding-right:38px;
}

.pw-toggle{
  position:absolute;
  top:50%;
  right:13px;
  transform:translateY(-50%);
  width:20px;
  height:20px;
  display:flex;
  align-items:center;
  justify-content:center;
  background:none;
  border:none;
  padding:0;
  cursor:pointer;
  opacity:0.6;
}

.pw-toggle:hover{ opacity:1; }

.pw-toggle img{
  width:13px;
  height:auto;
  display:block;
}

.forgot-link{
  display:inline-block;
  font-size:12px;
  color:var(--primary);
  margin:4px 0 20px;
}

/* ---------- Device-trust step (step 3 — offer to remember this device) ---------- */

.form-device{
  text-align:center;
}

.form-device h1{
  font-size:26px;
  font-weight:600;
  line-height:1.3;
  color:var(--heading);
  margin:0 0 16px;
}

.device-copy{
  font-size:14px;
  line-height:19px;
  color:var(--text-color);
  margin:0 0 32px;
}

.btn-secondary{
  width:100%;
  height:39px;
  margin-top:12px;
  border:1px solid var(--border);
  border-radius:6px;
  background:var(--surface);
  color:var(--heading);
  font-size:14px;
  font-weight:500;
  cursor:pointer;
}

.btn-secondary:hover{
  border-color:var(--text-color);
}

/* Figma nodes 1:547 / 1:546 (Tracking load customer) — a primary-
   colored outline button, ported to this project's own radius/type
   conventions instead of Figma's raw 10px radius + 16px semibold. */
.btn-outline{
  width:100%;
  height:39px;
  margin-top:12px;
  border:1px solid var(--primary);
  border-radius:6px;
  background:var(--surface);
  color:var(--primary);
  font-size:14px;
  font-weight:500;
  cursor:pointer;
  transition: background-color .2s ease;
}

.btn-outline:hover{
  background:color-mix(in srgb, var(--primary) 8%, transparent);
}

/* Figma node 1:537 — a plain centered divider label between two
   alternative actions. */
.divider-or{
  font-size:13px;
  color:var(--text-color);
  text-align:center;
  margin:16px 0 0;
}
.device-skip-link{
  display:block;
  text-align:center;
  font-size:12px;
  color:var(--primary);
  margin-top:20px;
}

/* ---------- Two-factor verification step (choose method → enter code
   → log in). All three sections live in one card that grows as each is
   revealed, matching the Figma frame's own progressive layout. ---------- */

.otp-card{
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:6px;
  padding:24px 18px 20px;
}

.otp-heading{
  font-size:14px;
  font-weight:500;
  line-height:1.42;
  color:var(--text-strong);
  text-align:center;
  margin:0 0 24px;
}

.otp-option{
  display:flex;
  align-items:center;
  gap:12px;
  border:1px solid var(--border);
  border-radius:6px;
  padding:0 16px;
  height:57px;
}

.otp-option + .otp-option{
  margin-top:12px;
}

.otp-option-icon{
  flex:none;
  width:35px;
  height:34px;
  display:flex;
  align-items:center;
  justify-content:center;
  border:0.5px solid var(--icon-border);
  border-radius:4px;
}

.otp-option-icon img{
  width:14px;
  height:auto;
  display:block;
}

/* The phone/mail glyphs are plain black shapes — invert them to white in
   dark mode rather than shipping a second set of icon assets. */
[data-theme="dark"] .otp-option-icon img{
  filter: invert(1);
}

.otp-option-text{
  flex:1;
  min-width:0;
}

.otp-option-label{
  font-size:12px;
  color:var(--text-strong);
  margin:0;
}

.otp-option-value{
  font-size:12px;
  color:var(--text-color);
  margin:2px 0 0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.btn-send-code{
  flex:none;
  width:81px;
  height:27px;
  border:none;
  border-radius:4px;
  background:var(--primary);
  color:#ffffff;
  font-size:12px;
  font-weight:500;
  letter-spacing:-0.288px;
  cursor:pointer;
}

.btn-send-code:hover{
  background:var(--primary-dark);
}

.otp-divider{
  border-top:1px solid var(--border);
  margin:20px 0 16px;
}

.otp-code-label{
  display:block;
  font-size:12px;
  color:var(--text-strong);
  margin:0 0 8px;
}

.otp-code-row{
  display:flex;
  gap:16px;
  margin-bottom:12px;
}

.otp-code-field{
  flex:1;
  margin-bottom:0;
}

.btn-verify{
  flex:none;
  width:81px;
  height:35px;
  border:none;
  border-radius:6px;
  background:var(--primary);
  color:#ffffff;
  font-size:12px;
  font-weight:500;
  letter-spacing:-0.288px;
  cursor:pointer;
}

.btn-verify:hover{
  background:var(--primary-dark);
}

.otp-retry-link{
  display:inline-block;
  font-size:12px;
  color:var(--primary);
}

@media (max-width: 480px){
  .otp-option{
    height:auto;
    padding:12px;
  }
  .otp-code-row{
    flex-direction:column;
  }
  .btn-verify{
    width:100%;
  }
}

.footer-logo{
  position:absolute;
  bottom:40px;
  height:40px;
}

.footer-logo img{
  height:100%;
  display:block;
}

/* The wordmark asset is inked for a white background — flatten it to
   solid white in dark mode rather than shipping a second logo file. */
[data-theme="dark"] .footer-logo img{
  filter: brightness(0) invert(1);
}

/* ---------- Light/dark theme toggle ---------- */

.theme-toggle{
  position:fixed;
  top:20px;
  right:20px;
  z-index:1000;
  width:44px;
  height:44px;
  border-radius:50%;
  border:1px solid var(--border);
  background:var(--surface);
  color:var(--text-color);
  display:flex;
  align-items:center;
  justify-content:center;
  cursor:pointer;
  box-shadow:0 2px 10px var(--toggle-shadow);
  transition: background-color .3s ease, border-color .3s ease, color .2s ease, transform .15s ease;
}

.theme-toggle:hover{
  color:var(--primary);
  transform:scale(1.06);
}

.theme-toggle:active{
  transform:scale(0.94);
}

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

/* Sun and moon are two full-size icons stacked on top of each other;
   switching theme cross-fades and rotates between them rather than
   hard-cutting, so the change itself reads as "this flipped" motion. */
.theme-icon{
  position:absolute;
  width:20px;
  height:20px;
  transition: opacity .35s ease, transform .5s cubic-bezier(.34,1.56,.64,1);
}

.theme-icon-sun{
  opacity:0;
  transform:rotate(-90deg) scale(0.4);
}

.theme-icon-moon{
  opacity:1;
  transform:rotate(0deg) scale(1);
}

[data-theme="dark"] .theme-icon-sun{
  opacity:1;
  transform:rotate(0deg) scale(1);
}

[data-theme="dark"] .theme-icon-moon{
  opacity:0;
  transform:rotate(90deg) scale(0.4);
}

@media (prefers-reduced-motion: reduce){
  .theme-icon{ transition:opacity .01ms !important; }
}

@media (max-width: 480px){
  .theme-toggle{
    top:12px;
    right:12px;
  }
}

@media (max-width: 860px){
  .split{ flex-direction:column; }
  .panel-left{
    flex:0 0 auto;
    max-width:100%;
    padding:48px 32px;
  }
  .panel-right{
    padding:48px 24px 96px;
  }
  .brand-wordmark-wrap{ width:calc(min(340px, 82%) * 1.3); }
  /* Narrow screens: back to the centered, wrapping full-width row — the
     indented single-line layout above would overflow here. */
  .badges{ width:calc(min(340px, 82%) * 1.3); margin:8px 0 0; justify-content:center; flex-wrap:wrap; row-gap:14px; }
  .footer-logo{ position:static; margin-top:40px; }
}

/* ==================== Boot animation ====================
   The original full-page boot overlay (played once on first landing,
   ahead of everything else) was removed at the user's direct request —
   .boot-screen/.boot-content live on only because #workspaceBootScreen
   (the post-login replay, still full-page/with its own progress bar)
   reuses them as-is. The first-landing merge now plays directly on the
   real .brand-wordmark-wrap instead of a separate overlay that handed
   off to it — see that rule's own comment, near .panel-left's other
   rules, for the full reveal chain (merge → text → enlarge → greeting
   → badges). The merge itself (.dp-lockup and friends) was rebuilt
   twice: first from an SVG-mask technique that rendered nothing at all
   across every browser tested, then from a version that split the
   whole lockup (icon + text together) instead of just the icon mark —
   see .dp-lockup's own comment for the current version. */

.boot-screen{
  position:fixed;
  inset:0;
  z-index:1000;
  display:flex;
  align-items:center;
  justify-content:center;
  background-image: linear-gradient(134.47deg, var(--primary) 1.5%, var(--primary-dark) 98.5%);
  background-size:200% 200%;
  /* A true sine ease instead of the default (slightly mechanical) quad
     ease-in-out — reads as a smoother, more organic breathing motion
     for a slow ambient loop like this. */
  animation: bgPan 6s cubic-bezier(.445,.05,.55,.95) infinite alternate;
  transition: opacity .6s cubic-bezier(.16,1,.3,1), transform .6s cubic-bezier(.16,1,.3,1);
}

.boot-screen.boot-done{
  opacity:0;
  transform: scale(1.02);
  pointer-events:none;
}

/* #workspaceBootScreen sits on this same class but starts out [hidden] —
   needs its own rule since .boot-screen's own display:flex above would
   otherwise outrank the UA's default [hidden] styling (same 0,1,0
   specificity, but author beats user-agent regardless). */
.boot-screen[hidden]{
  display:none;
}

.boot-content{
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
}

/* Rebuilt from scratch, three times now.
   Pass 1: the original SVG <mask> technique rendered nothing at all
   across every browser tested.
   Pass 2: two full copies of the whole lockup asset (icon + text
   together), each clip-path'd to its own half and slid together —
   worked, but split the WHOLE lockup, not just the icon.
   Pass 3 (current): traced from a reference recording the user
   provided of the original full-page boot screen's own crossfade.
   .dp-icon-window crops the single asset down to just its icon region
   (x:[0,37] of the asset's own 168.6-wide viewBox — see the --dp-*
   custom properties near :root) using plain calc() math, no image
   editing and no second asset — and THAT crop, centered in
   .brand-wordmark-wrap, is what splits into the "d"/"p" halves and
   merges (see .dp-icon-crop-top/-bottom below for the actual seam —
   it's a vertical cut down the middle, matched by hand against a
   screenshot of the resting icon). Once merged it holds briefly,
   then crossfades: it fades out while
   .dp-full-lockup — the complete, uncropped asset (icon + "DISPATCHPRO"
   together) — fades in over the SAME centered point, growing from 77%
   to its full (already 30%-enlarged) size, exactly like the
   reference's small-icon-dissolves-into-big-wordmark beat. */
.dp-lockup{
  position:relative;
  aspect-ratio: var(--dp-logo-w) / var(--dp-logo-h);
}

/* Window is deliberately much taller than the icon itself (300% of the
   whole lockup's height, re-centered with top:50%/translate(-50%,-50%))
   so the two halves below have real headroom to travel down a visible
   vertical line before they meet — at the old 100%-tall window they had
   nowhere to travel FROM, since anything beyond the icon's own tiny
   bounding box was clipped away instantly. overflow:hidden here still
   does its original job of cropping the wide asset down to just the
   icon horizontally; it just no longer doubles as a vertical clip. */
.dp-icon-window{
  position:absolute;
  top:50%;
  /* Was 50% (dead center of the whole "DISPATCHPRO" wordmark box) —
     the merge visibly played in the middle of the lockup instead of
     where the icon actually rests. --dp-icon-seam is already the
     icon's own horizontal center as a % of the full asset width (see
     its :root comment), which is exactly .dp-lockup's coordinate
     space too, so reusing it here lands the merge dead-center on the
     icon's resting spot instead. */
  left:var(--dp-icon-seam);
  width: calc(100% * var(--dp-icon-w) / var(--dp-logo-w));
  height:300%;
  overflow:hidden;
  pointer-events:none;
  transform:translate(-50%,-50%);
  transition: opacity .4s cubic-bezier(.16,1,.3,1), transform .5s cubic-bezier(.16,1,.3,1);
}

/* Used to fade out here once .dp-full-lockup crossfaded in over the
   same spot — now the icon stays merged and visible permanently, and
   .dp-text-window (below) fades the "DISPATCHPRO" text in beside it
   instead, so there's no rule needed here any more. */

/* height is aspect-ratio-driven (not 100% of the now-taller window)
   so enlarging .dp-icon-window above for travel room doesn't stretch
   the logo — the crop keeps its true, undistorted proportions and is
   simply re-centered with top:50%/translateY(-50%) inside the taller
   box. */
.dp-icon-crop{
  position:absolute;
  top:50%;
  left:0;
  width: calc(100% * var(--dp-logo-w) / var(--dp-icon-w));
  height:auto;
  aspect-ratio: var(--dp-logo-w) / var(--dp-logo-h);
  display:block;
  filter:brightness(0) invert(1) blur(3px);
  opacity:0;
  pointer-events:none;
  will-change:transform, opacity, filter;
  transition: opacity 1s cubic-bezier(.16,1,.3,1), transform 1s cubic-bezier(.16,1,.3,1), filter 1s cubic-bezier(.16,1,.3,1);
}

/* The "d" and "p" sit side by side in the actual icon (confirmed
   against a screenshot with the seam marked by hand) — so the cut
   between the two pieces has to be a VERTICAL line down the middle,
   not a horizontal one. Motion stays vertical though, per the
   original "coming from top and bottom" request: the "d" (left half)
   travels straight down that seam from well above, the "p" (right
   half) travels straight up from well below, both landing on the
   exact same point at translateY(-50%) — dead center, seam and all.
   A slight scale-in (.86→1, riding the same transition as the
   translateY above) plus a touch of blur that clears as they land
   gives the arrival some weight instead of a flat linear slide.

   The seam is a soft mask-image gradient, NOT clip-path — a hard
   clip-path edge cut straight across the icon's rounded strokes and
   read as an ugly flat/square edge where the letterform got
   truncated. The gradient feathers across --dp-icon-feather on
   either side of --dp-icon-seam so the two halves blend into each
   other instead of butting up against a visible seam. --dp-icon-seam
   itself is NOT 50% — .dp-icon-crop's own box is the FULL asset
   (icon + "DISPATCHPRO" text) stretched out to calc(100% * dp-logo-w
   / dp-icon-w) so only the icon's own 22%-ish slice survives
   .dp-icon-window's overflow:hidden crop (see that comment); a plain
   50% here would land deep in the text, not the icon's own center.

   -top and -bottom's ramps BOTH span the full [seam-feather,
   seam+feather] zone (not two adjacent ramps that meet at the seam)
   so their opacities sum to exactly 1 at every point in between —
   top fades 1→0 left-to-right across the zone, bottom fades 0→1
   across that SAME zone. Two adjacent ramps that each hit 0 right at
   the shared seam point (the previous version) left a genuine
   zero-opacity hairline permanently visible down the merged icon's
   center, even once fully landed — this is what actually fixes that,
   not just what avoided the earlier hard clip-path edge. Since both
   crops are the same image at the same position, overlapping them in
   the blend zone reveals identical pixel content, not a ghost. */
.dp-icon-crop-top{
  -webkit-mask-image: linear-gradient(to right, #000 0%, #000 calc(var(--dp-icon-seam) - var(--dp-icon-feather)), transparent calc(var(--dp-icon-seam) + var(--dp-icon-feather)));
  mask-image: linear-gradient(to right, #000 0%, #000 calc(var(--dp-icon-seam) - var(--dp-icon-feather)), transparent calc(var(--dp-icon-seam) + var(--dp-icon-feather)));
  transform: translateY(calc(-50% - 150px)) scale(.86);
}

.dp-icon-crop-bottom{
  -webkit-mask-image: linear-gradient(to right, transparent calc(var(--dp-icon-seam) - var(--dp-icon-feather)), #000 calc(var(--dp-icon-seam) + var(--dp-icon-feather)), #000 100%);
  mask-image: linear-gradient(to right, transparent calc(var(--dp-icon-seam) - var(--dp-icon-feather)), #000 calc(var(--dp-icon-seam) + var(--dp-icon-feather)), #000 100%);
  transform: translateY(calc(-50% + 150px)) scale(.86);
}

/* Both contexts that use these crops (the panel-left reveal via
   .brand-in, and #workspaceBootScreen's own .merge-in — see each
   one's own trigger script) land on the same end state. */
.brand-block.brand-in .dp-icon-crop-top,
.brand-block.brand-in .dp-icon-crop-bottom,
#workspaceBootScreen.merge-in .dp-icon-crop-top,
#workspaceBootScreen.merge-in .dp-icon-crop-bottom{
  opacity:1;
  transform: translateY(-50%) scale(1);
  filter:brightness(0) invert(1) blur(0px);
  transition-delay:.15s;
}

/* The login panel's icon merge deliberately keeps the shared 1s/.15s
   timing above — only the "DISPATCHPRO" text beside it runs the long
   fade (see .brand-block .dp-text-window below). */

/* Login panel only: the icon is enlarged so its bottom reaches roughly
   where the badge row used to end (the badges now tuck under the text —
   see .badges). The crop inside sizes itself off this window's width,
   so widening the window is what scales the icon: +30px width ≈ +25px
   height. top nudges the center down half that growth so the TOP edge
   stays put and all the growth reads downward; left shifts 7px so the
   wider icon's right edge clears the "D" of the text with ~2px of
   extra breathing room. */
.brand-block .dp-icon-window{
  width: calc(100% * var(--dp-icon-w) / var(--dp-logo-w) + 30px);
  top: calc(50% + 13px);
  left: calc(var(--dp-icon-seam) - 7px);
}

/* Keep the merge flare centered on the enlarged, shifted icon above. */
.brand-block .dp-flash{
  top: calc(50% + 13px);
  left: calc(var(--dp-icon-seam) - 7px);
  height:230%;
}

/* Used to be a JS-added class that nudged the merged icon to a bigger
   scale right before .dp-full-lockup crossfaded in over it — now the
   icon just holds at its merged, already-correct resting size (see
   .dp-icon-window's own comment) and .dp-text-window (below) fades
   the text in beside it instead, so that bridging nudge and its
   class-toggle CSS are no longer needed. */

/* Kept only as the prefers-reduced-motion / no-JS fallback now — see
   that media query and the <noscript> block near the end of <style>.
   In the normal, animated path it stays permanently hidden: the icon
   (.dp-icon-window, merged and left in place) and the text
   (.dp-text-window below) are what's actually visible instead of
   ever crossfading to this single combined image. */
.dp-full-lockup{
  position:absolute;
  top:50%;
  left:50%;
  width:100%;
  height:auto;
  display:block;
  filter:brightness(0) invert(1);
  opacity:0;
  pointer-events:none;
  transform:translate(-50%,-50%) scale(.77);
  transition: opacity .8s cubic-bezier(.16,1,.3,1), transform .8s cubic-bezier(.16,1,.3,1);
}

/* Crops the SAME lockup asset down to just the "DISPATCHPRO" text
   region (x:[--dp-text-x, --dp-logo-w] of the asset's own viewBox —
   see the --dp-* custom properties near :root), mirroring
   .dp-icon-window/.dp-icon-crop's crop technique but for the other
   half of the asset. Anchored to its own LEFT edge (not centered)
   so it sits flush against the icon's right edge, and fades in with
   a short rightward slide once the icon has finished merging and
   held — the icon itself never moves or swaps images, this is the
   only piece that animates in at this stage. */
.dp-text-window{
  position:absolute;
  top:0;
  left: calc(100% * var(--dp-text-x) / var(--dp-logo-w));
  width: calc(100% * (var(--dp-logo-w) - var(--dp-text-x)) / var(--dp-logo-w));
  height:100%;
  overflow:hidden;
  pointer-events:none;
  opacity:0;
  transform:translateX(10px);
  transition: opacity .8s cubic-bezier(.16,1,.3,1), transform .8s cubic-bezier(.16,1,.3,1);
}

.brand-block.brand-in .dp-text-window,
#workspaceBootScreen.merge-in .dp-text-window{
  opacity:1;
  transform:translateX(0);
  transition-delay:1.15s;
}

/* Login panel only (not #workspaceBootScreen): the "DISPATCHPRO" text
   is a pure fade — no rightward slide, so the resting transform matches
   the .brand-in end state and only opacity animates — starting with the
   icon merge (.125s, overriding the shared 1.15s above) but running its
   own slow 3s fade while the 1s merge plays and lands beside it.
   A near-linear timing function on purpose: the shared ease-out curve
   front-loads the opacity change so a long fade still LOOKS done in the
   first second — this keeps it visibly fading the whole 3s. */
.brand-block .dp-text-window{
  transform:translateX(0);
  transition-duration:3s, 3s;
  transition-timing-function:cubic-bezier(.4,.1,.6,.9), cubic-bezier(.4,.1,.6,.9);
}

.brand-block.brand-in .dp-text-window{
  transition-delay:.125s;
}

/* Same "full asset stretched out, then offset so only the target
   slice survives the window's overflow:hidden crop" technique as
   .dp-icon-crop — except this slice doesn't start at the asset's own
   x:0 like the icon's does, so the image also needs a negative left
   offset to pull that starting point back to the window's local x:0
   (see the --dp-text-x calc() math above for the window itself; this
   is the same ratio, just solved for the image's own offset). */
.dp-text-crop{
  position:absolute;
  top:50%;
  left: calc(-100% * var(--dp-text-x) / (var(--dp-logo-w) - var(--dp-text-x)));
  width: calc(100% * var(--dp-logo-w) / (var(--dp-logo-w) - var(--dp-text-x)));
  height:auto;
  aspect-ratio: var(--dp-logo-w) / var(--dp-logo-h);
  display:block;
  filter:brightness(0) invert(1);
  transform:translateY(-50%);
  pointer-events:none;
}

/* Traced from the reference video: right as the two icon halves land,
   there's a bright vertical flare/backlight through the merge point —
   a thin glowing streak, not a full-screen flash. A single class
   toggle can only transition TOWARD one target, so it can't rise then
   fall on its own; the JS trigger (see each context's own script)
   adds a second, later "flash-out" class once the flash has had time
   to fully ignite, which is what pulls it back down to 0. */
.dp-flash{
  position:absolute;
  top:50%;
  /* Same fix as .dp-icon-window above — the backlight has to ignite
     right at the icon's own merge seam, not the middle of the whole
     wordmark. */
  left:var(--dp-icon-seam);
  width:3px;
  height:190%;
  background:linear-gradient(to bottom,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,.85) 42%,
    #fff 50%,
    rgba(255,255,255,.85) 58%,
    rgba(255,255,255,0) 100%);
  filter:blur(1.5px);
  opacity:0;
  pointer-events:none;
  mix-blend-mode:screen;
  transform:translate(-50%,-50%) scaleY(.35);
  transition: opacity .2s cubic-bezier(.16,1,.3,1), transform .4s cubic-bezier(.16,1,.3,1);
}

.brand-block.brand-in .dp-flash,
#workspaceBootScreen.merge-in .dp-flash{
  opacity:1;
  transform:translate(-50%,-50%) scaleY(1);
  transition-delay:.85s;
}

.brand-block.brand-in.flash-out .dp-flash,
#workspaceBootScreen.merge-in.flash-out .dp-flash{
  opacity:0;
  transition: opacity .35s cubic-bezier(.16,1,.3,1);
  transition-delay:0s;
}

/* .boot-subtitle, .boot-features and .boot-feature (the first screen's
   "Welcome to Pros" line and feature row) were removed along with
   #bootScreen itself — nothing else references them.
   #workspaceBootScreen kept its own .ws-loader instead of a
   .boot-loader class from the start, so no replacement was needed
   there. */

.boot-loader-track{
  width:100%;
  height:4px;
  border-radius:999px;
  background:rgba(255,255,255,0.18);
  overflow:hidden;
}

.boot-loader-fill{
  width:0%;
  height:100%;
  border-radius:999px;
  background:#ffffff;
}

.boot-loader-label{
  display:block;
  margin-top:14px;
  font-size:12px;
  font-weight:500;
  letter-spacing:0.4px;
  color:rgba(255,255,255,0.65);
}

.boot-loader-label span{
  color:rgba(255,255,255,0.95);
  font-weight:600;
}

.boot-skip{
  position:absolute;
  bottom:32px;
  right:36px;
  font-size:13px;
  font-weight:500;
  color:rgba(255,255,255,0.7);
  background:rgba(255,255,255,0.1);
  border:1px solid rgba(255,255,255,0.18);
  border-radius:8px;
  padding:8px 16px;
  cursor:pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.boot-skip:hover{
  color:#fff;
  background:rgba(255,255,255,0.16);
}

/* Not from Figma — added at the user's direct request: a second
   playthrough of the same boot animation (reusing .boot-screen and the
   .dp-lockup pieces as-is) right after a successful login, before the
   Command Center appears. Sized at the full 650px/80vw hero scale since
   this one has the whole viewport to itself, same as the original
   full-page treatment did — no separate 30%-enlarge step here, that
   was specific to the panel-left reveal (see .brand-wordmark-wrap). */
#workspaceBootScreen .dp-lockup{
  width:min(650px, 80vw);
}

.ws-loader{
  width:220px;
  opacity:0;
  transform:translateY(14px);
  will-change:transform, opacity;
  transition: opacity .6s ease, transform .6s cubic-bezier(.22,.61,.36,1);
}

#workspaceBootScreen.merge-in .ws-loader{
  opacity:1;
  transform:translateY(0);
  transition-delay:2.1s;
}

@keyframes bgPan{
  0%{ background-position:0% 50%; }
  100%{ background-position:100% 50%; }
}

@media (prefers-reduced-motion: reduce){
  .boot-screen{ animation:none; }
  .ws-loader,
  .brand-wordmark-wrap,
  .badge{
    transition:none !important; opacity:1 !important; transform:none !important;
  }

  /* .dp-full-lockup is deliberately NOT in the group above.
     It is position:absolute at top:50%/left:50% and depends on
     translate(-50%,-50%) to centre itself - transform:none does not just
     stop an animation here, it moves the element, dropping its top-left
     corner onto the box's centre so the wordmark shifts down and right and
     lands across the feature badges. Losing scale(.77) also renders it ~30%
     oversized, which is why it spilled outside its own box.
     Keep the centring; neutralise only the animated part. */
  .dp-full-lockup{
    transition:none !important;
    opacity:1 !important;
    transform:translate(-50%,-50%) !important;
  }
  .dp-icon-window{ transition:none !important; opacity:0 !important; }
  .dp-text-window{ transition:none !important; opacity:0 !important; }
  .dp-flash{ transition:none !important; opacity:0 !important; }
  .brand-wordmark-wrap{ width:calc(min(460px, 92%) * 1.3) !important; }
}

/* ==================== Island additions (not in the design file) ====
   Small pieces the real backend needs that the prototype didn't have. */

/* The read-only email on the password step is a button here so it doubles
   as "use a different email" — same chrome as the design's .email-chip. */
.email-chip{
  gap:10px;
  font-family:inherit;
  text-align:left;
  cursor:pointer;
}

.email-chip:hover{
  border-color:var(--primary);
}

.email-chip-value{
  flex:1;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.email-chip-change{
  flex:none;
  font-size:11px;
  font-weight:500;
  color:var(--primary);
}

/* Forgot-password link and the company-admin "Keep me signed in" checkbox
   share one row under the password field. */
.pw-row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:12px;
  margin:4px 0 20px;
}

.pw-row .forgot-link{
  margin:0;
}

.remember-check{
  display:flex;
  align-items:center;
  gap:6px;
  font-size:12px;
  color:var(--text-color);
  cursor:pointer;
  user-select:none;
}

.remember-check input{
  width:14px;
  height:14px;
  margin:0;
  accent-color:var(--primary);
  cursor:pointer;
}

/* Success flash (e.g. "password reset") — the green twin of .field-error. */
.field-notice{
  display:flex;
  align-items:flex-start;
  gap:6px;
  margin:0 0 16px;
  padding:8px 10px;
  border-radius:6px;
  background:rgba(26,156,83,0.08);
  border:1px solid rgba(26,156,83,0.25);
  color:#1a7f47;
  font-size:12px;
  line-height:1.4;
}

.field-notice svg{
  flex:none;
  width:14px;
  height:14px;
  margin-top:1px;
}

[data-theme="dark"] .field-notice{
  background:rgba(26,156,83,0.14);
  border-color:rgba(26,156,83,0.35);
  color:#7fdcaa;
}

/* Privacy / SMS terms links pinned under whichever step is showing. */
.auth-footer-links{
  flex:none;
  margin:24px 0 0;
  font-size:12px;
  color:var(--text-faint);
  text-align:center;
}

.auth-footer-links a:hover{
  color:var(--primary);
}

/* The tab switcher on the 2FA / password pages is plain links back to the
   login page rather than in-page buttons. */
.auth-tab-link{
  display:flex;
  align-items:center;
  justify-content:center;
  text-decoration:none;
}

/* Authenticator-app option uses an inline SVG instead of a PNG glyph. */
.otp-option-icon svg{
  width:16px;
  height:16px;
  color:var(--text-strong);
}

/* "We texted a code…" / "Verified" line under the method list. */
.otp-status{
  font-size:12px;
  color:var(--text-color);
  margin:14px 0 0;
  text-align:center;
}

.otp-status.is-success{
  color:#1a9c53;
  font-weight:500;
}

.otp-status[hidden]{
  display:none;
}

.otp-code-field input{
  letter-spacing:2px;
}

.btn-send-code[disabled],
.btn-verify[disabled],
.btn-continue[disabled]{
  opacity:0.6;
  cursor:default;
}

.form-block .field label{
  font-weight:400;
  color:var(--text-strong);
}

/* Footer wordmark sits in normal flow above the Privacy/SMS links (the
   prototype pinned it absolutely, which would collide with those links). */
.footer-logo{
  position:static;
  flex:none;
  height:34px;
  margin:32px auto 0;
  display:block;
}

.auth-footer-links{
  margin-top:14px;
}

@media (max-width: 860px){
  .footer-logo{ margin-top:32px; }
}
