/* Kpopownia components. Requires tokens.css and base.css.
   Naming: block, block__element, block--modifier, state is-*.
   Sections: 1 atoms · 2 molecules · 3 page sections. */

/* =====================================================================
   1. ATOMS
   ===================================================================== */

/* Buttons ------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  padding: 0 var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  font-weight: 700;
  font-size: var(--control-font);
  line-height: 1;
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn:active {
  transform: translateY(1px);
}

/* On a phone, or at 200% zoom on a desk, a long label ("Przejdź do
   płatności") could outgrow the screen, and main clips its sides — the end
   of the label was simply cut off. Below 640px a button wraps instead. */
@media (max-width: 639px) {
  .btn {
    white-space: normal;
    line-height: 1.2;
    text-align: center;
  }
}

.btn:disabled,
.btn[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.5;
  box-shadow: none;
  transform: none;
}

.btn--primary {
  background: var(--primary-fill);
  color: var(--on-primary);
}

.btn--primary:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--primary-fill-deep);
  box-shadow: var(--glow-pink);
}

/* A lit primary: the glow other buttons earn on hover is on from the start.
   For the one CTA of a page whose whole job is that click (add to cart). */
.btn--glow:not(:disabled) {
  box-shadow: var(--glow-pink);
}

.btn--glow:hover:not(:disabled) {
  box-shadow: var(--glow-pink), var(--glow-blue);
}

.btn--secondary {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-strong);
}

.btn--secondary:hover:not(:disabled) {
  border-color: var(--secondary);
  color: var(--secondary);
  background: var(--secondary-soft);
}

.btn--ghost {
  background: transparent;
  color: var(--ink-soft);
}

.btn--ghost:hover:not(:disabled) {
  color: var(--ink);
  background: var(--surface-raised);
}

.btn--danger {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: transparent;
}

.btn--danger:hover:not(:disabled) {
  background: var(--danger);
  color: var(--stage);
}

.btn--sm {
  min-height: var(--control-height-sm);
  padding-inline: var(--space-4);
  font-size: var(--text-sm);
}

.btn--lg {
  min-height: 3.25rem;
  padding-inline: var(--space-8);
  font-size: var(--text-lg);
}

.btn--block {
  width: 100%;
  /* A full-width button can hold a sentence (a locker's address); it wraps
     rather than running out of its own box on a phone. */
  white-space: normal;
}

.btn--icon {
  width: var(--control-height);
  padding: 0;
  border-radius: var(--radius-control);
}

.btn--icon.btn--sm {
  width: var(--control-height-sm);
}

/* Loading: hide the label, show a spinner. Pair with htmx `hx-indicator`
   (htmx adds .htmx-request to the indicator; map it below). */
.btn.is-loading,
.btn.htmx-request {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}

.btn.is-loading::after,
.btn.htmx-request::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 1.1em;
  height: 1.1em;
  border-radius: 50%;
  border: 2px solid currentcolor;
  border-color: var(--on-primary) transparent var(--on-primary) transparent;
  animation: spin 0.8s linear infinite;
}

.btn--secondary.is-loading::after,
.btn--ghost.is-loading::after,
.btn--secondary.htmx-request::after,
.btn--ghost.htmx-request::after {
  border-color: var(--ink) transparent var(--ink) transparent;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Links --------------------------------------------------------------- */

a.link {
  color: var(--secondary);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.2em;
  transition: text-decoration-color var(--dur-fast), color var(--dur-fast);
}

a.link:hover {
  text-decoration-color: currentcolor;
}

.link--quiet {
  color: var(--ink-soft);
}

.link--quiet:hover {
  color: var(--ink);
}

.link--arrow::after {
  content: " →";
}

/* A link inside a sentence: underlined at rest, because the colour alone
   does not tell it from the words around it (WCAG 1.4.1). */
a.link--inline {
  text-decoration-color: currentcolor;
  text-decoration-thickness: 0.08em;
}

a.link--inline:hover {
  text-decoration-thickness: 0.16em;
}

/* A link that opens a new tab says so with an arrow; the arrow is drawn
   only, the words for a screen reader are the link's own hidden note. */
.link--external::after {
  content: "\00a0\2197" / "";
  font-size: 0.85em;
}

/* Badges ---------------------------------------------------------------
   Vocabulary for the shop: count (cart), preorder, new, promo, last,
   soldout, info. Keep the words short — they sit on album covers. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 0.2em 0.65em;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.4;
  background: var(--surface-raised);
  color: var(--ink);
  border: 1px solid var(--line);
}

/* A dot in front: the order's status on the account page and the order
   page, coloured by the status below. */
.badge--dot {
  color: var(--ink-soft);
}

.badge--dot::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentcolor;
}

.badge--status-new {
  background: var(--warning-soft);
  border-color: transparent;
  color: var(--warning);
}

.badge--status-confirmed {
  background: color-mix(in srgb, var(--accent-violet-ink) 14%, transparent);
  border-color: transparent;
  color: var(--accent-violet-ink);
}

.badge--status-shipped {
  background: var(--secondary-soft);
  border-color: transparent;
  color: var(--secondary);
}

.badge--status-delivered {
  background: var(--success-soft);
  border-color: transparent;
  color: var(--success);
}

.badge--status-cancelled {
  color: var(--ink-muted);
}

.badge--count {
  min-width: 1.4rem;
  height: 1.4rem;
  padding: 0 0.4em;
  justify-content: center;
  background: var(--primary-fill);
  color: var(--on-primary);
  border-color: transparent;
  transition: transform var(--dur-slow) var(--ease-spring);
}

/* A counter that arrives in an htmx swap (the cart after "add to cart")
   lands enlarged and springs back once htmx lifts .htmx-added. One drawn on
   page load never carries the class, so it stays still. A transform, so the
   navbar around it does not move. */
.badge--count.htmx-added {
  transform: scale(1.4);
  transition: none;
}

.badge--preorder {
  background: var(--secondary);
  color: var(--on-secondary);
  border-color: transparent;
}

.badge--new {
  background: var(--primary-fill);
  color: var(--on-primary);
  border-color: transparent;
}

.badge--promo {
  background: var(--primary-fill-deep);
  color: var(--on-primary);
  border-color: transparent;
}

.badge--last {
  background: var(--warning-soft);
  color: var(--warning);
  border-color: transparent;
}

.badge--soldout {
  background: var(--surface);
  color: var(--ink-muted);
  border-color: var(--line-strong);
}

.badge--info {
  background: var(--secondary-soft);
  color: var(--secondary);
  border-color: transparent;
}

/* Price --------------------------------------------------------------- */

.price {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-weight: 700;
  font-variant-numeric: var(--figures);
}

.price__current {
  font-size: var(--text-lg);
}

.price--lg .price__current {
  font-size: var(--text-3xl);
  font-family: var(--font-brand);
}

.price--promo .price__current {
  color: var(--primary);
}

.price__base {
  font-weight: 500;
  color: var(--ink-muted);
  text-decoration: line-through;
}

.price__lowest {
  flex-basis: 100%;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
}

/* Availability -------------------------------------------------------- */

.availability {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 700;
}

.availability::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentcolor;
  box-shadow: 0 0 6px currentcolor;
}

.availability--ok { color: var(--success); }
.availability--later { color: var(--warning); }
.availability--none { color: var(--danger); }
.availability--preorder { color: var(--secondary); }

/* Form fields --------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--ink-soft);
}

/* The star after a required label, and in the note that explains it. */
.req {
  color: var(--primary);
}

.field__control {
  width: 100%;
  min-height: var(--control-height);
  padding: 0 var(--space-4);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-md);
  color: var(--ink);
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}

.field__control::placeholder {
  color: var(--ink-muted);
}

.field__control:hover {
  border-color: var(--ink-muted);
}

/* A field's focus is its border in --focus, doubled to 2px by a hairline
   ring right outside it, with the soft halo beyond — the colour every other
   control's ring wears; :focus-visible like the rest, which a browser applies
   to a text field however it was focused. The halo alone is decoration
   (1.3:1); the 2px edge is what reads as focus. The outline is transparent
   rather than none: in Windows' high-contrast mode shadows vanish and a
   transparent outline is painted in the system colour, so focus survives. */
.field__control:focus-visible {
  outline: 2px solid transparent;
  outline-offset: 0;
  border-color: var(--focus);
  box-shadow:
    0 0 0 1px var(--focus),
    0 0 0 4px color-mix(in srgb, var(--focus) 18%, transparent);
}

textarea.field__control {
  min-height: 7rem;
  padding-block: var(--space-3);
  resize: vertical;
}

select.field__control {
  appearance: none;
  padding-right: var(--space-10);
}

/* Wrap a select in .select to draw the chevron in a token colour (a data-URI
   cannot read custom properties, so the arrow is a masked pseudo-element). */
.select {
  position: relative;
  display: block;
}

.select::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--space-4);
  width: 1rem;
  height: 1rem;
  transform: translateY(-50%);
  pointer-events: none;
  background: var(--secondary);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}

.field__help {
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.field__error {
  display: none;
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--danger);
}

.field--error .field__control {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-soft);
}

.field--error .field__error {
  display: block;
}

.field--readonly .field__control {
  background: var(--stage-mid);
  color: var(--ink-soft);
  border-style: dashed;
}

.field-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .field-grid {
    grid-template-columns: repeat(12, 1fr);
  }

  /* A field that asks for no width takes half the row. The grid was two literal
     columns for a long time and every form was written against that, so half is
     what "unset" has to keep meaning. */
  .field-grid > * {
    grid-column: span 6;
  }

  .field-grid > .span-full { grid-column: 1 / -1; }
}

