/* Box Builder — the customer-facing wizard.
   Companion to blocks/box-builder.liquid and assets/box-builder.js.

   ⚠️ SPECIFICITY. An earlier version of this file declared every button style
   inside :where(), so that a theme's own button class could override it. That
   was wrong in practice: :where() is specificity ZERO, so it also lost to the
   theme's bare `button { }` reset (0,0,1) — which strips padding and borders.
   The buttons rendered as unpadded outlines. Our own styling therefore uses
   ordinary class specificity, and a merchant who genuinely wants the theme's
   button class opts in with the "Theme button CSS class" setting, which adds
   .boldally-box--theme-buttons and makes the rules here stand down.

   Colour comes from the theme via `currentColor` and tints of it wherever a
   contrast pair is not needed. Where one is — a filled button, whose label
   must be legible against its own background — there is no way for a block to
   read the theme's background colour, so those two are literals with settings
   to override them. */

.boldally-box {
  --box-columns: 4;
  /* Theme sections span the full window on many themes; a wizard read at 1900px
     is not a wizard. Overridable so a merchant with a narrow section can undo it. */
  --box-max-width: 1200px;
  --box-padding-block: 48px;
  --box-step-size: 2.5rem;

  /* The filled primary button. Literals, because the label sitting on this
     background needs a colour that contrasts with it and CSS cannot read the
     theme's background to derive one. Both are block settings — see
     blocks/box-builder.liquid. */
  --box-accent: #1c1b2e;
  --box-accent-text: #ffffff;

  /* The random pick's own pair, and literals for the same reason: a filled
     control needs a label colour chosen against its own background. Kept
     distinct from --box-accent on purpose — the tray's dark "Add box to
     cart" is the forward action, and this one is an aside offering to skip
     the grid. Custom properties, so a merchant's palette can replace them.

     Marigold, and warm on purpose. This button sits directly above a grid of
     product photographs, and the first fill tried here was a soft mint that
     vanished against the mint product beneath it. What makes a control catch
     the eye in a grid of pastel goods is separating by HUE, not by lightness:
     a warm orange is the one thing on an otherwise cool page that no product
     can duplicate. It also clears 8:1 against its own label, which the vivid
     mid-tones (fuchsia, teal, lilac) cannot manage with white text — they sit
     around 4:1, under the AA floor, without being darkened past the point
     where they were bright in the first place. */
  --box-random-bg: #f7a416;
  --box-random-text: #2e1c00;

  /* Tints of the theme's text colour: correct on a dark theme as well as a
     light one, which a literal grey is not. The rgba() line is the fallback for
     a browser without color-mix(); the second declaration replaces it wherever
     it parses, and is dropped entirely where it does not. */
  --box-muted: rgba(128, 128, 128, 0.5);
  --box-muted: color-mix(in srgb, currentColor 32%, transparent);
  --box-hairline: rgba(128, 128, 128, 0.28);
  --box-hairline: color-mix(in srgb, currentColor 18%, transparent);
  --box-surface: rgba(128, 128, 128, 0.12);
  --box-surface: color-mix(in srgb, currentColor 7%, transparent);

  display: block;
  max-width: var(--box-max-width);
  margin-inline: auto;
  padding-block: var(--box-padding-block);
  /* One tray's worth of clearance at the end, measured by reserveTraySpace in
     box-builder.js. The tray is pinned to the bottom of the viewport, so on a
     short viewport it covers what is behind it; content above can scroll clear
     of it, but the last row could not — the only thing after it was the tray's
     own height, which is what is doing the covering. Falls back to 0 when the
     script has not measured yet, which is the pre-tray layout. */
  padding-bottom: calc(var(--box-padding-block) + var(--box-tray-height, 0px));
}

/* ----------------------------------------------------------------- Masthead */

/* The box's own title, above the stepper. Sized in em against the theme's own
   type scale rather than a fixed px, so it lands in proportion on a shop with
   large body text and one with small. */
.boldally-box__masthead {
  margin: 0 0 2rem;
  text-align: center;
}

.boldally-box__masthead-title {
  margin: 0;
  font-size: 1.75em;
  line-height: 1.2;
}

.boldally-box__masthead-sub {
  margin: 0.5rem auto 0;
  /* Capped so the line breaks where a reader expects rather than running the
     full 1200px the wizard is allowed. */
  max-width: 34em;
  font-size: 1.05em;
  line-height: 1.5;
  /* NOT --box-muted. That is 32% of the theme's text colour, which is right
     for a label the eye should skip and lands around 2.4:1 against a light
     background — under the 4.5:1 AA floor. This line carries the offer and
     the price, so it is quietened rather than faded: 70% reads about 7:1. */
  color: rgba(90, 89, 104, 0.95);
  color: color-mix(in srgb, currentColor 70%, transparent);
}

@media (max-width: 749px) {
  .boldally-box__masthead { margin-bottom: 1.5rem; }
  .boldally-box__masthead-title { font-size: 1.45em; }
  .boldally-box__masthead-sub { font-size: 1em; }
}

/* ------------------------------------------------------------------ Stepper */

.boldally-box__rail {
  display: flex;
  align-items: flex-start;
  margin: 0 0 2rem;
}

.boldally-box__rail-node {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.625rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: center;
  cursor: pointer;
}
.boldally-box__rail-node[disabled] { cursor: default; }

/* Two half-connectors per step rather than one line behind the whole row: the
   first and last steps get no dangling ends, and the gap around each circle
   needs no opaque background to punch through the line — which matters,
   because the section's background belongs to the theme, not to this block. */
.boldally-box__rail-node::before,
.boldally-box__rail-node::after {
  content: "";
  position: absolute;
  top: calc(var(--box-step-size) / 2);
  height: 1px;
  background: var(--box-hairline);
}
.boldally-box__rail-node::before {
  left: 0;
  right: calc(50% + var(--box-step-size) / 2 + 0.75rem);
}
.boldally-box__rail-node::after {
  left: calc(50% + var(--box-step-size) / 2 + 0.75rem);
  right: 0;
}
.boldally-box__rail-node:first-child::before,
.boldally-box__rail-node:last-child::after { content: none; }

