/*
 * admin.css — Phase 2 ADMIN USER-MANAGEMENT UI. Built from the existing OSR
 * system: Stone Wall .panel surfaces, 1px Mortar borders, border-radius:0, the
 * single accent (var(--color-accent)), white body text, Void Red (var(--color-danger))
 * for destructive actions only. No new visual vocabulary, no new colors — all
 * tokens reference base.css so a theme/accent change cascades automatically.
 *
 * Reuses: .module-view__title/__tagline (layout.css), .panel / .field__input /
 * .btn / .btn--primary (components.css), the .profile-counts__* and .profile-identity__*
 * tiles (profile.css), the action-modal confirm (showConfirmModal in character.js).
 *
 * Built from KDL 2026-06-27's direction (_team/design/user_profile_ux.md §7).
 */

/* nav.css sets `.nav-menu__item { display: block; }`, whose class specificity
   beats the [hidden] attribute's UA `display:none` — so a hidden menuitem would
   still render. Restore the [hidden] contract for the Admin item (auth.js toggles
   [hidden] by role): a non-admin must never see it. !important keeps it robust
   regardless of stylesheet order. */
.nav-menu__item[hidden] {
  display: none !important;
}

.admin {
  max-width: 920px;
}

.admin__page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  margin-bottom: var(--space-lg);
}

.admin__head-actions {
  display: flex;
  gap: var(--space-sm);
  flex-shrink: 0;
}

/* ----- Users-page tabs (Email v2 §G) -------------------------------------
   The "Users" page presents two tabs — User Management + Access Requests —
   over the existing SECTIONS.ADMIN / SECTIONS.ACCESS_REQUESTS routing. Mirrors
   the .bestiary__tabs OSR pattern (muted label, accent-underline active tab,
   bottom rule); no new tokens. The inactive tab is a real <button> that routes
   to its section, so it reads as a tab and behaves as a link. The strip sits
   below the page-head and above the panel it labels. */
.admin-tabs {
  display: flex;
  gap: var(--space-xs);
  margin-bottom: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

.admin-tabs__tab {
  appearance: none;
  background: none;
  border: 1px solid transparent;
  border-bottom: none;
  color: var(--color-text-muted);
  font-family: var(--font-heading-ui);
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps);
  padding: 0.5rem 1rem;
  cursor: pointer;
  margin-bottom: -1px;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.admin-tabs__tab:hover,
.admin-tabs__tab:focus-visible {
  color: var(--color-accent);
}

.admin-tabs__tab:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.admin-tabs__tab.is-active {
  color: var(--color-accent);
  border-color: var(--color-border);
  border-bottom: 2px solid var(--color-accent);
  cursor: default;
}

.admin__list-panel {
  padding: 0;
  overflow-x: auto;
}

.admin__list {
  min-width: 100%;
}

.admin__list .profile__loading,
.admin__list .auth-landing__notice-body {
  padding: var(--space-lg);
  margin: 0;
}

/* ============================================================
   User table — reuses the roster row pattern (real <table> for a11y, KDL 2026-06-27 §7).
   ============================================================ */
/* The admin/bug user table is now part of the shared .sd-dtable family
   (2026-06-24, KDL 2026-06-27 — table consolidation). The markup carries
   `class="sd-dtable admin-table"` (admin.js) / `class="sd-dtable admin-table
   bugs-admin__table"` (bug.js), so the table skeleton — width/collapse/body-font,
   the uppercase muted Cinzel sticky <th>, AND the canonical row-hover (surface
   tint + 2px accent bottom-border) — all come from .sd-dtable (lore.css). The
   previous bespoke pieces are removed: the duplicated table/thead declarations
   and the old left-rail (`inset 3px 0 0 accent`) hover, which was one of the
   three divergent hover signals the consistency audit flagged. Only the
   admin-specific cell delta stays below: <td>/<th> are vertically centred
   (the base data cell is top-aligned) because admin rows mix a status badge,
   role label, and an action-button cell that read best on one baseline. */
.admin-row > th,
.admin-row > td {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text);
  vertical-align: middle;
}

.admin-row__user {
  text-align: left;
  font-weight: 600;
  font-family: var(--font-body);
  color: var(--color-heading);
  word-break: break-word;
}

