/* ---------------------------------------------------------------------------
   cards.css - a ROUTE stylesheet, not part of the always-loaded core.

   The catalogue card family: SetCard, MinifigCard, PartTile, the condition badge, the
   theme chip, the tile grid, the catalogue imagery and the skeleton rows.

   Loaded by: every page that renders a card or a tile - the catalogue, the collection, the
   detail routes and /scan - and by /dev/gallery, which renders everything.
   Linked from those pages' own markup (<RouteStylesheets Sheets="..." />), so
   it arrives AFTER tokens/reset/app/components: at equal specificity a rule
   here beats the same selector in the core sheet, which reverses the order
   these rules had inside components.css. Why the split exists, and what a page
   that links the wrong set breaks, is in
   docs/architecture/performance-budgets.md.
   --------------------------------------------------------------------------- */

/* =========================================================================
   SetCard (§5) — large rounded card, hero image top ~60%, title, counts,
   dominant ConditionBadge.
   ========================================================================= */

.bd-set-card {
  display: flex;
  flex-direction: column;
  border-radius: var(--bd-radius-lg);
  /* Cards are solid, decided (design-system.md §2.4.3) — never wrapped in
     <GlassSurface>. --bd-color-card-surface resolves to --bd-color-surface-raised in
     both themes; the indirection is the point (see tokens.css). */
  background: var(--bd-color-card-surface);
  box-shadow: var(--bd-shadow-2);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
}

.bd-set-card__media {
  position: relative;
  width: 100%;
  aspect-ratio: 5 / 3; /* hero image occupies roughly the top 60% of the card */
  background: var(--bd-color-section-surface);
}

.bd-minifig-card .bd-set-card__media {
  aspect-ratio: 1;
}

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

/* CatalogImage (design-system.md §7) — a self-hosted catalogue render layered over its own
   designed empty state. The fallback sits underneath at all times, because the failed-load
   path is the plain HTML `onerror` attribute (an anonymous visitor gets no Blazor circuit,
   so no C# handler can run) and `onerror` can only hide the image, never conjure markup. */
.bd-catalog-image {
  position: absolute;
  inset: 0;
}

.bd-catalog-image__fallback {
  position: absolute;
  inset: 0;
}

/* Two classes deep so it beats `.bd-set-card__media img` (0,1,1) on specificity: a
   catalogue render is a whole product photographed against its own ground, so cropping it
   to fill the card - which is right for a user's own photo - cuts the set in half. */
.bd-catalog-image > .bd-catalog-image__img {
  /* PAINTS ABOVE THE FALLBACK, AND WITHOUT THIS IT DOES NOT.
     The image and the "No image" fallback are both position:absolute in the same box.
     With z-index:auto on both, paint order is DOM order and the fallback is second, so
     the placeholder was drawn on top of a perfectly loaded image - naturalWidth set,
     complete true, display block, opacity 1, and invisible. elementFromPoint at the card
     centre returned the fallback's icon. Shipped that way; found by looking at a
     screenshot after three rounds of DOM probes reported everything visible. */
  z-index: 1;

  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: var(--bd-space-2);
  object-fit: contain;
  background: var(--bd-color-section-surface);
}

/* NOT REDUNDANT. reset.css sets `img { display: block }`, and an author `display` beats the
   user-agent `[hidden] { display: none }` rule - so without this, `onerror` would set the
   attribute, the browser would honour nothing, and the broken-image glyph this whole
   component exists to avoid would stay on screen. */
.bd-catalog-image__img[hidden] {
  display: none;
}

.bd-set-card__badge-slot {
  z-index: 2;
  position: absolute;
  top: var(--bd-space-3);
  right: var(--bd-space-3);
}

.bd-set-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--bd-space-1);
  padding: var(--bd-space-4);
}

.bd-set-card__number,
.bd-scan-result__number {
  color: var(--bd-color-muted);
  font-size: var(--bd-type-caption-size);
}