.boldally-box__rail-number {
  position: relative; /* above the connector */
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--box-step-size);
  height: var(--box-step-size);
  border: 1px solid var(--box-hairline);
  border-radius: 50%;
  color: var(--box-muted);
  font-size: 0.875rem;
  line-height: 1;
}

/* Carries .boldally-box__rail-number too, so it inherits the circle's size,
   round border and position. Only the picture-specific bits live here. */
.boldally-box__rail-image {
  padding: 0;
  object-fit: cover;
  overflow: hidden;
  background: var(--box-surface);
}

/* The label stays full strength on every step: it is the step's NAME, and a
   customer scanning what the box contains needs to read all of them. State
   lives in the circle. */
.boldally-box__rail-label {
  font-size: 0.75rem;
  line-height: 1.3;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

/* Reached steps — done or current — come up to full strength rather than
   filling in. A filled disc needs a colour for the digit sitting on it, and
   there is no colour this block can know is legible against the theme's text
   colour.

   (The rule here used to be `background: currentColor; color: #fff` on one
   element, which is a white disc with a white digit on it: currentColor in
   `background` resolves against that same rule's `color`. It shipped, and the
   completed step rendered blank.) */
.boldally-box__rail-node.is-done .boldally-box__rail-number,
.boldally-box__rail-node.is-current .boldally-box__rail-number {
  border-color: currentColor;
  color: inherit;
}
.boldally-box__rail-node.is-current .boldally-box__rail-number {
  border-width: 2px;
  font-weight: 700;
}
.boldally-box__rail-node:not(.is-done):not(.is-current) .boldally-box__rail-label {
  color: var(--box-muted);
}

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

.boldally-box__grid {
  display: grid;
  gap: 1.5rem 1rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 750px) {
  .boldally-box__grid {
    grid-template-columns: repeat(var(--box-columns), minmax(0, 1fr));
  }
}

.boldally-box__card {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  padding: 0.5rem;
  border: 1px solid transparent;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-align: center;
}
.boldally-box__card.is-active { border-color: currentColor; }
.boldally-box__card.is-hidden { display: none; }
.boldally-box__card-image { width: 100%; aspect-ratio: 1; object-fit: cover; }
.boldally-box__card-title { line-height: 1.35; }
.boldally-box__card-price { color: var(--box-muted); font-size: 0.875em; }

/* --------------------------------------------------------------- Product view */

.boldally-box__product {
  display: grid;
  gap: 1.75rem;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
@media (min-width: 750px) {
  /* Two real columns rather than one column plus a max-width. Capping the
     media column's WIDTH while it still occupied a fraction of the grid left a
     band of dead space between the photo and the fields — several hundred
     pixels of it on a wide theme. */
  .boldally-box__product {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2.5rem;
  }
  /* Whatever the image's height, the form stays in view while it scrolls. */
  .boldally-box__product-details { position: sticky; top: 1.5rem; }
}

.boldally-box__product-title { margin: 0 0 0.75rem; }
.boldally-box__product-price { margin: 0 0 1rem; color: var(--box-muted); }

.boldally-box__variants { margin-bottom: 1.25rem; }
.boldally-box__variants-label {
  display: block;
  margin-bottom: 0.375rem;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--box-muted);
}
/* The chosen variant's own words, beside the option's name: "COLOR Lilac".
   Not uppercased with the label — it is a value the merchant typed, and
   "BERRY PINK" is a different word from the one on the cart line. */
.boldally-box__variants-value {
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.875rem;
  color: inherit;
}

.boldally-box__swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Round, and sized so five fit a phone's width on one line. The ring is drawn
   with box-shadow rather than a border so it appears OUTSIDE the circle:
   thickening a border would resize the picture inside it and make the row
   twitch as the customer moves along it. */
.boldally-box__swatch {
  position: relative;
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--box-hairline);
  transition: box-shadow 120ms ease;
}
.boldally-box__swatch:hover {
  box-shadow: inset 0 0 0 1px var(--box-muted);
}
.boldally-box__swatch.is-active {
  /* Keep the selection outside the photo: an inset shadow is covered by
     the full-size image. The gap separates the ring from dark thumbnails. */
  box-shadow: 0 0 0 2px var(--box-accent-text), 0 0 0 4px var(--box-accent);
}
/* One tab stop for the row, so the focus ring has to be visible on whichever
   swatch holds it — including the one that is already active. */
.boldally-box__swatch:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 6px;
}
.boldally-box__swatch-image {
  display: block;
  width: 100%;
  height: 100%;
  /* The only place in this file that crops. A swatch is an identifying
     thumbnail, not the tuned preview the embroidery is positioned against —
     see the note on .boldally-box__preview for why THAT one must not crop. */
  object-fit: cover;
}
/* A variant with no picture of its own — a size, usually. */
.boldally-box__swatch--text {
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto;
  min-width: 3rem;
  padding-inline: 0.75rem;
  border-radius: 999px;
}
.boldally-box__swatch-text {
  font-size: 0.8125rem;
  line-height: 1;
  white-space: nowrap;
}
/* Sold out: still shown, still reachable, visibly not on offer. The bar is a
   gradient rather than a child element so it needs no extra node and lands on
   the image and text swatches alike. */
.boldally-box__swatch.is-unavailable {
  cursor: not-allowed;
  opacity: 0.45;
}
.boldally-box__swatch.is-unavailable::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top left,
    transparent calc(50% - 1px),
    currentColor calc(50% - 1px),
    currentColor calc(50% + 1px),
    transparent calc(50% + 1px)
  );
}