.admin-row__you {
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  letter-spacing: var(--ls-cinzel-caps);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.admin-row__role,
.admin-row__created,
.admin-row__last-login {
  white-space: nowrap;
  color: var(--color-text-muted);
}

.admin-row__created,
.admin-row__last-login {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* Disabled user row reads muted — the whole row dims (KDL 2026-06-27 §7.1). */
.admin-row.is-disabled > th,
.admin-row.is-disabled > td {
  color: var(--color-text-muted);
}

.admin-row__actions {
  display: flex;
  gap: var(--space-xs);
  flex-wrap: wrap;
  justify-content: flex-end;
}

.admin-row__note {
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  padding: 0 var(--space-sm);
  align-self: center;
}

/* ============================================================
   Status indicator — decorative dot + ALWAYS-present text word (never
   color-only, KDL 2026-06-27 §7.1). Active = accent; Disabled = muted.
   ============================================================ */
.admin-status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  white-space: nowrap;
}

.admin-status__dot {
  width: 0.55em;
  height: 0.55em;
  flex-shrink: 0;
  /* Hard-edged square, not a circle — consistent with border-radius:0. */
  background-color: var(--color-accent);
}

.admin-status.is-disabled .admin-status__dot {
  background-color: transparent;
  border: 1px solid var(--color-text-muted);
}

.admin-status__word {
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  letter-spacing: var(--ls-cinzel-caps);
  text-transform: uppercase;
}

.admin-status.is-active .admin-status__word {
  color: var(--color-accent);
}

.admin-status.is-disabled .admin-status__word {
  color: var(--color-text-muted);
}

/* ============================================================
   Small button variant for in-row actions. Inherits .btn; just denser.
   ============================================================ */
.btn--sm {
  padding: 0.3rem 0.7rem;
  font-size: 0.72rem;
}

/* ============================================================
   One-time link box (invite / reset). Shown ONLY to the issuing admin; the
   value is set via input.value (never innerHTML). Accent-stripe "live content".
   ============================================================ */
.admin__linkout {
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
}

.admin__linkout-label {
  font-family: var(--font-body);
  color: var(--color-text);
  margin: 0 0 var(--space-sm);
}

.admin__linkout-row {
  display: flex;
  gap: var(--space-sm);
  align-items: stretch;
}

.admin__linkout-input {
  flex: 1 1 auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* ============================================================
   Per-user admin profile view
   ============================================================ */
.admin-user__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.admin-user__identity .profile-identity__role {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.admin-user__actions-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.admin-user__action-row {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/* Content & roles card (per-user CS access + Image Coverage user grant). Reuses
   the settings toggle + action-row chrome; only spacing/subheads are new. */
.admin-user__content-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.admin-user__subhead {
  margin: var(--space-sm) 0 0;
  font-size: 1rem;
}

.admin-user__subhead:first-of-type {
  margin-top: 0;
}

.admin-user__image-coverage {
  margin-top: var(--space-xs);
}

/* ============================================================
   Destructive modal-confirm button (Void Red). Passed as confirmClass to
   showConfirmModal for Disable / Demote. Overrides the default modal-confirm
   attack color with the danger family (KDL 2026-06-27 §7.3 — destructive treatment).
   ============================================================ */
/* The default .btn--modal-confirm:not(.btn--item-use):not(.btn--item-drink):not(
   .btn--item-light) rule carries (0,4,0) specificity (three :not(class) + the
   class). Match-and-beat it with the :not() chain plus the extra .btn--modal-danger
   class so the danger color reliably wins regardless of source order. */
.btn--modal-confirm.btn--modal-danger:not(.btn--item-use):not(.btn--item-drink):not(.btn--item-light) {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.btn--modal-confirm.btn--modal-danger:not(.btn--item-use):not(.btn--item-drink):not(.btn--item-light):hover {
  background-color: var(--color-danger-bg);
  color: var(--color-text);
  border-color: var(--color-danger);
}

.btn--modal-confirm.btn--modal-danger:not(.btn--item-use):not(.btn--item-drink):not(.btn--item-light):focus-visible {
  outline: 2px solid var(--color-danger);
  outline-offset: 2px;
}

/* ============================================================
   Read-only creations list + preview modal (#2b)
   The admin user-detail view lists each of the target user's creations as a
   clickable control that opens a READ-ONLY preview modal. No edit/delete here.
   ============================================================ */

.admin-creations__hint {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--color-text-muted);
  margin: var(--space-sm) 0 0;
}

.admin-creations {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-top: var(--space-sm);
}

.admin-creations__heading {
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  letter-spacing: var(--ls-cinzel-caps);
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-xs);
}

.admin-creations__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* Each creation: a hard-edged chip button. Reads as actionable (hover/focus
   accent edge); opens the read-only preview modal. */
.admin-creations__item {
  appearance: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--color-text);
  background-color: var(--color-bg);
  border: 1px solid var(--color-border);
  padding: var(--space-xs) var(--space-sm);
  transition:
    border-color var(--transition-fast),
    background-color var(--transition-fast);
}

.admin-creations__item:hover {
  background-color: var(--color-surface-hover);
  border-color: var(--color-accent);
}

.admin-creations__item:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Preview modal body — a definition-style list of read-only fields. */
.admin-preview__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  text-align: left;
  max-height: 60vh;
  overflow-y: auto;
}

.admin-preview__field {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.admin-preview__field-label {
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  letter-spacing: var(--ls-cinzel-caps);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.admin-preview__field-value {
  font-family: var(--font-body);
  color: var(--color-text);
  word-break: break-word;
  white-space: pre-wrap;
}

.admin-preview__empty {
  font-family: var(--font-body);
  font-style: italic;
  color: var(--color-text-muted);
  margin: 0;
}

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 600px) {
  .admin__page-head {
    align-items: flex-start;
  }
  .admin-row__actions {
    justify-content: flex-start;
  }
}

/* ============================================================
   Phase 2.6 (#8) — ADMIN SITE SETTINGS. Reuses the .admin shell + .panel
   surfaces; adds grouped setting fields, bounded number inputs, and on/off
   module toggle switches. No new colors — all tokens from base.css. The single
   accent drives the "on" toggle; Void Red marks dangerous-setting cautions only.
   ============================================================ */
.settings {
  max-width: 880px;
}

.settings__body {
  display: grid;
  gap: var(--space-lg);
}

.settings__group {
  padding: var(--space-lg);
}

.settings__field {
  margin-bottom: var(--space-md);
}

.settings__input {
  max-width: 12rem;
}

.settings__hint {
  margin: var(--space-xs) 0 0;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* Caption beneath a stacked toggle (e.g. the Foundry Connector group). Indents to
   align under the toggle label and separates it from the next toggle. */
.settings__toggle-caption {
  margin: var(--space-xs) 0 var(--space-md);
  padding-left: calc(2.4rem + var(--space-sm));
}

.settings__caution {
  display: inline-block;
  font-family: var(--font-heading-ui);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps);
  color: var(--color-danger);
  border: 1px solid var(--color-danger);
  padding: 0.05em 0.4em;
  vertical-align: middle;
}

.settings__field-error {
  margin: var(--space-xs) 0 0;
  color: var(--color-damage);
  font-size: var(--text-sm);
  font-weight: 600;
}

.settings__field-error[hidden] {
  display: none;
}

/* Module-toggle switches grid */
.settings__toggles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--space-sm) var(--space-lg);
}

