/* ==========================================================================
   components.css — every shared component from ARCHITECTURE.md §13.

   WHY this file exists at all: the UI modules build DOM with utils.el() and
   only ever hand out class names. If a component is not styled here it is not
   styled anywhere, so each block below defines the FULL state matrix —
   rest / hover / active / focus-visible / disabled — not just the happy path.

   Two rules worth knowing before editing:
   1. @keyframes are never redeclared here. animations.css owns them and also
      pre-wires `.skeleton`, `.spinner`, `.dot-pulse > *`, `.btn__ripple`,
      `.modal`, `.modal-backdrop`, `.menu` and `.toast` with their animation
      shorthand. This file only ever adds colour, geometry and state.
   2. Specificity is intentional where a component overrides `.btn`
      (`.search .search__clear`, `.modal .modal__close`): components.css loads
      after layout.css but a bare class would still tie with `.btn`.
   ========================================================================== */

/* ==========================================================================
   Icons
   ========================================================================== */

.icon {
  width: 20px;
  height: 20px;
  flex: none;
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: 36px;
  padding-inline: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  color: var(--text-0);
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  letter-spacing: -0.005em;
  white-space: nowrap;
  user-select: none;
  box-shadow: var(--shadow-1);
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    box-shadow var(--dur) var(--ease),
    transform var(--dur-fast) var(--ease);
}

/* The ripple is only clipped while one exists. A permanent `overflow:hidden`
   would also clip the [data-tip] tooltip pseudo-elements off these buttons. */
.btn:has(.btn__ripple) {
  overflow: hidden;
}

.btn:hover:not(:disabled) {
  background: var(--bg-3);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-2);
  transform: translateY(-1px);
}

.btn:active:not(:disabled) {
  background: var(--bg-3);
  box-shadow: var(--shadow-1);
  transform: translateY(0) scale(0.985);
}

.btn.is-disabled {
  /* base.css already dims + sets the cursor; killing the pointer keeps every
     hover/active rule from firing without needing :not(.is-disabled) noise. */
  pointer-events: none;
  transform: none;
}

/* ---- Variants -------------------------------------------------------- */

.btn--primary {
  border-color: transparent;
  background: var(--accent-gradient);
  color: var(--accent-contrast);
  box-shadow: var(--shadow-2);
}

.btn--primary:hover:not(:disabled) {
  background: var(--accent-hover);
  border-color: transparent;
  box-shadow: var(--shadow-2), var(--shadow-glow);
}

.btn--primary:active:not(:disabled) {
  background: var(--accent-press);
  box-shadow: var(--shadow-1);
}

.btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-1);
  box-shadow: none;
}

.btn--ghost:hover:not(:disabled) {
  background: var(--bg-2);
  border-color: var(--border);
  color: var(--text-0);
}

.btn--subtle {
  background: var(--bg-3);
  border-color: transparent;
  color: var(--text-0);
  box-shadow: none;
}

.btn--subtle:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 16%, var(--bg-3));
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--text-0);
}

.btn--danger {
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 34%, transparent);
  color: var(--danger);
  box-shadow: none;
}

.btn--danger:hover:not(:disabled) {
  background: var(--danger);
  border-color: var(--danger);
  /* --accent-contrast is the only token that is #fff in BOTH themes, which is
     exactly what a saturated fill needs. --text-inverse would go black. */
  color: var(--accent-contrast);
}

.btn.is-active {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 42%, transparent);
  color: var(--accent);
}

/* A saturated fill swallows an accent ring, so push it clear of the edge. */
.btn--primary:focus-visible,
.btn--danger:focus-visible {
  outline-offset: 3px;
}

/* ---- Sizes ----------------------------------------------------------- */

.btn--sm {
  height: 28px;
  padding-inline: var(--space-3);
  gap: var(--space-1);
  font-size: 12px;
}

.btn--sm .icon { width: 16px; height: 16px; }

