@font-face {
  font-family: "BDO Grotesk";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn.jsdelivr.net/gh/LCTipografi/BDO-Grotesk@main/Fonts/Webfont/BDOGrotesk-Regular.woff2") format("woff2");
}
@font-face {
  font-family: "BDO Grotesk";
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn.jsdelivr.net/gh/LCTipografi/BDO-Grotesk@main/Fonts/Webfont/BDOGrotesk-Medium.woff2") format("woff2");
}
@font-face {
  font-family: "BDO Grotesk";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn.jsdelivr.net/gh/LCTipografi/BDO-Grotesk@main/Fonts/Webfont/BDOGrotesk-Bold.woff2") format("woff2");
}
:root {
  --ink: #111;
  --ink-contrast: #fff;
  /* #666/#999 measured at 5.5:1 / 2.7:1 against --ground — the latter
     fails WCAG AA (4.5:1) for normal text outright. Darkened both a step;
     --muted now clears 4.5:1 and --body comfortably clears AAA (7:1),
     while staying close enough to the originals that nothing looks
     recolored, just crisper. */
  --body: #5c5c5c;
  --muted: #707070;
  --hairline: #e6e6e6;
  --field-border: #e0e0e0;
  --surface: #f4f4f4;
  --panel: #f6f6f6;
  --hover-fill: #f0f0f0;
  --active-fill: #e6e6e6;
  --footer-mark: #d6d6d6;
  --accent: #d8ff53;
  --accent-contrast: #111;
  --ground: #fafafa;
  --scrim: rgba(17, 17, 17, 0.16);
  --ease-fluid: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* Vertical rhythm baseline — every major section/block spacing below is
     an integer multiple of this, so the page reads on one consistent
     vertical grid rather than a scatter of hand-picked pixel values. */
  --grid: 8px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #f2f2f2;
    --ink-contrast: #111;
    --body: #a9a9a9;
    /* #7d7d80 measured ~4.1:1 against --surface/--panel, under the 4.5:1
       AA floor for the small body copy it's used on (tags, meta text,
       captions). Lightened just enough to clear it against every surface
       this sits on, ground included. */
    --muted: #8f8f92;
    --hairline: #29292c;
    --field-border: #333336;
    --surface: #1c1c1f;
    --panel: #19191c;
    --hover-fill: #232326;
    --active-fill: #2c2c30;
    --footer-mark: #262629;
    --accent: #d8ff53;
    --accent-contrast: #111;
    --ground: #0f0f11;
    --scrim: rgba(0, 0, 0, 0.38);
  }
}
:root[data-theme="dark"] {
  --ink: #f2f2f2;
  --ink-contrast: #111;
  --body: #a9a9a9;
  --muted: #8f8f92;
  --hairline: #29292c;
  --field-border: #333336;
  --surface: #1c1c1f;
  --panel: #19191c;
  --hover-fill: #232326;
  --active-fill: #2c2c30;
  --footer-mark: #262629;
  --accent: #d8ff53;
  --accent-contrast: #111;
  --ground: #0f0f11;
  --scrim: rgba(0, 0, 0, 0.38);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--ground);
  color: var(--ink);
}

body {
  font-family: "BDO Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  transition: background-color 0.9s ease-in-out, color 0.9s ease-in-out;
}
/* Catch-all for the dark/light toggle: any element that doesn't already
   declare its own transition (an explicit rule always wins the cascade
   over this universal one, so hover/selected-state feedback elsewhere —
   nav underlines, licence-row fills, button hovers — keeps its own
   snappier timing untouched) fades its theme-driven colors in slowly
   instead of snapping instantly, so borders, icons, hairlines and
   surfaces we haven't individually hand-tuned still fade in step with
   body/header/panels rather than popping ahead of them. */
* {
  transition: background-color 0.9s ease-in-out, color 0.9s ease-in-out, border-color 0.9s ease-in-out,
    fill 0.9s ease-in-out, stroke 0.9s ease-in-out;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; font-size: inherit; color: inherit; cursor: pointer; }
img { display: block; max-width: 100%; }

.muted { color: var(--muted); }

:focus-visible {
  /* the accent is a light lime — a crisp ink ring carries the actual
     contrast against --ground, with the accent as a soft brand-colored
     glow just outside it rather than the whole ring's own visibility */
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--accent);
}

::selection {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* ---------- Header ---------- */
.header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--ground);
  /* Taller than the content strictly needs, so the enlarged-on-load logo
     (and the icons either side of it) get real clearance above them
     instead of sitting flush against the browser's own top edge. */
  height: 60px;
  padding: 0 20px;
  display: flex;
  align-items: center;
  /* The logo lives centered via absolute positioning (see .nav-mark-wrap)
     rather than as a normal flex item, so header-right is the only child
     left in flow — flex-end keeps it pinned to the right edge same as
     before. */
  justify-content: flex-end;
  gap: 24px;
  transition: background-color 0.9s ease-in-out;
}
/* Soft handoff into whatever scrolls up underneath the sticky bar,
   instead of content ending in a hard line right at its bottom edge —
   a thin gradient "shadow" of the header's own background, fading to
   nothing, laid over the top of the content rather than part of the
   header's own (fully opaque) box. Three stops instead of a flat two-
   stop fade so it reads as a smooth curve rather than a visible seam
   partway down. */