.bd-set-card__number {
  font-family: var(--bd-font-body);
  font-weight: 500;
  line-height: var(--bd-type-caption-line);
  font-variant-numeric: tabular-nums;
}

.bd-set-card__title,
.bd-scan-result__name {
  font-size: var(--bd-type-title-size);
  font-weight: 600;
}

.bd-set-card__title {
  font-family: var(--bd-font-display);
  line-height: var(--bd-type-title-line);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.bd-set-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bd-space-3);
  margin-top: var(--bd-space-1);
  color: var(--bd-color-muted);
  font-family: var(--bd-font-body);
  font-weight: 500;
  font-size: var(--bd-type-body-sm-size);
  line-height: var(--bd-type-body-sm-line);
}

.bd-set-card__meta-item {
  display: inline-flex;
  align-items: center;
  gap: var(--bd-space-1);
  font-variant-numeric: tabular-nums;
}

.bd-set-card__meta-item svg {
  width: 16px;
  height: 16px;
  flex: none;
}

/* SetCard's "view details" link (issue: /sets/{SetNumber} shipped in #411 reachable from
   nowhere - neither /search's nor /themes/{id}'s card linked to it). Two separate <a> elements
   share this class - one wraps .bd-set-card__media, the other wraps the number/title/meta
   group inside .bd-set-card__body - because the card also carries the "Add to collection" CTA
   as its own <a>, and nesting an <a> inside another <a> is invalid HTML that breaks
   click-through. Siblings, never nested.

   The two usages need different box behaviour, so the base rule below is `block` (right for
   wrapping the image, which has no internal layout of its own to preserve) and the
   `.bd-set-card__body >` rule re-declares the flex column + gap that the wrapped
   number/title/meta spans had as direct children of .bd-set-card__body before this link
   existed - so wrapping them changes nothing about how they stack or space themselves. */
.bd-set-card__link {
  display: block;
  color: inherit;
  text-decoration: none;
}

.bd-set-card__body > .bd-set-card__link {
  display: flex;
  flex-direction: column;
  gap: var(--bd-space-1);
}

.bd-set-card__body > .bd-btn {
  margin-top: var(--bd-space-3);
}

/* =========================================================================
   PartTile (§5) — compact list/grid density row for OwnedPartView.
   ========================================================================= */

.bd-part-tile {
  display: flex;
  align-items: center;
  gap: var(--bd-space-3);
  min-height: 56px;
  padding: var(--bd-space-2) var(--bd-space-3);
  border-bottom: 1px solid var(--bd-color-border);
  /* Cards/rows are solid, never <GlassSurface> — design-system.md §2.4.3. A
     virtualised list of these is exactly the unbounded-scroll case backdrop-filter
     cannot afford. */
  background: var(--bd-color-card-surface);
}

.bd-part-tile__media {
  position: relative;
  flex: none;
  width: 48px;
  height: 48px;
  border-radius: var(--bd-radius-sm);
  background: var(--bd-color-section-surface);
  overflow: hidden;
}

.bd-part-tile__media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Colour swatch overlay, corner-anchored, indicating the part's LEGO colour. */
.bd-part-tile__swatch {
  position: absolute;
  bottom: 2px;
  right: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--bd-color-card-surface);
}