.btn--lg {
  height: 44px;
  padding-inline: var(--space-5);
  border-radius: var(--radius-md);
  font-size: 14px;
}

.btn--lg .icon { width: 22px; height: 22px; }

.btn--icon {
  width: 36px;
  padding-inline: 0;
}

.btn--icon.btn--sm { width: 28px; }
.btn--icon.btn--lg { width: 44px; }

.btn--block {
  width: 100%;
}

/* Any "favourited" toggle reads as danger-red once pressed. */
.btn[aria-pressed="true"] .icon--heart {
  color: var(--danger);
}

/* ---- Loading ---------------------------------------------------------
   `color: transparent` hides the label AND the icons (their stroke/fill is
   currentColor) in one property, which is why the spinner below cannot use
   currentColor and takes explicit token colours instead. Must stay after the
   variants so it wins the same-specificity tie on `color`. */

.btn.is-loading {
  color: transparent;
  pointer-events: none;
  text-shadow: none;
}

.btn.is-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--text-0);
  border-radius: var(--radius-full);
  animation: spin 700ms linear infinite;
}

.btn--primary.is-loading::after,
.btn--danger.is-loading::after {
  border-color: color-mix(in srgb, var(--accent-contrast) 32%, transparent);
  border-top-color: var(--accent-contrast);
}

/* ==========================================================================
   Card
   ========================================================================== */

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  transition:
    transform var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.card--hover:hover,
.card--hover:focus-within {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--accent) 34%, var(--border));
  box-shadow: var(--shadow-3), var(--shadow-glow);
}

.card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--bg-inset);
  overflow: hidden;
}

.card__media > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
  padding: var(--space-3) var(--space-4) var(--space-4);
}

.card__title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-0);
}

.card__meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 12px;
  color: var(--text-2);
}

/* ==========================================================================
   Fields
   ========================================================================== */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.field--inline {
  flex-direction: row;
  align-items: center;
}

.field__label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-1);
}

.field__hint {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-2);
}

.input,
.select,
.textarea {
  width: 100%;
  min-width: 0;
  height: 36px;
  padding-inline: var(--space-3);
  background-color: var(--bg-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-0);
  font-size: 13px;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

.textarea {
  height: auto;
  min-height: 92px;
  padding: var(--space-3);
  line-height: 1.55;
}

/* Two gradients draw the caret so it can be a token colour — a data: URI
   arrow would be frozen at one hex value and break in the light theme. */
.select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: var(--space-7);
  cursor: pointer;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-1) 50%),
    linear-gradient(135deg, var(--text-1) 50%, transparent 50%);
  background-position: right 17px center, right 12px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* The native popup is not themed by the control on Linux/Windows. */
.select option {
  background: var(--bg-2);
  color: var(--text-0);
}

.input:hover:not(:disabled),
.select:hover:not(:disabled),
.textarea:hover:not(:disabled) {
  border-color: var(--border-strong);
}

.input:focus,
.select:focus,
.textarea:focus {
  border-color: var(--accent);
  box-shadow: var(--ring);
}

/* ---- Search (leading icon + trailing clear) --------------------------- */

.search {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
}

.search__icon {
  position: absolute;
  left: var(--space-3);
  width: 18px;
  height: 18px;
  color: var(--text-2);
  pointer-events: none;
  transition: color var(--dur-fast) var(--ease);
}

.search:focus-within .search__icon {
  color: var(--accent);
}

.search__input {
  padding-inline: 38px 34px;
}

.search--sm .search__input {
  height: 32px;
  padding-inline: 34px var(--space-3);
  font-size: 12px;
}

.search--sm .search__icon {
  width: 16px;
  height: 16px;
}

