/*
Name: base.css
Author: @billz
Author URI: https://github.com/billz
Description: Base classes with minimal theming for the application
License: GNU General Public License v3.0
*/

:root {
  --raspap-brand-color: #155EEF;
  --raspap-content-main: #495057;
  --raspap-text-muted: #858796;
  --raspap-text-light: #999999;
  --raspap-offwhite: #faf9f6;
}

body {
  color: #212529;
  background-color: var(--raspap-offwhite);
}

/* Typography */
a {
  --bs-link-color: var(--raspap-brand-color);
  --bs-link-hover-color: var(--raspap-brand-color);
  text-decoration: none;
}

.info-item {
  text-transform: uppercase;
  flex-basis: 125px;
  color: var(--raspap-text-muted);
}

.info-item, .info-value {
  font-size: 0.9rem;
}

.info-item-xs {
  font-size: 0.7rem;
  margin-left: 0.75rem;
}

/* Connected-clients list (templates/connected_clients.php). Deliberately not
   reusing .info-item-xs here: its 0.75rem left margin is meant for indenting
   detail under a table cell, and would push the right-hand traffic column out
   of alignment with itself. */
.client-list .client-row {
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--bs-border-color, #e3e6f0);
}
.client-list .client-row:last-child {
  border-bottom: 0;
}
.client-meta {
  font-size: 0.72rem;
  line-height: 1.5;
}
/* min-width:0 is what actually lets text-truncate work inside a flex child -
   without it the item refuses to shrink below its content width and a long
   hostname pushes the traffic column off the card instead of ellipsising. */
.client-ident {
  min-width: 0;
  flex: 1 1 auto;
}
.client-traffic {
  flex: 0 0 auto;
}

.page-card-header, .modal-title {
  font-size: 16px;
}

.text-offwhite {
  color: var(--raspap-offwhite);
}

/* Elements */
.table {
  margin-bottom: 0rem;
}

.logoutput {
  width: 100%;
  height: 20rem;
  border: 1px solid #d1d3e2;
  border-radius: .35rem;
  font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace;
  font-size: 0.8rem;
}

pre.unstyled {
  border-width: 0;
  background-color: transparent;
  padding: 0;
}

figcaption.figure-caption a {
  color: var(--raspap-text-muted);
}

/* Icons */
.fa-reddit {
  color: #ff4500;
}
.fa-twitter {
  color: #55acee
}
.fa-discord {
  color: #7289da
}
.fa-github {
  color: #151b23
}

.fa-check {
  color: #90ee90;
}

.fa-times {
  color: #ff4500;
}

button > i.fas {
  pointer-events: none;
}

.heart {
  color: #e63946;
  animation: heart 1000ms infinite;
}
@keyframes heart {
  0%, 40%, 80%, 100% {
    transform: scale(1);
  }
  20%, 60% {
    transform: scale(1.15);
  }
}

/* Buttons */
button.btn.btn-light.js-toggle-password {
  border: 1px solid lightgrey;
}

button.btn i:not([class*="service-status"]) {
  color: inherit !important;
}

/* Forms */
.was-validated .form-control:valid,
.was-validated .form-control:invalid {
  background-position: center right calc(.375em + .4875rem);
}

.was-validated .form-control:invalid {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}

.was-validated .form-control:valid {
  background-size: calc(0.6em + 0.375rem) calc(0.6em + 0.375rem);
}

.input-group > .btn {
  border: 1px solid #ced4da;
}

.input-group > .input-group-append:not(:last-child) > .btn {
  border-top-right-radius: 0.35rem;
  border-bottom-right-radius: 0.35rem;
}

.check-hidden {
  visibility: hidden;
}
.check-progress {
  color: var(--raspap-text-light);
}

/* Layout */
.sb-sidenav .sb-sidenav-menu .nav .nav-link {
  padding-top: 0.6rem;
  padding-bottom: 0.6rem;
  font-size: 16px;
}
.sidebar .nav-item .nav-link {
  padding: 0.6rem 0.6rem 0.6rem 1.0rem;
}

.sidebar.toggled .nav-item .nav-link span {
  display: none;
}
.sidebar .nav-item .nav-link i,
.sidebar .nav-item .nav-link span {
    font-size: 1.0rem;
}

.sb-sidenav .sb-nav-link-icon.active .nav-link {
  font-weight: bolder;
  color: var(--raspap-brand-color);
}
.sb-sidenav .sb-nav-link-icon.active i.sb-nav-link-icon {
  color: var(--raspap-brand-color);
}

.sidebar-brand-text {
  text-transform: none;
  font-size: 2.0rem;
  font-weight: 500;
  font-family: Helvetica, Arial, sans-serif;
}

.h-underlined {
  border-bottom: 1px solid #e3e6f0;
  padding-bottom: 0.3rem;
}

/* Brand block at the top of the sidebar (renderStatus() in
   includes/functions.php). The sidebar is 225px wide, so the mark is capped
   at 150px and centred, leaving even breathing room either side rather than
   the old 70px mark hugging the left edge next to the status list. Sized in
   CSS rather than width/height attributes so the collapsed 68px rail can
   override it; app/img/firefly-logo.png is 512px square, so it stays sharp
   at 150px even on a 2x display. */
.sidebar-brand-block {
  display: flex;
  justify-content: center;
  padding: 1rem 0 0.75rem;
}

