/* The ui_* component layer.
 *
 * One block per component in views/components/. Every value comes from
 * tokens.css — no literal colours here.
 */

/* ---------------------------------------------------------------- button */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 0 var(--s-4);
  height: 38px;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font: inherit;
  font-weight: 550;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition),
              color var(--transition), transform var(--transition);
  text-decoration: none;
}

.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }

.btn[disabled],
.btn.is-disabled {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

.btn--primary {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-text-on-accent);
}
.btn--primary:hover:not([disabled]) {
  background: var(--c-accent-hover);
  border-color: var(--c-accent-hover);
}

.btn--secondary {
  background: var(--c-surface);
  border-color: var(--c-border-strong);
  color: var(--c-text);
}
.btn--secondary:hover:not([disabled]) { background: var(--c-surface-2); }

.btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--c-text-soft);
}
.btn--ghost:hover:not([disabled]) {
  background: var(--c-surface-2);
  color: var(--c-text);
}

.btn--danger {
  background: var(--c-danger);
  border-color: var(--c-danger);
  color: #fff;
}
.btn--danger:hover:not([disabled]) { filter: brightness(0.93); }

.btn--sm { height: 30px; padding: 0 var(--s-3); font-size: var(--fs-sm); }
.btn--lg { height: 46px; padding: 0 var(--s-5); font-size: var(--fs-lg); }
.btn--block { display: flex; width: 100%; }

.btn__icon { font-size: 1.05em; line-height: 1; }

/* Spinner shown instead of the icon while a request is in flight. */
.btn.is-loading { pointer-events: none; opacity: 0.75; }
.btn.is-loading .btn__icon { display: none; }
.btn.is-loading::before {
  content: "";
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: var(--r-full);
  animation: spin 0.6s linear infinite;
}

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

/* ----------------------------------------------------------------- field */

.field { display: flex; flex-direction: column; gap: var(--s-2); }

.field__label {
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--c-text);
}

.field__req { color: var(--c-danger); margin-left: 2px; }

.field__hint { font-size: var(--fs-xs); color: var(--c-text-mute); }

/* A stored value shown rather than edited. Same box as an .input so a reading
   and a form line up column for column.

   The em dash comes from :empty rather than from an {{if}} on each of fourteen
   fields — and from the stylesheet, not an inline style, which the CSP forbids
   anyway. */
.field__value { margin: 0; padding: 6px 0; line-height: 1.5; word-break: break-word; }
.field__value:empty::before { content: "—"; color: var(--c-text-mute); }
.field__value--pre { white-space: pre-wrap; }

.field__error {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--fs-xs);
  color: var(--c-danger);
}

.field__control { position: relative; display: flex; align-items: center; }

.field__icon {
  position: absolute;
  left: var(--s-3);
  pointer-events: none;
  font-size: 1em;
  opacity: 0.85;
}

.field__control--icon .input { padding-left: 38px; }

/* ----------------------------------------------------------------- input */

.input {
  width: 100%;
  min-height: 38px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-md);
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  transition: border-color var(--transition), box-shadow var(--transition);
}

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

/* Autofilled fields keep the browser's own background — a colour no stylesheet
 * can set directly. The inset shadow paints over it, and the absurd transition
 * delay stops Chrome from flashing its yellow back in on focus. */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus,
.input:-webkit-autofill:active {
  -webkit-box-shadow: 0 0 0 100px var(--c-surface) inset;
  -webkit-text-fill-color: var(--c-text);
  caret-color: var(--c-text);
  transition: background-color 100000s ease-in-out 0s;
}

.input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 100px var(--c-surface) inset, 0 0 0 3px var(--c-accent-ring);
}

.input:focus {
  outline: none;
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-ring);
}

.input[disabled] {
  background: var(--c-surface-2);
  color: var(--c-text-mute);
  cursor: not-allowed;
}

.input.is-invalid {
  border-color: var(--c-danger);
}
.input.is-invalid:focus {
  box-shadow: 0 0 0 3px var(--c-danger-soft);
}

/* -------------------------------------------------------------- textarea */

.textarea {
  min-height: 96px;
  resize: vertical;
  line-height: 1.5;
}