.bd-part-tile__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.bd-part-tile__name {
  font-family: var(--bd-font-body);
  font-weight: 400;
  font-size: var(--bd-type-body-size);
  line-height: var(--bd-type-body-line);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bd-part-tile__meta {
  display: flex;
  align-items: center;
  gap: var(--bd-space-2);
}

/* Source chip — "Loose" or "From: <set nickname>". */
.bd-part-tile__source {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--bd-space-2);
  border-radius: var(--bd-radius-sm);
  background: var(--bd-color-surface);
  border: 1px solid var(--bd-color-border);
  color: var(--bd-color-muted);
  font-family: var(--bd-font-body);
  font-weight: 500;
  font-size: var(--bd-type-caption-size);
  line-height: var(--bd-type-caption-line);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.bd-part-tile__qty {
  flex: none;
  font-family: var(--bd-font-body);
  font-weight: 500;
  font-size: var(--bd-type-body-sm-size);
  font-variant-numeric: tabular-nums;
  color: var(--bd-color-on-surface);
  min-width: 2.5ch;
  text-align: right;
}

.bd-part-tile__badge {
  flex: none;
}

/* =========================================================================
   ConditionBadge (§6) — the 9-step ConditionGrade encoding.
   Colour is never the sole channel: shape (rounded-square / circle /
   pennant) + fill texture (solid / diagonal-hatch / cross-hatch) + colour
   (hue ramp, band-stepped) + an always-visible caps text label. The badge
   chip's own FILL is the grade colour; the shape glyph and label are
   rendered in the on-grade contrast colour on top of it, so:
     - shape gets a colourblind/low-vision user to the right third (band),
     - texture narrows it further within the band,
     - the caps label resolves the exact grade regardless of any visual
       channel,
     - a full-word aria-label (set in markup, see gallery.html) backs every
       instance for assistive tech; the visible abbreviation is decorative
       (aria-hidden) so it is never the only text a screen reader gets.
   The shape/texture SVG (with its own <pattern> defs) lives in markup per
   instance — components.css only sizes and colours it via currentColor,
   which is why the shape glyph's fill/stroke must always be currentColor.
   ========================================================================= */

.bd-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--bd-space-1);
  height: 20px;
  padding: 0 var(--bd-space-2) 0 6px;
  border-radius: var(--bd-radius-sm);
  color: var(--bd-color-on-grade); /* on-grade also drives glyph currentColor */
  white-space: nowrap;
}

.bd-badge__shape {
  width: 14px;
  height: 14px;
  flex: none;
  display: block;
}

.bd-badge__label {
  font-family: var(--bd-font-body);
  font-weight: 600;
  font-size: var(--bd-type-label-size);
  line-height: var(--bd-type-label-line);
  letter-spacing: var(--bd-type-label-tracking);
  text-transform: uppercase;
}

/* Band 1 — Like-new: rounded square, green family */
.bd-badge--grade-1 { background: var(--bd-grade-1); } /* Sealed */
.bd-badge--grade-2 { background: var(--bd-grade-2); } /* Mint */
.bd-badge--grade-3 { background: var(--bd-grade-3); } /* NearMint */

/* Band 2 — Displayed: circle, blue-indigo family */
.bd-badge--grade-4 { background: var(--bd-grade-4); } /* Excellent */
.bd-badge--grade-5 { background: var(--bd-grade-5); } /* VeryGood */
.bd-badge--grade-6 { background: var(--bd-grade-6); } /* Good */

/* Band 3 — Well-used: pennant, amber-red family */
.bd-badge--grade-7 { background: var(--bd-grade-7); } /* Fair */
.bd-badge--grade-8 { background: var(--bd-grade-8); } /* Poor */
.bd-badge--grade-9 { background: var(--bd-grade-9); } /* ForParts */

/* =========================================================================
   ThemeChip (§2.6) — a set's LEGO theme in one of the six accent hues.

   Colour is decoration here, never information: ThemeChip.razor always renders
   the theme's name as text inside the chip, so a user who cannot tell the hues
   apart loses nothing. What the hue buys is scannability in a grid of forty
   cards.

   The 1px border is load-bearing, not trim. It is drawn in the hue's OWN ink,
   which is what gives the chip a visible edge in both directions of §2.6's
   boundary rule: a deep hue's fill already contrasts with the card and its
   white ink border disappears into the fill's edge harmlessly, while a bright
   yellow chip's fill reaches only 1.51:1 against a near-white card and it is
   the dark-indigo border that carries the boundary at 14.49:1. Removing the
   border makes the bright half of the family vanish into the card. Guard:
   BrickDb.App.Shared.Tests.Design.TokenContrastTests
   .EveryAccentChipHasAVisibleEdgeAgainstTheCardItSitsOn.
   ========================================================================= */