.navbar-logo {
  width: 150px;
  height: auto;
  max-width: 100%;
}

#navbar-system-mode {
  color: var(--raspap-text-muted);
}
#navbar-system-mode.active {
  color: var(--raspap-brand-color);
}

.login-logo {
  width: 70px;
  height: 70px;
  margin-left: 1.2rem;
  margin-bottom: -0.5rem;
}

.nav-item {
  font-size: 0.85rem;
}

.nav-user {
  position: relative;
  bottom: 11px;
  right: 3px;
}

.topbar-avatar {
  width: 38.4px!important;
  height: 38.4px!important;
}
@media (min-width: 641px) {
  .topbar-avatar {
    width: 48px!important;
    height: 48px!important;
  }
}

.page-header {
  font-size: 26pt;
  margin: 20px 0 20px;
}

.close {
  font-weight: 400;
  font-size: 1.3rem;
}

/* Improved progress bar */
.progress.progress-improved {
  position: relative;
  overflow: hidden;
}
.progress.progress-improved .progress-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  align-items: center;
  justify-content: center;
  clip-path: inset(0 calc(100% - var(--progress)) 0 0);
}
.progress.progress-improved .progress-bar span {
  position: relative;
  z-index: 2;
  color: white;
}
.progress.progress-improved[data-text]::before {
  content: attr(data-text);
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: black;
  z-index: 0;
}

/* --- Page Specific --- */
.nav-tabs-wrapper {
  width: 100%;
  overflow-x: auto;
}
.nav-tabs-wrapper::-webkit-scrollbar {
  display: none;
}
.nav-tabs-wrapper > .nav-tabs {
  flex-wrap: nowrap;
  min-width: 100%;
  width: min-content;
}
.nav-tabs .nav-link.active,
.nav-tabs .nav-link {
  font-size: 1.0rem;
}
.nav-tabs a.nav-link {
  color: #6e707e;
  white-space: nowrap;
}

/* Service Status */
.service-status {
  border-width: 0;
  align-items: center;
}

.service-status-up {
  color: #22C55E !important;
}

.service-status-warn {
  color: #F59E0B !important;
}

.service-status-down {
  color: #EF4444 !important;
}

.led-pulse {
  opacity: 0.3 !important;
}

.hostapd-led {
  color: #28a745;
  opacity: 1;
  transition: opacity 0.05s;
}


/* ==========================================================================
   Sign-in screen (templates/login.php)

   Two panels side by side: a brand panel and a sign-in card. Below
   900px they stack, with the brand panel shrinking to a compact header
   so the card stays above the fold on a phone. Colours are declared as
   locals on .login-shell so this whole block stays self-contained and
   can't leak into the rest of the admin panel.
   ========================================================================== */
#modal-admin-login .modal-content {
  border: none;
  background: transparent;
}

#modal-admin-login .modal-body {
  padding: 0;
}

.login-shell {
  /* Ink/accent/line now come from the --ff-* root tokens (see "Firefly
     surface system" at the foot of this file) so the sign-in screen and the
     dashboard cannot drift apart. Only the panel gradient below is unique to
     this screen. */
  --login-panel-from: #eaf2ff;
  --login-panel-via: #cfe3ff;
  --login-panel-to: #9ec8ff;

  display: flex;
  min-height: 100vh;
  min-height: 100dvh;
  /* One continuous field of colour across both halves - the sign-in card
     floats on top of it rather than sitting in its own hard-edged
     column, which is what gives the screen its depth. */
  background: linear-gradient(150deg, var(--login-panel-from) 0%, var(--login-panel-via) 55%, var(--login-panel-to) 100%);
}

/* --- brand panel --- */
.login-brandpanel {
  position: relative;
  flex: 0 0 46%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem 2.5rem;
  overflow: hidden;
}

/* Decorative light-streak waves. Purely presentational - marked
   aria-hidden in the template and never carries meaning. */
.login-brandpanel-bg {
  position: absolute;
  inset: 0;
  opacity: .75;
  pointer-events: none;
}

.login-brandpanel-bg svg {
  width: 100%;
  height: 100%;
  display: block;
}

.login-brand-inner {
  position: relative;
  text-align: center;
  max-width: 380px;
}

.login-firefly {
  width: clamp(104px, 13vw, 150px);
  height: auto;
  margin-bottom: .75rem;
}

.login-wordmark {
  font-size: clamp(1.75rem, 3.1vw, 2.6rem);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.1;
}

.login-wordmark-brand { color: var(--login-ink); }
.login-wordmark-model { color: var(--login-accent); }

.login-tagline {
  margin: .6rem 0 0;
  color: var(--login-ink-soft);
  font-size: .95rem;
}

/* --- sign-in panel --- */
.login-formpanel {
  flex: 1 1 54%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2.5rem;
}

.login-card {
  width: 100%;
  max-width: 460px;
  padding: 2.75rem 2.5rem;
  border-radius: 1.25rem;
  background: #ffffff;
  box-shadow: 0 1px 2px rgba(13, 27, 62, .04),
              0 12px 32px rgba(13, 27, 62, .10),
              0 40px 80px rgba(13, 27, 62, .06);
}

.login-badge {
  width: 96px;
  height: 96px;
  margin: 0 auto 1.25rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--login-accent);
  font-size: 2.25rem;
  background: radial-gradient(circle, #eaf3ff 0%, #f6faff 70%, #ffffff 100%);
  box-shadow: 0 0 0 1px #e6efff inset, 0 0 0 10px rgba(22, 104, 234, .04);
}