.search .search__clear {
  position: absolute;
  right: 4px;
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
  box-shadow: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.search .search__clear:hover:not(:disabled) {
  background: var(--bg-3);
  border-color: transparent;
  color: var(--text-0);
  box-shadow: none;
  transform: none;
}

/* Reveal on intent, or whenever there is actually something to clear. */
.search:hover .search__clear,
.search:focus-within .search__clear,
.search__input:not(:placeholder-shown) ~ .search__clear {
  opacity: 1;
  pointer-events: auto;
}

/* ==========================================================================
   Switch — a visually hidden checkbox drives a sliding thumb, so the label,
   keyboard and :checked state all come from the platform for free.
   ========================================================================== */

.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-size: 13px;
  cursor: pointer;
  user-select: none;
}

.switch__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

.switch__track {
  position: relative;
  flex: none;
  width: 40px;
  height: 22px;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg-3);
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.switch__thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-full);
  background: var(--text-1);
  box-shadow: var(--shadow-1);
  transition:
    transform var(--dur) var(--ease-spring),
    background-color var(--dur) var(--ease);
}

.switch:hover .switch__track {
  border-color: var(--border-strong);
}

.switch__input:checked ~ .switch__track {
  background: var(--accent);
  border-color: var(--accent);
}

.switch__input:checked ~ .switch__track .switch__thumb {
  background: var(--accent-contrast);
  transform: translateX(18px);
}

.switch__input:focus-visible ~ .switch__track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--ring);
}

.switch__input:disabled ~ .switch__track {
  opacity: 0.5;
}

.switch__input:disabled ~ .switch__text,
.switch:has(.switch__input:disabled) {
  cursor: not-allowed;
  color: var(--text-2);
}

.switch__text {
  color: var(--text-1);
}

/* ==========================================================================
   Range — every engine needs its own pseudo-elements; a selector list would
   invalidate the whole rule in the engine that does not know one of them, so
   WebKit and Firefox are written out separately on purpose.
   ========================================================================== */

.range {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 20px;
  background: transparent;
  cursor: pointer;
}

.range::-webkit-slider-runnable-track {
  height: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg-3);
}

.range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -5px;
  border: 2px solid var(--bg-1);
  border-radius: var(--radius-full);
  background: var(--accent);
  box-shadow: var(--shadow-1);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur) var(--ease);
}

.range::-moz-range-track {
  height: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg-3);
}

.range::-moz-range-progress {
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--accent);
}

.range::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 2px solid var(--bg-1);
  border-radius: var(--radius-full);
  background: var(--accent);
  box-shadow: var(--shadow-1);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur) var(--ease);
}

.range:hover:not(:disabled)::-webkit-slider-thumb { transform: scale(1.14); }
.range:hover:not(:disabled)::-moz-range-thumb { transform: scale(1.14); }
.range:active:not(:disabled)::-webkit-slider-thumb { transform: scale(1.02); }
.range:active:not(:disabled)::-moz-range-thumb { transform: scale(1.02); }
.range:focus-visible::-webkit-slider-thumb { box-shadow: var(--shadow-glow); }
.range:focus-visible::-moz-range-thumb { box-shadow: var(--shadow-glow); }

.range:disabled {
  cursor: not-allowed;
}

/* ==========================================================================
   Chips, badges, tag pills, keycaps
   ========================================================================== */

.chip,
.tag-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
  height: 26px;
  padding-inline: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg-2);
  color: var(--text-1);
  font-size: 12px;
  font-weight: 550;
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.tag-pill {
  min-width: 0;
  padding-inline: var(--space-3) var(--space-2);
}

/* The label is the only part allowed to shrink, so a long tag ellipses
   instead of pushing its pin/remove buttons out of the pill. */
.tag-pill > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip:hover:not(:disabled),
.tag-pill:hover:not(:disabled) {
  background: var(--bg-3);
  border-color: var(--border-strong);
  color: var(--text-0);
  transform: translateY(-1px);
}

.chip:active:not(:disabled),
.tag-pill:active:not(:disabled) {
  transform: translateY(0) scale(0.98);
}