.header::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 28px;
  /* A plain, theme-colour background-color (not a gradient of colour
     stops) masked into the fade shape, rather than building the gradient
     itself out of var(--ground)/color-mix(). background-color is a
     <color>-typed property browsers interpolate smoothly between two
     resolved colours regardless of the var() behind it — a gradient's
     colour stops are not reliably interpolated the same way (observed in
     Chrome: it just snaps to the new stops almost immediately rather
     than easing), which is what made theme toggles flash the wrong-theme
     fade here for most of the transition while .header's own background
     eased normally beside it. The mask shape itself never depends on the
     theme, so it never needs to transition at all. */
  background-color: var(--ground);
  mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.55) 45%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.55) 45%, transparent 100%);
  pointer-events: none;
  transition: background-color 0.9s ease-in-out;
}
.wordmark {
  display: flex;
  align-items: center;
  transition: opacity 0.15s ease;
}
.wordmark-logo {
  display: block;
  height: 22px;
  width: auto;
  fill: var(--ink);
}

/* The homepage header's logo — centered in the bar (not a normal flex
   item, see .header's justify-content comment), starting enlarged on
   load and settling to the standard 22px-equivalent height once the
   page has scrolled even slightly (app.js toggles .scrolled). Scaling
   the wrapper rather than resizing it keeps this cheap: the canvas
   underneath renders at a fixed resolution regardless of scale, so nothing
   needs to re-fit or re-render on scroll, just a GPU transform. */
.nav-mark-wrap {
  position: absolute;
  left: 50%;
  top: 50%;
  height: 22px;
  aspect-ratio: 3103.5804 / 1203.9609;
  width: auto;
  display: block;
  transform: translate(-50%, -50%) scale(1.8);
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.nav-mark-wrap.scrolled {
  transform: translate(-50%, -50%) scale(1);
}
.nav-mark {
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  fill: var(--ink);
  transition: fill 0.9s ease-in-out, opacity 0.5s ease, transform 0.9s ease-in-out;
  /* The flat SVG fills its box edge-to-edge per its viewBox, but the
     WebGL chrome render (nav-chrome-logo.js) carries some margin around
     the lettering from its own camera framing, so the flat version reads
     slightly larger right up until the crossfade swaps it for the real
     thing. A small nudge down — re-tune this value by eye against the
     live chrome render, this wasn't reliably measurable headlessly. */
  transform: scale(0.94);
}
/* Matches the hover flourish the WebGL version gets in nav-chrome-logo.js
   (one eased extra turn) — this is the flat-SVG equivalent, shown before
   that's rendered a frame, or permanently if WebGL never loads. Using an
   animation rather than a transition on :hover means it always plays the
   same 0->360 turn forward, rather than reversing on hover-out. */
@keyframes nav-mark-spin {
  from { transform: scale(0.94) rotate(0deg); }
  to { transform: scale(0.94) rotate(360deg); }
}
.nav-mark-wrap:hover .nav-mark {
  fill: var(--accent);
  animation: nav-mark-spin 0.9s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
  .nav-mark-wrap { transition: none; }
  .nav-mark { transition: none; animation: none; }
  /* Same specificity as the hover rule above (it'd otherwise win over the
     plain .nav-mark override on specificity, media query or not). */
  .nav-mark-wrap:hover .nav-mark { animation: none; }
}
/* The real-time WebGL wordmark (see nav-chrome-logo.js) layers on top of
   the flat SVG above and crossfades in once it's actually rendered a
   frame — the SVG never leaves the DOM, so a WebGL failure or a slow/
   failed script load just leaves the flat version showing, with nothing
   extra to detect or recover from. */
.nav-mark-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 0.6s ease;
}
.nav-mark-wrap.chrome-ready .nav-mark-canvas { opacity: 1; }
.nav-mark-wrap.chrome-ready .nav-mark { opacity: 0; }

.header-right {
  display: flex;
  align-items: center;
  gap: 18px;
}
.icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 6px;
  margin: -6px;
  border-radius: 4px;
  color: var(--ink);
}
.icon-btn:hover { background: var(--hover-fill); }
.icon-moon { display: none; }
:root[data-theme="dark"] .icon-sun { display: none; }
:root[data-theme="dark"] .icon-moon { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .icon-sun { display: none; }
  :root:not([data-theme="light"]) .icon-moon { display: block; }
}
.cart-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 6px;
  margin: -6px;
  border-radius: 4px;
  color: var(--ink);
}
.cart-toggle:hover { background: var(--hover-fill); }
.cart-count {
  font-weight: 500;
  font-size: 11px;
}

/* ---------- Layout helpers ---------- */
main { display: block; min-height: 60vh; }
.wrap { padding-left: 20px; padding-right: 20px; }

/* ---------- Hero ---------- */
.hero {
  padding: calc(var(--grid) * 6) 20px calc(var(--grid) * 10);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
}
.hero-copy { max-width: 680px; }
.hero-copy h1 {
  margin: 0 0 var(--grid);
  font-weight: 400;
  font-size: clamp(19px, 2.1vw, 25px);
  line-height: 1.3;
}
.hero-copy p {
  margin: 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--body);
}

