/* ============================================================
   IDENT // Infinity miniature identification dashboard
   ============================================================ */

/* ───── Self-hosted webfonts ─────
   These were previously loaded from fonts.googleapis.com / fonts.gstatic.com,
   which transmitted every visitor's IP to Google on first paint — before any
   consent banner could be acknowledged. Under EU rules that's a third-party
   transfer that requires prior consent (see LG München, 20 Jan 2022 re:
   Google Fonts). Both families are OFL-licensed, so self-hosting is allowed.
   Files in /wwwroot/fonts/ are static woff2 from Google's CDN with the latin
   subset only (≈54 KB total) — enough for English catalog content.
   font-display: swap mirrors what Google's CSS API requested. */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/space-grotesk-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2');
}

:root {
  --bg-0: oklch(0.16 0.012 240);
  --bg-1: oklch(0.20 0.012 240);
  --bg-2: oklch(0.24 0.014 240);
  --bg-3: oklch(0.28 0.016 240);
  --fg-0: oklch(0.96 0.005 240);
  --fg-1: oklch(0.82 0.008 240);
  --fg-2: oklch(0.62 0.010 240);
  --fg-3: oklch(0.46 0.012 240);
  --line-1: oklch(0.32 0.014 240);
  --line-2: oklch(0.40 0.016 240);
  --accent-hue: 220;
  --accent: oklch(0.78 0.14 var(--accent-hue));
  --accent-fg: oklch(0.85 0.16 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);
  /* Want = wishlist orange. Same hue family as --warn but a separate name because
     they mean different things (warn = OOP / production status; want = wishlist match). */
  --want:      oklch(0.78 0.14 60);
  --want-line: oklch(0.78 0.14 60 / 0.45);
  --want-dim:  oklch(0.78 0.14 60 / 0.14);
  /* Good = active production status (green). */
  --good:      oklch(0.78 0.14 150);
  --good-line: oklch(0.78 0.14 150 / 0.45);
  --good-dim:  oklch(0.78 0.14 150 / 0.14);
  --warn: oklch(0.78 0.14 60);
  --warn-dim: oklch(0.78 0.14 60 / 0.14);
  /* Bad = destructive action red. Used by ConfirmDialog's destructive variant. */
  --bad:      oklch(0.78 0.14 30);
  --bad-line: oklch(0.78 0.14 30 / 0.45);
  --bad-dim:  oklch(0.78 0.14 30 / 0.14);
  --ff-sans: "Space Grotesk", system-ui, sans-serif;
  --ff-mono: "JetBrains Mono", ui-monospace, monospace;
  --r-sm: 2px;
  --r-md: 4px;
  --r-lg: 8px;
  /* ── Responsive scale ──
     --page-gutter is the horizontal padding applied to every top-level
     region that sits flush with the viewport edge (topbar, faction-bar,
     compare-mode-banner, sheet). Using one token keeps the page's left
     edge perfectly aligned across those regions at every viewport width.
     The clamp tightens the gutter continuously below ~1280px down to a
     16px floor that is already mobile-friendly.

     --bp-narrow is the discrete breakpoint at which structural changes
     kick in (today: the topbar title text-collapse from "UNIT
     IDENTIFICATION" to "IDENT"). CSS @media rules can't read custom
     properties, so the literal 1180px is repeated in each media query
     that uses it — keep them aligned with the value below. */
  --page-gutter: clamp(16px, 2.5vw, 32px);
  /* Discrete responsive cliffs — comment-only because CSS @media rules can't
     read custom properties. Each literal repeats in the media queries that
     reference it; keep them aligned with the values listed here.
       --bp-narrow:  1180px  (today: topbar title collapse to "IDENT")
       --bp-tablet:   960px  (sidebar collapses to off-canvas filters drawer)
       --bp-mobile:   600px  (touch-first restructure: bottom-sheet roster,
                              fullscreen detail drawer, single-column cards,
                              hover -> tap, enlarged touch targets) */

  /* WCAG 2.5.5 target-size minimum. Consumed only inside
     `@media (max-width: 600px)` blocks where touch UX matters. */
  --touch-min: 44px;

  /* Height of the SiteHeader strip when it IS rendered (prose pages —
     About / CookiePolicy / NotFound). PageHeader's sticky offset reads
     this; SiteHeader's own `height` reads it too. Defined here on :root
     so it's visible to both — putting it inside .site-header's scoped
     CSS would isolate it from PageHeader's sibling scope. Dense pages
     (Catalog / Products / Buy) DO NOT render SiteHeader; they override
     this to 0 on .ident-app so PageHeader sticks at the top of the
     scroll container rather than leaving a 44px gap. */
  --site-header-h: 44px;
  /* Rendered height of the PageHeader strip (min-height: 38px in
     PageHeader.razor.css). Exposed on :root so the composite
     --topbar-h below can refer to it without crossing scope boundaries. */
  --page-header-h: 38px;
  /* Composite offset used by per-page secondary sticky bars
     (.faction-bar on Catalog, .products-filters on Products,
     .buy-filters on Buy, .compare-mode-banner stacked under them).
     These bars sit directly below the page chrome.
     - On prose pages (About / CookiePolicy / NotFound) the chrome is
       SiteHeader (44px) + PageHeader (38px), so this :root composite
       gives 82px.
     - On dense pages (Catalog / Products / Buy) .ident-app overrides
       --topbar-h to 53px (the rendered .topbar height), with
       tablet/phone bumps in the @media rules further down. */
  --topbar-h: calc(var(--site-header-h) + var(--page-header-h));
}