.chip--accent,
.chip.is-active,
.tag-pill.is-active,
.tag-pill.is-selected {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 42%, transparent);
  color: var(--accent);
}

.chip--nsfw {
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 42%, transparent);
  color: var(--danger);
}

.chip .icon,
.tag-pill .icon {
  width: 14px;
  height: 14px;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 22px;
  padding-inline: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-3);
  color: var(--text-1);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding-inline: 6px;
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-xs);
  background: var(--bg-2);
  color: var(--text-1);
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  box-shadow: var(--shadow-1);
}

/* ==========================================================================
   Modal
   The root is a click-through fixed layer; only the backdrop takes pointer
   events, so an empty #modal-root can never block the app underneath.
   ========================================================================== */

.modal-root {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  pointer-events: none;
}

.modal-root:empty {
  display: none;
}

.modal-backdrop {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  background: var(--overlay);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  pointer-events: auto;
}

.modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 520px;
  max-height: min(84dvh, 780px);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--bg-2);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}

.modal--sm { max-width: 380px; }
.modal--md { max-width: 520px; }
.modal--lg { max-width: 780px; }
.modal--xl { max-width: 1080px; }

.modal__header {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg-1) 65%, transparent);
}

.modal__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-0);
}

.modal .modal__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-2);
  box-shadow: none;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.modal .modal__close:hover:not(:disabled) {
  background: var(--danger-soft);
  border-color: transparent;
  color: var(--danger);
  transform: none;
  box-shadow: none;
}

/* min-height:0 is what lets a long body scroll instead of stretching. */
.modal__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--space-5);
  overflow-y: auto;
  overscroll-behavior: contain;
  font-size: 13px;
  line-height: 1.65;
  color: var(--text-1);
}

.modal__footer {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg-1) 65%, transparent);
}

/* ==========================================================================
   Toasts — bottom-right stack, newest nearest the corner.
   ========================================================================== */

.toast-root {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  width: min(390px, calc(100vw - var(--space-7)));
  pointer-events: none;
}

.toast {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  background: var(--glass);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  color: var(--text-0);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  pointer-events: auto;
}

/* Type is carried by a 3px accent rail rather than a tinted panel, so the
   glass stays legible over any image behind it. */
.toast::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--info);
}

.toast__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  color: var(--info);
}

.toast__msg {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}

.toast--success::before { background: var(--success); }
.toast--success .toast__icon { color: var(--success); }
.toast--warning::before { background: var(--warning); }
.toast--warning .toast__icon { color: var(--warning); }
.toast--error::before { background: var(--danger); }
.toast--error .toast__icon { color: var(--danger); }
.toast--info::before { background: var(--info); }
.toast--info .toast__icon { color: var(--info); }
.toast--loading::before { background: var(--accent); }
.toast--loading .toast__icon { color: var(--accent); }

.toast__action {
  flex: none;
  align-self: center;
  height: 26px;
  padding-inline: var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.toast__action:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 44%, transparent);
}

/* ==========================================================================
   Context menu
   Hidden until opened; accepts the .is-open state class OR aria-hidden="false"
   so the visual and accessibility states can never disagree.
   animations.css owns the scale-in and its --menu-origin transform-origin.
   ========================================================================== */

.menu {
  position: fixed;
  z-index: var(--z-menu);
  min-width: 196px;
  max-width: 300px;
  max-height: min(70dvh, 520px);
  padding: var(--space-1);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  background: var(--glass);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  box-shadow: var(--shadow-3);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.menu:not(.is-open):not([aria-hidden="false"]) {
  display: none;
}

.menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text-1);
  font-size: 13px;
  text-align: left;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.menu__item .icon {
  width: 16px;
  height: 16px;
  color: var(--text-2);
}

.menu__item:hover:not(:disabled),
.menu__item:focus-visible {
  background: var(--accent-soft);
  color: var(--text-0);
}

.menu__item:hover:not(:disabled) .icon {
  color: var(--accent);
}