/* The narrower widths are a wide-screen luxury. Below this a quarter is about
   130px, which clips a native date control, so everything stays two-up — the
   layout this grid had when it was two literal columns. */
@media (min-width: 1024px) {
  .field-grid > .span-3 { grid-column: span 3; }
  .field-grid > .span-4 { grid-column: span 4; }
  .field-grid > .span-6 { grid-column: span 6; }
  .field-grid > .span-8 { grid-column: span 8; }
}

/* A heading between groups of fields. It takes span-full like any other
   full-row cell; what is added here is the quiet uppercase register and the
   hairline that separates one group from the last. */
.field-section__title {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: calc(var(--label-tracking) * 1.5);
  text-transform: var(--label-transform);
  color: var(--ink-soft);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--line);
}

/* Extra air above a heading, so a group reads as separated from the one before
   it rather than merely labelled. The first heading needs none — nothing sits
   above it to be separated from. */
.field-grid > .field-section {
  margin-top: var(--space-4);
}

.field-grid > .field-section:first-child {
  margin-top: 0;
}

/* Checkbox --------------------------------------------------------------
   Same device as .radio-card: the input stays in the DOM (sr-only) so forms
   and htmx keep working, and :has() drives the mark. */

.checkbox {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  cursor: pointer;
}

.checkbox__mark {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.15rem;
  border: 2px solid var(--control-border);
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  transition: background-color var(--dur-fast), border-color var(--dur-fast);
}

.checkbox:hover .checkbox__mark {
  border-color: var(--primary);
}

.checkbox:has(input:checked) .checkbox__mark {
  background: var(--primary);
  border-color: var(--primary);
}

.checkbox:has(input:checked) .checkbox__mark::after {
  content: "";
  width: 0.55rem;
  height: 0.3rem;
  /* --stage rather than --on-primary: the box is --primary, which is deep in
     the light themes, and --on-primary's dark ink fell to 2.6:1 on Motki's. */
  border-left: 2px solid var(--stage);
  border-bottom: 2px solid var(--stage);
  transform: rotate(-45deg) translate(1px, -2px);
}

.checkbox:has(input:focus-visible) .checkbox__mark {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* A required box that was refused: the mark takes the error colour, the
   same signal a refused text field gets. */
.field--error .checkbox:not(:has(input:checked)) .checkbox__mark {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-soft);
}

.checkbox:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}

.checkbox__body {
  min-width: 0;
}

.checkbox__label {
  display: block;
  font-weight: 700;
}

.checkbox__hint {
  display: block;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* Chips ------------------------------------------------------------------
   A row of pills: the version picker on a product, the subcategories under
   a category, the active filters above a list. Core rather than the product
   pack, since a list page draws them too. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.25rem;
  padding: 0 var(--space-4);
  border-radius: var(--radius-pill);
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--ink-soft);
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
}

.chip:hover {
  border-color: var(--ink-muted);
  color: var(--ink);
}

.chip[aria-pressed="true"],
.chip:has(input:checked) {
  border-color: var(--primary);
  background: var(--primary-soft);
  color: var(--ink);
}

/* A chip that is a radio (a version, a sort order) keeps its input in the
   DOM as .sr-only; the global ring would land on that 1px box, so the chip
   draws it on itself. */
.chip:has(input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.chip--disabled,
.chip:has(input:disabled) {
  opacity: 0.45;
  cursor: not-allowed;
  text-decoration: line-through;
}

/* A chip that removes something — an active filter — carries a cross. */
.chip--remove {
  padding-inline-end: var(--space-3);
}

.chip--remove:hover {
  border-color: var(--danger);
  color: var(--danger);
}

/* Swatch (colour attribute) ------------------------------------------- */

.swatch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* inline-block: a dot standing alone (a table cell, a summary) is not a
   flex item, and as a bare inline span it ignores its size and shows as
   its 1px border. */
.swatch__dot {
  display: inline-block;
  vertical-align: middle;
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background-color: var(--swatch, #888);
  background-size: cover;
}

/* Icon ---------------------------------------------------------------- */

.icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  stroke: currentcolor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.icon--sm { width: 1em; height: 1em; }
.icon--lg { width: 1.75em; height: 1.75em; }
.icon--xl { width: 3rem; height: 3rem; }

/* Skeleton ------------------------------------------------------------ */

.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--surface-raised);
  border-radius: var(--radius-md);
}

.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--shimmer), transparent);
  animation: shimmer 1.4s infinite;
}

.skeleton--line {
  height: 1em;
  border-radius: var(--radius-sm);
}

.skeleton--tile {
  aspect-ratio: 1;
}

@keyframes shimmer {
  to { transform: translateX(100%); }
}

/* Quantity ------------------------------------------------------------ */

.qty {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  overflow: hidden;
}

/* The stepper clips its children to the pill, which cut the ring of a
   focused button or value off; inside the box it stays whole. */
.qty__btn:focus-visible,
.qty__value:focus-visible {
  outline-offset: -2px;
}

.qty__btn {
  width: var(--control-height-sm);
  border: 0;
  background: transparent;
  color: var(--ink-soft);
  font-size: var(--text-lg);
  font-weight: 700;
}

.qty__btn:hover:not(:disabled) {
  background: var(--surface-raised);
  color: var(--ink);
}

.qty__btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.qty__value {
  width: 2.75rem;
  text-align: center;
  border: 0;
  border-inline: 1px solid var(--line);
  background: transparent;
  font-weight: 700;
  font-variant-numeric: var(--figures);
  -moz-appearance: textfield;
}

.qty__value::-webkit-inner-spin-button,
.qty__value::-webkit-outer-spin-button {
  appearance: none;
}

.qty--select .field__control {
  width: 5rem;
  min-height: 2.5rem;
  border-radius: var(--radius-pill);
}

/* Divider / misc ------------------------------------------------------ */

.divider {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--space-6) 0;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}

.card--raised {
  background: var(--surface-raised);
  box-shadow: var(--shadow-1);
}

/* =====================================================================
   2. MOLECULES
   ===================================================================== */

/* Announce bar -------------------------------------------------------- */
/* The one line above the header: the keeper's announcement while it is on,
   else the shop's free-delivery offers. It is drawn only when it has
   something to say, so it never has to look empty. */

.announce {
  /* The fill, not --primary: in the light themes --primary is a deep text
     colour, and the dark ink on it fell to 3.6:1 (2.6 in Motki). */
  background: var(--primary-fill);
  color: var(--on-primary);
  font-size: var(--text-sm);
  font-weight: 700;
}

.announce__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  min-height: 2rem;
  padding-block: var(--space-1);
  text-align: center;
}

.announce .icon {
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
}

.announce__items {
  font-weight: 500;
}

/* The keeper's announcement, in the free-delivery bar's place: its own
   markdown, so the bold it asks for has to stand out from the text. */
.announce__text {
  font-weight: 500;
}

.announce__text strong {
  font-weight: 800;
}

.announce a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.announce a:hover {
  text-decoration-thickness: 2px;
}

/* Navbar -------------------------------------------------------------- */

.navbar {
  position: sticky;
  top: 0;
  z-index: var(--z-navbar);
  background: color-mix(in srgb, var(--stage) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.navbar__inner {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: 4rem;
}

.navbar__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-2xl);
  line-height: 1;
}

.navbar__brand img {
  height: 2rem;
  width: auto;
}

/* Shop logo from settings. One upload: shown as-is on the light theme and
   inverted on the dark one. Two uploads: each theme shows its own. */
.navbar__logo { height: 2rem; width: auto; }
.navbar__brand-text { font-size: var(--text-xl); }
.navbar__logo--invert { filter: invert(1); }
[data-theme="light"] .navbar__logo--invert { filter: none; }
.navbar__logo--light { display: none; }
[data-theme="light"] .navbar__logo--light { display: block; }
[data-theme="light"] .navbar__logo--dark { display: none; }

.navbar__links {
  display: none;
  align-items: center;
  gap: var(--space-5);
  margin-inline-start: var(--space-4);
}

.navbar__link {
  font-weight: 700;
  color: var(--ink-soft);
  padding: var(--space-2) 0;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}

.navbar__link:hover,
.navbar__link[aria-current="page"] {
  color: var(--ink);
  border-color: var(--primary);
}

/* A category link that drops down its sub-categories. The link stays a
   link to its own page, and a small chevron button beside it is the
   disclosure: a mouse opens the panel by hovering, a keyboard by pressing
   the button (.is-open, set by the script with aria-expanded). Escape shuts
   it even under the pointer (.is-dismissed) until the pointer leaves.
   Without a script the panel still opens whenever focus is inside the item.
   One level only. */
.navbar__item {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.navbar__toggle {
  display: inline-grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  margin-inline-start: calc(-1 * var(--space-1));
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--dur-fast), transform var(--dur-fast);
}

.navbar__toggle:hover,
.navbar__item:hover .navbar__toggle {
  color: var(--ink);
}

.navbar__toggle[aria-expanded="true"] .icon {
  transform: rotate(180deg);
}

.navbar__submenu {
  position: absolute;
  top: 100%;
  left: calc(-1 * var(--space-3));
  z-index: 1;
  display: none;
  flex-direction: column;
  min-width: 12rem;
  padding: var(--space-2);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-2);
}

.navbar__item:hover .navbar__submenu,
.navbar__item.is-open .navbar__submenu,
html:not(.js) .navbar__item:focus-within .navbar__submenu {
  display: flex;
}

.navbar__item.is-dismissed .navbar__submenu {
  display: none;
}