/* Decorative only — no click handler, goes nowhere. */
.badge {
  width: 80px;
  height: 80px;
  min-width: 80px;
  border-radius: 50%;
  background: var(--accent);
  overflow: hidden;
}
.badge svg { width: 100%; height: 100%; animation: spin 20s linear infinite; }
.badge text {
  fill: var(--accent-contrast);
  font-family: "BDO Grotesk", sans-serif;
  font-size: 16px;
  letter-spacing: -0.3px;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .badge svg { animation: none; }
}

/* ---------- Filter bar ---------- */
.filter-bar {
  padding: 0 20px 16px;
  border-bottom: 1px solid var(--hairline);
  display: flex;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}
.category-nav {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
}
.category-nav button {
  background: none;
  border: none;
  padding-bottom: 3px;
  font-size: 12px;
  color: var(--muted);
  border-bottom: 1px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.category-nav button.active { color: var(--ink); border-bottom-color: var(--ink); }
.filter-controls {
  display: flex;
  align-items: center;
  gap: 20px;
  color: var(--body);
  flex-wrap: wrap;
}
.filter-controls select {
  border: none;
  background: none;
  color: inherit;
  font-size: 12px;
  font-family: inherit;
}
.result-count { color: var(--muted); font-size: 12px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.density-toggle {
  display: flex;
  align-items: center;
  gap: 2px;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  padding: 2px;
}
.density-toggle button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 22px;
  background: none;
  border: none;
  border-radius: 3px;
  color: var(--muted);
  fill: currentColor;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.density-toggle button:hover { color: var(--ink); background: var(--hover-fill); }
.density-toggle button.active { color: var(--ink); background: var(--active-fill); }

/* ---------- Grid ---------- */
.grid-section { padding: 24px 20px 40px; }
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
  align-items: start;
  transition: grid-template-columns 0.45s var(--ease-fluid);
}
/* Column density, set via the grid-size toggle in the filter bar (see
   app.js) — an attribute selector so the "md" default above stays the one
   rule with no JS-set attribute needed at all. */
.product-grid[data-density="sm"] { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
.product-grid[data-density="lg"] { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
.card {
  padding-bottom: calc(var(--grid) * 3);
  animation: fadein 0.4s ease both;
}
.card-media {
  position: relative;
  background: var(--surface);
  overflow: hidden;
  border-radius: 3px;
  cursor: pointer;
}
.card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease-fluid);
  will-change: transform;
  transform: scale(1.001); /* avoids a slight edge-pixel seam on the very first hover transition */
}
/* Replaced a JS-driven "B-roll" hover that snapped through four zoomed/
   panned crops every 500ms — faster than the transition itself could
   settle, so it never stopped restlessly jittering. A single calm zoom
   on the existing transition reads as intentional instead of jarring. */
.card-media:hover img { transform: scale(1.06); }
@media (prefers-reduced-motion: reduce) {
  .card-media img { transition: none !important; transform: none !important; }
}
/* Any product carrying an image tagged "_rollover" (see lib/uploads.js)
   gets a second image stacked exactly on top of the first, crossfaded in
   on hover — a dedicated hover-state shot rather than just zooming the
   same photo. */
.card-img-base, .card-img-hover { position: absolute; inset: 0; }
.card-img-hover {
  opacity: 0;
  transition: opacity 0.35s ease, transform 0.6s var(--ease-fluid);
}
.card-media:hover .card-img-hover { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .card-img-hover { transition: none !important; }
}
.pill-new {
  position: absolute;
  left: 7px;
  top: 7px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-weight: 500;
  font-size: 8px;
  padding: 4.5px 7px;
  border-radius: 18px;
  letter-spacing: 0.2px;
}
.card-caption {
  margin-top: 10px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 11px;
  line-height: 1.4;
}
.card-caption .title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card-action {
  padding-top: 6px;
  height: 11px;
  font-size: 11px;
  line-height: 1;
  color: var(--muted);
}
.card-action button {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  padding: 0;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.card-action button svg { flex-shrink: 0; }
.card:hover .card-action button,
.card-action button:focus-visible {
  opacity: 1;
}
@media (hover: none) {
  .card-action button { opacity: 1; }
}

/* ---------- Loading skeleton (see index.html for the static markup) ---------- */
@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
.skeleton-block {
  background: var(--surface);
  animation: skeleton-pulse 1.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .skeleton-block { animation: none; opacity: 0.75; }
}
.skeleton-hero .hero-copy { display: flex; flex-direction: column; }
.skeleton-h1 { width: min(360px, 60vw); height: 19px; margin-bottom: var(--grid); }
.skeleton-p { width: min(280px, 46vw); height: 12px; }
.skeleton-filter-bar .skeleton-cats { width: min(260px, 40vw); height: 13px; }
.skeleton-filter-bar .skeleton-sort { width: 90px; height: 13px; margin-left: auto; }
.skeleton-caption { height: 11px; width: 65%; margin-top: 10px; }

.btn-outline {
  display: inline-block;
  border: 1px solid var(--ink);
  background: none;
  padding: 11px 18px;
  font-size: 12px;
  white-space: nowrap;
  text-decoration: none;
  color: var(--ink);
  transition: background 0.2s ease, color 0.2s ease;
}
.btn-outline:hover { background: var(--ink); color: var(--ink-contrast); }
.btn-sm { padding: 7px 12px; font-size: 11px; }

/* ---------- Order confirmation / account: celebratory chrome logo ----------
   Reuses the header's WebGL wordmark component (nav-chrome-logo.js looks
   for #nav-mark-wrap by id) but this isn't a fixed nav bar — no scroll to
   shrink on, so the .scrolled class never appears and it just sits at a
   bigger, static size as a one-off flourish above the confirmation. */
.confirm-mark-wrap { text-align: center; margin: 0 0 calc(var(--grid) * 4); }
.confirm-mark-wrap .nav-mark-wrap {
  /* relative, not static: .nav-mark-canvas inside is `position: absolute;
     inset: 0`, so this still has to stay a positioning context — dropping
     position entirely let the canvas size itself against the nearest
     positioned ancestor up the tree instead (rendered page-covering-huge). */
  position: relative;
  left: auto;
  top: auto;
  transform: none;
  height: 44px;
  margin: 0 auto;
}

/* ---------- Order panels with a product thumbnail (success + account pages) ----------
   A plain vertical list, not .hairline-grid's fixed 2-column layout: that
   grid leaves a phantom empty grey cell whenever there's an odd number of
   panels (a single order, a single product) — which, for a page showing
   variable-length order history, is the common case, not an edge case. */
.order-list { display: flex; flex-direction: column; border-top: 1px solid var(--hairline); text-align: left; }
.order-list .panel { background: none; border-bottom: 1px solid var(--hairline); padding: calc(var(--grid) * 3) 0; }
.panel-with-thumb { display: flex; gap: calc(var(--grid) * 2); align-items: flex-start; text-align: left; }
.panel-thumb {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 3px;
  background: var(--surface);
  flex-shrink: 0;
}
.panel-body { min-width: 0; }

/* ---------- Detail ---------- */
.detail { padding: 32px 20px calc(var(--grid) * 9); }
.breadcrumbs { font-size: 12px; color: var(--muted); margin-bottom: calc(var(--grid) * 3); }
.breadcrumbs a { color: var(--body); }
.breadcrumbs a:hover { color: var(--ink); }
.detail-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: 48px;
  align-items: start;
}
@media (max-width: 1040px) {
  .detail-grid { grid-template-columns: 1fr; }
}
.detail-hero {
  position: relative;
  background: var(--surface);
  overflow: hidden;
  border-radius: 3px;
}
.detail-hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Slower and symmetric (ease-in-out, not --ease-fluid's quick-start
     feel used for UI feedback elsewhere) reads as a deliberate dissolve
     rather than a flicker — this is the one crossfade on the site meant
     to feel unhurried, not responsive. */
  transition: transform 0.6s var(--ease-fluid), opacity 1.1s cubic-bezier(0.45, 0, 0.15, 1);
}
.detail-hero:hover img { transform: scale(1.03); }
/* Two of these stack exactly on top of each other (see renderDetail's
   markup: #detail-hero-img and #detail-hero-img-b) so setGalleryIndex()
   in app.js can crossfade between them — the incoming image fades in on
   the layer that was hidden while the outgoing one fades out on the
   other, at the same time, rather than one image fading out, swapping
   src, then fading back in, which always has a moment showing neither
   picture. */