.menu__item--danger {
  color: var(--danger);
}

.menu__item--danger .icon,
.menu__item--danger:hover:not(:disabled) .icon {
  color: var(--danger);
}

.menu__item--danger:hover:not(:disabled) {
  background: var(--danger-soft);
  color: var(--danger);
}

/* Negative inline margin pulls the hairline out to the menu's full width. */
.menu__sep {
  height: 1px;
  margin: var(--space-1) calc(-1 * var(--space-1));
  background: var(--border);
}

/* ==========================================================================
   Loading affordances
   ========================================================================== */

/* animations.css supplies the shimmer gradient + animation; only the base
   colour and geometry belong here. */
.skeleton {
  background-color: var(--bg-3);
  border-radius: var(--radius-sm);
}

.skeleton--image {
  width: 100%;
  height: 100%;
  min-height: 180px;
  border-radius: var(--radius-lg);
}

.skeleton--card {
  height: 180px;
  border-radius: var(--radius-lg);
}

.skeleton--text {
  height: 10px;
  border-radius: var(--radius-full);
}

.skeleton--text:last-child {
  width: 62%;
}

.spinner {
  display: inline-block;
  width: 18px;
  height: 18px;
  border: 2px solid color-mix(in srgb, currentColor 22%, transparent);
  border-top-color: currentColor;
  border-radius: var(--radius-full);
  color: var(--accent);
}

.dot-pulse {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--accent);
}

.dot-pulse > * {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: currentColor;
}

.progress {
  position: relative;
  width: 100%;
  height: 3px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--text-0) 8%, transparent);
  overflow: hidden;
}

.progress__bar {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: var(--accent-gradient);
  transition: width var(--dur) var(--ease);
}

/* Indeterminate: a fixed-size accent band swept across a full-width bar by
   the `shimmer` keyframes (reversed, so it travels left → right). */
.progress.is-indeterminate .progress__bar,
.progress.is-loading .progress__bar {
  width: 100%;
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    var(--accent) 42%,
    var(--accent-2) 58%,
    transparent 100%
  );
  background-size: 42% 100%;
  background-repeat: no-repeat;
  transition: none;
  animation: shimmer 1.15s linear infinite reverse;
}

/* ==========================================================================
   Tooltip
   Opens BELOW the element: most [data-tip] controls in this app live in the
   topbar, where an upward bubble would be clipped by the viewport edge.
   The delay is on show only — leaving must feel instant.
   ========================================================================== */

[data-tip] {
  position: relative;
}

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  z-index: var(--z-menu);
  width: max-content;
  max-width: 220px;
  padding: 5px var(--space-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xs);
  background: var(--bg-3);
  color: var(--text-0);
  font-size: 11.5px;
  font-weight: 550;
  line-height: 1.35;
  white-space: nowrap;
  box-shadow: var(--shadow-2);
  translate: -50% 4px;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--dur-fast) var(--ease),
    translate var(--dur-fast) var(--ease);
}

[data-tip]::before {
  content: "";
  position: absolute;
  top: calc(100% + 3px);
  left: 50%;
  z-index: var(--z-menu);
  border: 5px solid transparent;
  border-bottom-color: var(--border-strong);
  translate: -50% 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}

[data-tip=""]::after,
[data-tip=""]::before {
  content: none;
}

[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  opacity: 1;
  translate: -50% 0;
  transition-delay: 460ms;
}

[data-tip]:hover::before,
[data-tip]:focus-visible::before {
  opacity: 1;
  transition-delay: 460ms;
}

/* Same bubble for a tooltip built as a real node. */
.tooltip {
  width: max-content;
  max-width: 240px;
  padding: 5px var(--space-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xs);
  background: var(--bg-3);
  color: var(--text-0);
  font-size: 11.5px;
  line-height: 1.35;
  box-shadow: var(--shadow-2);
}

/* ==========================================================================
   Thumbnail
   ========================================================================== */