.navbar__sublink {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
  font-weight: 700;
  white-space: nowrap;
  transition: color var(--dur-fast), background var(--dur-fast);
}

.navbar__sublink:hover,
.navbar__sublink:focus-visible {
  color: var(--ink);
  background: color-mix(in srgb, var(--primary) 14%, transparent);
}

.navbar__actions {
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.navbar__cart {
  position: relative;
}

.navbar__cart .badge--count {
  position: absolute;
  top: -0.2rem;
  right: -0.2rem;
}

.navbar__burger {
  display: inline-flex;
}

.search {
  position: relative;
  display: flex;
  align-items: center;
}

/* The input wears the field control's look; the pill button sits inside
   its right edge. */
.search__input {
  padding-right: 3rem;
}

.search__submit {
  position: absolute;
  right: 0.25rem;
}

/* The dropdown under the field. Absolutely placed so the page under it
   does not move; the same raised surface and shadow the drawer uses.
   Anchored to the field's right edge, not its left: the form sits next to
   the navbar's actions, so a field narrowed by a long header menu would
   otherwise drag the pop's own left edge inward with it, collapsing a
   compact row's title column to nothing. Anchoring right instead means
   the pop's extra width — it is never narrower than a readable row —
   grows leftward, away from the actions, and stays on screen. The list can
   still run past the viewport's bottom, so it scrolls itself rather than
   the page. The phone bar sets it static, so it scrolls with the page
   instead. */
.search__pop {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: auto;
  right: 0;
  width: max(100%, 28rem);
  max-width: calc(100vw - 2rem);
  max-height: min(70vh, 32rem);
  overflow-y: auto;
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
  overflow-x: hidden;
  z-index: calc(var(--z-navbar) + 1);
}

.search__group {
  padding: var(--space-2) var(--space-2) var(--space-1);
}

.search__group + .search__group {
  border-top: 1px solid var(--line);
}

.search__group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: calc(var(--label-tracking) * 1.5);
  text-transform: var(--label-transform);
  color: var(--primary);
}

.search__group-count {
  letter-spacing: 0;
  text-transform: none;
  font-weight: 500;
  color: var(--ink-muted);
}

.search__more {
  display: block;
  padding: var(--space-1) var(--space-3) var(--space-2);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--secondary);
}

/* The arrow the same "all N" copy gets on the results page from
   .link--arrow; the copy itself carries none, or the page drew two. */
.search__more::after {
  content: " →";
}

/* The keyboard stops on the "show all" link like on any other option, so
   it gets the same highlight as one. */
.search__more:hover,
.search__more[aria-selected="true"] {
  background: var(--primary-soft);
}

.search__all {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--line);
  background: var(--stage-mid);
  font-weight: 700;
  color: inherit;
}

.search__all:hover,
.search__all[aria-selected="true"] {
  background: var(--primary-soft);
}

.search__all-count {
  color: var(--ink-muted);
  font-weight: 500;
}

.search__empty {
  padding: var(--space-4) var(--space-5);
  color: var(--ink-muted);
}

/* The search field: a form between the menu links and the actions. On the
   desktop it is always there; below 1024px it is hidden and [3/3] adds the
   magnifier that opens a bar under the navbar instead.
   This rule (and its media twin below) must come after .search so that at
   equal specificity display:none wins below 1024px and display:flex wins
   above it — the form carries both classes (ui/searchfield.go), and .search
   sets display:flex on the same element. */
.navbar__search {
  display: none;
  flex: 1 1 14rem;
  max-width: 30rem;
  margin-inline-start: var(--space-2);
  /* A flex item's automatic min-width is its content's intrinsic width; without
     resetting it to 0 the input refuses to shrink and overflows a busy header
     at the desk breakpoint, where the links come back. */
  min-width: 0;
}

@media (min-width: 1024px) {
  .navbar__search {
    display: flex;
  }
}

/* The phone: the field folds into a magnifier among the actions, and a
   bar under the navbar opens with the same form. Above 1024px the button
   and the bar do not exist; below it the inline form does not. */
.navbar__open-search {
  display: inline-flex;
}

.navbar__searchbar {
  display: none;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--stage) 82%, transparent);
}

.navbar__searchbar.is-open {
  display: block;
}

.navbar__searchbar .search {
  position: static;
  /* .search is display:flex; without wrapping the static pop below would
     sit beside the input as a flex item instead of dropping under it. */
  flex-wrap: wrap;
}

.navbar__searchbar .search__pop {
  position: static;
  width: auto;
  /* Forces the pop onto its own line under the input, and to the input's
     full width, inside the wrapping flex row above. */
  flex: 1 0 100%;
  margin-top: var(--space-2);
  box-shadow: none;
  max-height: none;
  overflow-y: visible;
}

@media (min-width: 1024px) {
  .navbar__open-search,
  .navbar__searchbar,
  .navbar__searchbar.is-open {
    display: none;
  }
}

/* The phone's bar holds the wordmark and three 44px targets — search, cart,
   burger. A fourth and a fifth (theme, account) pushed the bar to 413px on
   a 390px screen, and a phone answers that by zooming the whole page out.
   Below 1024px the theme toggle lives in the drawer's foot and the account
   link among the drawer's links, where both already had a place. The
   toggle is picked by the attribute the script finds it by, because Btn
   takes no class of its own. */
.navbar__actions [data-toggle-theme],
.navbar__account {
  display: none;
}

@media (min-width: 1024px) {
  .navbar__links {
    display: flex;
  }

  .navbar__burger {
    display: none;
  }

  .navbar__actions [data-toggle-theme],
  .navbar__account {
    display: inline-flex;
  }
}

/* Drawer (mobile menu) ------------------------------------------------ */

.drawer {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  visibility: hidden;
  /* Hidden only once the panel has slid out; without the delay it vanished
     in the first frame of closing and the slide was never seen. */
  transition: visibility 0s linear var(--dur-base);
}

.drawer.is-open {
  visibility: visible;
  transition-delay: 0s;
}

.drawer__backdrop {
  position: absolute;
  inset: 0;
  background: var(--backdrop);
  opacity: 0;
  transition: opacity var(--dur-base);
}

.drawer__panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(20rem, 85vw);
  padding: var(--space-6);
  background: var(--stage-mid);
  border-left: 1px solid var(--line);
  box-shadow: var(--shadow-2);
  transform: translateX(100%);
  transition: transform var(--dur-base) var(--ease-out);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  /* The panel's own menu runs to two dozen links across seven groups, which
     is taller than a phone; without this the last groups sit below the fold
     of a fixed panel with no way to reach them. */
  overflow-y: auto;
}

.drawer.is-open .drawer__backdrop {
  opacity: 1;
}

.drawer.is-open .drawer__panel {
  transform: none;
}

.drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* The brand in the drawer's head, the same wordmark or logo as the bar's. */
.drawer__brand {
  display: inline-flex;
  align-items: center;
  min-width: 0;
}

.drawer__links {
  display: flex;
  flex-direction: column;
}

/* The foot holds the theme toggle the phone's bar has no room for. It is
   pushed to the bottom of the panel when the links are short. */
.drawer__foot {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}

.drawer__foot .btn {
  justify-content: flex-start;
}

/* Dialog ---------------------------------------------------------------
   The one shell every native <dialog> in the shop wears: the version
   picker, the locker picker, the lightbox — and every native popover card,
   like the calendar's, which is open while :popover-open. showModal() or the
   popover attribute supplies the top layer and Escape; this supplies the
   surface, the head, the backdrop and the motion. It opens with a fade and a slight scale:
   @starting-style gives the opening its first frame, and overlay and
   display with allow-discrete keep the dialog in the top layer until the
   closing fade is over. A browser without either opens and closes at once.
   A pack adds only what its content needs — a width, a height, what sits
   inside — never a second surface or a second backdrop. */
.dialog {
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  color: var(--ink);
  box-shadow: var(--shadow-2);
  opacity: 0;
  transform: scale(0.97);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out),
    overlay var(--dur-base) allow-discrete,
    display var(--dur-base) allow-discrete;
}

.dialog:is([open], :popover-open) {
  opacity: 1;
  transform: none;
}

@starting-style {
  .dialog:is([open], :popover-open) {
    opacity: 0;
    transform: scale(0.97);
  }
}

.dialog::backdrop {
  background: transparent;
  transition:
    background-color var(--dur-base),
    overlay var(--dur-base) allow-discrete,
    display var(--dur-base) allow-discrete;
}

.dialog:is([open], :popover-open)::backdrop {
  background: var(--backdrop);
}

@starting-style {
  .dialog:is([open], :popover-open)::backdrop {
    background: transparent;
  }
}

.dialog__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--line);
}

.dialog__head h2 {
  margin: 0;
  font-size: var(--text-lg);
}

/* A control floating over the content — the lightbox's close and arrows. It
   carries its own dark disc, because a photograph can be any colour under
   it; the pack says where it sits. */
.dialog__control {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--scrim);
  color: var(--hero-ink);
  cursor: pointer;
}

/* A sheet: on a phone the question comes up from the bottom edge, the way
   an app asks. From the tablet up it is the dialog above, centred. */
@media (max-width: 639px) {
  .dialog--sheet {
    position: fixed;
    inset: auto 0 0 0;
    width: 100%;
    max-width: none;
    margin: 0;
    border-bottom: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    transform: translateY(24px);
  }

  .dialog--sheet:is([open], :popover-open) {
    transform: none;
  }

  @starting-style {
    .dialog--sheet:is([open], :popover-open) {
      transform: translateY(24px);
    }
  }
}