.textarea-counter {
  align-self: flex-end;
  font-size: var(--fs-xs);
  color: var(--c-text-mute);
  font-variant-numeric: tabular-nums;
}

.textarea-counter.is-over { color: var(--c-danger); }

/* ---------------------------------------------------------------- select */

.select-native {
  appearance: none;
  padding-right: 34px;
  /* Chevron drawn with a gradient pair so no external asset is needed —
   * remote images are blocked by the CSP anyway. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--c-text-soft) 50%),
    linear-gradient(135deg, var(--c-text-soft) 50%, transparent 50%);
  background-position: calc(100% - 18px) 52%, calc(100% - 13px) 52%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* ------------------------------------------------ dropdown / multiselect */

.dropdown { position: relative; width: 100%; }

.dropdown__trigger {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  min-height: 38px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-md);
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition), box-shadow var(--transition);
}

.dropdown__trigger:focus-visible {
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-ring);
  outline: none;
}

.dropdown.is-open .dropdown__trigger {
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-ring);
}

.dropdown__value {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  min-width: 0;
}

.dropdown__placeholder { color: var(--c-text-mute); }

.dropdown__caret {
  flex: none;
  font-size: 10px;
  color: var(--c-text-soft);
  transition: transform var(--transition);
}

.dropdown.is-open .dropdown__caret { transform: rotate(180deg); }

.dropdown__panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  display: none;
  flex-direction: column;
  max-height: 300px;
  padding: var(--s-2);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
  box-shadow: var(--sh-3);
}

.dropdown.is-open .dropdown__panel { display: flex; }

.dropdown__search {
  margin-bottom: var(--s-2);
  min-height: 32px;
  font-size: var(--fs-sm);
}

.dropdown__actions {
  display: flex;
  gap: var(--s-2);
  padding-bottom: var(--s-2);
  margin-bottom: var(--s-2);
  border-bottom: 1px solid var(--c-border);
}

.dropdown__options {
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.dropdown__option {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-md);
  transition: background var(--transition);
}

.dropdown__option:hover,
.dropdown__option.is-active {
  background: var(--c-surface-2);
}

.dropdown__option.is-hidden { display: none; }

/* Ruled out by a dependent list (data-filter-by), not by the search box. Two
   classes because the two filters are independent: one class would mean the
   last one to run undoes the other. */
.dropdown__option.is-filtered-out { display: none; }

.dropdown__option input { accent-color: var(--c-accent); flex: none; }

.dropdown__option-label { flex: 1; min-width: 0; }

.dropdown__option-hint {
  font-size: var(--fs-xs);
  color: var(--c-text-mute);
}

.dropdown__check {
  flex: none;
  color: var(--c-accent);
  visibility: hidden;
}

.dropdown__option.is-selected .dropdown__check { visibility: visible; }

.dropdown__empty {
  padding: var(--s-3);
  text-align: center;
  color: var(--c-text-mute);
  font-size: var(--fs-sm);
}

/* Chips inside a multiselect trigger */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 2px var(--s-2);
  border-radius: var(--r-full);
  background: var(--c-accent-soft);
  color: var(--c-accent);
  font-size: var(--fs-xs);
  font-weight: 550;
  max-width: 100%;
}

.chip__remove {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
}

.chip__remove:hover { opacity: 1; }

/* ------------------------------------------------- checkbox / radio / switch */

.check {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  cursor: pointer;
  font-size: var(--fs-md);
}

.check input {
  margin: 2px 0 0;
  accent-color: var(--c-accent);
  width: 16px;
  height: 16px;
  flex: none;
}

.check__text { display: flex; flex-direction: column; gap: 2px; }
.check__hint { font-size: var(--fs-xs); color: var(--c-text-mute); }

.radio-group { display: flex; flex-direction: column; gap: var(--s-2); }

.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  cursor: pointer;
}

.switch input { position: absolute; opacity: 0; width: 0; height: 0; }

.switch__track {
  position: relative;
  width: 40px;
  height: 22px;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  transition: background var(--transition);
  flex: none;
}

.switch__thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: var(--r-full);
  background: var(--c-surface);
  box-shadow: var(--sh-1);
  transition: transform var(--transition);
}

