/* Brand ramp, category accents and both faces: see theme.css. */
@import url("/theme.css");

:root {
  /*
   * The catalog's paper surfaces. Not in theme.css for the same reason the
   * landing page keeps its own: light paper under heavy ink is the template's
   * look, not a value a client sets.
   */
  --night: #09080b;
  --ink: #111015;
  --paper: #f1eee7;
  --paper-deep: #ded8cd;

  --accent: var(--brand);
  --accent-ink: var(--ink);

  /*
   * The same accent, deep enough to be read as text on paper. Five of the
   * seven themes fail there at full strength -- gold measures 2.01:1 on white,
   * blue 1.96, paper 1.63, green 3.71 and the brand purple 4.09 -- because
   * these values were chosen to be sat ON, not to be set IN. --accent stays
   * exactly as it was for every fill and border; only type reaches for this.
   */
  --accent-deep: var(--brand-deep);

  /*
   * Note this is a border shorthand, not a colour — styles-core.css has a
   * --line that is a colour. Same name, different type, and the two never meet
   * because no page loads both stylesheets.
   */
  --line: 3px solid var(--ink);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* clip on top of hidden: hidden alone leaves a touch-pannable scroll
   container on mobile; clip forbids sideways scroll outright. */
html {
  overflow-x: hidden;
  overflow-x: clip;
}
/* No pinch zoom -- but only over an element that asks for it by carrying
   `data-zoom-lock`, which page-boot.mjs reads for the same purpose. No shop
   page has one: the 3D rack is what pinching distorted, and it lives on the
   landing page. This rule is here so the attribute means the same thing if one
   ever does. See the longer note beside it in styles-core.css. */
[data-zoom-lock] { touch-action: pan-x pan-y; }
body {
  margin: 0;
  min-width: 320px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  overflow-x: hidden;
  overflow-x: clip;
}

/* White on the red for the same reason as the green below: #c8393f is a
   mid-tone, and ink measures 3.71:1 on it where white measures 5.11. */
body[data-theme="red"] { --accent: var(--red); --accent-ink: #fff; --accent-deep: #a82c31; }
body[data-theme="ink"] { --accent: #343139; --accent-ink: #fff; --accent-deep: #343139; }
body[data-theme="gold"] { --accent: var(--gold); --accent-deep: #8a6410; }
body[data-theme="blue"] { --accent: var(--blue); --accent-deep: #3c6d92; }
body[data-theme="paper"] { --accent: var(--brand-tint-deep); --accent-deep: var(--brand-deep); }

/*
 * The same seven themes again, on the card instead of the page.
 *
 * A condensed card is not always in a page of its own category. The shelf
 * under a product poster crosses categories by design (storefront-model.mjs
 * reaches past the current one once it runs out of neighbours), and so does
 * the pick page. Those cards used to resolve --accent off the body, so a
 * Flower product page painted its edible, vape and concentrate neighbours
 * purple -- the rack read as one category no matter what was on it.
 *
 * Every rule sets all three, including the ones the body list leaves to the
 * default. --accent-ink is the reason: a gold card sitting in a red page would
 * otherwise inherit that page's white and put white type on gold. The body
 * list above can lean on the defaults because it is only ever one theme deep.
 */
.catalog-compact-card[data-theme="purple"], .catalog-pull-row[data-theme="purple"] { --accent: var(--brand); --accent-ink: var(--ink); --accent-deep: var(--brand-deep); }
.catalog-compact-card[data-theme="red"], .catalog-pull-row[data-theme="red"] { --accent: var(--red); --accent-ink: #fff; --accent-deep: #a82c31; }
.catalog-compact-card[data-theme="ink"], .catalog-pull-row[data-theme="ink"] { --accent: #343139; --accent-ink: #fff; --accent-deep: #343139; }
.catalog-compact-card[data-theme="gold"], .catalog-pull-row[data-theme="gold"] { --accent: var(--gold); --accent-ink: var(--ink); --accent-deep: #8a6410; }
.catalog-compact-card[data-theme="blue"], .catalog-pull-row[data-theme="blue"] { --accent: var(--blue); --accent-ink: var(--ink); --accent-deep: #3c6d92; }
.catalog-compact-card[data-theme="green"], .catalog-pull-row[data-theme="green"] { --accent: var(--green); --accent-ink: #fff; --accent-deep: var(--green); }
.catalog-compact-card[data-theme="paper"], .catalog-pull-row[data-theme="paper"] { --accent: var(--brand-tint-deep); --accent-ink: var(--ink); --accent-deep: var(--brand-deep); }

a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
img { display: block; max-width: 100%; }
button { cursor: pointer; }

/*
 * Nothing on the storefront is selectable except the few things somebody
 * actually copies.
 *
 * This started as the narrower rule -- selection off on controls only, because
 * highlighting is how iOS Speak Selection and select-to-translate work, so
 * switching it off site-wide reads like an accessibility regression. It mostly
 * is not one. Screen readers do not use selection at all: VoiceOver, TalkBack
 * and NVDA read the document, so none of them notice this rule. Find-in-page,
 * whole-page translate and reader mode are all likewise untouched. What is
 * genuinely lost is copying, and on a shop that is close to nothing -- with
 * three exceptions, which are the allowlist below.
 *
 * What this buys: a tap on a card stops highlighting the product name, a
 * double-click stops selecting a headline, and a mouse-down that drifts two
 * pixels inside a card link stops becoming a drag-select that swallows the
 * click. It also retires a whole class of display-type bug -- see the note on
 * ::selection further down.
 *
 * Not the admin. It loads this stylesheet, and its operator has every reason
 * to copy an id, an email or a code out of a table she is working in.
 */
body:not(.admin-page) {
  -webkit-user-select: none;
  user-select: none;
}

/*
 * The three exceptions, each for a stated reason:
 *
 * - Form fields, spelled out rather than assumed. Selection inside an input is
 *   not automatically exempt from an ancestor's `user-select: none`, and
 *   Safari in particular has shipped versions where inheriting it breaks
 *   selecting and editing what you just typed.
 *
 * - #shareCode is the wishlist's short code. The Copy button beside it copies
 *   the *link*, not the code, and the page tells the visitor to "text it to
 *   yourself, or show the code at the counter" -- so highlighting it is the
 *   only way to get it. Switching that off would quietly break the feature.
 *
 * - .boom-announce-body is free text the owner types in the admin. If she ever
 *   puts a phone number or an address in the banner, people will copy it.
 */
input,
textarea,
select,
[contenteditable="true"],
#shareCode,
.boom-announce-body {
  -webkit-user-select: text;
  user-select: text;
}

/*
 * The press. 1px is enough to read as the button taking the weight and small
 * enough that nothing beside it appears to move. No transition on purpose:
 * an instant state change needs no reduced-motion guard, and a press that
 * eases is a press you feel late.
 *
 * .catalog-compact-link is deliberately absent -- it contains the 3D stage,
 * and a transform on the card would make it a containing block for the
 * absolutely positioned art inside it.
 */
:is(
  button,
  [role="button"],
  .catalog-compact-add,
  .catalog-story-add,
  .catalog-story-details,
  .cart-choose-link,
  .featured-card-open,
  .featured-sheet-add,
  .featured-sheet-product-link,
  .poster-action,
  .product-issue-walk-link,
  .empty-shelf-action
):active {
  transform: translateY(1px);
}

/*
 * What is left to select is the allowlist above -- form fields, the wishlist
 * share code, the announcement banner's text -- so this is what a highlight
 * looks like in those three places. It was browser-default blue, which on
 * paper-and-ink read like a rendering fault rather than something you did.
 *
 * Ink, and not --accent-deep. That was the obvious choice -- it is already the
 * vetted "dark enough to carry type" variant of each theme -- but the number
 * that matters for a highlight is not text-on-selection, it is
 * selection-on-page. The green and ink themes define --accent-deep as the same
 * value as --accent, so a highlight in those categories would have been
 * painted in the surrounding colour: 1.00:1, invisible, while still passing
 * every text-contrast check. Ink is 16.35:1 on the paper these three surfaces
 * sit on, with the paper text at 16.35 on top of it.
 *
 * background-color and not the shorthand -- only a short list of properties
 * is honoured inside ::selection, and the shorthand is not dependably on it.
 *
 * There used to be a second rule here inverting this on the ink theme's cover
 * band, where ink-on-near-black was 1.48:1. It is gone because that band no
 * longer contains anything selectable. Restore it if prose is ever allowlisted
 * back onto a dark surface.
 */
::selection {
  background-color: var(--ink);
  color: var(--paper);
}

/*
 * The halo's spread used to stop at 7px -- exactly the white ring's own outer
 * edge (3px offset + 4px width) -- so it only ever showed under the white,
 * never past it. On a light ground (this is the shop, mostly cream) that left
 * the ring's outer edge touching the page directly, at 1.16:1. 9px puts a
 * real 2px of --ink outside the white on every ground; see styles-core.css,
 * the landing page's copy of this same rule.
 */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
summary:focus-visible {
  outline: 4px solid #fff;
  outline-offset: 3px;
  box-shadow: 0 0 0 9px var(--ink);
}

/*
 * #shareResult (wishlist-page.mjs) is a plain div that only ever receives
 * focus programmatically, once, right after "Save & share this list"
 * reveals it -- the ring above doesn't cover bare divs, so this is the one
 * place a keyboard user needs to actually see where focus jumped to.
 */
#shareResult:focus-visible {
  outline: 4px solid #fff;
  outline-offset: 3px;
  box-shadow: 0 0 0 9px var(--ink);
}

.shop-header {
  position: sticky;
  top: 0;
  z-index: 50;
  min-height: 66px;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  padding: 0 14px;
  color: #fff;
  background: rgba(9, 8, 11, .96);
  border-bottom: 2px solid #fff;
}

.shop-home-mascot {
  position: relative;
  justify-self: start;
  width: 52px;
  height: 62px;
  display: grid;
  place-items: center;
  align-self: center;
  isolation: isolate;
}
.shop-home-mascot::before {
  content: "";
  position: absolute;
  z-index: -1;
  width: 48px;
  height: 48px;
  left: 50%;
  top: 50%;
  background: var(--paper);
  border: 3px solid var(--brand);
  border-radius: 50%;
  transform: translate(-50%, -50%) rotate(-7deg);
}
.shop-home-mascot img {
  width: 44px;
  height: 58px;
  object-fit: contain;
  object-position: center;
  filter:
    drop-shadow(1px 0 0 #fff)
    drop-shadow(-1px 0 0 #fff)
    drop-shadow(0 1px 0 #fff)
    drop-shadow(3px 3px 0 #000);
  transform: translateY(0) rotate(-3deg);
  transition: transform 160ms ease;
}
.shop-home-mascot:hover img,
.shop-home-mascot:focus-visible img {
  transform: translateY(0) rotate(2deg) scale(1.06);
}
.shop-back {
  justify-self: center;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 10px;
  font-size: .63rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .08em;
}
.shop-cart {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  gap: 7px;
  font-size: .68rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .08em;
}
/*
 * The icon that stands in for the word below 560px.
 *
 * Repeated from styles-core.css rather than shared: the shop pages load
 * catalog.css and nothing else, the landing page loads styles-*.css and not
 * this, and there is no stylesheet common to both to put it in. Sized here or
 * the SVG falls back to its own intrinsic box -- it rendered 12x12 inside a
 * 44x44 control before this rule existed.
 */
.cart-icon {
  display: none;
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The count badge, and only the count. This used to read `.shop-cart span`,
   which also caught .shop-cart-word beside it: "Wishlist" was a 63px word
   pushed into a 25px circle, so it rendered as a pale ellipse with the text
   hanging out of both ends and nearly touching the Cart link. The counts are
   addressed by the same attributes cart-courier.mjs animates. */
.shop-cart [data-cart-count],
.shop-cart [data-wishlist-count] {
  min-width: 25px;
  height: 25px;
  display: grid;
  place-items: center;
  color: var(--ink);
  background: var(--brand-tint-deep);
  border-radius: 50%;
}

.catalog-kicker {
  margin: 0 0 13px;
  font-size: .68rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .2em;
}

/*
 * The category cover. Two rules here are load-bearing and were measured rather
 * than guessed (2026-08-24, at 390 / 768 / 1280):
 *
 *   - min-height scales with the viewport instead of being a flat number. A
 *     fixed 660px cover on an 800px-tall laptop left 66px of the product stack
 *     showing, so every category visit opened on a full screen of nothing to
 *     buy. Tying it to vh keeps the statement on a tall monitor and lets the
 *     first row of products peek on a short one.
 *
 *   - the mascot is in the grid flow at this width, not absolute. See
 *     .cover-mascot below for why.
 */
.category-cover {
  position: relative;
  min-height: clamp(400px, 58vh, 500px);
  display: grid;
  align-items: end;
  overflow: hidden;
  padding: 26px 20px 36px;
  color: var(--accent-ink);
  background-color: var(--accent);
  border-bottom: var(--line);
  isolation: isolate;
}
.category-cover::before,
.pick-cover::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  opacity: .2;
  background-image: radial-gradient(var(--ink) 1.6px, transparent 1.6px);
  background-size: 10px 10px;
  transform: rotate(-8deg) scale(1.25);
}
.category-cover::after {
  content: "";
  position: absolute;
  z-index: -1;
  width: 380px;
  height: 380px;
  right: -145px;
  top: 40px;
  background: var(--paper);
  border: 5px solid var(--ink);
  border-radius: 50%;
}
.cover-copy {
  position: relative;
  z-index: 2;
  max-width: 570px;
}
.cover-copy h1 {
  margin: 0;
  max-width: 7ch;
  font-family: var(--font-display);
  font-size: clamp(5.24rem, 24.84vw, 8.28rem);
  font-weight: 400;
  line-height: .78;
  letter-spacing: 0;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}
/*
 * A name with no hyphen and no space past nine letters ("Concentrates") has
 * no break point the browser can use at the default size, so it wraps
 * mid-letter on a phone instead of mid-word. Widening the box and dropping
 * the size lets it break between whole words -- or, single-word, still land
 * two lines that read as a word each time, not a fragment. See the
 * is-long toggle in category-page.mjs.
 */
.cover-copy h1.is-long {
  max-width: 100%;
  font-size: clamp(2.6rem, 13vw, 4.4rem);
}
/*
 * Full width on a phone. The 68% cap was here to keep this paragraph out from
 * under the absolutely-placed mascot; he stacks above the copy at this width
 * now, so the cap only cost the description an extra line. 760px+ puts the cap
 * back, because there he really is standing beside it.
 */
.cover-copy > p:last-child {
  max-width: 410px;
  margin: 28px 0 0;
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.5;
}
/*
 * On a phone he sits above the copy. On 760px+ he goes back to standing beside
 * it, absolutely placed, which is the composition the cover was drawn for.
 *
 * He was absolute at every width until 2026-08-24, and that only works while
 * there is an empty column to his left. At 390px there is not: the headline box
 * ran x=20..355 and he ran x=158..459, so he crossed the entire 164px height of
 * the word. He is *behind* it in the stacking order and that did not help --
 * he is drawn in the same ink the letters are, so CONCENTRATES read as
 * "CONCENT" with the rest lost in his body, and the issue number in the eyebrow
 * went with it. The category name is the one word on the page a shopper needs.
 *
 * Two rows removes the overlap rather than arbitrating it. Nudging him further
 * right does not: at 390px he already bleeds 69px off-screen, and there is no
 * width left to give the headline that he is not standing in.
 */
.cover-mascot {
  position: relative;
  z-index: 1;
  order: -1;
  justify-self: end;
  width: min(46vw, 180px);
  margin-bottom: 8px;
  transform: rotate(5deg);
  filter: drop-shadow(10px 13px 0 var(--ink));
}
.cover-mascot img {
  width: 100%;
  max-height: 240px;
  object-fit: contain;
}
.cover-mascot span {
  position: absolute;
  z-index: 2;
  left: -48px;
  top: 14px;
  padding: 12px 14px 9px;
  color: #fff;
  background: var(--ink);
  border: 3px solid #fff;
  font-family: var(--font-display);
  font-size: 1.35rem;
  letter-spacing: .08em;
  transform: rotate(-10deg);
}

/*
 * The lead-in above the grid is deliberately thin. The cover directly above it
 * has already given the category its name, its issue number and a sentence of
 * description, so every pixel between the two is a second run-up to the same
 * place -- and the products are what the page is for.
 */
.catalog-stack-section {
  padding: 36px 16px 72px;
  background:
    linear-gradient(90deg, transparent 0 49.7%, rgba(17,16,21,.08) 49.8% 50.2%, transparent 50.3%),
    var(--paper);
}
.stack-toolbar {
  max-width: 1220px;
  margin: 0 auto 28px;
}
.stack-toolbar h2,
.drop-banner h2,
.cart-title h1,
.checkout-intro h1,
.route-error h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 13.8vw, 7.18rem);
  font-weight: 400;
  line-height: .86;
  letter-spacing: 0;
  text-transform: uppercase;
}
/* display:grid below would defeat the attribute on its own. */
.view-switch[hidden] { display: none; }
.view-switch {
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin-top: 28px;
  border: var(--line);
  background: var(--ink);
}
.view-switch button {
  min-height: 48px;
  border: 0;
  color: #fff;
  background: var(--ink);
  font-size: .72rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.view-switch button + button { border-left: 2px solid #fff; }
.view-switch button.is-active {
  color: var(--ink);
  background: var(--brand-tint-deep);
}

/* The way into the picker, above the filter bar it is an alternative to.
   Same recipe as the chips beside it: white, ink border, filled on hover. */
.pick-link-row { max-width: 1220px; margin: 0 auto 18px; }
.pick-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 8px 16px;
  color: var(--ink);
  border: 2px solid var(--ink);
  background: #fff;
  box-shadow: 3px 3px 0 var(--ink);
  font-size: .74rem;
  font-weight: 900;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.pick-link:hover { color: var(--paper); background: var(--ink); }

/* The filter bar. Every chip in it was derived from a product that carries the
   value, so this whole block is absent on a category nobody has tagged -- which
   is the seed catalogue, and is the state that ships. */
.catalog-filters {
  display: grid;
  gap: 20px;
  max-width: 1220px;
  margin: 0 auto 18px;
  padding: 20px;
  border: var(--line);
  background: #fff;
}
.catalog-filters[hidden] { display: none; }
.catalog-filter-groups { display: grid; gap: 16px; }
.catalog-filter-legend {
  margin: 0 0 8px;
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.catalog-filter-group > div {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.catalog-filter-group button {
  display: flex;
  align-items: center;
  /* 44px is the floor for a comfortable thumb, the same floor the size chips
     on a product page hold to. */
  min-height: 44px;
  gap: 8px;
  padding: 8px 14px;
  color: var(--ink);
  border: 2px solid var(--ink);
  background: #fff;
  font-size: .74rem;
  font-weight: 900;
  letter-spacing: .04em;
  cursor: pointer;
}
.catalog-filter-group button small {
  padding: 1px 6px;
  color: #615b64;
  background: #efecf1;
  font-size: .62rem;
}
/* Pressed is a filled chip rather than only a border change: at arm's length
   a two-pixel border is not the difference between "filtered" and "not". */
.catalog-filter-group button[aria-pressed="true"] {
  color: var(--paper);
  background: var(--ink);
  box-shadow: 3px 3px 0 var(--accent);
}
.catalog-filter-group button[aria-pressed="true"] small {
  color: var(--ink);
  background: var(--accent);
}
.catalog-filter-sort {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.catalog-filter-sort[hidden] { display: none; }
.catalog-filter-sort label {
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.catalog-filter-sort select {
  min-height: 44px;
  padding: 8px 12px;
  color: var(--ink);
  border: 2px solid var(--ink);
  background: #fff;
  font: inherit;
  font-size: .78rem;
  font-weight: 900;
}

/* What is on screen, said out loud. Filtering rewrites the stack with no focus
   movement, so a screen-reader user is told nothing by the redraw itself. */
.catalog-filter-summary {
  max-width: 1220px;
  margin: 0 auto 22px;
  font-size: .7rem;
  font-weight: 900;
  letter-spacing: .09em;
  text-transform: uppercase;
}
.catalog-filter-summary:empty { display: none; }

.catalog-stack {
  max-width: 1220px;
  margin: 0 auto;
}
.catalog-story {
  position: relative;
  min-height: 550px;
  display: grid;
  grid-template-rows: auto 1fr;
  overflow: hidden;
  margin-top: 22px;
  border: var(--line);
  background: #fff;
  box-shadow: 8px 8px 0 var(--ink);
}
.catalog-story:nth-child(even) {
  --story-copy-bg: var(--ink);
  --story-copy-ink: #fff;
  color: #fff;
  background: var(--ink);
  box-shadow: 8px 8px 0 var(--accent);
}
.catalog-story:nth-child(odd) {
  --story-copy-bg: #fff;
  --story-copy-ink: var(--ink);
}
.catalog-story-number {
  position: absolute;
  z-index: 3;
  right: 14px;
  top: 6px;
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 6.5rem;
  line-height: 1;
}
.catalog-story-copy {
  position: relative;
  z-index: 4;
  padding: 28px 24px 12px;
}
/* The poster's format line is named rather than positioned. It was
   `.product-poster-copy > p:first-child`, and the JUST DROPPED strip and Boom's
   Pick both go into that column ahead of it -- so on any product added in the
   last fortnight the eyebrow silently dropped to plain body text, and repaired
   itself on day 15 when the strip stopped rendering. */
.catalog-story-copy small,
.catalog-compact-card small,
.product-poster-eyebrow,
.cart-line-copy small {
  font-size: .65rem;
  font-weight: 900;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.catalog-story-copy h2 {
  max-width: 8ch;
  margin: 12px 0;
  font-family: var(--font-display);
  font-size: clamp(3.31rem, 14.72vw, 5.8rem);
  font-weight: 400;
  line-height: .88;
  text-transform: uppercase;
}
.catalog-story-copy p {
  max-width: 390px;
  margin: 0;
  color: #67616a;
  line-height: 1.55;
}
.catalog-story:nth-child(even) .catalog-story-copy p { color: #bdb7c1; }
.catalog-price {
  display: grid;
  gap: 3px;
  width: fit-content;
  margin: 14px 0 0;
  color: currentColor;
  font-family: var(--font-display);
  font-size: 1.65rem;
  font-weight: 400;
  line-height: 1;
  letter-spacing: .02em;
}
.catalog-price span {
  color: inherit;
  font-family: var(--font-body);
  font-size: .57rem;
  font-weight: 900;
  line-height: 1.3;
  letter-spacing: .09em;
  text-transform: uppercase;
}
.catalog-story-copy a {
  display: inline-flex;
  align-items: center;
  gap: 20px;
  margin-top: 24px;
  padding-bottom: 5px;
  border-bottom: 3px solid currentColor;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .08em;
}
.catalog-story-instruction {
  width: fit-content;
  display: block;
  margin-top: 24px;
  padding-bottom: 5px;
  border-bottom: 3px solid currentColor;
  font-size: .69rem;
  font-weight: 900;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.catalog-product-stage {
  min-height: 280px;
  display: grid;
  place-items: center;
  padding: 35px 20px 48px;
  background-color: var(--accent);
  background-image: repeating-linear-gradient(-48deg, transparent 0 18px, rgba(255,255,255,.18) 18px 22px);
  border-top: var(--line);
}
.catalog-story:nth-child(even) .catalog-product-stage { background-color: var(--brand-tint-deep); }

.has-interactive-model {
  grid-template-rows: minmax(300px, auto) minmax(430px, 56vh);
  grid-template-areas:
    "copy"
    "stage";
  contain: layout paint;
  isolation: isolate;
}
.has-interactive-model::after {
  position: absolute;
  z-index: 20;
  inset: 0;
  content: "";
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
}
.has-interactive-model.is-switching::after {
  animation: catalog-switch-flash 210ms steps(2, end);
}
@keyframes catalog-switch-flash {
  0% { opacity: 0; }
  35% { opacity: .32; }
  100% { opacity: 0; }
}
.has-interactive-model .catalog-story-copy {
  grid-area: copy;
  min-width: 0;
  min-height: 300px;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
  color: var(--story-copy-ink);
  background: var(--story-copy-bg);
  transition: opacity 170ms ease, visibility 170ms;
}
.has-interactive-model .catalog-product-stage {
  position: relative;
  z-index: 5;
  grid-area: stage;
  min-width: 0;
  min-height: 430px;
  overflow: hidden;
  padding: 0;
  transform-origin: center;
}
.has-interactive-model .catalog-product-stage canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: none;
}
.has-interactive-model .catalog-product-stage canvas.is-dragging { cursor: grabbing; }
.has-interactive-model .catalog-product-stage canvas.is-over-target { cursor: pointer; }

.catalog-model-loading {
  position: absolute;
  z-index: 3;
  top: 50%;
  left: 50%;
  display: grid;
  justify-items: center;
  gap: 10px;
  transform: translate(-50%, -50%);
  /* The stage behind this is background-color: var(--accent), which is a
     different colour on each of the seven themes -- so "LOADING" was white on
     the paper and gold stages, where it measured 1.63:1 and 2.01:1. --accent-ink
     is the token that already knows which of the two inks that accent takes. */
  color: var(--accent-ink);
  font-size: .62rem;
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
  transition: opacity 180ms, visibility 180ms;
}
.catalog-model-loading.is-hidden {
  visibility: hidden;
  opacity: 0;
}
.catalog-model-loading i {
  width: 42px;
  height: 42px;
  /* Rides the label's colour for the same reason the label does — a white ring
     on the paper stage was a spinner nobody could see it spin. */
  border: 3px solid color-mix(in srgb, currentColor 25%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: catalog-model-spin 700ms linear infinite;
}
@keyframes catalog-model-spin { to { transform: rotate(360deg); } }

.catalog-model-hint {
  position: absolute;
  z-index: 6;
  right: 50%;
  bottom: 15px;
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  padding: 8px 10px;
  color: #fff;
  background: rgba(10,7,13,.75);
  border: 1px solid rgba(255,255,255,.35);
  font-size: .58rem;
  font-weight: 900;
  letter-spacing: .09em;
  text-transform: uppercase;
  transform: translateX(50%);
  white-space: nowrap;
  pointer-events: none;
}
.catalog-model-hint b {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}
.catalog-model-toggle {
  position: absolute;
  z-index: 8;
  left: 14px;
  bottom: 14px;
  min-height: 38px;
  padding: 0 12px;
  border: 2px solid #fff;
  color: #fff;
  background: var(--ink);
  font-size: .62rem;
  font-weight: 900;
  letter-spacing: .09em;
  text-transform: uppercase;
}
.catalog-model-toggle:not(:focus-visible) {
  width: 1px;
  height: 1px;
  min-height: 0;
  overflow: hidden;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}
.catalog-model-fallback {
  position: absolute;
  z-index: 7;
  inset: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 18px;
}
.catalog-model-fallback .catalog-pack { width: 150px; }
.catalog-model-fallback button {
  min-height: 44px;
  padding: 0 18px;
  border: 2px solid #fff;
  color: #fff;
  background: var(--ink);
  font-weight: 900;
  text-transform: uppercase;
}

.catalog-story-info {
  position: relative;
  z-index: 4;
  grid-area: stage;
  min-width: 0;
  display: grid;
  grid-template-rows: 1fr auto;
  align-content: stretch;
  padding: 24px 20px;
  color: var(--story-copy-ink);
  background: var(--story-copy-bg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(22px);
  transition: opacity 220ms 120ms ease, transform 360ms 80ms ease, visibility 0s 360ms;
}
.catalog-story-info-copy {
  align-self: center;
  min-width: 0;
}
.catalog-story-info h3 {
  max-width: 8ch;
  margin: 0 0 18px;
  font-family: var(--font-display);
  font-size: clamp(2.76rem, 12.88vw, 5.34rem);
  font-weight: 400;
  line-height: .87;
  text-transform: uppercase;
}
.catalog-story-info-copy > p:not(.catalog-model-credit) {
  max-width: 430px;
  margin: 0;
  line-height: 1.58;
}
.catalog-model-credit {
  max-width: 520px;
  margin: 22px 0 0;
  padding-top: 14px;
  border-top: 2px solid currentColor;
  font-size: .61rem;
  font-weight: 700;
  line-height: 1.55;
  opacity: .72;
}
.catalog-model-credit a { text-decoration: underline; text-underline-offset: 2px; }
.catalog-story-details {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
  padding-bottom: 4px;
  border-bottom: 2px solid currentColor;
  color: inherit;
  font-size: .7rem;
  font-weight: 900;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.catalog-story-add {
  align-self: end;
  width: 100%;
  min-height: 82px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  margin-top: 26px;
  padding: 15px 17px;
  border: 3px solid currentColor;
  color: inherit;
  background: transparent;
  text-align: left;
}
.catalog-story-add span { display: grid; gap: 3px; }
.catalog-story-add small {
  font-size: .56rem;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.catalog-story-add strong {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 400;
  line-height: 1;
  text-transform: uppercase;
}
.catalog-story-add > b { font-size: 2rem; }
.has-interactive-model.is-added .catalog-story-add {
  color: #fff;
  background: var(--green);
  border-color: var(--ink);
}

.has-interactive-model.is-open .catalog-story-copy {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.has-interactive-model.is-open .catalog-product-stage {
  grid-area: copy;
  min-height: 300px;
  background-color: color-mix(in srgb, var(--accent) 48%, var(--ink));
}
.has-interactive-model.is-open .catalog-story-info {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 220ms 140ms ease, transform 360ms 90ms ease, visibility 0s;
}
.has-interactive-model.is-open .catalog-model-hint { opacity: .42; }
/* Screen-reader-only text, shared by the shop pages and admin (which loads
   this sheet): the rack's live region, the compact add button's spoken label,
   admin's .admin-sr spans. styles-core.css carries the same rule for the
   landing page. */
.catalog-status-region,
.catalog-pull-row .catalog-compact-add span,
.admin-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The live region has always sat fixed; absolute is only the shared default. */
.catalog-status-region { position: fixed; }

.catalog-pack {
  width: min(51vw, 230px);
  aspect-ratio: .68;
  display: grid;
  align-content: center;
  justify-items: center;
  color: #fff;
  background: var(--night);
  border: 5px solid #fff;
  outline: 4px solid var(--ink);
  box-shadow: 15px 18px 0 rgba(17,16,21,.9);
  transform: rotate(-4deg);
}
.catalog-pack::before {
  content: "✦";
  margin-bottom: 10px;
  color: var(--accent);
  font-size: 2.3rem;
}
.catalog-pack span {
  font-family: var(--font-display);
  font-size: clamp(2.76rem, 11.96vw, 4.97rem);
  line-height: .85;
}
.catalog-pack small {
  margin-top: 9px;
  color: var(--brand-tint-deep);
  font-size: .58rem;
  font-weight: 900;
  letter-spacing: .2em;
  text-transform: uppercase;
}
.catalog-story:nth-child(even) .catalog-pack { transform: rotate(5deg); }

.catalog-stack.is-condensed {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.catalog-compact-card {
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 10px 10px 16px;
  border: var(--line);
  background: #fff;
  box-shadow: 5px 5px 0 var(--ink);
}
.catalog-compact-stage {
  min-height: 210px;
  display: grid;
  place-items: center;
  margin-bottom: 16px;
  overflow: hidden;
  background: var(--accent);
  border-bottom: var(--line);
}
.catalog-compact-stage .catalog-pack {
  width: 94px;
  border-width: 3px;
  outline-width: 2px;
  box-shadow: 7px 8px 0 var(--ink);
}
.catalog-compact-stage .catalog-pack::before { font-size: 1rem; margin-bottom: 5px; }
.catalog-compact-stage .catalog-pack span { font-size: 2rem; }
/*
 * .catalog-compact-link small (below, same specificity, later in the file)
 * was winning the cascade for color on this element too -- the pack's own
 * dark-background caption was rendering in the card's light-background gray
 * (#6e6871 on this poster's near-black, 3.69:1) instead of its intended
 * --brand-tint-deep. Declaring color here, at higher specificity, restores it.
 */
.catalog-compact-stage .catalog-pack small { color: var(--brand-tint-deep); font-size: .65rem; }
.catalog-compact-link {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
}
.catalog-compact-link strong {
  font-family: var(--font-display);
  font-size: 1.45rem;
  font-weight: 400;
  line-height: 1;
  text-transform: uppercase;
}
.catalog-compact-link small {
  margin-top: 7px;
  color: #6e6871;
  font-size: .53rem;
}

/*
 * The pull list. A grid child on its own -- .catalog-stack.is-condensed sets
 * two columns for the cards renderCondensedProducts prints as its direct
 * children, and .is-pull-list overrides that back to one so the single <ol>
 * renderPullList prints in its place spans the full row instead of sitting in
 * half of it.
 */
.catalog-stack.is-condensed.is-pull-list {
  display: block;
}
.catalog-pull-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: var(--line);
  background: #fff;
  box-shadow: 5px 5px 0 var(--ink);
}
.catalog-pull-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-bottom: 2px solid var(--paper-deep);
}
.catalog-pull-row:last-child { border-bottom: 0; }
.catalog-pull-row.is-sold-through { color: #6e6871; }
.catalog-pull-link {
  display: flex;
  flex: 1;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.catalog-pull-thumb {
  flex: none;
  display: grid;
  width: 52px;
  height: 68px;
  place-items: center;
  overflow: hidden;
  background: var(--accent);
  border: 2px solid var(--ink);
  box-shadow: 3px 3px 0 var(--ink);
}
.catalog-pull-thumb img { width: 100%; height: 100%; object-fit: cover; }
.catalog-pull-thumb .catalog-pack {
  width: 40px;
  border-width: 2px;
  outline-width: 1px;
  box-shadow: 4px 5px 0 var(--ink);
}
.catalog-pull-thumb .catalog-pack::before { font-size: .5rem; margin-bottom: 3px; }
.catalog-pull-thumb .catalog-pack span { font-size: .95rem; }
.catalog-pull-thumb .catalog-pack small { font-size: .28rem; }
.catalog-pull-row.is-sold-through .catalog-pull-thumb { filter: grayscale(1); opacity: .6; }
.catalog-pull-copy {
  display: grid;
  gap: 2px;
  min-width: 0;
}
.catalog-pull-copy strong {
  overflow: hidden;
  font-family: var(--font-display);
  font-size: 1.12rem;
  font-weight: 400;
  line-height: 1;
  letter-spacing: .01em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}
.catalog-pull-copy small {
  color: #6e6871;
  font-size: .55rem;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.catalog-pull-copy .catalog-price { margin: 3px 0 0; font-size: .82rem; }
.catalog-pull-copy .catalog-price span { font-size: .48rem; }
.catalog-pull-row .catalog-compact-controls { flex: none; margin-top: 0; }
/*
 * The + shrinks from the grid card's full-width button to a thumb-sized
 * square -- the row already names the product and its price, so the button's
 * own label is redundant here and stays for a screen reader only.
 */
.catalog-pull-row .catalog-compact-add {
  position: relative;
  flex-direction: column;
  width: 48px;
  height: 48px;
  padding: 0;
}
.catalog-pull-row .catalog-compact-add b { font-size: 1.3rem; }
.catalog-pull-row .catalog-compact-sold-out {
  position: relative;
  width: 48px;
  height: 48px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  text-indent: -999px;
}
.catalog-pull-row .catalog-compact-sold-out::before {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  content: "×";
  font-size: 1.2rem;
  text-indent: 0;
}
.catalog-pull-row .catalog-compact-controls .quantity-control { width: 116px; }

.drop-banner {
  padding: 70px 20px;
  color: #fff;
  background-color: var(--brand-deep);
  background-image: radial-gradient(rgba(255,255,255,.19) 1px, transparent 1px);
  background-size: 11px 11px;
  border-block: var(--line);
}
.drop-banner h2 { max-width: 7ch; }
.drop-banner > p:not(.catalog-kicker) {
  max-width: 540px;
  margin: 24px 0 28px;
  color: #eee7f1;
  line-height: 1.6;
}
.drop-banner a,
.route-back-link {
  display: inline-flex;
  gap: 24px;
  padding: 14px 16px;
  color: var(--ink);
  background: #fff;
  border: var(--line);
  box-shadow: 5px 5px 0 var(--ink);
  font-size: .72rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/*
 * The footer payoff. Every shop page used to end on a 100px strip of legal
 * text, which is a correct way to end a page and a poor way to end a comic.
 * So the last thing above it is the wordmark with Boom leaning on it.
 *
 * On --night, the same ground as the strip below, so the two read as one
 * footer rather than as a band and then a footer. That also settles the
 * wordmark: its fill is black and its bevel is chrome, so on near-black the
 * fill disappears into the ground and what is left is the silver edge and the
 * gold pinstripe -- embossed, which is what a sign at the back of a shop
 * looks like.
 *
 * Purple was the other candidate, to rhyme with the home page's sendoff. It
 * is out because the category page already ends with .drop-banner, and two
 * identical purple bands stacked would read as one broken one.
 *
 * Nothing here moves and nothing here is a control: the stage is aria-hidden
 * and the sendoff line is the only sentence.
 */
.shop-sendoff {
  position: relative;
  padding: clamp(30px, 5vw, 56px) 20px clamp(18px, 2.6vw, 30px);
  background: var(--night);
  overflow: hidden;
}
/*
 * A flex row, bottom-aligned, rather than the two of them placed by
 * percentages against the stage. The stage has no height of its own until the
 * wordmark loads, so every percentage in the first version was measured off a
 * box that did not exist yet -- and he rendered entirely behind the sign.
 */
.shop-sendoff-stage {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  width: min(100%, 760px);
  margin: 0 auto;
}
.shop-sendoff-wordmark {
  position: relative;
  /* Above him, so the sign crosses his legs: what makes him read as leaning
     ON it rather than standing in front of it. */
  z-index: 2;
  width: min(62%, 470px);
  height: auto;
}
.shop-sendoff-boom {
  position: relative;
  z-index: 1;
  width: auto;
  /* Taller than the sign so his head clears it, and pulled back over its right
     end so his weight lands on the "!". */
  height: min(27vw, 208px);
  /* Enough to touch the "!" and not enough to bury it. -44px was the first
     try and his shoulder sat on the M. */
  margin-left: min(-2.4vw, -18px);
  transform: rotate(-7deg);
  transform-origin: 50% 100%;
}
.shop-sendoff-line {
  position: relative;
  z-index: 3;
  margin: clamp(10px, 1.6vw, 18px) 0 0;
  color: #aaa3ad;
  font-family: var(--font-display);
  font-size: clamp(.95rem, 2.4vw, 1.3rem);
  font-weight: 400;
  letter-spacing: .04em;
  text-align: center;
  text-transform: uppercase;
}

.shop-footer {
  min-height: 100px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 15px;
  padding: 24px 20px;
  color: #aaa3ad;
  background: var(--night);
  font-size: .64rem;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.shop-footer a { color: #fff; }
.shop-footer > a {
  align-self: start;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.shop-footer-social {
  display: inline-flex;
  gap: 14px;
}
.shop-footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  color: var(--brand-soft, #c9c1e7);
  transition: transform 180ms ease, color 180ms ease;
}
/* See .social-icon[hidden] in styles-content.css: without this a link the
   operator cleared in admin stays visible and clickable on the shop pages. */
.shop-footer-social a[hidden] { display: none; }
.shop-footer-social a:hover,
.shop-footer-social a:focus-visible {
  transform: translateY(-2px);
  color: #fff;
}

.product-main {
  min-height: calc(100svh - 66px);
  padding: 20px 14px 70px;
  background-color: var(--paper);
  background-image: radial-gradient(rgba(17,16,21,.13) 1px, transparent 1px);
  background-size: 13px 13px;
}
.product-poster {
  max-width: 1080px;
  margin: 0 auto;
  border: var(--line);
  background: #fff;
  box-shadow: 9px 9px 0 var(--ink);
}
.product-poster-art {
  position: relative;
  min-height: 470px;
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: 60px 20px 32px;
  background: var(--accent);
  border-bottom: var(--line);
}
.product-poster-art::before {
  content: "";
  position: absolute;
  width: 360px;
  height: 360px;
  background: var(--paper);
  border: 4px solid var(--ink);
  border-radius: 50%;
}
.product-poster-art .catalog-pack { position: relative; z-index: 2; }

/* Gallery: one photo showing, the rest a strip of thumbnails beneath it. The
   frame keeps its height as photos swap so choosing a colour does not shunt
   the page under the reader's thumb. */
.product-gallery {
  position: relative;
  z-index: 2;
  display: grid;
  gap: 14px;
  justify-items: center;
  width: 100%;
}
.product-gallery .catalog-photo { grid-area: 1 / 1; }
.product-gallery .catalog-photo[hidden] { display: none; }
.product-gallery-strip {
  grid-area: 2 / 1;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}
.product-gallery-pick {
  width: 56px;
  height: 56px;
  padding: 0;
  border: 2px solid var(--ink);
  background: var(--paper);
  cursor: pointer;
  overflow: hidden;
}
.product-gallery-pick img { width: 100%; height: 100%; object-fit: cover; display: block; }
.product-gallery-pick.is-current { box-shadow: 0 0 0 3px var(--ink); }
.product-gallery-pick:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
.product-poster-art img {
  position: absolute;
  z-index: 3;
  right: -38px;
  bottom: -72px;
  width: 48%;
  max-height: 330px;
  object-fit: contain;
  transform: rotate(8deg);
  filter: drop-shadow(8px 8px 0 rgba(17,16,21,.78));
}
.product-issue {
  position: absolute;
  z-index: 4;
  left: 12px;
  top: 12px;
  padding: 8px 10px;
  color: #fff;
  background: var(--ink);
  font-size: .58rem;
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
}
/* The comic corner box: the mascot standing in a framed square on the cover,
   the way every printed issue carries its character in the corner. Decorative
   only — hidden from assistive tech, transparent to the pointer. */
.product-corner-box {
  position: absolute;
  z-index: 4;
  top: 12px;
  right: 12px;
  width: 62px;
  height: 78px;
  display: block;
  padding: 5px 4px;
  background: var(--paper);
  border: 3px solid var(--ink);
  box-shadow: 4px 4px 0 rgba(17,16,21,.4);
  overflow: hidden;
  pointer-events: none;
  /* Hand-slapped on the cover, not typeset. The pop-in overshoots then
     settles onto this same tilt (no fill mode, so they must agree). */
  transform: rotate(2.5deg);
  animation: corner-box-in .5s cubic-bezier(.34, 1.56, .64, 1);
}
/* The starburst behind him: category-accent rays, printed comic style. */
.product-corner-box::before {
  content: "";
  position: absolute;
  inset: -14px;
  background: repeating-conic-gradient(
    from 8deg,
    var(--accent) 0deg 11deg,
    var(--paper) 11deg 30deg
  );
  opacity: .5;
}
/* Doubled-up selector: .product-poster-art img (and its desktop media-query
   variant) matches this img too, so the bust needs the higher specificity.
   relative, not static: he has to paint above the ::before rays. */
.product-corner-box img,
.product-poster-art .product-corner-box img {
  position: relative;
  inset: auto;
  width: 100%;
  height: 100%;
  max-height: none;
  /* The icon is a square canvas with wide transparent margins; cover crops
     into the figure instead of framing the empty air around it. */
  object-fit: cover;
  object-position: 50% 18%;
  /* The webp still has air inside its own canvas; the scale fills the frame
     with mascot, and the box's overflow: hidden crops the spill. */
  transform: scale(1.3);
  filter: none;
}
/* He notices you looking. Once per hover, then back to standing guard. */
.product-poster-art:hover .product-corner-box img {
  animation: corner-peek .5s ease-in-out;
}
@keyframes corner-box-in {
  0% { transform: scale(0) rotate(-20deg); }
  70% { transform: scale(1.15) rotate(6deg); }
  100% { transform: scale(1) rotate(2.5deg); }
}
@keyframes corner-peek {
  0%, 100% { transform: scale(1.3) rotate(0deg); }
  30% { transform: scale(1.42) rotate(-7deg); }
  65% { transform: scale(1.42) rotate(5deg); }
}

/* The cart courier: the mascot mid-flight from the add controls to a header
   badge. Created, animated, and removed by cart-courier.mjs; motion never
   starts under prefers-reduced-motion. */
.cart-courier {
  position: fixed;
  z-index: 300;
  width: 64px;
  pointer-events: none;
  filter: drop-shadow(4px 4px 0 rgba(17,16,21,.35));
}
/* The smoke he tumbles through: dropped along the live flight path by
   cart-courier.mjs, each puff swells, thins and removes itself. */
.courier-puff {
  position: fixed;
  z-index: 299;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: var(--paper-deep);
  border: 2px solid var(--ink);
  pointer-events: none;
  animation: courier-puff .45s ease-out forwards;
}
@keyframes courier-puff {
  0% { transform: scale(.5); opacity: .9; }
  100% { transform: scale(1.9); opacity: 0; }
}
/* The payoff: he is a bomb, so arrival is a detonation, not a fade. An
   eight-point gold star with the word on it, gone in half a second. */
.courier-burst {
  position: fixed;
  z-index: 301;
  display: grid;
  place-items: center;
  width: 118px;
  height: 118px;
  transform: translate(-50%, -50%);
  color: var(--ink);
  font-size: .92rem;
  font-weight: 900;
  letter-spacing: .02em;
  background: var(--gold);
  clip-path: polygon(50% 0%, 59% 35%, 90% 21%, 68% 46%, 100% 50%, 68% 54%, 90% 79%, 59% 65%, 50% 100%, 41% 65%, 10% 79%, 32% 54%, 0% 50%, 32% 46%, 10% 21%, 41% 35%);
  filter: drop-shadow(3px 3px 0 rgba(17,16,21,.5));
  pointer-events: none;
  animation: courier-burst .5s cubic-bezier(.22, 1.4, .36, 1) forwards;
}
@keyframes courier-burst {
  0% { transform: translate(-50%, -50%) scale(.2) rotate(-25deg); opacity: 1; }
  55% { transform: translate(-50%, -50%) scale(1.1) rotate(3deg); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1.3) rotate(8deg); opacity: 0; }
}
.shop-cart .courier-pop {
  display: inline-block;
  animation: courier-pop .4s ease-out;
}
@keyframes courier-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.9) rotate(-8deg); }
  70% { transform: scale(1.35) rotate(5deg); }
  100% { transform: scale(1) rotate(0deg); }
}
/* Sparks off the quantity stepper: each +/- tap strikes the fuse. Spawned at
   the tapped button by cart-courier.mjs, flung along per-spark --spark-x/-y
   vectors, gone in half a second. */
.stepper-spark {
  position: fixed;
  z-index: 300;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  background: var(--gold);
  clip-path: polygon(50% 0%, 63% 37%, 100% 50%, 63% 63%, 50% 100%, 37% 63%, 0% 50%, 37% 37%);
  filter: drop-shadow(1px 1px 0 var(--ink));
  pointer-events: none;
  animation: stepper-spark .5s cubic-bezier(.15, .7, .35, 1) forwards;
}
.stepper-spark.is-accent { background: var(--accent); }
@keyframes stepper-spark {
  0% { transform: translate(0, 0) rotate(0deg) scale(1.15); opacity: 1; }
  100% { transform: translate(var(--spark-x), var(--spark-y)) rotate(160deg) scale(.15); opacity: 0; }
}

/* SOLD THROUGH is a rubber stamp slammed across the cover, not dead space. It
   arrives from above at triple size and thunks down onto its tilt — the
   static transform and the keyframes' last frame must agree (no fill mode).

   One stamp, two sizes. The poster's cover is most of a screen; the grid
   card's art is a thumbnail two-across on a phone, so it scales with the
   viewport there rather than keeping the poster's type. Both are the same
   stamp, and that is deliberate: a sold-out comic looks the same wherever it
   is racked. */
.product-sold-through {
  position: absolute;
  z-index: 5;
  padding: 10px 24px 8px;
  border: 5px solid var(--ink);
  box-shadow: inset 0 0 0 3px var(--paper), inset 0 0 0 5px var(--ink);
  color: var(--ink);
  background: rgba(241, 238, 231, .85);
  font-family: var(--font-display);
  font-size: clamp(1.93rem, 6.44vw, 3.13rem);
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  transform: rotate(-11deg);
  pointer-events: none;
  animation: stamp-slam .4s cubic-bezier(.2, .9, .3, 1.3);
}
@keyframes stamp-slam {
  0% { transform: rotate(-11deg) scale(3); opacity: 0; }
  60% { transform: rotate(-11deg) scale(.94); opacity: 1; }
  100% { transform: rotate(-11deg) scale(1); }
}

/* The grid card and the story card. The stage is already position: relative
   (drop-stamp.css sets it on the compact one), so the stamp centres on the art
   the same way it does on a poster. */
.catalog-compact-stage,
.catalog-story .catalog-product-stage { position: relative; }
.catalog-compact-stage .product-sold-through,
.catalog-story .product-sold-through {
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  padding: 5px 12px 4px;
  border-width: 3px;
  box-shadow: inset 0 0 0 2px var(--paper), inset 0 0 0 3px var(--ink);
  font-size: clamp(.72rem, 3.1vw, 1.05rem);
  /* Opaque here, where the poster's stamp is 85%. The cover under this one has
     had its colour drained and a cream wash laid over it, and a translucent
     cream stamp on a cream-washed cover is a stamp nobody can read — measured
     at 1280 before this line existed. */
  background: var(--paper, #f2efe9);
}

/* The pull-list thumb: same centred slam, cut down for a 52x68 ground --
   at the grid card's own size the stamp would be wider than the row's art. */
.catalog-pull-thumb .product-sold-through {
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  padding: 2px 5px 1px;
  border-width: 2px;
  box-shadow: inset 0 0 0 1px var(--paper), inset 0 0 0 2px var(--ink);
  font-size: .34rem;
  background: var(--paper, #f2efe9);
}

/* Greyed, not dimmed, and the whole cover rather than the art inside it.

   This is sealSoldOut (featured-comic-art.mjs) ported to CSS: the 3D shelf
   drains the saturation out of the whole cover canvas with a `saturation`
   composite and then lays a cream wash over it, so a comic that is gone looks
   the same whether it is racked on the home page or in the grid. A plain
   opacity fade read as a loading failure, which is the one thing this must not
   look like — and filtering only the stage's CHILDREN left the category colour
   behind them at full strength, which read as half-greyed rather than spent.

   Two layers because one element carries one blend mode: ::before drains, and
   ::after washes. Both sit under the stamp, which is z-index 5 and stays in
   colour — a greyed stamp on greyed art is unreadable, and the stamp is the
   part that has to carry. */
.catalog-compact-card.is-sold-through .catalog-compact-stage::before,
.catalog-compact-card.is-sold-through .catalog-compact-stage::after,
.catalog-story.is-sold-through .catalog-product-stage::before,
.catalog-story.is-sold-through .catalog-product-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}
.catalog-compact-card.is-sold-through .catalog-compact-stage::before,
.catalog-story.is-sold-through .catalog-product-stage::before {
  background: hsl(0, 0%, 50%);
  mix-blend-mode: saturation;
}
.catalog-compact-card.is-sold-through .catalog-compact-stage::after,
.catalog-story.is-sold-through .catalog-product-stage::after {
  background: rgba(232, 228, 220, .42);
}

/* The cover furniture: the printed apparatus of a real comic — issue circle,
   barcode box, approval seal. All decorative; every number on them is real
   catalog data (see coverFurnitureMarkup). */
.product-cover-price {
  position: absolute;
  z-index: 4;
  left: 12px;
  top: 54px;
  width: 66px;
  height: 66px;
  display: grid;
  place-content: center;
  gap: 1px;
  text-align: center;
  color: var(--ink);
  background: var(--paper);
  border: 3px solid var(--ink);
  border-radius: 50%;
  box-shadow: 3px 3px 0 rgba(17,16,21,.4);
  transform: rotate(-6deg);
  pointer-events: none;
}
.product-cover-price small {
  font-size: .48rem;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.product-cover-price b {
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 400;
  line-height: 1;
}
.product-cover-price i {
  font-style: normal;
  font-size: .56rem;
  font-weight: 800;
}
.product-cover-barcode {
  position: absolute;
  z-index: 4;
  left: 12px;
  bottom: 12px;
  display: grid;
  gap: 3px;
  padding: 5px 7px 4px;
  background: #fff;
  border: 2px solid var(--ink);
  pointer-events: none;
}
/* Two stripe rhythms overlaid so the code reads printed, not ruled. */
.product-cover-barcode i {
  display: block;
  width: 76px;
  height: 24px;
  background:
    repeating-linear-gradient(90deg, var(--ink) 0 2px, transparent 2px 5px),
    repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 7px);
}
.product-cover-barcode small {
  font-size: .48rem;
  font-weight: 800;
  letter-spacing: .14em;
  text-align: center;
  color: var(--ink);
}
.product-cover-seal {
  position: absolute;
  z-index: 4;
  right: 8px;
  top: 102px;
  width: 96px;
  height: 96px;
  display: grid;
  place-items: center;
  text-align: center;
  color: var(--ink);
  background: var(--gold);
  /* The star's waist is about two thirds of its box, so the words have to be
     kept inside that rather than inside the square. Before this they ran out
     through both points of the star and the second line read as clipped. */
  padding: 0 17px;
  font-size: .5rem;
  font-weight: 900;
  line-height: 1.3;
  letter-spacing: .02em;
  text-transform: uppercase;
  clip-path: polygon(50% 0%, 59% 35%, 90% 21%, 68% 46%, 100% 50%, 68% 54%, 90% 79%, 59% 65%, 50% 100%, 41% 65%, 10% 79%, 32% 54%, 0% 50%, 32% 46%, 10% 21%, 41% 35%);
  transform: rotate(9deg);
  pointer-events: none;
}

/* The page-turn footer: the issues either side of this one. Same poster
   language — hard border, thrown shadow — with a dog-eared corner on each
   panel pointing the way it goes. */
/*
 * The shelf under the issue walk.
 *
 * Its heading is deliberately not the Anton slab the other section headings on
 * this site use. At clamp(3.8rem, 15vw, 7.8rem) it would be larger than the
 * product's own name a screen above it, and this is the smaller offer on the
 * page -- somebody arrived here for the poster, not for the shelf.
 *
 * The rule above it does the separating, so the grid needs no panel of its own
 * and the compact cards read as continuous with the category page they came
 * from.
 */
.product-related {
  max-width: 1080px;
  margin: 46px auto 0;
  padding-top: 34px;
  border-top: var(--line);
}
.product-related-head {
  margin-bottom: 20px;
}
.product-related-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.93rem, 6.44vw, 2.85rem);
  font-weight: 400;
  line-height: .9;
  letter-spacing: 0;
  text-transform: uppercase;
}
/* The width is set by .product-related above, so the grid does not carry the
   category page's own 1220px cap and end up wider than the poster. */
.product-related-stack { max-width: none; }

/*
 * "Last seen on the shelf" is the same section again, directly under the first
 * one, so it needs to be told apart at a glance rather than only by reading
 * the heading. A dashed rule instead of the solid one: what Boom put next to
 * this product is the shop talking, what you were looking at is a note to
 * yourself, and the two should not carry the same weight.
 *
 * Quieter in every other way too -- smaller heading, less air above -- because
 * the shelf above it is the one with the argument behind it.
 */
.product-last-seen {
  margin-top: 34px;
  padding-top: 26px;
  border-top: 3px dashed rgba(17, 16, 21, .28);
}
.product-last-seen .product-related-head h2 {
  font-size: clamp(1.44rem, 4.6vw, 2rem);
}

.product-issue-walk {
  max-width: 1080px;
  margin: 26px auto 0;
  display: grid;
  gap: 16px;
}
.product-issue-walk-link {
  position: relative;
  display: grid;
  gap: 6px;
  padding: 18px 20px;
  border: var(--line);
  background: #fff;
  box-shadow: 6px 6px 0 var(--ink);
  color: var(--ink);
  text-decoration: none;
  transition: transform .15s ease, box-shadow .15s ease;
}
.product-issue-walk-link:hover {
  transform: translate(-2px, -2px);
  box-shadow: 8px 8px 0 var(--ink);
}
.product-issue-walk-link:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}
.product-issue-walk-link small {
  font-size: .6rem;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.product-issue-walk-link strong {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 400;
  letter-spacing: .01em;
  text-transform: uppercase;
}
.product-issue-walk-link.is-next { text-align: right; }
/* The turned corner, folded toward where the link goes. */
.product-issue-walk-link::after {
  content: "";
  position: absolute;
  top: 0;
  width: 0;
  height: 0;
  border-style: solid;
}
.product-issue-walk-link.is-next::after {
  right: 0;
  border-width: 0 22px 22px 0;
  border-color: var(--paper) var(--paper) var(--paper-deep) transparent;
}
.product-issue-walk-link.is-previous::after {
  left: 0;
  border-width: 22px 22px 0 0;
  border-color: var(--paper) transparent var(--paper) var(--paper-deep);
}
/* An edge with no issue past it keeps its column at desktop widths so the
   remaining panel stays on its own side; on one column it takes no room. */
.product-issue-walk-gap { display: none; }

@media (prefers-reduced-motion: reduce) {
  .cart-courier,
  .courier-puff,
  .courier-burst,
  .stepper-spark { display: none; }
  .shop-cart .courier-pop { animation: none; }
  .product-corner-box { animation: none; }
  .product-poster-art:hover .product-corner-box img { animation: none; }
  .product-sold-through { animation: none; }
  .product-issue-walk-link { transition: none; }
}

.product-poster-copy {
  padding: 28px 20px 34px;
}
.product-poster-copy h1 {
  margin: 10px 0 18px;
  overflow-wrap: anywhere;
  font-family: var(--font-display);
  font-size: clamp(3.22rem, 13.8vw, 7.36rem);
  font-weight: 400;
  line-height: .85;
  letter-spacing: 0;
  text-transform: uppercase;
}
/* The short description is the paragraph under the name -- not "the last
   paragraph", which on a flat-priced merch or CBD product is the price, and
   which this rule was quietly restyling into sixteen pixels of grey. */
.product-poster-copy > h1 + p {
  max-width: 610px;
  margin: 0;
  color: #615b64;
  font-size: 1rem;
  line-height: 1.6;
}
.product-poster-copy .catalog-price { margin-top: 20px; font-size: 2.2rem; }
.product-price-options {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 22px 20px;
  border: 0;
  border-top: var(--line);
  background: #fff;
}
/* The legend names the choice and then answers it -- "SIZE  L" -- so the
   current pick is readable without working out which chip looks pressed. */
.product-price-options legend,
.product-color-options legend {
  /* Deliberately not flex: a flex legend stops cutting its notch out of the
     fieldset's rule, and the rule then strikes through the text. */
  padding: 0;
  font-size: .7rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.product-price-options legend > span,
.product-color-options legend > span {
  margin-left: 10px;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 400;
  letter-spacing: .02em;
  text-transform: none;
  color: var(--accent-deep);
}
.product-price-options > div {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: 8px;
}
.product-price-options label {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 4px 8px;
  /* 44px is the floor for a comfortable thumb; the chips are taller than
     that, but the rule is what stops a future tidy-up shrinking them. */
  min-height: 44px;
  padding: 10px;
  border: 2px solid var(--ink);
  cursor: pointer;
}
.product-price-options input { accent-color: var(--accent); }
.product-price-options label span { font-weight: 900; }

/* Shown, not hidden: a size the shop doesn't carry stays in the row so the
   run reads XS-4XL, struck out and unselectable. */
.product-price-options label.is-unavailable {
  border-color: #c9c3cd;
  color: #8b8590;
  cursor: not-allowed;
}
.product-price-options label.is-unavailable span { text-decoration: line-through; }
.product-price-options label.is-unavailable strong {
  font-family: inherit;
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .07em;
  text-transform: uppercase;
}
.product-price-options label strong {
  grid-column: 2;
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 400;
}
.product-price-options p {
  margin: 0;
  color: #615b64;
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .07em;
  text-transform: uppercase;
}

/* Merch colours. Same frame as the size row above so a shirt reads as one
   set of choices, but laid out for a swatch and a name rather than a price. */
.product-color-options {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 22px 20px;
  border: 0;
  border-top: var(--line);
  background: #fff;
}
.product-color-options > div {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.product-color-options label {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 8px 14px 8px 10px;
  border: 2px solid var(--ink);
  font-weight: 900;
  cursor: pointer;
}
.product-color-options input { accent-color: var(--accent); }
.product-color-swatch {
  flex: none;
  width: 24px;
  height: 24px;
  border: 2px solid var(--ink);
}

/* Selected has to survive a glance at arm's length, so it is a filled chip
   and not only the radio dot -- which on a swatch row is the smallest thing
   on screen and the easiest to miss. */
.product-price-options label:has(input:checked),
.product-color-options label:has(input:checked) {
  background: var(--ink);
  color: var(--paper);
  box-shadow: 3px 3px 0 var(--accent);
}
.product-price-options label:has(input:focus-visible),
.product-color-options label:has(input:focus-visible) {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
.product-color-options label:has(input:checked) .product-color-swatch {
  border-color: var(--paper);
}

/* What is about to be added, and what it costs, immediately above the button
   that adds it. */
.product-pick {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 0;
  padding: 14px 20px;
  border-top: var(--line);
  background: #fff;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .07em;
  font-size: .72rem;
}
.product-pick strong {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
  letter-spacing: 0;
}
/* Nothing picked and nothing priced -- an empty bar between the chips and the
   button reads as a rendering fault, so it goes away entirely. */
.product-pick:not(:has(span:not(:empty), strong:not(:empty))) { display: none; }

/* Sold out. Sits in the slot the add button would have taken, so the page has
   the same shape either way and the answer is where the action was. */
.cart-sold-out {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 18px 20px;
  border-top: var(--line);
  background: #efecf1;
  color: #4a444f;
}
.cart-sold-out small {
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.cart-sold-out strong {
  font-family: var(--font-display);
  font-size: 1.6rem;
  font-weight: 400;
}

.poster-action,
.poster-story,
.product-description > summary {
  width: 100%;
  min-height: 92px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 18px 20px;
  border: 0;
  border-top: var(--line);
  color: #fff;
  background: var(--ink);
  text-align: left;
  list-style: none;
}
.poster-action { background: var(--brand-deep); }
.poster-action.is-added { background: var(--green); }

/*
 * What the add button turns into. The same bar, the same two lines of type,
 * now in the confirmed green -- and the stepper alongside, so "how many" is
 * answered in the place "add" was asked. Under 768px the stepper takes its
 * own row: three 44px targets do not share a line with a sentence on a phone.
 */
.cart-added {
  display: grid;
  gap: 14px;
  padding: 18px 20px;
  border-top: var(--line);
  color: #fff;
  background: var(--green);
}
.cart-added-line {
  display: grid;
  gap: 8px;
  margin: 0;
}
.cart-added-line > span { display: grid; gap: 4px; }
.cart-added-line small {
  font-size: .58rem;
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.cart-added-line strong {
  font-family: var(--font-display);
  font-size: 1.6rem;
  font-weight: 400;
  text-transform: uppercase;
}
.cart-added-line a {
  width: fit-content;
  color: inherit;
  font-size: .72rem;
  font-weight: 900;
  letter-spacing: .07em;
  text-transform: uppercase;
  text-decoration: underline;
  text-underline-offset: .3em;
}
.cart-added .quantity-control { border-color: #fff; }
.cart-added .quantity-control button { color: var(--green); background: #fff; }
/* Read, not seen: the sentence a screen reader gets when the add lands. */
.cart-announce {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.poster-action span,
.poster-story span,
.product-description summary span {
  display: grid;
  gap: 4px;
}
.poster-action small,
.poster-story small,
.product-description summary small {
  color: #fff;
  font-size: .58rem;
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.poster-action strong,
.poster-story strong,
.product-description summary strong {
  font-family: var(--font-display);
  font-size: 1.8rem;
  font-weight: 400;
  text-transform: uppercase;
}
.poster-action b,
.poster-story b,
.product-description summary b {
  font-size: 2rem;
  font-weight: 400;
}
/*
 * The story button reads as the poster's own furniture -- ink, not the brand
 * purple -- so it never competes with the add button above it for the thumb.
 * While the card is being drawn the label says so and the button goes quiet.
 */
.poster-story { cursor: pointer; }
.poster-story:disabled { cursor: progress; opacity: .7; }
.poster-story-status {
  margin: 0;
  padding: 12px 20px;
  border-top: var(--line);
  background: var(--paper);
  font-size: .82rem;
  font-weight: 700;
  line-height: 1.45;
}
.poster-story-status.is-error { background: #f7dcdd; }
.product-description summary::-webkit-details-marker { display: none; }
.product-description[open] summary b { transform: rotate(45deg); }
.product-description > div {
  padding: 30px 20px 35px;
  border-top: 2px solid #fff;
  color: #fff;
  background: var(--ink);
}
.product-description > div strong {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 400;
  text-transform: uppercase;
}
.product-description > div p {
  max-width: 680px;
  margin: 13px 0 0;
  color: #c6c0c8;
  line-height: 1.7;
}
.product-next {
  padding: 36px 20px;
  border-top: var(--line);
  background: var(--brand-tint-deep);
}
.product-next span {
  font-family: var(--font-display);
  font-size: 2.1rem;
  text-transform: uppercase;
}
.product-next p {
  max-width: 620px;
  margin: 8px 0 0;
  line-height: 1.55;
}
.route-error {
  max-width: 900px;
  margin: 80px auto;
  padding: 40px 28px;
  color: #fff;
  background: var(--ink);
  border: 4px solid var(--accent);
}
.route-error a { display: inline-block; margin-top: 24px; border-bottom: 2px solid; }

.cart-main {
  min-height: calc(100svh - 166px);
  padding: 55px 16px 78px;
}
.cart-title { max-width: 1160px; margin: 0 auto 38px; }
.cart-title > p:last-child {
  max-width: 470px;
  margin: 18px 0 0;
  color: #645f67;
  line-height: 1.55;
}
.cart-lines,
.cart-checkout,
.cart-empty {
  max-width: 1160px;
  margin-inline: auto;
}
.cart-line {
  display: grid;
  grid-template-columns: 86px minmax(0, 1fr);
  gap: 16px;
  padding: 13px;
  margin-top: 14px;
  border: var(--line);
  background: #fff;
  box-shadow: 5px 5px 0 var(--ink);
}
.cart-line-art {
  aspect-ratio: .82;
  display: grid;
  place-content: center;
  justify-items: center;
  color: #fff;
  background: var(--brand-deep);
  border: 2px solid var(--ink);
}
.cart-line-art strong { font-family: var(--font-display); font-size: 1.6rem; }
.cart-line-art span {
  font-size: .46rem;
  font-weight: 900;
  letter-spacing: .12em;
  text-transform: uppercase;
}
/* Sold through while it sat in the cart. The line keeps its place, its art and
   its remove button — it is told, not taken away — and the art tile drains the
   same way a sold-through cover does in the grid, so "gone" looks like one
   thing wherever it is met. What it loses is the till: cart-page.mjs stops
   counting it toward the subtotal and toward the Checkout button, and this is
   what says so before the button turns out to be off. */
.cart-line.is-sold-through .cart-line-art {
  filter: saturate(0);
  opacity: .72;
}
.cart-line-gone {
  margin: 6px 0 0;
  font-size: .66rem;
  font-weight: 900;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #a0252b;
}
.cart-line-copy { min-width: 0; align-self: center; }
.cart-line-copy h2 {
  margin: 6px 0 0;
  font-family: var(--font-display);
  font-size: 1.85rem;
  font-weight: 400;
  line-height: 1;
  text-transform: uppercase;
}
.quantity-control {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 48px 1fr 48px;
  min-height: 48px;
  border: 2px solid var(--ink);
}
.quantity-control button {
  border: 0;
  color: #fff;
  background: var(--ink);
  font-size: 1.1rem;
}
.quantity-control span { display: grid; place-items: center; font-weight: 900; }
.remove-line {
  grid-column: 1 / -1;
  justify-self: start;
  padding: 16px 0;
  border: 0;
  background: transparent;
  text-decoration: underline;
  text-underline-offset: 3px;
  font-size: .62rem;
  font-weight: 900;
  letter-spacing: .09em;
  text-transform: uppercase;
}
.cart-empty {
  padding: 35px 24px;
  border: var(--line);
  background: var(--brand-tint-deep);
}
.cart-empty strong {
  font-family: var(--font-display);
  font-size: 2.4rem;
  font-weight: 400;
  text-transform: uppercase;
}
.cart-empty a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 10px;
  font-weight: 900;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.cart-checkout {
  margin-top: 38px;
  border: var(--line);
  background: var(--ink);
}
.cart-checkout > div {
  display: flex;
  justify-content: space-between;
  padding: 18px;
  color: #fff;
  border-bottom: 2px solid #fff;
  text-transform: uppercase;
  letter-spacing: .1em;
  font-size: .68rem;
  font-weight: 900;
}
.checkout-link {
  min-height: 72px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px;
  color: var(--ink);
  background: var(--brand-tint-deep);
  font-family: var(--font-display);
  font-size: 1.7rem;
  text-transform: uppercase;
}
.checkout-link.is-disabled { opacity: .42; cursor: not-allowed; }

.checkout-main {
  min-height: calc(100svh - 166px);
  padding: 58px 16px 80px;
  background-color: var(--paper);
  background-image: radial-gradient(rgba(17,16,21,.12) 1px, transparent 1px);
  background-size: 14px 14px;
}
.checkout-intro,
.fulfillment-form {
  max-width: 780px;
  margin-inline: auto;
}
.checkout-intro h1 { font-size: clamp(3.08rem, 12.88vw, 7.18rem); }
.checkout-intro > p:last-child {
  max-width: 580px;
  margin: 24px 0 40px;
  color: #5e5861;
  line-height: 1.6;
}
.fulfillment-form { padding: 18px; border: var(--line); background: #fff; box-shadow: 8px 8px 0 var(--ink); }
.fulfillment-options { display: grid; gap: 11px; padding: 0; border: 0; }
.fulfillment-options legend {
  margin-bottom: 13px;
  font-size: .65rem;
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.fulfillment-card {
  position: relative;
  display: block;
  cursor: pointer;
}
.fulfillment-card input { position: absolute; opacity: 0; pointer-events: none; }
.fulfillment-card > span {
  display: grid;
  gap: 4px;
  padding: 18px;
  border: var(--line);
  background: var(--paper);
}
.fulfillment-card input:checked + span {
  color: #fff;
  background: var(--brand-deep);
  box-shadow: 5px 5px 0 var(--ink);
}
.fulfillment-card small {
  font-size: .56rem;
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.fulfillment-card strong {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 400;
  text-transform: uppercase;
}
.fulfillment-card em { font-size: .78rem; font-style: normal; }
.fulfillment-panel {
  margin-top: 24px;
  padding-top: 24px;
  border-top: 3px solid var(--ink);
}
.fulfillment-panel > label,
.contact-grid label {
  display: grid;
  align-content: start;
  gap: 8px;
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.fulfillment-panel select,
.contact-grid input {
  width: 100%;
  min-height: 52px;
  padding: 0 12px;
  border: 2px solid var(--ink);
  border-radius: 0;
  background: #fff;
  /*
   * The label wrapping each field is the caption -- small, uppercase, 900 --
   * and `font: inherit` on line 68 handed all of that straight to the input,
   * so a typed address came out at 10.88px in black bold: smaller than the
   * word "FULL NAME" sitting above it.
   *
   * 1rem is not a taste call. Mobile Safari zooms the viewport on focus for
   * any field under 16px and does not zoom back out, so each of the seven
   * fields here charged the visitor a pinch to recover. Keep this at 1rem or
   * larger whatever the caption does.
   */
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
}
.fulfillment-panel > p {
  margin: 12px 0 0;
  color: #706a73;
  font-size: .76rem;
  line-height: 1.5;
}
.contact-grid { display: grid; gap: 14px; }
.contact-grid label span { font-weight: 500; }
.field-legend {
  margin: 0 0 16px;
  color: #706a73;
  font-size: .76rem;
}
.contact-grid .field-name { font-weight: 900; }
.field-flag { color: #706a73; text-transform: none; letter-spacing: 0; }
.contact-grid input[aria-invalid="true"] { border-color: #a0252b; }
.field-error:empty { display: none; }
.field-error {
  color: #a0252b;
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}
.field-hint {
  color: #706a73;
  font-size: .74rem;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.45;
  text-transform: none;
}
.fulfillment-panel select[aria-invalid="true"] { border-color: #a0252b; }
.field-optional > summary {
  cursor: pointer;
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.field-optional[open] > summary { margin-bottom: 14px; }
.checkout-summary {
  margin-top: 26px;
  border-top: 3px solid var(--ink);
  padding-top: 22px;
}
.checkout-summary > span {
  font-size: .66rem;
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.checkout-summary p {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  margin: 12px 0 0;
  padding-bottom: 10px;
  border-bottom: 1px solid #d4cfd7;
}
.checkout-summary a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-weight: 800;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.checkout-error { color: #a0252b; font-weight: 800; }
/* What sold through between the cart and here, named rather than counted. It
   sits above the order it is missing from. Selector carries .checkout-summary
   because the rule for `p` inside that block is a two-column price row, and
   this is a sentence. */
.checkout-summary .checkout-sold-through {
  display: block;
  border-bottom: 0;
  font-weight: 800;
  color: #a0252b;
}
.checkout-submit {
  width: 100%;
  min-height: 68px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 18px;
  border: var(--line);
  color: #fff;
  background: var(--ink);
  font-family: var(--font-display);
  font-size: 1.45rem;
  text-transform: uppercase;
}
.checkout-status {
  margin: 16px 0 0;
  padding: 17px;
  border: 2px solid var(--ink);
  background: var(--brand-tint-deep);
  font-weight: 750;
  line-height: 1.5;
}

[hidden] { display: none !important; }

@media (min-width: 760px) {
  .shop-header { min-height: 74px; padding-inline: 28px; }
  .shop-home-mascot { width: 58px; height: 70px; }
  .shop-home-mascot::before { width: 52px; height: 52px; }
  .shop-home-mascot img { width: 48px; height: 66px; }
  .shop-back, .shop-cart { font-size: .74rem; }
  .category-cover {
    min-height: clamp(470px, 66vh, 660px);
    grid-template-columns: 1fr 1fr;
    align-items: center;
    padding: clamp(52px, 8vh, 90px) clamp(40px, 6vw, 90px);
  }
  .category-cover::after { width: 670px; height: 670px; right: -180px; top: -30px; }
  /*
   * Its own figure, below the 8% the rest of the display type took, because
   * this is the tightest fit on the site: the longest single-word category --
   * FLOWER -- has to set on one line in a 556px copy column at 1280, and at the
   * shared scale it came to 601px and broke to "FLOWE / R". The longer names
   * (CONCENTRATES, PRE-ROLLS) wrap here as they always did.
   */
  .cover-copy h1 { font-size: clamp(6.45rem, 12.9vw, 10.88rem); }
  .cover-copy > p:last-child { max-width: 410px; }
  /*
   * Back out of the mobile stack: absolute, beside the copy, no grid ordering.
   * His height is capped against the viewport as well as in pixels so that a
   * cover shortened by the clamp above never crops his feet -- overflow is
   * hidden here, so a mascot too tall for the band loses his shoes rather than
   * overflowing, and a floating or footless mascot has fooled a check before.
   */
  .cover-mascot {
    position: absolute;
    order: 0;
    justify-self: auto;
    margin-bottom: 0;
    width: min(42vw, 520px);
    right: 3vw;
    top: clamp(24px, 5vh, 78px);
  }
  .cover-mascot img { max-height: min(56vh, 590px); }
  .catalog-stack-section { padding: 52px clamp(28px, 5vw, 72px) 110px; }
  .stack-toolbar {
    display: flex;
    align-items: end;
    justify-content: space-between;
    gap: 30px;
  }
  .view-switch { width: 255px; margin: 0; }
  .catalog-filters {
    grid-template-columns: 1fr auto;
    align-items: end;
    padding: 24px 26px;
  }
  .catalog-filter-groups { grid-auto-flow: row; }
  .catalog-story {
    min-height: 530px;
    grid-template-columns: .82fr 1.18fr;
    grid-template-rows: 1fr;
  }
  .catalog-story:nth-child(even) { grid-template-columns: 1.18fr .82fr; }
  .catalog-story:nth-child(even) .catalog-story-copy { order: 2; }
  .catalog-story-copy { align-self: center; padding: 45px; }
  .catalog-product-stage { min-height: 530px; border-top: 0; border-left: var(--line); }
  .catalog-story:nth-child(even) .catalog-product-stage { border-left: 0; border-right: var(--line); }
  .has-interactive-model {
    grid-template-columns: .82fr 1.18fr;
    grid-template-rows: minmax(530px, auto);
    grid-template-areas: "copy stage";
  }
  .has-interactive-model:nth-child(even) {
    grid-template-columns: 1.18fr .82fr;
    grid-template-areas: "stage copy";
  }
  .has-interactive-model .catalog-story-copy,
  .has-interactive-model .catalog-product-stage,
  .has-interactive-model .catalog-story-info {
    order: initial;
  }
  .has-interactive-model .catalog-story-copy {
    min-height: 530px;
    padding: 45px;
  }
  .has-interactive-model .catalog-product-stage {
    min-height: 530px;
  }
  .has-interactive-model .catalog-story-info {
    min-height: 530px;
    padding: clamp(30px, 4vw, 46px);
  }
  .has-interactive-model.is-open .catalog-product-stage {
    min-height: 530px;
    border-right: 0;
    border-left: 0;
  }
  .has-interactive-model.is-open:nth-child(odd) .catalog-product-stage {
    border-right: var(--line);
  }
  .has-interactive-model.is-open:nth-child(even) .catalog-product-stage {
    border-left: var(--line);
  }
  .catalog-model-hint {
    right: 16px;
    transform: none;
  }
  .catalog-story-info h3 { font-size: clamp(3.04rem, 5.52vw, 5.34rem); }
  .catalog-story-add strong { font-size: 2.25rem; }
  .catalog-stack.is-condensed { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
  .product-related { margin-top: 64px; padding-top: 44px; }
  /*
   * Four across, not the category grid's three. The shelf holds exactly four
   * cards, so on a three-column grid the fourth wrapped alone and left two
   * empty slots beside it -- a row of one reads as a rendering fault rather
   * than as the end of a list. Two columns below this width takes the same four
   * as two full rows.
   */
  /* Scoped through .product-related for the weight, not for the reading:
     .catalog-stack.is-condensed above is two class selectors, so a bare
     .product-related-stack loses to it and silently does nothing. */
  .product-related .catalog-stack.is-condensed {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 16px;
  }
  .catalog-compact-card { padding: 15px 15px 22px; }
  .catalog-compact-stage { min-height: 360px; }
  .catalog-compact-stage .catalog-pack { width: min(13vw, 170px); }
  .catalog-compact-stage .catalog-pack span { font-size: 3.2rem; }
  .catalog-compact-link strong { font-size: 2rem; }
  .drop-banner { padding: 100px clamp(40px, 8vw, 140px); }
  .shop-footer { flex-direction: row; align-items: center; justify-content: space-between; padding-inline: 40px; }
  .product-main { padding: 55px clamp(28px, 5vw, 80px) 110px; }
  .product-poster {
    display: grid;
    grid-template-columns: 1.1fr .9fr;
    /* One row per thing the right column can hold -- copy, controls, story
       button, story status, description -- plus one spare. The art spans them
       all; a fixed span used to stop at three, which put the description under
       the art and left a blank cell beside it. Empty rows are zero tall; the
       first one is the 1fr and takes the slack. */
    grid-template-rows: 1fr repeat(5, auto);
  }
  .product-poster-art { min-height: 720px; border-right: var(--line); border-bottom: 0; grid-row: 1 / -1; }
  .product-poster-art img { width: 50%; max-height: 430px; }
  /* min-width: 0, or the name -- measured unbroken by poster-name-fit --
     widens its column to fit itself and the art gives way instead. */
  .product-poster-copy { align-self: end; min-width: 0; padding: 52px 42px; }
  .product-poster-copy h1 { font-size: clamp(3.77rem, 7.36vw, 7.36rem); }
  .poster-action, .poster-story, .poster-story-status, .cart-added, .product-description > summary { padding-inline: 42px; }
  .cart-added { grid-template-columns: 1fr 150px; align-items: center; }
  .product-description > div { padding-inline: 42px; }
  .product-next { padding: 46px clamp(40px, 8vw, 140px); }
  .product-issue-walk { grid-template-columns: 1fr 1fr; }
  .product-issue-walk-gap { display: block; }
  .cart-main { padding: 80px clamp(28px, 6vw, 90px) 110px; }
  .cart-line {
    grid-template-columns: 118px minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 24px;
    padding: 18px;
  }
  .quantity-control { grid-column: auto; width: 150px; }
  .remove-line { grid-column: auto; }
  .cart-line-copy h2 { font-size: 2.5rem; }
  .checkout-main { padding: 90px clamp(28px, 7vw, 100px) 120px; }
  .checkout-main { display: grid; grid-template-columns: minmax(0, .85fr) minmax(440px, 1.15fr); gap: clamp(50px, 8vw, 130px); align-items: start; }
  .checkout-intro, .fulfillment-form { width: 100%; }
  .checkout-intro h1 { font-size: clamp(4.42rem, 6.62vw, 7.18rem); }
  .fulfillment-options { grid-template-columns: 1fr 1fr; }
  .contact-grid { grid-template-columns: 1fr 1fr; }
  .contact-grid-wide { grid-column: 1 / -1; }
}

@media (prefers-reduced-motion: reduce) {
  /*
   * The universal kill, the same one styles-shell.css carries for the landing
   * page. Without it these pages only stopped scroll animation: every
   * keyframe and transition still ran for somebody who asked for less
   * motion, and each per-feature reduced-motion block was covering only its
   * own corner. Delay is deliberately not crushed -- see the note in
   * styles-shell.css about stickers frozen at their 0% keyframe.
   */
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* Inline add/stepper controls on product surfaces (outside the cart page).
   The add buttons carry display:flex classes, so the hidden attribute needs
   the explicit override to win. */
.cart-controls { display: grid; gap: 10px; }
.cart-controls [hidden] { display: none !important; }
.cart-inline-stepper { border-color: currentColor; }
.cart-inline-stepper button { min-width: 44px; }
.catalog-story-info .cart-controls { align-self: end; }

/* Cart page group headings: wishlist (THC pickup/drop) vs merch (ships). */
.cart-group-title {
  margin: 26px 0 4px;
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .03em;
}
.cart-group-title small {
  display: block;
  font-family: var(--font-body);
  font-size: .62rem;
  font-weight: 800;
  letter-spacing: .09em;
  opacity: .66;
}
.cart-group-title:first-child { margin-top: 0; }

.merch-shipping-title {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 400;
  text-transform: uppercase;
}

/* Split wishlist/cart badges in the shop header (grid gives the last column
   one cell, so the pair shares a wrapper). The word hides on narrow screens
   and the count bubbles stay. */
.shop-cart-group {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 14px;
}
.shop-cart-group .shop-cart { justify-self: auto; }
@media (max-width: 560px) {
  .shop-cart-group { gap: 8px; }
  .shop-cart-group .shop-cart-word { display: none; }
  /* Same swap as the landing header: the icon appears exactly where the word
     leaves, and the control is held to a fingertip rather than shrinking to
     whatever the count happens to need. */
  .shop-cart-group .cart-icon { display: block; }
  .shop-cart-group .shop-cart {
    min-width: 44px;
    min-height: 44px;
    justify-content: center;
  }
}

/* Merch checkout totals and the wishlist/merch cross-links. */
.checkout-total {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 2px solid var(--ink);
  font-weight: 900;
  text-transform: uppercase;
  font-size: .72rem;
  letter-spacing: .08em;
}
.checkout-crosslink {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
  font-size: .78rem;
}
.checkout-crosslink a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-weight: 800;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Shipping-law disclaimer on the merch/CBD checkout. Styled here because the
   shop pages don't load the landing page's stylesheet where .warning-box
   lives; wording is placeholder until Boom's compliance review. */
.checkout-ship-disclaimer {
  border: 2px solid var(--ink);
  padding: 16px 18px;
  font-size: .8rem;
  line-height: 1.5;
}
.checkout-ship-disclaimer strong {
  display: block;
  margin-bottom: 6px;
  font-size: .72rem;
  font-weight: 900;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.checkout-ship-disclaimer p { margin: 0; }

/* White on the green, not ink. Deepening --green so white could sit on the
   "added" button and the CBD panel took ink the other way: #111015 measures
   3.75:1 on it, where white measures 5.06. Green joins ink as an accent
   dark enough to carry white, which is what --accent-ink is for. */
body[data-theme="green"] { --accent: var(--green); --accent-ink: #fff; --accent-deep: var(--green); }

/* Admin-uploaded product photos: fill the same stage the BOOM pack graphic
   occupies, on cards, compact tiles, and the product poster alike. */
.catalog-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.catalog-compact-stage { position: relative; }
.catalog-compact-stage .catalog-photo { position: absolute; inset: 0; }
.product-poster-art .catalog-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Category switcher — hop between categories without going back to the
   landing lineup. Scrolls horizontally on narrow screens. */
.category-switch {
  display: flex;
  gap: 9px;
  margin-bottom: clamp(18px, 3vw, 30px);
  padding-bottom: 6px;
  overflow-x: auto;
  scrollbar-width: thin;
}
.category-switch a {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 15px;
  border: 2px solid var(--ink);
  background: #fff;
  font-size: .66rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.category-switch a:hover { background: var(--paper-deep); }
.category-switch a.is-active {
  color: var(--accent-ink, var(--ink));
  background: var(--accent);
  box-shadow: 3px 3px 0 var(--ink);
}

/* Compact-card cart controls — add without opening the product page. */
.catalog-compact-controls { margin-top: 12px; }
.catalog-compact-add {
  width: 100%;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 9px 12px;
  border: 2px solid var(--ink);
  color: var(--accent-ink, var(--ink));
  background: var(--accent);
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.catalog-compact-add b { font-size: 1rem; line-height: 1; }
/* The choose link stands in the add button's slot and has to be
   indistinguishable from it, or a grid row goes ragged wherever one card
   offers sizes and the card beside it does not. Anchors arrive underlined;
   buttons do not. */
a.catalog-compact-add,
a.catalog-story-add,
.cart-choose-link { text-decoration: none; }
.catalog-compact-controls .quantity-control { width: 100%; }

/* Occupies the add button's slot so a sold-out card is the same height as the
   ones beside it and the grid doesn't go ragged. */
.catalog-compact-sold-out {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 9px 12px;
  border: 2px dashed currentColor;
  color: #6f6875;
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* Shared wishlist: the code, the QR, and the ways to send it on. */
.wishlist-share {
  margin-top: 34px;
  border-top: 3px solid var(--ink);
  padding-top: 24px;
}
.wishlist-share h2 {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 1.5rem;
  margin: 0 0 10px;
}
.wishlist-share > p { margin: 0 0 18px; line-height: 1.55; }
.wishlist-share-code {
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin: 22px 0 0;
}
.wishlist-share-code span {
  font-size: .66rem;
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.wishlist-share-code strong {
  font-family: var(--font-display);
  /* Read aloud at a counter, so it gets tracking and room to breathe. */
  font-size: clamp(1.84rem, 8.28vw, 2.58rem);
  letter-spacing: .16em;
  line-height: 1;
}
.wishlist-share-qr {
  margin: 18px 0;
  display: flex;
  justify-content: center;
  /* The morph canvas is positioned against this box. */
  position: relative;
}
.wishlist-share-qr svg {
  border: var(--line);
  box-shadow: 6px 6px 0 var(--ink);
  /* The quiet zone is drawn into the SVG; nothing may crop it. */
  max-width: 100%;
  height: auto;
}
/*
 * The svg is the artifact and the canvas is only ever laid over it, so the
 * default state of this box is the one that scans. `is-morphing` hides the svg
 * while the animation plays; if the script dies mid-morph the class comes off
 * with it and the svg is back. The canvas is never the only copy.
 */
.wishlist-share-qr.is-morphing svg { opacity: 0; }
.wishlist-share-qr.is-handoff svg { transition: opacity 200ms linear; opacity: 1; }
.wishlist-share-qr-canvas {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  /*
   * The same box as the svg, down to the border, because the two are laid over
   * each other and swapped: --line is a 3px rule on catalog pages, so a canvas
   * without it draws the symbol 2% larger and the swap shows as a jump. The
   * border and shadow also mean the frame is there for the whole animation
   * rather than appearing with the paper at the end.
   */
  box-sizing: border-box;
  border: var(--line);
  box-shadow: 6px 6px 0 var(--ink);
  width: 300px;
  max-width: 100%;
  aspect-ratio: 1;
  pointer-events: none;
  transition: opacity 200ms linear;
}
@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: wishlist-page.mjs does not even import the module here. */
  .wishlist-share-qr.is-morphing svg { opacity: 1; }
  .wishlist-share-qr-canvas { display: none; }
}
.wishlist-share-link { margin: 0 0 16px; word-break: break-all; }
.wishlist-share-link a { border-bottom: 1px solid; font-weight: 700; }
/* Same footprint as the save button beside it, on paper rather than ink: a
   second, quieter way to take the list along. */
.wishlist-story {
  width: 100%;
  min-height: 68px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 12px;
  padding: 0 18px;
  border: var(--line);
  color: var(--ink);
  background: var(--paper);
  font-family: var(--font-display);
  font-size: 1.45rem;
  text-transform: uppercase;
  cursor: pointer;
}
.wishlist-story:disabled { cursor: progress; opacity: .7; }
.wishlist-share-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.wishlist-share-actions button {
  flex: 1 1 auto;
  min-height: 52px;
  padding: 0 20px;
  border: var(--line);
  background: var(--paper);
  font-weight: 850;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.checkout-status.is-error { background: #f7dcdd; }

/* Skip link -- WCAG 2.0 2.4.1 Bypass Blocks.

   Placed inside the page header rather than before it. On the landing page that
   is what puts it under the age gate's `inert` sweep for free: the gate inerts
   .site-header, #top and the footer, so a skip link outside all three would stay
   tabbable while the gate was up and would land a keyboard user in inert
   content. Inside the header it is simply unreachable until the gate lifts, and
   is then the first thing Tab reaches.

   `position: absolute` while hidden, so it is out of the header's flex flow and
   cannot shift the layout; `fixed` once focused, so it appears at the top of the
   viewport regardless of where the header has scrolled to. */
.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link:focus {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 10000;
  width: auto;
  height: auto;
  margin: 0;
  padding: .75rem 1.25rem;
  overflow: visible;
  clip-path: none;
  border-radius: .5rem;
  background: #171219;
  color: #fff;              /* 16.6:1 on #171219 */
  font: 700 1rem/1 system-ui, sans-serif;
  text-decoration: none;
  outline: 3px solid #fff;
  outline-offset: 3px;
}

/* The skip destination is a landmark, not a control -- it takes focus so the
   next Tab continues from the content, but a ring drawn around the whole of
   <main> reads as a rendering fault rather than an indicator. */
main:focus { outline: none; }