.settings__toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
}

.settings__toggle-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
}

/* Visually-hidden native checkbox drives the styled track via :checked. */
.settings__toggle-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.settings__toggle-track {
  position: relative;
  display: inline-block;
  width: 2.4rem;
  height: 1.2rem;
  background: var(--color-surface-hover);
  border: 1px solid var(--color-border);
  flex-shrink: 0;
  transition: background 0.12s ease, border-color 0.12s ease;
}

.settings__toggle-track::after {
  content: '';
  position: absolute;
  top: 1px;
  left: 1px;
  width: 1rem;
  height: 1rem;
  background: var(--color-text-muted);
  transition: transform 0.12s ease, background 0.12s ease;
}

.settings__toggle-input:checked + .settings__toggle-track {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.settings__toggle-input:checked + .settings__toggle-track::after {
  transform: translateX(1.2rem);
  background: var(--ink-on-accent);
}

.settings__toggle-input:focus-visible + .settings__toggle-track {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.settings__toggle-text {
  font-family: var(--font-body);
}

.settings__actions {
  display: flex;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.settings__actions[hidden] {
  display: none;
}

.settings__error {
  color: var(--color-damage);
  font-weight: 600;
  margin: var(--space-md) 0 0;
}

.settings__error[hidden] {
  display: none;
}

/* ============================================================
   Invite email preview + Copy-email (admin Invite flow)
   The preview holds the invite email, whose styling is entirely
   self-contained inline (table-based, dark ground) — so this only
   frames/contains it. The email renders at 600px; the container
   scrolls horizontally on narrow viewports rather than reflowing
   the email (which must keep its mail-client geometry).
   ============================================================ */
.admin-invite-email {
  margin-top: var(--space-lg);
}

.admin-invite-email__actions {
  display: flex;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.admin-invite-email__preview {
  border: 1px solid var(--color-border);
  background-color: #0f0d0b; /* matches the email ground so letterboxing reads intentional */
  max-height: 520px;
  overflow: auto;
}

/* ============================================================
   EMAIL — Settings panel (master toggle + test send) and the
   reusable proof-before-send modal preview (email-preview.js).
   The email BODY renders inside a sandboxed iframe; To/From/Subject
   are textContent rows. Shared between the Settings test result and
   the access-request / invite proof modals.
   ============================================================ */

.email-settings__hint {
  margin-top: var(--space-xs);
}

.email-settings__test {
  margin-top: var(--space-md);
}

.email-settings__test-why {
  margin-top: var(--space-xs);
}

.email-settings__result {
  margin-top: var(--space-md);
  padding: var(--space-md);
  border-left: 3px solid var(--color-accent);
  background: var(--color-surface);
}

.email-settings__result[hidden] {
  display: none;
}

.email-settings__result-msg {
  margin: 0 0 var(--space-sm);
}

.email-settings__result-msg:last-child {
  margin-bottom: 0;
}

.email-settings__result-msg--ok {
  color: var(--color-accent);
  font-weight: 600;
}

.email-settings__result-msg--err {
  color: var(--color-damage);
  font-weight: 600;
}

/* ---- shared preview block (labeled headers + sandboxed body) ---- */

.email-proof__headers {
  display: grid;
  gap: var(--space-xs);
  margin-bottom: var(--space-sm);
}

.email-proof__field {
  display: flex;
  gap: var(--space-sm);
  align-items: baseline;
}

.email-proof__field-label {
  flex: 0 0 4.5rem;
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps);
  color: var(--color-text-muted);
}

.email-proof__field-value {
  flex: 1 1 auto;
  word-break: break-word;
}

.email-proof__toggle {
  display: flex;
  gap: var(--space-xs);
  margin-bottom: var(--space-sm);
}

.email-proof__toggle-btn.is-active {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.email-proof__frame {
  display: block;
  width: 100%;
  min-height: 360px;
  border: 1px solid var(--color-border);
  background-color: #0f0d0b; /* matches the email ground */
}

.email-proof__frame[hidden],
.email-proof__text[hidden] {
  display: none;
}

.email-proof__text {
  margin: 0;
  padding: var(--space-md);
  max-height: 420px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text);
}

/* ---- proof modal sizing + result/link fallback ---- */

.email-proof__card {
  max-width: 680px;
  width: min(680px, 92vw);
}

.email-proof__preview {
  border: 1px solid var(--color-border);
  padding: var(--space-md);
  background: var(--color-bg);
  max-height: 60vh;
  overflow: auto;
}

.email-proof__result {
  margin-top: var(--space-md);
  padding: var(--space-md);
  border-left: 3px solid var(--color-accent);
  background: var(--color-surface);
}

.email-proof__result[hidden] {
  display: none;
}

.email-proof__error {
  margin: 0 0 var(--space-sm);
  color: var(--color-damage);
  font-weight: 600;
}

.email-proof__linkout-label {
  margin: 0 0 var(--space-sm);
}

.email-proof__linkout-row {
  display: flex;
  gap: var(--space-sm);
  align-items: stretch;
}

.email-proof__linkout-input {
  flex: 1 1 auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* ============================================================
   Admin "View as User" read-only panel (Phase 2.6+)
   Full-bleed overlay layered over the user-detail screen; the real view
   component mounts into __host. z-index 900 sits below the shared confirm
   modals (.action-modal = 1000) so any confirm still appears above it.
   ============================================================ */

.admin-view-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  background-color: var(--color-bg);
  display: flex;
  flex-direction: column;
  animation: action-modal-fade-in 120ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .admin-view-overlay { animation: none; }
}

.admin-view-overlay__bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-lg);
  border-bottom: 1px solid var(--color-border);
  background-color: var(--color-surface);
}

.admin-view-overlay__title {
  margin: 0;
  font-size: 1rem;
  color: var(--color-text);
  /* The accent rail echoes the read-only banner so the mode is unmistakable. */
  border-left: 3px solid var(--color-accent);
  padding-left: var(--space-sm);
}

.admin-view-overlay__sub {
  color: var(--color-text-muted);
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1 1 auto;
}

.admin-view-overlay__close {
  flex: 0 0 auto;
  margin-left: auto;
}

.admin-view-overlay__host {
  flex: 1 1 auto;
  overflow: auto;
  padding: var(--space-md) var(--space-lg);
}

/* In-view read-only banner (character sheet + item). A muted strip with the
   accent rail; never relies on color alone (the words say "read-only"). */
.char-dash__ro-banner {
  display: flex;
  align-items: center;
  gap: var(--space-xs, 6px);
  padding: var(--space-xs, 6px) var(--space-sm);
  margin-bottom: var(--space-sm);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  background-color: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

/* The banner icon is an inline <svg> with NO intrinsic size. Without an explicit
   box it balloons to fill the (unconstrained) full-bleed admin-view overlay host,
   rendering as a giant grey "page" glyph at the top of the read-only sheet (the
   image Ken kept seeing). Pin it to a small inline glyph and forbid it from
   flex-growing. (Matches the .char-dash__panel-icon sizing pattern.) */
.char-dash__ro-banner-icon {
  flex: none;
  width: 1rem;
  height: 1rem;
  opacity: 0.8;
}

/* Any control caught by the read-only sweep reads as inert (the disabled
   attribute already blocks interaction; this makes it look intentional). */
.is-readonly-disabled,
.char-dash--readonly [disabled],
.item-edit--readonly [disabled],
.enc-readonly [disabled] {
  opacity: 0.5;
  cursor: default !important;
}

/* ============================================================
   Invite-a-user modal (Surface 3) — email OR Discord ID. Reuses the shared
   .action-modal chrome (card / title / body / actions); these rules only style
   the by-email / by-Discord-ID toggle and the field spacing. The toggle segs are
   themed .btn--secondary; the active seg reads as selected via the same accent
   fill the secondary buttons use on hover, so there's no new color vocabulary.
   ============================================================ */

.admin-invite-modal__toggle {
  display: flex;
  gap: var(--space-sm);
  margin: var(--space-xs) 0 var(--space-sm);
}

.admin-invite-modal__seg {
  flex: 1 1 0;
}

/* Active (selected) segment: the accent fill that .btn--secondary uses on hover,
   so the chosen mode reads clearly. The aria-checked attribute carries the real
   state for assistive tech — color is never the sole cue. */
.admin-invite-modal__seg.is-active {
  background-color: var(--color-accent);
  color: var(--ink-on-accent, var(--color-text));
  border-color: var(--color-accent);
}

.admin-invite-modal__field {
  margin: 0;
}

.admin-invite-modal__field .field__help {
  margin: var(--space-xs) 0 0;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

.admin-invite-modal__error {
  margin: var(--space-xs) 0 0;
  padding: var(--space-sm) var(--space-md);
  background-color: var(--color-danger-bg);
  border-left: 3px solid var(--color-danger);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.4;
}
.admin-invite-modal__error[hidden] { display: none; }

/* ============================================================
 * DISCORD LINK STATE (DEF-2)
 * List row: a small accent glyph beside the username when the user
 * has linked Discord (the accessible name/title carries the username,
 * set via setAttribute — XSS-inert). Detail view: a "Discord: <name>"
 * line in the identity meta, em dash when unlinked.
 * ============================================================ */
.admin-row__discord {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-left: var(--space-2xs, 0.25rem);
  color: var(--color-accent);
}

.admin-row__discord svg {
  display: block;
}

/* Discord line dims with the rest of a disabled row. */
.admin-row.is-disabled .admin-row__discord {
  color: var(--color-text-muted);
}

.admin-user__discord-name {
  color: var(--color-text);
}

/* ============================================================
 * IMAGE COVERAGE (batch-11 #3) — admin-only, READ-ONLY diagnostic.
 * A per-category responsive grid of art "cells": PRESENT cells show the
 * actual thumbnail; MISSING cells show a clear danger-bordered placeholder
 * (broken-image glyph + key + expected served path) so the exact combination
 * and filename to generate is visible. On-brand etched/dark aesthetic.
 * ============================================================ */
.imgcov__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

/* --- summary line + show-only-missing toggle --- */
.imgcov-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
}

.imgcov-summary__stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  font-family: var(--font-heading-ui);
  letter-spacing: 0.02em;
}

.imgcov-summary__stat {
  color: var(--color-text-muted);
}

.imgcov-summary__stat strong {
  color: var(--color-text);
  font-size: 1.25rem;
}

.imgcov-summary__stat--missing.is-gap strong {
  color: var(--color-danger);
}

.imgcov-summary__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  color: var(--color-text-muted);
  user-select: none;
}