.switch input:checked + .switch__track { background: var(--c-accent); }
.switch input:checked + .switch__track .switch__thumb { transform: translateX(18px); }
.switch input:focus-visible + .switch__track { box-shadow: 0 0 0 3px var(--c-accent-ring); }

/* ------------------------------------------------------------------ card */

.card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  overflow: hidden;
}

/* A dropdown panel hangs below its field on position: absolute and is taller
   than the field, so the clipping above cuts the list off at the card's edge —
   which looked like a dropdown showing one country out of two hundred.
   Cards clip so nothing pokes out past the rounded corners; a card hosting a
   dropdown gives that up, because the panel is meant to poke out. */
.card:has(.dropdown) {
  overflow: visible;
}

.card__head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--c-border);
}

.card__icon { font-size: 1.15em; }
.card__title { margin: 0; font-size: var(--fs-lg); font-weight: 600; }
.card__sub { margin: 2px 0 0; font-size: var(--fs-sm); color: var(--c-text-soft); }
.card__actions { margin-left: auto; display: flex; gap: var(--s-2); }
.card__body { padding: var(--s-5); }
.card__foot {
  padding: var(--s-3) var(--s-5);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-2);
}

/* ------------------------------------------------------------------ stat */

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-5);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
}

.stat__top { display: flex; align-items: center; gap: var(--s-2); }
.stat__icon { font-size: 1.2em; }
.stat__label { font-size: var(--fs-sm); color: var(--c-text-soft); }

.stat__value {
  font-size: var(--fs-2xl);
  font-weight: 660;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.stat__hint { font-size: var(--fs-xs); color: var(--c-text-mute); }

.stat--success .stat__value { color: var(--c-success); }
.stat--warning .stat__value { color: var(--c-warning); }
.stat--danger  .stat__value { color: var(--c-danger); }

/* ----------------------------------------------------------------- alert */

.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-size: var(--fs-md);
}

.alert__icon { font-size: 1.05em; line-height: 1.4; flex: none; }
.alert__body { flex: 1; min-width: 0; }
.alert__title { font-weight: 600; margin-bottom: 2px; }

.alert--success { background: var(--c-success-soft); border-color: var(--c-success); color: var(--c-success); }
.alert--error   { background: var(--c-danger-soft);  border-color: var(--c-danger);  color: var(--c-danger); }
.alert--warning { background: var(--c-warning-soft); border-color: var(--c-warning); color: var(--c-warning); }
.alert--info    { background: var(--c-info-soft);    border-color: var(--c-info);    color: var(--c-info); }

/* ----------------------------------------------------------------- badge */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 2px var(--s-2);
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.5;
  background: var(--c-surface-3);
  color: var(--c-text-soft);
}

.badge--success { background: var(--c-success-soft); color: var(--c-success); }
.badge--warning { background: var(--c-warning-soft); color: var(--c-warning); }
.badge--danger  { background: var(--c-danger-soft);  color: var(--c-danger); }
.badge--info    { background: var(--c-info-soft);    color: var(--c-info); }
.badge--accent  { background: var(--c-accent-soft);  color: var(--c-accent); }

/* ---------------------------------------------------------------- avatar */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--r-full);
  background: var(--c-accent-soft);
  color: var(--c-accent);
  font-size: var(--fs-sm);
  font-weight: 650;
  text-transform: uppercase;
  flex: none;
}

.avatar--sm { width: 26px; height: 26px; font-size: var(--fs-xs); }
.avatar--lg { width: 46px; height: 46px; font-size: var(--fs-lg); }

/* ------------------------------------------------------------------ list */

.list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  overflow: hidden;
}

.list__item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--c-border);
  color: inherit;
  text-decoration: none;
  transition: background var(--transition);
}

.list__item:last-child { border-bottom: 0; }
.list__item:hover { background: var(--c-surface-2); text-decoration: none; }

.list__icon { font-size: 1.15em; flex: none; }
.list__body { flex: 1; min-width: 0; }
.list__title { font-weight: 550; }
.list__sub { font-size: var(--fs-sm); color: var(--c-text-soft); }
.list__meta { font-size: var(--fs-xs); color: var(--c-text-mute); flex: none; }

/* ----------------------------------------------------------------- table */

.table-wrap {
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  /* Wide tables scroll inside their own box; the page never does. */
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-md);
}