.bd-theme-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--bd-space-2);
  border-radius: var(--bd-radius-sm);
  border: 1px solid currentColor;
  font-family: var(--bd-font-body);
  font-weight: 600;
  font-size: var(--bd-type-caption-size);
  line-height: var(--bd-type-caption-line);
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bd-theme-chip--red {
  background: var(--bd-accent-red);
  color: var(--bd-accent-red-ink);
}

.bd-theme-chip--orange {
  background: var(--bd-accent-orange);
  color: var(--bd-accent-orange-ink);
}

.bd-theme-chip--yellow {
  background: var(--bd-accent-yellow);
  color: var(--bd-accent-yellow-ink);
}

.bd-theme-chip--green {
  background: var(--bd-accent-green);
  color: var(--bd-accent-green-ink);
}

.bd-theme-chip--blue {
  background: var(--bd-accent-blue);
  color: var(--bd-accent-blue-ink);
}

.bd-theme-chip--purple {
  background: var(--bd-accent-purple);
  color: var(--bd-accent-purple-ink);
}

/* =========================================================================
   SkeletonList (§5) — shimmer placeholder rows matching the target row
   height exactly, both for named first-page loading and decorative Virtualize placeholders.
   ========================================================================= */

.bd-skeleton-row {
  display: flex;
  align-items: center;
  gap: var(--bd-space-3);
  min-height: 56px; /* matches .bd-part-tile row height exactly */
  padding: var(--bd-space-2) var(--bd-space-3);
}

/* The shimmer's mid-stop is --bd-color-primary-light rather than a second neutral grey
   (was --bd-color-border-strong until 2026-09-06, design-system.md §2.7) — a small,
   deliberately subtle way of making a loading row feel like BrickDb's own rather than a
   generic placeholder, without needing new tokens or reopening any contrast question: the
   gradient carries no text and no information, and --bd-color-primary-light already exists
   and already resolves correctly in all four palettes (tokens.css). */
.bd-skeleton-block {
  border-radius: var(--bd-radius-sm);
  background: linear-gradient(
    100deg,
    var(--bd-color-border) 30%,
    var(--bd-color-primary-light) 50%,
    var(--bd-color-border) 70%
  );
  background-size: 200% 100%;
  animation: bd-shimmer 1.5s ease-in-out infinite;
}

.bd-skeleton-block--media {
  flex: none;
  width: 48px;
  height: 48px;
}

.bd-skeleton-block--line {
  flex: 1 1 auto;
  height: 14px;
}

.bd-skeleton-block--line-short {
  flex: 0 0 40%;
  height: 12px;
}

.bd-skeleton-row__lines {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: var(--bd-space-2);
}

.bd-skeleton-row--vertical {
  flex-direction: column;
  align-items: stretch;
}

.bd-skeleton-row--card {
  height: 220px;
}

.bd-skeleton-row--tile {
  height: 96px;
}

.bd-skeleton-block--card-media {
  height: 60%;
}

.bd-skeleton-block--tile-media {
  height: 40%;
}

.bd-skeleton-block--detail-media {
  height: 220px;
}

.bd-skeleton-row--vertical .bd-skeleton-row__lines {
  margin-top: var(--bd-space-2);
}

@media (prefers-reduced-motion: reduce) {
  .bd-skeleton-block {
    animation: none;
    background: var(--bd-color-border);
  }}