.imgcov-summary__toggle input {
  accent-color: var(--color-accent);
  width: 1rem;
  height: 1rem;
}

/* --- type picker (per-type sections) ---
 * Reuses the app-standard .field / .field__label / .field__select select. The
 * <select> is 100% width (mobile-safe at 375px); capped so it reads as a single
 * control rather than a full-width bar on wide screens. */
.imgcov-picker {
  max-width: 24rem;
}

/* Section host: single selected category renders here on demand. Spacing comes
 * from .imgcov__body's flex gap; this just guards width on narrow screens. */
.imgcov__section {
  min-width: 0;
}

/* Grouped entries (e.g. "Defaults") stack several category sections in the host;
   separate consecutive sections so each reads as its own panel. */
.imgcov__section .imgcov-cat + .imgcov-cat {
  margin-top: var(--space-lg);
}

/* --- category section --- */
.imgcov-cat__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-md);
}

.imgcov-cat__title {
  margin: 0;
  flex: 1 1 auto;
}

.imgcov-cat__count {
  font-family: var(--font-mono);
  color: var(--color-text-muted);
}

.imgcov-cat__missing-badge {
  font-family: var(--font-heading-ui);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-danger);
  border: 1px solid var(--color-danger);
  background: var(--color-danger-bg);
  border-radius: 2px;
  padding: 0.1rem 0.5rem;
}