.table th,
.table td {
  padding: var(--s-3) var(--s-4);
  text-align: left;
  border-bottom: 1px solid var(--c-border);
  white-space: nowrap;
}

.table th {
  font-size: var(--fs-xs);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-mute);
  background: var(--c-surface-2);
}

.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--c-surface-2); }

/* ----------------------------------------------------------------- tabs */

.tabs__list {
  display: flex;
  gap: var(--s-1);
  border-bottom: 1px solid var(--c-border);
  margin-bottom: var(--s-4);
  overflow-x: auto;
}

.tabs__tab {
  padding: var(--s-2) var(--s-3);
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--c-text-soft);
  font: inherit;
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--transition), border-color var(--transition);
}

.tabs__tab:hover { color: var(--c-text); }

.tabs__tab.is-active {
  color: var(--c-accent);
  border-bottom-color: var(--c-accent);
}

.tabs__panel { display: none; }
.tabs__panel.is-active { display: block; }

/* A tab strip that *is* the card's header rather than something sitting inside
 * it: the buttons span the full width and there is no padding around them, so
 * the underline of the active tab meets the card's own edges.
 *
 * The header keeps no border of its own — .tabs__list already draws one, and two
 * lines a pixel apart is what the previous layout looked like. */
.card__head--tabs {
  padding: 0;
  border-bottom: 0;
}

.card__head--tabs .tabs__list {
  flex: 1;
  gap: 0;
  margin-bottom: 0;
}

/* Equal shares of the row. min-width:0 because a flex item defaults to its
 * content width as a floor, which would push a long label past its share
 * instead of letting the label itself scroll. */
.card__head--tabs .tabs__tab {
  flex: 1;
  min-width: 0;
  padding: var(--s-3) var(--s-4);
  text-align: center;
}

/* ---------------------------------------------------------------- modal */

.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
  background: rgba(9, 12, 17, 0.55);
}

.modal.is-open { display: flex; }

/* Search boxes sit in a flex row next to their button, and a bare .field
 * would stretch to fill it. This caps the width without an inline style,
 * which the CSP would block anyway. */
.search-field { flex: 1 1 320px; max-width: 420px; }

/* A from/to pair reads as one control, so the two inputs share a row and the
   dash between them stays out of the tab order. */
.range {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.range__dash { color: var(--c-text-soft); }

/* The search form is itself a flex item now that the selection bar shares its
   row: without this it would size to its content and the field inside it would
   never get the width the rule above asks for. */
.search-form { flex: 1 1 320px; }

/* The dialog is the frame, not the scroller: only .modal__body scrolls, so the
   title and the buttons stay where they were put. It used to carry overflow-y
   itself, which meant a tall dialog scrolled its own ✕ off the screen.

   overflow: hidden also keeps the body's scrollbar inside the rounded corners. */
.modal__dialog {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 480px;
  max-height: 90vh;
  overflow: hidden;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
}

/* Some dialogs wrap head/body/foot in a <form> — see views/pages/users.html.
   The column would stop at that form, so it has to be one too. */
.modal__dialog > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* A dialog holding a full record form needs more room than a confirmation. */
.modal__dialog--wide { max-width: 760px; }

.modal__head {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--c-border);
}

.modal__title { margin: 0; font-size: var(--fs-lg); }
.modal__close { margin-left: auto; }

/* min-height is load-bearing. A flex item defaults to min-height: auto, which
   refuses to shrink below its content — without this the body never scrolls and
   the dialog grows straight past the 90vh above it. */
.modal__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s-5);
}

.modal__foot {
  display: flex;
  flex: none;
  justify-content: flex-end;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-2);
}

/* ----------------------------------------------------------- pagination */

.pagination { display: flex; align-items: center; gap: var(--s-2); }

.pagination__page {
  min-width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--s-2);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-md);
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  cursor: pointer;
  text-decoration: none;
}

.pagination__page:hover { background: var(--c-surface-2); text-decoration: none; }

.pagination__page.is-active {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-text-on-accent);
}

/* ------------------------------------------------------- empty / spinner */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-7) var(--s-4);
  text-align: center;
  color: var(--c-text-soft);
}

.empty__icon { font-size: 40px; line-height: 1; }
.empty__title { font-weight: 600; color: var(--c-text); }
.empty__text { font-size: var(--fs-sm); max-width: 42ch; }

