/* /Components/Layout/AdminLayout.razor.rz.scp.css */
/* ============================================================
   ADMIN UI — owned by AdminLayout
   ============================================================
   AdminLayout itself renders the shell + topbar wrappers
   (.admin-shell, .admin-topbar, .admin-brand, .admin-nav,
   .admin-back, .admin-main); those selectors are scoped naturally
   because the elements get this layout's b-xxx attribute.

   .admin-nav-link is the exception: it's applied to <NavLink>, a
   child Blazor component whose rendered <a> carries NavLink's scope
   (effectively none for our purposes), NOT AdminLayout's. So those
   selectors need ::deep to reach descendants of the scoped
   .admin-shell wrapper. Same reason .admin-h1, .admin-card, etc.
   need ::deep — they're all rendered by descendant components.

   Children that need to override use their own ::deep .admin-card
   { ... } from their own scoped CSS. */

.admin-shell[b-5lix6w4u9y] {
  min-height: 100vh;
  display: flex; flex-direction: column;
  background: var(--bg-0);
  color: var(--fg-1);
  font-family: var(--ff-sans);
}
.admin-topbar[b-5lix6w4u9y] {
  display: flex; align-items: center; gap: 24px;
  padding: 10px 24px;
  background: var(--bg-1);
  border-bottom: 1px solid var(--line-1);
  position: sticky; top: 0; z-index: 10;
}
.admin-brand[b-5lix6w4u9y] { display: flex; gap: 8px; align-items: baseline; }
.admin-nav[b-5lix6w4u9y] { display: flex; gap: 4px; flex: 1; }
/* ::deep — .admin-nav-link sits on <NavLink>, a child component whose
   rendered <a> doesn't carry AdminLayout's scope attribute. */
[b-5lix6w4u9y] .admin-nav-link {
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.16em;
  color: var(--fg-2);
  padding: 6px 12px;
  text-decoration: none;
  border-radius: var(--r-sm);
  text-transform: uppercase;
}
[b-5lix6w4u9y] .admin-nav-link:hover { background: var(--bg-2); color: var(--fg-0); }
[b-5lix6w4u9y] .admin-nav-link.active { background: var(--accent-dim); color: var(--accent-fg); }
.admin-back[b-5lix6w4u9y] {
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.16em;
  color: var(--fg-3);
  text-decoration: none;
  padding: 6px 10px;
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
}
.admin-back:hover[b-5lix6w4u9y] { color: var(--accent); border-color: var(--accent-line); }

.admin-main[b-5lix6w4u9y] {
  flex: 1;
  padding: 28px 32px 60px;
  max-width: 1280px;
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
}

/* ── Admin page chrome (rendered by child admin pages) ─────── */
[b-5lix6w4u9y] .admin-h1 { font-size: 24px; font-weight: 600; letter-spacing: 0.02em; margin: 0 0 4px; color: var(--fg-0); }
[b-5lix6w4u9y] .admin-h2 { font-size: 14px; font-weight: 600; letter-spacing: 0.04em; margin: 0 0 12px; color: var(--fg-0); }
[b-5lix6w4u9y] .admin-sub { color: var(--fg-2); margin: 0 0 28px; font-size: 13px; }
[b-5lix6w4u9y] .admin-crumbs { font-family: var(--ff-mono); font-size: 10px; letter-spacing: 0.18em; color: var(--fg-3); margin-bottom: 6px; }
[b-5lix6w4u9y] .admin-crumbs a { color: var(--fg-3); text-decoration: none; }
[b-5lix6w4u9y] .admin-crumbs a:hover { color: var(--accent); }

[b-5lix6w4u9y] .admin-page-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 24px;
  gap: 16px;
}
[b-5lix6w4u9y] .admin-actions { display: flex; gap: 8px; }

[b-5lix6w4u9y] .admin-empty { padding: 48px 16px; text-align: center; color: var(--fg-3); font-family: var(--ff-mono); letter-spacing: 0.14em; font-size: 11px; }
[b-5lix6w4u9y] .admin-empty.inline { padding: 18px 12px; }

[b-5lix6w4u9y] .admin-msg {
  padding: 10px 14px;
  border-radius: var(--r-sm);
  margin-bottom: 16px;
  font-size: 13px;
}
[b-5lix6w4u9y] .admin-msg.ok    { background: var(--accent-dim); border: 1px solid var(--accent-line); color: var(--fg-0); }
[b-5lix6w4u9y] .admin-msg.error { background: oklch(0.30 0.10 25 / 0.4); border: 1px solid oklch(0.65 0.18 25 / 0.6); color: oklch(0.92 0.06 25); }

[b-5lix6w4u9y] .admin-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  margin-bottom: 24px;
}
[b-5lix6w4u9y] .admin-stat {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 14px 16px;
  text-decoration: none;
  color: var(--fg-1);
  display: block;
  transition: border-color 0.12s, background 0.12s;
}
[b-5lix6w4u9y] .admin-stat:hover { border-color: var(--accent-line); background: var(--bg-2); }
[b-5lix6w4u9y] .admin-stat-label { font-size: 10px; letter-spacing: 0.16em; color: var(--fg-3); }
[b-5lix6w4u9y] .admin-stat-value { font-size: 32px; font-weight: 600; color: var(--fg-0); margin-top: 4px; }
[b-5lix6w4u9y] .admin-stat-sub { font-size: 10px; letter-spacing: 0.12em; color: var(--fg-2); margin-top: 4px; }

[b-5lix6w4u9y] .admin-recent-units {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 14px 16px;
  display: inline-block;
  min-width: 220px;
}
[b-5lix6w4u9y] .admin-recent-units-label {
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--fg-3);
}
[b-5lix6w4u9y] .admin-recent-units-list {
  list-style: none;
  margin: 8px 0 0 0;
  padding-left: 18px;
  color: var(--fg-1);
  font-size: 13px;
}
[b-5lix6w4u9y] .admin-recent-units-list li { margin-bottom: 3px; }

[b-5lix6w4u9y] .admin-quick { display: flex; gap: 10px; margin-top: 20px; }

[b-5lix6w4u9y] .admin-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid var(--line-2);
  color: var(--fg-1);
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.16em;
  padding: 8px 14px;
  cursor: pointer;
  text-decoration: none;
  border-radius: var(--r-sm);
}
[b-5lix6w4u9y] .admin-btn:hover { border-color: var(--accent-line); color: var(--accent-fg); }
[b-5lix6w4u9y] .admin-btn.small { padding: 4px 10px; font-size: 10px; letter-spacing: 0.14em; }
[b-5lix6w4u9y] .admin-btn.primary {
  background: var(--accent-dim);
  border-color: var(--accent-line);
  color: var(--accent-fg);
}
[b-5lix6w4u9y] .admin-btn.primary:hover { background: var(--accent); color: var(--bg-0); }
[b-5lix6w4u9y] .admin-btn.danger { color: oklch(0.7 0.16 25); border-color: oklch(0.55 0.18 25 / 0.5); }
[b-5lix6w4u9y] .admin-btn.danger:hover { background: oklch(0.55 0.18 25 / 0.2); border-color: oklch(0.65 0.18 25); color: oklch(0.85 0.16 25); }
[b-5lix6w4u9y] .admin-btn.ghost  { color: var(--fg-2); border-color: var(--line-2); }
[b-5lix6w4u9y] .admin-btn.ghost:hover { color: var(--fg-1); border-color: var(--line-1); }

[b-5lix6w4u9y] .admin-mini {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  color: var(--fg-1);
  width: 24px; height: 24px;
  cursor: pointer;
  font-size: 13px; line-height: 1;
  margin-right: 4px;
}
[b-5lix6w4u9y] .admin-mini:hover { color: var(--accent); border-color: var(--accent-line); }
[b-5lix6w4u9y] .admin-mini.danger { color: oklch(0.65 0.18 25); }
[b-5lix6w4u9y] .admin-mini.danger:hover { color: oklch(0.8 0.18 25); border-color: oklch(0.65 0.18 25); }

/* ── Inline qty stepper (−/qty/+) ──────────────────────────
   When the stepper lives inside a <label> (e.g. SculptCheckRow's
   slot), the wrapper takes @onclick:stopPropagation so a stray
   click on the qty readout doesn't toggle the labeled checkbox. */
[b-5lix6w4u9y] .admin-qty-stepper { display: inline-flex; gap: 4px; align-items: center; margin-left: 10px; }
[b-5lix6w4u9y] .admin-qty-stepper .qty-readout { min-width: 1.5em; text-align: center; }

/* ── Tables ────────────────────────────────────────────────── */
[b-5lix6w4u9y] .admin-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  overflow: hidden;
  font-size: 13px;
}
[b-5lix6w4u9y] .admin-table th {
  text-align: left;
  background: var(--bg-2);
  padding: 8px 10px;
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.14em;
  color: var(--fg-2);
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-1);
}
[b-5lix6w4u9y] .admin-table td { padding: 8px 10px; border-bottom: 1px solid var(--line-1); color: var(--fg-1); vertical-align: middle; }
[b-5lix6w4u9y] .admin-table tr:last-child td { border-bottom: 0; }
[b-5lix6w4u9y] .admin-table tr:hover td { background: var(--bg-2); }
[b-5lix6w4u9y] .admin-table.inner { margin-top: 0; box-shadow: none; }

/* Severity-tinted rows for the logs list (and any future admin table that
   needs to flag rows by warn/error). The left-border accent is enough on its
   own — we don't tint the whole row so the hover state still reads. */
[b-5lix6w4u9y] .admin-table tbody tr.admin-row-warn td:first-child { border-left: 3px solid var(--warn); }
[b-5lix6w4u9y] .admin-table tbody tr.admin-row-err  td:first-child { border-left: 3px solid var(--bad); }

/* Preformatted block for the log detail view (exception stack, pretty-printed
   properties JSON). Wraps long lines so the page stays readable, and uses the
   project's theme tokens so it adapts to light/dark. */
[b-5lix6w4u9y] .admin-pre {
  white-space: pre-wrap;
  overflow-x: auto;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  padding: 12px;
  font-size: 12px;
  font-family: var(--ff-mono);
  color: var(--fg-1);
}

[b-5lix6w4u9y] .admin-link { color: var(--accent-fg); text-decoration: none; }
[b-5lix6w4u9y] .admin-link:hover { color: var(--accent); text-decoration: underline; }
[b-5lix6w4u9y] .admin-muted { color: var(--fg-3); }

[b-5lix6w4u9y] .admin-pill {
  display: inline-block;
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.12em;
  padding: 1px 5px;
  border: 1px solid var(--line-2);
  margin-right: 4px;
  border-radius: 1px;
}
[b-5lix6w4u9y] .admin-pill.active { color: var(--accent); border-color: var(--accent-line); background: var(--accent-dim); }
[b-5lix6w4u9y] .admin-pill.oop    { color: var(--warn); border-color: oklch(0.78 0.14 60 / 0.5); }
[b-5lix6w4u9y] .admin-pill.legacy { color: var(--fg-2); border-color: var(--line-2); border-style: dashed; }
[b-5lix6w4u9y] .admin-pill.draft  { color: var(--fg-2); border-color: var(--line-2); border-style: dotted; }

/* ── Filter bar ────────────────────────────────────────────── */
[b-5lix6w4u9y] .admin-filterbar {
  display: flex; gap: 12px; align-items: flex-end;
  margin-bottom: 16px;
  padding: 12px 16px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
}

[b-5lix6w4u9y] .admin-bulkbar {
  display: flex; gap: 12px; align-items: center;
  margin-bottom: 12px;
  padding: 10px 14px;
  background: var(--accent-dim);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  position: sticky;
  top: 0;
  z-index: 10;
}
[b-5lix6w4u9y] .admin-bulkbar .mono { color: var(--accent-fg); font-size: 11px; letter-spacing: 0.14em; }

/* ── Forms ─────────────────────────────────────────────────── */
[b-5lix6w4u9y] .admin-form-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}
[b-5lix6w4u9y] .admin-card {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 18px 20px;
}
[b-5lix6w4u9y] .admin-card--wide { grid-column: span 2; }
[b-5lix6w4u9y] .admin-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
[b-5lix6w4u9y] .admin-card-head .admin-h2 { margin: 0; }

[b-5lix6w4u9y] .admin-fields {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}
[b-5lix6w4u9y] .admin-product-fields { grid-template-columns: repeat(4, 1fr); }
[b-5lix6w4u9y] .admin-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
[b-5lix6w4u9y] .admin-field.admin-checkbox { flex-direction: row; align-items: center; gap: 8px; grid-column: span 2; }
[b-5lix6w4u9y] .admin-label { font-family: var(--ff-mono); font-size: 10px; letter-spacing: 0.14em; color: var(--fg-3); }
[b-5lix6w4u9y] .admin-hint  { font-size: 11px; color: var(--fg-3); margin-top: 2px; }

/* Feature-flag rows: name + description stacked on the left, toggle on the right.
   Overrides .admin-field's default flex-column so the toggle sits beside the
   label block instead of below it. */
[b-5lix6w4u9y] .flag-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1rem;
  align-items: start;
}
[b-5lix6w4u9y] .flag-row-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
[b-5lix6w4u9y] .flag-row .admin-hint {
  margin-top: 0; /* override the default 2px so it sits flush under the label */
}

/* The toggle widget at the right of each .flag-row. Mirrors the
   .admin-membership-flag pattern further down the file — small mono
   uppercase label sitting next to a native checkbox tinted with the
   accent color. No background, no border: the only affordances are
   cursor:pointer on hover and the browser's native checkbox focus
   ring. Orphan rows don't reuse this wrapper — they have a REMOVE
   button instead (styled via .admin-btn.danger). */
[b-5lix6w4u9y] .flag-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--fg-2);
}
[b-5lix6w4u9y] .flag-toggle input[type="checkbox"] { accent-color: var(--accent); }

/* Unrecognized-flags section: a thin top divider separates it from the
   declared-flag list. The divider is the only visual signal that this is a
   separate group — no danger-colored badges, no shaded background. The
   operator's restraint preference applies here too. */
[b-5lix6w4u9y] .orphan-section {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line-1);
}
[b-5lix6w4u9y] .admin-h3 {
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--fg-3);
  margin: 0 0 0.5rem 0;
  text-transform: uppercase;
}
[b-5lix6w4u9y] .orphan-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1rem;
  align-items: center;
}

[b-5lix6w4u9y] .admin-field input[type="text"],
[b-5lix6w4u9y] .admin-field input[type="number"],
[b-5lix6w4u9y] .admin-field select {
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  color: var(--fg-0);
  padding: 6px 8px;
  font-family: inherit;
  font-size: 13px;
  border-radius: var(--r-sm);
  width: 100%;
  box-sizing: border-box;
}
[b-5lix6w4u9y] .admin-field input:focus,
[b-5lix6w4u9y] .admin-field select:focus { outline: none; border-color: var(--accent-line); }

[b-5lix6w4u9y] .admin-inline-input {
  background: transparent;
  border: 1px solid transparent;
  color: var(--fg-0);
  padding: 4px 6px;
  font-family: inherit;
  font-size: 13px;
  width: 100%;
  border-radius: var(--r-sm);
}
[b-5lix6w4u9y] .admin-inline-input:hover { border-color: var(--line-1); background: var(--bg-0); }
[b-5lix6w4u9y] .admin-inline-input:focus { outline: none; border-color: var(--accent-line); background: var(--bg-0); }

[b-5lix6w4u9y] .admin-section-group { margin-bottom: 16px; }
[b-5lix6w4u9y] .admin-section-title { font-size: 10px; letter-spacing: 0.18em; color: var(--accent); margin-bottom: 6px; }

[b-5lix6w4u9y] .admin-checks { display: flex; flex-direction: column; gap: 4px; }
[b-5lix6w4u9y] .admin-checks.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; }
[b-5lix6w4u9y] .admin-check {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 6px;
  border-radius: var(--r-sm);
  font-size: 12px; color: var(--fg-1);
  cursor: pointer;
}
[b-5lix6w4u9y] .admin-check:hover { background: var(--bg-2); }
[b-5lix6w4u9y] .admin-check input[type="checkbox"] { accent-color: var(--accent); }

/* Per-faction membership rows in UnitEdit — checkbox + inline playable + last-edition select. */
[b-5lix6w4u9y] .admin-memberships { display: flex; flex-direction: column; gap: 6px; }
[b-5lix6w4u9y] .admin-membership {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) auto auto;
  align-items: center;
  gap: 12px;
  padding: 4px 6px;
  border-radius: var(--r-sm);
}
[b-5lix6w4u9y] .admin-membership:hover { background: var(--bg-2); }
[b-5lix6w4u9y] .admin-membership .admin-check { flex: 0 0 auto; padding: 0; }
[b-5lix6w4u9y] .admin-membership-flag {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--fg-2); letter-spacing: .08em;
  cursor: pointer;
}
[b-5lix6w4u9y] .admin-membership-flag input[type="checkbox"] { accent-color: var(--accent); }
[b-5lix6w4u9y] .admin-membership-edition {
  font: inherit;
  font-size: 12px;
  padding: 3px 6px;
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  color: var(--fg-1);
}

[b-5lix6w4u9y] .admin-product-row {
  position: relative;
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 12px 14px;
  margin-bottom: 12px;
}
[b-5lix6w4u9y] .admin-product-sculpts { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line-1); }
[b-5lix6w4u9y] .admin-product-del { position: absolute; top: 8px; right: 8px; }

/* ── Image upload + thumbnail ──────────────────────────────── */
[b-5lix6w4u9y] .admin-upload-cell {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
}
[b-5lix6w4u9y] .admin-upload-cell input[type="text"] { flex: 1; min-width: 0; }
/* Hide the actual file input, use the wrapping label as the visible button */
[b-5lix6w4u9y] .admin-upload-label {
  position: relative;
  cursor: pointer;
  flex-shrink: 0;
  text-decoration: none;
}
[b-5lix6w4u9y] .admin-upload-input {
  position: absolute; inset: 0;
  opacity: 0;
  cursor: pointer;
  width: 100%; height: 100%;
}
/* Image preview thumbnail (40x40) used inline in admin tables and forms */
[b-5lix6w4u9y] .admin-img-thumb {
  width: 36px; height: 36px;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-1);
  display: block;
  flex-shrink: 0;
}
[b-5lix6w4u9y] .admin-img-thumb--empty {
  display: grid; place-items: center;
  background: var(--bg-2);
  color: var(--fg-3);
  font-family: var(--ff-mono);
  font-size: 14px;
}

/* ─────────── Admin: data import page ─────────── */

/* -- details/summary baseline -------------------------------------------- */
/* <details> is only used by the admin import components today
   (ImportSummaryPanel, ImportHistoryList). Lives here as an admin chrome
   primitive — if a non-admin page ever needs <details>, move this back
   to app.css. */
[b-5lix6w4u9y] details > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
}
[b-5lix6w4u9y] details > summary::-webkit-details-marker { display: none; }
[b-5lix6w4u9y] details > summary::before {
  content: '\25B6';
  display: inline-block;
  font-size: 9px;
  margin-right: 6px;
  color: var(--fg-3);
  transition: transform 0.15s;
}
[b-5lix6w4u9y] details[open] > summary::before { transform: rotate(90deg); }

/* -- import-progress-stack (page wrapper) --------------------------------- */
[b-5lix6w4u9y] .import-progress-stack {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 24px;
}

/* -- ImportProgressBar ---------------------------------------------------- */
[b-5lix6w4u9y] .import-progress-bar {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 12px 14px;
}
[b-5lix6w4u9y] .import-progress-bar.is-running { border-color: var(--accent-line); background: var(--accent-dim); }
[b-5lix6w4u9y] .import-progress-bar.is-complete { border-color: oklch(0.55 0.15 145 / 0.5); background: oklch(0.20 0.04 145 / 0.4); }
[b-5lix6w4u9y] .import-progress-bar.is-error { border-color: oklch(0.65 0.18 25 / 0.6); background: oklch(0.30 0.10 25 / 0.3); }

[b-5lix6w4u9y] .import-progress-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
  gap: 12px;
}
[b-5lix6w4u9y] .import-progress-phase {
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--fg-0);
  text-transform: uppercase;
}
[b-5lix6w4u9y] .import-progress-status {
  font-family: var(--ff-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  color: var(--fg-2);
}
[b-5lix6w4u9y] .is-running  .import-progress-status { color: var(--accent-fg); }
[b-5lix6w4u9y] .is-complete .import-progress-status { color: oklch(0.72 0.14 145); }
[b-5lix6w4u9y] .is-error    .import-progress-status { color: oklch(0.80 0.12 25); }

[b-5lix6w4u9y] .import-progress-track {
  height: 6px;
  background: var(--bg-2);
  border-radius: 3px;
  overflow: hidden;
  margin-bottom: 6px;
}
[b-5lix6w4u9y] .import-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 3px;
  transition: width 0.3s ease;
}
[b-5lix6w4u9y] .is-running .import-progress-fill {
  background: var(--accent);
  animation: import-pulse-b-5lix6w4u9y 1.4s ease-in-out infinite;
}
[b-5lix6w4u9y] .is-complete .import-progress-fill { background: oklch(0.60 0.14 145); }
[b-5lix6w4u9y] .is-error    .import-progress-fill { background: oklch(0.65 0.18 25 / 0.7); }

@keyframes import-pulse-b-5lix6w4u9y {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.6; }
}

[b-5lix6w4u9y] .import-progress-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
[b-5lix6w4u9y] .import-progress-label {
  font-family: var(--ff-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  color: var(--fg-3);
}

/* -- ImportSummaryPanel --------------------------------------------------- */
[b-5lix6w4u9y] .import-summary-panel {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 16px 18px;
  margin-bottom: 20px;
}
[b-5lix6w4u9y] .import-summary-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  gap: 12px;
}
[b-5lix6w4u9y] .import-summary-tag {
  display: inline-block;
  font-family: var(--ff-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  padding: 2px 6px;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  color: var(--fg-2);
  text-transform: uppercase;
  margin-right: 6px;
}
[b-5lix6w4u9y] .import-summary-tag.dry-run  { color: var(--warn, oklch(0.82 0.14 60)); border-color: oklch(0.78 0.14 60 / 0.5); background: oklch(0.24 0.06 60 / 0.3); }
[b-5lix6w4u9y] .import-summary-tag.committed { color: var(--accent-fg); border-color: var(--accent-line); background: var(--accent-dim); }

[b-5lix6w4u9y] .import-summary-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  overflow: hidden;
  font-size: 13px;
  margin-bottom: 12px;
}
[b-5lix6w4u9y] .import-summary-table th {
  text-align: left;
  background: var(--bg-2);
  padding: 7px 10px;
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.14em;
  color: var(--fg-2);
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-1);
  font-weight: 600;
}
[b-5lix6w4u9y] .import-summary-table td {
  padding: 7px 10px;
  border-bottom: 1px solid var(--line-1);
  color: var(--fg-1);
  vertical-align: middle;
}
[b-5lix6w4u9y] .import-summary-table tr:last-child td { border-bottom: 0; }
[b-5lix6w4u9y] .import-summary-table tr:hover td { background: var(--bg-2); }

[b-5lix6w4u9y] .import-summary-section {
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 10px 12px;
  margin-top: 8px;
  font-size: 13px;
}
[b-5lix6w4u9y] .import-summary-section.is-error { border-color: oklch(0.65 0.18 25 / 0.5); background: oklch(0.30 0.10 25 / 0.2); }
[b-5lix6w4u9y] .import-summary-section > summary {
  font-family: var(--ff-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--fg-2);
  text-transform: uppercase;
  padding: 2px 0;
}
[b-5lix6w4u9y] .import-summary-section.is-error > summary { color: oklch(0.80 0.12 25); }

[b-5lix6w4u9y] .import-summary-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
[b-5lix6w4u9y] .import-summary-list li {
  font-size: 12px;
  color: var(--fg-1);
  padding: 3px 0 3px 14px;
  position: relative;
}
[b-5lix6w4u9y] .import-summary-list li::before {
  content: '\2013';
  position: absolute; left: 2px;
  color: var(--fg-3);
}
/* Needs-curation items look clickable */
[b-5lix6w4u9y] .import-summary-list li a,
[b-5lix6w4u9y] .import-summary-list li.link {
  color: var(--accent-fg);
  text-decoration: none;
  cursor: pointer;
}
[b-5lix6w4u9y] .import-summary-list li a:hover,
[b-5lix6w4u9y] .import-summary-list li.link:hover {
  color: var(--accent);
  text-decoration: underline;
}

[b-5lix6w4u9y] .import-summary-subgroup {
  margin-top: 8px;
}
[b-5lix6w4u9y] .import-summary-subgroup-header {
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--fg-2);
  margin-bottom: 4px;
}

/* -- ImportHistoryList ---------------------------------------------------- */
[b-5lix6w4u9y] .import-history-list {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  overflow: hidden;
}
[b-5lix6w4u9y] .import-history-rows {
  list-style: none;
  margin: 0; padding: 0;
}
[b-5lix6w4u9y] .import-history-rows > li {
  border-bottom: 1px solid var(--line-1);
}
[b-5lix6w4u9y] .import-history-rows > li:last-child { border-bottom: 0; }
[b-5lix6w4u9y] .import-history-rows > li > details > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  font-size: 13px;
  color: var(--fg-1);
  transition: background 0.1s;
}
[b-5lix6w4u9y] .import-history-rows > li > details > summary:hover { background: var(--bg-2); }
[b-5lix6w4u9y] .import-history-rows > li > details > summary::before { color: var(--fg-3); }

[b-5lix6w4u9y] .import-history-tag {
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.12em;
  padding: 1px 5px;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  text-transform: uppercase;
  flex-shrink: 0;
}
[b-5lix6w4u9y] .import-history-tag.dry  { color: var(--warn, oklch(0.82 0.14 60)); border-color: oklch(0.78 0.14 60 / 0.5); }
[b-5lix6w4u9y] .import-history-tag.real { color: var(--accent-fg); border-color: var(--accent-line); background: var(--accent-dim); }

[b-5lix6w4u9y] .import-history-status {
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.12em;
  margin-left: auto;
  flex-shrink: 0;
}
[b-5lix6w4u9y] .import-history-status.ok    { color: oklch(0.72 0.14 145); }
[b-5lix6w4u9y] .import-history-status.info  { color: var(--fg-2); }
[b-5lix6w4u9y] .import-history-status.error { color: oklch(0.80 0.12 25); }

[b-5lix6w4u9y] .import-history-detail {
  padding: 10px 14px 12px 32px;
  border-top: 1px solid var(--line-1);
  background: var(--bg-0);
  font-size: 12px;
  color: var(--fg-2);
}

/* -- DataDumpUpload ------------------------------------------------------- */
[b-5lix6w4u9y] .data-dump-upload {
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 16px 18px;
  margin-bottom: 20px;
}
[b-5lix6w4u9y] .data-dump-status {
  margin-bottom: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
[b-5lix6w4u9y] .data-dump-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 13px;
  color: var(--fg-1);
}
[b-5lix6w4u9y] .data-dump-label {
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.14em;
  color: var(--fg-3);
  min-width: 110px;
  flex-shrink: 0;
}
[b-5lix6w4u9y] .data-dump-value {
  color: var(--fg-0);
  font-size: 13px;
}
[b-5lix6w4u9y] .data-dump-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 8px;
}
/* /Components/Layout/BuyHeroStrip.razor.rz.scp.css */
/* Base .hero-strip / .hero-text rules mirrored from Products.razor.css:107-137.
   Blazor CSS isolation prevents those scoped rules from applying on /purchaseplanner, so we
   re-declare them here under the .buy-hero scope. If a future refactor promotes
   these primitives to wwwroot/app.css, the .buy-hero qualifier can drop.
   The .hero-num rule is buy-specific (uses --accent-hue, not --want) and
   defined directly below — no need for a shadowed base rule. */

.buy-hero[b-tqdemkbdek] {
  margin: 28px clamp(20px, 3vw, 36px) 0;
  padding: 18px 22px;
  background: linear-gradient(90deg,
    oklch(0.78 0.14 60 / 0.08) 0%,
    oklch(0.78 0.14 60 / 0.02) 50%,
    transparent 100%);
  border-left: 2px solid var(--want);
  display: flex;
  align-items: center;
  gap: 22px;
}

.buy-hero .hero-text[b-tqdemkbdek] { font-size: 14px; line-height: 1.5; color: var(--fg-1); }
.buy-hero .hero-text strong[b-tqdemkbdek] { color: var(--fg-0); font-weight: 600; }
.buy-hero .hero-text small[b-tqdemkbdek] {
  display: block;
  margin-top: 4px;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-3);
}

.buy-hero .hero-num[b-tqdemkbdek] {
    font-family: var(--ff-mono);
    font-size: 2.4rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.02em;
    color: oklch(95% 0.05 var(--accent-hue, 220));
}

.buy-hero .hero-num-sep[b-tqdemkbdek] {
    opacity: 0.5;
    margin: 0 0.05em;
}

/* /Components/Layout/BuyOopAutoBanner.razor.rz.scp.css */
/* Mirrors the warning callout pattern from OrphanedUnitsList.razor.css
   (same --warn token, comparable padding/border-radius) so the two
   warning surfaces on /purchaseplanner read as a family. Sits above the hero strip
   on auto-enable; not dismissed by the same control that toggles OOP. */

.buy-oop-banner[b-9s80t49yyq] {
    margin: 1rem clamp(20px, 3vw, 36px) 0;
    padding: 0.6rem 0.9rem;
    display: flex;
    align-items: center;
    gap: 1rem;
    border: 1px solid oklch(from var(--warn) l c h / 0.6);
    background: oklch(from var(--warn) l c h / 0.06);
    border-radius: 6px;
    font-size: 0.78rem;
    letter-spacing: 0.04em;
}

.buy-oop-banner-body[b-9s80t49yyq] {
    flex: 1 1 auto;
}

.buy-oop-banner-head[b-9s80t49yyq] {
    color: var(--warn);
    letter-spacing: 0.08em;
}

.buy-oop-banner-text[b-9s80t49yyq] {
    color: var(--fg-1);
    letter-spacing: normal;
}

