<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
import { useSystemsStore } from "@/stores/systems";
import { useCanonicalSystemsStore } from "@/stores/canonicalSystems";
import { useToastStore } from "@/stores/toast";
import { getProjectIssues } from "@/api/systems";
import type { System, TaskLike } from "@/api/systems";
import type { CanonicalMatch } from "@/api/canonicalSystems";
import { apiErrorMessage } from "@/api/client";
import { Pencil, Trash2, Archive, ArchiveRestore } from "lucide-vue-next";

const props = defineProps<{ projectId: number }>();

const store = useSystemsStore();
const canon = useCanonicalSystemsStore();
const toast = useToastStore();

const error = ref<string | null>(null);
const showArchived = ref(false);
const issues = ref<TaskLike[]>([]);

// Create state
const showCreate = ref(false);
const newName = ref("");
const newDescription = ref("");
// The global area, chosen explicitly. A PICKER rather than a live matcher on
// purpose: reproducing the server's slug rule in TypeScript would give this
// feature two matchers to keep in step, which is the exact drift the catalog
// exists to end. The server still applies an exact hit on submit.
const newCanonicalId = ref<number | null>(null);
const creating = ref(false);
// An `overlap` the server offered after a create — an offer, never applied.
const suggestion = ref<{ systemId: number; match: CanonicalMatch } | null>(null);

// Edit state
const editingId = ref<number | null>(null);
const editName = ref("");
const editDescription = ref("");
const editCanonicalId = ref<number | null>(null);
const savingEdit = ref(false);

// Mapping review
const showReview = ref(false);
const reviewBusy = ref<number | null>(null);

// Delete confirmation
const deletingSystem = ref<System | null>(null);

const systems = computed<System[]>(() => store.systemsByProject[props.projectId] ?? []);
const activeSystems = computed(() => systems.value.filter((s) => s.status === "active"));
const archivedSystems = computed(() => systems.value.filter((s) => s.status === "archived"));
const visibleSystems = computed(() =>
  showArchived.value ? systems.value : activeSystems.value,
);

const proposals = computed(() => canon.proposalsByProject[props.projectId] ?? []);

function areaName(system: System): string | null {
  return canon.byId(system.canonical_id)?.name ?? null;
}

async function load() {
  error.value = null;
  try {
    await store.fetchSystems(props.projectId);
  } catch {
    error.value = "Failed to load systems.";
  }
  try {
    issues.value = await getProjectIssues(props.projectId);
  } catch {
    issues.value = [];
  }
  // Both fail soft: the catalog is a naming aid, and a review prompt that
  // cannot load must not take the Systems list down with it.
  await canon.fetchCatalog();
  try {
    await canon.fetchProposals(props.projectId);
  } catch {
    /* no proposals shown */
  }
}

onMounted(load);
watch(() => props.projectId, load);

function openCreate() {
  showCreate.value = true;
  newName.value = "";
  newDescription.value = "";
  newCanonicalId.value = null;
}

function cancelCreate() {
  showCreate.value = false;
  newName.value = "";
  newDescription.value = "";
  newCanonicalId.value = null;
}

async function submitCreate() {
  const name = newName.value.trim();
  if (!name || creating.value) return;
  creating.value = true;
  try {
    const created = await store.createSystem(props.projectId, {
      name,
      description: newDescription.value.trim() || undefined,
      canonical_id: newCanonicalId.value ?? undefined,
    });
    cancelCreate();
    if (created.canonical_suggestion) {
      // An overlap: shown as an offer beside the new System, never applied.
      suggestion.value = { systemId: created.id, match: created.canonical_suggestion };
    }
    toast.show(
      created.canonical_id
        ? `System created and filed under ${canon.byId(created.canonical_id)?.name}`
        : "System created",
    );
  } catch (e) {
    // 409 = this project already has that System. Say WHICH one, so the
    // answer is actionable rather than "it didn't work".
    toast.show(apiErrorMessage(e, "Failed to create system"), "error");
  } finally {
    creating.value = false;
  }
}

async function acceptSuggestion() {
  const pending = suggestion.value;
  if (!pending) return;
  suggestion.value = null;
  try {
    await canon.mapSystem(props.projectId, pending.systemId, pending.match.id);
    await store.fetchSystems(props.projectId);
    toast.show(`Filed under ${pending.match.name}`);
  } catch {
    /* the store already reported it */
  }
}