.detail-hero-layer {
  position: absolute;
  inset: 0;
  opacity: 0;
}
.detail-hero-layer.is-active { opacity: 1; }
.gallery-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.85);
  color: #111;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.2s ease, background 0.2s ease;
}
.detail-hero:hover .gallery-arrow { opacity: 1; }
.gallery-arrow:hover { background: #fff; }
.gallery-arrow.prev { left: 10px; }
.gallery-arrow.next { right: 10px; }
.request-more-btn {
  position: absolute;
  right: 10px;
  bottom: 10px;
  height: 26px;
  width: 26px;
  border-radius: 20px;
  border: none;
  background: rgba(255, 255, 255, 0.85);
  color: #111;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 0;
  opacity: 0.85;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  transition: opacity 0.2s ease, background 0.2s ease, transform 0.15s ease, width 0.15s ease, padding 0.15s ease;
  font-size: 10px;
  white-space: nowrap;
  overflow: hidden;
}
/* .has-label (removed by app.js in the same click that removes the label
   span itself) is what stretches the button from a plain circle into a
   pill — kept as an explicit class rather than a :has() selector so this
   doesn't depend on a specific browser's CSS support. */
.request-more-btn.has-label { width: auto; padding: 0 12px 0 9px; }
.request-more-label { line-height: 1; }
.request-more-btn:hover { opacity: 1; background: #fff; transform: scale(1.05); }
.request-more-btn.voted { opacity: 0.9; background: var(--accent); color: var(--accent-contrast); cursor: default; }
.request-more-btn.voted:hover { transform: none; background: var(--accent); }
.request-more-btn:disabled { cursor: default; }
.gallery-dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: 12px;
}
.gallery-dots button {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: none;
  background: var(--field-border);
  padding: 0;
  transition: background 0.2s ease, transform 0.2s ease;
}
.gallery-dots button.active { background: var(--ink); transform: scale(1.2); }

.detail-side { width: 100%; }
/* Eyebrow label above the title — matches the .kicker treatment used
   above every other section heading on the site (bundle strip, legal
   pages), rather than sitting flush against the title with no margin
   of its own. */
.detail-category { font-size: 11px; color: var(--muted); margin: 0 0 6px; }
.detail-side h2 {
  font-weight: 400;
  /* Was clamp(18px, 2vw, 22px) — at any phone width the 2vw term never
     wins against the 22px cap, so this rendered at its 18px floor: only
     6px larger than the 12px body copy below it, and smaller than some
     UI labels elsewhere on the page. The brand's whole type system stays
     at font-weight 400 even for headings (see .hero-copy h1, .page h2)
     — weight 500 is reserved for small tags and badges — so hierarchy
     here has to come from size and leading alone,
     which means the title needs real size headroom over the body text,
     not a token bump. */
  font-size: clamp(23px, 5.5vw, 28px);
  line-height: 1.2;
  margin: 0 0 14px;
}
.detail-side .description { font-size: 12px; line-height: 1.7; color: var(--body); margin: 0; }
.spec-row {
  display: flex;
  flex-wrap: wrap;
  gap: 26px;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  padding: calc(var(--grid) * 2) 0;
  margin: calc(var(--grid) * 3) 0;
  font-size: 11px;
  line-height: 1.5;
}
.spec-row div span { display: flex; align-items: center; gap: 4px; color: var(--muted); }
.spec-row div span svg { flex-shrink: 0; }
.spec-row div span.format-badge {
  display: inline-block;
  border: 1px solid var(--field-border);
  border-radius: 3px;
  padding: 1px 5px;
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.3px;
  color: var(--muted);
}
/* Reads as a section heading for the licence-row list below it (same
   role "Full access" plays for the panel next to it), not a small
   muted label like the spec-row/category eyebrows — so it gets the
   site's existing subsection-heading treatment (see
   .legal-section-body h3) instead of blending into the metadata text
   around it. */
.licence-label { font-size: 15px; font-weight: 500; color: var(--ink); margin: 0 0 6px; }
.licence-row {
  width: 100%;
  display: flex;
  flex-direction: column;
  /* No flex `gap` here on purpose — with .licence-desc always in the DOM
     (collapsed via grid-template-rows: 0fr when not selected), a gap would
     add dead space between the row's top line and the collapsed
     description even when it's rendering at zero height. The only spacing
     between them comes from .licence-desc's own margin-top, which is
     itself part of the selected-state transition. */
  /* Horizontal padding + a matching negative margin: the row's hover/active
     fill still spans edge-to-edge like a proper list item, but the text
     inside gets real breathing room instead of sitting flush against it. */
  padding: 11px 16px;
  margin: 0 -16px;
  width: calc(100% + 32px);
  border-bottom: 1px solid var(--hairline);
  background: none;
  border-left: none;
  border-right: none;
  border-top: none;
  text-align: left;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
  transition: color 0.15s ease, background-color 0.1s ease;
}
.licence-row:hover { background: var(--hover-fill); }
/* Grey "pressed" feedback — deliberately only while the mouse/finger is
   actually down, distinct from (and stronger than) the :hover fill above,
   and never lingers once the button is selected or released. */
.licence-row:active { background: var(--active-fill); }
.licence-row-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  width: 100%;
}
.licence-row .name,
.licence-row .price {
  font-weight: 400;
  color: var(--muted);
  transition: color 0.15s ease;
}
.licence-row.selected .name,
.licence-row.selected .price {
  color: var(--ink);
}
.licence-row .name {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
/* Color is per-tier (see --dot-color, set inline per row in app.js) —
   every row shows its own color at rest; the selected row's dot additionally
   gets a glow in that same color so selection still reads at a glance. */
.licence-row .name::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--dot-color, var(--muted));
  flex-shrink: 0;
  transition: box-shadow 0.15s ease;
}
.licence-row.selected .name::before {
  box-shadow: 0 0 4px var(--dot-color, var(--accent));
}
/* Every row's description is always in the DOM (see app.js) — switching
   the selected licence now just toggles this class instead of destroying
   and recreating the element, so there's something to actually transition
   between. The 0fr/1fr grid-rows swap animates height precisely to the
   real content height with no fixed max-height guess to overshoot.
   Crucially, overflow:hidden has to live on .licence-desc-inner (the grid
   ITEM), not .licence-desc (the grid container) — grid items get an
   automatic minimum size floor equal to their content's intrinsic size
   unless their own overflow is non-visible, so with overflow only on the
   container, the 0fr track was still being held open to one line's
   height by the text inside it, which is exactly the "unnecessary
   padding" collapsed rows kept showing. */