.login-heading {
  margin: 0;
  text-align: center;
  font-size: clamp(1.6rem, 2.6vw, 2rem);
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--login-ink);
}

.login-subheading {
  margin: .4rem 0 1.75rem;
  text-align: center;
  color: var(--login-ink-soft);
  font-size: .95rem;
}

.login-alert {
  display: flex;
  align-items: center;
  margin-bottom: 1.25rem;
  padding: .7rem .9rem;
  border-radius: .6rem;
  border: 1px solid #f3c2c2;
  background: #fdf1f1;
  color: #a52020;
  font-size: .9rem;
}

.login-field { margin-bottom: 1.1rem; }

.login-label {
  display: block;
  margin-bottom: .4rem;
  font-size: .9rem;
  font-weight: 600;
  color: var(--login-ink);
}

.login-input-wrap { position: relative; }

.login-input-icon {
  position: absolute;
  top: 50%;
  left: 1rem;
  transform: translateY(-50%);
  color: #9aa4c2;
  font-size: .95rem;
  pointer-events: none;
}

/* Override Bootstrap's .form-control sizing rather than fighting it
   with !important - these are more specific selectors. */
.login-input.form-control {
  height: 54px;
  padding: 0 1rem 0 2.85rem;
  border: 1px solid var(--login-line);
  border-radius: .7rem;
  font-size: 1rem;
  color: var(--login-ink);
  background-color: #fff;
  transition: border-color .15s ease, box-shadow .15s ease;
}

.login-input.form-control::placeholder { color: #a6b0cc; }

.login-input.form-control:focus {
  border-color: var(--login-accent);
  box-shadow: 0 0 0 4px rgba(22, 104, 234, .12);
}

.login-input-password.form-control { padding-right: 3rem; }

.login-eye {
  position: absolute;
  top: 50%;
  right: .55rem;
  transform: translateY(-50%);
  width: 2.2rem;
  height: 2.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: .5rem;
  background: transparent;
  color: #9aa4c2;
  cursor: pointer;
}

.login-eye:hover { color: var(--login-ink-soft); }

.login-eye:focus-visible {
  outline: 2px solid var(--login-accent);
  outline-offset: 1px;
}

.login-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 0 1.5rem;
}

.login-remember { margin: 0; }

.login-remember .form-check-input {
  width: 1.1rem;
  height: 1.1rem;
  margin-top: .1rem;
  border-color: #c3cde6;
  cursor: pointer;
}

.login-remember .form-check-input:checked {
  background-color: var(--login-accent);
  border-color: var(--login-accent);
}

.login-remember .form-check-label {
  padding-left: .15rem;
  color: var(--login-ink-soft);
  font-size: .92rem;
  cursor: pointer;
}

.login-forgot {
  color: var(--login-accent);
  font-size: .92rem;
  font-weight: 500;
  text-decoration: none;
}

.login-forgot:hover { text-decoration: underline; }

.login-submit {
  width: 100%;
  height: 54px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  border: 0;
  border-radius: .7rem;
  font-size: 1.02rem;
  font-weight: 600;
  color: #fff;
  cursor: pointer;
  background: linear-gradient(180deg, var(--login-accent) 0%, var(--login-accent-deep) 100%);
  box-shadow: 0 6px 16px rgba(16, 80, 200, .28);
  transition: filter .15s ease, box-shadow .15s ease, transform .05s ease;
}

.login-submit:hover {
  filter: brightness(1.06);
  box-shadow: 0 8px 20px rgba(16, 80, 200, .34);
}

.login-submit:active { transform: translateY(1px); }

.login-submit:focus-visible {
  outline: 2px solid var(--login-accent-deep);
  outline-offset: 2px;
}

.login-submit i { font-size: .95rem; }

/* --- stacked layout --- */
@media (max-width: 900px) {
  .login-shell { flex-direction: column; }

  .login-brandpanel {
    flex: 0 0 auto;
    padding: 2rem 1.5rem 1.75rem;
  }

  .login-brand-inner { max-width: none; }

  .login-firefly {
    width: 84px;
    margin-bottom: .4rem;
  }

  .login-tagline { display: none; }

  .login-formpanel {
    flex: 1 1 auto;
    align-items: flex-start;
    padding: 0 1rem 2.5rem;
  }

  .login-card {
    padding: 2rem 1.5rem;
    border-radius: 1rem;
  }

  .login-badge {
    width: 72px;
    height: 72px;
    font-size: 1.7rem;
    margin-bottom: 1rem;
  }
}

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

/* --- dark mode --- */
html[data-bs-theme="dark"] .login-shell {
  --login-ink: #eaefff;
  --login-ink-soft: #9aa6cc;
  --login-line: #2a3560;
  --login-panel-from: #0b1533;
  --login-panel-via: #10265c;
  --login-panel-to: #17408f;
}

html[data-bs-theme="dark"] .login-card {
  background: #101a3f;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 12px 32px rgba(0, 0, 0, .45);
}

html[data-bs-theme="dark"] .login-input.form-control {
  background-color: #111a3d;
  border-color: var(--login-line);
  color: var(--login-ink);
}