.thumb {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-inset);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  cursor: pointer;
  transition:
    transform var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.thumb__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out), filter var(--dur) var(--ease);
}

.thumb:hover,
.thumb:focus-visible {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-2);
}

.thumb:hover .thumb__img {
  transform: scale(1.06);
}

.thumb:active {
  transform: translateY(0) scale(0.99);
}

/* Gradient scrim: it is the overlay that makes white-on-image captions and
   the action row legible over an arbitrary picture. */
.thumb__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-1);
  padding: var(--space-2);
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--bg-inset) 92%, transparent) 0%,
    color-mix(in srgb, var(--bg-inset) 45%, transparent) 38%,
    transparent 72%
  );
  color: var(--text-0);
  font-size: 11px;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.thumb:hover .thumb__overlay,
.thumb:focus-visible .thumb__overlay,
.thumb:focus-within .thumb__overlay {
  opacity: 1;
}

.thumb__actions {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: flex;
  gap: 4px;
  opacity: 0;
  translate: 0 -6px;
  transition: opacity var(--dur) var(--ease), translate var(--dur) var(--ease-out);
}

.thumb:hover .thumb__actions,
.thumb:focus-visible .thumb__actions,
.thumb:focus-within .thumb__actions {
  opacity: 1;
  translate: 0 0;
}

/* Inset ring rather than a border so the tile never changes size when it
   becomes the current image. */
.thumb--active,
.thumb.is-selected {
  border-color: var(--accent);
  box-shadow: var(--shadow-glow);
}

.thumb--active::after,
.thumb.is-selected::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 2px var(--accent);
  pointer-events: none;
}

/* The NSFW veil lives ONLY on `.is-blurred` (see views.css), which the view modules add
   or remove according to settings.blurNsfw.
   There used to be a second pair of hooks here — `.thumb--nsfw` and `[data-nsfw="true"]` —
   added as a "whichever flag the modules emit" hedge. That hedge was a bug: those
   selectors keyed off whether an image IS nsfw, not off the setting, so a history
   thumbnail carrying data-nsfw stayed blurred forever no matter what the user chose.
   One hook, driven by the setting. Do not reintroduce an attribute-based veil. */

/* ==========================================================================
   Stat tile + masonry + empty-state modifiers
   ========================================================================== */

.stat-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-2);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  transition:
    transform var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.stat-tile::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--accent-gradient);
}

.stat-tile:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-2);
}

.stat-tile__value {
  font-size: 26px;
  font-weight: 680;
  letter-spacing: -0.02em;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  color: var(--text-0);
}

.stat-tile__label {
  font-size: 12px;
  color: var(--text-2);
}

.masonry {
  columns: 4 220px;
  column-gap: var(--space-4);
}

.masonry > *,
.masonry__item {
  break-inside: avoid;
  margin-bottom: var(--space-4);
}

/* base.css owns .empty-state; these are the two variants index.html uses. */
.empty-state--sm {
  min-height: 0;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-3);
}

.empty-state--sm > svg,
.empty-state--sm .empty-state__icon {
  width: 36px;
  height: 36px;
  font-size: 18px;
}

.empty-state--inline {
  flex-direction: row;
  min-height: 0;
  padding: var(--space-3) var(--space-4);
  font-size: 12px;
}

/* index.html uses __hint where base.css defined __text; support both. */
.empty-state__hint {
  max-width: 44ch;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-2);
}

/* ==========================================================================
   Pieces introduced by the view modules (sidebar, settings, stats, dialogs).
   These class names are generated in JS, so they had no rules until now —
   which is why the sidebar rendered as a run-on line of text.
   ========================================================================== */

/* --- colour dot, used for collections and chart legends --- */
.dot {
  inline-size: 9px; block-size: 9px; border-radius: var(--radius-full);
  flex: 0 0 auto; background: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--bg-2) 70%, transparent);
}