/* ───── Reduced-motion override ─────
   Disables every animation and transition site-wide when the OS / browser
   sets prefers-reduced-motion: reduce. ~0ms duration rather than 0ms so any
   logic relying on transitionend events still fires. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ───── Focus visibility — base rule ─────
   Native interactive elements get the same 2px accent outline at 2px offset
   as the existing .toggle / .cta-primary / .cta-secondary primitives.
   Custom interactive elements (with role="button" or @onclick) and
   component-specific styles should follow the same pattern via the
   utility-class block below. */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role="button"]:focus-visible,
[role="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* <FocusOnNavigate Selector="h1"> (Routes.razor) moves focus to the page heading
   on every navigation/refresh for screen-reader orientation. It does this by
   adding tabindex="-1" and calling .focus() — which, on initial page load,
   trips Chrome's :focus-visible heuristic and paints the browser's default
   blue outline around the H1 for a half-second until the interactive circuit
   attaches and focus is lost. Suppress the visual ring on programmatically-
   focused headings; the focus itself (and the a11y benefit) is preserved. */
:is(h1, h2, h3, h4, h5, h6)[tabindex="-1"]:focus { outline: none; }

/* ───── Skeleton placeholder ───── */
.skeleton {
  background: var(--bg-2);
  border-radius: var(--r-sm);
  animation: skeletonPulse 1.5s ease-in-out infinite;
}
@keyframes skeletonPulse {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 0.80; }
}

/* ───── Skeleton card variants ─────
   Per-card dimensions moved to SkeletonUnitCard.razor.css /
   SkeletonProductCard.razor.css. The shared .skeleton base + keyframe
   above remain here. */

/* ───── State-swap fade-in ───── */
.fade-in { animation: fadeIn 180ms ease-out; }
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Per-faction accent scope (.accent-scope) moved to Catalog.razor.css.
   The .products-section per-faction re-tie moved to Products.razor.css.
   The .sculpt per-sculpt re-tie lives in ProductCard.razor.css. */

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg-0);
  color: var(--fg-0);
  font-family: var(--ff-sans);
  font-size: 14px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  overflow-x: hidden;
}
button { font-family: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
input { font-family: inherit; }
.mono { font-family: var(--ff-mono); letter-spacing: 0.02em; }

/* ───── Scrollbars ─────
   Firefox paints thin, auto-hiding scrollbars on its own (it honors the OS
   "automatically hide scrollbars" setting), so we leave it alone: the
   ::-webkit-scrollbar rules below are silently dropped by Firefox. They only
   affect Chromium / WebKit (Chrome / Edge / Safari), which on most setups
   show a chunky always-on scrollbar instead.

   Caveat: defining any ::-webkit-scrollbar rule opts the element out of
   Chromium's native overlay scrollbars (on configs that have them) into a
   custom one that reserves a gutter — true zero-width auto-hide isn't
   achievable here. We keep the gutter thin and the track transparent, with a
   thumb that's invisible until the pointer is over the scrollable area, then
   shows dim and brightens when hovered directly. The colour change is instant
   — WebKit doesn't transition scrollbar pseudo-elements. */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  /* 2px transparent border + padding-box clip insets the visible thumb to
     ~6px, so it reads as thin even though the gutter is 10px wide. */
  background-color: transparent;
  border: 2px solid transparent;
  background-clip: padding-box;
  border-radius: 6px;
}
/* Equal specificity — the direct-thumb hover must stay last to win. */
:hover::-webkit-scrollbar-thumb { background-color: var(--line-2); }
::-webkit-scrollbar-thumb:hover { background-color: var(--fg-3); }