html[data-bs-theme="dark"] .login-badge {
  background: radial-gradient(circle, #14224d 0%, #0d1638 70%, #0a1130 100%);
  box-shadow: 0 0 0 1px #1d2c5e inset, 0 0 0 10px rgba(22, 104, 234, .06);
}

html[data-bs-theme="dark"] .login-alert {
  border-color: #6d2b2b;
  background: #2a1214;
  color: #ff9d9d;
}

.no-right-radius {
  border-top-right-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
}

.btn-passwd-append {
  border: 1px solid #ced4da;
}

/* Dashboard */
.dashboard-container {
  display: flex;
  position: relative;
  width: 100%;
  max-width: 1230px;
  min-height: 400px;
  padding: 2rem;
  margin: 0 auto;
}

/* Dashboard -- Connections */
.connections-left,
.connections-right {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}

.connection-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  z-index: 5;
}

.connection-item .fa-stack {
  min-width: 2.5em;
}

.connections-left > .connection-item > span {
  margin-right: 0.5rem;
}

.connection-right {
  align-items: center;
  margin-left: 10rem;
}

.connections-left i {
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: left;
}

.connections-left i:first-child {
  margin-top: 0;
}

.connections-left i:last-child {
  margin-bottom: 0;
  margin-left: 0.5rem;
}

.connection-item.active > span {
  color: var(--bs-primary) !important;
}
.connection-item.active > i {
  color: var(--bs-primary) !important;
}

.center-device {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  position: relative;
  z-index: 10;
}

.center-device-top {
  margin-bottom: 30px;
}

.ip-info-toggle {
  display: inline-block;
  background: #e3e3e3;
  padding: 0.29rem;
  border-radius: 0.375rem;
  font-size: 0.8rem;
}
.ip-info-toggle button {
  padding: 0.1rem 0.75rem;
  border-radius: 0.275rem;
  transition: background 0.15s linear;
  background: transparent;
  font-weight: 600;
  color: var(--raspap-brand-color);
  border: none;
}
.ip-info-toggle button.active {
  background: var(--raspap-brand-color);
  color: white;
}

.client-group {
  display: flex;
  align-items: center;
  flex-direction: row-reverse;
  gap: 0.5rem;
}

.client-count {
  text-align: right;
}

.clients-status {
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-end;
  padding-right: 1rem;
}

.dashed-lines,
.solid-lines {
  --connection-lines-offset: 112px;
  position: absolute;
  top: 0;
  left: var(--connection-lines-offset);
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 1rem;
}

.dashed-lines-right,
.solid-lines-right {
  left: calc(var(--connection-lines-offset) * -1);
}

.solid-lines, .solid-lines-right {
  z-index: 3;
}

.dashed-lines, .dashed-lines-right {
  z-index: 0;
}

.device-status {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: center;
  margin: 0.8rem 0;
}

.wifi-bands {
  display: flex;
  gap: 0.5rem;
}

.band {
  padding: 0.25rem 1rem;
  border: 2px solid var(--raspap-text-light);
  border-radius: 4px;
  background: transparent;
  font-weight: 600;
  color: var(--raspap-text-light);
}

.band.active {
  border-color: var(--bs-primary);
  color: var(--bs-primary);
}

.device-label {
  font-size: 1.3rem;
  text-align: center;
  margin-top: 1rem;
}

.bottom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.3rem;
  width: 100%;
}

.status-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  color: var(--raspap-text-light);
}

.status-item .fa-stack {
  height: 27.2px !important;
}
@media (min-width: 641px) {
  .status-item .fa-stack {
    height: 34px !important;
  }
}

.status-item.active > span,
.status-item.active > div > i {
  color: var(--bs-primary)!important;
}

.inactive {
    color: var(--raspap-text-light)!important;
}

a.inactive:hover,
a.inactive:focus {
    color: var(--raspap-text-light) !important;
}

@media (max-width: 1200px) {
  .dashed-lines,
  .solid-lines {
    --connection-lines-offset: 70px;
  }
  .connections-right .connection-item {
    margin-left: auto;
  }
  .connection-item a > span:not(.fa-stack) {
    display: none!important;
  }
}

@media (max-width: 991px) {
  .connections-right,
  .connections-left {
    display: none!important;
  }
  .dashboard-container {
    width: auto;
    padding: 0;

  }
  .device-status {
    gap: 0.5rem;
  }
  .clients-mobile {
    display: flex!important;
    flex-direction: row!important;
  }
}

.clients-mobile {
  display: none;
  flex-direction: column;
  gap: 1rem;
  margin-top: 2rem;
}

.client-type {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 1rem;
}

.client-type i {
  font-size: 1.5rem;
  /* color: var(--raspap-brand-color); */
  width: 45px;
  height: 45px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--raspap-brand-color);
}

.client-type i.badge-icon {
  font-size: 0.7rem;
  background: var(--raspap-brand-color);
  color: var(--raspap-offwhite);
  width: 20px;
  height: 20px;
  border: none;
}

.client-count {
  position: absolute;
  top: -5px;
  right: -5px;
  background: var(--raspap-brand-color);
  color: var(--raspap-offwhite);
  border-radius: 50%;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
}

.device-illustration {
  min-width: 220px;
  max-width: 250px;
}

/* Dashboard -- card-header title/controls split, shared by all four
   redesigned card headers (Dashboard, Network Traffic, Top Connected
   Clients, Nearby Networks). Nested flex-wrap inside .card-header was
   observed to unreliably decide when to break the controls onto their
   own line (verified in isolated reduced test cases - not consistently
   explained by any single rule, so not relied on for correctness here).
   flex-direction: column at narrow widths is an unambiguous guarantee
   instead of a wrap heuristic. */