.imgcov-cat__complete {
  font-family: var(--font-heading-ui);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* --- the responsive coverage grid --- */
.imgcov-cat__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: var(--space-sm);
}

.imgcov-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: 2px;
  background: var(--color-bg);
  text-align: center;
  min-height: 8rem;
}

.imgcov-cell__thumb {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  overflow: hidden;
}

.imgcov-cell__thumb:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.imgcov-cell__thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.imgcov-cell__key {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--color-text-muted);
  word-break: break-word;
  line-height: 1.2;
}

/* MISSING cell — visually obvious danger treatment. */
.imgcov-cell--missing {
  border: 1px solid var(--color-danger);
  background: var(--color-danger-bg);
  justify-content: center;
}

.imgcov-cell__glyph {
  font-size: 1.75rem;
  line-height: 1;
  color: var(--color-danger);
}

.imgcov-cell__tag {
  font-family: var(--font-heading-ui);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-danger);
}

.imgcov-cell__path {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--color-text-muted);
  word-break: break-all;
  line-height: 1.2;
}

/* --- click-to-swap / upload affordance (#7) --- */
/* Per-cell upload button. Present cells -> "Replace image"; missing -> "Add image".
   A real <button> so it's keyboard-actionable + announced; the cell stays a
   listitem and the present cell's view-image link is untouched alongside it. */