.buy-oop-banner-dismiss[b-9s80t49yyq] {
    flex: 0 0 auto;
    background: transparent;
    border: 1px solid var(--line-1);
    color: var(--fg-2);
    padding: 0.15rem 0.5rem;
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: inherit;
    line-height: 1;
}

.buy-oop-banner-dismiss:hover[b-9s80t49yyq] {
    background: var(--bg-2);
    color: var(--fg-0);
}
/* /Components/Layout/BuyProductCard.razor.rz.scp.css */
.buy-card[b-vg8h7jl107] {
    position: relative;
}

.buy-card-cov-overlay[b-vg8h7jl107] {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 2;
    pointer-events: none;
}
/* /Components/Layout/CompareBar.razor.rz.scp.css */
/* ── Compare bar ── */
.compare-bar[b-n607kxdrzy] {
  position:fixed; bottom:16px; left:50%; transform:translateX(-50%) translateY(140%);
  /* z:55 sits below the compare modal (z:60). The unit detail drawer is now a
     native <dialog> opened via showModal() — it lives in the browser's top
     layer, above all z-index'd content including this bar. That's intentional:
     users adding to compare from inside the open drawer should still see items
     land in the bar when they close the drawer; the bar isn't competing for
     visibility while the drawer is open (the drawer's ::backdrop dims the bar). */
  z-index:55; background:var(--bg-1); border:1px solid var(--accent-line);
  border-radius:var(--r-md); padding:12px 14px; display:flex; align-items:center; gap:12px;
  box-shadow:0 12px 40px oklch(0 0 0 / 0.6); transition:transform .24s cubic-bezier(.2,.7,.2,1);
  max-width:calc(100vw - 64px);
}
.compare-bar[data-open="True"][b-n607kxdrzy] { transform:translateX(-50%) translateY(0); }
.compare-bar .label[b-n607kxdrzy] { font-family:var(--ff-mono); font-size:10px; letter-spacing:0.18em; color:var(--accent); border-right:1px solid var(--line-1); padding-right:12px; }
@media (max-width: 600px) {
  .compare-bar[b-n607kxdrzy] {
    bottom: max(16px, calc(16px + env(safe-area-inset-bottom)));
  }
}
.compare-chips[b-n607kxdrzy] { display:flex; gap:6px; flex-wrap:wrap; }
.compare-chip[b-n607kxdrzy] { font-family:var(--ff-mono); font-size:11px; padding:4px 8px; border:1px solid var(--line-1); display:flex; align-items:center; gap:6px; background:var(--bg-2); }
.compare-chip .x[b-n607kxdrzy] { color:var(--fg-3); cursor:pointer; }
.compare-chip .x:hover[b-n607kxdrzy] { color:var(--accent); }
/* /Components/Layout/CompareModal.razor.rz.scp.css */
/* ── Compare modal ── */
/* Native <dialog> usage: the dialog itself fills the viewport (acting as the
   scrim host) so backdrop clicks can be detected by clicking the dialog
   element directly. The actual dim layer is provided by ::backdrop. */
.compare-modal[b-bps8s0l35c] { position:fixed; inset:0; background:transparent; z-index:60; padding:40px; overflow-y:auto; max-width:none; max-height:none; width:100%; height:100%; border:none; color:inherit; }
.compare-modal[b-bps8s0l35c]::backdrop { background:oklch(0 0 0 / 0.7); }
.compare-modal-inner[b-bps8s0l35c] { max-width:1200px; margin:0 auto; background:var(--bg-1); border:1px solid var(--line-1); }
.compare-modal-head[b-bps8s0l35c] { padding:18px 24px; display:flex; align-items:center; border-bottom:1px solid var(--line-1); }
.compare-modal-head h3[b-bps8s0l35c] { margin:0; font-size:18px; letter-spacing:0.04em; }
.compare-modal-head .crumbs[b-bps8s0l35c] { font-family:var(--ff-mono); font-size:10px; letter-spacing:0.16em; color:var(--fg-3); margin-right:12px; }
.compare-grid[b-bps8s0l35c] { display:grid; font-size:13px; }
.compare-grid > div[b-bps8s0l35c] { padding:10px 14px; border-top:1px solid var(--line-1); border-right:1px solid var(--line-1); }
.compare-grid > div:last-child[b-bps8s0l35c] { border-right:0; }
.compare-grid .row-label[b-bps8s0l35c] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.16em; color:var(--fg-3); background:var(--bg-2); }
.compare-grid .col-head[b-bps8s0l35c] { font-weight:600; font-size:14px; background:var(--bg-2); }
.compare-grid .col-head .sub[b-bps8s0l35c] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.14em; color:var(--fg-3); margin-top:2px; }

/* Skills / weapons cells: the .ref-chip leaf is styled in RefChip.razor.css
   (its own isolation scope follows the chip components); this wrapper just
   gives them a wrapping flex layout, mirroring the drawer's .ref-chips. */
.compare-grid .compare-chips[b-bps8s0l35c] { display:flex; flex-wrap:wrap; gap:6px; align-content:flex-start; }

/* In-boxes cells: one product per line; OOP boxes are de-emphasised, matching
   the OOP treatment on the STATUS pill above. The em-dash placeholder reuses
   the muted token for an empty cell. */
.compare-grid .compare-boxes[b-bps8s0l35c] { display:flex; flex-direction:column; gap:4px; }
.compare-grid .box-line[b-bps8s0l35c] { font-size:12px; line-height:1.35; }
.compare-grid .box-line.oop[b-bps8s0l35c] { color:var(--fg-3); }
.compare-grid .compare-boxes .muted[b-bps8s0l35c] { color:var(--fg-3); }
/* /Components/Layout/ConfirmDialog.razor.rz.scp.css */
/* ───── Confirm dialog ───── */
.confirm-dialog[b-dt73v46mrb] {
  margin: auto;
  padding: 0;
  background: oklch(from var(--bg-0) l c h / 0.96);
  border: 1px solid var(--line-2);
  color: var(--fg-0);
  min-width: 360px;
  max-width: 480px;
  font-family: var(--ff-sans);
}
.confirm-dialog[b-dt73v46mrb]::backdrop {
  background: oklch(0 0 0 / 0.55);
  backdrop-filter: blur(2px);
}
.confirm-dialog .cd-body[b-dt73v46mrb] {
  padding: 22px 24px 18px;
}
.confirm-dialog .cd-title[b-dt73v46mrb] {
  font-family: var(--ff-mono);
  font-size: 11px;
  font-weight: 600;
  /* Was 0.22em — distinctive headline tracking on a dialog title makes every
     "Are you sure?" prompt feel like a poster. 0.08em keeps mono+caps as an
     "important label" cue without shouting. */
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 10px;
}
.confirm-dialog .cd-message[b-dt73v46mrb] {
  font-size: 14px;
  line-height: 1.5;
  color: var(--fg-1);
  margin: 0;
}
.confirm-dialog .cd-actions[b-dt73v46mrb] {
  padding: 14px 24px 18px;
  border-top: 1px solid var(--line-1);
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.confirm-dialog .cd-btn[b-dt73v46mrb] {
  font-family: var(--ff-mono);
  font-size: 11px;
  font-weight: 600;
  /* Was 0.22em — buttons are interactive surfaces, not headlines. 0.06em keeps
     the mono-caps "uniform" feel without making "DELETE" or "CONFIRM" demand
     to be read in slow motion. */
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 9px 16px;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--line-2);
  color: var(--fg-1);
  transition: background .12s, border-color .12s, color .12s;
}
.confirm-dialog .cd-btn:hover[b-dt73v46mrb] {
  border-color: var(--accent-line);
  color: var(--fg-0);
}
.confirm-dialog .cd-btn:focus-visible[b-dt73v46mrb] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.confirm-dialog .cd-btn--confirm[b-dt73v46mrb] {
  background: var(--accent);
  border-color: var(--accent);
  color: oklch(0.10 0.012 240);
}
.confirm-dialog .cd-btn--confirm:hover[b-dt73v46mrb] {
  filter: brightness(1.12);
}
.confirm-dialog .cd-btn--destructive[b-dt73v46mrb] {
  background: var(--bad);
  border-color: var(--bad);
  color: oklch(0.10 0.012 30);
}
.confirm-dialog .cd-btn--destructive:hover[b-dt73v46mrb] {
  filter: brightness(1.12);
}
/* /Components/Layout/CookieConsent.razor.rz.scp.css */
/* Cookie consent modal — minimal compliant styling.

   IMPORTANT: .consent-btn--primary applies to BOTH "Reject all" and
   "Accept all" so they're rendered with equal visual prominence. This is an
   EU regulatory requirement (the "Reject all" option must be as obvious and
   one click away as "Accept all"). Do NOT give one of them a stronger color,
   a different size, or a different weight. If you need to tweak this CSS,
   keep the primary class identical for both buttons.

   Why the primary buttons are NEUTRAL (no brand accent):
   ──────────────────────────────────────────────────────
   Earlier revisions of this stylesheet rendered both Reject and Accept in
   --accent (cyan). That technically satisfies the equal-prominence rule,
   but regulators (notably CNIL and ICO updated guidance) increasingly push
   back on "Reject is styled as a primary brand CTA" — the brand-coloured
   button reads as the recommended path, which biases consent even when
   both buttons share a class. We render both in a neutral chrome instead:
   subdued background, fg-2 border, no accent saturation. The focus outline
   is still --accent for keyboard discoverability. This also aligns with
   the project's restraint preference for chrome surfaces. */