/* ── IDENT app shell ──
   `.ident-app` + `.ident-main` are rendered by Catalog, Products and
   Buy (three sibling renderers) — neither IdentLayout nor any single
   page owns them. IdentLayout itself has no wrapper element (it renders
   <HeadContent>, @Body, and the blazor-error-ui div as siblings), so a
   scoped `::deep .ident-app` from IdentLayout.razor.css would not
   resolve — the layout's b-attribute lands on the error UI div, which
   is a sibling of the page root, not an ancestor. Shared primitive
   in app.css instead.
   IdentSidebar styles moved to IdentSidebar.razor.css.
   FiltersDrawer wrapper + scrim moved to FiltersDrawer.razor.css. */
.ident-app {
  /* Dense pages (Catalog / Products / Buy) do NOT render <SiteHeader />
     above their content — the per-page .topbar is the topmost sticky
     element inside .ident-main's scroll container. Override
     --site-header-h to 0 here so any nested calc(... - var(--site-header-h))
     resolves correctly without us having to special-case each rule. The
     :root default (44px) still applies on the prose pages (About /
     CookiePolicy / NotFound) where <SiteHeader /> IS rendered.
     --topbar-h pins to the rendered .topbar height (53px on desktop;
     bumped to 93/89px on tablet/phone wrap — see @media rules below)
     so the per-page secondary sticky bars (.faction-bar on Catalog,
     .products-filters on Products, .buy-filters on Buy,
     .compare-mode-banner stacked under them) land flush against it. */
  --site-header-h: 0px;
  --topbar-h: 53px;
  min-height: 100vh;
  display: grid;
  grid-template-columns: 240px 1fr;
}

/* The `.ident-app` mobile grid-collapse (@media max-width: 960px) lives in
   Catalog.razor.css; Products.razor.css sets its own
   `.ident-app.products-page { grid-template-columns: 1fr }` override. */

/* ── Main panel ── */
/* IdentLayout's <HeadContent> pins html/body to height:100% with overflow:hidden,
   so .ident-main needs a fixed-height scroll surface inside the locked viewport.
   On dense pages there is no <SiteHeader /> above .ident-app, so the calc
   subtraction below resolves to 100vh - 0 = 100vh via the --site-header-h: 0
   override declared on .ident-app. The fallback on var() handles the rare
   initial paint before the override has cascaded. */
.ident-main { padding:0; min-width:0; display:flex; flex-direction:column; height:calc(100vh - var(--site-header-h, 0px)); overflow-y:auto; }

/* The .topbar primitive is shared by Catalog, Products and Buy — all
   three pages render the same topbar layout. Stays in app.css for that
   reason rather than getting duplicated into each page's scoped CSS.
   Catalog-only topbar children (.roster-toggle, .roster-pip) are in
   Catalog.razor.css. About / CookiePolicy / NotFound do NOT use this
   primitive — they render <PageHeader> as a thin H1 strip below
   SiteHeader instead. */
.topbar {
  position:sticky; top:0; z-index:10;
  background:oklch(from var(--bg-0) l c h / 0.92);
  backdrop-filter:blur(8px); border-bottom:1px solid var(--line-1);
  padding:10px var(--page-gutter); display:flex; gap:clamp(8px, 1vw, 16px); align-items:center; flex-shrink:0;
}
.topbar-title { display:flex; align-items:baseline; gap:12px; }
.topbar-title h1 { margin:0; font-size:20px; font-weight:600; letter-spacing:0.04em; text-transform: uppercase; }
.topbar-title .sub { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.14em; color:var(--fg-3); }
.spacer { flex:1; }