.imgcov-cell__swap {
  margin-top: auto;
  font-family: var(--font-heading-ui);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-accent);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: 2px;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}

.imgcov-cell__swap:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-hover);
}

.imgcov-cell__swap:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.imgcov-cell__swap:disabled {
  opacity: 0.6;
  cursor: progress;
}

/* While a swap upload is in flight, dim the cell so the action reads as pending. */
.imgcov-cell--uploading {
  opacity: 0.7;
}

/* DEFAULT cell (mapSymbols) — Ken: a glyph already set BY DEFAULT is shown but
   outlined in red to say "there's a default here, but no custom image yet." So it
   renders the shipped default thumbnail like a present cell, but with a red outline
   and a DEFAULT tag; it is still a swap target ("Replace image"). It counts as a gap
   until a custom image lands, at which point JS flips it to .imgcov-cell--present. */
.imgcov-cell--default {
  border: 2px solid var(--color-danger);
}

.imgcov-cell--default .imgcov-cell__thumb {
  border-color: var(--color-danger);
}

.imgcov-cell__tag--default {
  color: var(--color-danger);
}

/* --- map-symbol sub-groups (mirror the Browse Symbols modal's grouped look) ---
   A sub-grouped category stacks .imgcov-subgroup blocks inside its section: a small
   uppercase heading + present/total count, then the normal .imgcov-cat__grid of cells.
   Spacing/typography track maps.css .maps-symbols__grouphead so the two read the same,
   while the cells stay in the .imgcov-* family. */