.licence-row .licence-desc {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s var(--ease-fluid), margin-top 0.3s var(--ease-fluid);
}
.licence-row.selected .licence-desc {
  grid-template-rows: 1fr;
  margin-top: 4px;
}
.licence-row .licence-desc-inner {
  display: block;
  overflow: hidden;
  min-height: 0;
  opacity: 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--body);
  transition: opacity 0.2s ease;
  /* No max-width: the row already has a sensible width from the detail
     grid column it sits in (~400px+), and a 46ch cap on top of that was
     forcing an early wrap well before the row actually ran out of room. */
}
.licence-row.selected .licence-desc-inner {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .licence-row .licence-desc,
  .licence-row .licence-desc-inner { transition: none; }
}
.licence-disclaimer {
  display: flex;
  align-items: flex-start;
  gap: 5px;
  margin: 12px 0 0;
  font-size: 9px;
  line-height: 1.6;
  color: var(--muted);
}
.licence-disclaimer svg { flex-shrink: 0; margin-top: 2px; }
.btn-primary {
  display: block;
  width: 100%;
  margin-top: calc(var(--grid) * 2);
  padding: 12px 20px;
  background: var(--ink);
  color: var(--ink-contrast);
  border: none;
  font-size: 12px;
  transition: opacity 0.15s ease;
  animation: btn-primary-glow 2.8s ease-in-out infinite;
}
.btn-primary:hover { opacity: 0.85; }
.btn-primary:disabled { opacity: .55; cursor: default; animation: none; box-shadow: none; }
/* Breathing neon-yellow rim in the brand accent, around the radius rather
   than a flat drop shadow underneath — spread with no blur at 0% keeps the
   edge crisp, then blurs and expands outward at the midpoint before
   pulling back in. */