.iconbtn {
  width:32px; height:32px; border:1px solid var(--line-1); border-radius:var(--r-sm);
  display:grid; place-items:center; color:var(--fg-1); background:var(--bg-1);
  flex-shrink: 0;
}
.iconbtn:hover { border-color:var(--accent-line); color:var(--fg-0); }
.iconbtn[data-active="True"], .iconbtn.is-active { background:var(--accent-dim); border-color:var(--accent-line); color:var(--fg-0); }

/* ── Cross-page nav CTA ──
   Filled-accent button promoting the catalog's primary cross-page nav
   action (→ PRODUCTS from the catalog page header). Stays cyan
   (--accent-hue pinned to 220) so the "leaves this surface" signal
   looks the same regardless of which faction's accent the rest of the
   page is wearing — chrome that means the same thing on every page
   should look the same on every page. Rendered inside PageHeader's
   controls slot. */
.nav-cta {
  --accent-hue: 220;
  --accent: oklch(0.78 0.14 var(--accent-hue));
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 14px;
  font-family: var(--ff-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: oklch(0.10 0.012 240);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--r-sm);
  text-decoration: none;
  box-shadow:
    0 0 0 0.5px var(--accent),
    0 8px 24px -8px oklch(0.78 0.14 var(--accent-hue) / 0.5);
  transition: transform 120ms ease, box-shadow 120ms ease;
  flex-shrink: 0;
}
.nav-cta:hover {
  transform: translateY(-1px);
  box-shadow:
    0 0 0 0.5px var(--accent),
    0 12px 32px -8px oklch(0.78 0.14 var(--accent-hue) / 0.65);
}
.nav-cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.nav-cta .arrow { font-size: 13px; font-weight: 400; }

/* ── Topbar at tablet width ──
   Below 960px the topbar wraps to two rows: search bar drops to its own
   full-width row via order:99 + flex:1 1 100%; row-gap separates the
   stacked rows. coll-counts hide here to avoid double-reporting against
   the collection counters (the same data lives inside the sidebar /
   filters drawer). */
@media (max-width: 960px) {
  .topbar { flex-wrap: wrap; row-gap: 8px; }
  .topbar .gsearch { flex: 1 1 100%; width: auto; order: 99; }
  .topbar .coll-counts { display: none; }
  /* Two rows now: 10 padding-top + 32 iconbtn + 8 row-gap + 32 gsearch + 10
     padding-bottom + 1 border ≈ 93px. Bumps any element using --topbar-h
     (faction-bar, products-filters, compare-mode-banner) accordingly. */
  .ident-app { --topbar-h: 93px; }
}

/* ── Topbar at phone width ──
   At <=600px additionally: title block hides entirely (FILTERS button is now
   the first-row anchor), search remains its own row, density-toggle hides
   (single-density on a 2-col phone card grid — see .sheet @media below),
   topbar bottom-padding tightens. The two remaining row-3 buttons (COMPARE,
   PRODUCTS) flow naturally via the wrap.

   Touch targets — selective enlargement to meet WCAG 2.5.5 (44x44 min).
   Sidebar filter rows / faction-switcher buttons are handled in their
   own @media blocks; here we cover the topbar surface. */
@media (max-width: 600px) {
  /* justify-content: space-between anchors FILTERS to the left edge and the
     rightmost button (PRODUCTS) to the right edge of the topbar, matching
     the full-width search bar on the row below. Without this the buttons
     pack left and the row looks narrower than the search, which reads as
     misaligned. The wrapped search at order:99 has flex:1 1 100% so it
     remains its own row regardless. */
  .topbar {
    padding-top: 8px;
    padding-bottom: 8px;
    justify-content: space-between;
  }
  .topbar-title { display: none; }
  .topbar .density-toggle { display: none; }
  /* .spacer becomes a no-op — wrap handles spacing instead of a flex-grow gap. */
  .topbar .spacer { display: none; }

  /* Keep min-width at --touch-min for comfortable horizontal taps, but
     leave height to .iconbtn's natural 32px. The search bar below is
     about the same height (~30px from 6px vertical padding + 13px
     line-height), so both topbar rows match without inflating the
     button row to 44px. Compromise on AAA touch-target height in favor
     of a quieter, more restrained chrome strip. */
  .iconbtn, .roster-toggle, .filters-toggle { min-width: var(--touch-min); }

  /* Phone topbar: same 2-row layout as <=960px but padding tightens to 8/8.
     8 + 32 + 8 + 32 + 8 + 1 border ≈ 89px. */
  .ident-app { --topbar-h: 89px; }
}