.spinner {
  display: inline-block;
  width: 18px;
  height: 18px;
  border: 2px solid var(--c-border-strong);
  border-right-color: var(--c-accent);
  border-radius: var(--r-full);
  animation: spin 0.65s linear infinite;
}

.spinner--sm { width: 13px; height: 13px; border-width: 2px; }
.spinner--lg { width: 28px; height: 28px; border-width: 3px; }

/* ------------------------------------------------------------ girl grid */

.girl-grid {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

.girl-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-1);
  transition: border-color var(--transition), box-shadow var(--transition),
              transform var(--transition);
}

.girl-card:hover {
  box-shadow: var(--sh-2);
  transform: translateY(-2px);
}

.girl-card.is-selected {
  border-color: var(--c-accent);
  box-shadow: 0 0 0 2px var(--c-accent-ring);
}

/* Above the link, not inside it: a checkbox nested in the anchor would
 * navigate to the profile instead of ticking the card. */
.girl-card__select {
  position: absolute;
  top: var(--s-2);
  left: var(--s-2);
  z-index: 2;
  display: inline-flex;
  padding: var(--s-1);
  border-radius: var(--r-sm);
  background: var(--c-surface);
  box-shadow: var(--sh-1);
  cursor: pointer;
}

.girl-card__select input {
  width: 18px;
  height: 18px;
  accent-color: var(--c-accent);
  cursor: pointer;
}

.girl-card__link {
  display: flex;
  flex-direction: column;
  color: inherit;
  text-decoration: none;
}

.girl-card__link:hover { text-decoration: none; }

.girl-card__cover {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  background: var(--c-surface-2);
  display: block;
}

.girl-card__cover--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  color: var(--c-text-mute);
}

.girl-card__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--s-3);
}

.girl-card__name { font-weight: 600; }
.girl-card__sub  { font-size: var(--fs-xs); color: var(--c-text-mute); }

.girl-card__meta {
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
  font-size: var(--fs-sm);
  color: var(--c-text-soft);
}

.girl-card__short {
  align-self: flex-start;
  margin-top: var(--s-1);
  font-size: var(--fs-xs);
}

/* --------------------------------------------------------- selection bar */

/* The row between the filters and the grid it acts on: master checkbox at the
   left, above the first card, where a grid with bulk actions always keeps it;
   the actions at the right, once there is something to act on.

   min-height reserves the line the actions will occupy, so revealing them does
   not shove the whole grid down. */
/* Same surface as .card, because that is what every other block on the page
   sits on — a bare row of controls floating between the filters and the grid
   read as leftover markup rather than as part of the panel. */
.select-row {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-2) var(--s-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
}

/* Lit while a selection exists: the row is the only thing on the page that
   says how many records the next click will destroy. */
.select-row.is-active {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}

.select-row__main {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-3);
  min-height: 2rem;
}

/* .check comes from ui_checkbox.html — the master box is the same control as
   every other checkbox in the panel, and only its label is toned down. */
.select-row__master { font-size: var(--fs-sm); user-select: none; }

.select-row__summary { font-size: var(--fs-sm); color: var(--c-text-soft); }
.select-row__summary[hidden] { display: none; }

/* margin-left: auto keeps the destructive button at the far end of the row,
   away from the checkbox the cursor has just left. */
.select-row__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-left: auto;
}

.select-row__actions[hidden] { display: none; }

.select-row__offer {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--c-text-soft);
}

.select-row__offer[hidden] { display: none; }

/* A button that reads as a link: it continues a sentence ("…Выбрать все 4211 по
   фильтру"), and a bordered control mid-sentence would read as a second,
   competing action to the one in the row above. */
.link-btn {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--c-accent);
  text-decoration: underline;
  cursor: pointer;
}

.link-btn:hover { color: var(--c-accent-hover); }

/* ------------------------------------------------------- send dialog */

.send-loading { display: flex; justify-content: center; padding: var(--s-5); }

/* Step one: the channels. Shown side by side because the choice is between
   two things, and a disabled one still has to be readable — a channel hidden
   for being unlinked leaves "почему тут пусто" unanswered. */
.channels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}

.channel {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}