@media (max-width: 575.98px) {
  .card-header-flex {
    flex-direction: column;
    align-items: stretch !important;
  }
}

/* Dashboard -- Header */
/* Chevron flips to point up once expanded. Bootstrap's collapse plugin
   toggles the trigger's own .collapsed class automatically - no custom JS
   needed here, just a CSS hook on that class. */
.js-classic-toggle .fa-chevron-down {
  transition: transform 0.2s ease;
}
.js-classic-toggle:not(.collapsed) .fa-chevron-down {
  transform: rotate(180deg);
}
.dashboard-subtitle {
  font-size: 0.8rem;
  font-weight: 400;
  color: var(--raspap-text-muted);
  margin-top: 0.15rem;
}
/* Wraps the status pills + Refresh button. Deliberately NOT relying on
   flex-wrap alone here: nested flex-wrap containers inside .card-header
   were observed to sometimes fail to wrap a trailing flex item (verified
   in isolated reduced test cases) - a plain column stack below the sm
   breakpoint is a more robust, predictable fallback than debugging that
   further. */
.dashboard-header-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}
@media (max-width: 575.98px) {
  .dashboard-header-actions {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
  }
  .dashboard-header-actions .btn {
    width: 100%;
  }
}
.dashboard-status-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.dashboard-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--raspap-content-main);
  background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
}
.dashboard-status-pill i {
  font-size: 0.6rem;
}

/* Dashboard -- KPI cards */
.kpi-card .card-body {
  padding: 1.1rem 1.25rem;
}
.kpi-icon {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  margin-right: 1rem;
}
.kpi-icon-primary {
  background: rgba(21, 94, 239, 0.12);
  color: var(--raspap-theme-color, #155EEF);
}
.kpi-icon-success {
  background: rgba(34, 197, 94, 0.12);
  color: #22C55E;
}
.kpi-icon-info {
  background: rgba(21, 94, 239, 0.12);
  color: #0EA5E9;
}
.kpi-icon-muted {
  background: var(--bs-tertiary-bg);
  color: var(--raspap-text-light);
}
.kpi-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--raspap-text-muted);
  font-weight: 600;
}
.kpi-value {
  font-size: 1.7rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--raspap-content-main);
}
.kpi-trend {
  font-size: 0.78rem;
}

/* Dashboard -- Network Traffic */
.js-traffic-chart {
  width: 100%;
  height: 220px !important;
}
.js-traffic-note {
  min-height: 1.2rem;
}
/* At narrow widths, rather than fight flexbox to force this button group
   to exactly match its container's width (unreliable in nested
   .card-header flex contexts - see the .card-header-flex comment above),
   just guarantee it can never overflow the viewport: contain it in its
   own horizontal scroller. The group is only 4 short buttons, so this is
   rarely if ever actually needed in practice, but it's a hard guarantee
   instead of another flex heuristic. */
@media (max-width: 575.98px) {
  .js-traffic-range-wrap {
    max-width: 100%;
    overflow-x: auto;
  }
  .js-traffic-range {
    flex-wrap: nowrap;
  }
}

/* Dashboard -- Nearby Networks */
.js-nearby-aps {
  position: relative;
  min-height: 4rem;
}
.js-nearby-aps table code {
  font-size: 0.8rem;
}
.js-nearby-aps-status {
  color: var(--raspap-text-light);
}
.js-nearby-aps-controls .js-nearby-search {
  max-width: 200px;
}
.js-nearby-aps-controls .js-nearby-band-filter {
  max-width: 140px;
}
/* Same rationale as .dashboard-header-actions above: a plain column stack
   is a more robust fallback than nested flex-wrap at narrow widths. */
@media (max-width: 575.98px) {
  .js-nearby-aps-controls {
    flex-direction: column;
    align-items: stretch !important;
    width: 100%;
  }
  .js-nearby-aps-controls .js-nearby-search,
  .js-nearby-aps-controls .js-nearby-band-filter {
    max-width: none;
  }
}

/* DHCP Server */
.dhcp-static-leases {
  margin-top: 1em;
  margin-bottom: 1em;
}

.dhcp-static-lease-row {
  margin-top: 0.5em;
  margin-bottom: 0.5em;
}

/* WiFi Client */
.info-item-wifi {
  width: 6rem;
  float: left;
}

#wifiClientContent #wpaConf {
  min-height: calc(100vh / 3);
}

.loading-spinner::before {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: var(--raspap-text-muted);
  content: "\f1ce";
  font-family: "Font Awesome 5 Free";
  font-weight: 900;
  font-size: 54px;
  animation: loading-spin 1.2s linear infinite;
}
@keyframes loading-spin {
    0% { transform: translate(-50%, -50%) rotate(0deg); }
    100% { transform: translate(-50%, -50%) rotate(360deg); }
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(292px, 3fr));
  grid-gap: 1rem;
}
@media (max-width: 657px) {
  .card-grid {
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
}

/* WiFi Client -- Signal Icon */
.signal-icon {
  margin-top: 2px;
  height: 16px;
  width: 16px;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: baseline;
}
.signal-icon .signal-bar {
  width: 4px;
  border-radius: 1px;
  opacity: 30%;
}

.signal-icon .signal-bar:nth-child(1) {
  height: 40%;
}
.signal-icon .signal-bar:nth-child(2) {
  height: 70%;
}
.signal-icon .signal-bar:nth-child(3) {
  height: 100%;
}

.signal-icon.weak .signal-bar:nth-child(1),
.signal-icon.medium .signal-bar:nth-child(1),
.signal-icon.medium .signal-bar:nth-child(2),
.signal-icon.strong .signal-bar:nth-child(1),
.signal-icon.strong .signal-bar:nth-child(2),
.signal-icon.strong .signal-bar:nth-child(3)
{
  opacity: 100%;
}

/* Data Usage */
canvas#divDBChartBandwidthhourly {
  height: 509px!important;
}