@keyframes btn-primary-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(216, 255, 83, 0); }
  50% { box-shadow: 0 0 16px 1px rgba(216, 255, 83, 0.65); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-primary { animation: none; box-shadow: 0 0 10px 1px rgba(216, 255, 83, 0.4); }
}
/* ---------- Content pages ---------- */
.page { padding: 32px 20px calc(var(--grid) * 11); }
.page-inner { max-width: 720px; margin: 0 auto; text-align: center; }
.page-inner .breadcrumbs { text-align: left; }
.page h2 { font-weight: 400; font-size: clamp(19px, 2.1vw, 25px); line-height: 1.3; margin: 0 0 12px; }
.page > .page-inner > .intro { font-size: 11px; line-height: 1.5; color: var(--body); max-width: 56ch; margin: 0 auto calc(var(--grid) * 4); }
.hairline-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--hairline);
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  text-align: left;
}
@media (max-width: 640px) {
  .hairline-grid { grid-template-columns: 1fr; }
}
.hairline-grid .panel { background: var(--ground); padding: calc(var(--grid) * 3); transition: background-color 0.9s ease-in-out; }
.hairline-grid .kicker { font-size: 11px; color: var(--muted); margin: 0 0 8px; }
.hairline-grid h3 { font-size: 14px; line-height: 1.35; font-weight: 400; margin: 0 0 8px; }
.hairline-grid p { font-size: 11px; line-height: 1.5; color: var(--body); margin: 0; }
.page .closing-note { font-size: 10px; line-height: 1.5; color: var(--muted); max-width: 60ch; margin: calc(var(--grid) * 3) auto 0; }

/* ---------- Long-form legal pages (Terms, Privacy) ----------
   Opts out of .hairline-grid's 2-up card grid — fine for Licence's four
   short comparison tiers, but a squeeze once a page is reading material
   with 5–7 sections. Full-width numbered rows instead, generously spaced,
   with a jump list once there's enough of them to need one. */