.channel.is-disabled { opacity: 0.6; }

.channel__icon { font-size: 1.5em; }
.channel__name { font-weight: 600; }
.channel__state { font-size: var(--fs-sm); color: var(--c-text-soft); }
.channel__state--ok { color: var(--c-success, var(--c-accent)); }

.send-search { margin-bottom: var(--s-3); }

/* No scroller of its own any more. It used to be capped at 45vh, back when the
   whole dialog scrolled and a long phone book pushed the header off screen —
   .modal__body now scrolls instead, and a second scrollbar inside the first one
   makes the wheel behave differently depending on where the pointer sits. */
.chat-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
}

.chat-list__item + .chat-list__item { border-top: 1px solid var(--c-border); }

.chat-list__row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  cursor: pointer;
}

.chat-list__row:hover { background: var(--c-surface-2); }

.chat-list__row input {
  width: 16px;
  height: 16px;
  accent-color: var(--c-accent);
  flex: none;
  cursor: pointer;
}

.chat-list__text { display: flex; flex-direction: column; min-width: 0; }
.chat-list__name { font-weight: 500; }
.chat-list__meta { font-size: var(--fs-xs); color: var(--c-text-mute); }

.send-note {
  margin: var(--s-3) 0 0;
  font-size: var(--fs-sm);
  color: var(--c-text-soft);
}

/* Above the list rather than below it: it counts what follows. */
.send-note--head { margin: 0 0 var(--s-2); }

/* The template picker and the language switch. Side by side above 640px —
   they are one decision, "какой текст на каком языке", and splitting them
   across two rows on a wide dialog reads as two. */
.send-compose {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin: var(--s-4) 0;
}

.send-compose__row {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 640px) {
  .send-compose__row { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}

.send-compose__langs { display: flex; gap: var(--s-3); flex-wrap: wrap; }

/* The gap under the preview. Without it the last message card sits flush on the
   chat search below, and the two read as one control. */
.send-preview { margin-bottom: var(--s-4); }

.send-preview__toggle {
  padding: var(--s-2) 0;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--c-text-soft);
  cursor: pointer;
}

/* A batch on its way. Sits between the message and the chat picker, so the
   answer to "did it go" is where the button that sent it was. */
.send-progress:not(:empty) { margin: var(--s-4) 0; }

.send-progress__line { margin: 0 0 var(--s-2); font-size: var(--fs-sm); }

/* Native <progress>: the browser draws it, and the CSP forbids the inline style
   a hand-rolled bar would need. */
.send-progress__bar { width: 100%; height: 8px; }

.send-progress__failures {
  margin: var(--s-3) 0 0;
  padding-left: var(--s-4);
  font-size: var(--fs-sm);
  color: var(--c-text-soft);
}

.send-form { display: flex; flex-direction: column; gap: var(--s-3); }
.send-form__foot { display: flex; justify-content: flex-end; }

.send-preview__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.send-preview__item {
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
}

.send-preview__head { display: flex; align-items: baseline; gap: var(--s-2); margin-bottom: var(--s-2); }
.send-preview__name { font-weight: 600; }
.send-preview__id { font-size: var(--fs-xs); color: var(--c-text-mute); }

/* ------------------------------------------------------ message templates */

/* The list and the editor side by side above 1000px, stacked below it: the
   editor is a textarea and a preview, and neither survives half a phone. */
.tpl-layout {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 1000px) {
  .tpl-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.tpl-list__items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-3); }

.tpl-list__item {
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}

.tpl-list__head { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); }
.tpl-list__name { font-weight: 600; }

/* pre, not a div: the body is a message, and its line breaks are the layout.
   The wrap keeps a long line from widening the whole panel. */
.tpl-list__body,
.tpl-preview__text {
  margin: 0;
  font-family: inherit;
  font-size: var(--fs-sm);
  color: var(--c-text-soft);
  white-space: pre-wrap;
  word-break: break-word;
}

/* Both languages, labelled and stacked. Two identical Cyrillic blocks is what
   an untranslated template looks like, and spotting those is the point. */
.tpl-list__bodies { display: flex; flex-direction: column; gap: var(--s-2); }
.tpl-list__lang { display: flex; gap: var(--s-2); min-width: 0; }