/* .density-toggle internal styles moved to DensityToggle.razor.css.
   The .topbar .density-toggle hide rule above stays here. The
   .sheet-density-mobile overrides moved to IdentSheet.razor.css with
   ::deep to reach into DensityToggle. */

/* Faction bar + scroll-snap strip phone variant + compare-mode-banner
   moved to Catalog.razor.css. */

/* IdentSheet styles moved to IdentSheet.razor.css.
   This includes .sheet base + the silhouette grid + .sheet-density-mobile +
   the card grid phone reflow.

   Catalog.razor renders a `<div class="sheet">` loading-state skeleton; a
   minimal subset of .sheet / .role-cluster / .units (padding + density
   custom properties + flex-wrap) is duplicated in Catalog.razor.css so the
   skeleton still lays out before IdentSheet takes over. */

/* Detail drawer (.drawer + ::backdrop, .drawer-img, .drawer-img > img,
   .drawer-head/body/block/etc., .drawer-nav, .recent-strip, .ref-chips,
   .product-card + product-card phone reflow, .product-cta*, .affiliate-disclosure,
   .spec-row, .tag-row/tag-pill, .sculpt-list/row/marker/thumb/meta/name,
   .sculpt-own/.sculpt-step/.sculpt-want, .collection-block/summary/actions,
   .coll-btn, .link-btn, .drawer.rail-shifted) moved to UnitDetailDrawer.razor.css. */

/* ── Shared primitive: status-pill ──
   Used by UnitDetailDrawer, ProductCard, and CompareModal. The .legacy
   variant is the dashed/dim treatment for retired units in the drawer
   and compare modal; the standalone .unit-legacy badge + .unit[data-legacy]
   dashed card border have moved to UnitCard.razor.css (single renderer). */
.status-pill { display:inline-flex; align-items:center; gap:6px; font-family:var(--ff-mono); font-size:11px; letter-spacing:0.16em; padding:3px 8px; border:1px solid var(--line-2); }
.status-pill.active { color:oklch(0.78 0.14 150); border-color:oklch(0.78 0.14 150 / 0.5); }
.status-pill.oop { color:var(--warn); border-color:oklch(0.78 0.14 60 / 0.5); }
/* Pre-order shares the amber --warn "coming soon" family with .oop and the
   PRE-ORDER availability badge, so the header pill reads as a future/just-launched
   state rather than a live "active" box. */
.status-pill.preorder { color:var(--warn); border-color:oklch(0.78 0.14 60 / 0.5); }
.status-pill.legacy { color:var(--fg-2); border-color:var(--line-2); border-style:dashed; }
.status-pill::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; }

/* ── Empty / stub states ── */
/* .empty-state moved to EmptyState.razor.css.
   .stub-* moved to IdentSheet.razor.css.
   .inline-error and .clear-btn stay here as free-standing global primitives. */

/* .clear-btn — "Reset filters / ← Back" link primitive used by IdentSidebar,
   IdentSheet's empty-state CTA, Products, NotFound and Error. Lives in app.css
   because it's rendered by several different components. */
.clear-btn { font-family:var(--ff-mono); font-size:11px; letter-spacing:0.06em; color:var(--fg-3); padding:4px 0; text-align:left; }
.clear-btn:hover { color:var(--accent); }

/* ───── Inline error — uses the existing --warn token family (hue 60) ───── */
.inline-error {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  background: var(--warn-dim);
  border-left: 2px solid var(--warn);
  font-size: 12px;
  color: var(--fg-1);
}
.inline-error .retry-btn {
  margin-left: auto;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--warn);
  border: 1px solid oklch(0.78 0.14 60 / 0.45);
  padding: 4px 10px;
  cursor: pointer;
}
.inline-error .retry-btn:hover { background: oklch(0.78 0.14 60 / 0.20); }