async function applyProposal(systemId: number, canonicalId: number) {
  reviewBusy.value = systemId;
  try {
    await canon.mapSystem(props.projectId, systemId, canonicalId);
    await store.fetchSystems(props.projectId);
  } catch {
    /* the store already reported it */
  } finally {
    reviewBusy.value = null;
  }
}

function startEdit(system: System) {
  editingId.value = system.id;
  editName.value = system.name;
  editDescription.value = system.description;
  editCanonicalId.value = system.canonical_id;
}

function cancelEdit() {
  editingId.value = null;
}

async function submitEdit(system: System) {
  const name = editName.value.trim();
  if (!name || savingEdit.value) return;
  savingEdit.value = true;
  try {
    await store.updateSystem(props.projectId, system.id, {
      name,
      description: editDescription.value.trim(),
    });
    // The mapping is a separate write with its own validation — one column,
    // one writer (services/canonical_systems.set_system_canonical).
    if (editCanonicalId.value !== system.canonical_id) {
      await canon.mapSystem(props.projectId, system.id, editCanonicalId.value);
      await store.fetchSystems(props.projectId);
    }
    editingId.value = null;
    toast.show("System updated");
  } catch {
    toast.show("Failed to update system", "error");
  } finally {
    savingEdit.value = false;
  }
}

async function archive(system: System) {
  try {
    await store.archiveSystem(props.projectId, system.id);
    toast.show("System archived");
  } catch {
    toast.show("Failed to archive system", "error");
  }
}

async function unarchive(system: System) {
  try {
    await store.unarchiveSystem(props.projectId, system.id);
    toast.show("System restored");
  } catch {
    toast.show("Failed to restore system", "error");
  }
}

async function confirmDelete() {
  const system = deletingSystem.value;
  if (!system) return;
  deletingSystem.value = null;
  try {
    await store.deleteSystem(props.projectId, system.id);
    toast.show("System deleted");
  } catch {
    toast.show("Failed to delete system", "error");
  }
}
</script>