.legal-jump {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  text-align: left;
  margin: 0 0 calc(var(--grid) * 4);
}
.legal-jump-pill {
  font-size: 11px;
  color: var(--body);
  border: 1px solid var(--field-border);
  border-radius: 20px;
  padding: 6px 12px;
  text-decoration: none;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.legal-jump-pill:hover { border-color: var(--ink); color: var(--ink); }
.legal-sections { text-align: left; border-top: 1px solid var(--hairline); }
.legal-section {
  display: flex;
  gap: calc(var(--grid) * 3);
  padding: calc(var(--grid) * 4) 0;
  border-bottom: 1px solid var(--hairline);
  /* Anchor targets land mid-list once jumped to — offset the scroll stop
     so a section's top isn't flush against the overlay's edge. */
  scroll-margin-top: 24px;
}
.legal-section-body { min-width: 0; }
.legal-section-body .kicker { font-size: 11px; color: var(--muted); margin: 0 0 6px; }
.legal-section-body h3 { font-size: 15px; line-height: 1.4; font-weight: 500; margin: 0 0 8px; }
.legal-section-body p { font-size: 12px; line-height: 1.5; color: var(--body); margin: 0; max-width: 62ch; }
@media (max-width: 560px) {
  .legal-section { gap: calc(var(--grid) * 2); }
  .legal-section-body h3 { font-size: 14px; }
}

/* ---------- Mailing list form (footer -> legal overlay) ---------- */
.mailing-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 340px;
  margin: 0 auto;
  text-align: left;
}
.mailing-form .field { display: flex; flex-direction: column; gap: 6px; }
.mailing-form label { font-size: 11px; color: var(--muted); }
.mailing-form input[type="text"],
.mailing-form input[type="email"] {
  border: none;
  border-bottom: 1px solid var(--field-border);
  padding: 9px 0;
  font-size: 13px;
  font-family: inherit;
  background: none;
  color: var(--ink);
}
.mailing-form input[type="text"]:focus,
.mailing-form input[type="email"]:focus { outline: none; border-bottom-color: var(--ink); }
.mailing-form .btn-primary { margin-top: 4px; }
.mailing-status { font-size: 12px; margin: 0; }
.mailing-status.success { color: #14532d; }
.mailing-status.error { color: #b3261e; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .mailing-status.success { color: #4ade80; }
  :root:not([data-theme="light"]) .mailing-status.error { color: #f87171; }
}
:root[data-theme="dark"] .mailing-status.success { color: #4ade80; }
:root[data-theme="dark"] .mailing-status.error { color: #f87171; }

/* ---------- Empty state ---------- */
.empty-state { padding: calc(var(--grid) * 14) 20px calc(var(--grid) * 16); text-align: left; }
.empty-state h2 { font-size: 16px; font-weight: 400; line-height: 1.4; margin: 0 0 8px; }
.empty-state p { font-size: 12px; line-height: 1.6; color: var(--body); margin: 0 0 12px; }

.fly-to-cart {
  position: fixed;
  z-index: 200;
  object-fit: cover;
  border-radius: 3px;
  pointer-events: none;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
  will-change: transform, opacity;
}

/* ---------- Cart drawer ---------- */
.scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  backdrop-filter: blur(0px) saturate(140%);
  -webkit-backdrop-filter: blur(0px) saturate(140%);
  z-index: 80;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s var(--ease-fluid), backdrop-filter 0.35s var(--ease-fluid), -webkit-backdrop-filter 0.35s var(--ease-fluid);
}
.scrim.is-open {
  opacity: 1;
  pointer-events: auto;
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
}
@media (prefers-reduced-motion: reduce) {
  .scrim { transition-duration: 0.01ms; }
}
.cart-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(380px, 100%);
  background: var(--ground);
  border-left: 1px solid var(--hairline);
  display: flex;
  flex-direction: column;
  z-index: 90;
  transform: translateX(100%);
  transition: transform 0.4s var(--ease-fluid);
}
.cart-drawer.is-open { transform: translateX(0); }
@media (prefers-reduced-motion: reduce) {
  .scrim, .cart-drawer { transition-duration: 0.01ms; }
}
.cart-header {
  padding: 16px;
  border-bottom: 1px solid var(--hairline);
  font-size: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.link-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--body);
  font-size: 12px;
}
.link-btn:hover { color: var(--ink); }
.cart-body { flex: 1; overflow-y: auto; padding: 0 16px; }
.cart-empty { padding: 32px 0; font-size: 12px; line-height: 1.6; color: var(--body); }
.line-item {
  padding: 16px 0;
  border-bottom: 1px solid var(--hairline);
  display: flex;
  gap: 12px;
  animation: fadein 0.25s ease both;
}
.line-item .thumb {
  width: 64px;
  height: 48px;
  min-width: 64px;
  background: var(--surface);
  overflow: hidden;
  border-radius: 3px;
}
.line-item .thumb img { width: 100%; height: 100%; object-fit: cover; }
.line-item .mid { flex: 1; min-width: 0; }
.line-item .title { font-size: 12px; line-height: 1.4; }
.line-item .meta { font-size: 11px; color: var(--muted); margin: 2px 0 8px; }
.licence-select-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--muted);
}
.licence-select {
  border: none;
  border-bottom: 1px solid var(--field-border);
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: 11px;
  padding: 1px 0 2px;
}
.licence-select:focus-visible { border-bottom-color: var(--ink); }
.qty-stepper {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  font-size: 11px;
}
.qty-stepper button {
  width: 18px;
  height: 18px;
  border: 1px solid var(--field-border);
  background: none;
  color: var(--ink);
  line-height: 1;
  padding: 0;
  border-radius: 3px;
}
.qty-stepper button:hover { border-color: var(--ink); }
.qty-stepper span { min-width: 12px; text-align: center; }
.remove-btn {
  margin-top: 8px;
  background: none;
  border: none;
  padding: 0;
  font-size: 11px;
  color: var(--muted);
}
.remove-btn:hover { color: var(--ink); }
.line-item .price { font-size: 12px; white-space: nowrap; }
.cart-footer {
  border-top: 1px solid var(--hairline);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.subtotal-row { display: flex; justify-content: space-between; font-size: 12px; }
.subtotal-row .label { color: var(--body); }
.checkout-btn {
  width: 100%;
  padding: 12px;
  background: var(--ink);
  color: var(--ink-contrast);
  border: none;
  font-size: 12px;
  transition: opacity 0.15s ease;
}
.checkout-btn:hover:not(:disabled) { opacity: 0.85; }
.checkout-btn:disabled { opacity: .55; cursor: default; }
.cart-note { font-size: 11px; line-height: 1.5; color: var(--muted); margin: 0; }
.checkout-error {
  font-size: 11px;
  line-height: 1.5;
  color: #e5645a;
  margin: 0;
}

/* ---------- Legal overlay ---------- */
.legal-overlay {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: flex;
  justify-content: center;
  padding: 64px 20px;
  overflow-y: auto;
  opacity: 0;
  pointer-events: none;
  transform: translateY(10px) scale(0.98);
  transition: opacity 0.3s var(--ease-fluid), transform 0.3s var(--ease-fluid);
  scroll-behavior: smooth;
}
.legal-overlay.is-open { opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); }
@media (prefers-reduced-motion: reduce) {
  .legal-overlay { transition-duration: 0.01ms; scroll-behavior: auto; }
}
.legal-panel {
  position: relative;
  background: var(--ground);
  max-width: 760px;
  width: 100%;
  height: fit-content;
  padding: calc(var(--grid) * 6) 40px calc(var(--grid) * 6);
  border-radius: 8px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.22);
}
:root[data-theme="dark"] .legal-panel {
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .legal-panel {
    box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
  }
}
.legal-close {
  position: absolute;
  top: 18px;
  right: 20px;
}
.legal-panel .page { padding: 0; }
.legal-panel .page-inner { max-width: none; }