.imgcov-subgroup + .imgcov-subgroup {
  margin-top: var(--space-md);
}

.imgcov-subgroup__head {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-heading-ui);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps, 0.04em);
  color: var(--color-heading);
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}

.imgcov-subgroup__count {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--color-text-muted);
}

/* Room Kinds glyphs are room stamps drawn to sit on the dungeon map's parchment
   canvas, so on the neutral coverage surface they read poorly. Give ONLY the Room
   Kinds cells' image area the map's default page color so the glyphs read as they do
   on the map. Source: maps.js PALETTES.parchment.page (#e8dcc0, the default canvas).
   Scoped to the Room Kinds sub-group + the thumbnail area only — every other
   sub-group and category keeps the neutral surface. Applies in both cell states; the
   red-outline "default" marker (2px danger border on the cell + thumb) and the
   present treatment still read clearly against the light parchment. */
.imgcov-subgroup--room-kinds .imgcov-cell__thumb {
  background: #e8dcc0;
}

/* --- "show only missing" view filter (no refetch, no mutation) --- */
.imgcov--only-missing .imgcov-cell--present {
  display: none;
}

.imgcov--only-missing .imgcov-cat:not(.imgcov-cat--has-gap) {
  display: none;
}

/* A sub-group with no gaps hides under "only missing" too, mirroring the section
   filter above; default (red) and absent cells count as gaps and stay visible. */
.imgcov--only-missing .imgcov-subgroup:not(.imgcov-subgroup--has-gap) {
  display: none;
}

/* ============================================================
   Email Templates editor (Settings > Email section)
   Edits the invite / early-access-approved templates: subject + body, an
   insertable placeholder legend, save / reset / preview. The rendered preview
   reuses the shared .email-proof__* sandboxed renderer (email-preview.js).
   ============================================================ */

.email-templates {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.email-templates__pick {
  max-width: 22rem;
}

/* Custom-vs-default state badge under the picker. */
.email-templates__state {
  display: inline-block;
  align-self: flex-start;
  padding: 0.15em 0.6em;
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps);
  border: 1px solid var(--color-border);
}