/* =========================================================================
   ThemeTile + ThemeGrid (design-system.md §2.8) — the browse-by-theme
   surface. A theme is a card whose top is a studded brick in that theme's
   accent hue (§2.6, the same hue ThemeChip gives it), with the name and the
   counts on the ordinary card surface below.

   THE BRICK IS DECORATION AND THE TEXT IS THE INFORMATION, which is what
   lets the tile be as bright as it is. Nothing is written on the accent
   fill, so the pair never has to clear 4.5:1 and no new row is needed in
   §2.1's table — the same reasoning BrickRow's own block sets out, and the
   reason the accent family could not be used this way for a label (§2.6's
   closing paragraph: on a light ground a fill cannot be both bright and
   carry small text). The tile is a link, so its focus ring and its label
   come from the anchor, not from the colour.

   A denser grid than .bd-card-grid: there are 150 root themes, and a theme
   tile has no image to give room to.
   ========================================================================= */

.bd-tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--bd-space-4);
}

.bd-scan-result {
	display: flex;
	flex-direction: column;
	gap: var(--bd-space-2);
	padding: var(--bd-space-4);
	border-radius: var(--bd-radius-lg);
	background: var(--bd-color-card-surface);
	box-shadow: var(--bd-shadow-1);
}

.bd-scan-result__lede {
	margin: 0;
	color: var(--bd-color-muted);
	font-size: var(--bd-type-body-sm-size);
}

.bd-scan-result__message {
	margin: 0;
	font-size: var(--bd-type-body-size);
}

.bd-scan-result__figure {
	display: flex;
	gap: var(--bd-space-3);
	align-items: flex-start;
}

/* The frame CatalogImage sits in on a scan result (#264). A positioning context and a
   fixed size, the same job .bd-catalog-thumb does on the add-to-collection header - the
   component itself is absolutely positioned over its own empty state, so it needs a box
   to be absolute within, and a box that is sized before the bytes arrive is what stops
   the name and the confidence jumping when they do. Square rather than 5:3: a minifigure
   render is taller than it is wide. */
.bd-scan-result__media {
	position: relative;
	flex: 0 0 auto;
	width: 96px;
	height: 96px;
	overflow: hidden;
	border-radius: var(--bd-radius-md);
	background: var(--bd-color-section-surface);
}

.bd-scan-result__figure-body {
	display: flex;
	flex-direction: column;
	gap: var(--bd-space-1);
	min-width: 0;
}

/* The confidence and the headcount are one block on purpose - see
   ScanResultView's header. A percentage without its sample size is a claim the
   Wilson score was chosen specifically to avoid making. */
.bd-scan-result__belief {
	display: flex;
	flex-wrap: wrap;
	gap: var(--bd-space-2);
	align-items: baseline;
}

.bd-scan-result__confidence {
	font-variant-numeric: tabular-nums;
	font-weight: 600;
}

.bd-scan-result__confirmations,
.bd-scan-result__source,
.bd-scan-result__status {
	margin: 0;
	color: var(--bd-color-muted);
	font-size: var(--bd-type-caption-size);
}

.bd-scan-result__other-list {
	display: flex;
	flex-direction: column;
	gap: var(--bd-space-2);
	margin: 0;
	padding: 0;
	list-style: none;
}

.bd-scan-result__other {
	display: flex;
	flex-direction: column;
	gap: var(--bd-space-1);
}

.bd-scan-result__code {
	display: flex;
	flex-wrap: wrap;
	gap: var(--bd-space-2);
	margin: 0;
	font-size: var(--bd-type-caption-size);
}

.bd-scan-result__code-label {
	color: var(--bd-color-muted);
}

.bd-scan-result__code-value {
	overflow-wrap: anywhere;
	/* A literal stack rather than a token: this is the only monospace in the
	   design system, and adding a token for one use would put a colour-adjacent
	   decision in tokens.css that nothing else reads. A scanned payload is a
	   string of digits somebody may read back aloud, so the fixed pitch earns
	   its place. */
	font-family: ui-monospace, "SFMono-Regular", menlo, consolas, monospace;
}

/* =========================================================================
   Photo reorder controls (issue #71) — up/down move buttons on each strip
   tile.
   ========================================================================= */

.bd-part-tile__actions {
	display: flex;
	flex: none;
	gap: var(--bd-space-1);
}

@keyframes bd-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