/* ── Blazor error UI ── */
#blazor-error-ui { color-scheme:light only; background:lightyellow; bottom:0; box-shadow:0 -1px 2px rgba(0,0,0,.2); box-sizing:border-box; display:none; left:0; padding:.6rem 1.25rem .7rem 1.25rem; position:fixed; width:100%; z-index:1000; }
#blazor-error-ui .dismiss { cursor:pointer; position:absolute; right:.75rem; top:.5rem; }
.blazor-error-boundary { background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem,#b32121; padding:1rem 1rem 1rem 3.7rem; color:white; }
.blazor-error-boundary::after { content:"An error has occurred." }

/* ============================================================
   Below: shared primitives kept in app.css because they have 2+
   renderers across the component tree. Single-renderer rules that
   used to live in this block (Skill/Weapon refs, legacy badge,
   roster rail, drawer collection block, drawer sculpt toggles)
   have moved to their owning component's scoped .razor.css.
   ============================================================ */

/* ── Collection counts (★/OWN/WANT readout) ──
   Rendered by Catalog, Products, and Buy inside PageHeader's controls
   slot. Lives in app.css because it has multiple renderers. */
.coll-counts {
  display: flex; gap: 8px;
  flex-shrink: 0;
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.14em;
  color: var(--fg-3);
  padding: 0 12px;
  border-left: 1px solid var(--line-1);
  border-right: 1px solid var(--line-1);
  margin: 0 4px;
  height: 28px;
  align-items: center;
}
.coll-counts span {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-width: 48px;
  line-height: 1;
}
.coll-counts span strong { color: var(--accent); font-weight: 500; }
.coll-counts .fav strong { color: oklch(0.82 0.13 75); }
.coll-counts .wnt strong { color: var(--warn); }

/* GlobalSearch styles moved to GlobalSearch.razor.css. */

/* .ref-chips container + .drawer-nav + .recent-strip / .recent-pill
   moved to UnitDetailDrawer.razor.css (the drawer is the only renderer). */

/* Roster rail (.roster-rail, .rr-head, .rr-title, .rr-close, .rr-name*,
   .rr-picker*, .rr-empty*, .rr-list, .rr-item*, .rr-count, .rr-uname*,
   .rr-meta*, .rr-foot, .rr-totals, .rr-actions, .rr-btn, drag-over states,
   bottom-sheet phone variant) moved to RosterRail.razor.css.
   `.drawer.rail-shifted` lives in UnitDetailDrawer.razor.css. (The
   SiteHeader ROSTER pill and its CSS were removed along with the pill.) */

/* ── Shared roster primitive: .rr-mini ──
   The small +/− button rendered by both RosterRail and RosterEditor.
   Stays in app.css because it has 2+ renderers (same rule as .clear-btn,
   .status-pill). The mobile touch-target growth
   below covers both `.rr-close` (rail header) and `.rr-mini` — they're
   shared primitives between RosterRail and RosterEditor, so the @media
   rule has to be unscoped for both renderers to pick it up. */
.rr-mini {
  background: var(--bg-0);
  border: 1px solid var(--line-1);
  color: var(--fg-1);
  width: 22px; height: 22px;
  cursor: pointer;
  font-size: 13px; line-height: 1;
}
.rr-mini:hover { color: var(--fg-0); border-color: var(--accent-line); }

/* Mobile touch-target enlargement. Lives here (not in RosterRail.razor.css)
   because .rr-mini is rendered by RosterEditor too — scoping the rule to
   RosterRail's component would leave RosterEditor's +/- buttons at 22x22. */
@media (max-width: 600px) {
  .rr-close, .rr-mini { width: 32px; height: 32px; font-size: 14px; }
}

/* ── Roster editor overlay shell (shared by RosterEditor + SharedRosterPreview) ─
   `.roster-editor`, `.re-shell`, `.re-head`, `.re-name`, `.re-edit-hint`,
   `.re-close`, `.re-body`, `.re-list-wrap`, `.re-empty`, `.re-entries`,
   `.re-entry*`, `.re-idx`, `.re-uname*`, `.re-usub`, `.re-fcode`,
   `.re-count-num`, `.re-instances*`, `.re-instance`, `.re-inst-num`,
   `.re-action` are rendered by both RosterEditor.razor and
   SharedRosterPreview.razor; they live here as shared primitives.
   Editor-unique extras (.re-toast, .re-side, .re-stat-*, .re-spacer,
   .re-name-input, .re-picker*, .re-count-ctl, .re-remove-line) live in
   RosterEditor.razor.css. */
.roster-editor {
  position: fixed; inset: 0;
  background: transparent;
  z-index: 60;
  display: grid; place-items: center;
  /* Native <dialog> resets: fill the viewport so .re-shell centers via place-items. */
  width: 100%; height: 100%;
  max-width: none; max-height: none;
  border: none; padding: 0;
  color: inherit;
}
.roster-editor::backdrop {
  background: rgba(0,0,0,0.7);
}
.re-shell {
  width: min(1100px, 95vw);
  height: min(800px, 92vh);
  background: var(--bg-1);
  border: 1px solid var(--line-2);
  display: flex; flex-direction: column;
  position: relative;   /* anchors absolutely-positioned children like .re-toast */
}
.re-head {
  display: flex; align-items: center; gap: 16px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--line-1);
}
.re-crumbs { font-size: 10px; letter-spacing: 0.18em; color: var(--fg-2); }
.re-name { font-size: 22px; margin: 0; cursor: pointer; color: var(--fg-0); }
.re-edit-hint { font-size: 12px; color: var(--fg-2); margin-left: 6px; opacity: 0.5; }
.re-name:hover .re-edit-hint { opacity: 1; }
.re-close {
  margin-left: auto;
  background: transparent;
  border: 1px solid var(--line-1);
  color: var(--fg-1);
  width: 28px; height: 28px;
  font-size: 16px; cursor: pointer;
}
.re-close:hover { color: var(--fg-0); }
.re-body { display: flex; flex: 1; min-height: 0; }
.re-list-wrap { flex: 1; overflow-y: auto; padding: 16px 20px; }
.re-empty {
  padding: 60px 20px;
  text-align: center;
  font-size: 11px; letter-spacing: 0.16em;
  color: var(--fg-2);
  border: 1px dashed var(--line-1);
}
.re-entries { display: flex; flex-direction: column; gap: 12px; }
.re-entry {
  border: 1px solid var(--line-1);
  background: var(--bg-0);
  padding: 12px 14px;
  display: flex; flex-direction: column; gap: 10px;
}
.re-entry-head {
  display: flex; align-items: center; gap: 12px;
}
.re-idx {
  font-size: 11px; letter-spacing: 0.16em;
  color: var(--fg-3);
  min-width: 28px;
}
.re-entry-name { flex: 1; min-width: 0; }
.re-uname { color: var(--fg-0); font-size: 13px; }
.re-usub { color: var(--fg-2); font-size: 11px; }
.re-uname--missing { color: var(--fg-3); }
.re-fcode { font-size: 11px; letter-spacing: 0.16em; }
.re-count-num { min-width: 24px; text-align: center; font-size: 12px; color: var(--fg-0); }
.re-instances {
  display: flex; flex-wrap: wrap; gap: 6px;
  padding-top: 8px;
  border-top: 1px dashed var(--line-1);
}
.re-instances-label {
  width: 100%;
  font-size: 11px; letter-spacing: 0.18em;
  color: var(--fg-3);
  padding-bottom: 2px;
}
.re-instance {
  display: flex; align-items: center; gap: 6px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  padding: 4px 6px;
}
.re-inst-num { color: var(--fg-3); font-size: 11px; min-width: 22px; }
.re-action {
  background: var(--accent-dim);
  border: 1px solid var(--accent-line);
  color: var(--accent-fg);
  font-family: var(--ff-mono);
  font-size: 11px; letter-spacing: 0.06em;
  padding: 10px 0;
  cursor: pointer;
  margin-bottom: 8px;
}
.re-action:hover:not(:disabled) { background: var(--accent-line); color: var(--bg-0); }
.re-action:disabled { opacity: 0.4; cursor: not-allowed; }
.re-action.danger {
  background: transparent;
  border-color: var(--line-2);
  color: var(--fg-2);
}
.re-action.danger:hover:not(:disabled) {
  border-color: oklch(60% 0.18 25);
  color: oklch(70% 0.18 25);
  background: transparent;
}