.tpl-list__lang-name {
  flex: none;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--c-text-mute);
}

.tpl-list__actions { display: flex; gap: var(--s-2); margin-top: var(--s-3); }

.tpl-editor { display: flex; flex-direction: column; gap: var(--s-4); }

.tpl-preview,
.tpl-vars {
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
}

.tpl-preview__title { margin: 0 0 var(--s-2); font-size: var(--fs-md); font-weight: 600; }

/* Which pane is which. The two sit one above the other, so without a label the
   second one reads as a continuation of the first. */
.tpl-preview__lang {
  margin: var(--s-3) 0 var(--s-1);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--c-text-mute);
}
.tpl-preview__note,
.tpl-preview__empty { margin: 0 0 var(--s-2); font-size: var(--fs-xs); color: var(--c-text-mute); }
.tpl-preview__warn { margin: 0 0 var(--s-2); font-size: var(--fs-sm); color: var(--c-warning, var(--c-text-soft)); }

.tpl-vars__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-1); }
.tpl-vars__list li { display: flex; gap: var(--s-2); font-size: var(--fs-sm); }
.tpl-vars__list span { color: var(--c-text-soft); }

/* -------------------------------------------------------------- uploader */

.uploader { display: flex; flex-direction: column; gap: var(--s-3); }

.uploader__zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-6) var(--s-4);
  border: 2px dashed var(--c-border-strong);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
  cursor: pointer;
  text-align: center;
  transition: border-color var(--transition), background var(--transition);
}

.uploader__zone:hover,
.uploader__zone.is-dragover {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}

/* The input stays in the accessibility tree and keeps its keyboard behaviour;
 * only its default appearance is hidden. display:none would make the label
 * unfocusable and break picking files without a mouse. */
.uploader__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.uploader__zone:focus-within {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

.uploader__icon { font-size: 30px; line-height: 1; }
.uploader__title { font-weight: 550; }
.uploader__hint { font-size: var(--fs-xs); color: var(--c-text-mute); }
.uploader__hint.is-over { color: var(--c-danger); font-weight: 600; }

.uploader__previews {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}

.uploader__item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.uploader__thumb {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--c-border);
  background: var(--c-surface-2);
}

.uploader__thumb--file {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
}

.uploader__name {
  font-size: var(--fs-xs);
  color: var(--c-text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.uploader__remove {
  position: absolute;
  top: var(--s-1);
  right: var(--s-1);
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--r-full);
  background: var(--c-scrim);
  color: #fff;
  font-size: var(--fs-xs);
  cursor: pointer;
}

/* ------------------------------------------------- upload progress overlay */

/* Fullscreen, like the modal: an upload that takes minutes must own the
 * screen, or the page under it stays editable and a second submit would
 * race the first. [hidden] wins over display:flex — one attribute is the
 * whole show/hide contract. */
.upoverlay {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
  background: var(--c-scrim);
}

.upoverlay[hidden] { display: none; }

.upoverlay__card {
  width: min(460px, 100%);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  border-radius: var(--r-lg);
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  box-shadow: var(--sh-3);
}

.upoverlay__title {
  margin: 0;
  font-size: var(--fs-lg);
}

/* The file being sent, plus the i/N counter the script appends to the title.
 * One line: a long filename is elided, not wrapped. */
.upoverlay__name {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--c-text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.upoverlay__track {
  height: 10px;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  overflow: hidden;
}

/* Width is driven from JS as a percentage. The transition smooths the steps
 * upload events arrive in; under prefers-reduced-motion base.css disables it
 * and the bar simply jumps, which reads fine. */
.upoverlay__fill {
  width: 0%;
  height: 100%;
  border-radius: inherit;
  background: var(--c-accent);
  transition: width 160ms ease-out;
}

.upoverlay__errors {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  font-size: var(--fs-sm);
  color: var(--c-danger);
  max-height: 30vh;
  overflow-y: auto;
}

.upoverlay__errors:empty { display: none; }

.upoverlay__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
}

.upoverlay__actions:empty { display: none; }

/* ----------------------------------------------------------- media grid */

.media-grid {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}

.media-tile { position: relative; }

.media-tile__content {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--c-border);
  background: var(--c-surface-2);
  display: block;
}

.media-tile__delete {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
}