.drawer__link {
  padding: var(--space-3) 0;
  font-size: var(--text-lg);
  font-weight: 700;
  border-bottom: 1px solid var(--line);
}

.drawer__link:hover {
  color: var(--primary);
}

/* A drop-down link in the drawer: a native disclosure, so the browser keeps
   its open state and tells a screen reader. Its first entry is the parent's
   own page, since the summary itself is no link. */
.drawer__group {
  border-bottom: 1px solid var(--line);
}

.drawer__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) 0;
  font-size: var(--text-lg);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}

.drawer__summary::-webkit-details-marker {
  display: none;
}

.drawer__summary .icon {
  transition: transform var(--dur-fast);
}

.drawer__group[open] .drawer__summary .icon {
  transform: rotate(180deg);
}

.drawer__sublinks {
  display: flex;
  flex-direction: column;
  padding: 0 0 var(--space-3) var(--space-4);
}

.drawer__sublink {
  padding: var(--space-2) 0;
  color: var(--ink-soft);
  font-weight: 700;
}

.drawer__sublink:hover {
  color: var(--primary);
}

/* Breadcrumbs --------------------------------------------------------- */

.breadcrumbs {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  padding-block: var(--space-4);
}

.breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.breadcrumbs li + li::before {
  content: "/";
  margin-inline-end: var(--space-2);
  opacity: 0.6;
}

.breadcrumbs a:hover {
  color: var(--secondary);
}

.breadcrumbs [aria-current="page"] {
  color: var(--ink);
  font-weight: 700;
}

/* On a phone the trail is one step back: the parent crumb alone, with a
   chevron, where the whole trail wrapped a product's full name over three
   lines above its gallery and then repeated it in the h1 right below. The
   markup keeps every crumb — the crawler's BreadcrumbList and a desk see
   the whole trail. */
@media (max-width: 639px) {
  .breadcrumbs li {
    display: none;
  }

  .breadcrumbs li:nth-last-child(2) {
    display: block;
  }

  .breadcrumbs li:nth-last-child(2)::before {
    content: "‹";
    margin-inline-end: var(--space-1);
    opacity: 0.6;
  }
}

/* Product tile -------------------------------------------------------- */

.product-tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  color: inherit;
}

/* The caption: name, an optional version line, and the price, in one box so
   a carousel can move all three onto the cover together. In flow it draws
   nothing of its own — the spacing below is the rhythm the tile already had
   when the price stood outside the box holding the other two. */
.product-tile__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The price stands apart; the name and its version line read as one unit. */
.product-tile__body > .price {
  margin-top: var(--space-3);
}

/* The cover's box, its outline and its lift are read from variables rather
   than written here, so a carousel can restyle its tiles without a second
   copy of this block. Every default is what the tile had before the
   variables existed: a square, a large radius, a hairline, no filter. A
   tile outside a strip therefore renders exactly as it always did — the
   modifiers that set these live on .product-strip and nowhere else.

   --tile-filter is a filter rather than another box-shadow because a
   box-shadow traces the element's box and ignores what is painted inside
   it, while drop-shadow follows the alpha of the result — the difference
   between a halo that hugs a circle and one that squares it off.

   The clip does NOT go here, and that is the whole reason .product-tile__media
   and its content are two elements rather than one. CSS applies filter
   BEFORE clip-path, so an element that carries both has its own glow cut
   away at the clip: a star would come out with no halo at all. The filter
   stays on this box, the clip moves to what the box contains. */
.product-tile__media {
  position: relative;
  aspect-ratio: var(--tile-ratio, 1);
  overflow: hidden;
  border-radius: var(--tile-radius, var(--radius-lg));
  background: var(--tile-bg, var(--surface));
  border: var(--tile-border, 1px solid var(--line));
  filter: var(--tile-filter, none);
  transition: box-shadow var(--dur-base), border-color var(--dur-base), filter var(--dur-base);
}

/* The cover itself, cut to shape. The badges are the one child that keeps
   its own outline — a plakietka clipped to a star point is unreadable, and
   the shapes that clip drop their badges anyway. */
.product-tile__media > *:not(.product-tile__badges) {
  clip-path: var(--tile-clip, none);
}

.product-tile__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-base) var(--ease-out);
}

/* The lift is a variable for one reason: a clipped shape cannot use it, and
   a strip whose glow is set by the shop should not add a second one on top
   of it. Both cases set --tile-hover-shadow to none and light the tile with
   --tile-filter instead. */
.product-tile[href]:hover .product-tile__media {
  border-color: var(--primary);
  box-shadow: var(--tile-hover-shadow, var(--glow-pink));
}

.product-tile[href]:hover .product-tile__media img {
  transform: scale(1.03);
}

/* A tile with no page (an event without content) is drawn, not linked. */
.product-tile:not([href]) { cursor: default; }

/* The same badge stack sits on a tile's cover and on the product page's
   gallery panel, so the two read as one dictionary (README rule 6). */
.product-tile__badges,
.gallery__badges {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

/* Badges sit on the cover, so translucent variants get a solid ground. */
.product-tile__badges .badge,
.gallery__badges .badge {
  box-shadow: var(--shadow-1);
}

.product-tile__badges .badge--last,
.gallery__badges .badge--last {
  /* The gold, which stays light in the light themes; --warning there is a
     deep brown text colour, and --on-warning on it read 2.95:1. */
  background: var(--accent-gold);
  color: var(--on-warning);
}

.product-tile__badges .badge--info,
.gallery__badges .badge--info {
  background: var(--surface-raised);
  color: var(--secondary);
}

.product-tile__badges .badge--soldout,
.gallery__badges .badge--soldout {
  background: var(--surface-raised);
}

/* Same rule as the cover above: every axis of the caption is a variable
   whose default is what the tile already looked like, and only a strip's
   modifiers set them. The second line follows the first one's alignment
   and size — a name to the right over a price to the left reads as a
   mistake, not as two settings. */
.product-tile__name {
  font-family: var(--tile-text-font, inherit);
  font-size: var(--tile-text-size, inherit);
  font-weight: var(--tile-text-weight, 700);
  font-style: var(--tile-text-style, normal);
  letter-spacing: var(--tile-text-tracking, normal);
  text-transform: var(--tile-text-transform, none);
  text-align: var(--tile-text-align, inherit);
  color: var(--tile-text-color, inherit);
  line-height: 1.3;
}

.product-tile__meta {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  text-align: var(--tile-text-align, inherit);
}

/* .price is the second line on a product tile; .product-tile__meta is the
   second line on the tiles that carry no price (a collection's elements).
   Both are the same slot, so the alignment reaches both — but .price is an
   inline-flex row of its own parts, and text-align does not move a flex
   container's children. Hence a second variable rather than reusing the
   first: it carries the same choice in the words flexbox understands, and
   "justify" has no flex meaning at all, so it falls back to the start. */
.product-tile .price {
  justify-content: var(--tile-text-justify, flex-start);
}

/* A sold-out tile dims whatever stands in its cover, photo or placeholder —
   otherwise a product with no photo would be the one tile that ignores the
   state. Named rather than a child wildcard on purpose: the badges sit in the
   same slot, and the Soldout badge is the one thing that must stay legible. */
.product-tile--soldout .product-tile__media > img,
.product-tile--soldout .product-tile__media > .placeholder {
  filter: grayscale(0.8);
  opacity: 0.55;
}

.product-tile--soldout .price {
  color: var(--ink-muted);
}

/* The words "sold out" in the caption. The badge on the cover says it
   already, so the line stays out of sight (and out of a screen reader)
   until a tile shape with no corner for the badge drops it — tiles.css
   shows the line then, so the state is never grey alone. */
.product-tile__soldout {
  display: none;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--ink-muted);
  text-align: var(--tile-text-align, inherit);
}

/* Placeholder when there is no photo ------------------------------------
   A missing photo is not a missing thing, so the placeholder is drawn rather
   than greyed out: one of three marks over one of six covers.

   The cover is fixed, not themed. The placeholder stands where a photograph
   would stand, and a photograph does not change with the theme either — the
   same reason --hero-ink exists. Every ground therefore mixes its two accents
   into --ph-base rather than into a surface token.

   Both axes are modifiers of the same block: --vinyl/--stage/--camera choose
   the mark, --pink/--azure/--violet/--magenta/--mint/--brand choose the cover.
   A placeholder always carries one of each; ui.Placeholder fills in the
   subject's default mark and derives the cover from the name when the shop
   has not picked one. */
.placeholder {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  overflow: hidden;
  color: color-mix(in srgb, var(--hero-ink) 85%, transparent);
}

.placeholder .icon {
  filter: drop-shadow(0 2px 10px var(--scrim));
}

/* Covers. --ph-a and --ph-b are the two accents the marks paint with;
   --ph-base is the dark ground they are mixed into — the --duotone-* palette
   of tokens.css, which the hero's coloured grounds share. */
.placeholder--pink    { --ph-a: var(--duotone-pink-a);    --ph-b: var(--duotone-pink-b);    --ph-base: var(--duotone-pink-base); }
.placeholder--azure   { --ph-a: var(--duotone-azure-a);   --ph-b: var(--duotone-azure-b);   --ph-base: var(--duotone-azure-base); }
.placeholder--violet  { --ph-a: var(--duotone-violet-a);  --ph-b: var(--duotone-violet-b);  --ph-base: var(--duotone-violet-base); }
.placeholder--magenta { --ph-a: var(--duotone-magenta-a); --ph-b: var(--duotone-magenta-b); --ph-base: var(--duotone-magenta-base); }
.placeholder--mint    { --ph-a: var(--duotone-mint-a);    --ph-b: var(--duotone-mint-b);    --ph-base: var(--duotone-mint-base); }
.placeholder--brand   { --ph-a: var(--duotone-brand-a);   --ph-b: var(--duotone-brand-b);   --ph-base: var(--duotone-brand-base); }