.dbChart {
  display: flex;
  height: 80%;
}

/* System */
/* System -- Plugin */
textarea.plugin-log {
  width: 100%;
  height: 150px;
  resize: none;
  border: 1px solid #dee2e6;
  border-radius: 0.25rem;
  padding: 0.5rem;
  background-color: #f8f9fa;
  font-family: monospace;
  font-size: 0.9rem;
}

/* About */
#aboutsponsors img {
  max-width: 100%;
}

#aboutsponsors table {
  width: 100%;
}

#aboutsponsors table tbody {
  display: grid;
  width: 100%;
  max-width: 100%;
  grid-template-columns: repeat(auto-fill, minmax(130px, 3fr));
  align-items: center;
}

#aboutsponsors table tr {
  display: contents;
}

#aboutsponsors table td {
  white-space: nowrap;
  word-break: none;
}

/* Admin */
.avatar-placeholder {
  font-size: 13rem;
  color: lightgrey;
  position: absolute;
  top: -3px;
}

.avatar {
  display: inline-block;
  position: relative;
  overflow: hidden;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background-color: #f6f6f6;
  cursor: pointer
}

.embed-avatar {
  display: block;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  background-size: 100%;
}

.camera {
  width: 100%;
  font-size: 2rem;
  height: 100%;
  z-index: 9999;
  text-align: center;
  position: absolute;
  top: 0;
  left: 0;
  background: #F0F0F0;
  opacity: 0.5;
  padding-top: 40%;
}

.avatar .camera {
  display: none;
}

.avatar:hover .camera {
  display: block;
}

/* ==========================================================================
   Firefly surface system

   The sign-in screen was designed as a self-contained block - its palette
   lived as locals on .login-shell specifically so it "can't leak into the
   rest of the admin panel". That kept it safe, but it also meant signing in
   moved you from a considered, warm blue surface onto a stock Bootstrap grey
   one. Two different products, half a second apart.

   This promotes that palette to root tokens and dresses the dashboard in it.
   .login-shell now consumes the same tokens rather than declaring its own
   values, so the two screens cannot drift: change the accent here and both
   move together.

   Everything below is surface treatment - colour, radius, shadow, easing,
   press feedback. No layout is changed, so nothing here can reflow a panel.
   ========================================================================== */