/* ------------------------------------------------------ loading indicator */

/* htmx sets .htmx-request on the element issuing a request for as long as it
 * is in flight. A boosted link swaps the whole body, so on a slow response
 * the page just sits there — this bar is the only sign anything is happening. */
.htmx-request::after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 200;
  background: linear-gradient(90deg, transparent, var(--c-accent), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: htmx-progress 1s ease-in-out infinite;
}

@keyframes htmx-progress {
  0%   { background-position: -40% 0; }
  100% { background-position: 140% 0; }
}

/* ----------------------------------------------------------------- toast */

.toast-host {
  position: fixed;
  right: var(--s-4);
  bottom: var(--s-4);
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  max-width: min(360px, calc(100vw - 2 * var(--s-4)));
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
  box-shadow: var(--sh-3);
  animation: toast-in 180ms ease-out;
}

.toast__icon { font-size: 1.05em; line-height: 1.4; flex: none; }
.toast__body { flex: 1; min-width: 0; font-size: var(--fs-md); }

/* The dismiss button is quiet until reached: a toast leaves on its own, so
   the affordance is there for whoever wants it gone sooner, not an action the
   eye should be pulled to. */
.toast__close {
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-text-mute);
  font-size: var(--fs-sm);
  line-height: 1.4;
  cursor: pointer;
  transition: color var(--transition);
}

.toast__close:hover,
.toast__close:focus-visible { color: var(--c-text); }

/* Unlike .alert--*, the surface stays neutral and only the edge carries the
   colour. An alert sits in the page and can afford a tinted block; a toast
   floats over arbitrary content, where a tint would fight whatever is under
   it in one theme or the other. */
.toast--success { border-left: 3px solid var(--c-success); }
.toast--error   { border-left: 3px solid var(--c-danger); }
.toast--warning { border-left: 3px solid var(--c-warning); }
.toast--info    { border-left: 3px solid var(--c-info); }

.toast.is-leaving { animation: toast-out 160ms ease-in forwards; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

@keyframes toast-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(8px); }
}

/* ------------------------------------------------------------------ whatsapp */
/* The WhatsApp panel on the settings page. Lives here rather than in panel.css
 * because it is card content, not chrome — the same reason .empty and .alert
 * are here. */

.wa { display: grid; gap: var(--s-5); }

.wa-account {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
}

/* Matches .avatar--lg, so the fallback initials and a real picture leave the
 * layout identical — a linked account must not jump when the photo loads. */
.wa-account__avatar {
  width: 46px;
  height: 46px;
  border-radius: var(--r-full);
  object-fit: cover;
  background: var(--c-surface-2);
}

.wa-account__body { display: grid; gap: var(--s-3); min-width: 0; }

.wa-account__name {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  font-size: var(--fs-lg);
  font-weight: 600;
}

/* Two columns: label, value. The labels stay narrow so the numbers line up. */
.wa-account__facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--s-1) var(--s-4);
  margin: 0;
  font-size: var(--fs-sm);
}

.wa-account__facts dt { color: var(--c-text-mute); }
.wa-account__facts dd { margin: 0; color: var(--c-text); }

/* The device JID is long and has no spaces to break at. */
.wa-account__jid {
  font-family: var(--f-mono);
  font-size: var(--fs-xs);
  overflow-wrap: anywhere;
}

.wa-steps {
  display: grid;
  gap: var(--s-2);
  margin: 0;
  padding-left: var(--s-5);
  color: var(--c-text-soft);
  font-size: var(--fs-sm);
}

.wa-qr {
  display: grid;
  justify-items: center;
  gap: var(--s-3);
}

/* Encoded at 320px and scaled down on narrow screens rather than re-encoded:
 * a QR code is square and scales without losing anything a camera needs. */
.wa-qr__code {
  width: min(320px, 100%);
  height: auto;
  padding: var(--s-3);
  border-radius: var(--r-md);
  /* White regardless of theme: a dark-inverted QR code is one many phone
   * cameras refuse to read. */
  background: #ffffff;
}

.wa-qr__hint {
  margin: 0;
  color: var(--c-text-mute);
  font-size: var(--fs-sm);
  text-align: center;
}

.wa-actions { display: flex; gap: var(--s-3); flex-wrap: wrap; }