/* Vinyl: a record in its sleeve — grooves, a coloured label, the spindle
   hole. The record is the mark, so this one draws no icon over it. */
.placeholder--vinyl {
  background:
    radial-gradient(circle at 50% 50%, var(--duotone-ink) 0 2.5%, transparent 3%),
    repeating-radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--white) 5.5%, transparent) 0 1.5px, transparent 1.5px 7px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--ph-a) 88%, var(--duotone-magenta-base)) 0 13%, transparent 13.5%),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--ph-base) 78%, var(--duotone-ink)) 0 47%, transparent 47.6%),
    linear-gradient(145deg, color-mix(in srgb, var(--ph-a) 34%, var(--ph-base)), color-mix(in srgb, var(--ph-b) 34%, var(--ph-base)));
}

.placeholder--vinyl .icon {
  display: none;
}

/* Stage: the concert backdrop the hero and the footer already use, at tile
   size — two beams from above, a glow on the floor, a few sparkles. */
.placeholder--stage {
  background:
    radial-gradient(1.6px 1.6px at 24% 27%, color-mix(in srgb, var(--white) 90%, transparent), transparent 62%),
    radial-gradient(1.4px 1.4px at 71% 17%, color-mix(in srgb, var(--white) 70%, transparent), transparent 62%),
    radial-gradient(2px 2px at 80% 47%, color-mix(in srgb, var(--white) 50%, transparent), transparent 62%),
    radial-gradient(1.2px 1.2px at 41% 63%, color-mix(in srgb, var(--white) 60%, transparent), transparent 62%),
    radial-gradient(80% 48% at 50% 108%, color-mix(in srgb, var(--ph-a) 62%, transparent), transparent 74%),
    linear-gradient(180deg, transparent 20%, color-mix(in srgb, var(--ph-base) 85%, transparent) 95%),
    conic-gradient(from 165deg at 18% -16%, transparent 0 4deg, color-mix(in srgb, var(--ph-b) 40%, transparent) 9deg, transparent 15deg 360deg),
    conic-gradient(from 177deg at 82% -16%, transparent 0 5deg, color-mix(in srgb, var(--ph-a) 34%, transparent) 11deg, transparent 18deg 360deg),
    linear-gradient(180deg, var(--ph-base) 0%, var(--duotone-ink) 100%);
}

/* Camera: the plain mark, on the same duotone ground the other two are
   built from. The choice for a shop that would rather not imply a record. */
.placeholder--camera {
  background:
    radial-gradient(120% 80% at 12% 0%, color-mix(in srgb, var(--white) 18%, transparent), transparent 55%),
    linear-gradient(150deg, color-mix(in srgb, var(--ph-a) 72%, var(--ph-base)), color-mix(in srgb, var(--ph-b) 76%, var(--ph-base)));
}

/* The four marks below are the events' own: a photo-less event is drawn
   with the one its kind names (cake for a birthday, ticket for a plain
   event, cd for a release, fireworks for an anniversary; a concert keeps
   the stage). Each is a picture on its own, so like the vinyl it hides the
   icon laid over it. Everything is in percentages of the box, so the same
   drawing fits the panel's 2.75rem chip and a 16rem tile.

   Where a shape needs rounded corners or a soft edge — the cake's tiers,
   the ticket, the discs, the bursts — it is a pseudo-element, since a
   gradient layer cannot be rounded or masked on its own. */

/* Cake: two tiers with scalloped icing on a plate, three candles lit, and
   confetti in the air. The tiers take the cover's two accents, the icing is
   near-white, the flames the one fixed warm colour the palette has. */
.placeholder--cake {
  --ph-icing: color-mix(in srgb, var(--white) 90%, var(--ph-a));
  background:
    /* flames, and the glow they throw */
    radial-gradient(3.2% 4.6% at 38% 34%, var(--accent-gold-deep) 0 40%, transparent 100%),
    radial-gradient(3.2% 4.6% at 50% 32%, var(--accent-gold-deep) 0 40%, transparent 100%),
    radial-gradient(3.2% 4.6% at 62% 34%, var(--accent-gold-deep) 0 40%, transparent 100%),
    radial-gradient(circle at 50% 36%, color-mix(in srgb, var(--accent-gold-deep) 26%, transparent), transparent 24%),
    /* candles: striped, from the flame down into the top tier */
    repeating-linear-gradient(-55deg, var(--ph-icing) 0 1.5px, var(--ph-b) 1.5px 3.5px) 37.6% 41% / 3% 12% no-repeat,
    repeating-linear-gradient(-55deg, var(--ph-icing) 0 1.5px, var(--ph-b) 1.5px 3.5px) 50% 39% / 3% 12% no-repeat,
    repeating-linear-gradient(-55deg, var(--ph-icing) 0 1.5px, var(--ph-b) 1.5px 3.5px) 62.4% 41% / 3% 12% no-repeat,
    /* confetti: dots and slanted dashes in both accents */
    radial-gradient(2px 2px at 17% 26%, var(--ph-b) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 84% 22%, var(--ph-a) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 76% 56%, var(--ph-icing) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 22% 62%, var(--ph-icing) 0 60%, transparent 100%),
    linear-gradient(35deg, transparent 43%, var(--ph-a) 43% 57%, transparent 57%) 13% 42% / 6% 6% no-repeat,
    linear-gradient(-30deg, transparent 43%, var(--ph-b) 43% 57%, transparent 57%) 88% 40% / 6% 6% no-repeat,
    linear-gradient(60deg, transparent 43%, var(--ph-icing) 43% 57%, transparent 57%) 27% 14% / 5% 5% no-repeat,
    linear-gradient(-60deg, transparent 43%, var(--ph-a) 43% 57%, transparent 57%) 70% 12% / 5% 5% no-repeat,
    /* the plate, the glow behind the cake, the ground */
    radial-gradient(38% 5.5% at 50% 83%, color-mix(in srgb, var(--white) 48%, var(--ph-base)) 0 96%, transparent 100%),
    radial-gradient(circle at 50% 58%, color-mix(in srgb, var(--ph-a) 34%, transparent), transparent 52%),
    linear-gradient(180deg, var(--ph-base) 0%, var(--duotone-ink) 100%);
}

.placeholder--cake::before,
.placeholder--cake::after {
  content: "";
  position: absolute;
  left: 50%;
  translate: -50% 0;
  border-radius: 10% / 22%;
  box-shadow: 0 4% 6% color-mix(in srgb, var(--duotone-ink) 45%, transparent);
  /* the icing: a band with a scalloped lower edge, dripping into the tier */
  background:
    radial-gradient(circle at 50% 50%, var(--ph-icing) 0 44%, transparent 48%) 0 6% / 14% 30% repeat-x,
    linear-gradient(180deg, var(--ph-icing) 0 24%, transparent 24%),
    linear-gradient(180deg, var(--ph-tier), color-mix(in srgb, var(--ph-tier) 70%, var(--duotone-ink)));
}

.placeholder--cake::before {
  --ph-tier: color-mix(in srgb, var(--ph-a) 78%, var(--ph-base));
  top: 63%;
  width: 58%;
  height: 21%;
}

.placeholder--cake::after {
  --ph-tier: color-mix(in srgb, var(--ph-b) 78%, var(--ph-base));
  top: 47%;
  width: 42%;
  height: 18%;
}

.placeholder--cake .icon {
  display: none;
}

/* Ticket: two stubs fanned out, the front one with its perforation and
   punched notches, a coloured band and two lines of print. The notches are
   cut with a mask, which is the one thing a gradient cannot do to a box. */
.placeholder--ticket {
  background:
    radial-gradient(120% 80% at 12% 0%, color-mix(in srgb, var(--white) 14%, transparent), transparent 55%),
    radial-gradient(2px 2px at 20% 78%, var(--ph-b) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 82% 24%, var(--ph-a) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 88% 70%, var(--white) 0 60%, transparent 100%),
    linear-gradient(150deg, color-mix(in srgb, var(--ph-a) 46%, var(--ph-base)), color-mix(in srgb, var(--ph-b) 50%, var(--ph-base)));
}

.placeholder--ticket::before,
.placeholder--ticket::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 66%;
  height: 36%;
  border-radius: 7% / 13%;
  box-shadow: 0 5% 7% color-mix(in srgb, var(--duotone-ink) 45%, transparent);
  -webkit-mask:
    radial-gradient(circle at 68% 0, transparent 0 5.5%, var(--duotone-ink) 6%),
    radial-gradient(circle at 68% 100%, transparent 0 5.5%, var(--duotone-ink) 6%);
  -webkit-mask-composite: source-in;
  mask:
    radial-gradient(circle at 68% 0, transparent 0 5.5%, var(--duotone-ink) 6%),
    radial-gradient(circle at 68% 100%, transparent 0 5.5%, var(--duotone-ink) 6%);
  mask-composite: intersect;
}

/* The one behind: plain paper, turned the other way, showing only a rim. */
.placeholder--ticket::after {
  translate: -46% -58%;
  rotate: 6deg;
  background: color-mix(in srgb, var(--white) 62%, var(--ph-b));
}