<template>
  <div class="systems-section">
    <!-- Open issues -->
    <div v-if="issues.length" class="open-issues">
      <div class="open-issues-label">⚠ Open issues ({{ issues.length }})</div>
      <ul class="issue-list">
        <li v-for="issue in issues" :key="issue.id" class="issue-item">
          <router-link :to="`/tasks/${issue.id}`" class="issue-link">
            <span class="issue-mark" :class="`imk-${issue.status}`">{{ issue.status === 'in_progress' ? '▸' : '○' }}</span>
            <span class="issue-name">{{ issue.title }}</span>
            <span v-if="issue.systems && issue.systems.length" class="issue-systems">
              <span v-for="s in issue.systems" :key="s.id" class="issue-sys-chip">{{ s.name }}</span>
            </span>
          </router-link>
        </li>
      </ul>
    </div>

    <!-- Mapping review. Only appears when there is something to decide, and
         it says HOW MANY rather than nagging with a permanent banner. -->
    <div v-if="proposals.length" class="area-review">
      <button class="area-review-head" @click="showReview = !showReview">
        <span class="area-review-count">{{ proposals.length }}</span>
        {{ proposals.length === 1 ? "system" : "systems" }} may belong to a shared area
        <span class="area-review-chev">{{ showReview ? "▾" : "▸" }}</span>
      </button>
      <ul v-if="showReview" class="area-proposals">
        <li v-for="p in proposals" :key="p.system_id" class="area-proposal">
          <div class="area-proposal-text">
            <span class="area-proposal-name">{{ p.system_name }}</span>
            <span class="area-proposal-arrow" aria-hidden="true">→</span>
            <span class="area-proposal-target">{{ p.canonical_name }}</span>
            <!-- The basis is the decision the reviewer is making: `exact`
                 differs only in spelling, `overlap` is a judgment call.
                 Showing them identically is how a wrong mapping is waved
                 through, so they never share a style. -->
            <span
              class="area-basis"
              :class="p.basis === 'exact' ? 'area-basis--exact' : 'area-basis--overlap'"
              :title="
                p.basis === 'exact'
                  ? 'Same name up to spelling — safe to accept.'
                  : 'Shares a word. Accept only if it is really the same area.'
              "
            >{{ p.basis === "exact" ? "same name" : "similar" }}</span>
          </div>
          <div class="area-proposal-actions">
            <button
              class="btn-primary btn-compact"
              :disabled="reviewBusy === p.system_id"
              @click="applyProposal(p.system_id, p.canonical_id)"
            >
              {{ reviewBusy === p.system_id ? "Filing…" : "File here" }}
            </button>
            <button
              class="btn-ghost btn-compact"
              @click="canon.dismissProposal(props.projectId, p.system_id)"
            >
              Not this
            </button>
          </div>
        </li>
      </ul>
    </div>

    <!-- An overlap offered by the server after a create. Never applied. -->
    <div v-if="suggestion" class="area-offer">
      <span>
        Is this the same area as
        <strong>{{ suggestion.match.name }}</strong>?
      </span>
      <div class="area-proposal-actions">
        <button class="btn-primary btn-compact" @click="acceptSuggestion">File it there</button>
        <button class="btn-ghost btn-compact" @click="suggestion = null">No, it's ours</button>
      </div>
    </div>

    <!-- Toolbar -->
    <div class="systems-toolbar">
      <button v-if="!showCreate" class="btn-ghost btn-inline btn-add-system" @click="openCreate">
        + System
      </button>
      <label v-if="archivedSystems.length" class="archived-toggle">
        <input v-model="showArchived" type="checkbox" class="archived-checkbox" />
        Show archived ({{ archivedSystems.length }})
      </label>
    </div>

    <!-- Create form -->
    <form v-if="showCreate" class="system-form" @submit.prevent="submitCreate">
      <input
        v-model="newName"
        class="fs-input system-input"
        placeholder="System name"
        aria-label="System name"
        autofocus
        @keydown.escape="cancelCreate"
      />
      <textarea
        v-model="newDescription"
        class="fs-input system-textarea"
        rows="2"
        placeholder="What is this subsystem responsible for? (optional)"
        aria-label="System description"
      ></textarea>
      <label v-if="canon.catalog.length" class="area-field">
        <span class="area-label">Shared area</span>
        <select v-model="newCanonicalId" class="fs-input area-select" aria-label="Shared area">
          <option :value="null">None — specific to this project</option>
          <option v-for="entry in canon.catalog" :key="entry.id" :value="entry.id">
            {{ entry.name }}
          </option>
        </select>
        <!-- .field-hint is the shared hint class beside .fs-input
             (components.css) — not restated scoped. -->
        <span class="field-hint">
          Files this system under an area shared by every project. Your name stays as you typed it.
        </span>
      </label>
      <div class="system-form-actions">
        <button type="submit" class="btn-primary btn-compact" :disabled="!newName.trim() || creating">
          {{ creating ? "Creating…" : "Create" }}
        </button>
        <button type="button" class="btn-ghost btn-compact" @click="cancelCreate">Cancel</button>
      </div>
    </form>

    <!-- Loading -->
    <div v-if="store.loading && !systems.length" class="systems-skeleton" aria-label="Loading systems">
      <div class="skel-row"></div>
      <div class="skel-row skel-row--short"></div>
      <div class="skel-row"></div>
    </div>

    <!-- Error -->
    <p v-else-if="error" class="error-msg">{{ error }}</p>

    <!-- Empty -->
    <div v-else-if="!visibleSystems.length" class="systems-empty">
      <p class="empty-title">No systems yet</p>
      <p class="empty-sub">Define a reusable subsystem or area to organize issues against.</p>
      <button v-if="!showCreate" class="btn-primary btn-compact" @click="openCreate">+ Create a system</button>
    </div>

    <!-- List -->
    <ul v-else class="systems-list">
      <li
        v-for="system in visibleSystems"
        :key="system.id"
        class="system-card"
        :class="{ 'system-card--archived': system.status === 'archived' }"
      >
        <!-- Inline edit -->
        <template v-if="editingId === system.id">
          <form class="system-form system-form--inline" @submit.prevent="submitEdit(system)">
            <input
              v-model="editName"
              class="fs-input system-input"
              placeholder="System name"
              aria-label="System name"
              autofocus
              @keydown.escape="cancelEdit"
            />
            <textarea
              v-model="editDescription"
              class="fs-input system-textarea"
              rows="2"
              placeholder="Description (optional)"
              aria-label="System description"
            ></textarea>
            <label v-if="canon.catalog.length" class="area-field">
              <span class="area-label">Shared area</span>
              <select v-model="editCanonicalId" class="fs-input area-select" aria-label="Shared area">
                <option :value="null">None — specific to this project</option>
                <option v-for="entry in canon.catalog" :key="entry.id" :value="entry.id">
                  {{ entry.name }}
                </option>
              </select>
            </label>
            <div class="system-form-actions">
              <button type="submit" class="btn-primary btn-compact" :disabled="!editName.trim() || savingEdit">
                {{ savingEdit ? "Saving…" : "Save" }}
              </button>
              <button type="button" class="btn-ghost btn-compact" @click="cancelEdit">Cancel</button>
            </div>
          </form>
        </template>

        <!-- Display -->
        <template v-else>
          <span
            class="system-swatch"
            :style="{ background: system.color || 'var(--fs-text-tertiary)' }"
            aria-hidden="true"
          ></span>
          <div class="system-body">
            <div class="system-name-row">
              <span class="system-name">{{ system.name }}</span>
              <span
                class="issue-badge"
                :title="`${system.open_issue_count} open issue(s)`"
              >{{ system.open_issue_count }} open</span>
              <span v-if="system.status === 'archived'" class="archived-badge">Archived</span>
              <!-- Not a TagPill: that recipe prefixes "#" and means a tag.
                   This is the shared AREA this system is an instance of. -->
              <span
                v-if="areaName(system)"
                class="area-chip"
                :title="`Filed under the shared area “${areaName(system)}” — records and rules about this area line up across projects.`"
              >{{ areaName(system) }}</span>
            </div>
            <p v-if="system.description" class="system-description">{{ system.description }}</p>
          </div>
          <div class="system-actions">
            <button class="action-btn" title="Edit" aria-label="Edit system" @click="startEdit(system)">
              <Pencil :size="16" />
            </button>
            <button
              v-if="system.status === 'active'"
              class="action-btn"
              title="Archive"
              aria-label="Archive system"
              @click="archive(system)"
            >
              <Archive :size="16" />
            </button>
            <button
              v-else
              class="action-btn"
              title="Restore"
              aria-label="Restore system"
              @click="unarchive(system)"
            >
              <ArchiveRestore :size="16" />
            </button>
            <button
              class="action-btn action-delete"
              title="Delete"
              aria-label="Delete system"
              @click="deletingSystem = system"
            >
              <Trash2 :size="16" />
            </button>
          </div>
        </template>
      </li>
    </ul>

    <!-- Delete confirmation -->
    <teleport to="body">
      <div v-if="deletingSystem" class="modal-overlay" @click.self="deletingSystem = null">
        <div class="modal-card">
          <h3 class="modal-title">Delete System</h3>
          <p class="modal-message">
            Delete <strong>{{ deletingSystem.name }}</strong>? This cannot be undone.
          </p>
          <div class="modal-actions">
            <button class="modal-btn" @click="deletingSystem = null">Cancel</button>
            <button class="modal-btn modal-btn-danger" @click="confirmDelete">Delete</button>
          </div>
        </div>
      </div>
    </teleport>
  </div>