.consent-modal[b-hxq3cb97dr] {
    /* Opened via dialog.showModal() — browser handles top-layer placement, the inert
       backdrop, and centering. We only need the visual styling here. */
    max-width: 540px;
    width: calc(100% - 32px);
    padding: 24px;
    border: 1px solid var(--fg-3, #444);
    border-radius: 8px;
    background: var(--bg-1, #1a1a1a);
    color: var(--fg-0, #f5f5f5);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}

.consent-modal[b-hxq3cb97dr]::backdrop {
    background: rgba(0, 0, 0, 0.55);
}

.consent-title[b-hxq3cb97dr] {
    margin: 0 0 12px 0;
    font-size: 18px;
    font-weight: 600;
}

.consent-body[b-hxq3cb97dr],
.consent-row-desc[b-hxq3cb97dr] {
    margin: 0 0 12px 0;
    line-height: 1.5;
    color: var(--fg-1, #ddd);
    font-size: 14px;
}

.consent-links a[b-hxq3cb97dr] {
    color: var(--accent, #6cf);
}

.consent-row[b-hxq3cb97dr] {
    border-top: 1px solid var(--fg-3, #333);
    padding-top: 12px;
    margin-bottom: 12px;
}

.consent-row label[b-hxq3cb97dr] {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 600;
}

.consent-row-label[b-hxq3cb97dr] {
    user-select: none;
}

.consent-actions[b-hxq3cb97dr] {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 16px;
    flex-wrap: wrap;
}

/* SAME STYLES FOR REJECT AND ACCEPT — equal-prominence requirement.
   Neutral chrome (no brand accent) — see file header comment for rationale. */
.consent-btn--primary[b-hxq3cb97dr] {
    background: var(--bg-2, #2a2a2a);
    color: var(--fg-0, #f5f5f5);
    border: 1px solid var(--fg-2, #888);
    border-radius: 4px;
    padding: 10px 16px;
    font-weight: 600;
    cursor: pointer;
}
.consent-btn--primary:hover[b-hxq3cb97dr] {
    background: var(--bg-3, #333);
    border-color: var(--fg-1, #ccc);
}
.consent-btn--primary:focus-visible[b-hxq3cb97dr] {
    outline: 2px solid var(--accent, #6cf);
    outline-offset: 2px;
}

/* Secondary buttons (Customize / Back) are intentionally less prominent —
   regulators have said it's fine to differentiate the NEUTRAL option. */
.consent-btn--secondary[b-hxq3cb97dr] {
    background: transparent;
    color: var(--fg-0, #f5f5f5);
    border: 1px solid var(--fg-3, #555);
    border-radius: 4px;
    padding: 10px 16px;
    font-weight: 500;
    cursor: pointer;
}
.consent-btn--secondary:hover[b-hxq3cb97dr] { background: rgba(255, 255, 255, 0.05); }
.consent-btn--secondary:focus-visible[b-hxq3cb97dr] {
    outline: 2px solid var(--accent, #6cf);
    outline-offset: 2px;
}

/* ── Stacked action buttons on very narrow screens ──
   Below 480px the default flex-wrap pattern with three buttons + 8px gap
   visually splits into two-then-one which reads awkwardly. Stack them in
   a column instead — all full-width — so the order is visually clear.
   Reject all and Accept all KEEP their identical .consent-btn--primary
   styling (same background, same border, same width). The EU regulatory
   requirement that the two be equally prominent stays satisfied. */
@media (max-width: 480px) {
    .consent-actions[b-hxq3cb97dr] {
        flex-direction: column;
        align-items: stretch;
    }
    .consent-btn--primary[b-hxq3cb97dr],
    .consent-btn--secondary[b-hxq3cb97dr] {
        width: 100%;
        text-align: center;
    }
}
/* /Components/Layout/CookieSettingsLink.razor.rz.scp.css */
/* Discreet link to re-open the consent modal.

   Two visual modes selected via the data-position attribute on the
   button element (set from the Position parameter):

     - data-position="fixedcorner" (default) — bottom-right of the
       viewport, unobtrusive, but never hidden behind passive layout
       chrome (sticky topbar/filter bar live at z:9-10). Sits BELOW
       the compare bar (z:55) and compare/roster modals (z:60). The
       unit detail drawer and consent modal both use
       <dialog>.showModal() (browser top layer), so they always render
       above this link regardless of z-index — and the drawer's
       ::backdrop intercepts clicks while it's open, so this link
       can't be activated through it.

     - data-position="inline" — flow-positioned variant used by
       <SiteFooter />. No position/coordinates/z-index/opacity — the
       host (.site-footer__links flex row) owns placement, and the
       link inherits the surrounding footer link styling. */

/* Shared chrome (both modes). The fixed-corner-only positioning
   chrome lives in the [data-position="fixedcorner"] block below so
   the inline embed stays a plain flow-positioned element. */
.cookie-settings-link[b-l2inw99gng] {
    background: rgba(0, 0, 0, 0.55);
    color: var(--fg-1, #ddd);
    border: 1px solid var(--fg-3, #444);
    border-radius: 4px;
    padding: 6px 10px;
    font-size: 12px;
    font-family: var(--ff-mono, monospace);
    letter-spacing: 0.08em;
    cursor: pointer;
    transition: opacity 0.15s ease-in-out;
}

/* Fixed-corner mode — the original behavior. position:fixed pins
   the pill to the viewport corner regardless of where the component
   is rendered in the DOM (Catalog and AdminLayout both render it as
   a direct child of their root layout fragment). */
.cookie-settings-link[data-position="fixedcorner"][b-l2inw99gng] {
    position: fixed;
    bottom: 8px;
    right: 8px;
    z-index: 30;
    opacity: 0.5;
}

/* Inline mode — used by SiteFooter. Strip the fixed-positioning
   chrome; the surrounding flex row (.site-footer__links) places it
   alongside the other links. Background + border kept so the
   trigger still reads as a button rather than a plain link
   (matches the visual weight of the fixed-corner pill, just without
   the corner anchoring). */
.cookie-settings-link[data-position="inline"][b-l2inw99gng] {
    /* No position, no coordinates, no z-index — flows in document order. */
    opacity: 1;
}

.cookie-settings-link:hover[b-l2inw99gng],
.cookie-settings-link:focus-visible[b-l2inw99gng] {
    opacity: 1;
    outline: 2px solid var(--accent, #6cf);
    outline-offset: 2px;
}

/* When printing, the fixed-position chip is visual noise — hide it.
   The cookie policy itself prints normally; this chip is only useful
   for interactive reopening of the consent modal. The inline variant
   inside <SiteFooter /> stays in printed output as part of the
   page's natural footer (a static "Cookie settings" label with no
   handler is acceptable in print). */
@media print {
    .cookie-settings-link[data-position="fixedcorner"][b-l2inw99gng] {
        display: none;
    }
}
/* /Components/Layout/CoverageBadge.razor.rz.scp.css */
.cov[b-qzq2blxjc0] {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.25rem 0.55rem;
    border: 1px solid oklch(60% 0.13 var(--accent-hue, 220) / 0.45);
    background: oklch(20% 0.04 var(--accent-hue, 220) / 0.85);
    color: oklch(92% 0.04 var(--accent-hue, 220));
    border-radius: 999px;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    line-height: 1;
}

.cov strong[b-qzq2blxjc0] {
    color: oklch(98% 0.05 var(--accent-hue, 220));
    font-weight: 600;
}

.cov-sep[b-qzq2blxjc0] {
    opacity: 0.6;
}
/* /Components/Layout/DensityToggle.razor.rz.scp.css */
/* Density segmented control — sits in the catalog topbar between the
   compare button and the PRODUCTS link. Cells share .iconbtn-ish dimensions
   so the topbar height stays steady. */
.density-toggle[b-6rytlub54f] {
  display:inline-flex; height:32px;
  flex-shrink: 0;
  border:1px solid var(--line-1); border-radius:var(--r-sm);
  background:var(--bg-1); overflow:hidden;
}
.density-toggle .density-cell[b-6rytlub54f] {
  font-family:var(--ff-mono); font-size:11px; letter-spacing:0.14em;
  color:var(--fg-1); background:transparent; border:0;
  padding:0; width:28px; cursor:pointer;
  border-right:1px solid var(--line-1);
  transition:background .12s,color .12s;
}
.density-toggle .density-cell:last-child[b-6rytlub54f] { border-right:0; }
.density-toggle .density-cell:hover[b-6rytlub54f] { color:var(--fg-0); }
.density-toggle .density-cell.is-active[b-6rytlub54f] {
  background:var(--accent-dim); color:var(--fg-0);
}
.density-toggle .density-cell:focus-visible[b-6rytlub54f] {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
/* /Components/Layout/EmptyState.razor.rz.scp.css */
/* ───── Empty-state primitive ───── */
.empty-state[b-jshes5isa6] {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 64px 24px;
  text-align: center;
  min-height: 240px;
}
.empty-state .empty-mark[b-jshes5isa6] {
  /* The brand-mark glyph at empty-state size */
  width: 56px;
  height: 56px;
  border: 1px solid var(--accent);
  background: var(--accent-dim);
  position: relative;
  opacity: 0.40;
  margin-bottom: 6px;
}
.empty-state .empty-mark[b-jshes5isa6]::before {
  content: "";
  position: absolute;
  inset: 8px;
  border: 1px solid var(--accent);
  transform: rotate(45deg);
}
.empty-state .empty-headline[b-jshes5isa6] {
  font-size: 16px;
  font-weight: 500;
  color: var(--fg-1);
  letter-spacing: 0.005em;
  margin: 0;
}
.empty-state .empty-subhead[b-jshes5isa6] {
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  color: var(--fg-3);
  text-transform: none;
  margin: 0;
  max-width: 48ch;
}
.empty-state .empty-cta[b-jshes5isa6] {
  margin-top: 8px;
}
/* /Components/Layout/FiltersDrawer.razor.rz.scp.css */
/* ── Filters drawer wrapper (mobile sidebar off-canvas) ──
   Above 960px: `display: contents` — wrapper is structurally invisible,
   the sidebar sits in the .ident-app grid as today.
   At or below 960px: wrapper becomes a fixed off-canvas panel + scrim,
   slide-in from the left. State is driven by an `.is-open` class set
   from Catalog.razor's _filtersOpen field. */
.filters-drawer[b-3yf3m9ixyz] { display: contents; }
.filters-scrim[b-3yf3m9ixyz]  { display: none; }

@media (max-width: 960px) {
  .filters-drawer[b-3yf3m9ixyz] {
    display: block;
    position: fixed;
    top: 0; left: 0; bottom: 0;
    width: min(320px, 88vw);
    background: var(--bg-1);
    border-right: 1px solid var(--line-1);
    z-index: 50;
    transform: translateX(-100%);
    transition: transform .24s cubic-bezier(.2,.7,.2,1);
    overflow-y: auto;
    /* The contained .sidebar gets its own padding from its own rule above; the
       wrapper adds no padding of its own so the off-canvas slide is purely transform. */
  }
  .filters-drawer.is-open[b-3yf3m9ixyz] { transform: translateX(0); }
  /* ::deep reaches into the now-scoped IdentSidebar so the drawer can override
     sidebar layout when the sidebar is hosted inside it on mobile. */
  [b-3yf3m9ixyz] .sidebar {
    /* When inside an active drawer, the sidebar shouldn't be position:sticky —
       it's already inside a fixed wrapper. Reset to static so its native
       height + overflow takes over inside the panel. */
    position: static;
    height: auto;
    border-right: 0;
  }

  .filters-scrim[b-3yf3m9ixyz] {
    display: block;
    position: fixed; inset: 0;
    background: oklch(0 0 0 / 0.5);
    z-index: 49;
    opacity: 0; pointer-events: none;
    transition: opacity .2s;
  }
  .filters-scrim.is-open[b-3yf3m9ixyz] { opacity: 1; pointer-events: auto; }
}
/* /Components/Layout/FiltersToggleButton.razor.rz.scp.css */
/* The FILTERS button (rendered in PageHeader's controls slot on Catalog)
   is the trigger for the off-canvas filters drawer at <=960px. Hidden
   above that — the sidebar is visible in its own column. */
.filters-toggle[b-rg99t22nty] { display: none; }
@media (max-width: 960px) {
  .filters-toggle[b-rg99t22nty] {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 0 10px;
    width: auto;
  }
}
.filters-pip[b-rg99t22nty] {
  background: var(--accent-dim);
  border: 1px solid var(--accent-line);
  color: var(--accent-fg);
  font-size: 11px;
  padding: 1px 5px;
  min-width: 18px;
  text-align: center;
}
/* /Components/Layout/GlobalSearch.razor.rz.scp.css */
/* ── Global Search ───────────────────────────────────────────── */
.search-icon[b-4ngqjrc52v] { color:var(--fg-3); display:grid; place-items:center; }

.gsearch[b-4ngqjrc52v] {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  padding: 6px 10px;
  width: 380px;
  transition: border-color 0.12s;
}
.gsearch:focus-within[b-4ngqjrc52v] { border-color: var(--accent-line); }
.gsearch input[b-4ngqjrc52v] {
  flex: 1; min-width: 0;
  background: none; border: 0; outline: none;
  color: var(--fg-0);
  font-size: 13px;
}
.gsearch input:focus-visible[b-4ngqjrc52v] { outline: 2px solid var(--accent); outline-offset: 2px; }
.gsearch-shortcut[b-4ngqjrc52v] {
  font-size: 10px; letter-spacing: 0.12em;
  color: var(--fg-3);
  border: 1px solid var(--line-1);
  border-radius: 3px;
  padding: 1px 5px;
  line-height: 1;
}
.gsearch[data-open="True"] .gsearch-shortcut[b-4ngqjrc52v] { display: none; }

.gsearch-pop[b-4ngqjrc52v] {
  position: absolute;
  top: calc(100% + 6px); right: 0;
  width: 520px; max-width: 90vw;
  background: var(--bg-0);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  box-shadow: 0 12px 40px oklch(0% 0 0 / 0.5);
  z-index: 50;
  display: flex; flex-direction: column;
  max-height: 70vh; overflow: hidden;
}
.gsearch-pop-head[b-4ngqjrc52v] {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line-1);
  background: var(--bg-1);
}
.gsearch-scope[b-4ngqjrc52v] {
  display: flex; gap: 0;
  border: 1px solid var(--line-1);
  border-radius: 3px;
  overflow: hidden;
}
.gsearch-scope button[b-4ngqjrc52v] {
  background: none; border: 0;
  color: var(--fg-3);
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.14em;
  padding: 4px 10px;
  cursor: pointer;
}
.gsearch-scope[data-current="all"]     button[data-value="all"][b-4ngqjrc52v],
.gsearch-scope[data-current="faction"] button[data-value="faction"][b-4ngqjrc52v] {
  background: var(--accent-dim);
  color: var(--accent-fg);
  font-weight: 600;
}
.gsearch-count[b-4ngqjrc52v] { margin-left: auto; font-size: 11px; letter-spacing: 0.14em; color: var(--fg-3); }
.gsearch-list[b-4ngqjrc52v] { overflow-y: auto; padding: 4px 0; }
.gsearch-item[b-4ngqjrc52v] {
  display: grid;
  grid-template-columns: 36px 1fr auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  background: none;
  border: 0;
  border-left: 2px solid transparent;
  padding: 8px 12px;
  text-align: left;
  cursor: pointer;
  color: var(--fg-1);
}
.gsearch-item.is-active[b-4ngqjrc52v] {
  background: var(--accent-dim);
  border-left-color: oklch(70% 0.18 var(--accent-hue));
  color: var(--fg-0);
}
.gsearch-faction[b-4ngqjrc52v] {
  font-size: 11px; letter-spacing: 0.14em;
  text-align: center;
  border: 1px solid var(--line-1);
  border-radius: 3px;
  padding: 3px 0;
  color: oklch(70% 0.12 var(--fh, var(--accent-hue)));
  background: oklch(20% 0.02 var(--fh, var(--accent-hue)) / 0.4);
}
.gsearch-faction.is-self[b-4ngqjrc52v] { border-color: oklch(70% 0.18 var(--fh, var(--accent-hue))); }
.gsearch-name[b-4ngqjrc52v] { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 6px; }
.gsearch-sub[b-4ngqjrc52v] { color: var(--fg-3); font-weight: 400; }
.gsearch-leg[b-4ngqjrc52v] {
  font-size: 11px; letter-spacing: 0.14em;
  border: 1px dashed var(--line-2);
  padding: 1px 5px;
  color: var(--fg-3);
  border-radius: 2px;
}
/* Non-play category badge (HVT, Civilian, …). Accent-tinted — vs the neutral dashed
   .gsearch-leg — so it reads as a distinct kind marker, matching .unit-npc on cards. */
.gsearch-npc[b-4ngqjrc52v] {
  font-size: 11px; letter-spacing: 0.14em;
  border: 1px solid var(--accent-line);
  padding: 1px 5px;
  color: var(--accent-fg);
  background: var(--accent-dim);
  border-radius: 2px;
}
.gsearch-tags[b-4ngqjrc52v] {
  display: flex; gap: 6px; margin-top: 3px;
  font-size: 11px; letter-spacing: 0.1em;
  color: var(--fg-3);
  flex-wrap: wrap;
}
.gsearch-mtag[b-4ngqjrc52v] {
  text-transform: uppercase;
  border: 1px solid var(--line-1);
  border-radius: 2px;
  padding: 1px 5px;
}
.gsearch-arrow[b-4ngqjrc52v] { color: var(--fg-3); font-size: 12px; opacity: 0; }
.gsearch-item.is-active .gsearch-arrow[b-4ngqjrc52v] { opacity: 1; }
.gsearch-empty[b-4ngqjrc52v] { padding: 32px 16px; text-align: center; }
.gsearch-empty .big[b-4ngqjrc52v] { font-size: 12px; letter-spacing: 0.18em; color: var(--fg-2); margin-bottom: 6px; }
.gsearch-empty .hint[b-4ngqjrc52v] { font-size: 11px; color: var(--fg-3); letter-spacing: 0.04em; font-family: var(--ff-sans); }
.gsearch-foot[b-4ngqjrc52v] {
  display: flex; gap: 16px;
  padding: 8px 12px;
  border-top: 1px solid var(--line-1);
  background: var(--bg-1);
  font-size: 10px; letter-spacing: 0.12em;
  color: var(--fg-3);
}
.gsearch-foot kbd[b-4ngqjrc52v] {
  font-family: var(--ff-mono);
  border: 1px solid var(--line-1);
  border-radius: 2px;
  padding: 1px 5px;
  margin-right: 4px;
  background: var(--bg-0);
  color: var(--fg-2);
}
/* /Components/Layout/IdentSheet.razor.rz.scp.css */
/* ── Sheet ── */
.sheet[b-xbc6ovafuu] { padding:clamp(20px, 2.5vw, 32px); display:flex; flex-direction:column; gap:28px; }

.sheet-header[b-xbc6ovafuu] { display:flex; align-items:center; gap:20px; border-bottom:1px solid var(--line-1); padding-bottom:22px; }
/* Mark + title sized down from 64/18/36 → 56/14/32 so the hero footprint shrinks
   without restructuring. Visual weight stays on the title + breadcrumb; the mark
   is a recognition glyph, not a hero element. */
.sheet-mark[b-xbc6ovafuu] {
  width:56px; height:56px; border:1px solid var(--accent); position:relative;
  display:grid; place-items:center; background:var(--accent-dim); flex-shrink:0;
}
.sheet-mark[b-xbc6ovafuu]::before { content:""; position:absolute; inset:8px; border:1px solid var(--accent); transform:rotate(45deg); }
.sheet-mark[b-xbc6ovafuu]::after { content:""; position:absolute; width:14px; height:14px; border:1px solid var(--accent); border-radius:50%; }
.sheet-title[b-xbc6ovafuu] { display:flex; flex-direction:column; gap:4px; }
.sheet-title .crumbs[b-xbc6ovafuu] { font-family:var(--ff-mono); font-size:10px; letter-spacing:0.18em; color:var(--fg-3); }
.sheet-title h2[b-xbc6ovafuu] { margin:0; font-size:32px; font-weight:600; letter-spacing:0.04em; line-height:1; }
.sheet-title .desc[b-xbc6ovafuu] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.18em; color:var(--fg-2); margin:4px 0 0; }

/* Hero metadata strip — right-aligned key/value rows. .v.filtered flips to cyan
   when the rail filter has reduced the visible count below the faction's total.
   Per-row not per-column layout (vs the old grid) because the value lengths vary
   widely (3-digit fractions next to single integers); column alignment would
   have either expanded gutters or truncated values. If this grows past ~6 rows
   the handoff suggests switching to `display:grid;grid-template-columns:auto auto`. */
.sheet-meta[b-xbc6ovafuu] {
  margin-left:auto;
  display:flex; flex-direction:column;
  align-items:flex-end;
  gap:4px;
  font:500 11px/1.6 var(--ff-mono);
  letter-spacing:0.1em;
  color:var(--fg-3);
}
.sheet-meta .row[b-xbc6ovafuu] { display:flex; gap:10px; align-items:baseline; }
.sheet-meta .k[b-xbc6ovafuu] { color:var(--fg-3); }
.sheet-meta .v[b-xbc6ovafuu] { color:var(--fg-1); font-variant-numeric:tabular-nums; }
.sheet-meta .v.filtered[b-xbc6ovafuu] { color:var(--accent); }

/* Wrapper for .sheet-meta + (mobile) .sheet-density-mobile. On desktop it
   uses display:contents so the inner .sheet-meta still floats right via its
   own margin-left:auto and the layout is unchanged. On phone (see @media
   below) it becomes a flex column so the density toggle stacks under UNITS
   on the right side of the sheet-header. */
.sheet-meta-stack[b-xbc6ovafuu] { display: contents; }

/* Mobile-only density toggle inside the sheet header — hidden above 600px
   where the topbar copy is the canonical one. See @media block below. */
.sheet-density-mobile[b-xbc6ovafuu] { display: none; }

/* ── Troop-type blocks ──
   Each troop type is its own structural .ttype container with a code chip +
   rotated label in its left gutter. No background or border on .ttype — the
   role-label underlines below carry the visual rhythm. The block exists only
   to (a) reserve gutter padding for the chip + rotated label, and (b) act as
   a flex-wrap item so units wrapping to a second row stay tied to the right
   troop type rather than visually bleeding into the next.

   --ttype-gutter scales with the active card density (see .sheet[data-density]
   block below). XS / S leave extra room beyond the chip so it doesn't crowd
   the first card; the chip is fixed-size while gutter shrinks below M, so
   over-tight gutter values would push the chip right up against the cluster. */
.sheet[b-xbc6ovafuu]                    { --ttype-gutter: 44px; }
.sheet[data-density="xs"][b-xbc6ovafuu] { --ttype-gutter: 40px; }
.sheet[data-density="s"][b-xbc6ovafuu]  { --ttype-gutter: 42px; }
.sheet[data-density="m"][b-xbc6ovafuu]  { --ttype-gutter: 44px; }
.sheet[data-density="l"][b-xbc6ovafuu]  { --ttype-gutter: 50px; }

.ttype-flow[b-xbc6ovafuu] {
  display: flex; flex-wrap: wrap;
  gap: 24px;
  align-items: flex-start;
}
.ttype[b-xbc6ovafuu] {
  position: relative;
  padding: 6px 0 0 var(--ttype-gutter);
  flex: 0 1 auto;       /* size to content, allowed to shrink, never overflow */
  min-width: 0;
  max-width: 100%;
}

/* Code chip — pinned to top-left of the gutter, absolutely positioned so it
   contributes zero height to the block. Tinted from the active faction's accent
   via the existing --accent-* family (re-derived in this scope through the
   .accent-scope cascade — same pattern as ProductCard's .sculpt). */
.ttype__code[b-xbc6ovafuu] {
  position: absolute;
  left: 12px;
  top:  12px;
  padding: 3px 5px;
  background: var(--accent-dim);
  color: var(--accent);
  border: 1px solid var(--accent-line);
  font-family: var(--ff-mono);
  font-size: 9.5px; font-weight: 600;
  letter-spacing: 0.12em;
}

/* Rotated full label — sits in the gutter below the chip. bottom: 12px is the
   critical bit: writing-mode vertical-rl + rotate(180deg) lays out text
   aligned to the bottom of the box, so stretching the box across the full
   gutter is what pins the label down nicely next to the cards rather than
   floating at the top.
   white-space: nowrap locks single-line — at XS (the smallest density) the
   shortest block still gives ~147px of vertical space, more than enough for
   any troop-type name. If a particularly long faction name ever doesn't fit,
   chip-only (no rotated label) is the right fallback, not column-wrapping. */
.ttype__label[b-xbc6ovafuu] {
  position: absolute;
  left:   calc(var(--ttype-gutter) - 26px);
  top:    40px;
  bottom: 12px;
  width:  18px;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--ff-mono);
  font-size: 10px; font-weight: 500;
  line-height: 1;
  letter-spacing: 0.22em;
  color: var(--fg-3);
  text-transform: capitalize;
  white-space: nowrap;
}

/* Shrink the chip a notch at XS so it doesn't dominate the smallest gutter. */
.sheet[data-density="xs"] .ttype__code[b-xbc6ovafuu] { font-size: 9px; padding: 2px 4px; }

/* No chip (Silhouette.Short is null — operator hasn't filled it in yet) →
   the label takes the chip's spot so a half-migrated row doesn't render with
   28px of empty gutter above its rotated label. :has() is supported in every
   evergreen browser the project already targets. */
.ttype:not(:has(.ttype__code)) .ttype__label[b-xbc6ovafuu] { top: 12px; }

/* Each cluster is sized by its own units row, not by an equal grid column — so a 1-unit
   cluster stays narrow and a 4-unit cluster stretches wide, both side-by-side until the
   .ttype block fills. Eliminates the dead space that grid auto-fit was reserving. */
.roles[b-xbc6ovafuu] { display:flex; flex-wrap:wrap; gap:14px 18px; align-items: flex-start; }
.role-cluster[b-xbc6ovafuu] { flex:0 0 auto; display:flex; flex-direction:column; gap:12px; padding:6px 0; min-width: var(--uc-min, 124px); max-width:100%; }
.role-cluster .role-label[b-xbc6ovafuu] {
  /* Cluster width is constrained by the unit cards underneath (~140px).
     Letter-spacing dropped to 0 to keep longer labels ("Spec. Trained
     Troops 01") within the cluster width. */
  font-family:var(--ff-mono); font-size:10px; letter-spacing:0; color:var(--fg-2);
  border-bottom:1px solid var(--line-1); padding-bottom:8px;
  display:flex; justify-content:space-between;
}
.role-cluster .role-count[b-xbc6ovafuu] { color:var(--fg-3); }
.role-cluster .units[b-xbc6ovafuu] { display:flex; flex-wrap:wrap; gap:4px; }

/* ── Packed layout (cross-role-packing feature flag ON) ──
   Replaces the .role-cluster wrapper with a flat flow where cards from
   different roles share rows. Role boundaries are marked by .role-marker
   chips — a vertical-chip companion to the existing .ttype__code chip
   in the silhouette gutter. Same accent-tinted treatment, smaller scale. */
.roles-packed[b-xbc6ovafuu] {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: stretch;
}

.role-marker[b-xbc6ovafuu] {
  width: 28px;
  height: var(--uc-min-h, 263px);
  background: var(--accent-dim);
  border: 1px solid var(--accent-line);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  font-family: var(--ff-mono);
  color: var(--accent);
}

.role-marker__label[b-xbc6ovafuu] {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.22em;
}

.role-marker__count[b-xbc6ovafuu] {
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Card-density steps. Variables consumed by .unit, .unit-img, .role-cluster,
   and .skeleton-unit-card. The default block matches
   today's hardcoded values so a .sheet without [data-density] renders identically
   to pre-refactor markup. Custom-property cascade works across scope boundaries,
   so .unit (in UnitCard's scope) reads --uc-w etc. set here. */
/* --uc-min-h is sized to the natural content below the image: 5px flex-gap +
   2-line name slot + 5px flex-gap + sectorial badges. Bottom padding is 0 (see
   .unit padding) so no extra space is reserved. At XS the badges are hidden via
   the ::deep rule below, so the slot collapses to "gap + name" only. */
/* --uc-rb-size / --uc-rb-gap drive the hover-revealed +/- roster buttons on
   .unit-roster-btn / .unit-roster-ctl. They scale with density so the buttons
   stay proportional to the card image — see the roster-button-density spec. */
.sheet[b-xbc6ovafuu]                    { --uc-w:140px; --uc-img-h:208px; --uc-min-h:263px; --uc-min:124px; --uc-rb-size:42px; --uc-rb-gap:16px; }
.sheet[data-density="xs"][b-xbc6ovafuu] { --uc-w: 96px; --uc-img-h:144px; --uc-min-h:173px; --uc-min: 88px; --uc-rb-size:28px; --uc-rb-gap:10px; }
.sheet[data-density="s"][b-xbc6ovafuu]  { --uc-w:116px; --uc-img-h:172px; --uc-min-h:227px; --uc-min:104px; --uc-rb-size:34px; --uc-rb-gap:12px; }
.sheet[data-density="l"][b-xbc6ovafuu]  { --uc-w:168px; --uc-img-h:248px; --uc-min-h:303px; --uc-min:148px; --uc-rb-size:50px; --uc-rb-gap:18px; }
/* Density-conditional overrides that target classes rendered by the child
   UnitCard.razor (.unit-sub, .unit-badges, .unit-name, .unit-coll, .unit-status)
   MUST use `::deep` to cross the scoped-CSS boundary. Without `::deep`, the
   Blazor scoped-CSS rewriter appends IdentSheet's scope marker to the leaf
   selector, but the actual elements carry UnitCard's scope marker — the rules
   would never match at runtime. */
.sheet[data-density="xs"][b-xbc6ovafuu]  .unit-badges { display:none; }
.sheet[data-density="xs"][b-xbc6ovafuu]  .unit-name { font-size:11px; }

/* ── Empty / stub states ──
   "No units in this faction" stub rendered by IdentSheet's totalFactionCount==0
   branch. The shared .empty-state primitive lives in EmptyState.razor.css. */
.stub-empty[b-xbc6ovafuu] { padding:80px 32px; display:flex; flex-direction:column; align-items:center; text-align:center; gap:14px; color:var(--fg-2); }
.stub-mark[b-xbc6ovafuu] { width:96px; height:96px; border:1px solid var(--accent-line); background:var(--accent-dim); display:grid; place-items:center; position:relative; margin-bottom:8px; }
.stub-mark[b-xbc6ovafuu]::before { content:""; position:absolute; inset:16px; border:1px solid var(--accent); transform:rotate(45deg); }
.stub-mark[b-xbc6ovafuu]::after { content:"?"; font-family:var(--ff-mono); font-size:24px; font-weight:700; color:var(--accent); position:relative; }
.stub-empty h3[b-xbc6ovafuu] { margin:0; font-size:22px; letter-spacing:0.04em; font-weight:600; color:var(--fg-0); }
/* Empty-state body copy — was 0.16em (headline tracking) which made "Nothing here
   yet — this faction's units haven't been added." physically slow to read.
   0.04em keeps the mono+lowercase identity without slowing comprehension. */
.stub-msg[b-xbc6ovafuu] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.04em; color:var(--fg-3); max-width:480px; line-height:1.6; }
.stub-meta[b-xbc6ovafuu] { display:flex; gap:24px; margin-top:12px; font-family:var(--ff-mono); font-size:11px; letter-spacing:0.08em; color:var(--fg-3); }
.stub-meta span strong[b-xbc6ovafuu] { color:var(--accent); font-weight:500; }

/* Legacy archive section at bottom of sheet */
.legacy-archive[b-xbc6ovafuu] {
  margin-top: 12px;
  border: 1px dashed var(--line-2);
  background: oklch(from var(--bg-0) calc(l - 0.01) c h);
  padding: 20px 24px 24px;
  border-radius: var(--r-sm);
}
.legacy-archive-head[b-xbc6ovafuu] {
  display: flex; align-items: flex-end; gap: 16px;
  border-bottom: 1px dashed var(--line-1);
  padding-bottom: 14px;
  margin-bottom: 18px;
}
.legacy-archive-title[b-xbc6ovafuu] { display: flex; flex-direction: column; gap: 2px; }
.legacy-archive-title .crumbs[b-xbc6ovafuu] { font-size: 10px; letter-spacing: 0.18em; color: var(--fg-3); }
.legacy-archive-title h3[b-xbc6ovafuu] {
  margin: 0;
  font-size: 22px; font-weight: 600; letter-spacing: 0.04em;
  color: var(--fg-1);
}
.legacy-archive-desc[b-xbc6ovafuu] { font-size: 11px; letter-spacing: 0.16em; color: var(--fg-3); margin-top: 2px; }
.legacy-archive-count[b-xbc6ovafuu] {
  margin-left: auto;
  font-size: 11px; letter-spacing: 0.16em; color: var(--fg-3);
  border: 1px solid var(--line-1);
  padding: 4px 10px;
}

/* Non-playable band: accent-tinted dashed border distinguishes it from the
   neutral legacy archive above. Cards lay out as a flat flex-wrap strip —
   non-play models have no silhouette/role, so there's no troop-type grouping. */
.nonplay-band[b-xbc6ovafuu] {
  margin-top: 12px;
  border: 1px dashed var(--accent-line);
  background: oklch(from var(--bg-0) calc(l - 0.01) c h);
  padding: 20px 24px 24px;
  border-radius: var(--r-sm);
}
.nonplay-grid[b-xbc6ovafuu] {
  display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px;
}

/* ── Card grid at phone width ──
   At <=600px each .ttype block's vertical gutter collapses to a horizontal
   mini-header (chip + un-rotated label sit inline above the cards) — gutter
   space is too precious to spend on a rotated label at phone widths. Roles
   stack full-width and cards drop into a density-driven grid (XS=4, S=3, M=2,
   L=1 columns). The desktop topbar density-toggle is hidden (per the topbar
   @media above); a mirror copy of the toggle lives inside the sheet-header so
   phone users can still tune card size. Useful especially for factions with
   150+ units where 2 columns is too coarse. */
@media (max-width: 600px) {
  .sheet[b-xbc6ovafuu] { padding: 16px var(--page-gutter); gap: 20px; }

  /* Sheet-header stays a flex row on phone. The right side (UNITS count +
     density toggle) becomes a vertical stack so it fits next to a shrunk
     mark + title rather than dropping below them. */
  .sheet-header[b-xbc6ovafuu] {
    gap: 12px;
    padding-bottom: 16px;
    align-items: center;
  }
  .sheet-mark[b-xbc6ovafuu] { width: 48px; height: 48px; }      /* down from 64px */
  .sheet-title[b-xbc6ovafuu] { min-width: 0; }                  /* allow long names to shrink rather than overflow */
  .sheet-title h2[b-xbc6ovafuu] { font-size: 22px; }            /* down from 36px — column is narrower */
  .sheet-title .crumbs[b-xbc6ovafuu] { font-size: 9px; }
  .sheet-title .desc[b-xbc6ovafuu] { display: none; }           /* faction tagline drops on phone — title + meta carry the message */
  .sheet-meta-desktop[b-xbc6ovafuu] { display: none; }
  .sheet-density-mobile[b-xbc6ovafuu] { display: block; }

  .sheet-meta-stack[b-xbc6ovafuu] {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
    margin-left: auto;       /* wrapper now provides the push-right */
  }
  .sheet-meta[b-xbc6ovafuu] { margin-left: 0; font-size: 10px; }
  /* Compact density toggle — segmented controls aren't critical-action
     targets, so 28x28 cells (smaller than --touch-min) is acceptable for
     a tightly-packed picker the user can re-tap to correct.
     ::deep reaches into the now-scoped DensityToggle so the mobile sheet
     can shrink the toggle when it's rendered in the sheet header. */
  .sheet-density-mobile[b-xbc6ovafuu]  .density-toggle { height: 28px; }
  .sheet-density-mobile[b-xbc6ovafuu]  .density-cell { width: 28px; font-size: 10px; }

  .ttype-flow[b-xbc6ovafuu] { gap: 12px; }

  /* Block becomes a vertical stack: header row (chip + label) + roles below.
     Collapse the gutter (no left padding) since the label is no longer rotated
     into a left column. */
  .ttype[b-xbc6ovafuu] {
    padding: 36px 12px 12px 12px;
    width: 100%;
  }
  /* Chip stays absolutely positioned at top-left of the block — same anchor
     point as desktop, just sized for the now-horizontal header. */
  .ttype__code[b-xbc6ovafuu] { left: 12px; top: 12px; }
  /* Un-rotate the label and place it inline next to the chip in the header. */
  .ttype__label[b-xbc6ovafuu] {
    position: absolute;
    left: 44px;
    top: 13px;
    bottom: auto;
    width: auto;
    writing-mode: horizontal-tb;
    transform: none;
    font-size: 11px;
    letter-spacing: 0.16em;
    color: var(--fg-2);
  }

  .roles[b-xbc6ovafuu] { gap: 16px 0; }
  .role-cluster[b-xbc6ovafuu] { min-width: 0; width: 100%; }

  /* Density-driven column count. The `.sheet` / `.sheet[data-density="..."]`
     prefix gives 0,3,0 specificity — beats the unconditional
     `.role-cluster .units { display:flex; flex-wrap:wrap }` rule (0,2,0)
     declared earlier in this file. */
  .sheet .role-cluster .units[b-xbc6ovafuu]                      { display: grid; gap: 6px; grid-template-columns: repeat(2, 1fr); }
  .sheet[data-density="xs"] .role-cluster .units[b-xbc6ovafuu]   { grid-template-columns: repeat(4, 1fr); gap: 4px; }
  .sheet[data-density="s"]  .role-cluster .units[b-xbc6ovafuu]   { grid-template-columns: repeat(3, 1fr); gap: 6px; }
  .sheet[data-density="m"]  .role-cluster .units[b-xbc6ovafuu]   { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .sheet[data-density="l"]  .role-cluster .units[b-xbc6ovafuu]   { grid-template-columns: 1fr;            gap: 8px; }

  /* Packed layout on mobile — same markup, different shape.
     .roles-packed becomes a CSS grid and .role-marker becomes
     a full-row separator with un-rotated label. Visual end-result
     is equivalent to today's mobile stacked layout. */
  .roles-packed[b-xbc6ovafuu] {
    display: grid;
    gap: 6px;
    grid-template-columns: repeat(2, 1fr);
  }
  .sheet[data-density="xs"] .roles-packed[b-xbc6ovafuu] { grid-template-columns: repeat(4, 1fr); gap: 4px; }
  .sheet[data-density="s"]  .roles-packed[b-xbc6ovafuu] { grid-template-columns: repeat(3, 1fr); gap: 6px; }
  .sheet[data-density="m"]  .roles-packed[b-xbc6ovafuu] { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .sheet[data-density="l"]  .roles-packed[b-xbc6ovafuu] { grid-template-columns: 1fr;            gap: 8px; }

  .role-marker[b-xbc6ovafuu] {
    width: auto;
    height: 28px;
    grid-column: 1 / -1;
    flex-direction: row;
    justify-content: flex-start;
    padding: 0 12px;
    gap: 10px;
  }
  .role-marker__label[b-xbc6ovafuu] {
    writing-mode: horizontal-tb;
    transform: none;
    font-size: 10px;
    letter-spacing: 0.16em;
  }

  /* Cards drop their fixed pixel width inside the grid; image height
     scales per-density so a 4-col XS card doesn't tower over its neighbors.
     The density token overrides need specificity 0,2,0 to beat the existing
     `.sheet[data-density="..."]` rules above (which set --uc-w etc. at
     0,2,0 regardless of viewport). We list all four densities + the bare
     .sheet so the override applies uniformly; source order then resolves
     the tie in our favor. */
  .sheet[b-xbc6ovafuu],
  .sheet[data-density="xs"][b-xbc6ovafuu],
  .sheet[data-density="s"][b-xbc6ovafuu],
  .sheet[data-density="m"][b-xbc6ovafuu],
  .sheet[data-density="l"][b-xbc6ovafuu] { --uc-w: 100%; --uc-min-h: auto; }
  .sheet[data-density="xs"][b-xbc6ovafuu] { --uc-img-h: clamp( 92px, 22vw, 130px); }
  .sheet[data-density="s"][b-xbc6ovafuu]  { --uc-img-h: clamp(118px, 30vw, 170px); }
  .sheet[data-density="m"][b-xbc6ovafuu]  { --uc-img-h: clamp(140px, 38vw, 200px); }
  .sheet[data-density="l"][b-xbc6ovafuu]  { --uc-img-h: clamp(200px, 56vw, 320px); }
  /* Mobile width/min-height for the unit card are driven by --uc-w / --uc-min-h
     above, which cascade through scope boundaries into UnitCard.razor.css. */
}
/* /Components/Layout/IdentSidebar.razor.rz.scp.css */
/* ── Sidebar ── */
.sidebar[b-9xegsqs5i4] {
  border-right: 1px solid var(--line-1);
  background: var(--bg-1);
  /* Extra bottom padding lifts the "Reset filters" button clear of the
     fixed .unofficial-badge (bottom-left, ~30px tall counting its 8px
     inset) when the sidebar is scrolled to the end. */
  padding: 20px 16px 56px;
  position: sticky;
  top: 0;
  /* Fits the locked-overflow viewport. .ident-app overrides --site-header-h
     to 0 on dense pages (SiteHeader is NOT rendered above them), so the calc
     resolves to 100vh - 0 = 100vh. The variable form is preserved so prose
     pages (About / CookiePolicy / NotFound) — which DO render <SiteHeader />
     and don't use .ident-app — fall back to the :root default (44px) if this
     sidebar ever gets rendered there. */
  height: calc(100vh - var(--site-header-h, 0px));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.brand[b-9xegsqs5i4] { display:flex; align-items:center; gap:10px; padding-bottom:14px; border-bottom:1px solid var(--line-1); }
.brand-mark[b-9xegsqs5i4] {
  width:28px; height:28px; border:1px solid var(--accent);
  background:var(--accent-dim); display:grid; place-items:center; position:relative;
}
.brand-mark[b-9xegsqs5i4]::before { content:""; position:absolute; inset:4px; border:1px solid var(--accent); transform:rotate(45deg); }
.brand-name[b-9xegsqs5i4] { font-family:var(--ff-mono); font-weight:700; font-size:13px; letter-spacing:0.16em; }
.brand-sub[b-9xegsqs5i4]  { font-family:var(--ff-mono); font-size:10px; letter-spacing:0.1em; color:var(--fg-3); margin-top:2px; }

.section-label[b-9xegsqs5i4] {
  font-family:var(--ff-mono); font-size:11px; letter-spacing:0.18em; color:var(--fg-3);
  margin-bottom:8px; display:flex; justify-content:space-between; align-items:baseline;
}
.section-count[b-9xegsqs5i4] { color:var(--accent); }

.faction-list[b-9xegsqs5i4], .filter-list[b-9xegsqs5i4] { display:flex; flex-direction:column; gap:2px; }
.faction-row[b-9xegsqs5i4], .filter-row[b-9xegsqs5i4] {
  display:flex; align-items:center; gap:8px; padding:6px 8px;
  border-radius:var(--r-sm); font-size:12px; color:var(--fg-1);
  cursor:pointer; border:1px solid transparent;
  /* width + text-align defaults are wrong on <button>; reset so a <button class="filter-row">
     fills its column and reads left-aligned like the div version did. */
  width:100%; text-align:left;
  transition:background .12s,border-color .12s,color .12s;
}
.faction-row:hover[b-9xegsqs5i4],.filter-row:hover[b-9xegsqs5i4] { background:var(--bg-2); color:var(--fg-0); }
.faction-row.is-active[b-9xegsqs5i4],.filter-row.is-active[b-9xegsqs5i4] { background:var(--bg-2); border-color:var(--accent-line); color:var(--fg-0); }
.filter-row.is-active .check[b-9xegsqs5i4] { background:var(--accent); border-color:var(--accent); }
.filter-row.is-active .check[b-9xegsqs5i4]::after { content:""; position:absolute; inset:2px; background:var(--bg-0); }
.faction-row.is-active .check[b-9xegsqs5i4] { background:var(--accent); border-color:var(--accent); }
.faction-row.is-active .check[b-9xegsqs5i4]::after { content:""; position:absolute; inset:2px; background:var(--bg-0); }
.check[b-9xegsqs5i4] { width:12px; height:12px; border:1px solid var(--line-2); border-radius:1px; position:relative; flex-shrink:0; }
.code-tag[b-9xegsqs5i4] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.1em; color:var(--fg-3); margin-left:auto; }
.faction-row.is-active .code-tag[b-9xegsqs5i4],.filter-row.is-active .code-tag[b-9xegsqs5i4] { color:var(--accent); }
@media (max-width: 600px) {
  .faction-row[b-9xegsqs5i4], .filter-row[b-9xegsqs5i4] { padding: 12px 8px; }
}
.logic-toggle[b-9xegsqs5i4] {
  display:flex; border:1px solid var(--line-1); border-radius:var(--r-sm);
  font-family:var(--ff-mono); font-size:11px; letter-spacing:0.14em; margin-bottom:8px; overflow:hidden;
}
.logic-toggle button[b-9xegsqs5i4] { flex:1; padding:6px; color:var(--fg-2); background:transparent; border-right:1px solid var(--line-1); }
.logic-toggle button:last-child[b-9xegsqs5i4] { border-right:0; }
/* Selection: parent's data-current matches the button's data-value */
.logic-toggle[data-current="OR"]  button[data-value="OR"][b-9xegsqs5i4],
.logic-toggle[data-current="AND"] button[data-value="AND"][b-9xegsqs5i4] {
  background:var(--accent-dim);
  color:var(--accent-fg);
  font-weight:600;
}

/* .clear-btn is a shared "Reset filters / Back" primitive used by IdentSidebar,
   IdentSheet's empty-state CTA, Products, NotFound and Error — kept as a global
   primitive in app.css (mirrors how .inline-error was restored in Wave 1). */
/* /Components/Layout/ImportArmyBuilderDialog.razor.rz.scp.css */
/* ── Import dialog (ImportArmyBuilderDialog.razor) ────────────────
   Three states share the same <dialog> shell:
     · Paste      — mono-caps "Import from Army builder" h2 + textarea + actions
     · Preview    — header-block (kicker / Inter title / dest), stats-row
                    (matched · unrecognized), preview-list, optional skip-note,
                    close ×, actions
     · Committing — same header-block + stats-row + preview-list as Preview
                    (so the structure doesn't reflow on state change), with the
                    actions row replaced by a commit-caption. The whole dialog
                    runs the cyan scan via two pseudos gated by data-state.

   Visual language anchors on ConfirmDialog (dark surface, mono accent, ghost +
   primary action pair). Tuned to the v2 handoff from the designer. */

/* Dialog shell — position:relative is the scan's positioning context.
   overflow:hidden clips the 14px sweep overshoot at the top/bottom edges so
   the bright line doesn't drift past the dialog. Both are needed regardless
   of which state is active. */
.import-dialog[b-yrtf1881xr] {
  margin: auto;
  padding: 0;
  background: oklch(from var(--bg-1) l c h / 0.98);
  border: 1px solid var(--line-2);
  color: var(--fg-0);
  min-width: 380px;
  max-width: 520px;
  font-family: var(--ff-sans);
  position: relative;
  overflow: hidden;
  /* Scan loop period — public knob so a faction page or feature flag can
     override per-context without touching this file. */
  --scan-duration: 1400ms;
}
.import-dialog[b-yrtf1881xr]::backdrop {
  background: oklch(0 0 0 / 0.55);
  backdrop-filter: blur(2px);
}

/* Each top-level block inside <dialog> shares the same horizontal gutter
   so the title, body copy, textarea, list, stats, and actions all align
   flush on both edges. */
.import-dialog > h2[b-yrtf1881xr],
.import-dialog > .helper[b-yrtf1881xr],
.import-dialog > textarea[b-yrtf1881xr],
.import-dialog > .error[b-yrtf1881xr],
.import-dialog > .header-block[b-yrtf1881xr],
.import-dialog > .stats-row[b-yrtf1881xr],
.import-dialog > .preview-list[b-yrtf1881xr],
.import-dialog > .skip-note[b-yrtf1881xr] {
  margin-left: 24px;
  margin-right: 24px;
}

/* ── Paste-state title — mono caps, unchanged from the previous styling ── */
.import-dialog h2[b-yrtf1881xr] {
  font-family: var(--ff-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  margin-top: 22px;
  margin-bottom: 10px;
}

.import-dialog .helper[b-yrtf1881xr] {
  font-size: 13px;
  line-height: 1.5;
  color: var(--fg-1);
  margin-top: 0;
  margin-bottom: 14px;
}

.import-dialog textarea[b-yrtf1881xr] {
  display: block;
  /* `width: auto` falls back to the textarea's intrinsic `cols`-based width
     (default 20ch) on a replaced element, leaving a stub in the middle of
     the dialog. Pin the width explicitly to "dialog content minus the 24px
     gutters on each side" so it actually fills the available space. */
  width: calc(100% - 48px);
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  color: var(--fg-0);
  font-family: var(--ff-mono);
  font-size: 12px;
  line-height: 1.5;
  padding: 10px 12px;
  resize: vertical;
  transition: border-color .12s;
}
.import-dialog textarea:hover[b-yrtf1881xr] { border-color: var(--line-2); }
.import-dialog textarea:focus[b-yrtf1881xr],
.import-dialog textarea:focus-visible[b-yrtf1881xr] {
  border-color: var(--accent-line);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Quiet warning copy — the user pasted something unreadable or a commit
   failed. Body text stays in --fg-1 so it reads as a normal sentence; only
   the left rule + soft warn-tint background mark it as "needs attention". */
.import-dialog .error[b-yrtf1881xr] {
  font-size: 12px;
  line-height: 1.5;
  color: var(--fg-1);
  background: var(--warn-dim);
  border-left: 2px solid var(--warn);
  padding: 8px 12px;
  margin-top: 10px;
  margin-bottom: 4px;
}

/* ── Preview / Committing header-block — replaces the old h2 + subtitle.
   The title carries the heading-class fact (army name); the kicker tags
   the *kind* of thing this is (function label, mono caps). */
.import-dialog .header-block[b-yrtf1881xr] {
  padding-top: 20px;
  margin-bottom: 14px;
}
.import-dialog .kicker[b-yrtf1881xr] {
  font-family: var(--ff-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-3);
  margin-bottom: 8px;
}
.import-dialog .title[b-yrtf1881xr] {
  font-family: var(--ff-sans);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.012em;
  color: var(--fg-0);
  margin-bottom: 6px;
}
.import-dialog .dest[b-yrtf1881xr] {
  font-family: var(--ff-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.10em;
  color: var(--fg-2);
  display: flex;
  align-items: center;
  gap: 6px;
}
.import-dialog .dest .arrow[b-yrtf1881xr],
.import-dialog .dest .roster[b-yrtf1881xr] { color: var(--accent); }

/* ── Stats row — "N matched · M unrecognized". Top + bottom rule frame
   the quantitative summary between the header and the list. */
.import-dialog .stats-row[b-yrtf1881xr] {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--line-1);
  border-bottom: 1px solid var(--line-1);
  font-family: var(--ff-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.10em;
  text-transform: uppercase;
}
.import-dialog .stats-row .stat[b-yrtf1881xr] {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--fg-2);
}
.import-dialog .stats-row .stat .dot[b-yrtf1881xr] {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fg-3);
}
.import-dialog .stats-row .stat.ok[b-yrtf1881xr]        { color: var(--fg-0); }
.import-dialog .stats-row .stat.ok .dot[b-yrtf1881xr]   { background: var(--good); }
.import-dialog .stats-row .stat.warn[b-yrtf1881xr]      { color: var(--warn); }
.import-dialog .stats-row .stat.warn .dot[b-yrtf1881xr] { background: var(--warn); }
.import-dialog .stats-row .sep[b-yrtf1881xr]            { color: var(--fg-3); }

/* ── Preview list — the resolved units. The stats-row above already paints
   a bottom rule, so the list skips its own top border to avoid doubling up. */
.import-dialog .preview-list[b-yrtf1881xr] {
  list-style: none;
  padding: 0;
  margin-top: 0;
  margin-bottom: 14px;
  max-height: 320px;
  overflow-y: auto;
}
.import-dialog .stats-row + .preview-list[b-yrtf1881xr] { border-top: 0; }
.import-dialog .preview-list li[b-yrtf1881xr] {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 4px;
  border-bottom: 1px solid var(--line-1);
  font-size: 13px;
}
.import-dialog .unit-name[b-yrtf1881xr] {
  flex: 1;
  color: var(--fg-0);
}
/* Muted count chip — border only, no accent fill. ×N is metadata, not an
   action; the accent surface was reading as "important info" when it's just
   a quantity. */
.import-dialog .unit-count[b-yrtf1881xr] {
  font-family: var(--ff-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--fg-2);
  background: transparent;
  border: 1px solid var(--line-2);
  padding: 2px 6px;
  border-radius: var(--r-sm);
}

/* ── Skip-note — replaces the old .unresolved aside. Smaller surface, mono
   text, single warn glyph carrying the signal. Only rendered when there are
   unrecognized units in the preview (Razor conditional). */
.import-dialog .skip-note[b-yrtf1881xr] {
  font-family: var(--ff-mono);
  font-size: 10px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: 0.06em;
  color: var(--fg-3);
  margin-top: 10px;
  margin-bottom: 14px;
  display: flex;
  gap: 8px;
  align-items: flex-start;
}
.import-dialog .skip-note .info[b-yrtf1881xr] {
  color: var(--warn);
  flex-shrink: 0;
}

/* ── Close × — top-right corner. Above the scan layers (z-index 5)
   so it stays clickable in Committing if anyone re-enables it there;
   we don't render it during Committing, but the rule is safe to leave on. */
.import-dialog .close[b-yrtf1881xr] {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--fg-2);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 5;
  transition: color .12s, background .12s;
}
.import-dialog .close:hover[b-yrtf1881xr] {
  color: var(--fg-0);
  background: var(--bg-2);
}
.import-dialog .close:focus-visible[b-yrtf1881xr] {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ── Action row — Paste & Preview. Top rule + right-aligned ghost+primary
   pair. ConfirmDialog button treatment carried through. */
.import-dialog .actions[b-yrtf1881xr] {
  padding: 14px 24px 18px;
  border-top: 1px solid var(--line-1);
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 4px;
}
.import-dialog .actions button[b-yrtf1881xr] {
  font-family: var(--ff-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 9px 16px;
  cursor: pointer;
  background: var(--accent);
  border: 1px solid var(--accent);
  color: oklch(0.10 0.012 240);
  transition: background .12s, border-color .12s, color .12s, filter .12s;
}
.import-dialog .actions button:hover:not(:disabled)[b-yrtf1881xr] { filter: brightness(1.12); }
.import-dialog .actions button:focus-visible[b-yrtf1881xr] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.import-dialog .actions button:disabled[b-yrtf1881xr] {
  background: var(--bg-2);
  border-color: var(--line-1);
  color: var(--fg-3);
  cursor: not-allowed;
  filter: none;
}
.import-dialog .actions button.ghost[b-yrtf1881xr] {
  background: transparent;
  border-color: var(--line-2);
  color: var(--fg-1);
}
.import-dialog .actions button.ghost:hover[b-yrtf1881xr] {
  border-color: var(--accent-line);
  color: var(--fg-0);
  filter: none;
}

/* ── Committing scan animation ──────────────────────────────────
   When data-state="committing":
     1. ::before  — bright accent line sweeps the whole dialog top→bottom,
                    looping until the state flips. The drop-shadow gives it
                    a soft glow.
     2. ::after   — soft accent wash, same period, mix-blend-mode: screen for
                    a "warming" pass behind the line without a real backdrop-
                    filter compositing cost.
     3. The header / stats-row / preview-list dim to 0.72 — reads as "locked
        in, being processed" without hiding what's being imported.
     4. .commit-caption replaces .actions. Sits at z-index 4 so the caption
        text stays legible above the wash.
   The site-wide reduced-motion rule in app.css already clamps all animation
   durations to 1ms — no local @media override needed. */
.import-dialog[data-state="committing"][b-yrtf1881xr]::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  background: linear-gradient(
    180deg,
    transparent 0%,
    var(--accent) 49%,
    var(--accent) 50%,
    var(--accent) 51%,
    transparent 100%
  );
  background-size: 100% 14px;
  background-repeat: no-repeat;
  background-position: 0 -14px;
  animation: dialog-scan-sweep-b-yrtf1881xr var(--scan-duration) ease-in-out infinite;
  filter: drop-shadow(0 0 8px var(--accent));
}
.import-dialog[data-state="committing"][b-yrtf1881xr]::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background: var(--accent);
  opacity: 0;
  mix-blend-mode: screen;
  animation: dialog-scan-fill-b-yrtf1881xr var(--scan-duration) ease-out infinite;
}
.import-dialog[data-state="committing"] .header-block[b-yrtf1881xr],
.import-dialog[data-state="committing"] .stats-row[b-yrtf1881xr],
.import-dialog[data-state="committing"] .preview-list[b-yrtf1881xr] {
  opacity: 0.72;
  transition: opacity 200ms ease;
}

.import-dialog .commit-caption[b-yrtf1881xr] {
  padding: 14px 24px 18px;
  border-top: 1px solid var(--line-1);
  margin-top: 4px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--ff-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  position: relative;
  z-index: 4;
}
.import-dialog .commit-caption .pulse[b-yrtf1881xr] {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: commit-pulse-b-yrtf1881xr 1s ease-in-out infinite;
}

@keyframes dialog-scan-sweep-b-yrtf1881xr {
  0%   { background-position: 0 -14px; opacity: 0; }
  8%   { opacity: 1; }
  100% { background-position: 0 calc(100% + 14px); opacity: 1; }
}
@keyframes dialog-scan-fill-b-yrtf1881xr {
  0%   { opacity: 0; }
  20%  { opacity: 0.08; }
  100% { opacity: 0; }
}
@keyframes commit-pulse-b-yrtf1881xr {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}

/* Phone (≤600px): drop the min-width floor so the dialog can shrink to
   fit narrow viewports. Matches the responsive pattern in sibling dialogs. */
@media (max-width: 600px) {
  .import-dialog[b-yrtf1881xr] {
    min-width: 0;
    width: calc(100vw - 32px);
  }
}
/* /Components/Layout/NewReleasesRail.razor.rz.scp.css */
/* ── New & Pre-order rail (NewReleasesRail.razor) ───────────────────────
   A single horizontal strip above the faction grid on /products. Deliberately
   quiet: a thin top/bottom hairline, the same 24px/32px section padding and
   14px gap the products grid already uses, and a faction-neutral accent hue so
   the rail reads as a calm "what's new" shelf rather than a loud promo banner.

   The cards inside are stock ProductCard instances; CSS custom properties cascade
   across the CSS-isolation boundary, so the rail's --accent-hue tints each card's
   accent family (the card's own per-sculpt hues still override at the sculpt level). */

.new-rail[b-xnhvkifgg8] {
  padding: 24px 32px;
  border-bottom: 1px solid var(--line-1);
  /* Faction-neutral cyan — the rail mixes boxes from many factions, so a single
     section hue would be arbitrary. 220 matches the page-level default in
     Products.razor's wrapper. */
  --accent-hue: 220;
}

.new-rail-head[b-xnhvkifgg8] {
  margin: 0 0 16px;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-2);
}

/* Horizontal track. Cards keep their natural height; the row scrolls sideways
   when the featured set overflows. Each card is given a comfortable fixed-ish
   width so the strip reads as a shelf of peers rather than stretching one card
   to fill the row. */
.new-rail-track[b-xnhvkifgg8] {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 4px;          /* room for the scrollbar without clipping card shadows */
  scroll-snap-type: x proximity;
}
/* Size the .product article, NOT the direct child. Master's crawlable-card change
   wraps each card in <a class="product-link"> which is display:contents — it
   generates no box, so a flex/size rule on it is ignored and the .product articles
   it passes through become the real flex items of the track. Targeting the direct
   child (the anchor) left those articles unsized and they collapsed into an
   overlapping stack. The article is the effective flex item, so size that. */
.new-rail-track[b-xnhvkifgg8]  .product {
  flex: 0 0 360px;
  max-width: 360px;
  scroll-snap-align: start;
}

/* On phones let a single card fill the viewport width so it doesn't feel
   cramped; the track still scrolls one card at a time. */
@media (max-width: 600px) {
  .new-rail[b-xnhvkifgg8] { padding: 18px clamp(16px, 4vw, 32px); }
  .new-rail-track[b-xnhvkifgg8]  .product {
    flex-basis: 86vw;
    max-width: 86vw;
  }
}
/* /Components/Layout/NoImagePlaceholder.razor.rz.scp.css */
/* Fills the consuming parent. The parent must supply position:relative —
   each current consumer's scoped CSS sets it (the placeholder is used in
   UnitCard, ProductCard, and UnitDetailDrawer; see those files for the
   exact selectors). */
.ph-frame[b-fn1c992jck] {
    position: absolute;
    inset: 0;
    container-type: inline-size;
}

/* The SVG carries the centered diamond geometry via <use href="#no-image">.
   It fills the frame; the symbol's own preserveAspectRatio handles the
   centering inside non-portrait containers (the diamond is square, so the
   meet behavior keeps it centered without distortion). */
.ph-frame > svg[b-fn1c992jck] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

/* Four 12×12 brackets, one per corner. Fixed pixel size: at the smallest
   surface (40×52 drawer sculpt-thumb) this is ~30% of width — visibly
   prominent but the label is also hidden at that size, so the brackets
   become the entire mark and that prominence reads as intentional. At
   the next-smallest (78×92 ProductCard sculpt thumb) it's ~15%, and at
   the largest (168×248 L-density unit card) ~7% — proportionate across
   the visible-label range. The borders come from --line-2, the same
   muted line color the empty-state chrome uses elsewhere in the app. */
.ph-corner[b-fn1c992jck] {
    position: absolute;
    width: 12px;
    height: 12px;
    border: 1px solid var(--line-2);
    z-index: 1;
}
.ph-corner-tl[b-fn1c992jck] { top: 6px;    left: 6px;  border-right: 0; border-bottom: 0; }
.ph-corner-tr[b-fn1c992jck] { top: 6px;    right: 6px; border-left: 0;  border-bottom: 0; }
.ph-corner-bl[b-fn1c992jck] { bottom: 6px; left: 6px;  border-right: 0; border-top: 0; }
.ph-corner-br[b-fn1c992jck] { bottom: 6px; right: 6px; border-left: 0;  border-top: 0; }

/* Centered label. clamp(9px, 9cqi, 14px) keeps it readable across the
   visible-label range: ~9px floor at the 78×92 ProductCard sculpt thumb
   (9cqi = 7.0px, clamped up), ~12.6px on the m-density unit card (140
   wide), 14px ceiling on the l-density unit card (168 wide). Below ~55px
   the label is hidden entirely by the @container rule below — see there
   for why. Container queries require .ph-frame to carry
   container-type: inline-size (set at the top of this file). */
.ph-label[b-fn1c992jck] {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-family: var(--ff-mono);
    font-size: clamp(9px, 9cqi, 14px);
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--fg-1);
    text-transform: uppercase;
    white-space: nowrap;
    z-index: 2;
}

/* Hide the label on very narrow surfaces. "NO IMAGE" at the 9px clamp
   floor needs ~47px of horizontal room (8 chars × ~5.4px advance + ~4px
   letter-spacing); the 55px threshold adds ~8px of breathing room before
   the bracket inset. Below it (e.g. the 40×52 sculpt-thumb in
   UnitDetailDrawer) the label is hidden and the placeholder reads as
   just the diamond + brackets — still unambiguous as a "no image" mark. */
@container (max-width: 55px) {
    .ph-label[b-fn1c992jck] { display: none; }
}
/* /Components/Layout/OrphanedUnitsList.razor.rz.scp.css */
.orphaned[b-9gmj17mvyo] {
    margin: 2rem 0;
    padding: 1rem;
    border: 1px solid oklch(from var(--warn) l c h / 0.6);
    background: oklch(from var(--warn) l c h / 0.06);
    border-radius: 6px;
}

.orphaned-head h2[b-9gmj17mvyo] {
    margin: 0;
    font-size: 0.85rem;
    letter-spacing: 0.08em;
    color: var(--warn);
}

.orphaned-head small[b-9gmj17mvyo] {
    display: block;
    margin-top: 0.25rem;
    color: var(--fg-2);
}

.orphaned-list[b-9gmj17mvyo] {
    list-style: none;
    padding: 0;
    margin: 1rem 0 0 0;
}

.orphaned-row[b-9gmj17mvyo] {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.5rem 0;
    border-top: 1px solid var(--line-1);
}

.orphaned-row:first-child[b-9gmj17mvyo] {
    border-top: none;
}

.orphaned-unit[b-9gmj17mvyo] {
    flex: 1 0 auto;
}

.orphaned-sub[b-9gmj17mvyo] {
    color: var(--fg-2);
}

.orphaned-chips[b-9gmj17mvyo] {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.orphaned-chip[b-9gmj17mvyo] {
    padding: 0.2rem 0.5rem;
    border: 1px solid var(--line-1);
    background: var(--bg-2);
    color: var(--fg-1);
    border-radius: 4px;
    font-size: 0.72rem;
    text-decoration: none;
}

.orphaned-chip:hover[b-9gmj17mvyo] {
    background: var(--bg-3);
    color: var(--fg-0);
}
/* /Components/Layout/PageHeader.razor.rz.scp.css */
/* Per-page strip. Sticky at the top of its scroll container so
   controls stay accessible as the page scrolls.
   --site-header-h is defined on :root (44px) for the prose pages
   (About / CookiePolicy / NotFound) which DO render <SiteHeader />
   above PageHeader; .ident-app overrides it to 0 for the dense
   pages (Catalog / Products / Buy) which do NOT render <SiteHeader />
   — there PageHeader is the topmost sticky element. The fallback
   44px covers the rare initial-paint case before either declaration
   has cascaded. */
.page-header[b-rzzmfyan9a] {
    position: sticky;
    top: var(--site-header-h, 44px);
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 8px 14px;
    background: var(--bg-1, #0c0e12);
    border-bottom: 1px solid var(--line-2, #2a2f38);
    min-height: 38px;
}

/* H1 — uppercase HUD treatment to match PR 183's topbar styling.
   Natural-case in the markup (Title="Aleph units"); CSS uppercases
   for visual presentation. Letter-spacing and font-weight inherit the
   existing display family. */
.page-header__title[b-rzzmfyan9a] {
    margin: 0;
    color: var(--fg-0, #e6ecf3);
    font-family: var(--ff-display, var(--ff-mono));
    font-size: 13px;
    letter-spacing: 0.18em;
    font-weight: 700;
    text-transform: uppercase;
    white-space: nowrap;
}

/* Right-side slot. Flex container so each control inside can lay
   itself out without the parent caring. Always rendered so layout
   stays consistent across pages with and without controls. */
.page-header__controls[b-rzzmfyan9a] {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-left: auto;
    flex-wrap: wrap;
    justify-content: flex-end;
}
/* /Components/Layout/ProductCard.razor.rz.scp.css */
/* ════════════════════════════════════════════════════════════════════════
   Products page redesign — ProductCard styles.

   The per-faction hue propagation (`.products-section { --accent-hue: ... }`)
   that wraps these cards is currently still in app.css; it will move into
   its own isolated stylesheet in a later wave. CSS custom properties cascade
   through scope boundaries, so the cards still pick up the section's
   accent-hue via inheritance regardless of where the wrapper rule lives.
   The shared .status-pill primitive remains in app.css — used here plus by
   CompareModal and UnitDetailDrawer.
   ════════════════════════════════════════════════════════════════════════ */

/* Wrapping anchor around .product — gives every card a real <a href="/products/{code}">
   for crawlers + middle-click while preserving the existing in-place click UX. The
   anchor is invisible to layout: display:contents removes its own box from the box
   tree so .product's grid math (and the .products-grid layout above) is unchanged.
   Do NOT change to display:block — it'll re-introduce an extra grid item and break
   the layout. */
.product-link[b-ejcw48rx06] {
  display: contents;
  color: inherit;
  text-decoration: none;
}

/* ── Card root ──────────────────────────────────────────────────────── */
.product[b-ejcw48rx06] {
  display: flex;
  flex-direction: column;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  position: relative;
  /* Container query context: lets the footer CTA shrink based on the card's
     own width (not the viewport), so a narrow card in any column layout
     gets the compact CTA treatment below.
     Side effect: `inline-size` also implies `contain: layout inline-size`,
     which makes `.product` a containing block for any `position: fixed`
     descendant. None today — but if a future overlay (modal, tooltip) inside
     a card uses `position: fixed`, it will anchor to the card, not the
     viewport. Move the overlay outside the card or use `position: absolute`
     against a parent that's not a container. */
  container-type: inline-size;
}
.product[data-state="oop"][b-ejcw48rx06] { background: oklch(0.18 0.012 240); }

/* Reticle highlight on the product card landed-to via /products/{code}. Four
   corner brackets fly in from the cardinal directions, blink once, and settle
   as a persistent target frame. The bracket spans are only rendered on the
   focused card (see ProductCard.razor); the data-focus attribute on .product
   gates the animation. Tokenised so the same primitive can be retuned without
   touching the per-bracket positions or keyframes:

     --reticle-color:    bracket stroke + glow (defaults to the section's faction
                         accent so it tints with the surrounding cards)
     --reticle-size:     length of each bracket arm
     --reticle-duration: full sequence (fly-in → blink → settle)

   Reduced motion is handled site-wide by the prefers-reduced-motion rule in
   app.css — animation-duration collapses to 1ms and animation-fill-mode:forwards
   lands the brackets at their 100% (visible, in-position) state immediately. */
.product[b-ejcw48rx06] {
  --reticle-color: oklch(0.78 0.14 var(--accent-hue));
  --reticle-size: 22px;
  --reticle-duration: 1800ms;
}
/* Light the card's existing 1px border up in the reticle color so the four
   corner brackets read as a single targeted frame. Without this the brackets
   float disconnected at the corners while the default muted border draws the
   sides — exactly the look the handoff prototype avoids. */
.product[data-focus="true"][b-ejcw48rx06] { border-color: var(--reticle-color); }
.reticle[b-ejcw48rx06] {
  position: absolute;
  width: var(--reticle-size);
  height: var(--reticle-size);
  pointer-events: none;
  z-index: 4;
  opacity: 0;
}
.reticle[b-ejcw48rx06]::before,
.reticle[b-ejcw48rx06]::after {
  content: "";
  position: absolute;
  background: var(--reticle-color);
  box-shadow: 0 0 8px var(--reticle-color);
}
.reticle[b-ejcw48rx06]::before { width: 100%; height: 2px; }
.reticle[b-ejcw48rx06]::after  { width: 2px;  height: 100%; }

/* Position each bracket just outside its corner so the arms read as a target
   frame rather than a border. -5px keeps the brackets clear of the card edge;
   no ancestor in the products grid has overflow:hidden, so they don't clip. */
.reticle--tl[b-ejcw48rx06] { top: -5px;    left: -5px; }
.reticle--tr[b-ejcw48rx06] { top: -5px;    right: -5px; }
.reticle--bl[b-ejcw48rx06] { bottom: -5px; left: -5px; }
.reticle--br[b-ejcw48rx06] { bottom: -5px; right: -5px; }
.reticle--tl[b-ejcw48rx06]::before, .reticle--tl[b-ejcw48rx06]::after { top: 0;    left: 0; }
.reticle--tr[b-ejcw48rx06]::before, .reticle--tr[b-ejcw48rx06]::after { top: 0;    right: 0; }
.reticle--bl[b-ejcw48rx06]::before, .reticle--bl[b-ejcw48rx06]::after { bottom: 0; left: 0; }
.reticle--br[b-ejcw48rx06]::before, .reticle--br[b-ejcw48rx06]::after { bottom: 0; right: 0; }

/* Animation fires when .product carries data-focus="true" (set by Products.razor
   on the deep-linked card). animation-fill-mode: forwards retains the final
   keyframe state so the brackets stay visible after settling. */
.product[data-focus="true"] .reticle--tl[b-ejcw48rx06] { animation: reticle-tl-b-ejcw48rx06 var(--reticle-duration) ease-out forwards; }
.product[data-focus="true"] .reticle--tr[b-ejcw48rx06] { animation: reticle-tr-b-ejcw48rx06 var(--reticle-duration) ease-out forwards; }
.product[data-focus="true"] .reticle--bl[b-ejcw48rx06] { animation: reticle-bl-b-ejcw48rx06 var(--reticle-duration) ease-out forwards; }
.product[data-focus="true"] .reticle--br[b-ejcw48rx06] { animation: reticle-br-b-ejcw48rx06 var(--reticle-duration) ease-out forwards; }

@keyframes reticle-tl-b-ejcw48rx06 {
  0%   { transform: translate(-24px, -24px); opacity: 0; }
  18%  { transform: translate(-24px, -24px); opacity: 0; }
  40%  { transform: translate(0, 0);         opacity: 1; }
  55%  { opacity: 0.4; }
  70%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes reticle-tr-b-ejcw48rx06 {
  0%   { transform: translate(24px, -24px); opacity: 0; }
  18%  { transform: translate(24px, -24px); opacity: 0; }
  40%  { transform: translate(0, 0);        opacity: 1; }
  55%  { opacity: 0.4; }
  70%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes reticle-bl-b-ejcw48rx06 {
  0%   { transform: translate(-24px, 24px); opacity: 0; }
  18%  { transform: translate(-24px, 24px); opacity: 0; }
  40%  { transform: translate(0, 0);        opacity: 1; }
  55%  { opacity: 0.4; }
  70%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes reticle-br-b-ejcw48rx06 {
  0%   { transform: translate(24px, 24px); opacity: 0; }
  18%  { transform: translate(24px, 24px); opacity: 0; }
  40%  { transform: translate(0, 0);       opacity: 1; }
  55%  { opacity: 0.4; }
  70%  { opacity: 1; }
  100% { opacity: 1; }
}

/* ── Card header (thumb + title block + status pill) ─────────────────── */
.p-head[b-ejcw48rx06] {
  display: grid;
  grid-template-columns: 88px 1fr auto;
  gap: 16px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line-1);
  align-items: center;
}
.p-thumb[b-ejcw48rx06] {
  width: 88px;
  aspect-ratio: 1 / 1;
  background: var(--bg-2);
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line-1);
}
.p-thumb img[b-ejcw48rx06] { width: 100%; height: 100%; object-fit: cover; }
.p-thumb .product-status-tag[b-ejcw48rx06] {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: grid; place-items: center;
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.18em; font-weight: 600;
  color: var(--warn);
  background: oklch(from var(--bg-0) l c h / 0.82);
  border: 0; border-top: 1px solid var(--warn);
  padding: 2px 4px;
}

.p-title[b-ejcw48rx06] { min-width: 0; }
/* Element is <h3> for document outline; reset margin so the heading doesn't
   introduce default vertical space (the .p-head grid handles spacing). */
.p-name[b-ejcw48rx06] { margin: 0; font-weight: 600; font-size: 15px; letter-spacing: -0.005em; line-height: 1.25; color: var(--fg-0); }
.p-meta[b-ejcw48rx06] {
  margin-top: 5px;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-2);
}
.p-meta .sep[b-ejcw48rx06] { opacity: 0.4; margin: 0 6px; }
.p-meta .owned-tally[b-ejcw48rx06] { color: var(--accent); }
.p-meta .wanted-tally[b-ejcw48rx06] { color: var(--want); }

/* ── NEW chip + top-right status stack ───────────────────────────────────
   The header's right-hand column holds a vertical stack: the NEW chip above the
   status pill, so the corner reads "NEW / ● PRE-ORDER" (or "NEW / ● ACTIVE").
   align-self: start lifts the stack to the top of the otherwise center-aligned
   .p-head row so it hugs the top-right corner. The NEW chip keeps the quiet
   bordered, dim-fill treatment of the .sculpt-tag (not a loud solid chip) and
   takes the section's faction --accent so it tints per faction and reads as a
   positive arrival. It marks recently-released actives AND pre-orders. The old
   PRE-ORDER availability badge was removed — the status pill already states the
   pre-order state, so a second PRE-ORDER chip was redundant. */
.p-head-meta[b-ejcw48rx06] {
  align-self: start;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}
.badge[b-ejcw48rx06] {
  display: inline-grid;
  place-items: center;
  padding: 1px 6px;
  font-family: var(--ff-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 600;
  border: 1px solid transparent;
}
.badge-new[b-ejcw48rx06] {
  color: var(--accent);
  background: var(--accent-dim);
  border-color: var(--accent-line);
}

/* Empty-state thumbnail: a single <NoImagePlaceholder />. The radial
   gradient stays — it matches the visual language of the existing
   product-thumb area and the placeholder reads cleanly over it. The
   2x2 grid layout, scan line, and sculpt-count overlay are gone (the
   sculpt count still renders in .p-meta below). */
.p-thumb.empty[b-ejcw48rx06] {
  background:
    radial-gradient(ellipse at 50% 30%, oklch(0.30 0.020 240) 0%, oklch(0.18 0.012 240) 70%, transparent 100%),
    var(--bg-2);
}

/* ── Value-prop band (between header and sculpts grid) ──────────────── */
/* Two render modes — wishlist-value ("hot") and neutral ("muted") — at the
   same min-height so sculpts grids align across cards in the same row. */
.p-value[b-ejcw48rx06] {
  padding: 14px 18px;
  border-bottom: 1px solid var(--line-1);
  display: flex;
  align-items: center;
  gap: 14px;
  position: relative;
  min-height: 78px;
}
.p-value[b-ejcw48rx06]::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--want);
}
.p-value[data-tone="muted"][b-ejcw48rx06]::before { background: var(--line-2); }
.p-value[data-tone="muted"][b-ejcw48rx06] { background: oklch(0.18 0.012 240); }
.p-value[data-tone="hot"][b-ejcw48rx06] {
  background: linear-gradient(90deg,
    oklch(0.78 0.14 60 / 0.10) 0%,
    oklch(0.78 0.14 60 / 0.02) 70%,
    transparent 100%);
}
.vp-num[b-ejcw48rx06] {
  font-family: var(--ff-mono);
  font-weight: 600;
  font-size: 30px;
  color: var(--want);
  line-height: 1;
  letter-spacing: -0.02em;
}
.vp-stack[b-ejcw48rx06] { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.vp-key[b-ejcw48rx06] {
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-1);
  line-height: 1.2;
}
.vp-key.dim[b-ejcw48rx06] { color: var(--fg-3); font-size: 11px; letter-spacing: 0.16em; }

/* ── Sculpts grid ───────────────────────────────────────────────────── */
.p-sculpts[b-ejcw48rx06] {
  padding: 16px 18px;
  flex: 1;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  gap: 14px 10px;
  align-content: start;
}

/* Collapsed state for products with > 12 sculpts. 3 rows of min-width
   (78px) tiles = 3 * (14px head + 6px head→img gap + 78px*1.18 image
   aspect) + 2 * 14px row gap = 364px content; +32px vertical padding
   from .p-sculpts above (border-box) = 396px total. Wider cards have
   slightly taller tiles, so the 4th row may peek by a few pixels — the
   fade gradient softens that cut and signals "more below". */
.p-sculpts[data-collapsed="true"][b-ejcw48rx06] {
  max-height: 396px;
  overflow: hidden;
  position: relative;
}
.p-sculpts[data-collapsed="true"][b-ejcw48rx06]::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 40px;
  background: linear-gradient(to bottom, transparent, var(--bg-1));
  pointer-events: none;
}
/* OOP cards have a different .product background — match the fade to it
   so the gradient blends into the actual surface, not into a phantom
   colour that doesn't exist anywhere else on the card. */
.product[data-state="oop"] .p-sculpts[data-collapsed="true"][b-ejcw48rx06]::after {
  background: linear-gradient(to bottom, transparent, oklch(0.18 0.012 240));
}

.p-sculpts-toggle[b-ejcw48rx06] {
  display: block;
  width: 100%;
  padding: 8px 18px;
  margin: 0;
  border: 0;
  border-top: 1px solid var(--line-1);
  background: transparent;
  color: var(--fg-2);
  font-family: var(--ff-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  text-align: center;
  transition: color .12s, background .12s;
}
.p-sculpts-toggle:hover[b-ejcw48rx06] { color: var(--fg-1); background: oklch(from var(--bg-0) l c h / 0.4); }
/* OOP cards sit at L≈0.18 (close to --bg-0 at L=0.16), so the default
   hover overlay barely shifts the surface. Mirror the OOP override done
   for the fade gradient above and use --bg-2 (L≈0.24) instead — produces
   a visible lift rather than blending into the dim OOP background. */
.product[data-state="oop"] .p-sculpts-toggle:hover[b-ejcw48rx06] {
  background: oklch(from var(--bg-2) l c h / 0.4);
}
.p-sculpts-toggle:focus-visible[b-ejcw48rx06] { outline: 2px solid var(--accent); outline-offset: -2px; }
.sculpt[b-ejcw48rx06] {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  position: relative;       /* anchors the rail ::before below */
  transition: transform .12s;
  /* Per-sculpt --accent-hue is set inline (style="--accent-hue: <unit's faction>").
     Re-derive the accent family in this scope so the sculpt's own faction colors
     drive the tag chip / rail / count chip — see comment on .products-section above
     for why the :root-level accent vars don't pick up nested --accent-hue overrides. */
  --accent:      oklch(0.78 0.14 var(--accent-hue));
  --accent-dim:  oklch(0.78 0.14 var(--accent-hue) / 0.18);
  --accent-line: oklch(0.78 0.14 var(--accent-hue) / 0.45);
  --accent-soft: oklch(0.78 0.14 var(--accent-hue) / 0.55);
  --accent-fg:   oklch(0.85 0.16 var(--accent-hue));
}
.sculpt:hover[b-ejcw48rx06] { transform: scale(1.02); }
.sculpt-head[b-ejcw48rx06] { display: flex; align-items: center; gap: 5px; height: 14px; }
.sculpt-tag[b-ejcw48rx06] {
  display: inline-grid; place-items: center;
  height: 14px; padding: 0 4px;
  background: var(--accent-dim);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  flex-shrink: 0;
}
.sculpt-name[b-ejcw48rx06] {
  font-family: var(--ff-mono);
  font-size: 11px;
  color: var(--fg-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 1;
}
.sculpt-img[b-ejcw48rx06] {
  aspect-ratio: 1 / 1.18;
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(ellipse 70% 55% at 50% 38%, oklch(0.26 0.018 240) 0%, oklch(0.16 0.010 240) 75%, transparent 100%),
    oklch(0.14 0.010 240);
}
.sculpt-img img[b-ejcw48rx06] { width: 100%; height: 100%; object-fit: contain; }
/* Status rail in the gap between the head row and the image — solid accent for owned,
   dashed warn for wanted. Lives on the .sculpt parent (not on .sculpt-img) so it doesn't
   overlay the figure's head. The 14px head row + 2px buffer = top:16px, sitting in the
   6px gap; pointer-events:none so the rail doesn't intercept clicks on the head row. */
.sculpt[data-rail="own"][b-ejcw48rx06]::before,
.sculpt[data-rail="want"][b-ejcw48rx06]::before {
  content: "";
  position: absolute;
  left: 8%; right: 8%;
  top: 16px;
  height: 2px;
  z-index: 2;
  pointer-events: none;
}
.sculpt[data-rail="own"][b-ejcw48rx06]::before { background: var(--accent); }
.sculpt[data-rail="want"][b-ejcw48rx06]::before {
  background: repeating-linear-gradient(90deg, var(--want) 0 4px, transparent 4px 7px);
}
/* Owned-count chip in the head row: only renders when count > 1. flex:none keeps it at
   its natural width so the .sculpt-name (flex:1, ellipsis) just truncates a character
   earlier. Single-owned and wanted states need no chip — the rail in the gap above the
   image is enough to read each at a glance. */
.sculpt-count[b-ejcw48rx06] {
  flex: none;
  font-family: var(--ff-mono);
  font-size: 11px;
  color: var(--accent);
  letter-spacing: 0.04em;
  padding-left: 6px;
}

/* Non-play category badge (HVT, objective mini) in the contents row, sitting next to
   the faction-code chip. Same dimensions and mono treatment as .sculpt-tag so the head
   row stays aligned, but tinted with the --warn family instead of the per-faction
   --accent so a non-play model reads as "not a normal trooper" at a glance — these
   models have no army-list profile. flex-shrink:0 keeps the badge intact while the
   adjacent .sculpt-name (flex:1) truncates with ellipsis. */
.product-npc[b-ejcw48rx06] {
  display: inline-grid; place-items: center;
  height: 14px; padding: 0 4px;
  background: var(--warn-dim);
  border: 1px solid oklch(0.78 0.14 60 / 0.45);
  color: var(--warn);
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  flex-shrink: 0;
}

/* Global-legacy badge in the contents row — same box geometry as .product-npc, but the
   neutral dashed treatment of the catalog's .unit-legacy badge (grey + dashed border)
   instead of the --warn family, so "legacy" reads distinctly from the orange NPC badge. */
.product-legacy[b-ejcw48rx06] {
  display: inline-grid; place-items: center;
  height: 14px; padding: 0 4px;
  background: transparent;
  border: 1px dashed var(--line-2);
  color: var(--fg-3);
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  flex-shrink: 0;
}

/* ── Card footer (toggles + primary/secondary CTA) ───────────────────── */
.p-foot[b-ejcw48rx06] {
  margin-top: auto;
  padding: 14px 18px;
  border-top: 1px solid var(--line-1);
  display: flex;
  align-items: center;
  gap: 10px;
  background: oklch(from var(--bg-0) l c h / 0.4);
  flex-wrap: wrap;
}
.toggles[b-ejcw48rx06] { display: flex; gap: 8px; flex-wrap: wrap; }
.foot-spacer[b-ejcw48rx06] { flex: 1; }

.toggle[b-ejcw48rx06] {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 9px;
  border: 1px solid var(--line-1);
  background: transparent;
  color: var(--fg-2);
  font-family: var(--ff-mono);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}
.toggle:hover[b-ejcw48rx06] { color: var(--fg-1); border-color: var(--line-2); }
.toggle:focus-visible[b-ejcw48rx06] { outline: 2px solid var(--accent); outline-offset: 2px; }
.toggle[data-on="own"][b-ejcw48rx06] {
  background: var(--accent-dim);
  border-color: var(--accent-line);
  color: var(--accent);
}
.toggle[data-on="want"][b-ejcw48rx06] {
  background: var(--want-dim);
  border-color: var(--want-line);
  color: var(--want);
}

/* Chamfered primary CTA — the affiliate buy link. Filled accent (faction-tinted),
   dark text, 8px clipped corners on TR + BL for the catalog HUD chrome look. */
.cta-primary[b-ejcw48rx06] {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  background: var(--accent);
  color: oklch(0.10 0.012 240);
  font-family: var(--ff-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  text-decoration: none;
  position: relative;
  clip-path: polygon(
    8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px
  );
  transition: filter .15s ease, gap .15s ease;
}
.cta-primary:hover[b-ejcw48rx06] { filter: brightness(1.12); gap: 14px; }
.cta-primary:focus-visible[b-ejcw48rx06] { outline: 2px solid var(--accent); outline-offset: 2px; }
.cta-primary .arrow[b-ejcw48rx06] { font-weight: 400; }

/* Same shape as primary, softer fill — used for OOP secondhand links (no affiliate revenue). */
.cta-secondary[b-ejcw48rx06] {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  background: var(--accent-dim);
  color: var(--accent);
  font-family: var(--ff-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  text-decoration: none;
  position: relative;
  clip-path: polygon(
    8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px
  );
  transition: background .15s ease, gap .15s ease;
}
.cta-secondary:hover[b-ejcw48rx06] {
  /* Faction-tinted hover (per-section --accent-hue inheritance does the work). */
  background: oklch(0.78 0.14 var(--accent-hue) / 0.28);
  gap: 14px;
}
.cta-secondary:focus-visible[b-ejcw48rx06] { outline: 2px solid var(--accent); outline-offset: 2px; }
.cta-secondary .arrow[b-ejcw48rx06] { font-weight: 400; }

/* Compact CTA on narrow cards. Triggered by the card's own width via the
   container query on .product above, so any layout that produces a sub-440px
   card (the 2-col forced grid below 960px viewport, or the 3-col auto-fit at
   ~1000–1200px) gets the smaller CTA — and the wider 3-col / 2-col-comfy
   layouts keep the full chrome. */
@container (max-width: 440px) {
  .cta-primary[b-ejcw48rx06],
  .cta-secondary[b-ejcw48rx06] {
    font-size: 9px;
    letter-spacing: 0.14em;
    padding: 7px 9px;
    gap: 5px;
  }
  /* Tighten the footer's own padding on narrow cards too — buys back ~12px
     of usable inline space so toggles + CTA fit without wrapping. */
  .p-foot[b-ejcw48rx06] { padding: 14px 12px; }
}

/* Wishlist toggle is hidden on phone-sized viewports — it's a nice-to-have
   the user can still reach from the unit drawer; the primary mobile actions
   are "Add to collection" and the CTA. Matches the 1-col grid breakpoint
   in Products.razor.css so the rule fires exactly where touch ergonomics
   take over from desktop pointer use.
   The CTA's compact treatment above is also unwound here: on a touch device
   the primary affiliate CTA needs a comfortable tap target (~44px), so we
   restore the default padding/font even though the card width still trips
   the @container threshold. Wrapping the CTA to its own row is fine on
   phones — it's the stacked layout users expect. */
@media (max-width: 600px) {
  .toggle--wish[b-ejcw48rx06] { display: none; }
  .cta-primary[b-ejcw48rx06],
  .cta-secondary[b-ejcw48rx06] {
    font-size: 10.5px;
    letter-spacing: 0.22em;
    padding: 10px 14px;
    gap: 8px;
  }
  .p-foot[b-ejcw48rx06] { padding: 14px 18px; }
}

/* Roster-match ring — overlays the sculpt tile when its unit is on the
   active roster (data-roster="match" set by ProductCard when RosterUnitIds
   is provided by the parent). Drawn as ::after so it composes with the
   existing data-rail ::before (own / want) — a sculpt can be simultaneously
   owned + on the roster and show both signals. The ring uses --accent-hue,
   which the .sculpt scope already binds to the sculpt's per-unit faction
   hue, so the ring tints to the figure's faction. */
.sculpt[data-roster="match"][b-ejcw48rx06]::after {
  content: "";
  position: absolute;
  inset: -2px;
  border: 2px solid oklch(0.78 0.14 var(--accent-hue));
  pointer-events: none;
}

/* ── Compact-mode "Show more" (New & Pre-order rail) ──────────────────────
   Rendered instead of the value band / sculpts / footer when Compact=true,
   directly under the header rule. Quiet full-width action — same mono/upper
   treatment as .p-sculpts-toggle so it reads as part of the card chrome, not
   a loud CTA. The arrow slides on hover (mirrors the footer CTAs) and the
   label warms toward the section's faction accent. */
.p-showmore[b-ejcw48rx06] {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  margin: 0;
  padding: 13px 18px;
  border: 0;
  background: transparent;
  color: var(--fg-2);
  font-family: var(--ff-mono);
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color .12s, background .12s, gap .15s ease;
}
.p-showmore:hover[b-ejcw48rx06] {
  color: var(--accent);
  background: oklch(from var(--bg-0) l c h / 0.4);
  gap: 13px;
}
.p-showmore:focus-visible[b-ejcw48rx06] { outline: 2px solid var(--accent); outline-offset: -2px; }
.p-showmore .arrow[b-ejcw48rx06] { font-weight: 400; }
/* /Components/Layout/ProductCardModal.razor.rz.scp.css */
/* ProductCardModal — centered overlay showing one full ProductCard.
   The native <dialog> (opened via showModal() in modal-a11y.js) centers itself
   and provides the ::backdrop; we only style the panel + dim. */

.pcard-modal[b-as0te2ur3s] {
  border: none;
  padding: 0;
  background: transparent;
  max-width: 100vw;
  max-height: 100vh;
  color: inherit;
}

.pcard-modal[b-as0te2ur3s]::backdrop {
  background: rgba(0, 0, 0, 0.62);
}

.pcard-modal-inner[b-as0te2ur3s] {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(460px, 92vw);
  max-height: 88vh;
  overflow-y: auto;
  overflow-x: hidden;
}

.pcard-modal-close[b-as0te2ur3s] {
  align-self: flex-end;
  flex: 0 0 auto;
}

/* The card relies on a sizing parent (the products grid) for its width; in the
   modal there is no grid, so force it to fill the panel — otherwise the flex
   child collapses to ~0 width (the same CSS-isolation gotcha as the rail).
   ::deep pierces ProductCard's scope. */
.pcard-modal-inner[b-as0te2ur3s]  .product {
  width: 100%;
}
/* /Components/Layout/ProseShell.razor.rz.scp.css */
/* Flex column at least as tall as the viewport. SiteFooter carries
   `margin-top: auto`, so as the last flex child it is pushed to the
   bottom edge whenever the content above it is shorter than the
   viewport (e.g. the 404 page). On long-form pages the column simply
   grows past 100vh and the footer trails the content as normal.

   SiteHeader stays `position: sticky; top: 0` against the document
   scroll — this wrapper sets no overflow of its own, so it does not
   become the scroll container and sticky keeps tracking the viewport. */
.prose-shell[b-98f29bdb1d] {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}
/* /Components/Layout/ReconnectModal.razor.rz.scp.css */
.components-reconnect-first-attempt-visible[b-46nlem3wf0],
.components-reconnect-repeated-attempt-visible[b-46nlem3wf0],
.components-reconnect-failed-visible[b-46nlem3wf0],
.components-pause-visible[b-46nlem3wf0],
.components-resume-failed-visible[b-46nlem3wf0],
.components-rejoining-animation[b-46nlem3wf0] {
    display: none;
}

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-paused .components-pause-visible[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-retrying[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-failed[b-46nlem3wf0],
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible[b-46nlem3wf0] {
    display: block;
}


#components-reconnect-modal[b-46nlem3wf0] {
    background-color: white;
    width: 20rem;
    margin: 20vh auto;
    padding: 2rem;
    border: 0;
    border-radius: 0.5rem;
    box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
    opacity: 0;
    transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
    animation: components-reconnect-modal-fadeOutOpacity-b-46nlem3wf0 0.5s both;
    &[open]

{
    animation: components-reconnect-modal-slideUp-b-46nlem3wf0 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity-b-46nlem3wf0 0.5s ease-in-out 0.3s;
    animation-fill-mode: both;
}

}

#components-reconnect-modal[b-46nlem3wf0]::backdrop {
    background-color: rgba(0, 0, 0, 0.4);
    animation: components-reconnect-modal-fadeInOpacity-b-46nlem3wf0 0.5s ease-in-out;
    opacity: 1;
}

@keyframes components-reconnect-modal-slideUp-b-46nlem3wf0 {
    0% {
        transform: translateY(30px) scale(0.95);
    }

    100% {
        transform: translateY(0);
    }
}

@keyframes components-reconnect-modal-fadeInOpacity-b-46nlem3wf0 {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes components-reconnect-modal-fadeOutOpacity-b-46nlem3wf0 {
    0% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}

.components-reconnect-container[b-46nlem3wf0] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
}

#components-reconnect-modal p[b-46nlem3wf0] {
    margin: 0;
    text-align: center;
}

#components-reconnect-modal button[b-46nlem3wf0] {
    border: 0;
    background-color: #6b9ed2;
    color: white;
    padding: 4px 24px;
    border-radius: 4px;
}

    #components-reconnect-modal button:hover[b-46nlem3wf0] {
        background-color: #3b6ea2;
    }

    #components-reconnect-modal button:active[b-46nlem3wf0] {
        background-color: #6b9ed2;
    }

.components-rejoining-animation[b-46nlem3wf0] {
    position: relative;
    width: 80px;
    height: 80px;
}

    .components-rejoining-animation div[b-46nlem3wf0] {
        position: absolute;
        border: 3px solid #0087ff;
        opacity: 1;
        border-radius: 50%;
        animation: components-rejoining-animation-b-46nlem3wf0 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
    }

        .components-rejoining-animation div:nth-child(2)[b-46nlem3wf0] {
            animation-delay: -0.5s;
        }

@keyframes components-rejoining-animation-b-46nlem3wf0 {
    0% {
        top: 40px;
        left: 40px;
        width: 0;
        height: 0;
        opacity: 0;
    }

    4.9% {
        top: 40px;
        left: 40px;
        width: 0;
        height: 0;
        opacity: 0;
    }

    5% {
        top: 40px;
        left: 40px;
        width: 0;
        height: 0;
        opacity: 1;
    }

    100% {
        top: 0px;
        left: 0px;
        width: 80px;
        height: 80px;
        opacity: 0;
    }
}
/* /Components/Layout/RefChip.razor.rz.scp.css */
/* ── Skill / weapon chips + popover ──────────────────────────── */
.ref-chip[b-hbmv44n918] {
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.1em;
  border: 1px solid var(--line-1);
  border-radius: 3px;
  padding: 4px 8px;
  background: var(--bg-1);
  color: var(--fg-1);
  cursor: pointer;
  position: relative;
  text-transform: uppercase;
  text-decoration: none;
}
.ref-chip:hover[b-hbmv44n918] { border-color: var(--accent-line); color: var(--fg-0); }
.ref-chip[data-kind="weapon"][b-hbmv44n918] {
  border-color: oklch(45% 0.06 var(--accent-hue));
  color: oklch(78% 0.10 var(--accent-hue));
}
.ref-chip.is-active[b-hbmv44n918] {
  background: var(--accent-dim);
  border-color: oklch(70% 0.18 var(--accent-hue));
  color: var(--fg-0);
}
.ref-pop[b-hbmv44n918] {
  position: absolute;
  top: calc(100% + 6px); left: 0;
  z-index: 60;
  background: var(--bg-0);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  padding: 12px 14px;
  width: 280px;
  box-shadow: 0 8px 28px oklch(0% 0 0 / 0.5);
  font-family: var(--ff-sans);
  text-transform: none;
  letter-spacing: 0;
}
.ref-pop-head[b-hbmv44n918] { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.ref-pop-name[b-hbmv44n918] { font-size: 13px; font-weight: 600; color: var(--fg-0); }
.ref-pop-cat[b-hbmv44n918] {
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.16em;
  color: var(--fg-3);
  border: 1px solid var(--line-1);
  border-radius: 2px;
  padding: 1px 5px;
  text-transform: uppercase;
}
.ref-pop-range[b-hbmv44n918] {
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.08em;
  color: oklch(78% 0.10 var(--accent-hue));
  margin-bottom: 6px;
}
.ref-pop-desc[b-hbmv44n918] { font-size: 12px; line-height: 1.5; color: var(--fg-1); text-wrap: pretty; }
/* /Components/Layout/RosterCoverageList.razor.rz.scp.css */
.roster-coverage[b-zyp9k2b6fx] {
    padding: 16px 32px;
    border-top: 1px solid var(--line-1);
}

.rc-head[b-zyp9k2b6fx] {
    margin-bottom: 8px;
}

.rc-toggle[b-zyp9k2b6fx] {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    color: inherit;
    font: inherit;
}

.rc-toggle:hover .rc-label[b-zyp9k2b6fx] { color: var(--fg-0); }

.rc-chev[b-zyp9k2b6fx] {
    width: 12px;
    color: var(--fg-3);
    font-size: 10px;
}

.rc-label[b-zyp9k2b6fx] {
    font: 600 11px/1 var(--ff-mono);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg-1);
    transition: color 0.12s;
}

.rc-meta[b-zyp9k2b6fx] {
    font: 11px/1 var(--ff-mono);
    color: var(--fg-3);
}

/* Tabular multi-column layout.
   - Outer: CSS columns (column-major flow) preserves the user's "scan down
     the list" reading order — first column reads roster entries 1..N/3,
     second column N/3+1..2N/3, etc. 420px column-width + 1500px max-width
     caps at 3 columns even on very wide viewports.
   - Inner: each row is a 3-column CSS grid (count | name | status) with a
     FIXED name-column width so statuses start at the same x within each
     column — proper vertical alignment rather than ragged right-justify. */
.rc-list[b-zyp9k2b6fx] {
    list-style: none;
    margin: 0;
    padding: 0;
    columns: 420px;
    column-gap: 32px;
    max-width: 1500px;
}

.rc-row[b-zyp9k2b6fx] {
    display: grid;
    grid-template-columns: 28px 240px auto;
    column-gap: 12px;
    align-items: baseline;
    padding: 4px 0;
    font-size: 13px;
    color: var(--fg-1);
    /* Keep each row inside one column. */
    break-inside: avoid;
}

.rc-row .rc-count[b-zyp9k2b6fx] {
    color: var(--fg-3);
}

/* Unit-name button — looks like text, opens the unit detail drawer on click.
   Truncates names that exceed the fixed 240px column so the status x stays
   consistent; the full name + subname is available on hover via the title
   attribute on the button (see .razor markup). */
.rc-name[b-zyp9k2b6fx] {
    /* Button reset — sit in the grid track like the original span did. */
    background: none;
    border: 0;
    padding: 0;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;

    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.rc-name:hover[b-zyp9k2b6fx] {
    color: var(--fg-0);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
}

.rc-name:focus-visible[b-zyp9k2b6fx] {
    outline: 1px solid oklch(0.78 0.14 var(--accent-hue, 220));
    outline-offset: 2px;
}

.rc-sub[b-zyp9k2b6fx] {
    color: var(--fg-3);
}

.rc-glyph[b-zyp9k2b6fx] {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    font-size: 12px;
}

.rc-mark[b-zyp9k2b6fx] {
    font-size: 14px;
    line-height: 1;
}

.rc-text[b-zyp9k2b6fx] {
    color: var(--fg-2);
}

/* Per-status glyph tinting. Codes match the StatusInfo records in
   RosterCoverageList.razor (in-plan / orphan / owned / ranked). */
.rc-row[data-status="in-plan"] .rc-mark[b-zyp9k2b6fx] { color: oklch(0.78 0.14 var(--accent-hue, 220)); }
.rc-row[data-status="orphan"]  .rc-mark[b-zyp9k2b6fx] { color: var(--warn); }
.rc-row[data-status="owned"]   .rc-mark[b-zyp9k2b6fx] { color: var(--fg-3); }
.rc-row[data-status="ranked"]  .rc-mark[b-zyp9k2b6fx] { color: var(--fg-2); }
/* /Components/Layout/RosterEditor.razor.rz.scp.css */
/* ── Roster editor overlay (RosterEditor.razor) ─────────────────
   Full-screen roster editor: per-instance sculpt picker, rename,
   totals, COPY/CLEAR.

   The `.roster-editor` dialog shell, `.re-shell`, `.re-head`,
   `.re-name`, `.re-body`, and most of the `.re-entry` / `.re-action`
   primitives are SHARED with SharedRosterPreview (sibling read-only
   modal) and stay in app.css. Only RosterEditor-unique styles live
   here: the toast, the right-side stats panel, the inline name
   input, the per-line picker, and the count-control cluster. */

/* Toast overlay shown after share-link copy. Anchored to .re-shell's
   relative positioning context. */
.re-toast[b-qolj884wrh] {
  position: absolute;
  top: 12px; left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  background: var(--accent);
  color: var(--bg-0);
  padding: 6px 14px;
  font-size: 11px;
  letter-spacing: 0.14em;
  border-radius: var(--r-sm);
  box-shadow: 0 4px 12px oklch(0 0 0 / 0.4);
  animation: re-toast-fade-b-qolj884wrh 2.4s ease;
}
@keyframes re-toast-fade-b-qolj884wrh {
  0%   { opacity: 0; transform: translate(-50%, -8px); }
  10%  { opacity: 1; transform: translate(-50%, 0); }
  85%  { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -8px); }
}

.re-name-input[b-qolj884wrh] {
  font-size: 22px;
  background: var(--bg-0);
  border: 1px solid var(--accent-line);
  color: var(--fg-0);
  padding: 4px 8px;
  font-family: inherit;
  flex: 1;
}

.re-picker[b-qolj884wrh] {
  display: flex; gap: 6px; align-items: center;
  margin-left: 12px;
}
.re-picker-select[b-qolj884wrh] {
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  color: var(--fg-0);
  padding: 5px 8px;
  font-family: inherit;
  font-size: 12px;
  max-width: 220px;
}

.re-count-ctl[b-qolj884wrh] { display: flex; align-items: center; gap: 6px; }
.re-remove-line[b-qolj884wrh] { color: var(--fg-3); }
.re-remove-line:hover[b-qolj884wrh] { color: oklch(70% 0.18 25); border-color: oklch(60% 0.18 25); }

/* Sub-header that splits the entry list into a "NOT PLAYABLE" group below the
   playable models. Quiet by design — a dashed rule + dim mono label. */
.re-group-head[b-qolj884wrh] {
  font-size: 10px; letter-spacing: 0.18em; color: var(--fg-3);
  border-top: 1px dashed var(--line-1); margin-top: 10px; padding-top: 10px;
}

.re-side[b-qolj884wrh] {
  width: 240px;
  border-left: 1px solid var(--line-1);
  padding: 16px 20px;
  display: flex; flex-direction: column;
  background: var(--bg-0);
}
.re-stat-block[b-qolj884wrh] { margin-bottom: 16px; }
.re-stat-label[b-qolj884wrh] { font-size: 11px; letter-spacing: 0.14em; color: var(--fg-2); }
.re-stat-value[b-qolj884wrh] { font-size: 28px; color: var(--fg-0); margin-top: 2px; }
.re-stat-value-sm[b-qolj884wrh] { font-size: 11px; color: var(--fg-1); margin-top: 4px; }
.re-spacer[b-qolj884wrh] { flex: 1; }
/* /Components/Layout/RosterPicker.razor.rz.scp.css */
.roster-picker[b-tr2vq9cqks] {
  flex: 1; min-width: 0;
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  color: var(--fg-0);
  padding: 5px 6px;
  font-family: inherit;
  font-size: 13px;
}
/* /Components/Layout/RosterRail.razor.rz.scp.css */
/* ── Roster rail (RosterRail.razor) ─────────────────────────────
   Slide-out rail on the right edge listing the active roster. Drop
   zone for unit cards (drag-and-drop adds units).
   The .rr-mini button primitive is shared with RosterEditor — it
   stays in app.css as a shared primitive. */
.roster-rail[b-mvo91bh3ps] {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 320px;
  background: var(--bg-1);
  border-left: 1px solid var(--line-1);
  z-index: 50;
  display: flex; flex-direction: column;
  font-family: var(--ff-sans);
  box-shadow: -8px 0 30px rgba(0,0,0,0.4);
}
.rr-head[b-mvo91bh3ps] {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-1);
}
.rr-title[b-mvo91bh3ps] { font-size: 11px; letter-spacing: 0.18em; color: var(--fg-1); }
.rr-close[b-mvo91bh3ps] {
  background: transparent;
  border: 1px solid var(--line-1);
  color: var(--fg-1);
  width: 22px; height: 22px;
  cursor: pointer;
  font-size: 14px; line-height: 1;
}
.rr-close:hover[b-mvo91bh3ps] { color: var(--fg-0); border-color: var(--line-2); }
.rr-name[b-mvo91bh3ps] {
  padding: 14px 16px 8px;
  font-size: 14px; color: var(--fg-0);
  border-bottom: 1px dashed var(--line-1);
  cursor: pointer;
}
.rr-name:hover[b-mvo91bh3ps] { background: var(--bg-0); }
.rr-name-edit-hint[b-mvo91bh3ps] {
  font-size: 11px; color: var(--fg-3);
  margin-left: 6px;
  opacity: 0.4;
}
.rr-name:hover .rr-name-edit-hint[b-mvo91bh3ps] { opacity: 1; }
.rr-name-input[b-mvo91bh3ps] {
  display: block;
  width: calc(100% - 32px);
  margin: 12px 16px 8px;
  padding: 4px 8px;
  background: var(--bg-0);
  border: 1px solid var(--accent-line);
  color: var(--fg-0);
  font-family: inherit;
  font-size: 14px;
}
.rr-picker[b-mvo91bh3ps] {
  display: flex; gap: 6px; align-items: center;
  padding: 10px 16px;
  border-bottom: 1px dashed var(--line-1);
}
.rr-empty[b-mvo91bh3ps] {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
  color: var(--fg-2);
  text-align: center;
  padding: 30px;
}
.rr-empty-mark[b-mvo91bh3ps] {
  font-size: 36px;
  color: var(--accent-line);
  border: 2px dashed var(--accent-line);
  width: 60px; height: 60px;
  display: grid; place-items: center;
  border-radius: 50%;
}
.rr-empty-msg[b-mvo91bh3ps] { font-size: 13px; line-height: 1.5; }
.rr-list[b-mvo91bh3ps] { flex: 1; list-style: none; margin: 0; padding: 8px 0; overflow-y: auto; }
.rr-item[b-mvo91bh3ps] {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 16px;
  border-bottom: 1px solid var(--line-1);
  gap: 8px;
}
.rr-item:hover[b-mvo91bh3ps] { background: var(--bg-0); }
/* Sub-header <li> that splits the list into a "NOT PLAYABLE" group below the playable
   models. Quiet by design — a dashed rule + dim mono label. Mirrors RosterEditor's
   .re-group-head, adapted to the rail's <li> context (list-style:none, list padding). */
.rr-group-head[b-mvo91bh3ps] {
  list-style: none;
  font-size: 10px; letter-spacing: 0.18em; color: var(--fg-3);
  border-top: 1px dashed var(--line-1);
  margin-top: 8px; padding: 12px 16px 6px;
}
.rr-item-main[b-mvo91bh3ps] { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.rr-count[b-mvo91bh3ps] {
  font-size: 11px;
  color: var(--accent-fg);
  background: var(--accent-dim);
  padding: 3px 6px;
  border: 1px solid var(--accent-line);
  flex-shrink: 0;
}
.rr-item-name[b-mvo91bh3ps] { min-width: 0; }
.rr-uname[b-mvo91bh3ps] { font-size: 13px; color: var(--fg-0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rr-sub[b-mvo91bh3ps] { color: var(--fg-2); font-size: 12px; }
.rr-meta[b-mvo91bh3ps] { font-size: 11px; letter-spacing: 0.12em; color: var(--fg-2); margin-top: 2px; }
.rr-uname--missing[b-mvo91bh3ps] { color: var(--fg-3); }
.rr-meta--missing[b-mvo91bh3ps]  { color: var(--fg-3); }
.rr-item-ctl[b-mvo91bh3ps] { display: flex; gap: 4px; }
.rr-foot[b-mvo91bh3ps] {
  border-top: 1px solid var(--line-1);
  padding: 12px 16px;
  background: var(--bg-0);
}
.rr-totals[b-mvo91bh3ps] {
  display: flex; justify-content: space-between;
  font-size: 11px; letter-spacing: 0.14em; color: var(--fg-1);
  margin-bottom: 10px;
}
.rr-totals strong[b-mvo91bh3ps] { color: var(--fg-0); font-size: 14px; margin-left: 4px; }
.rr-actions[b-mvo91bh3ps] { display: flex; gap: 6px; }
.rr-btn[b-mvo91bh3ps] {
  flex: 1;
  background: transparent;
  border: 1px solid var(--line-2);
  color: var(--fg-1);
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.16em;
  padding: 8px 0;
  cursor: pointer;
}
.rr-btn:hover:not(:disabled)[b-mvo91bh3ps] { color: var(--fg-0); border-color: var(--accent-line); }
.rr-btn:disabled[b-mvo91bh3ps] { opacity: 0.4; cursor: not-allowed; }

.roster-rail[data-drag-over="True"][b-mvo91bh3ps] {
  background: var(--accent-dim);
  outline: 2px dashed var(--accent-fg);
  outline-offset: -2px;
}
.roster-rail[data-drag-over="True"] .rr-empty-mark[b-mvo91bh3ps] {
  border-color: var(--accent-fg);
  color: var(--accent-fg);
  transform: scale(1.1);
}

/* The mobile touch-target enlargement for .rr-close and .rr-mini lives
   in app.css alongside the .rr-mini base — both selectors are shared
   primitives rendered by RosterRail AND RosterEditor, so the @media
   rule has to be unscoped for both renderers to pick it up. */

/* ── Roster rail as bottom sheet (phone) ──
   At <=600px the 320px right-side rail becomes a bottom sheet covering
   the lower portion of the viewport. The existing RosterRail.razor uses
   `@if (Open)` to gate the whole <aside> — it pops in/out via Razor
   render rather than a CSS transform. So we don't animate here either;
   the rail just appears at the bottom of the viewport. Users dismiss
   via the existing ROSTER toggle in the topbar or the `x` close button
   in the rail header (enlarged to 32x32 by the shared `.rr-close,
   .rr-mini` @media rule in app.css).

   `padding-bottom: max(12px, env(safe-area-inset-bottom))` keeps the
   bottom of the sheet clear of the iOS home indicator. */
@media (max-width: 600px) {
  .roster-rail[b-mvo91bh3ps] {
    top: auto; right: 0; left: 0;
    width: 100%;
    /* Half-height bottom sheet leaves the catalog above visible so the user
       can see what they're adding to the roster. 70vh was too dominant on a
       915px-tall phone — felt like a full-screen takeover. */
    height: min(55vh, 460px);
    border-left: 0;
    border-top: 1px solid var(--line-1);
    box-shadow: 0 -8px 30px rgba(0,0,0,0.4);   /* override the desktop -8px 0 ... shadow */
    padding-bottom: max(12px, env(safe-area-inset-bottom));
  }

  /* Decorative drag handle. We don't implement drag-to-dismiss (JS only);
     the handle reads as a "this is a sheet" affordance. */
  .roster-rail[b-mvo91bh3ps]::before {
    content: "";
    position: absolute;
    top: 6px; left: 50%;
    transform: translateX(-50%);
    width: 36px; height: 4px;
    border-radius: 2px;
    background: var(--line-2);
    pointer-events: none;
  }
  .rr-head[b-mvo91bh3ps] { padding-top: 18px; }   /* clear the drag handle */
}
/* /Components/Layout/SculptCheckRow.razor.rz.scp.css */
/* ── SculptCheckRow ───────────────────────────────────────────────────
   One row in the admin Product Editor's sculpt-selection list. Inherits
   .admin-check from AdminLayout for the base row layout/hover; adds a
   fixed-width thumb slot and a floating card-size preview on hover.
   Vocabulary mirrors SculptPicker (used in the roster editor). */

.sculpt-row[b-k5qm6ycyfj] { position: relative; }

.sculpt-row-thumb[b-k5qm6ycyfj] {
  width: 32px; height: 44px; flex-shrink: 0;
  border-radius: 1px;
  background: radial-gradient(ellipse at 50% 28%, oklch(1 0 0 / 0.04), transparent 65%);
  display: grid; place-items: center;
  overflow: hidden;
}
.sculpt-row-thumb.is-empty[b-k5qm6ycyfj] { color: var(--fg-3); font-size: 14px; }
.sculpt-row-thumb img[b-k5qm6ycyfj] { width: 100%; height: 100%; object-fit: contain; }

.sculpt-row-trail[b-k5qm6ycyfj] { margin-left: auto; font-size: 11px; }

/* Floating preview to the right of the row — pointer-events:none so it can't
   steal hover. Sits above the admin card. */
.sculpt-row-preview[b-k5qm6ycyfj] {
  position: absolute; top: 0; left: calc(100% + 8px);
  background: var(--bg-1);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  box-shadow: 0 8px 24px oklch(0 0 0 / 0.5);
  padding: 8px;
  z-index: 101;
  pointer-events: none;
  width: 132px;
}
.sculpt-row-preview img[b-k5qm6ycyfj] {
  display: block;
  width: 116px; height: 156px;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: radial-gradient(ellipse at 50% 28%, oklch(1 0 0 / 0.04), transparent 65%);
}
.sculpt-row-preview-name[b-k5qm6ycyfj] {
  font-size: 11px; letter-spacing: 0.08em; color: var(--fg-1);
  text-align: center;
  margin-top: 6px;
  word-break: break-word;
}
/* /Components/Layout/SculptPicker.razor.rz.scp.css */
/* ── SculptPicker (used in roster editor) ──────────────────────────────
   Custom Blazor dropdown: each option carries a small thumb, hovering shows a bigger
   preview to the right of the menu. Replaces a native <select> which can't render images. */
.sculpt-picker[b-sz1nusqcyj] { position: relative; display: inline-block; min-width: 240px; }
.sculpt-picker-trigger[b-sz1nusqcyj] {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 6px 10px;
  background: var(--bg-2);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  color: var(--fg-0);
  font-family: inherit; font-size: 12px;
  cursor: pointer;
  text-align: left;
}
.sculpt-picker-trigger:hover[b-sz1nusqcyj] { border-color: var(--line-2); }
.sculpt-picker-trigger[aria-expanded="true"][b-sz1nusqcyj] { border-color: var(--accent-line); }
.sculpt-picker-trigger-name[b-sz1nusqcyj] { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sculpt-picker-caret[b-sz1nusqcyj] { font-size: 10px; color: var(--fg-3); }

/* Menu sits below the trigger. Scrolls if a unit has many sculpts. */
.sculpt-picker-menu[b-sz1nusqcyj] {
  position: absolute; top: calc(100% + 2px); left: 0;
  min-width: 100%;
  background: var(--bg-1);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  box-shadow: 0 8px 24px oklch(0 0 0 / 0.5);
  padding: 4px;
  z-index: 100;
  max-height: 60vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
}
.sculpt-picker-option[b-sz1nusqcyj] {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 4px 6px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 1px;
  color: var(--fg-0);
  font-family: inherit; font-size: 12px;
  text-align: left;
  cursor: pointer;
  transition: background .12s, border-color .12s;
}
.sculpt-picker-option:hover[b-sz1nusqcyj] { background: var(--bg-2); border-color: var(--line-1); }
.sculpt-picker-option.is-current[b-sz1nusqcyj] { background: var(--accent-dim); border-color: var(--accent-line); }
.sculpt-picker-option-thumb[b-sz1nusqcyj] {
  width: 32px; height: 44px; flex-shrink: 0;
  border-radius: 1px;
  background: radial-gradient(ellipse at 50% 28%, oklch(1 0 0 / 0.04), transparent 65%);
  display: grid; place-items: center;
  overflow: hidden;
}
.sculpt-picker-option-thumb--empty[b-sz1nusqcyj] { color: var(--fg-3); font-size: 14px; }
.sculpt-picker-option-thumb img[b-sz1nusqcyj] { width: 100%; height: 100%; object-fit: contain; }
.sculpt-picker-option-name[b-sz1nusqcyj] { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sculpt-picker-option-year[b-sz1nusqcyj] { font-size: 11px; letter-spacing: 0.14em; color: var(--fg-3); flex-shrink: 0; }

/* Floating preview to the right of the menu — pointer-events:none so it can't steal hover. */
.sculpt-picker-preview[b-sz1nusqcyj] {
  position: absolute; top: calc(100% + 2px); left: calc(100% + 8px);
  background: var(--bg-1);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  box-shadow: 0 8px 24px oklch(0 0 0 / 0.5);
  padding: 8px;
  z-index: 101;
  pointer-events: none;
  width: 132px;
}
.sculpt-picker-preview img[b-sz1nusqcyj] {
  display: block;
  width: 116px; height: 156px;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: radial-gradient(ellipse at 50% 28%, oklch(1 0 0 / 0.04), transparent 65%);
}
.sculpt-picker-preview-name[b-sz1nusqcyj] {
  font-size: 11px; letter-spacing: 0.08em; color: var(--fg-1);
  text-align: center;
  margin-top: 6px;
  word-break: break-word;
}
/* /Components/Layout/SharedRosterPreview.razor.rz.scp.css */
/* SharedRosterPreview (read-only modal for ?r=… shared roster URLs) shares
   its entire visual language with RosterEditor — `.roster-editor` dialog
   shell, `.re-shell`, `.re-head`, `.re-name`, `.re-body`, `.re-list-wrap`,
   `.re-entries`, `.re-entry*`, `.re-action`, `.re-uname--missing`, etc.

   Those rules live in app.css as shared primitives because both
   RosterEditor and SharedRosterPreview render them and CSS isolation
   would otherwise force per-file duplication. This file exists as a
   placeholder so the component participates in the scoped-CSS pipeline
   and any future preview-only styling has a home. */
/* /Components/Layout/SiteFooter.razor.rz.scp.css */
/* Single-line footer for scrollable pages. Modest height, top-border
   separator from the page body. flex-wrap lets the right cluster fall
   below the left one on narrow viewports without JS.

   margin-top: auto pushes the footer to the bottom of its parent when
   the parent is a flex container with extra space. On /products and
   /purchaseplanner the footer is a child of <main class="ident-main">, which is
   itself a flex column with a fixed viewport-minus-header height (see
   app.css). When the page content is shorter than that height
   (e.g. filtered-down product list, empty buy roster) the footer
   would otherwise sit immediately under the content with empty dark
   space below it; margin-top:auto absorbs that empty space above the
   footer instead, so the bottom edge stays flush against the
   container. On the body-level pages (About, Cookie policy, NotFound)
   the parent is not a flex container and the rule is a no-op — those
   pages naturally extend to fit their long-form prose. */
.site-footer[b-xukvhhka5m] {
    display: flex;
    align-items: center;
    gap: 18px;
    padding: 10px 14px;
    background: var(--bg-1, #0c0e12);
    border-top: 1px solid var(--line-2, #2a2f38);
    color: var(--fg-2, #6b7380);
    font-size: 10px;
    letter-spacing: 0.04em;
    flex-wrap: wrap;
    margin-top: auto;
}

.site-footer__brand-cluster[b-xukvhhka5m] {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.site-footer__brand[b-xukvhhka5m] {
    color: var(--fg-0, #e6ecf3);
    letter-spacing: 0.12em;
    font-weight: 700;
    font-size: 11px;
}

.site-footer__sep[b-xukvhhka5m] {
    color: var(--fg-3, #5a6270);
}

.site-footer__muted[b-xukvhhka5m] {
    color: var(--fg-2, #8b95a3);
}

.site-footer__disclaimer[b-xukvhhka5m] {
    color: var(--fg-3, #5a6270);
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-size: 9px;
}

.site-footer__links[b-xukvhhka5m] {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-left: auto;
    flex-wrap: wrap;
}

.site-footer__links a[b-xukvhhka5m] {
    color: var(--fg-1, #c6cdd6);
    text-decoration: none;
}

.site-footer__links a:hover[b-xukvhhka5m] {
    color: var(--fg-0, #e6ecf3);
    text-decoration: underline;
}
/* /Components/Layout/SiteHeader.razor.rz.scp.css */
/* Universal top strip. Sticky at the top of every public page. Reads
   --site-header-h (defined on :root in wwwroot/app.css) so its height
   stays in sync with the per-page strip (PageHeader), which uses the
   same variable to offset its own sticky position. */
.site-header[b-rwc6so3qcu] {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: 22px;
    padding: 8px 14px;
    height: var(--site-header-h);
    background: linear-gradient(180deg, var(--bg-2, #14171d) 0%, var(--bg-1, #0f1115) 100%);
    border-bottom: 1px solid var(--line-2, #2a2f38);
}

/* Brand cluster — logo mark + wordmark. Plain <a> for SEO and
   middle-click; enhanced navigation handles the SPA transition. */
.site-header__brand[b-rwc6so3qcu] {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--accent);
    text-decoration: none;
    letter-spacing: 0.12em;
    font-weight: 700;
    font-size: 13px;
}
.site-header__mark[b-rwc6so3qcu] {
    display: block;
    color: var(--accent);
}
.site-header__wordmark[b-rwc6so3qcu] {
    color: var(--fg-0, #e6ecf3);
}

/* Primary nav — four links, active item gets cyan + underline.
   The is-active class and aria-current="page" are both set together
   from Razor; CSS uses .is-active for visual state. */
.site-header__nav[b-rwc6so3qcu] {
    display: flex;
    gap: 18px;
}
.site-header__nav-link[b-rwc6so3qcu] {
    color: var(--fg-2, #8b95a3);
    text-decoration: none;
    letter-spacing: 0.08em;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    padding: 4px 2px;
    border-bottom: 1px solid transparent;
    transition: color 120ms ease, border-color 120ms ease;
}
.site-header__nav-link:hover[b-rwc6so3qcu] {
    color: var(--fg-0, #e6ecf3);
}
.site-header__nav-link.is-active[b-rwc6so3qcu] {
    color: var(--accent);
    border-bottom-color: var(--accent);
}
/* /Components/Layout/SkeletonProductCard.razor.rz.scp.css */
/* Per-card dimensions for SkeletonProductCard. The shared .skeleton
   base rule and @keyframes skeletonPulse stay in app.css. */
.skeleton-product-card[b-wha9mbh0cv] {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  width: 100%;
  min-height: 320px;
}
.skeleton-product-card .sk-head-row[b-wha9mbh0cv] {
  display: grid;
  grid-template-columns: 88px 1fr auto;
  gap: 16px;
  align-items: center;
}
.skeleton-product-card .sk-thumb[b-wha9mbh0cv] { width: 88px; aspect-ratio: 1 / 1; }
.skeleton-product-card .sk-title[b-wha9mbh0cv] { height: 14px; width: 60%; margin-bottom: 6px; }
.skeleton-product-card .sk-meta[b-wha9mbh0cv]  { height: 10px; width: 40%; }
.skeleton-product-card .sk-pill[b-wha9mbh0cv]  { width: 60px; height: 20px; }
.skeleton-product-card .sk-sculpts[b-wha9mbh0cv] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  gap: 14px 10px;
  margin-top: 8px;
}
.skeleton-product-card .sk-sculpt[b-wha9mbh0cv] { height: 92px; }
/* /Components/Layout/SkeletonUnitCard.razor.rz.scp.css */
/* ───── Skeleton card variants ─────
   Per-card dimensions for SkeletonUnitCard. The shared .skeleton base
   rule and @keyframes skeletonPulse stay in app.css (used by every
   variant). The --uc-w / --uc-img-h custom properties are set by the
   parent .sheet (IdentSheet) at the appropriate density and cascade
   through CSS isolation scope. */
.skeleton-unit-card[b-va8gqq0iil] {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 12px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  width: var(--uc-w, 141px);
  /* Match approximate dimensions of UnitCard (silhouette grid) */
}
.skeleton-unit-card .sk-image[b-va8gqq0iil]  { height: var(--uc-img-h, 208px); aspect-ratio: auto; }
.skeleton-unit-card .sk-name[b-va8gqq0iil]   { height: 12px; width: 70%; }
.skeleton-unit-card .sk-sub[b-va8gqq0iil]    { height: 10px; width: 50%; }
.skeleton-unit-card .sk-tags[b-va8gqq0iil]   { height: 14px; width: 80%; }
/* /Components/Layout/StorageRecoveryNotice.razor.rz.scp.css */
/* Fixed-position wrapper for the global load-failure recovery alert. The alert body itself
   is the shared .inline-error primitive; only its placement is component-local. z-index 900:
   below #blazor-error-ui (1000), above all app chrome (topbar 10). */
.storage-recovery[b-uumy14e3ou] {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 900;
    display: flex;
    justify-content: center;
    padding: 8px;
    pointer-events: none;   /* don't block clicks in the full-width gutter… */
}

.storage-recovery .inline-error[b-uumy14e3ou] {
    pointer-events: auto;   /* …only the alert itself is interactive */
    max-width: 640px;
    width: 100%;
}

.storage-recovery-dismiss[b-uumy14e3ou] {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--fg-3);
    font-family: var(--ff-mono);
    padding: 0 4px;
}
.storage-recovery-dismiss:hover[b-uumy14e3ou] { color: var(--fg-1); }
/* /Components/Layout/UnitCard.razor.rz.scp.css */
/* ── Unit card ── */
/* The card root is an <a href="/units/{code}"> — plain left-click opens the
   in-page drawer (see UnitCard.razor HandleClick + @onclick:preventDefault),
   middle-click / right-click "Open in new tab" follow the real link, and
   crawlers see a normal anchor. text-decoration/color reset keeps the card's
   typography unchanged from the prior <div> rendering. */
.unit[b-t2a1gr1bmr] {
  display:flex; flex-direction:column; align-items:center; gap:5px;
  padding:0px 0px 0px;
  width: var(--uc-w, 140px);
  min-height: var(--uc-min-h, 288px);
  border:1px solid transparent; border-radius:var(--r-sm);
  cursor:pointer; position:relative;
  transition:background .12s,border-color .12s,box-shadow .12s;
  text-decoration: none;
  color: inherit;
}
.unit:hover[b-t2a1gr1bmr] { background:var(--bg-1); border-color:var(--line-1); }
/* Stretched crawlable link overlay (see UnitCard.razor). Covers the whole card so a
   plain click anywhere opens the drawer; sits above the image / name (z:1) but BELOW
   the interactive controls + faction chips (z:2+) so those stay clickable. Transparent
   — no visual change; gives the card a single keyboard-focusable link. */
.unit-link[b-t2a1gr1bmr] { position:absolute; inset:0; z-index:1; border-radius:var(--r-sm); }
/* The decorative state markers paint above the overlay (z:2) but carry no interaction,
   so let clicks fall through to the .unit-link — the whole card surface opens the drawer,
   as it did when these badges were descendants of the old wrapping card anchor. */
.unit-status[b-t2a1gr1bmr], .unit-coll[b-t2a1gr1bmr], .unit-legacy[b-t2a1gr1bmr] { pointer-events:none; }
.unit[data-compare="True"][b-t2a1gr1bmr]::before {
  content:""; position:absolute; top:4px; left:4px;
  width:10px; height:10px; background:var(--accent); border:1px solid var(--bg-0);
  z-index:2;   /* above the .unit-link overlay (z:1) so the compare dot stays visible */
}
.unit[data-expanded="True"][b-t2a1gr1bmr] { background:var(--bg-1); border-color:var(--accent-line); z-index:5; }

/* The figure IS the card. No inner widget framing — no border, no heavy backdrop. State
   indicators (owned / wanted / legacy) light up the whole card via .unit, not an inner box.
   A barely-there ellipse highlight gives the figure a hint of vignette so it doesn't sit
   dead-flat on the card. */
.unit-img[b-t2a1gr1bmr] {
  width:100%;
  height: var(--uc-img-h, 208px);
  border-radius:var(--r-sm);
  background:radial-gradient(ellipse at 50% 28%, oklch(1 0 0 / 0.04), transparent 65%);
  border:0; position:relative; overflow:hidden;
  display:grid; place-items:center;
}
/* Image children of any of the figure surfaces inherit the portrait fit + parent radius.
   The drawer's .drawer-img > img counterpart is currently still in app.css; it will
   move into its own isolated stylesheet in a later wave. */
.unit-img > img[b-t2a1gr1bmr],
.foldout-thumb > img[b-t2a1gr1bmr] {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}
/* OOP badge — image bottom-left corner. Anchored via top + translateY(-100%) so the
   badge sits 4px inside the image's bottom-left edge (sibling of .unit-img, can't
   absolute-position relative to it directly). Yields the primary right-corner slot
   to .unit-coll (owned/wanted) — the more common state.

   The anchor formula `top: calc(var(--uc-img-h) - 4px); transform: translateY(-100%);`
   is intentionally mirrored on `.unit-coll` below — if you change one, change the other. */
.unit-status[b-t2a1gr1bmr] {
  position: absolute;
  top: calc(var(--uc-img-h, 208px) - 4px);
  left: 4px;
  transform: translateY(-100%);
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.12em; color: var(--warn);
  border: 1px solid var(--warn); padding: 1px 4px; background: var(--bg-0);
  z-index: 2;
}

.multi-sculpt-marker[b-t2a1gr1bmr] {
  position:absolute; top:4px; left:4px; font-size:11px; letter-spacing:0.08em;
  color:var(--accent); border:1px solid var(--accent-line); padding:1px 4px;
  background:var(--bg-0); display:flex; align-items:center; gap:3px; z-index:3; border-radius:1px;
}
.multi-sculpt-marker:hover[b-t2a1gr1bmr] { background:var(--accent); color:var(--bg-0); }
.unit[data-expanded="True"] .multi-sculpt-marker[b-t2a1gr1bmr] { background:var(--accent); color:var(--bg-0); }

/* Sculpt foldout — absolute popover anchored to the right (or left) of the card */
.sculpt-foldout[b-t2a1gr1bmr] {
  position:absolute; top:-1px;
  background:var(--bg-1); border:1px solid var(--accent-line);
  border-radius:var(--r-sm); padding:10px 12px;
  display:flex; flex-direction:column; gap:8px;
  /* Single horizontal row of thumbs — width grows to fit, capped to viewport so a unit
     with many sculpts scrolls inside the panel instead of running off-screen. */
  cursor:default; max-width:min(440px, calc(100vw - 24px));
  z-index:6; box-shadow:0 8px 24px oklch(0 0 0 / 0.5);
}
.unit[data-side="right"] .sculpt-foldout[b-t2a1gr1bmr] { left:calc(100% + 6px); }
.unit[data-side="left"]  .sculpt-foldout[b-t2a1gr1bmr] { right:calc(100% + 6px); }
.sculpt-foldout[b-t2a1gr1bmr]::before {
  content:""; position:absolute; top:14px; width:8px; height:8px;
  background:var(--bg-1); transform:rotate(45deg);
}
.unit[data-side="right"] .sculpt-foldout[b-t2a1gr1bmr]::before {
  left:-5px; border-left:1px solid var(--accent-line); border-bottom:1px solid var(--accent-line);
}
.unit[data-side="left"] .sculpt-foldout[b-t2a1gr1bmr]::before {
  right:-5px; border-right:1px solid var(--accent-line); border-top:1px solid var(--accent-line);
}
.foldout-label[b-t2a1gr1bmr] { font-size:9px; letter-spacing:0.18em; color:var(--accent); }
/* Horizontal row of sculpt thumbs (.foldout-list) — overflow-x:auto so many-sculpt units
   scroll inside the panel rather than running off-screen. Each cell (.foldout-sculpt) holds
   its natural size (flex:0 0 auto + fixed width) so the row is uniformly portrait. */
.foldout-list[b-t2a1gr1bmr] { display:flex; flex-direction:row; gap:10px; overflow-x:auto; padding-bottom:2px; }
.foldout-sculpt[b-t2a1gr1bmr] { flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:4px; background:var(--bg-1); border:1px solid var(--line-1); border-radius:var(--r-sm); width:88px; }
/* Thumb mirrors the new card aesthetic — no inner border, subtle ellipse highlight only,
   portrait ratio matching the main .unit-img (116:208 ≈ 80:144). */
/* align-self:stretch forces the thumb to fill .foldout-sculpt's 88px width.
   Without it, the parent's align-items:center sizes .foldout-thumb to its
   content width — fine when the empty state was an inline placeholder-glyph
   span with intrinsic text, but with <NoImagePlaceholder /> the only child is
   position:absolute (out of flow), which would collapse .foldout-thumb to 0
   width and starve the placeholder of its containing block. */
.foldout-thumb[b-t2a1gr1bmr] { position:relative; align-self:stretch; height:112px; border-radius:var(--r-sm); background:radial-gradient(ellipse at 50% 28%, oklch(1 0 0 / 0.04), transparent 65%); border:0; display:grid; place-items:center; overflow:hidden; }
.foldout-name[b-t2a1gr1bmr] { font-size:11px; letter-spacing:0.04em; color:var(--fg-1); text-align:center; line-height:1.3; }
.foldout-year[b-t2a1gr1bmr] { font-size:11px; letter-spacing:0.14em; color:var(--fg-3); }

/* Unit name slot is fixed at exactly 2 lines tall so cards in a row stay aligned
   regardless of name length. Single-word names reserve the second line as empty;
   3+-line names are truncated with an ellipsis (the drawer shows the full name
   when the card is clicked). The 1em-relative min-height composes with the
   density-conditional font-size override at XS (11px) so the slot scales down
   automatically. */
.unit-name[b-t2a1gr1bmr] {
  font-size:12px; font-weight:500; text-align:center; letter-spacing:0.02em; color:var(--fg-0);
  line-height: 1.2;
  min-height: calc(2 * 1.2 * 1em);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
/* position:relative + z-index lifts the chip row above the .unit-link overlay (z:1)
   so the faction chips / ALL button stay clickable. */
.unit-badges[b-t2a1gr1bmr] { display:flex; gap:2px; flex-wrap:wrap; justify-content:center; margin-top:auto; position:relative; z-index:2; }
.sect-badge[b-t2a1gr1bmr] {
  font-family:var(--ff-mono); font-size:10px; letter-spacing:0.08em;
  padding:1px 2px; min-width:27px; box-sizing:border-box; text-align:center;
  border:1px solid var(--line-2); color:var(--fg-2);
  border-radius:1px; background:var(--bg-1); cursor:pointer;
  text-decoration:none;   /* the Factions-mode chips are <a>; kill the default link underline */
}
.sect-badge:hover[b-t2a1gr1bmr] { border-color:var(--accent-line); color:var(--fg-0); }

/* Tinted (per-faction colour) state — shared by the all-units faction chips (.is-fac)
   and a SELECTED sectorial chip (.is-active). An UNSELECTED sectorial chip keeps the
   grey base above; it lights up in this tint when selected (and the default no-filter
   view counts every sectorial as selected), so a faction/sectorial page matches the
   all-units catalog look. Hue source via the var() fallback: .is-fac anchors set --fh
   inline (a card can span several factions on the all-units page); sectorial chips
   carry no --fh and fall back to --accent-hue — the active faction's hue from Catalog's
   .accent-scope wrapper. */
.sect-badge.is-fac[b-t2a1gr1bmr],
.sect-badge[data-active="True"][b-t2a1gr1bmr],
.sect-badge.is-active[b-t2a1gr1bmr] {
  color: oklch(0.78 0.14 var(--fh, var(--accent-hue)));
  border-color: oklch(0.78 0.14 var(--fh, var(--accent-hue)) / 0.35);
  background: oklch(0.78 0.14 var(--fh, var(--accent-hue)) / 0.08);
}
.sect-badge.is-fac:hover[b-t2a1gr1bmr],
.sect-badge.is-active:hover[b-t2a1gr1bmr] {
  color: oklch(0.86 0.15 var(--fh, var(--accent-hue)));
  border-color: oklch(0.78 0.14 var(--fh, var(--accent-hue)) / 0.7);
  background: oklch(0.78 0.14 var(--fh, var(--accent-hue)) / 0.16);
}

/* Collapsed "ALL" summary chip: neutral grey so it reads as a summary, distinct
   from any single faction's colour. Reuses the .sect-badge box metrics for grid
   alignment; clickable (opens the drawer's AVAILABLE IN section). */
.sect-badge.is-all[b-t2a1gr1bmr] { color: var(--fg-2); border-color: var(--line-2); background: var(--bg-1); }
.sect-badge.is-all:hover[b-t2a1gr1bmr] { border-color: var(--accent-line); color: var(--fg-0); }

/* Non-play category badge — replaces the sectorial/faction chip row on non-play
   models (HVTs, objective minis). Accent-tinted (vs the neutral .sect-badge) so the
   single category chip reads as a distinct, non-interactive marker. */
.unit-npc[b-t2a1gr1bmr] {
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: 0.12em;
  padding: 1px 4px; border: 1px solid var(--accent-line); color: var(--accent-fg);
  background: var(--accent-dim); border-radius: 1px;
}
.unit[data-kind="non_playable"][b-t2a1gr1bmr] { /* reserved for future non-play card tinting */ }

/* ── Collection: card overlays ───────────────────────────────── */
.unit-fav[b-t2a1gr1bmr] {
  position: absolute;
  top: 4px; right: 4px;
  z-index: 3;
  width: 18px; height: 18px;
  display: grid; place-items: center;
  color: oklch(0.72 0.10 75);            /* muted gold at rest — readable, not loud */
  background: oklch(from var(--bg-0) l c h / 0.6);
  border: 1px solid oklch(0.72 0.10 75 / 0.35);
  border-radius: 1px;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.unit-fav:hover[b-t2a1gr1bmr] { color: oklch(0.85 0.16 75); border-color: oklch(0.82 0.13 75 / 0.6); }
.unit-fav.is-on[b-t2a1gr1bmr] {
  color: oklch(0.85 0.16 75);            /* full gold when favorited */
  border-color: oklch(0.82 0.13 75 / 0.6);
  background: oklch(0.82 0.13 75 / 0.18);
}

/* Owned / wanted badge — image bottom-right corner. Anchor formula mirrors
   `.unit-status` above (kept in sync intentionally). Translucent neutral backdrop +
   blur is consistent with `.unit-legacy` and stays legible over any figure image;
   per-state text + border colour (set below) carry the owned vs wanted signal. */
.unit-coll[b-t2a1gr1bmr] {
  position: absolute;
  top: calc(var(--uc-img-h, 208px) - 4px);
  right: 4px;
  transform: translateY(-100%);
  z-index: 2;
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.14em;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 4px;
  border: 1px solid var(--line-2);
  background: oklch(from var(--bg-0) l c h / 0.85);
  backdrop-filter: blur(2px);
  border-radius: 1px;
  color: var(--fg-2);
}
.unit-coll[b-t2a1gr1bmr]::before {
  content: ""; width: 6px; height: 6px;
  border-radius: 50%; background: currentColor;
}
/* Per-state colour treatment: text + border only. The base `background` (translucent
   neutral) is intentionally not overridden — a tinted backdrop reads poorly over a
   unit photo, and the green/orange text + matching border carry the state signal. */
.unit-coll[data-coll="owned"][b-t2a1gr1bmr] {
  color: var(--accent);
  border-color: var(--accent-line);
}
.unit-coll[data-coll="wanted"][b-t2a1gr1bmr] {
  color: var(--warn);
  border-color: oklch(0.78 0.14 60 / 0.5);
}
.unit-coll[data-coll="wanted"][b-t2a1gr1bmr]::before {
  background: transparent;
  border: 1px solid currentColor;
}

/* ── Legacy badge + demoted card treatment ───────────────────── */
.unit-legacy[b-t2a1gr1bmr] {
  position: absolute;
  /* Top-center — out of the way of the sculpts toggle (top-left) and favorite star (top-right). */
  top: 4px; left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.14em;
  color: var(--fg-3);
  border: 1px dashed var(--line-2);
  padding: 1px 5px;
  background: oklch(from var(--bg-0) l c h / 0.85);
  backdrop-filter: blur(2px);
  white-space: nowrap;
}
/* Legacy units only show inside the explicitly-revealed archive section, so we keep their
   colors at full strength — visitors who got there asked to see them. The dashed outline
   is enough to differentiate at-a-glance without muting the figure. */
.unit[data-legacy="true"][b-t2a1gr1bmr] { border-style: dashed; border-color: var(--line-2); }

/* ── Roster: centered ±  controls revealed on card hover ─────── */
/* --uc-rb-size / --uc-rb-gap come from .sheet[data-density=...] in
   IdentSheet.razor.css so the buttons scale with card density. */
.unit-roster-ctl[b-t2a1gr1bmr] {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
  display: flex;
  gap: var(--uc-rb-gap, 6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s;
}
.unit:hover .unit-roster-ctl[b-t2a1gr1bmr] {
  opacity: 1;
  pointer-events: auto;
}
.unit-roster-btn[b-t2a1gr1bmr] {
  width: var(--uc-rb-size, 28px); height: var(--uc-rb-size, 28px);
  border-radius: 50%;
  background: oklch(from var(--bg-0) l c h / 0.88);
  border: 1px solid var(--accent-line);
  color: var(--fg-0);
  font-size: calc(var(--uc-rb-size, 28px) * 0.57); line-height: 1;
  cursor: pointer;
  display: grid; place-items: center;
  font-family: var(--ff-mono);
  backdrop-filter: blur(2px);
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.unit-roster-btn:hover:not(:disabled)[b-t2a1gr1bmr] {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: var(--accent-fg);
}
.unit-roster-btn:disabled[b-t2a1gr1bmr] {
  opacity: 0.35;
  cursor: not-allowed;
}

/* ── Hover-only affordances disabled on touch ──
   Touch devices don't fire :hover predictably. The +/- roster controls and
   the multi-sculpt foldout are both reachable from the detail drawer in
   one tap, so hide them entirely when the user has no hover capability.
   This uses (hover: none) AND (pointer: coarse) intentionally — phones
   with a paired Bluetooth mouse keep the hover affordances. */
@media (hover: none) and (pointer: coarse) {
  .unit-roster-ctl[b-t2a1gr1bmr] { display: none; }
  .sculpt-foldout[b-t2a1gr1bmr] { display: none; }
}

/* In-roster card visual: thin left-edge accent rail painted via an `::after` pseudo
   so it sits ABOVE the image (an inset `box-shadow` paints under child backgrounds,
   so the leftmost 3px of the figure ended up covering the rail). Absolute
   positioning keeps the rail out of the box model — toggling roster state does
   not reflow the card. Opacity transition gives a smooth fade on toggle. */
.unit[b-t2a1gr1bmr]::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 3px;
  background: var(--accent-fg);
  /* z:2 sits above the image (z:0) so the rail wins the left-edge overlap. The
     sculpt marker and favorite star (z:3) still paint above it. */
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s;
}
.unit[data-in-roster="true"][b-t2a1gr1bmr]::after {
  opacity: 1;
}
.unit-roster-count[b-t2a1gr1bmr] {
  color: var(--accent-fg);
  font-size: 11px;
  letter-spacing: 0.04em;
  margin-left: 2px;
}

/* Drag cursor — the html[data-unit-dragging="true"] attribute is set by ident.js
   during drag-and-drop. Scoped CSS preserves the ancestor descendant relationship:
   the rewritten rule is html[data-unit-dragging="true"] .unit[b-xxxxx], which still
   matches because html is an unscoped ancestor of every .unit. */
html[data-unit-dragging="true"] .unit[b-t2a1gr1bmr] { cursor: grabbing; }
/* /Components/Layout/UnitDetailDrawer.razor.rz.scp.css */
/* ── Detail drawer (UnitDetailDrawer.razor) ──
   The slide-in unit detail panel. Renders the drawer head (portrait + nav),
   body (collection actions, spec rows, skills/weapons, sculpts, products),
   plus the bottom-of-body recent-strip and CTA cluster.
   Mobile (<=600px): fullscreen treatment with a BACK affordance. */

/* The drawer image's portrait fit + parent radius. The unit card's matching
   .unit-img > img / .foldout-thumb > img rule lives in UnitCard.razor.css. */
.drawer-img > img[b-nau9xsw5c7] {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}

/* The native <dialog>'s ::backdrop replaces the formerly-separate .drawer-scrim
   div. ::backdrop only exists while the dialog is open via showModal() (lives
   in the browser top layer), so no .is-open variant, no transition, no z-index,
   no pointer-events plumbing — the pseudo-element comes and goes with the dialog. */
.drawer[b-nau9xsw5c7]::backdrop {
  background: oklch(0 0 0 / 0.5);
}

/* Override the UA <dialog> defaults (centered, fit-content, default border /
   padding / background) — the drawer slides in from the right edge and fills
   the viewport height. max-width / right:0 / top:0 / bottom:0 keep it anchored.
   The native <dialog> sits in the top layer when opened via showModal(); the
   former z-index:50 is no longer needed (kept off so the dialog stays above
   all z-index'd content, which is the intended behavior). */
.drawer[b-nau9xsw5c7] {
  position:fixed; top:0; right:0; bottom:0; left:auto; width:480px; max-width:92vw;
  max-height:100vh; height:100vh; margin:0;
  padding:0; border:0; border-left:1px solid var(--line-1);
  background:var(--bg-1); color:inherit;
  transform:translateX(100%); transition:transform .24s cubic-bezier(.2,.7,.2,1);
  display:flex; flex-direction:column;
}
/* Two-step reveal for the slide-in transition: showModal() flips
   display:none → block AND lands [open] in the same tick — if [open]
   also drove the transform, the engine would see the start AND end
   transform values arrive simultaneously and skip the transition.
   Instead, modal-a11y.js's showDialog() adds .is-open on the NEXT
   animation frame (after the display:block layout commits), giving
   the engine a real start→end window to interpolate `transform:
   translateX(100%) → translateX(0)`. The drawer-as-dialog design
   spec calls this out explicitly. Close has no transition (dialog.close()
   flips display back to none, which CSS transitions can't span); accepted. */
.drawer.is-open[b-nau9xsw5c7] { transform:translateX(0); }

.drawer-head[b-nau9xsw5c7] { padding:20px 24px; border-bottom:1px solid var(--line-1); display:flex; align-items:flex-start; gap:14px; }
.drawer-close[b-nau9xsw5c7] { margin-left:auto; width:28px; height:28px; border:1px solid var(--line-1); display:grid; place-items:center; color:var(--fg-2); }
.drawer-close:hover[b-nau9xsw5c7] { color:var(--accent); border-color:var(--accent-line); }

.drawer-img[b-nau9xsw5c7] {
  position:relative;
  width:104px; height:140px; border-radius:var(--r-sm);
  background:radial-gradient(circle at 50% 30%,var(--bg-3),var(--bg-2));
  border:1px solid var(--accent-line); display:grid; place-items:center; flex-shrink:0;
  overflow:hidden;
}
.drawer-id[b-nau9xsw5c7] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.18em; color:var(--accent); }
.drawer h3[b-nau9xsw5c7] { margin:4px 0 2px; font-size:24px; font-weight:600; letter-spacing:0.02em; }
.drawer-sub[b-nau9xsw5c7] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.14em; color:var(--fg-3); }
/* Category badge for a non-play model (HVT, objective mini). Mirrors .drawer-sub's
   mono/size/tracking but accent-tinted to read as a status marker rather than a
   sub-label, and boxed like a pill so it stands in for the trooper stat rows the
   drawer hides for these models. */
.drawer-npc[b-nau9xsw5c7] {
  display:inline-block; margin-top:4px;
  font-family:var(--ff-mono); font-size:11px; letter-spacing:0.16em;
  color:var(--accent-fg);
  background:var(--accent-dim);
  border:1px solid var(--accent-line);
  border-radius:var(--r-sm);
  padding:2px 7px;
}

.drawer-body[b-nau9xsw5c7] { padding:20px 24px; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:24px; }
.drawer-block[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:8px; }
.drawer-block-head[b-nau9xsw5c7] { display:flex; align-items:baseline; justify-content:space-between; border-bottom:1px solid var(--line-1); padding-bottom:6px; margin-bottom:4px; }
.drawer-block-head .k[b-nau9xsw5c7] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.18em; color:var(--fg-2); }
.drawer-block-head .k-count[b-nau9xsw5c7] { font-size:11px; color:var(--accent); letter-spacing:0.14em; }
.drawer-tiny[b-nau9xsw5c7] { font-size:11px; letter-spacing:0.14em; color:var(--fg-3); }

/* ── Detail drawer fullscreen (phone) ──
   At <=600px the 480px side drawer becomes a fullscreen view. The close
   button visually becomes a "back" arrow via ::before and label-size
   swap; the underlying Razor markup is unchanged. .drawer-head padding
   tightens. .spec-row labels stack above values (single column).
   Specificity notes: .drawer.drawer.rail-shifted (0,3,0) bumps past the
   later .drawer.rail-shifted at the bottom of the file. .drawer-body
   .spec-row prefixes bump past the unconditional .spec-row rules
   immediately following this block. */
@media (max-width: 600px) {
  .drawer[b-nau9xsw5c7] {
    width: 100vw;
    max-width: none;
    border-left: 0;
  }
  .drawer.drawer.rail-shifted[b-nau9xsw5c7] { right: 0; }   /* roster bottom-sheet doesn't push drawer */

  .drawer-head[b-nau9xsw5c7] { padding: 12px 16px; gap: 10px; }

  .drawer-close[b-nau9xsw5c7] {
    width: auto; height: auto;
    padding: 6px 10px;
    font-size: 0;        /* hide the original "x" glyph */
    line-height: 1;
  }
  .drawer-close[b-nau9xsw5c7]::before {
    content: "\2190 BACK";   /* "<- BACK" */
    font-family: var(--ff-mono);
    font-size: 11px;
    letter-spacing: 0.14em;
    color: var(--fg-1);
  }

  .drawer-body .spec-row[b-nau9xsw5c7] {
    grid-template-columns: 1fr;
    gap: 2px;
    padding: 8px 0;
  }
  .drawer-body .spec-row .k[b-nau9xsw5c7] { font-size: 10px; }
}

.spec-row[b-nau9xsw5c7] { display:grid; grid-template-columns:92px 1fr; gap:12px; padding:8px 0; border-top:1px solid var(--line-1); align-items:center; }
.spec-row .k[b-nau9xsw5c7] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.16em; color:var(--fg-3); }
.spec-row .v[b-nau9xsw5c7] { font-size:13px; color:var(--fg-0); }

.tag-row[b-nau9xsw5c7] { display:flex; flex-wrap:wrap; gap:6px; }
.tag-pill[b-nau9xsw5c7] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.12em; padding:4px 8px; border:1px solid var(--line-2); color:var(--fg-1); background:var(--bg-2); cursor:pointer; text-decoration:none; }
.tag-pill:hover[b-nau9xsw5c7] { border-color:var(--accent); color:var(--fg-0); }
.tag-pill[data-active="True"][b-nau9xsw5c7], .tag-pill.is-active[b-nau9xsw5c7] { background:var(--accent); color:var(--bg-0); border-color:var(--accent); }

.avail-wrap[b-nau9xsw5c7] { position: relative; }
.avail-groups[b-nau9xsw5c7] { display: flex; flex-direction: column; }
.avail-grp[b-nau9xsw5c7] {
  display: grid; grid-template-columns: 38px 1fr; gap: 10px;
  padding: 5px 0;
  border-top: 1px dashed var(--line-1);
}
.avail-grp:first-child[b-nau9xsw5c7] { border-top: 0; }
.avail-grp .fcode[b-nau9xsw5c7] {
  font-family: var(--ff-mono); font-size: 11px;
  letter-spacing: 0.14em; color: var(--fg-3);
  padding-top: 5px;
}
.avail-grp .pills[b-nau9xsw5c7] { display: flex; flex-wrap: wrap; gap: 4px; }

.avail-wrap.collapsed .avail-groups[b-nau9xsw5c7] {
  max-height: 120px; overflow: hidden;
}
.avail-fade[b-nau9xsw5c7] {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 64px; pointer-events: none;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--bg-1) 0%, transparent) 0%,
    color-mix(in srgb, var(--bg-1) 85%, transparent) 50%,
    var(--bg-1) 100%
  );
}
.avail-wrap.expanded .avail-fade[b-nau9xsw5c7] { display: none; }

.avail-toggle[b-nau9xsw5c7] {
  width: 100%; display: flex; align-items: center; justify-content: center;
  gap: 6px; padding: 6px 0;
  background: transparent; border: 0; cursor: pointer;
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: 0.16em;
  color: var(--fg-3);
}
.avail-toggle:hover[b-nau9xsw5c7] { color: var(--fg-0); }
.avail-wrap.collapsed .avail-toggle[b-nau9xsw5c7] { margin-top: -18px; position: relative; z-index: 1; }
.avail-wrap.expanded .avail-toggle[b-nau9xsw5c7] { margin-top: 6px; }

.sculpt-list[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:6px; }
.sculpt-row[b-nau9xsw5c7] {
  display:flex; align-items:center; gap:12px; padding:6px 8px;
  background:var(--bg-2); border:1px solid var(--line-1); border-radius:var(--r-sm);
  cursor:pointer; text-align:left; width:100%; position:relative;
  transition:border-color .12s,background .12s,opacity .12s;
}
.sculpt-row:hover[b-nau9xsw5c7] { border-color:var(--accent-line); }
.sculpt-row[data-active="True"][b-nau9xsw5c7], .sculpt-row.is-active[b-nau9xsw5c7] { background:var(--accent-dim); border-color:var(--accent-line); }
.sculpt-row[data-dim="True"][b-nau9xsw5c7], .sculpt-row.is-dim[b-nau9xsw5c7] { opacity:.45; }
.sculpt-marker[b-nau9xsw5c7] { width:6px; height:6px; border-radius:50%; background:var(--line-2); margin-left:auto; transition:background .12s,box-shadow .12s; }
.sculpt-row[data-active="True"] .sculpt-marker[b-nau9xsw5c7], .sculpt-row.is-active .sculpt-marker[b-nau9xsw5c7] { background:var(--accent); box-shadow:0 0 8px var(--accent); }

.link-btn[b-nau9xsw5c7] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.06em; color:var(--accent); }
.link-btn:hover[b-nau9xsw5c7] { color:var(--fg-0); }
.sculpt-thumb[b-nau9xsw5c7] { position:relative; width:40px; height:52px; border-radius:var(--r-sm); background:radial-gradient(circle at 50% 30%,var(--bg-3),var(--bg-1)); border:1px solid var(--line-1); display:grid; place-items:center; flex-shrink:0; overflow:hidden; }
    .sculpt-thumb > img[b-nau9xsw5c7] {
        width: 100%;
        height: 100%;
        object-fit: cover;
        border-radius: inherit;
    }
.sculpt-meta[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:2px; min-width:0; }
.sculpt-name[b-nau9xsw5c7] { font-size:13px; color:var(--fg-0); }

.product-list[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:8px; }
.product-card[b-nau9xsw5c7] {
  display:grid; grid-template-columns:64px 1fr auto; gap:12px;
  padding:10px; background:var(--bg-2); border:1px solid var(--line-1);
  border-radius:var(--r-sm); text-decoration:none; color:inherit;
  transition:border-color .12s,background .12s; align-items:center;
}
.product-card:hover[b-nau9xsw5c7] { border-color:var(--accent-line); background:var(--bg-3); }
.product-card[data-status="oop"][b-nau9xsw5c7] { opacity:.72; }
.product-card[data-status="oop"]:hover[b-nau9xsw5c7] { opacity:1; }
.product-card[data-highlight="true"][b-nau9xsw5c7] { border-color:var(--accent); background:var(--accent-dim); box-shadow:0 0 0 1px var(--accent-line); }
.product-card[data-highlight="dim"][b-nau9xsw5c7] { opacity:.32; }
.product-card[data-highlight="dim"]:hover[b-nau9xsw5c7] { opacity:.7; }

.product-thumb[b-nau9xsw5c7] { width:64px; height:64px; background:var(--bg-1); border:1px solid var(--line-1); border-radius:var(--r-sm); position:relative; display:grid; place-items:center; overflow:hidden; }
.product-thumb img[b-nau9xsw5c7] { width:100%; height:100%; object-fit:cover; }
.product-status-tag[b-nau9xsw5c7] { position:absolute; bottom:2px; left:2px; font-family:var(--ff-mono); font-size:11px; letter-spacing:0.12em; padding:1px 4px; background:var(--bg-0); color:var(--warn); border:1px solid var(--warn); }
.product-meta[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:4px; min-width:0; }
.product-name[b-nau9xsw5c7] { font-size:13px; font-weight:500; color:var(--fg-0); letter-spacing:0.02em; }
.product-contains[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:4px; }
.contains-list[b-nau9xsw5c7] { display:flex; flex-wrap:wrap; gap:3px; }
.contains-chip[b-nau9xsw5c7] { font-size:11px; letter-spacing:0.1em; padding:1px 5px; background:var(--bg-0); border:1px solid var(--line-1); color:var(--fg-2); border-radius:1px; }
.contains-chip[data-active="True"][b-nau9xsw5c7], .contains-chip.is-active[b-nau9xsw5c7] { background:var(--accent); border-color:var(--accent); color:var(--bg-0); }
.product-cta[b-nau9xsw5c7] { display:flex; align-items:center; gap:6px; font-family:var(--ff-mono); font-size:11px; letter-spacing:0.08em; color:var(--accent); padding:0 4px; }
.product-card:hover .product-cta[b-nau9xsw5c7] { color:var(--fg-0); }

/* Split CTA stack on product cards inside the unit drawer */
.product-cta-stack[b-nau9xsw5c7] { display:flex; flex-direction:column; gap:4px; align-self:center; }
.product-cta-btn[b-nau9xsw5c7] {
  display:grid; place-items:center;
  font-family:var(--ff-mono);
  font-size:11px; letter-spacing:0.08em;
  padding:6px 10px;
  border:1px solid var(--line-1);
  background:var(--bg-1);
  color:var(--fg-2);
  text-decoration:none;
  border-radius:var(--r-sm);
  white-space:nowrap;
  transition:border-color 0.12s, color 0.12s, background 0.12s;
}
.product-cta-btn:hover[b-nau9xsw5c7] { border-color:var(--accent-line); color:var(--accent-fg); background:var(--accent-dim); }
.product-cta-btn.content[b-nau9xsw5c7] { /* default styling fits */ }
.product-cta-btn.store[b-nau9xsw5c7]   { color:var(--accent-fg); }
.affiliate-disclosure[b-nau9xsw5c7] { padding-top:8px; font-size:11px; letter-spacing:0.04em; color:var(--fg-3); line-height:1.5; }

/* ── Product card reflow (phone) ──
   At <=600px the desktop 3-column layout (thumb | meta | CTA-auto) crowds
   the meta column. Move the CTAs onto their own row below the meta with
   a divider line, and shrink the thumb to 56px to make more room for
   the product name.
   Placed after the .product-* cluster (not directly after .product-card[data-highlight="dim"]:hover
   as the task spec suggested) so source-order cascade lets these @media rules
   win against the unconditional .product-thumb / .product-cta / .affiliate-disclosure
   declarations above — same single-class specificity, so source order is the tiebreak. */
@media (max-width: 600px) {
  .product-card[b-nau9xsw5c7] {
    grid-template-columns: 56px 1fr;
    gap: 10px;
  }
  .product-thumb[b-nau9xsw5c7] { width: 56px; height: 56px; }
  .product-cta[b-nau9xsw5c7], .product-cta-stack[b-nau9xsw5c7] {
    grid-column: 1 / -1;
    flex-direction: row;
    justify-content: flex-end;
    padding-top: 6px;
    border-top: 1px solid var(--line-1);
  }
  .product-cta-btn[b-nau9xsw5c7] { padding: 10px 14px; }
  /* `.affiliate-disclosure` lives inside `.drawer-body` (which already has
     `padding: 20px 24px`) AND on the standalone Products page. Re-adding
     the page gutter horizontally double-pads inside the drawer on phone —
     leave horizontal padding to the parent and only bump the vertical
     spacing + font size here. */
  .affiliate-disclosure[b-nau9xsw5c7] { font-size: 12px; padding-top: 12px; padding-bottom: 12px; }
}

/* ── Drawer collection block ─────────────────────────────────── */
.collection-block[b-nau9xsw5c7] { display: flex; flex-direction: column; gap: 10px; }
.collection-summary[b-nau9xsw5c7] {
  font-size: 11px; letter-spacing: 0.04em;
  color: var(--fg-3);
  padding-top: 4px;
}
.collection-summary .cs-owned[b-nau9xsw5c7]  { color: var(--accent-fg); }
.collection-summary .cs-wanted[b-nau9xsw5c7] { color: var(--warn); }
.collection-summary .cs-muted[b-nau9xsw5c7]  { color: var(--fg-3); }
.collection-actions[b-nau9xsw5c7] { display: grid; grid-template-columns: auto 1fr 1fr; gap: 6px; }
.coll-btn[b-nau9xsw5c7] {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 8px 12px;
  border: 1px solid var(--line-2);
  background: var(--bg-2);
  color: var(--fg-1);
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.16em;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.coll-btn:hover[b-nau9xsw5c7] { border-color: var(--accent-line); color: var(--fg-0); }
.coll-btn.is-on[b-nau9xsw5c7] {
  border-color: var(--accent);
  background: var(--accent-dim);
  color: var(--fg-0);
}
.coll-btn.fav.is-on[b-nau9xsw5c7] {
  color: oklch(0.85 0.16 75);
  border-color: oklch(0.82 0.13 75 / 0.6);
  background: oklch(0.82 0.13 75 / 0.18);
}
.coll-btn.wanted.is-on[b-nau9xsw5c7] {
  color: var(--warn);
  border-color: oklch(0.78 0.14 60 / 0.6);
  background: oklch(0.78 0.14 60 / 0.14);
}

/* Sculpt row owned check */
.sculpt-own[b-nau9xsw5c7] {
  min-width: 16px; height: 16px;
  padding: 0 3px;
  border: 1px solid var(--line-2);
  border-radius: 1px;
  display: grid; place-items: center;
  margin-right: 4px;
  cursor: pointer;
  flex-shrink: 0;
  color: transparent;
  font-family: var(--ff-mono);
  font-size: 11px; font-weight: 700; line-height: 1;
  transition: border-color 0.12s, background 0.12s, color 0.12s;
}
.sculpt-own:hover[b-nau9xsw5c7] { border-color: var(--accent-line); }
.sculpt-own.is-on[b-nau9xsw5c7] {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg-0);
}

/* Inline +/− stepper next to the ✓ in the drawer's sculpt-rows.
   Fine-grained per-sculpt count adjustment without needing the products page. */
.sculpt-step[b-nau9xsw5c7] {
  width: 14px; height: 16px;
  padding: 0;
  border: 1px solid var(--line-2);
  background: transparent;
  color: var(--fg-2);
  font-family: var(--ff-mono);
  font-size: 11px; line-height: 1; font-weight: 700;
  cursor: pointer;
  margin-right: 2px;
  border-radius: 1px;
  display: grid; place-items: center;
  flex-shrink: 0;
  transition: border-color 0.12s, color 0.12s;
}
.sculpt-step:hover:not(:disabled)[b-nau9xsw5c7] {
  color: var(--accent-fg);
  border-color: var(--accent-line);
}
.sculpt-step:disabled[b-nau9xsw5c7] {
  opacity: 0.35;
  cursor: not-allowed;
}

/* Sculpt wanted toggle — same shape as .sculpt-own but in the warn/gold colorway */
.sculpt-want[b-nau9xsw5c7] {
  width: 16px; height: 16px;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  display: grid; place-items: center;
  margin-right: 4px;
  cursor: pointer;
  flex-shrink: 0;
  color: transparent;
  transition: border-color 0.12s, background 0.12s, color 0.12s;
}
.sculpt-want:hover[b-nau9xsw5c7] { border-color: oklch(0.78 0.14 60 / 0.5); }
.sculpt-want.is-on[b-nau9xsw5c7] {
  border-color: var(--warn);
  background: transparent;
  color: var(--warn);
}
.sculpt-want.is-on[b-nau9xsw5c7]::after {
  content: "◎";
  font-family: var(--ff-mono);
  font-size: 11px; font-weight: 700;
}

/* ── Skill / weapon chips container ───────────────────────────
   The .ref-chip leaf is in RefChip.razor.css; this flex wrapper
   configures the chips' layout context inside the drawer body. */
.ref-chips[b-nau9xsw5c7] { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }

/* ── Drawer prev/next nav + recently viewed ──────────────────── */
.drawer-nav[b-nau9xsw5c7] {
  display: flex; align-items: center; gap: 4px;
  margin-left: auto; margin-right: 8px;
}
.drawer-nav button[b-nau9xsw5c7] {
  width: 26px; height: 26px;
  background: none;
  border: 1px solid var(--line-1);
  border-radius: 3px;
  color: var(--fg-2);
  cursor: pointer;
  font-family: var(--ff-mono);
  font-size: 12px;
}
.drawer-nav button:hover:not(:disabled)[b-nau9xsw5c7] { border-color: var(--accent-line); color: var(--fg-0); }
.drawer-nav button:disabled[b-nau9xsw5c7] { opacity: 0.3; cursor: not-allowed; }
.drawer-nav .pos[b-nau9xsw5c7] {
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.12em;
  color: var(--fg-3);
  padding: 0 4px;
  min-width: 56px;
  text-align: center;
}

.recent-strip[b-nau9xsw5c7] {
  border-top: 1px solid var(--line-1);
  padding: 10px 16px;
  background: var(--bg-1);
}
.recent-strip-head[b-nau9xsw5c7] {
  font-family: var(--ff-mono);
  font-size: 10px; letter-spacing: 0.14em;
  color: var(--fg-3);
  margin-bottom: 6px;
}
.recent-list[b-nau9xsw5c7] { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: thin; }
.recent-pill[b-nau9xsw5c7] {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 6px;
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  border-radius: 3px;
  padding: 5px 9px;
  font-size: 11px;
  color: var(--fg-1);
  cursor: pointer;
  white-space: nowrap;
}
.recent-pill:hover[b-nau9xsw5c7] { border-color: var(--accent-line); color: var(--fg-0); }
.recent-pill[data-current="True"][b-nau9xsw5c7] {
  border-color: oklch(70% 0.18 var(--accent-hue));
  background: var(--accent-dim);
  color: var(--fg-0);
}
.recent-pill .fcode[b-nau9xsw5c7] { font-family: var(--ff-mono); font-size: 11px; letter-spacing: 0.14em; color: var(--fg-3); }

/* When rail open, shift the detail drawer left so they coexist */
.drawer.rail-shifted[b-nau9xsw5c7] { right: 320px; }

/* One-time accent pulse on the AVAILABLE IN list when reached via the "ALL"
   faction chip. Honours reduced-motion. The id'd element is .avail-groups. */
@keyframes avail-flash-b-nau9xsw5c7 {
  0%   { box-shadow: 0 0 0 0 oklch(0.78 0.14 var(--accent-hue) / 0); }
  20%  { box-shadow: 0 0 0 3px oklch(0.78 0.14 var(--accent-hue) / 0.45); }
  100% { box-shadow: 0 0 0 0 oklch(0.78 0.14 var(--accent-hue) / 0); }
}
.flash-once[b-nau9xsw5c7] { animation: avail-flash-b-nau9xsw5c7 1s ease-out 1; border-radius: var(--r-sm); }
@media (prefers-reduced-motion: reduce) { .flash-once[b-nau9xsw5c7] { animation: none; } }
/* /Components/Layout/UnofficialFanProjectBadge.razor.rz.scp.css */
/* ── Fixed-position chip, bottom-left ───────────────────────────────
   Mirrors the positioning of .cookie-settings-link (bottom-right) so
   the two utility chips occupy opposite corners. The chip is plain
   static text — no popover, no interactivity — so it stays out of the
   way of catalog UX while remaining visible on every page.

   The bottom inset matches the cookie-settings-link's so they sit at
   the same baseline. Z-index is 30 to match CookieSettingsLink — both
   utility chips share the same band, deliberately below the in-app
   panels (z:50: RosterRail / GlobalSearch / FiltersDrawer) and the
   compare bar (z:55) so they don't overlay interactive surfaces. The
   topbar (z:10) and any uncovered catalog content sit below us. */

.unofficial-badge[b-rw94yufjvp] {
    position: fixed;
    bottom: 8px;
    left: 12px;
    z-index: 30;

    display: inline-flex;
    align-items: baseline;
    gap: 6px;

    font-family: var(--ff-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--fg-3);

    padding: 4px 8px;
    border: 1px solid var(--line-1);
    border-radius: 3px;
    background: var(--bg-1);
    opacity: 0.92;

    transition: opacity 120ms ease, border-color 120ms ease, color 120ms ease;
}

.unofficial-badge:hover[b-rw94yufjvp],
.unofficial-badge:focus-visible[b-rw94yufjvp] {
    opacity: 1;
    border-color: var(--accent-line);
    color: var(--fg-1);
}

.unofficial-badge-prefix[b-rw94yufjvp] {
    font-weight: 600;
    color: var(--fg-1);
}

.unofficial-badge-sep[b-rw94yufjvp] {
    color: var(--fg-3);
}

.unofficial-badge-attr[b-rw94yufjvp] {
    /* Slightly muted so the "Unofficial" prefix reads first. */
    color: var(--fg-3);
}

/* On narrow viewports the chip should not collide with the cookie-
   settings-link in the opposite corner. The badge is short enough
   (~20 characters) that this is unlikely until below ~380px width,
   but at that point we hide the attribution sub-text and keep just
   the "Unofficial" prefix so the chip stays compact. */
@media (max-width: 380px) {
    .unofficial-badge-sep[b-rw94yufjvp],
    .unofficial-badge-attr[b-rw94yufjvp] {
        display: none;
    }
}

/* When printing, the fixed-position chip is visual noise — hide it.
   The full attribution still lives in printed form on /about's
   #attribution section, which is part of the article body and prints
   normally. */
@media print {
    .unofficial-badge[b-rw94yufjvp] {
        display: none;
    }
}
/* /Components/Pages/About.razor.rz.scp.css */
/* ── About page (About.razor) ──────────────────────────────────── */

/* The html/body overflow override (needed because IdentLayout locks
   them to `overflow: hidden`) lives in a <HeadContent><style> block in
   About.razor. `::deep` can't reach html/body because they're ancestors
   of this component's scope, not descendants. Same arrangement as
   CookiePolicy. */

.about-page[b-sm3n57srgz] {
  max-width: 720px;
  margin: 0 auto;
  padding: 48px 24px;
  color: var(--fg-1, #ddd);
  line-height: 1.6;
}
.about-page header[b-sm3n57srgz] { margin-bottom: 32px; }
.about-page h1[b-sm3n57srgz] {
  font-size: 40px;
  margin: 0 0 8px 0;
  letter-spacing: -1px;
}
.about-page header .muted[b-sm3n57srgz] {
  color: var(--fg-2, #999);
  font-size: 14px;
  margin: 0;
}
.about-page h2[b-sm3n57srgz] {
  font-size: 18px;
  margin: 32px 0 8px 0;
  color: var(--fg-2, #999);
}
.about-page p[b-sm3n57srgz] { margin: 12px 0; color: var(--fg-1, #ddd); }
.about-page ul[b-sm3n57srgz] { padding-left: 20px; }
.about-page li[b-sm3n57srgz] { margin: 6px 0; }
.about-page a[b-sm3n57srgz] { color: oklch(0.7 0.15 220); }
/* /Components/Pages/Buy.razor.rz.scp.css */
/* Layout primitives mirrored from Products.razor.css. Blazor CSS isolation
   scopes those rules to /products' component, so we re-declare the section /
   grid / filter styles here so the buy page renders with the same visual
   vocabulary without depending on global plumbing. If a future refactor
   moves these primitives into wwwroot/app.css, drop them here. */

/* No sidebar on this page — collapse the .ident-app grid (default 240px | 1fr)
   to a single column, same trick Products.razor.css:13 uses. Without this,
   the lone <main class="ident-main"> auto-places into the 240px first cell
   and the page renders crammed into a narrow left sidebar. */
.ident-app.buy-page[b-7tniqv4e0p] { grid-template-columns: 1fr; }

/* Filter row — mirrored from Products.razor.css :16-55 so /purchaseplanner and /products
   read as siblings. CSS-isolation scopes the rules to this page; the class
   names are renamed but the rule bodies are intentionally identical so a
   future restyle on either side keeps them aligned. If these layout
   primitives ever land in wwwroot/app.css, drop the .buy-* copies. */

.buy-filters[b-7tniqv4e0p] {
  position: sticky; top: var(--topbar-h); z-index: 9;
  display: flex; flex-direction: column; gap: 10px;
  background: oklch(from var(--bg-0) l c h / 0.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line-1);
  padding: 14px 32px;
}
.buy-filter-row[b-7tniqv4e0p] {
  display: flex; align-items: center; gap: 12px;
  font-size: 13px; color: var(--fg-1);
}
/* Fixed-width label column so the chips on every row start at the same x —
   matches the same trick on /products (the labels themselves vary in width).
   70px clears the widest current label with a few pixels of slack. */
.buy-filter-row > .admin-label[b-7tniqv4e0p] { min-width: 70px; }
.buy-chips[b-7tniqv4e0p] { display: flex; flex-wrap: wrap; gap: 4px; }
.buy-chip[b-7tniqv4e0p] {
  /* Tighter, blockier mono per the design pass — 600 weight + 10px gives the
     chip text more "data-readout" presence; sharper corners (border-radius 0)
     match the rest of the page's HUD chrome. */
  font: 600 10px/1 var(--ff-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 4px 10px;
  border: 1px solid var(--line-1);
  background: var(--bg-1);
  color: var(--fg-2);
  cursor: pointer;
  border-radius: 0;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.buy-chip:hover[b-7tniqv4e0p] { color: var(--fg-0); border-color: var(--line-2); }
.buy-chip.is-active[b-7tniqv4e0p] {
  border-color: oklch(70% 0.18 var(--fh, var(--accent-hue)));
  background: oklch(70% 0.18 var(--fh, var(--accent-hue)) / 0.16);
  color: var(--fg-0);
}

.buy-section[b-7tniqv4e0p] {
  padding: 24px 32px;
  border-top: 1px solid var(--line-1);
}
.buy-section:first-of-type[b-7tniqv4e0p] { border-top: 0; }
.buy-section-head[b-7tniqv4e0p] {
  display: flex; align-items: baseline; gap: 12px;
  margin-bottom: 16px;
}
.buy-section-code[b-7tniqv4e0p] {
  font-size: 11px; letter-spacing: 0.18em;
  padding: 3px 10px;
  border: 1px solid oklch(70% 0.18 var(--accent-hue, 220));
  background: oklch(70% 0.18 var(--accent-hue, 220) / 0.18);
  color: oklch(85% 0.16 var(--accent-hue, 220));
  border-radius: var(--r-sm);
}
.buy-section-head h2[b-7tniqv4e0p] { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: 0.02em; color: var(--fg-0); }
.buy-section-count[b-7tniqv4e0p] { margin-left: auto; font-size: 11px; letter-spacing: 0.14em; color: var(--fg-3); }

.buy-grid[b-7tniqv4e0p] {
  display: grid;
  /* Cap auto-fit at 4 columns: when (100% - 3*gap) / 4 exceeds 380px the
     effective min width forces only 4 tracks to fit, regardless of viewport. */
  grid-template-columns: repeat(auto-fit, minmax(max(380px, calc((100% - 3 * 14px) / 4)), 1fr));
  gap: 14px;
}
@media (max-width: 960px) {
  .buy-grid[b-7tniqv4e0p] { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .buy-grid[b-7tniqv4e0p] { grid-template-columns: 1fr; }
}
/* /Components/Pages/Catalog.razor.rz.scp.css */
/* ── Catalog page (Catalog.razor) ───────────────────────────────
   Top-level page styles for the catalog at `/`. Covers the
   per-faction accent scope wrapper, the faction bar + scroll-snap
   strip, the compare-mode banner, the topbar ROSTER toggle, the
   mobile `.ident-app` grid collapse, and a small skeleton-only
   subset of `.sheet`/`.role-cluster`/`.units` for the loading
   state before IdentSheet renders.

   The shared `.coll-counts`, `.iconbtn`, and `.nav-cta` primitives
   rendered inside PageHeader's controls slot live in app.css so
   Products and Buy can render them too. */

/* Per-faction accent scope — wraps the catalog UI plus its sibling drawer / modals / rail.
   The wrapper sets --accent-hue inline (style="--accent-hue: <faction>"); we redeclare the
   accent family here so the var(--accent-hue) substitutions inside --accent / --accent-dim /
   --accent-line / --accent-soft / --accent-fg re-resolve against the per-faction hue rather
   than inheriting the resolved cyan values from :root. Same pattern as .products-section
   on the Products page and .sculpt on ProductCard. Custom-property cascade works through
   scope boundaries, so descendant components (drawer, rail, sidebar, sheet) pick up the
   per-faction values without needing ::deep. */
.accent-scope[b-uas05varca] {
  --accent:      oklch(0.78 0.14 var(--accent-hue));
  --accent-dim:  oklch(0.78 0.14 var(--accent-hue) / 0.18);
  --accent-line: oklch(0.78 0.14 var(--accent-hue) / 0.45);
  --accent-soft: oklch(0.78 0.14 var(--accent-hue) / 0.55);
  --accent-fg:   oklch(0.85 0.16 var(--accent-hue));
}

/* When the filters drawer becomes off-canvas at <=960px, the .ident-app grid
   collapses from "sidebar | main" to a single column. .ident-app itself is
   rendered by Catalog.razor (and Products.razor, which sets its own override
   via `.ident-app.products-page { grid-template-columns: 1fr }`). */
@media (max-width: 960px) {
  .ident-app[b-uas05varca] { grid-template-columns: 1fr; }
}

/* ── Faction bar ──────────────────────────────────────────────── */
/* --faction-bar-h is this bar's rendered height, exposed for the same
   reason --topbar-h is: the compare-mode-banner stacks under both bars and
   needs to derive its sticky offset from their combined height rather than
   carrying a third magic number. Defined here on .ident-main (Catalog's
   scroll container) since the var is Catalog-specific — Products has its
   own secondary bar (.products-filters) with a different rendered height
   and no third-tier sticky element to compose with. */
.ident-main[b-uas05varca] { --faction-bar-h: 43px; }
@media (max-width: 600px) {
  /* Anchor padding grows from 6/10 to 10/14 at this breakpoint (touch
     target — see .faction-switcher a block below). 8 + (10+text+10)
     + 8 + 1 border ≈ 51px. */
  .ident-main[b-uas05varca] { --faction-bar-h: 51px; }
}
.faction-bar[b-uas05varca] {
  position:sticky; top:var(--topbar-h); z-index:9;
  background:oklch(from var(--bg-0) l c h / 0.92);
  backdrop-filter:blur(8px); border-bottom:1px solid var(--line-1);
  padding:8px var(--page-gutter); display:flex; align-items:center; gap:12px; flex-shrink:0;
}
.faction-bar .label[b-uas05varca] { font-family:var(--ff-mono); font-size:10px; letter-spacing:0.18em; color:var(--fg-3); border-right:1px solid var(--line-1); padding-right:12px; }
.faction-switcher[b-uas05varca] { display:flex; gap:2px; flex:1; flex-wrap:wrap; }
.faction-switcher a[b-uas05varca] {
  --fh: 220; display:flex; align-items:baseline; gap:8px; padding:6px 10px;
  border-radius:var(--r-sm); white-space:nowrap; border:1px solid transparent;
  background:var(--bg-1); transition:background .12s,border-color .12s;
  text-decoration:none; color:inherit;
}
.faction-switcher a .fcode[b-uas05varca] { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.16em; color:oklch(0.78 0.14 var(--fh)); opacity:.7; }
.faction-switcher a .fname[b-uas05varca] { font-size:12px; letter-spacing:0.04em; color:var(--fg-2); }
.faction-switcher a:hover[b-uas05varca] { background:var(--bg-2); border-color:var(--line-1); }
.faction-switcher a:hover .fname[b-uas05varca] { color:var(--fg-0); }
.faction-switcher a.is-active[b-uas05varca] { background:oklch(0.78 0.14 var(--fh) / 0.18); border-color:oklch(0.78 0.14 var(--fh) / 0.55); }
.faction-switcher a.is-active .fcode[b-uas05varca] { opacity:1; }
.faction-switcher a.is-active .fname[b-uas05varca] { color:var(--fg-0); }

/* ── Faction-bar scroll-snap strip (phone) ──
   At <=600px the wrap behavior (4-5 rows on a 14-faction list) becomes
   too much chrome. Single horizontal scroll strip with snap; the .label
   prefix ("FACTION") hides as the strip is self-evident. The active
   button gets scroll-margin-inline-start so existing C# .scrollIntoView()
   calls land it at the gutter rather than flush against the viewport edge. */
@media (max-width: 600px) {
  .faction-bar[b-uas05varca] { gap: 0; }
  .faction-bar .label[b-uas05varca] { display: none; }
  .faction-switcher[b-uas05varca] {
    flex-wrap: nowrap;
    overflow-x: auto;
    /* min-width: 0 is load-bearing: without it, the flex item's default
       `min-width: auto` resolves to the natural width of its nowrap content,
       which defeats overflow-x: auto and lets the strip push the page wider
       than the viewport. */
    min-width: 0;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    /* Hide the scrollbar visually but keep keyboard scrollability. */
    scrollbar-width: none;
  }
  .faction-switcher[b-uas05varca]::-webkit-scrollbar { display: none; }
  .faction-switcher a[b-uas05varca] {
    scroll-snap-align: start;
    scroll-margin-inline-start: var(--page-gutter);
    flex: 0 0 auto;
    padding: 10px 14px;   /* up from 6px 10px — touch target */
  }

  /* Scroll affordance — soft fade on BOTH edges so users see the strip is
     horizontally scrollable. With the scrollbar hidden, these are the
     only signals that there's more content offscreen.

     Fades sit on .faction-bar (sticky parent) rather than inside
     .faction-switcher (which scrolls) so they stay pinned to the viewport
     edges as content scrolls under them. pointer-events: none keeps
     them from blocking taps on the edge buttons. At scroll-start the
     left fade lands mostly over the bar's page-gutter padding (16px) so
     only a sliver overlaps the first button — readable, signals "more
     here too". Same logic at scroll-end on the right. */
  .faction-bar[b-uas05varca]::before,
  .faction-bar[b-uas05varca]::after {
    content: "";
    position: absolute;
    top: 0; bottom: 0;
    width: 32px;
    pointer-events: none;
  }
  .faction-bar[b-uas05varca]::before {
    left: 0;
    background: linear-gradient(to right,
      var(--bg-0),
      oklch(from var(--bg-0) l c h / 0));
  }
  .faction-bar[b-uas05varca]::after {
    right: 0;
    background: linear-gradient(to right,
      oklch(from var(--bg-0) l c h / 0),
      var(--bg-0));
  }
}

/* ── Compare-mode banner ──────────────────────────────────────── */
/* Sticks below the topbar + faction-bar so it stays visible while scrolling
   — otherwise it's easy to miss that compare mode is on when toggled from
   halfway down the page. z:8 sits below faction-bar (z:9) and topbar (z:10)
   so they cleanly stack above it. The two height vars handle every
   responsive breakpoint — no per-media-query override needed here. */
.compare-mode-banner[b-uas05varca] {
  position:sticky; top:calc(var(--topbar-h) + var(--faction-bar-h)); z-index:8;
  background:var(--accent-dim); border-bottom:1px solid var(--accent-line);
  padding:8px var(--page-gutter); font-family:var(--ff-mono); font-size:11px; letter-spacing:0.16em; color:var(--fg-0);
}

/* ── Topbar ROSTER button ─────────────────────────────────────── */
/* Catalog's own per-page Roster toggle, restored after the universal
   SiteHeader pill was dropped from dense pages. The button's open/
   active state is driven by the RosterDrawerState scoped service —
   shared with SiteHeader's pill on the prose pages (About / Cookie /
   NotFound) so a single state store backs both triggers. */
.roster-toggle[b-uas05varca] {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  width: auto;
}
.roster-pip[b-uas05varca] {
  background: var(--accent-dim);
  border: 1px solid var(--accent-line);
  color: var(--accent-fg);
  font-size: 11px;
  padding: 1px 5px;
  min-width: 18px;
  text-align: center;
}

/* ── Loading skeleton ─────────────────────────────────────────────
   Catalog renders `<div class="sheet" data-density="..."><div class="role-cluster">
   <div class="units">…SkeletonUnitCards…</div></div></div>` before the catalog
   snapshot resolves. The full .sheet styling (silhouette grid, role clusters,
   topbar wraps, density-driven phone reflow) lives in IdentSheet.razor.css and
   is scoped to that component — so a Catalog-rendered .sheet doesn't pick it up.
   We replicate the minimal subset the skeleton needs: outer padding, the cluster
   wrapper, the units row's flex-wrap, and the density custom properties consumed
   by SkeletonUnitCard via the cascade. */
.sheet[b-uas05varca] { padding: clamp(20px, 2.5vw, 32px); display: flex; flex-direction: column; gap: 28px; }
/* --uc-min-h kept in sync with IdentSheet.razor.css — see comment there for the
   per-density sizing rationale. */
.sheet[b-uas05varca]                    { --uc-w:140px; --uc-img-h:208px; --uc-min-h:263px; --uc-min:124px; }
.sheet[data-density="xs"][b-uas05varca] { --uc-w: 96px; --uc-img-h:144px; --uc-min-h:173px; --uc-min: 88px; }
.sheet[data-density="s"][b-uas05varca]  { --uc-w:116px; --uc-img-h:172px; --uc-min-h:227px; --uc-min:104px; }
.sheet[data-density="l"][b-uas05varca]  { --uc-w:168px; --uc-img-h:248px; --uc-min-h:303px; --uc-min:148px; }
.role-cluster[b-uas05varca] { flex: 0 0 auto; display: flex; flex-direction: column; gap: 12px; padding: 6px 0; min-width: var(--uc-min, 124px); max-width: 100%; }
.role-cluster .units[b-uas05varca] { display: flex; flex-wrap: wrap; gap: 4px; }

/* /Components/Pages/CookiePolicy.razor.rz.scp.css */
/* ── Cookie policy page (CookiePolicy.razor) ──────────────────── */

/* The html/body overflow override (needed because IdentLayout locks
   them to `overflow: hidden`) lives in a <HeadContent><style> block in
   CookiePolicy.razor. `::deep` can't reach html/body because they're
   ancestors of this component's scope, not descendants. */

.cookie-policy[b-29cj1n4pr7] {
  max-width: 760px;
  margin: 32px auto;
  padding: 0 16px;
  line-height: 1.6;
}
.cookie-policy h1[b-29cj1n4pr7] { margin-bottom: 4px; }
.cookie-policy h2[b-29cj1n4pr7] { margin-top: 28px; }
.cookie-policy .muted[b-29cj1n4pr7] { color: var(--fg-2, #999); margin-top: 0; }
.cookie-policy .policy-table[b-29cj1n4pr7] {
  width: 100%;
  border-collapse: collapse;
  margin-top: 8px;
}
.cookie-policy .policy-table th[b-29cj1n4pr7],
.cookie-policy .policy-table td[b-29cj1n4pr7] {
  text-align: left;
  padding: 8px 12px;
  border-bottom: 1px solid var(--fg-3, #333);
  vertical-align: top;
}
.cookie-policy .policy-table code[b-29cj1n4pr7] {
  font-family: var(--ff-mono, monospace);
  font-size: 0.9em;
}
@media (max-width: 600px) {
  .cookie-policy .policy-table[b-29cj1n4pr7],
  .cookie-policy .policy-table thead[b-29cj1n4pr7],
  .cookie-policy .policy-table tbody[b-29cj1n4pr7],
  .cookie-policy .policy-table tr[b-29cj1n4pr7],
  .cookie-policy .policy-table td[b-29cj1n4pr7] { display: block; width: 100%; }
  .cookie-policy .policy-table thead[b-29cj1n4pr7] { display: none; }
  .cookie-policy .policy-table tr[b-29cj1n4pr7] {
    border: 1px solid var(--fg-3, #333);
    border-radius: var(--r-sm, 2px);
    margin-bottom: 12px;
    padding: 8px 12px;
    background: var(--bg-1, #1a1a1a);
  }
  .cookie-policy .policy-table td[b-29cj1n4pr7] {
    border: 0;
    padding: 6px 0;
    display: grid;
    grid-template-columns: 110px 1fr;
    gap: 8px;
    align-items: baseline;
  }
  .cookie-policy .policy-table td[b-29cj1n4pr7]::before {
    content: attr(data-label) " \00b7";
    font-family: var(--ff-mono, monospace);
    font-size: 11px;
    letter-spacing: 0.14em;
    color: var(--fg-3, #888);
    text-transform: uppercase;
  }
}
/* /Components/Pages/NotFound.razor.rz.scp.css */
.notfound-page[b-j6ci8kzniv] {
    max-width: 480px;
    margin: 0 auto;
    padding: 80px 24px;
    color: var(--fg-1);
    text-align: center;
}
.notfound-page h1[b-j6ci8kzniv] {
    font-size: 32px;
    margin: 0 0 16px 0;
    letter-spacing: -0.5px;
}
.notfound-page p[b-j6ci8kzniv] {
    color: var(--fg-2);
    margin: 8px 0;
}
.notfound-links[b-j6ci8kzniv], .notfound-factions[b-j6ci8kzniv] {
    list-style: none;
    padding: 0;
    margin: 16px 0;
    display: flex;
    gap: 16px;
    justify-content: center;
    flex-wrap: wrap;
}
.notfound-links a[b-j6ci8kzniv], .notfound-factions a[b-j6ci8kzniv] {
    color: oklch(0.7 0.15 220);
    text-decoration: none;
    padding: 8px 12px;
    border: 1px solid var(--bg-3);
    border-radius: 4px;
}
.notfound-links a:hover[b-j6ci8kzniv], .notfound-factions a:hover[b-j6ci8kzniv] {
    border-color: var(--fg-3);
}
.notfound-section-label[b-j6ci8kzniv] {
    margin-top: 32px;
    font-size: 12px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--fg-3);
}
/* /Components/Pages/Products.razor.rz.scp.css */
/* ── Products page (Products.razor) ─────────────────────────────
   Top-level page styles for /products. Hosts the filter chip rows,
   the hero strip ("N of M wishlisted available"), and the
   faction-grouped product grid.

   ProductCard internals (`.product`, `.p-*`, `.vp-*`, `.sculpt-*`,
   `.toggle`, `.cta-primary`/`.cta-secondary`) live in
   ProductCard.razor.css — descendants of `.products-section` pick
   up the section's `--accent-hue` via the custom-property cascade
   without ::deep. */

/* No sidebar on this page — collapse the .ident-app grid to a single column. */
.ident-app.products-page[b-1zf76qgxke] { grid-template-columns: 1fr; }
/* The pre-SiteFooter 60px padding-bottom under the product grid is gone:
   .ident-main is a flex column and <SiteFooter /> sits at the bottom with
   its own top-border + 10px padding, which is the breathing room now. A
   non-zero padding-bottom here would push the (margin-top:auto-anchored)
   footer up off the container's bottom edge — exactly the bug being fixed. */

.products-filters[b-1zf76qgxke] {
  position: sticky; top: var(--topbar-h); z-index: 9;
  display: flex; flex-direction: column; gap: 10px;
  background: oklch(from var(--bg-0) l c h / 0.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line-1);
  padding: 14px 32px;
}
.products-filter-row[b-1zf76qgxke] {
  display: flex; align-items: center; gap: 12px;
  font-size: 13px; color: var(--fg-1);
}
/* Fixed-width label column so the chips on FACTION / STATUS / SHOW rows all
   start at the same x — the labels themselves vary in width ("FACTION" is
   wider than "SHOW"), which made the chip rows look subtly crooked when each
   row's chips slid up against its own label. 70px clears the widest current
   label ("FACTION" at ~63px with the mono font) with a few pixels of slack. */
.products-filter-row > .admin-label[b-1zf76qgxke] { min-width: 70px; }
.products-faction-chips[b-1zf76qgxke] { display: flex; flex-wrap: wrap; gap: 4px; }
.products-faction-chip[b-1zf76qgxke] {
  /* Tighter, blockier mono per the design pass — 600 weight + 10px gives the
     chip text more "data-readout" presence than the prior 11px/400; sharper
     corners (border-radius 0) match the rest of the page's HUD chrome. */
  font: 600 10px/1 var(--ff-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 4px 10px;
  border: 1px solid var(--line-1);
  background: var(--bg-1);
  color: var(--fg-2);
  cursor: pointer;
  border-radius: 0;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.products-faction-chip:hover[b-1zf76qgxke] { color: var(--fg-0); border-color: var(--line-2); }
.products-faction-chip.is-active[b-1zf76qgxke] {
  border-color: oklch(70% 0.18 var(--fh, var(--accent-hue)));
  background: oklch(70% 0.18 var(--fh, var(--accent-hue)) / 0.16);
  color: var(--fg-0);
}

.products-section[b-1zf76qgxke] {
  padding: 24px 32px;
  border-top: 1px solid var(--line-1);
  /* Re-tie the accent family to the locally-overridden --accent-hue. The :root-level
     --accent / --accent-dim / --accent-line have their var(--accent-hue) substitutions
     resolved at :root and inherit as fixed colors; redefining them here forces them to
     re-resolve against the section's faction hue. Without these lines, the CTA / toggle
     / sculpt-tag colors stay cyan no matter which faction's section the card lives in.
     Custom properties cascade across CSS-isolation scope boundaries, so ProductCard
     descendants pick up the section's per-faction accent values. */
  --accent-hue:  var(--fh, 220);
  --accent:      oklch(0.78 0.14 var(--accent-hue));
  --accent-dim:  oklch(0.78 0.14 var(--accent-hue) / 0.18);
  --accent-line: oklch(0.78 0.14 var(--accent-hue) / 0.45);
  --accent-soft: oklch(0.78 0.14 var(--accent-hue) / 0.55);
  --accent-fg:   oklch(0.85 0.16 var(--accent-hue));
}
.products-section:first-of-type[b-1zf76qgxke] { border-top: 0; }
.products-section-head[b-1zf76qgxke] {
  display: flex; align-items: baseline; gap: 12px;
  margin-bottom: 16px;
}
.products-section-code[b-1zf76qgxke] {
  font-size: 11px; letter-spacing: 0.18em;
  padding: 3px 10px;
  border: 1px solid oklch(70% 0.18 var(--fh, var(--accent-hue)));
  background: oklch(70% 0.18 var(--fh, var(--accent-hue)) / 0.18);
  color: oklch(85% 0.16 var(--fh, var(--accent-hue)));
  border-radius: var(--r-sm);
}
.products-section-head h2[b-1zf76qgxke] { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: 0.02em; color: var(--fg-0); }
.products-section-count[b-1zf76qgxke] { margin-left: auto; font-size: 11px; letter-spacing: 0.14em; color: var(--fg-3); }

.products-grid[b-1zf76qgxke] {
  display: grid;
  /* Cap auto-fit at 4 columns: when (100% - 3*gap) / 4 exceeds 380px the
     effective min width forces only 4 tracks to fit, regardless of viewport. */
  grid-template-columns: repeat(auto-fit, minmax(max(380px, calc((100% - 3 * 14px) / 4)), 1fr));
  gap: 14px;
}
/* ── Products grid breakpoints ──
   The desktop auto-fit minmax(380px, 1fr) collapses to 1-column below
   ~400px naturally, but explicit rules at 960px / 600px make intent
   clear and let us tune the cliffs independently of the 380px min. */
@media (max-width: 960px) {
  .products-grid[b-1zf76qgxke] { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .products-grid[b-1zf76qgxke] { grid-template-columns: 1fr; }
}

/* ── Page hero strip (above the faction sections) ───────────────────── */
.hero-strip[b-1zf76qgxke] {
  margin: 28px clamp(20px, 3vw, 36px) 0;
  padding: 18px 22px;
  background: linear-gradient(90deg,
    oklch(0.78 0.14 60 / 0.08) 0%,
    oklch(0.78 0.14 60 / 0.02) 50%,
    transparent 100%);
  border-left: 2px solid var(--want);
  display: flex;
  align-items: center;
  gap: 22px;
}
.hero-num[b-1zf76qgxke] {
  font-family: var(--ff-mono);
  font-weight: 600;
  font-size: 36px;
  color: var(--want);
  line-height: 1;
  letter-spacing: -0.02em;
}
.hero-text[b-1zf76qgxke] { font-size: 14px; line-height: 1.5; color: var(--fg-1); }
.hero-text strong[b-1zf76qgxke] { color: var(--fg-0); font-weight: 600; }
.hero-text small[b-1zf76qgxke] {
  display: block;
  margin-top: 4px;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-3);
}
/* /Components/Pages/WhatsNew.razor.rz.scp.css */
/* ── /whatsnew page (WhatsNew.razor) ────────────────────────────────────
   A centered, padded grid of ProductCards — the same auto-fit / 380px-min /
   14px-gap layout the /products faction grid uses (Products.razor.css), so a
   card looks identical on both surfaces. Unlike /products this page has no
   .ident-app shell, so the grid owns its own outer padding + max-width and
   centers in the viewport (matching the About / CookiePolicy content frame).

   --accent-hue is set faction-neutral (220) here because the page mixes boxes
   from many factions; ProductCard's per-sculpt hues still override at the
   sculpt level via the cascade across the isolation boundary. */

.whatsnew-grid[b-y57tojadlr] {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px clamp(16px, 3vw, 32px) 48px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(max(380px, calc((100% - 3 * 14px) / 4)), 1fr));
  gap: 14px;
  --accent-hue: 220;
}

@media (max-width: 960px) {
  .whatsnew-grid[b-y57tojadlr] { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .whatsnew-grid[b-y57tojadlr] { grid-template-columns: 1fr; }
}