.placeholder--ticket::before {
  translate: -52% -44%;
  rotate: -9deg;
  z-index: 1;
  background:
    /* the perforation between the two stubs */
    repeating-linear-gradient(180deg, transparent 0 7%, color-mix(in srgb, var(--ph-base) 55%, transparent) 7% 14%) 68% 0 / 1.6% 100% no-repeat,
    /* two lines of print and a coloured band on the big stub */
    linear-gradient(var(--ph-a), var(--ph-a)) 12% 38% / 34% 7% no-repeat,
    linear-gradient(color-mix(in srgb, var(--ph-a) 55%, transparent), color-mix(in srgb, var(--ph-a) 55%, transparent)) 12% 60% / 22% 7% no-repeat,
    linear-gradient(90deg, var(--ph-a), var(--ph-b)) 0 0 / 68% 16% no-repeat,
    /* a bar on the small stub */
    linear-gradient(var(--ph-b), var(--ph-b)) 87% 50% / 5% 44% no-repeat,
    color-mix(in srgb, var(--white) 90%, var(--ph-a));
}

.placeholder--ticket .icon {
  display: none;
}

/* CD: a compact disc sliding out of its case, its face throwing the rainbow
   a disc does — the cover's two accents through silver — with a clear ring
   and the hole in the middle. */
.placeholder--cd {
  --ph-silver: color-mix(in srgb, var(--white) 80%, var(--ph-base));
  background:
    radial-gradient(circle at 44% 50%, color-mix(in srgb, var(--ph-a) 42%, transparent), transparent 46%),
    radial-gradient(1.6px 1.6px at 18% 24%, var(--white) 0 60%, transparent 100%),
    radial-gradient(1.6px 1.6px at 84% 78%, var(--white) 0 60%, transparent 100%),
    linear-gradient(180deg, color-mix(in srgb, var(--ph-b) 22%, var(--ph-base)) 0%, var(--duotone-ink) 100%);
}

/* The case: a translucent square standing behind and to the right. */
.placeholder--cd::after {
  content: "";
  position: absolute;
  top: 17%;
  left: 36%;
  width: 52%;
  height: 66%;
  border-radius: 5%;
  border: 1px solid color-mix(in srgb, var(--white) 34%, transparent);
  background:
    linear-gradient(color-mix(in srgb, var(--white) 26%, transparent), color-mix(in srgb, var(--white) 26%, transparent)) 0 0 / 14% 100% no-repeat,
    color-mix(in srgb, var(--white) 8%, transparent);
}

.placeholder--cd::before {
  content: "";
  position: absolute;
  top: 19%;
  left: 12%;
  width: 62%;
  aspect-ratio: 1;
  border-radius: 50%;
  z-index: 1;
  box-shadow: 0 5% 8% color-mix(in srgb, var(--duotone-ink) 55%, transparent);
  background:
    /* hole, clear ring, and the highlight across the face */
    radial-gradient(circle, var(--duotone-ink) 0 13%, color-mix(in srgb, var(--white) 30%, var(--ph-base)) 13.5% 20%, color-mix(in srgb, var(--white) 18%, transparent) 20.5% 22%, transparent 22.5%),
    radial-gradient(circle at 34% 28%, color-mix(in srgb, var(--white) 55%, transparent), transparent 42%),
    conic-gradient(from 200deg,
      var(--ph-silver) 0,
      var(--ph-a) 11%,
      var(--ph-silver) 21%,
      var(--ph-b) 36%,
      var(--ph-silver) 48%,
      var(--ph-a) 64%,
      var(--ph-silver) 76%,
      var(--ph-b) 89%,
      var(--ph-silver) 100%);
}

.placeholder--cd .icon {
  display: none;
}

/* Fireworks: a big burst in one accent, a smaller one in the other, and
   sparks hanging in the air. A burst is rays from a repeating conic
   gradient, faded out at the rim by a mask so they trail off rather than
   stop. */
.placeholder--fireworks {
  background:
    /* sparks at the tips of the big burst's rays, and a few strays */
    radial-gradient(2.2px 2.2px at 48% 16%, var(--white) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 68% 24%, var(--ph-a) 0 60%, transparent 100%),
    radial-gradient(2.2px 2.2px at 76% 46%, var(--white) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 66% 66%, var(--ph-a) 0 60%, transparent 100%),
    radial-gradient(2.2px 2.2px at 30% 68%, var(--white) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 20% 46%, var(--ph-a) 0 60%, transparent 100%),
    radial-gradient(2px 2px at 28% 24%, var(--white) 0 60%, transparent 100%),
    radial-gradient(1.6px 1.6px at 86% 14%, var(--ph-b) 0 60%, transparent 100%),
    radial-gradient(1.6px 1.6px at 90% 36%, var(--white) 0 60%, transparent 100%),
    radial-gradient(1.6px 1.6px at 12% 84%, var(--ph-b) 0 60%, transparent 100%),
    radial-gradient(1.6px 1.6px at 56% 88%, var(--ph-a) 0 60%, transparent 100%),
    /* the glow at each heart, and the ground */
    radial-gradient(circle at 48% 44%, color-mix(in srgb, var(--ph-a) 55%, transparent), transparent 16%),
    radial-gradient(circle at 80% 26%, color-mix(in srgb, var(--ph-b) 45%, transparent), transparent 10%),
    linear-gradient(180deg, var(--ph-base) 0%, var(--duotone-ink) 100%);
}

.placeholder--fireworks::before,
.placeholder--fireworks::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  -webkit-mask: radial-gradient(circle, var(--duotone-ink) 0 30%, transparent 70%);
  mask: radial-gradient(circle, var(--duotone-ink) 0 30%, transparent 70%);
}

.placeholder--fireworks::before {
  top: 44%;
  left: 48%;
  width: 62%;
  aspect-ratio: 1;
  translate: -50% -50%;
  background:
    repeating-conic-gradient(from 8deg, color-mix(in srgb, var(--white) 72%, var(--ph-a)) 0 1.3deg, transparent 1.3deg 22.5deg),
    repeating-conic-gradient(from -3deg, color-mix(in srgb, var(--ph-a) 90%, transparent) 0 2deg, transparent 2deg 22.5deg);
}

.placeholder--fireworks::after {
  top: 26%;
  left: 80%;
  width: 34%;
  aspect-ratio: 1;
  translate: -50% -50%;
  background:
    repeating-conic-gradient(from 5deg, color-mix(in srgb, var(--ph-b) 92%, transparent) 0 2.2deg, transparent 2.2deg 30deg);
}

.placeholder--fireworks .icon {
  display: none;
}

/* Result row ---------------------------------------------------------- */

/* One row of the search results for the kinds that are not products: a
   track, an article, an event. Cover on the left (a photo, the kind's icon
   on a flat ground, or a day-and-month tile), title and a context line in
   the middle, an aside on the right. The event list has its own .event-row
   with a 6rem photo and no aside; this one is denser and carries three
   covers, which is why it is not the same block. */
.result-rows {
  display: grid;
  gap: var(--space-2);
}

.result-row {
  display: grid;
  grid-template-columns: 3.25rem 1fr auto;
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: inherit;
  transition: border-color var(--dur-fast);
}

.result-row[href]:hover {
  border-color: var(--primary);
}
/* A hit with no page (an event without content) is a row, not a link. */
.result-row:not([href]) { cursor: default; }

.result-row__cover {
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--stage-mid);
  color: var(--ink-soft);
}

.result-row__cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.result-row__cover--flat .icon {
  width: 1.3rem;
  height: 1.3rem;
}

/* The date tile: the day large in the brand face, the month small above it
   in the tracking uppercase labels use. */
.result-row__cover--date {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

.result-row__day {
  font-family: var(--font-brand);
  font-size: var(--text-xl);
  font-weight: 700;
}

.result-row__month {
  font-size: var(--text-xs);
  letter-spacing: var(--label-tracking);
  text-transform: var(--label-transform);
  color: var(--ink-muted);
}

.result-row__body {
  min-width: 0;
}

.result-row__title {
  /* A span (flow content is invalid inside .result-row__body's span) blocked
     out to keep the title and sub stacked on their own lines. */
  display: block;
  font-weight: 700;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.result-row__sub {
  display: block;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-top: 0.1rem;
}

.result-row__aside {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--secondary);
  white-space: nowrap;
  text-align: right;
  font-variant-numeric: var(--figures);
}

.result-row__note {
  display: block;
  font-weight: 500;
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

@media (max-width: 639px) {
  .result-row {
    grid-template-columns: 2.75rem 1fr;
  }

  .result-row__aside {
    display: none;
  }
}

/* The same row, packed for the dropdown: a smaller cover, no border of its
   own, a tinted ground when hovered or chosen with the keyboard. */
.result-row--compact {
  grid-template-columns: 2.5rem 1fr auto;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 0;
  background: transparent;
}

.result-row--compact .result-row__cover {
  width: 2.5rem;
  height: 2.5rem;
}

.result-row--compact:hover,
.result-row--compact[aria-selected="true"] {
  background: var(--primary-soft);
}

/* A count beside a tab's label: the results page says how many hits each
   kind has before the visitor clicks. */
/* The count beside a tab's label is a .badge; this is only what differs. */
.tabs__count {
  font-style: normal;
  margin-left: var(--space-2);
  background: var(--stage-mid);
  font-variant-numeric: var(--figures);
}

/* JS tabs mark the open one with aria-selected; link tabs (page-to-page,
   no tab widget behind them) mark it with aria-current="page" instead —
   same look, either way. */
.tabs__tab[aria-selected="true"] .tabs__count,
.tabs__tab[aria-current="page"] .tabs__count {
  background: var(--primary-soft);
  border-color: transparent;
}

/* A tab with nothing behind it is drawn, so the row keeps its shape, and
   is not a link. */
.tabs__tab[aria-disabled="true"] {
  opacity: 0.5;
  cursor: default;
}

/* Search head: the results page's heading and its one-line count. The
   query sits in the heading in the primary colour, quoted by the view. */
.search-head__lead {
  color: var(--ink-muted);
  margin-top: var(--space-1);
}

.search-head q {
  quotes: "„" "”";
  color: var(--primary);
}

/* Search block: the same field standing in a page's body rather than the
   header — a "search" content section, or the results page when the keeper
   has switched the header's field off. Capped at a reading width, and the
   dropdown hangs from the block's left edge at the block's width: here
   nothing crowds the field from the right, so the header's right-anchored
   overflow trick would only push the list off the block. */
.search-block {
  max-width: 36rem;
}

.search-block .search__pop {
  left: 0;
  right: auto;
  width: 100%;
}

/* Search section: the block as a content section, title and field on the
   page's centre line. A field hugging the left edge under a left-aligned
   heading reads as a form; in the middle of the page it reads as an
   invitation, which is what a page puts it there for. */
.search-section {
  text-align: center;
}

.search-section .section-header {
  justify-content: center;
}

.search-section .search-block {
  margin-inline: auto;
}

/* Product grid / strip ------------------------------------------------ */

/* Two covers a row from the narrowest phone up: one a row made a tile 500px
   tall on a 390px screen and a category of fifteen albums 8 500px long. The
   gap tightens with the tile, so the row reads as a pair, not two islands. */
.product-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-6) var(--space-3);
}