/* ---------- FAQ ---------- */
.faq-section { padding: calc(var(--grid) * 4) 20px calc(var(--grid) * 2); border-top: 1px solid var(--hairline); }
.faq-inner { max-width: 720px; margin: 0 auto; text-align: center; }
.faq-inner .kicker { font-size: 11px; color: var(--muted); margin: 0 0 8px; }
.faq-inner h2 { font-weight: 400; font-size: clamp(19px, 2.1vw, 25px); line-height: 1.3; margin: 0 0 calc(var(--grid) * 4); }
.faq-list { text-align: left; border-top: 1px solid var(--hairline); }
.faq-item {
  border-bottom: 1px solid var(--hairline);
  padding: calc(var(--grid) * 2.5) 0;
}
.faq-item summary {
  font-size: 13px;
  line-height: 1.4;
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  color: var(--muted);
  font-size: 15px;
  flex-shrink: 0;
  transition: transform 0.2s ease;
}
.faq-item[open] summary::after { content: "+"; transform: rotate(45deg); }
.faq-item p {
  font-size: 12px;
  line-height: 1.6;
  color: var(--body);
  margin: calc(var(--grid) * 2) 0 0;
  max-width: 62ch;
}

/* ---------- Footer ---------- */
.footer { padding: 56px 20px 20px; }
.footer-brand-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* A footer-char grows upward from its own baseline when it magnifies
     on rollover (see .footer-char's transform-origin), up to 1.6x its
     resting height — the plain 4-grid-unit gap this row used before left
     a magnified character's top edge reaching into the logo/payment-icon
     row above it at most viewport widths. */
  margin-bottom: calc(var(--grid) * 9);
}
.footer-payment-icons {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-shrink: 0;
  transition: opacity 0.15s ease;
}
.footer-payment-icons:hover { opacity: 0.7; }
.payment-icon {
  display: block;
  height: 24px;
  width: auto;
  fill: var(--muted);
}
/* Spans the same full width as the characters below it — both are
   edge-to-edge children of .footer's padding box, space-between pins
   the first/last character flush to those same edges. */
.footer-chars {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: nowrap;
  gap: clamp(3px, 1vw, 8px);
  margin-bottom: calc(var(--grid) * 4);
}
.footer-char {
  position: relative;
  height: clamp(18px, 8.6vw, 160px);
  width: auto;
  flex: 0 0 auto;
  min-width: 0;
  /* Grows from its own baseline, matching .footer-chars' flex-end
     alignment, so a magnified character rises in place rather than
     growing from its middle and pushing into the row above it. The
     transition only smooths between the per-mousemove scale values
     initFooterMagnify() writes inline — it isn't what drives the effect. */
  transform-origin: bottom center;
  transition: transform 0.12s ease-out;
}
/* Without this, a scaled-up character can be visually covered by an
   unscaled neighbor that simply comes later in source order — flex
   siblings otherwise stack by DOM order, not by size. */
.footer-char:hover { z-index: 1; }
@media (prefers-reduced-motion: reduce) {
  .footer-char { transition: none; }
}
.footer-char-mono, .footer-char-color {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* Recolored via CSS mask (not a pre-baked grey file) so it tracks
   --footer-mark exactly, including the light/dark swap the wordmark
   itself gets for free. */
.footer-char-mono {
  background-color: var(--footer-mark);
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  transition: background-color 0.9s ease-in-out;
}
/* Asymmetric on purpose: fading IN on hover stays snappy (the rule below,
   which only applies while :hover matches, governs that direction), but
   fading back OUT to mono uses this much slower duration instead — once a
   character lights up it lingers in colour for a while after the cursor
   moves on, rather than snapping back instantly. */
.footer-char-color {
  opacity: 0;
  transition: opacity 2.5s ease;
}
.footer-char:hover .footer-char-color {
  opacity: 1;
  transition: opacity 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
  .footer-char-color, .footer-char-mono { transition: none; }
}
/* Sized to match the copyright line's own text width below, rather than
   spanning the footer like it used to — the credit card icons need the
   other half of this row. The real-time chrome wordmark now lives in the
   header instead (see .nav-mark-wrap) — this is back to the plain flat
   mark it was before that moved in. */
.footer-mark-wrap {
  position: relative;
  width: 93px;
  flex-shrink: 0;
}
.footer-mark {
  width: 100%;
  height: auto;
  display: block;
  pointer-events: none;
  fill: var(--footer-mark);
  transition: fill 0.9s ease-in-out;
}
.footer-mark-wrap:hover .footer-mark {
  fill: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
  .footer-mark { transition: none; }
}
.footer-row {
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--hairline);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px 20px;
  flex-wrap: wrap;
  font-size: 11px;
  line-height: 1.6;
  color: var(--muted);
}
.footer-links { display: flex; gap: 20px; flex-wrap: wrap; }
.footer-links a { transition: color 0.2s ease; }
.footer-links a:hover, .footer-links a.active { color: var(--ink); }

/* ---------- Animations ---------- */
@keyframes pagein {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
@keyframes fadein {
  from { opacity: 0; }
  to { opacity: 1; }
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.32s;
  animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ---------- Small screens ---------- */
@media (max-width: 480px) {
  .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .hero { padding: calc(var(--grid) * 6) 20px calc(var(--grid) * 4); }
}