:root {
  /* Ink + accent, lifted verbatim from the sign-in screen */
  --ff-ink: #0d1b3e;
  --ff-ink-soft: #5b6690;
  --ff-accent: #1668ea;
  --ff-accent-deep: #0b48b8;
  --ff-line: #e2e8f5;

  /* One radius scale. The login card is 1.25rem; dashboard cards are much
     smaller and denser, so they take the step below it rather than the same
     value - equal radius on unequal boxes reads as inconsistent, not
     consistent. */
  --ff-radius-lg: 1.25rem;
  --ff-radius: 0.875rem;
  --ff-radius-sm: 0.6rem;

  /* The login card's three-layer shadow, thinned out. That card is alone on
     screen and can afford the full weight; a dashboard shows a dozen at once,
     where the same shadow turns to soot. */
  --ff-shadow: 0 1px 2px rgba(13, 27, 62, .04),
               0 4px 12px rgba(13, 27, 62, .05);
  --ff-shadow-lift: 0 1px 2px rgba(13, 27, 62, .05),
                    0 10px 24px rgba(13, 27, 62, .09);
  --ff-focus-ring: 0 0 0 4px rgba(22, 104, 234, .12);

  /* Stronger than the stock CSS keywords, which are too weak to read as
     intentional. */
  --ff-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

/* Single source of truth: the sign-in screen consumes the tokens above
   instead of its own hard-coded copies. */
.login-shell {
  --login-ink: var(--ff-ink);
  --login-ink-soft: var(--ff-ink-soft);
  --login-accent: var(--ff-accent);
  --login-accent-deep: var(--ff-accent-deep);
  --login-line: var(--ff-line);
}

/* --- the field the content sits on -------------------------------------- */

/* The sign-in screen's gradient, heavily diluted. At login strength it would
   fight a screen full of cards and charts; at this strength it just stops the
   page reading as flat grey, and picks up the same blue the sidebar and
   accent already use. */
html:not([data-bs-theme="dark"]) #layoutSidenav_content {
  background: linear-gradient(170deg, #f7faff 0%, #eef4fe 55%, #e8f0fd 100%);
  background-attachment: fixed;
}

/* --- cards --------------------------------------------------------------- */

html:not([data-bs-theme="dark"]) .card {
  border: 1px solid var(--ff-line);
  border-radius: var(--ff-radius);
  box-shadow: var(--ff-shadow);
}

/* .shadow is on nearly every dashboard card in markup; Bootstrap's version is
   a hard grey drop that undoes the tinted shadow above. */
html:not([data-bs-theme="dark"]) .card.shadow {
  box-shadow: var(--ff-shadow) !important;
}

html:not([data-bs-theme="dark"]) .card-header {
  background: transparent;
  border-bottom: 1px solid var(--ff-line);
}

/* Scoped through .card so this outranks the theme's `.card .card-header`
   (0,2,0), which loads after this file and was silently winning on colour -
   the weight and tracking below applied, the ink did not. */
html:not([data-bs-theme="dark"]) .card .page-card-header {
  color: var(--ff-ink);
}
.page-card-header {
  font-weight: 600;
  letter-spacing: -.01em;
}

/* --- KPI tiles ----------------------------------------------------------- */

/* The icon chip gets the sign-in badge's treatment: a radial wash with an
   inset hairline ring, instead of a flat 12%-alpha square. It is the same
   shape language at 48px that the login screen uses at 96px. */
html:not([data-bs-theme="dark"]) .kpi-icon {
  border-radius: var(--ff-radius-sm);
  box-shadow: inset 0 0 0 1px rgba(22, 104, 234, .10);
}
html:not([data-bs-theme="dark"]) .kpi-icon-primary {
  background: radial-gradient(circle at 30% 25%, #eaf3ff 0%, #f6faff 75%, #ffffff 100%);
  color: var(--ff-accent);
}
html:not([data-bs-theme="dark"]) .kpi-icon-info {
  background: radial-gradient(circle at 30% 25%, #e8f6ff 0%, #f5fbff 75%, #ffffff 100%);
}
html:not([data-bs-theme="dark"]) .kpi-icon-success {
  background: radial-gradient(circle at 30% 25%, #e9f9ef 0%, #f6fdf9 75%, #ffffff 100%);
  box-shadow: inset 0 0 0 1px rgba(34, 197, 94, .12);
}

.kpi-label {
  color: var(--ff-ink-soft);
}

.kpi-value {
  color: var(--ff-ink);
  letter-spacing: -.02em;
  /* These are polled figures. Proportional digits change width as the value
     changes, so the number visibly jitters on every refresh while saying
     nothing new. */
  font-variant-numeric: tabular-nums;
}

/* --- live readouts ------------------------------------------------------- */

/* Same reasoning, and more acute: this pair updates every 2 seconds. */
.js-traffic-download,
.js-traffic-upload,
.client-meta,
.client-traffic {
  font-variant-numeric: tabular-nums;
}

/* --- connected clients list ---------------------------------------------- */

.client-list .client-row {
  border-radius: var(--ff-radius-sm);
  padding-left: .5rem;
  padding-right: .5rem;
  margin-left: -.5rem;
  margin-right: -.5rem;
  transition: background-color 140ms ease;
}

@media (hover: hover) and (pointer: fine) {
  html:not([data-bs-theme="dark"]) .client-list .client-row:hover {
    background-color: rgba(22, 104, 234, .04);
  }
}

/* --- buttons ------------------------------------------------------------- */

/* Match the sign-in submit: a vertical accent gradient with a coloured
   shadow, rather than a flat fill. */
html:not([data-bs-theme="dark"]) .btn-primary {
  background: linear-gradient(180deg, var(--ff-accent) 0%, var(--ff-accent-deep) 100%);
  border-color: transparent;
  box-shadow: 0 2px 6px rgba(16, 80, 200, .22);
}
@media (hover: hover) and (pointer: fine) {
  html:not([data-bs-theme="dark"]) .btn-primary:hover {
    filter: brightness(1.06);
    box-shadow: 0 4px 10px rgba(16, 80, 200, .28);
  }
}

/* Press feedback on everything pressable. Before this the entire admin panel
   had exactly one :active rule - the login button - so every other control
   gave no acknowledgement that the click had landed. */
.btn,
.js-traffic-range button {
  transition: transform 160ms var(--ff-ease-out),
              filter 160ms ease,
              box-shadow 160ms ease,
              background-color 160ms ease,
              border-color 160ms ease,
              color 160ms ease;
}
.btn:active,
.js-traffic-range button:active {
  transform: scale(0.97);
}

.btn:focus-visible {
  outline: none;
  box-shadow: var(--ff-focus-ring);
}

/* --- in-flight state ----------------------------------------------------- */

/* A refresh that scans the radio can now legitimately take up to 25 seconds
   (the hard ceiling added to stop a wedged `iw` hanging a worker). Without a
   pending state that is 25 seconds of a button that looks like it did
   nothing. */
.btn[data-busy="true"] {
  pointer-events: none;
  opacity: .65;
}
.btn[data-busy="true"] .fa-sync-alt {
  animation: ff-spin 800ms linear infinite;
}
@keyframes ff-spin {
  to { transform: rotate(360deg); }
}

/* --- utilisation meters -------------------------------------------------- */

/* Memory/Storage/CPU/Temp render as bg-success below their thresholds, which
   paints three saturated green bars across the System Overview card. Two
   problems: green is the loudest thing on a blue screen, and it reads as
   "good news" when the bar is really just a level. Colour should be spent on
   exceptions, so normal is the accent and bg-warning/bg-danger keep their
   amber and red - which now actually stand out, because nothing else is
   competing with them.

   Scoped to these meters so semantic green elsewhere (status pills, the
   "domains blocklisted" tick) is untouched. */
html:not([data-bs-theme="dark"]) .progress.progress-improved .progress-bar.bg-success {
  background: linear-gradient(180deg, var(--ff-accent) 0%, var(--ff-accent-deep) 100%) !important;
}
html:not([data-bs-theme="dark"]) .progress.progress-improved {
  background-color: #e9eefb;
  border-radius: 999px;
}
.progress.progress-improved .progress-bar {
  border-radius: 999px;
}

/* --- dark mode ----------------------------------------------------------- */

html[data-bs-theme="dark"] {
  /* Restores Bootstrap 5.3's own dark body colour.
     dist/sb-admin/css/styles.css ships a :root block that hard-pins
     --bs-body-color: #212529. It loads after bootstrap.min.css, and :root
     matches <html> just as [data-bs-theme=dark] does, so that flat value won
     in *both* themes - Bootstrap's dark #dee2e6 never applied. Everything
     keyed off the token (card headers via .card .card-header, table text,
     muted copy) therefore rendered near-black on near-black: dark mode has
     been effectively unreadable, independent of anything in this file.
     Sibling tokens like --bs-tertiary-bg were unaffected and did flip, which
     is exactly why surfaces went dark while the text on them did not. */
  --bs-body-color: #dee2e6;

  --ff-ink: #e8ecf8;
  --ff-ink-soft: #9aa4c4;
  --ff-line: #2a3560;
  --ff-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 12px rgba(0, 0, 0, .35);
  --ff-shadow-lift: 0 1px 2px rgba(0, 0, 0, .35), 0 10px 24px rgba(0, 0, 0, .45);
}
html[data-bs-theme="dark"] .card {
  border-radius: var(--ff-radius);
  /* The theme darkens .card-body/.card-footer but never .card itself, so the
     card's own white background showed through as a bright sliver wherever
     the body did not cover it - most visibly as a white band under the
     dashboard header. Pre-existing; only obvious once the headers became
     readable. */
  background-color: var(--bs-dark);
}
/* Beats the theme's `.card .card-header` (0,2,0), which is what the ink
   colour lost to in light mode too. */
html[data-bs-theme="dark"] .card .page-card-header {
  color: var(--ff-ink);
}
/* Same reasoning as the light-mode meters: normal is a level, not good news. */
html[data-bs-theme="dark"] .progress.progress-improved .progress-bar.bg-success {
  background: linear-gradient(180deg, #3b82f6 0%, var(--ff-accent) 100%) !important;
}
html[data-bs-theme="dark"] .progress.progress-improved {
  background-color: #1c2340;
  border-radius: 999px;
}
@media (hover: hover) and (pointer: fine) {
  html[data-bs-theme="dark"] .client-list .client-row:hover {
    background-color: rgba(255, 255, 255, .04);
  }
}

/* --- reduced motion ------------------------------------------------------ */

/* Keep colour and opacity transitions - they aid comprehension. Drop movement
   and the indefinite spinner, which are the parts that provoke motion
   sickness. */
@media (prefers-reduced-motion: reduce) {
  .btn:active,
  .js-traffic-range button:active {
    transform: none;
  }
  .btn[data-busy="true"] .fa-sync-alt,
  .card-body [class*="fa-circle-notch"] {
    animation: none;
  }
}

/* ==========================================================================
   Firewall plugin - rules table

   The rules table is intentionally wider than a phone screen (ten columns of
   controls), so it lives in a .table-responsive scroller. That alone was not
   enough: the flex ancestors between it and the card (.card-body, .col-md-12)
   default to min-width:auto, which lets the table's intrinsic ~620px width
   propagate outwards and stretch the whole document to 567px at a 390px
   viewport - the page scrolled sideways, dragging the top navbar with it.
   Bisecting by hiding each child of the tab confirmed the wrapper was the
   source: hiding it dropped scrollWidth from 567 straight back to 390.

   min-width:0 stops that propagation; the table then scrolls inside its own
   box, which is what .table-responsive was for.
   ========================================================================== */
#firewallrules .table-responsive {
  max-width: 100%;
  min-width: 0;
  /* min-width:0 up the chain is necessary but NOT sufficient here: even with
     every ancestor at min-width:0 the document still measured 567px against a
     390px viewport. Containment is what actually severs it - the scroller's
     layout stops participating in its ancestors' width calculation, so the
     620px table can only scroll inside its own box.
     Measured, not assumed: of contain:inline-size, contain:layout paint,
     overflow:hidden, width/max-width:100% and a grid parent, only this one
     brought scrollWidth back to 390. */
  contain: layout paint;
}
/* The chain has to be walked all the way up from the scroller to the card:
   min-width:auto anywhere between them re-opens the leak, and the offenders
   (.card-body, .col-lg-12) sit ABOVE #firewallrules, so a selector scoped to
   the tab cannot reach them. .fw-plugin-page is a hook added to the plugin's
   outermost row in templates/main.php for exactly this. */
.fw-plugin-page,
.fw-plugin-page .card,
.fw-plugin-page .card-body,
.fw-plugin-page form,
.fw-plugin-page .tab-content,
.fw-plugin-page .tab-pane,
.fw-plugin-page .row,
.fw-plugin-page [class^="col-"],
.fw-plugin-page [class*=" col-"] {
  min-width: 0;
}
/* Keep the columns usable rather than letting ten of them crush together;
   below this width the wrapper scrolls instead. */
.fw-rules-table {
  min-width: 620px;
}