/* Admin chrome (.admin-shell, .admin-topbar, .admin-nav-link, .admin-back,
   .admin-main, .admin-h1/h2/sub/crumbs, .admin-card, .admin-table,
   .admin-btn, .admin-stat, .admin-pill, .admin-form-grid, .admin-fields,
   .admin-field, .admin-checks, .admin-memberships, .admin-product-row,
   .admin-upload-cell, .admin-img-thumb, etc.) moved to AdminLayout.razor.css.
   Page-rendered admin chrome uses ::deep inside that file; AdminLayout's
   own shell/topbar selectors are scoped naturally.
   The old `.admin-link-btn` dropped — no callsite remains in the catalog topbar.

   Products page styles (.ident-app.products-page collapse, .products-page
   .ident-main, .products-*, .hero-*) moved to Products.razor.css. */


/* ── Crop dialog ──────────────────────────────────────────────
   Modal cropper for the admin upload flow. Built by JS (ident.js → mountCropper),
   styled here. Sits above everything. */
.crop-scrim {
  position: fixed; inset: 0; z-index: 9999;
  background: oklch(0 0 0 / 0.65);
  display: grid; place-items: center;
  padding: 24px;
}
.crop-panel {
  background: var(--bg-1); border: 1px solid var(--accent-line); border-radius: var(--r-sm);
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px 18px 14px;
  max-width: min(960px, 96vw); max-height: 92vh;
  box-shadow: 0 12px 48px oklch(0 0 0 / 0.4);
}
.crop-head h3 {
  margin: 0; font-size: 16px; font-weight: 600;
  font-family: var(--ff-mono); letter-spacing: 0.16em; color: var(--accent);
}
.crop-hint { font-size: 11px; color: var(--fg-3); margin-top: 4px; line-height: 1.4; max-width: 60ch; }