</template>

<style scoped>
.systems-section { display: flex; flex-direction: column; gap: 0.75rem; }

/* ── Open issues ──────────────────────────────────────────────── */
.open-issues { display: flex; flex-direction: column; gap: 0.35rem; }
.open-issues-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fs-text-tertiary); }
.issue-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.issue-link { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.5rem; border-radius: var(--fs-radius-sm); text-decoration: none; color: var(--fs-text-primary); font-size: 0.85rem; }
.issue-link:hover { background: var(--fs-surface-raised); }
.issue-mark { color: var(--fs-text-tertiary); flex-shrink: 0; }
.issue-mark.imk-in_progress { color: var(--fs-accent); }
.issue-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.issue-systems { display: flex; gap: 0.25rem; flex-shrink: 0; flex-wrap: wrap; }
.issue-sys-chip { font-size: 0.66rem; color: var(--fs-text-secondary); background: var(--fs-surface-raised); border-radius: 999px; padding: 0.05rem 0.4rem; }

/* ── Shared-area mapping (milestone 307) ──────────────────────────
   The review is a disclosure, not a banner: it exists only while there is
   something to decide, and collapses to one line until opened. */
.area-review {
  border: 1px solid var(--fs-border-color);
  border-radius: var(--fs-radius-lg);
  background: var(--fs-surface-raised);
}
.area-review-head {
  display: flex;
  align-items: center;
  gap: var(--fs-space-2);
  width: 100%;
  padding: var(--fs-space-3);
  background: none;
  border: none;
  color: var(--fs-text-secondary);
  font: inherit;
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
  border-radius: var(--fs-radius-lg);
}
.area-review-head:hover { color: var(--fs-text-primary); }
.area-review-head:focus-visible { outline: none; box-shadow: var(--fs-focus-ring); }
.area-review-count {
  background: var(--fs-accent-soft);
  color: var(--fs-accent);
  border-radius: var(--fs-radius-pill);
  padding: 0.05rem 0.45rem;
  font-variant-numeric: tabular-nums;
}
.area-review-chev { margin-left: auto; color: var(--fs-text-tertiary); }