/* The live embroidery preview (Core.mountOverlay). position: relative is what
   .boldally-embroidery-overlay (assets/personalize.css) needs to fill its
   wrapper — mountOverlay also sets this inline if it finds `static`, so this
   rule is not load-bearing for that, only for a clean layout before JS mounts.

   NO aspect-ratio and NO object-fit here, deliberately. The overlay's x/y are
   percentages of this wrapper, and merchants tune them against the product
   page, where the photo is shown at its NATURAL ratio. Cropping to a square
   re-maps those percentages onto a different picture — a 2:3 portrait loses
   ~17% top and bottom, so a tuned y=70% renders at ~63%, enough to move an
   embroidery off a teddy's foot.

   The cost is that the wrapper has no height until the image loads, and
   .boldally-embroidery-overlay is `inset: 0` with `overflow: hidden` — so the
   embroidery would be clipped to nothing while the fields beside it are
   already interactive. min-height plus a background reserves something to look
   at; box-builder.js re-runs renderText on the image's `load` event to fix the
   font size, which is computed from the element's width and cannot
   self-correct the way the percentage positions do. */
.boldally-box__preview {
  position: relative;
  min-height: 12rem;
  background: var(--box-surface);
}
.boldally-box__preview-image {
  display: block;
  width: 100%;
  height: auto;
}

/* A scroll row rather than a wrapping grid: a product with nine photos would
   otherwise push everything below it down by three rows of thumbnails. */
.boldally-box__thumbs {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.625rem;
  overflow-x: auto;
  padding-bottom: 0.25rem;
  scrollbar-width: thin;
}
.boldally-box__thumb {
  flex: 0 0 auto;
  width: 3.75rem;
  padding: 0;
  border: 1px solid transparent;
  background: none;
  cursor: pointer;
  line-height: 0;
}
.boldally-box__thumb.is-active { border-color: currentColor; }
.boldally-box__thumb-image { display: block; width: 100%; height: auto; }

/* The shared personalisation fields (snippets/personalize-fields.liquid, styled
   by assets/personalize.css) fill the details column here, the way they fill
   the form column on a product page. */
.boldally-box__product-details .boldally-personalize__group {
  max-width: none;
  margin-bottom: 1.125rem;
}
.boldally-box__product-details .boldally-personalize__swatches { margin-top: 0.5rem; }

/* -------------------------------------------------------------------- Review */

.boldally-box__review-list { list-style: none; margin: 0; padding: 0; }
.boldally-box__review-item {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1rem 0;
  border-bottom: 1px solid var(--box-hairline);
}
.boldally-box__review-image { width: 5rem; height: auto; flex: none; }
.boldally-box__review-body {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  flex: 1 1 auto;
  min-width: 0;
}
.boldally-box__review-step {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--box-muted);
}
.boldally-box__review-title { font-weight: 700; }
.boldally-box__review-detail { font-size: 0.875em; color: var(--box-muted); }

/* ------------------------------------------- Boxes already in the cart */

.boldally-box__cart-boxes {
  margin-top: 2.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--box-hairline);
  border-radius: 2px;
}
.boldally-box__cart-heading {
  margin: 0 0 0.75rem;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--box-muted);
}
.boldally-box__cart-list { list-style: none; margin: 0; padding: 0; }
.boldally-box__cart-item {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.75rem 0;
  border-top: 1px solid var(--box-hairline);
}
/* One thumbnail per pick, so a box is recognisable at a glance rather than
   needing its list of titles read. */
.boldally-box__cart-thumbs { display: flex; gap: 0.375rem; flex: none; }
.boldally-box__cart-thumb {
  display: block;
  width: 3rem;
  height: 3rem;
  object-fit: cover;
}
.boldally-box__cart-item:first-child { border-top: 0; }
.boldally-box__cart-item.is-editing { font-weight: 700; }
.boldally-box__cart-body {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  flex: 1 1 auto;
  min-width: 0;
}
.boldally-box__cart-label {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--box-muted);
}
.boldally-box__cart-items { font-size: 0.9375rem; }

.boldally-box__editing {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
  padding: 0.75rem 1rem;
  background: var(--box-surface);
  border-radius: 2px;
  font-size: 0.9375rem;
}

.boldally-box__added { margin: 0 0 1.25rem; font-size: 1.125rem; }

/* ------------------------------------------------------------------- Summary */

/* The tray: what is in the box so far, pinned to the bottom of the viewport.
   The wizard shows one step at a time, so without this nothing on screen says
   what has already been chosen.

   `position: sticky` with `bottom: 0`, NOT `position: fixed`. Fixed would take
   it out of flow and let it sit over the theme's own footer forever; sticky
   parks it at the bottom of the viewport only while the wizard is on screen,
   and releases it back into the page when the customer scrolls past. It also
   needs no compensating padding at the end of the page. */