.crop-stage {
  flex: 1; min-height: 0; display: grid; place-items: center;
  background: repeating-conic-gradient(var(--bg-2) 0% 25%, var(--bg-1) 0% 50%) 0 0 / 16px 16px;
  border: 1px solid var(--line-1); border-radius: var(--r-sm);
  overflow: auto;
}
.crop-wrap {
  position: relative; line-height: 0;
  max-width: 100%; max-height: 80vh;
}
.crop-img { display: block; max-width: 100%; max-height: 80vh; user-select: none; }

/* Overlay sits on top of the image; pointer events go through to the rect/handles below it. */
.crop-overlay { position: absolute; inset: 0; pointer-events: none; }

/* The visible rectangle. The "halo" effect — dimming everything outside the rect — is done
   with an oversized box-shadow rather than a separate mask layer, so resizing is one element. */
.crop-rect {
  position: absolute;
  border: 1.5px solid var(--accent);
  box-shadow: 0 0 0 9999px oklch(0 0 0 / 0.55);
  cursor: move;
  pointer-events: auto;
  box-sizing: border-box;
}

.crop-handle {
  position: absolute; width: 12px; height: 12px;
  background: var(--accent); border: 1px solid var(--bg-0);
  border-radius: 2px;
  pointer-events: auto;
}
.crop-handle-nw { top: -6px;  left: -6px;  cursor: nwse-resize; }
.crop-handle-n  { top: -6px;  left: 50%;   transform: translateX(-50%); cursor: ns-resize; }
.crop-handle-ne { top: -6px;  right: -6px; cursor: nesw-resize; }
.crop-handle-e  { top: 50%;   right: -6px; transform: translateY(-50%); cursor: ew-resize; }
.crop-handle-se { bottom: -6px; right: -6px; cursor: nwse-resize; }
.crop-handle-s  { bottom: -6px; left: 50%;  transform: translateX(-50%); cursor: ns-resize; }
.crop-handle-sw { bottom: -6px; left: -6px; cursor: nesw-resize; }
.crop-handle-w  { top: 50%;   left: -6px;  transform: translateY(-50%); cursor: ew-resize; }

.crop-foot {
  display: flex; align-items: center; gap: 8px;
}
.crop-size { color: var(--fg-3); font-size: 11px; letter-spacing: 0.14em; margin-right: auto; }
.crop-btn {
  font-family: var(--ff-mono); font-size: 11px; letter-spacing: 0.14em;
  padding: 6px 12px; border-radius: var(--r-sm);
  background: var(--bg-2); color: var(--fg-1);
  border: 1px solid var(--line-1); cursor: pointer;
}
.crop-btn:hover { background: var(--bg-3); border-color: var(--line-2); }
.crop-btn.primary {
  background: var(--accent); color: var(--accent-fg); border-color: var(--accent);
}
.crop-btn.primary:hover { background: var(--accent); filter: brightness(1.08); }

/* Admin import page styles (details/summary baseline, .import-progress-*,
   .import-summary-*, .import-history-*, .data-dump-*) moved to
   AdminLayout.razor.css. */