.area-proposals { list-style: none; margin: 0; padding: 0 var(--fs-space-3) var(--fs-space-3); display: flex; flex-direction: column; gap: var(--fs-space-2); }
.area-proposal {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fs-space-3);
  flex-wrap: wrap;
  padding: var(--fs-space-2) var(--fs-space-3);
  background: var(--fs-surface-page);
  border: 1px solid var(--fs-border-color);
  border-radius: var(--fs-radius-md);
}
.area-proposal-text { display: flex; align-items: center; gap: var(--fs-space-2); flex-wrap: wrap; font-size: 0.85rem; min-width: 0; }
.area-proposal-name { color: var(--fs-text-primary); }
.area-proposal-arrow { color: var(--fs-text-tertiary); }
.area-proposal-target { color: var(--fs-accent); }
.area-proposal-actions { display: flex; gap: var(--fs-space-2); flex-shrink: 0; }

/* The two bases must never look alike — one is mechanical, the other is the
   reviewer's judgment, and that difference is the whole decision. */
.area-basis { font-size: 0.68rem; border-radius: var(--fs-radius-sm); padding: 0.05rem 0.4rem; }
.area-basis--exact { background: var(--fs-status-done-bg); color: var(--fs-status-done); }
.area-basis--overlap { background: var(--fs-priority-medium-bg); color: var(--fs-priority-medium); }

.area-offer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fs-space-3);
  flex-wrap: wrap;
  padding: var(--fs-space-3);
  font-size: 0.85rem;
  color: var(--fs-text-secondary);
  background: var(--fs-accent-faint);
  border: 1px solid var(--fs-border-color);
  border-radius: var(--fs-radius-lg);
}

.area-field { display: flex; flex-direction: column; gap: 0.3rem; }
.area-label { font-size: 0.78rem; color: var(--fs-text-tertiary); }
.area-select { box-sizing: border-box; width: 100%; }

.area-chip {
  font-size: 0.66rem;
  color: var(--fs-accent);
  background: var(--fs-accent-soft);
  border-radius: var(--fs-radius-pill);
  padding: 0.05rem 0.45rem;
  white-space: nowrap;
}

/* ── Toolbar ──────────────────────────────────────────────────── */
.systems-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.btn-add-system {
  background: none;
  border: 1px dashed var(--fs-border-color);
  color: var(--fs-text-secondary);
  padding: 0.28rem 0.65rem;
  border-radius: var(--fs-radius-sm);
  cursor: pointer;
  font-size: 0.78rem;
  font-family: inherit;
}
.btn-add-system:hover { border-color: var(--fs-accent); color: var(--fs-accent); }
.btn-add-system:focus-visible { outline: none; border-color: var(--fs-accent); color: var(--fs-accent); }

.archived-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--fs-text-tertiary);
  cursor: pointer;
  user-select: none;
}
.archived-checkbox { accent-color: var(--fs-accent); cursor: pointer; }