@media (min-width: 640px) {
  .product-grid { gap: var(--space-10) var(--space-6); }
}

@media (min-width: 1024px) {
  .product-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 1280px) {
  .product-grid { grid-template-columns: repeat(4, 1fr); gap: var(--space-10) var(--space-8); }
}

.product-strip {
  position: relative;
  /* A grid so the track and both arrows can share its first row: an arrow
     centred on that row sits at the middle of the tiles, whatever the dots
     or the progress bar add underneath. Measured against the whole strip,
     it drifted off the middle of round covers. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* Odstęp między kafelkami mieszka w zmiennej, bo czyta go i tor, i wzór na
     szerokość kafla. Ta sama liczba w dwóch miejscach rozjeżdża się przy
     pierwszej zmianie. */
  --strip-gap: var(--space-6);
}

.product-strip__track {
  grid-row: 1;
  grid-column: 1;
  display: flex;
  gap: var(--strip-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  padding-block: var(--space-2);
  margin-inline: calc(-1 * var(--space-2));
  padding-inline: var(--space-2);
  /* Musi być równe padding-inline. scroll-snap mierzy od obszaru przewijania,
     czyli od pudełka razem z zapasem, więc bez tego wyrównanie "start" zjeżdża
     o całą szerokość zapasu: pasek wczytuje się już przewinięty, z pierwszym
     kaflem do połowy za krawędzią, a przewinięcie skryptem do konkretnej
     strony ląduje obok. Na gołej stronie tego nie widać — zjazd nie ma czego
     obciąć — więc wychodzi dopiero w kadrze, który przycina. */
  scroll-padding-inline: var(--space-2);
}

.product-strip__track::-webkit-scrollbar {
  display: none;
}

.product-strip__track > * {
  flex: 0 0 16rem;
  scroll-snap-align: start;
}

/* Ile kafelków widać naraz. Liczba jedzie we własności --strip-per-view*,
   a obecność atrybutu włącza regułę: CSS umie zaczepić się o atrybut, ale nie
   umie wyjąć z niego liczby do calc(). Bez atrybutu obowiązuje sztywne 16rem
   wyżej, czyli dokładnie to, co pasek miał zawsze. */
.product-strip[data-per-view-narrow] .product-strip__track > * {
  flex-basis: calc(
    (100% - (var(--strip-per-view-narrow) - 1) * var(--strip-gap)) /
    var(--strip-per-view-narrow)
  );
}

@media (min-width: 640px) {
  .product-strip[data-per-view] .product-strip__track > * {
    flex-basis: calc(
      (100% - (var(--strip-per-view) - 1) * var(--strip-gap)) /
      var(--strip-per-view)
    );
  }
}

/* Kafelek „więcej” na końcu paska. Nie pożycza klas kafla produktu: jest
   dzieckiem toru, więc regułę na szerokość dostaje razem z resztą, a wygląd
   kafla zostaje wygladowi kafla. */
.product-strip__more {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 12rem;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  color: var(--ink-muted);
  text-align: center;
  padding: var(--space-4);
}

.product-strip__more:hover {
  border-color: var(--primary);
  color: var(--primary);
}

.product-strip__more-label {
  font-weight: 700;
}

.product-strip__dots {
  display: flex;
  justify-content: center;
  margin-top: var(--space-3);
}

/* The dot a person sees is 8px; the button around it is 24px, the smallest
   target WCAG 2.5.8 accepts, drawn by padding with the background clipped
   to the content box. The buttons touch, so the dots stand 24px apart. The
   current one is a longer pill as well as pink, so it is told apart by its
   shape and not by colour alone. */
.product-strip__dot {
  box-sizing: content-box;
  width: 0.5rem;
  height: 0.5rem;
  padding: 0.5rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--control-border);
  background-clip: content-box;
  cursor: pointer;
  transition: width var(--dur-fast) var(--ease-out);
}

.product-strip__dot.is-active {
  width: 1.25rem;
  background-color: var(--primary);
}

.product-strip__dot:focus-visible {
  outline-offset: -2px;
}

.product-strip__progress {
  height: 2px;
  margin-top: var(--space-4);
  background: var(--line);
  border-radius: 1px;
  overflow: hidden;
}

.product-strip__progress-bar {
  height: 100%;
  width: 0;
  background: var(--primary);
}

.product-strip__arrow {
  grid-row: 1;
  grid-column: 1;
  align-self: center;
  position: relative;
  z-index: 1;
  width: var(--control-height);
  height: var(--control-height);
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--surface-raised);
  color: var(--ink);
  box-shadow: var(--shadow-1);
  display: none;
  place-items: center;
}

.product-strip.has-overflow .product-strip__arrow {
  display: grid;
}

/* On a phone the strip is swiped, and the next tile already peeks in from
   the edge to say so; the arrows only covered the first and last cover.
   They stay for a mouse, which has no swipe. */
@media (max-width: 767px), (hover: none) and (pointer: coarse) {
  .product-strip.has-overflow .product-strip__arrow {
    display: none;
  }
}

.product-strip__arrow:hover {
  border-color: var(--primary);
  color: var(--primary);
}

.product-strip__arrow--prev { justify-self: start; }
.product-strip__arrow--next { justify-self: end; }

/* The pause of a self-scrolling strip: under the row, at its end, shown by
   the script once the strip moves. It shows what a press does — pause while
   the strip runs, play once it stands (aria-pressed="true"). */
.product-strip__pause {
  justify-self: end;
  margin-top: var(--space-2);
}

.product-strip__pause .icon + .icon,
.product-strip__pause[aria-pressed="true"] .icon:first-child {
  display: none;
}

.product-strip__pause[aria-pressed="true"] .icon + .icon {
  display: inline-block;
}

/* The arrows stand out past the row only where the page has a margin to take
   them. On a phone the gutter is narrower than the overhang, and an arrow
   sticking out past the screen made the whole page scroll sideways. */
@media (min-width: 1024px) {
  .product-strip--overhang .product-strip__arrow--prev { margin-inline-start: calc(-1 * var(--space-6)); }
  .product-strip--overhang .product-strip__arrow--next { margin-inline-end: calc(-1 * var(--space-6)); }
}

/* Section header ------------------------------------------------------ */

.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.section-header__title {
  font-size: var(--text-2xl);
}

.section-header__eyebrow {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: calc(var(--label-tracking) * 1.5);
  text-transform: var(--label-transform);
  color: var(--primary);
  margin-bottom: var(--space-1);
}

/* Pagination ---------------------------------------------------------- */

.pagination {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-8);
}

.pagination__item {
  min-width: var(--control-height-sm);
  height: var(--control-height-sm);
  padding-inline: var(--space-3);
  display: inline-grid;
  place-items: center;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-soft);
  font-weight: 700;
  text-decoration: none;
}

@media (max-width: 639px) {
  .pagination {
    gap: var(--space-1);
  }

  .pagination__item {
    padding-inline: var(--space-2);
  }
}

.pagination__item:hover {
  background: var(--surface-raised);
  color: var(--ink);
}

.pagination__item[aria-current="page"] {
  background: var(--primary-fill);
  color: var(--on-primary);
}

.pagination__item:disabled,
.pagination__item[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Sort bar ------------------------------------------------------------ */

.sort-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.sort-bar__count {
  color: var(--ink-muted);
  font-size: var(--text-sm);
  margin-inline-end: auto;
}

@media (max-width: 639px) {
  .sort-bar__count {
    flex-basis: 100%;
  }
}

.sort-bar .field__control {
  width: auto;
  min-height: 2.5rem;
  border-radius: var(--radius-pill);
}

/* Alerts -------------------------------------------------------------- */

.alert {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--surface);
}

.alert__body {
  flex: 1;
}

.alert__title {
  font-weight: 700;
  margin-bottom: var(--space-1);
}

/* A call-to-action inside an alert keeps its distance from the text. */
.alert__body .btn {
  margin-top: var(--space-3);
}

.alert ul {
  list-style: disc;
  padding-inline-start: 1.2em;
  font-size: var(--text-sm);
  line-height: 1.5rem;
}