/* --- NSFW badge --- */
.badge--nsfw {
  background: color-mix(in srgb, var(--danger) 22%, transparent);
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent);
  font-weight: 600; letter-spacing: .02em;
}

/* --- checkbox chip (tag picker) --- */
.chip--check { cursor: pointer; user-select: none; gap: var(--space-2); }
.chip--check input { accent-color: var(--accent); margin: 0; }
.chip--check:has(input:checked) {
  background: var(--accent-soft); border-color: var(--accent);
  color: var(--text-0);
}

/* --- definition grid used by the Details dialog --- */
.info-grid {
  display: grid; grid-template-columns: minmax(88px, auto) 1fr;
  gap: var(--space-2) var(--space-4); margin: 0;
}
.info-grid dt { color: var(--text-2); font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; }
.info-grid dd { margin: 0; color: var(--text-0); word-break: break-all; }

/* --- chart legend --- */
.legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); margin-top: var(--space-3); }
.legend__item { display: inline-flex; align-items: center; gap: var(--space-2); font-size: .85rem; color: var(--text-1); }
.legend__swatch { inline-size: 10px; block-size: 10px; border-radius: 3px; flex: 0 0 auto; }

/* --- context-menu extras --- */
.menu__item-icon { inline-size: 16px; block-size: 16px; flex: 0 0 auto; opacity: .8; }
.menu__item-arrow { margin-inline-start: auto; opacity: .6; }
.menu--sub { position: absolute; inset-inline-start: 100%; inset-block-start: -6px; }

/* --- prompt dialog --- */
.modal--prompt .modal__body { padding-block-end: var(--space-3); }

/* --- range control --- */
.range__wrap { align-items: center; }
.range__value {
  min-inline-size: 5ch; text-align: end; color: var(--text-1);
  font-size: .85rem; font-variant-numeric: tabular-nums;
}

/* --- bulk-selection bar --- */
.selection-bar {
  position: sticky; inset-block-end: var(--space-4); z-index: var(--z-dock);
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin-block-start: var(--space-4); padding: var(--space-3) var(--space-4);
  background: var(--glass); backdrop-filter: blur(var(--blur));
  border: 1px solid var(--border-strong); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}
.selection-bar > span:first-child { font-weight: 600; margin-inline-end: var(--space-2); }

/* --- radio row in the "add to collection" dialog --- */
.pick-row {
  align-items: center; padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm); cursor: pointer;
}
.pick-row:hover { background: var(--bg-3); }
.pick-row input { accent-color: var(--accent); }

/* --- colour swatches --- */
.swatches { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.swatch {
  inline-size: 30px; block-size: 30px; border-radius: var(--radius-full);
  cursor: pointer; position: relative; border: 2px solid transparent;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.swatch:hover { transform: scale(1.12); }
.swatch input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.swatch:has(input:checked) { border-color: var(--text-0); transform: scale(1.12); }

/* --- keyboard cheat sheet --- */
.shortcuts { display: flex; flex-direction: column; gap: var(--space-5); }
.shortcuts__group { display: flex; flex-direction: column; gap: var(--space-2); }
.shortcuts__list {
  display: grid; grid-template-columns: minmax(84px, auto) 1fr;
  gap: var(--space-2) var(--space-4); margin: 0;
}
.shortcuts__list dt { margin: 0; }
.shortcuts__list dd { margin: 0; color: var(--text-1); }

/* --- skeleton card + short text line --- */
.skeleton-card { display: flex; flex-direction: column; gap: var(--space-2); margin-block-end: var(--space-4); }
.skeleton--text-short { inline-size: 55%; }

/* --- toast close affordance --- */
.toast__close {
  margin-inline-start: auto; background: none; border: 0; cursor: pointer;
  color: var(--text-2); padding: var(--space-1); border-radius: var(--radius-xs);
}
.toast__close:hover { color: var(--text-0); background: var(--bg-3); }

/* --- utility --- */
.wrap { flex-wrap: wrap; }