/* ── Create / edit form ───────────────────────────────────────── */
.system-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
  background: var(--fs-surface-raised);
  border: 1px solid var(--fs-border-color);
  border-radius: var(--fs-radius-lg);
}
.system-form--inline { padding: 0; background: none; border: none; flex: 1; }
/* The input itself is the .fs-input canon (components.css); only the
   layout remainder lives here. */
.system-input, .system-textarea { box-sizing: border-box; width: 100%; }
.system-textarea { resize: vertical; }

.system-form-actions { display: flex; gap: 0.4rem; }

/* RESTORED (#2444). Both lost their base rule to a CSS sweep; only the
   `--archived` modifier and the `:hover .system-actions` reveal survived.
   The card WAS a flex row and every child still says so — `.system-swatch`
   and `.system-actions` are `flex-shrink: 0`, `.system-body` is `flex: 1`,
   and `.system-form--inline` is `flex: 1`. `align-items: flex-start` is why
   the swatch carries `margin-top: 0.3rem`: it is nudged onto the first line
   of text rather than centred against the whole card.

   The list had no rule at all, so it rendered with browser bullets and
   indent — invisible to the dangling-style check, which can only see a class
   that is PARTLY styled. A class with no rules anywhere looks exactly like a
   semantic-only hook.

   Surface values match `.system-form` above, which is the same card shape in
   this file and the reason they can be recovered rather than guessed. */
.systems-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.system-card {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.6rem 0.75rem;
  background: var(--fs-surface-raised);
  border: 1px solid var(--fs-border-color);
  border-radius: var(--fs-radius-lg);
}

.system-card--archived { opacity: 0.6; }

.system-swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 0.3rem;
}
.system-body { flex: 1; min-width: 0; }
.system-name-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.system-name { font-weight: 500; color: var(--fs-text-primary); word-break: break-word; }
.issue-badge {
  font-size: 0.7rem;
  font-weight: 500;
  background: color-mix(in srgb, var(--fs-accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--fs-accent) 30%, transparent);
  color: var(--fs-accent);
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
  flex-shrink: 0;
}
.archived-badge {
  font-size: 0.65rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fs-text-tertiary);
  background: color-mix(in srgb, var(--fs-text-tertiary) 12%, transparent);
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
}
.system-description {
  margin: 0.25rem 0 0;
  font-size: 0.82rem;
  color: var(--fs-text-secondary);
  line-height: 1.4;
  word-break: break-word;
}

.system-actions { display: flex; gap: 0.15rem; flex-shrink: 0; opacity: 0; transition: opacity 0.15s; }
.system-card:hover .system-actions,
.system-card:focus-within .system-actions { opacity: 1; }
.action-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fs-text-tertiary);
  width: 26px;
  height: 26px;
  border-radius: var(--fs-radius-sm);
  transition: background 0.12s, color 0.12s;
}
.action-btn:hover { background: var(--fs-surface-raised); color: var(--fs-text-primary); }
.action-btn:focus-visible { outline: 2px solid var(--fs-accent); outline-offset: 1px; opacity: 1; }
.action-delete:hover { color: var(--fs-error); }

/* ── Empty ────────────────────────────────────────────────────── */
.systems-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 2rem 1rem;
  text-align: center;
  border: 1px dashed var(--fs-border-color);
  border-radius: var(--fs-radius-lg);
}
/* remainders over the shared recipes (components.css, m302) */
.empty-title { margin: 0; color: var(--fs-text-primary); }
.empty-sub { margin: 0 0 0.5rem; font-size: 0.82rem; max-width: 32ch; }


/* ── Skeleton ─────────────────────────────────────────────────── */
@keyframes skel-shine { to { background-position: 200% center; } }
.systems-skeleton { display: flex; flex-direction: column; gap: 0.4rem; }
.skel-row {
  height: 3rem;
  border-radius: var(--fs-radius-lg);
  background: linear-gradient(
    90deg,
    var(--fs-surface-raised) 25%,
    color-mix(in srgb, var(--fs-text-tertiary) 16%, var(--fs-surface-raised)) 50%,
    var(--fs-surface-raised) 75%
  );
  background-size: 200% 100%;
  animation: skel-shine 1.5s ease infinite;
}
.skel-row--short { width: 65%; }

</style>