/* An error summary is a list of links, one per refused field: rows a
   24px pitch apart plus a gap, so each link keeps the 24px target the
   standard asks for (WCAG 2.5.8) even as a single line of small text. */
.alert li + li {
  margin-top: var(--space-1);
}

/* A link in an alert speaks the alert's colour, underlined so it still
   reads as a link: the shop's violet on a tinted banner falls short of
   the contrast the text needs (WCAG 1.4.3). */
.alert a.link {
  color: inherit;
  text-decoration-color: currentcolor;
}

.alert--error { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.alert--success { border-color: var(--success); background: var(--success-soft); color: var(--success); }
.alert--info { border-color: var(--secondary); background: var(--secondary-soft); color: var(--secondary); }
.alert--warning { border-color: var(--warning); background: var(--warning-soft); color: var(--warning); }

.alert--bleed {
  border-radius: 0;
  border-inline: 0;
  justify-content: center;
  text-align: center;
}

/* Swapped content ------------------------------------------------------
   A region htmx replaces in place — a collection's next page, the next
   month of the calendar — fades in instead of cutting. It covers both
   shapes of swap: outerHTML puts .htmx-added on the region itself, innerHTML
   on its new child. The swap itself is never delayed: the new content is in
   place, at full size, in the same frame, and only its opacity catches up. */

.swap-fade,
.swap-fade > * {
  transition: opacity var(--dur-base) var(--ease-out);
}

.swap-fade.htmx-added,
.swap-fade > .htmx-added {
  opacity: 0;
}

/* Toast --------------------------------------------------------------- */

.toast-stack {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: min(24rem, calc(100vw - 2 * var(--space-4)));
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-left: 3px solid var(--secondary);
  box-shadow: var(--shadow-2);
  /* backwards, not both: a finished animation left filling forwards would
     hold opacity 1 over .is-leaving below, and the toast would drop out of
     the stack without ever fading. */
  animation: rise-in 0.35s var(--ease-out) backwards;
  transition:
    opacity var(--dur-base) var(--ease-in),
    transform var(--dur-base) var(--ease-in);
}

/* Set by the script that dismisses a toast, which removes it once this has
   played. The stack hangs from the bottom and the oldest toast, at the top,
   goes first, so the toasts under it do not move. */
.toast.is-leaving {
  opacity: 0;
  transform: translateX(1.5rem);
}

.toast--success { border-left-color: var(--success); }
.toast--error { border-left-color: var(--danger); }
.toast--info { border-left-color: var(--secondary); }

.toast__title {
  font-weight: 700;
}

.toast__text {
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.toast__close {
  margin-inline-start: auto;
  background: none;
  border: 0;
  color: var(--ink-muted);
  padding: 0;
}

/* Empty state --------------------------------------------------------- */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-12) var(--space-4);
  color: var(--ink-soft);
}

.empty-state .icon {
  color: var(--primary);
  filter: drop-shadow(var(--glow-pink));
}

.empty-state__title {
  font-size: var(--text-xl);
  color: var(--ink);
}

.empty-state--compact {
  padding: var(--space-6) var(--space-4);
}

/* A page whose whole content is one message — the 404, and anything else that
   is only a sentence and a way out.

   Without a floor the page is as tall as its three lines, so the footer climbs
   into the middle of the window and leaves a third of the screen empty below
   it, which reads as a half-loaded page rather than a quiet one. 60dvh puts
   the footer near the bottom on a laptop and off the first screen on a phone,
   and the message sits centred in what the navbar and the footer leave. */
.empty-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 60dvh;
}

/* Tabs ---------------------------------------------------------------- */

/* The bar scrolls sideways rather than squashing its tabs: the product editor
   has twelve, and a plain flex row shrank them into slivers long before it
   ran out of room. The tabs keep their own width and the bar takes the
   overflow. */
.tabs__list {
  display: flex;
  gap: var(--space-4);
  overflow-x: auto;
  /* overflow-x: auto on its own turns the other axis to auto as well, and a
     tab that sticks out below the row by even a pixel then gets a vertical
     scrollbar the height of the row. The tab used to do exactly that: its
     underline was pulled down over the row's own rule by a negative margin.
     The underline now sits inside the tab, so nothing sticks out; the
     vertical axis is pinned shut so nothing ever can. */
  overflow-y: hidden;
  scrollbar-width: thin;
  border-bottom: 1px solid var(--line);
  /* A disabled tab carries its count as a visually hidden span, which is
     position: absolute. Without a positioned ancestor that span is placed
     against the page, at the tab's own x — for a tab scrolled off the right
     edge that is past the viewport, and the whole document grows to hold
     it: the results page was 493px wide on a 390px phone. Positioning the
     list keeps the span inside the scroller, where the overflow clips it. */
  position: relative;
}

.tabs__tab {
  flex: none;
  /* A breath of room either side: the bar clips (overflow-y: hidden below),
     so the focus ring is drawn inside the tab, and this keeps it off the
     letters. The gap above is narrower by the same amount. */
  padding: var(--space-3) var(--space-1);
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--ink-muted);
  font-weight: 700;
  white-space: nowrap;
}

.tabs__tab:hover {
  color: var(--ink);
}

.tabs__tab:focus-visible {
  outline-offset: -2px;
}

/* JS tabs mark the open one with aria-selected; link tabs mark it with
   aria-current="page" instead — same underline, either way. */
.tabs__tab[aria-selected="true"],
.tabs__tab[aria-current="page"] {
  color: var(--ink);
  border-color: var(--primary);
}

.tabs__panel {
  padding-top: var(--space-6);
}

.tabs__panel[hidden] {
  display: none;
}

/* =====================================================================
   3. PAGE SECTIONS
   ===================================================================== */

/* Footer -------------------------------------------------------------- */

.footer {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin-top: var(--space-16);
  padding-block: var(--space-12) var(--space-8);
  background: var(--stage-mid);
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: var(--text-sm);
}

.footer::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -40%;
  width: min(90vw, 900px);
  height: 60%;
  transform: translateX(-50%);
  filter: blur(30px);
  background: radial-gradient(at 50% 100%, color-mix(in srgb, var(--primary-tint) 25%, transparent) 0%, color-mix(in srgb, var(--secondary) 12%, transparent) 45%, transparent 70%);
  z-index: -1;
  pointer-events: none;
}

.footer__inner {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .footer__inner {
    grid-template-columns: 2fr 1fr 1fr;
  }
}

.footer__brand {
  font-size: var(--text-xl);
  margin-bottom: var(--space-3);
}

.footer__title {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: calc(var(--label-tracking) * 1.5);
  text-transform: var(--label-transform);
  color: var(--ink-muted);
  margin-bottom: var(--space-3);
}

.footer__links {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.footer__links a:hover {
  color: var(--primary);
}

/* Cookie settings opens the banner and goes nowhere, so it is a button —
   dressed as the links it stands among. */
.footer__links button {
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.footer__links button:hover {
  color: var(--primary);
}

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

.footer__social,
.contact__social {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.footer__social a,
.contact__social a {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  transition: color var(--dur-fast), border-color var(--dur-fast), box-shadow var(--dur-base);
}

.footer__social a:hover,
.contact__social a:hover {
  color: var(--primary);
  border-color: var(--primary);
  box-shadow: var(--glow-pink);
}

.footer__social svg,
.contact__social svg {
  width: 1.1rem;
  height: 1.1rem;
  fill: currentcolor;
}

.footer__bottom {
  margin-top: var(--space-8);
  padding-top: var(--space-6);
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-3);
  color: var(--ink-muted);
}

/* =====================================================================
   Cookie consent banner — the fixed-bottom ask the storefront renders
   until the visitor decides; frontjs folds it away on either button, and
   the footer's cookie settings button opens it again.
   ===================================================================== */
.cookie-banner {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--z-toast);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-4);
  background: var(--surface-raised);
  border-top: 1px solid var(--line);
  box-shadow: var(--shadow-2);
}

.cookie-banner__body {
  max-width: 60ch;
  color: var(--ink-soft);
  font-size: var(--text-sm);
}

.cookie-banner__body p { margin: 0; }
.cookie-banner__body p + p { margin-top: var(--space-2); }

.cookie-banner__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* =====================================================================
   Forced colours (Windows high contrast)
   ===================================================================== */

/* The browser repaints every colour in the system palette and drops
   backgrounds and shadows, so a state drawn by a fill or a tint alone — a
   chosen chip, a ticked box, the current page, the carousel's dot, the
   availability dot — would vanish. Each of them is redrawn here in the
   system colours: Highlight for "chosen", CanvasText for the rest, with a
   thicker edge where the tint used to carry it. */
@media (forced-colors: active) {
  .chip[aria-pressed="true"],
  .chip:has(input:checked),
  .pagination__item[aria-current="page"] {
    border: 2px solid Highlight;
  }

  .tabs__tab[aria-selected="true"],
  .tabs__tab[aria-current="page"] {
    border-bottom-color: Highlight;
  }

  .checkbox:has(input:checked) .checkbox__mark {
    forced-color-adjust: none;
    background: Highlight;
    border-color: Highlight;
  }

  .checkbox:has(input:checked) .checkbox__mark::after {
    border-color: HighlightText;
  }

  .availability::before {
    border: 0.25rem solid currentcolor;
  }

  .product-strip__dot {
    forced-color-adjust: none;
    background-color: GrayText;
  }

  .product-strip__dot.is-active {
    background-color: Highlight;
  }

  .product-strip__progress {
    border: 1px solid CanvasText;
  }

  .product-strip__progress-bar {
    forced-color-adjust: none;
    background: Highlight;
  }
}