.email-templates__state--custom {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.email-templates__state--default {
  color: var(--color-text-muted);
}

/* Neutral prompt shown until a template is picked; the editor body hides while
   it is visible so the empty option never reads as "this template is blank". */
.email-templates__empty {
  margin-top: var(--space-sm);
}

.email-templates__empty[hidden],
.email-templates__editor-body[hidden] {
  display: none;
}

.email-templates__body {
  width: 100%;
  min-height: 14rem;
  resize: vertical;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* Placeholder legend — clickable token chips inserted at the cursor. */
.email-templates__legend {
  padding: var(--space-md);
  border-left: 3px solid var(--color-accent);
  background: var(--color-surface);
}

.email-templates__legend-title {
  margin: 0 0 var(--space-sm);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.email-templates__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.email-templates__chip {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.email-templates__error {
  margin: 0;
}

.email-templates__error[hidden] {
  display: none;
}

/* Offending {{tokens}} from a 400 placeholder rejection, listed inline. */
.email-templates__bad-tokens {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-left: var(--space-xs);
}

.email-templates__bad-token {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  padding: 0 0.35em;
  border-radius: 3px;
  background: var(--color-danger-bg);
  color: var(--color-text);
  border: 1px solid var(--color-danger);
}

.email-templates__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* Why the Send-test button is gated (e.g. no template picked yet). */
.email-templates__test-why {
  margin-top: var(--space-xs);
}

.email-templates__test-why[hidden] {
  display: none;
}

.email-templates__test-result {
  margin-top: var(--space-md);
}

.email-templates__preview {
  margin-top: var(--space-sm);
}

.email-templates__preview[hidden] {
  display: none;
}

.email-templates__preview-label {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps);
  color: var(--color-text-muted);
}

/* ----- Body Text / HTML tabs --------------------------------------------- */
.email-templates__tabs {
  display: flex;
  gap: 2px;
  border-bottom: 1px solid var(--color-border);
}

.email-templates__tab {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-bottom: none;
  margin-bottom: -1px;
  padding: 0.4em 1em;
  font-family: var(--font-heading-ui);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--ls-cinzel-caps);
  color: var(--color-text-muted);
  cursor: pointer;
}

.email-templates__tab:hover {
  color: var(--color-text);
}

.email-templates__tab.is-active {
  color: var(--color-accent);
  border-color: var(--color-border);
  border-bottom: 1px solid var(--color-surface);
  background: var(--color-surface);
}

.email-templates__tab:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.email-templates__panel[hidden] {
  display: none;
}

/* ----- WYSIWYG toolbar --------------------------------------------------- */
.email-templates__toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-sm) 0;
}

.email-templates__tool {
  min-width: 2.4em;
}

.email-templates__tool--bold {
  font-weight: 700;
}

.email-templates__tool--italic {
  font-style: italic;
}

/* ----- WYSIWYG canvas — painted to echo the themed email shell ----------
   Alex's TORCH-ON-BLACK re-skin (2026-06-30): the canvas reads like the sent
   email (serif stack, sizes, colours from the shell) so authoring matches
   sending. The shell's content cell collapsed from #1e1a16 parchment to solid
   #000000, so the canvas ground is now black; the 1px #2e2820 mortar border is
   the carved-panel edge on black. Type colours are UNCHANGED by the re-skin
   (body #d8d7d7, heading #e8e7e7, link Cornflower #6495ed — already AAA on
   black). The full themed shell (torch header + footer) is NOT re-drawn here;
   the live Preview renders the server-built shell in the sandboxed iframe. */
.email-templates__wysiwyg {
  min-height: 14rem;
  max-height: 28rem;
  overflow-y: auto;
  padding: var(--space-md) var(--space-lg);
  border: 1px solid #2e2820;
  background: #000000;
  color: #d8d7d7;
  font-family: Georgia, 'Times New Roman', Cambria, serif;
  font-size: 1.0625rem; /* ~17px, shell body copy */
  line-height: 1.6;
}

.email-templates__wysiwyg:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 0;
}

.email-templates__wysiwyg p {
  margin: 0 0 1em;
  font-family: inherit;
  color: inherit;
}

.email-templates__wysiwyg h1 {
  margin: 0 0 0.5em;
  font-family: inherit;
  font-size: 1.5rem; /* ~24px */
  font-weight: 700;
  color: #e8e7e7;
}

.email-templates__wysiwyg h2 {
  margin: 1em 0 0.4em;
  font-family: inherit;
  font-size: 1.1875rem; /* ~19px */
  font-weight: 700;
  color: #e8e7e7;
}

.email-templates__wysiwyg strong {
  font-weight: 700;
  color: #e8e7e7;
}

.email-templates__wysiwyg a {
  color: #6495ed;
  text-decoration: underline;
}

.email-templates__wysiwyg ul,
.email-templates__wysiwyg ol {
  margin: 0 0 1em;
  padding-left: 1.4em;
}

.email-templates__wysiwyg li {
  margin: 0 0 0.25em;
}

.email-templates__wysiwyg-hint {
  margin-top: var(--space-sm);
}