.boldally-box__summary {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.75rem;
  margin-top: 2rem;
  /* Real padding on all four sides. This is a card floating over the page, and
     content flush to its edge reads as a bug — the horizontal padding used to
     be 0, which put the headline hard against the panel edge. */
  padding: 1.125rem 1.25rem max(1.125rem, env(safe-area-inset-bottom));
  border: 1px solid var(--box-hairline);
  border-radius: 0.75rem;
  /* Lifts the card off the bottom edge so it reads as floating rather than
     welded to the viewport. */
  margin-bottom: 0.75rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
  /* The theme's own page colour, measured at mount by resolvePageBackground in
     box-builder.js — the product grid scrolls underneath and would otherwise
     read straight through. The literal fallback is only for a page where
     nothing in the ancestor chain paints a background at all; hardcoding white
     here is what made the tray a foreign white band on this grey theme. */
  background: var(--box-page-bg, #ffffff);
}

.boldally-box__tray-headline { font-weight: 600; }

/* Slots sit on one row and scroll sideways rather than wrapping onto a second
   line: the tray's height must stay predictable, since it is pinned over the
   page and every extra row eats the product grid it is meant to sit beside. */
/* ⚠️ `overflow-x: auto` makes `overflow-y` compute to `auto` too — the spec
   does not allow one axis to be visible while the other scrolls. Anything
   positioned outside a slot's own box is therefore CLIPPED here, which is why
   the remove control below is inset rather than overhanging the corner the way
   a badge normally would. Do not "fix" it back to a negative offset. */
.boldally-box__slots {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding-bottom: 0.25rem;
  scrollbar-width: thin;
}

/* A fixed basis, not min/max width: slots are a row of identical containers
   standing for the same thing, and sizing each to its product's name makes the
   row read as a list of unrelated cards. It also stops the row reflowing every
   time a pick changes. */
.boldally-box__slot {
  position: relative;
  flex: 0 0 13.5rem;
}
.boldally-box__slot-open {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  height: 100%;
  min-height: 3.5rem;
  padding: 0.5rem;
  border: 1px dashed var(--box-hairline);
  border-radius: 0.5rem;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* Room for the remove control, so a two-line title never runs under it. Only
   on filled slots — an empty one has no control and would sit visibly
   off-centre with the same padding. */
.boldally-box__slot.is-filled .boldally-box__slot-open { padding-right: 1.9rem; }
.boldally-box__slot:not(.is-filled) .boldally-box__slot-open { justify-content: center; }
/* A filled slot is a solid card; an empty one stays dashed, which is what
   makes "still to choose" readable at a glance without any text. */
.boldally-box__slot.is-filled .boldally-box__slot-open {
  border-style: solid;
  border-color: var(--box-hairline);
}
.boldally-box__slot.is-current .boldally-box__slot-open {
  border-color: currentColor;
}
.boldally-box__slot-image {
  width: 2.5rem;
  height: 2.5rem;
  object-fit: cover;
  border-radius: 0.25rem;
  flex: 0 0 auto;
}
.boldally-box__slot-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.boldally-box__slot-title {
  font-size: 0.8125rem;
  line-height: 1.25;
  font-weight: 500;
  /* Two lines, then ellipsis. A long plushie name must not make one slot
     taller than its neighbours and ripple the whole pinned bar. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.boldally-box__slot-step,
.boldally-box__slot-empty { font-size: 0.75rem; color: var(--box-muted); }
.boldally-box__slot-plus { font-size: 1.25rem; line-height: 1; color: var(--box-muted); }

/* Inset, not overhanging — see the clipping note on .boldally-box__slots. */
.boldally-box__slot-remove {
  position: absolute;
  top: 0.3rem;
  right: 0.3rem;
  width: 1.25rem;
  height: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--box-surface);
  color: inherit;
  font: inherit;
  font-size: 0.8rem;
  line-height: 1;
  opacity: 0.65;
  cursor: pointer;
  transition: opacity 0.15s ease, background 0.15s ease;
}
.boldally-box__slot-remove:hover,
.boldally-box__slot-remove:focus-visible {
  opacity: 1;
  background: var(--box-hairline);
}

.boldally-box__tray-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.boldally-box__count { color: var(--box-muted); font-size: 0.875rem; }
.boldally-box__clear {
  padding: 0;
  border: 0;
  background: none;
  color: var(--box-muted);
  font-size: 0.875rem;
  text-decoration: underline;
  cursor: pointer;
}
.boldally-box__total { display: flex; align-items: baseline; gap: 0.35rem; }
.boldally-box__total-label { color: var(--box-muted); font-size: 0.875rem; }
.boldally-box__total-value { font-weight: 600; }

/* Pushes the actions to the far end of the bar whatever else is in it. */
.boldally-box__tray-actions {
  display: flex;
  /* stretch, not center: Back inherits the theme's font and line-height while
     the primary is uppercase at 0.8125rem/1.2, so left to their own content
     the two came out different heights sitting side by side. The taller sets
     the row and both fill it. */
  align-items: stretch;
  gap: 0.5rem;
  margin-left: auto;
}

/* The tray's own actions sit side by side in a row, so they size to their
   label — unlike the review's CTA, which spans its column.

   The `:not()` is carried through from the primary-button rule below on
   purpose. It is not decoration: `:not(.x)` contributes its argument's
   specificity, so that rule is (0,3,0) and a plain
   `.boldally-box__tray-actions .boldally-box__cta` here would be (0,2,0) and
   lose — regardless of which comes first in the file. Matching the pattern
   makes this (0,4,0) and lets it win. Repeating it also keeps the opt-out
   honest: a merchant using their theme's button class gets neither rule. */
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__tray-next,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__review-cta,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__cta {
  display: inline-block;
  width: auto;
  min-width: 8rem;
  padding: 0.75rem 1.5rem;
  border-radius: 999px;
}
.boldally-box__tray-back {
  padding: 0.75rem 1.5rem;
  border: 1px solid var(--box-hairline);
  border-radius: 999px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.boldally-box__message:empty { display: none; }
.boldally-box__message { margin: 0; flex-basis: 100%; }

.boldally-box__notice { padding: 1rem; border: 1px dashed currentColor; }
.boldally-box__notice-list { padding: 1rem; border: 1px dashed currentColor; margin-top: 1rem; }
.boldally-box__notice-list-items { list-style: none; margin: 0; padding: 0; }
.boldally-box__notice-list-item { display: flex; gap: 0.5rem; padding: 0.25rem 0; }
/* The handle is the token the merchant copies into the block setting, so it
   has to be visually separable from the name beside it. Without this the row
   reads "jellico-box Jellico Box Ready" and they have to guess which is which. */
.boldally-box__notice-list-handle { font-family: monospace; font-weight: 700; }
.boldally-box__notice-list-name { opacity: 0.8; }

/* --------------------------------------------------------------- Celebration */

/* The flare when the last pick lands. Deliberately the same vocabulary as
   blocks/reward-bar.liquid — confetti, a shake, a top banner, the same six
   colours — because a customer meets that bar in the cart drawer of this same
   store, and two celebration languages in one purchase reads as two shops.

   Reimplemented rather than shared: the reward bar ships with the theme
   extension and this page is served by the app server, so neither can import
   the other. This comment is the contract. */

.boldally-box.is-celebrating { animation: boldally-box-shake 0.6s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
@keyframes boldally-box-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}

.boldally-box-confetti {
  position: fixed;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 999999;
}
.boldally-box-confetti i {
  position: absolute;
  top: -12px;
  width: 8px;
  height: 12px;
  border-radius: 2px;
  opacity: 0;
  animation-name: boldally-box-fall;
  animation-timing-function: ease-in;
  animation-fill-mode: forwards;
}
@keyframes boldally-box-fall {
  0% { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  100% { opacity: 0; transform: translate(var(--drift, 0), 105vh) rotate(540deg); }
}

.boldally-box-banner {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1000000;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 12px;
  width: max-content;
  max-width: min(360px, calc(100vw - 32px));
  padding: 13px 20px;
  border-radius: 16px;
  background: linear-gradient(120deg, #ff6ea9, #c77dff);
  color: #fff;
  box-shadow: 0 14px 38px rgba(160, 90, 200, 0.42);
  animation: boldally-box-banner-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.boldally-box-banner__emoji { font-size: 1.7rem; line-height: 1; }
.boldally-box-banner__text { display: flex; flex-direction: column; line-height: 1.2; text-align: left; }
.boldally-box-banner__title { font-weight: 800; font-size: 1rem; }
.boldally-box-banner__sub { font-size: 0.78rem; opacity: 0.92; margin-top: 1px; }
.boldally-box-banner.is-out { animation: boldally-box-banner-out 0.5s ease forwards; }
@keyframes boldally-box-banner-in {
  0% { opacity: 0; transform: translate(-50%, -26px) scale(0.96); }
  100% { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
@keyframes boldally-box-banner-out {
  100% { opacity: 0; transform: translate(-50%, -18px) scale(0.98); }
}

/* The per-pick receipt. Same corner and same geometry as the celebration
   above, deliberately: they are the same kind of message from the same page,
   and only one of the two is ever on screen at a time (see announceAdded).

   Quieter on purpose, though. The celebration is the finish line and gets the
   gradient; this one fires up to five times in a single session, so it wears
   the dark of the primary CTA instead — a confirmation, not a party. Literal
   colours because it is appended to <body>, outside .boldally-box and its
   custom properties. */
.boldally-box-toast {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1000000;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 10px;
  width: max-content;
  max-width: min(340px, calc(100vw - 32px));
  padding: 9px 16px 9px 9px;
  border-radius: 999px;
  background: #1c1b2e;
  color: #fff;
  box-shadow: 0 12px 30px rgba(28, 27, 46, 0.34);
  animation: boldally-box-toast-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* Sized in px rather than em so a theme's own font-size cannot stretch the
   pill: this element inherits from <body>, not from the wizard. */
.boldally-box-toast__thumb {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  object-fit: cover;
  background: rgba(255, 255, 255, 0.14);
  flex: none;
}
.boldally-box-toast__tick {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  font-size: 1.1rem;
  flex: none;
}
.boldally-box-toast__text {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
  text-align: left;
  min-width: 0;
}
.boldally-box-toast__title { font-weight: 700; font-size: 0.9rem; }
/* The product title is the one part of this that can be long enough to wrap
   the pill onto a second line, so it gets the ellipsis rather than the width. */
.boldally-box-toast__sub {
  font-size: 0.78rem;
  opacity: 0.85;
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.boldally-box-toast.is-out { animation: boldally-box-toast-out 0.35s ease forwards; }
@keyframes boldally-box-toast-in {
  0% { opacity: 0; transform: translate(-50%, -18px) scale(0.97); }
  100% { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
@keyframes boldally-box-toast-out {
  100% { opacity: 0; transform: translate(-50%, -12px) scale(0.98); }
}

/* Unlike the confetti, this one is NOT suppressed under reduced motion — it
   carries the only confirmation a customer gets that their pick registered,
   and information is not decoration. It arrives and leaves without moving. */
@media (prefers-reduced-motion: reduce) {
  .boldally-box-toast,
  .boldally-box-toast.is-out { animation: none; }
  .boldally-box-toast.is-out { opacity: 0; }
}

/* The controller already returns early under reduced motion, so nothing here
   is ever built. These rules exist for the case where it runs anyway — a
   preference changed mid-session, say — so the page still cannot lurch. */
@media (prefers-reduced-motion: reduce) {
  .boldally-box.is-celebrating,
  .boldally-box-confetti i,
  .boldally-box-banner,
  /* Specific enough to beat .boldally-box__reel-frame.is-landed, which is
     declared further down the file: a media query adds no specificity of its
     own, so a bare .boldally-box__reel-frame here lost on both counts and
     this rule quietly did nothing. */
  .boldally-box .boldally-box__reel-frame.is-landed { animation: none; }
  .boldally-box-confetti { display: none; }
}
/* The random pick's own motion is gated the other way round — see the
   no-preference block in the Random pick section below. Suppressing it from
   up here had the same source-order problem as the rule above. */

/* ----------------------------------------------------------- Random pick */

/* An offer, not a card. It carries its own colour pair rather than the theme's
   text colour so that it reads as a distinct kind of thing from both the
   product cards below it and the dark "Add box to cart" in the tray.

   Capped and centred rather than full-bleed. Stretched across the grid it was
   a 72px slab holding a small island of text, and two thirds of it was empty —
   the width made it shout without making it any easier to read, and on a
   desktop it out-weighed the products it sits above. Sized to its content, it
   reads as what it is: an aside, offering to skip the grid.

   The inset bottom edge is the whole of the decoration: it gives the pill a
   lit face and a shadowed one, so it reads as a key with somewhere to travel.
   That is the right suggestion for the one control on this page whose entire
   proposition is "press it and something happens". */
.boldally-box .boldally-box__random {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  width: 100%;
  max-width: 24rem;
  margin: 0 auto 1.25rem;
  padding: 0.9375rem 1.75rem;
  border: 0;
  border-radius: 999px;
  background: var(--box-random-bg);
  color: var(--box-random-text);
  font: inherit;
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.5;
  cursor: pointer;
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.16);
}

/* brightness(), not a second literal: one colour to change, and it tracks
   whatever a merchant overrides --box-random-bg with. A shade change is not
   motion, so it stands outside the no-preference gating below. */
.boldally-box .boldally-box__random:hover { filter: brightness(0.96); }
/* Pressed, the key travels into its own shadow: the edge shrinks by exactly
   what the button moves down, so the top face lands where the bottom edge
   was and the whole thing occupies the same space throughout. Not gated on
   reduced motion — this is the button reporting the click, not an
   animation, and it is instant either way. */
.boldally-box .boldally-box__random:active {
  filter: brightness(0.93);
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.16);
  transform: translateY(2px);
}

/* ------------------------------------------------------------------- Die */

.boldally-box__die {
  flex: none;
  width: 1.5em;
  height: 1.5em;
  /* Off-square at rest, like a die that has been thrown rather than an icon
     of one. Nine degrees is enough to read as a tilt and not enough to look
     like a mistake. */
  transform: rotate(-9deg);
}
.boldally-box__die-body {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
}
.boldally-box__die-pip { fill: currentColor; }

/* At rest the die shows a five. These are the same values the animation's
   own 0% frame sets, so a browser that runs the animation and one that
   cannot both show the same face. */
.boldally-box__die-pip--ml,
.boldally-box__die-pip--mr { opacity: 0; }

/* Four faces — 5, 2, 6, 3 — one per quarter of the roll. Every one of those
   faces lights the top-left and bottom-right pips, so neither ever animates;
   of the remaining five, two pairs move together, which is why there are
   three keyframe sets here and not seven. */
/* Starts and ends at the resting tilt, not at 0 — an animation's keyframes
   replace the element's own transform outright, so a 0deg start would snap
   the die square the instant it began and back off-square when it stopped. */
@keyframes boldally-die-tumble {
  from { transform: rotate(-9deg); }
  to { transform: rotate(351deg); }
}
/* The other two corners: lit on 5 and 6, dark on 2 and 3. */
@keyframes boldally-die-corners {
  0%, 24.99% { opacity: 1; }
  25%, 49.99% { opacity: 0; }
  50%, 74.99% { opacity: 1; }
  75%, 100% { opacity: 0; }
}
/* The middle row: lit on 6 alone. */
@keyframes boldally-die-middles {
  0%, 49.99% { opacity: 0; }
  50%, 74.99% { opacity: 1; }
  75%, 100% { opacity: 0; }
}
/* The centre pip: lit on the odd faces, 5 and 3. */
@keyframes boldally-die-centre {
  0%, 24.99% { opacity: 1; }
  25%, 74.99% { opacity: 0; }
  75%, 100% { opacity: 1; }
}

/* Which animation each part runs is a property on the part itself, so the
   trigger below can be ONE rule listing where the die rolls rather than one
   rule per part per trigger. */
.boldally-box__die { --box-die-anim: boldally-die-tumble; }
/* Custom properties inherit, so the top-left and bottom-right pips — the two
   lit on all four faces, which must never move — would otherwise pick up the
   body's tumble from the <svg> above them and rotate about the SVG origin.
   They have to say "none" out loud. */
.boldally-box__die-pip { --box-die-anim: none; }
.boldally-box__die-pip--tr,
.boldally-box__die-pip--bl { --box-die-anim: boldally-die-corners; }
.boldally-box__die-pip--ml,
.boldally-box__die-pip--mr { --box-die-anim: boldally-die-middles; }
.boldally-box__die-pip--mc { --box-die-anim: boldally-die-centre; }

/* Gated on no-preference rather than switched off in a `reduce` block below,
   because unlike the confetti and the reel there is no controller path that
   declines to build this — the die is on the button whether or not it moves,
   so the animation must never be attached in the first place.
   The pips snap between faces; only the body eases. */
@media (prefers-reduced-motion: no-preference) {
  /* Only the easing, gated here rather than cancelled in the `reduce` block
     near the top of the file — that block is declared BEFORE these rules, so
     it lost on source order and never suppressed anything.

     No hover lift: a key does not rise when you point at it, and a lift plus
     a press are two contradictory physical stories about the same object.
     The press itself is declared ungated above — reporting a click is not an
     animation, and it is instantaneous whether or not this eases it. */
  .boldally-box .boldally-box__random {
    transition: filter 0.15s ease, transform 0.08s ease, box-shadow 0.08s ease;
  }

  .boldally-box .boldally-box__random:hover .boldally-box__die,
  .boldally-box .boldally-box__random:hover .boldally-box__die-pip,
  .boldally-box .boldally-box__random:focus-visible .boldally-box__die,
  .boldally-box .boldally-box__random:focus-visible .boldally-box__die-pip,
  .boldally-box__die.is-rolling,
  .boldally-box__die.is-rolling .boldally-box__die-pip {
    animation: var(--box-die-anim) 0.66s step-end;
  }
  .boldally-box .boldally-box__random:hover .boldally-box__die,
  .boldally-box .boldally-box__random:focus-visible .boldally-box__die,
  .boldally-box__die.is-rolling { animation-timing-function: ease-in-out; }

  /* One roll on hover — with the default fill-mode the die drops back to its
     resting five when it finishes, so pointing at it again rolls it again.
     The reel's die keeps going until the reel lands and JS takes the class
     off. */
  .boldally-box__die.is-rolling,
  .boldally-box__die.is-rolling .boldally-box__die-pip {
    animation-iteration-count: infinite;
  }
}


.boldally-box__step--spin {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 50vh;
}

.boldally-box__reel {
  width: min(20rem, 80vw);
  text-align: center;
}

/* Square, and object-fit: cover, matching .boldally-box__card-image — so a
   frame that swaps to a differently-shaped photo cannot reflow the panel and
   make the tray jump on a phone. */
.boldally-box__reel-frame {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 0.75rem;
}

/* The landing beat. Only the winner gets it, and only once — the frame is a
   single <img> reused for every swap, so the class is added and never
   removed within one spin. */
.boldally-box__reel-frame.is-landed {
  animation: boldally-box-land 0.45s cubic-bezier(0.22, 1.4, 0.5, 1);
}

@keyframes boldally-box-land {
  0% { transform: scale(1); }
  45% { transform: scale(1.07); }
  100% { transform: scale(1); }
}

.boldally-box__reel-title {
  margin: 0.75rem 0 0.25rem;
  font-size: 1.05rem;
  font-weight: 600;
}

/* A row, so the rolling die sits beside the words rather than above them. */
.boldally-box__reel-status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin: 0;
  font-size: 0.9rem;
  opacity: 0.7;
}

/* The one line that says this product was not chosen by the customer. Above
   the title, quieter than it — the title is still the product's name. */
.boldally-box__picked {
  margin: 0 0 0.25rem;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.7;
}

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

/* Primary: the actions that move the customer forward. Ordinary class
   specificity so a theme's `button { }` reset cannot strip the padding and the
   fill — see the note at the top of this file. Stood down entirely when the
   merchant opts into their theme's button class. */
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__cta,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__accept,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__review-cta,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-next {
  display: block;
  width: 100%;
  padding: 1rem 1.5rem;
  border: 0;
  border-radius: 2px;
  background: var(--box-accent);
  color: var(--box-accent-text);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  line-height: 1.2;
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;
  transition: opacity 0.15s ease;
}
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__cta:hover,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-next:hover,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__accept:hover,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__review-cta:hover {
  opacity: 0.85;
}
.boldally-box .boldally-box__cta[disabled] { cursor: default; opacity: 0.55; }

/* Secondary: outlined, in the theme's own text colour. These never take the
   theme's button class — a class that is full-width and uppercase is right for
   "Add box to cart" and wrong for "Back". */
.boldally-box .boldally-box__back,
.boldally-box .boldally-box__show-more,
.boldally-box .boldally-box__review-edit,
.boldally-box .boldally-box__cart-edit,
.boldally-box .boldally-box__editing-cancel,
.boldally-box .boldally-box__random-again,
.boldally-box .boldally-box__retry {
  font: inherit;
  font-size: 0.875rem;
  color: inherit;
  padding: 0.5rem 1rem;
  border: 1px solid var(--box-hairline);
  border-radius: 2px;
  background: none;
  cursor: pointer;
  line-height: 1.4;
}
.boldally-box .boldally-box__show-more { margin-top: 1.25rem; }
.boldally-box .boldally-box__review-edit { flex: none; align-self: center; }

/* Stacked at the end of the details column: the accept action on top, Back
   beneath it. Both span the column, matching how a product page stacks its own
   two actions — Back stays visually lighter through its outline rather than by
   being narrower. */
.boldally-box__actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.625rem;
  margin-top: 1.5rem;
}
.boldally-box__actions .boldally-box__back,
.boldally-box__actions .boldally-box__random-again {
  margin: 0;
  padding: 0.875rem 1.5rem;
  text-align: center;
}
.boldally-box__actions .boldally-box__accept { margin: 0; }

/* The forward action does not need to span 1200px on a desktop review screen;
   it does need to span the details column beside a product photo, which is
   what `width: 100%` above already gives it. */
@media (min-width: 750px) {
  .boldally-box__summary .boldally-box__cta,
  .boldally-box__summary .boldally-box__review-cta {
    width: auto;
    min-width: 18rem;
    align-self: flex-end;
  }
}

/* A pinned bar costs the same height on every screen, and on a short one that
   is a large share of what the customer can see — a 190px tray over a 700px
   viewport covers most of a product row. Under that height the tray drops the
   parts that are explanation rather than control: the headline (the count in
   the bar says the same thing), and some of the slot padding. The slots, the
   total and the actions stay, because those are the reason it is pinned.

   Keyed on viewport HEIGHT, not width: this is about how much vertical room
   the customer has, which a phone in landscape and a small laptop share. */
@media (max-height: 800px) {
  .boldally-box__tray-headline { display: none; }
  .boldally-box__summary {
    gap: 0.5rem;
    padding: 0.75rem 1rem max(0.75rem, env(safe-area-inset-bottom));
    margin-bottom: 0.5rem;
  }
  .boldally-box__slot-open { min-height: 3rem; padding: 0.375rem; }
  .boldally-box__slot-image { width: 2rem; height: 2rem; }
}

/* Back and the primary action, the same height.
   
   Back inherits the theme's font and line-height; the primary is uppercase at
   0.8125rem/1.2 with its own padding. Side by side they came out 52px and
   40px. `align-items: stretch` on the row was not enough on its own, because
   the primary's own rule sets `display: block` at (0,4,0) and sat LATER in
   this file — so this block has to come after it, and does. An explicit shared
   height then makes the match exact rather than dependent on which label
   happens to be taller. */
.boldally-box__tray-actions .boldally-box__tray-back,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__cta,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__review-cta,
.boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__tray-next {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3rem;
  padding-block: 0;
}

/* ------------------------------------------------------------------- Mobile */

@media (max-width: 749px) {
  .boldally-box {
    --box-step-size: 2rem;
    --box-padding-block: 32px;
    /* Nothing constrains the wizard at this width — max-width is 1200px and the
       screen is 390 — so without this the grid, the rail labels and the tray
       all sit flush against the edge of the phone. */
    padding-inline: 1rem;
  }

  /* THE TRAY ON A PHONE.
     Desktop slots are wide cards carrying a thumbnail, the product name and
     the step name. Four of those are 54rem — five times a phone's width — so
     they became a horizontal scroller, and the customer could see two of the
     four things in their box at once. A tray whose whole job is "what have I
     chosen" that hides half the answer is worse than no tray.

     So on a phone a slot is a square tile: image or +, no text, all of them
     visible at once and sized to the row. The product names are one tap away
     on the review, and the step name is already the heading above the grid.

     The compact rules also live here, not only in the max-height query: a
     phone is 844px tall in portrait, so keying compactness on viewport height
     alone left the tallest, narrowest screens with the full-size tray. */
  .boldally-box__tray-headline { display: none; }
  .boldally-box__summary {
    gap: 0.5rem;
    padding: 0.625rem 0.75rem max(0.625rem, env(safe-area-inset-bottom));
    margin-bottom: 0.375rem;
    border-radius: 0.625rem;
  }
  .boldally-box__tray-bar { gap: 0.5rem 0.625rem; }

  /* No scroller: every slot is on screen. `overflow: visible` is safe here
     precisely BECAUSE there is no scrolling — the clipping note on
     .boldally-box__slots applies to the desktop scroller, not to this. */
  .boldally-box__slots { gap: 0.375rem; overflow: visible; padding-bottom: 0; }
  .boldally-box__slot { flex: 1 1 0; min-width: 0; }
  /* A fixed height, not aspect-ratio: 1. Square tiles are as tall as the
     column is wide, so a three-step box got 110px tiles and a six-step box got
     55px — the tray's height swinging with the step count on the screen with
     the least room. */
  .boldally-box__slot-open {
    flex-direction: column;
    justify-content: center;
    gap: 0;
    padding: 0;
    min-height: 0;
    height: 2.75rem;
    overflow: hidden;
  }
  .boldally-box__slot-text,
  .boldally-box__slot-empty { display: none; }
  .boldally-box__slot-image { width: 100%; height: 100%; border-radius: 0; object-fit: cover; }
  .boldally-box__slot-plus { font-size: 1.125rem; }
  .boldally-box__slot.is-filled .boldally-box__slot-open { padding-right: 0; }
  .boldally-box__slot-remove {
    top: 0.125rem;
    right: 0.125rem;
    width: 1.125rem;
    height: 1.125rem;
    font-size: 0.7rem;
    background: rgba(255, 255, 255, 0.92);
    opacity: 1;
  }

  /* `display: contents` dissolves the actions wrapper so Back and the primary
     become direct children of the bar and lay out with the count and the
     total. Stacking them as their own two rows cost the tray a third of the
     phone's screen; this puts Back on the meta row, which had empty space to
     its right, and leaves the primary a full row of its own.

     Not "drop Back on mobile": the tiles above can already reach every step,
     so it is arguably redundant — but here it costs nothing, and a bottom bar
     with no way back is a worse trade than sharing a row that exists anyway. */
  /* The rail's circles now show each picked product's picture, so the tray's
     tiles are the same answer twice — and they were the single biggest row in
     a bar that was taking a third of the screen. The rail scrolls away with
     the page where the tray does not, which is the honest cost; the count and
     the total stay pinned, and the rail is one flick away. */
  .boldally-box__slots { display: none; }

  .boldally-box__tray-actions { display: contents; }
  .boldally-box__tray-back {
    margin-left: auto;
    padding: 0.4rem 0.9rem;
    font-size: 0.8125rem;
  }
  .boldally-box__count,
  .boldally-box__clear,
  .boldally-box__total-label { font-size: 0.8125rem; }

  /* Storefronts conventionally park a chat or help bubble in the bottom-right
     corner, and the tray is pinned to that same corner — on this store a
     WhatsApp button sat on top of "Add box to cart".

     Lifting the whole tray clear of it cost 76px of HEIGHT, which is the very
     thing this breakpoint is trying to save. A bubble and a full-bleed bottom
     button want the same pixels and one of them has to give; giving up ~56px
     of the button's WIDTH is the cheaper half of that trade, and it is still
     the widest thing on screen.

     (`bottom`, not `margin-bottom`, was what actually moved a stuck sticky
     element — a margin shifts the flow position and leaves the parked position
     alone. Recorded because it cost a measurement to learn, and the next
     person to want the tray lifted will reach for the margin first.)

     The class is set by reserveWidgetGutter in box-builder.js only when
     something fixed is really there, so a shop without one keeps the whole
     width. */
  .boldally-box.has-corner-widget .boldally-box__cta,
  .boldally-box.has-corner-widget .boldally-box__review-cta,
  .boldally-box.has-corner-widget .boldally-box__tray-next { margin-right: 3.5rem; }

  /* The `:not()` is carried through AGAIN, for the third time in this file and
     for the same reason: the desktop rule that makes these inline-block and
     auto-width is `.boldally-box:not(…) .boldally-box__tray-actions .x`, which
     is (0,4,0). A plain `.boldally-box__tray-actions > *` here is (0,2,0) and
     loses however far down the file it sits — which is exactly what happened:
     Back went full width and the primary action, the one that matters, did
     not. Matching the pattern ties the specificity, and this block is later,
     so it wins. */
  .boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__cta,
  .boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__review-cta,
  .boldally-box:not(.boldally-box--theme-buttons) .boldally-box__tray-actions .boldally-box__tray-next {
    display: block;
    /* flex-basis, not width: with the wrapper dissolved these are flex children
       of the bar, and a 100% basis is what puts each on its own row. */
    /* Shrink is 1, not 0, so the corner-widget margin below can actually
       narrow this — with shrink 0 the margin overflowed the bar instead. */
    flex: 1 1 100%;
    width: auto;
    min-width: 0;
    padding-block: 0.7rem;
  }

  .boldally-box__rail { margin-bottom: 1.5rem; }
  /* One line, ellipsised. "Choose your Tray" wrapped to two and pushed the
     grid down by a row of text on the screen with least room for it. */
  .boldally-box__rail-label {
    font-size: 0.625rem;
    letter-spacing: 0.03em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
  }

  /* Rows that put an image, a block of text and a button side by side have no
     room for all three on a phone. Each becomes a stack, and the control that
     ends the row spans it — a full-width target rather than whatever slice of
     width happened to be left over. */
  .boldally-box__review-item { flex-wrap: wrap; gap: 0.75rem; }
  .boldally-box__review-image { width: 3.5rem; }
  .boldally-box .boldally-box__review-edit { align-self: stretch; flex: 1 0 100%; }

  .boldally-box__cart-boxes { padding: 0.875rem 1rem; }
  .boldally-box__cart-item { flex-direction: column; align-items: stretch; gap: 0.625rem; }
  .boldally-box__cart-thumb { width: 2.75rem; height: 2.75rem; }
  .boldally-box .boldally-box__cart-edit { width: 100%; }

  .boldally-box__editing { gap: 0.75rem; }
  .boldally-box .boldally-box__editing-cancel { width: 100%; }

  /* The label above the stepper and the control below it, rather than a row
     that leaves the field 4rem wide. */
}
