/* Cart, checkout, order-complete and account pages — shared by every theme.
   Colours and fonts come from the active theme's CSS custom properties (with fallbacks for
   the internal base theme). Linked from the pages themselves and from the header partial. */

:root {
  --c-primary: var(--color-primary, #0f172a);
  --c-accent: var(--color-accent, #4f46e5);
  --c-bg: var(--color-background, #ffffff);
  --c-text: var(--color-text, #0f172a);
  --c-line: color-mix(in srgb, var(--c-text) 12%, transparent);
  --c-line-strong: color-mix(in srgb, var(--c-text) 25%, transparent);
  --c-soft: color-mix(in srgb, var(--c-text) 5%, var(--c-bg));
  --c-muted: color-mix(in srgb, var(--c-text) 62%, var(--c-bg));
  --c-accent-soft: color-mix(in srgb, var(--c-accent) 12%, var(--c-bg));
  --c-ok: #15803d; --c-ok-bg: #dcfce7; --c-warn: #b45309; --c-warn-bg: #fef3c7;
  --c-danger: #b91c1c; --c-danger-bg: #fee2e2; --c-info: #0369a1; --c-info-bg: #e0f2fe;
  --c-radius: 14px; --c-radius-sm: 10px;
  --c-shadow: 0 10px 30px color-mix(in srgb, var(--c-text) 8%, transparent);
}
/* The *derived* tokens are restated on `body`, and that is not a duplicate of the block above.
   A custom property's `var()`s resolve against the element the property is declared on, so a
   token mixed in `:root` is mixed against `<html>`'s values — while every theme re-points the
   palette from `body.theme-<name>`, one element lower. Each theme restates five of these six by
   hand; `--c-accent-soft` is the one none of the six ever did, so on all six it stayed
   `mix(accent 12%, var(--color-background))` — the mode-static legacy setting — and rendered a
   near-white panel in dark mode under `--c-text` ink that *does* follow the palette. Ratio 1.03
   on the account nav's current item, the applied-discount pill, the download box and the cookie
   panel: the menu item you were standing on was the one you could not read.
   Declared on `body` at (0,0,1) so a theme's own `body.theme-<name>` block still wins. */
body {
  --c-line: color-mix(in srgb, var(--c-text) 12%, transparent);
  --c-line-strong: color-mix(in srgb, var(--c-text) 25%, transparent);
  --c-soft: color-mix(in srgb, var(--c-text) 5%, var(--c-bg));
  --c-muted: color-mix(in srgb, var(--c-text) 62%, var(--c-bg));
  --c-accent-soft: color-mix(in srgb, var(--c-accent) 12%, var(--c-bg));
  --c-shadow: 0 10px 30px color-mix(in srgb, var(--c-text) 8%, transparent);
}
[x-cloak] { display: none !important; }

/* Accessibility utilities the *shared* partials depend on (the search form's label, the
   switchers' labels, the skip link in the page shell). They live here, in the stylesheet every
   theme loads, because a theme that forgot them did not merely look plain — it printed "Search
   products" and "Skip to content" as visible body text on every page. Wrapped in :where() so
   they carry zero specificity and any theme's own rule still wins. */
:where(.sr-only) {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
:where(.skip-link) {
  position: absolute; left: 1rem; top: -100px; z-index: 100;
  padding: 0.6rem 1rem; border-radius: var(--c-radius-sm);
  background: var(--c-primary); color: var(--c-bg); font-weight: 600; text-decoration: none;
}
:where(.skip-link):focus { top: 1rem; }
/* ...but zero specificity also loses to a theme's own blanket anchor rule, and every theme has
   one: `body.theme-<name> a { color: inherit }` (0,2,1) beat `:where(.skip-link)` (0,0,0), so
   the link took the body's ink on a background that *is* the body's ink. Ratio 1.00 — the skip
   link was invisible on atelier and marquee at the one moment it exists, when a keyboard user
   has just tabbed onto it. Restated here at a specificity that clears that rule, and on :focus
   because that is the only time it is on screen. A theme that wants its own goes one higher,
   from `body.theme-<name> .skip-link:focus`. */
a.skip-link:focus,
a.skip-link:focus-visible {
  background: var(--c-primary);
  color: var(--c-bg);
}

/* The announcement bar is the theme's to colour (every one of them styles `.announcement`), but
   an announcement that links somewhere is new, and no theme has a rule for the anchor inside it.
   Every theme's blanket `body.theme-<name> a { color: inherit }` already gives it the bar's own
   ink; this only makes it look like a link. */
.announcement a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* A link inside a sentence is only a link if it looks like one.
   Every theme opens with `body.theme-<name> :where(a) { color: inherit; text-decoration: none }`,
   which is right for a menu entry, a product card and a footer column — there, position is what
   says "link" — and wrong for a run of text, where nothing else does. So "By placing this order
   you agree to our Terms and conditions…", "Already have an account? Sign in", the cookie
   notice's links out and a merchant's own prose all read as plain words in a colour a palette
   may well have made identical to the ink around them.

   Scoped to prose: a paragraph, a list item or a definition inside one, plus the rich-text
   containers where a merchant's own words live. `:where(a)` carries no specificity, so one
   class is enough to beat it — the leading `body` is there because a theme may reach the same
   anchor at (0,2,1) and this has to win that too. Anything that is a *control* wearing an
   anchor opts out by class. `.text-link` does not: "Already have an account? Sign in" is the
   same sentence case, and the class is only left alone where it stands on its own in a card or
   a table row, which is not a run of text and is not matched here. */
body :is(p, li, dd) a:not(.btn):not(.btn-outline):not(.btn-accent):not(.pill):not(.ck-btn),
body :is(.product-description, .lesson-body, .policy-doc, .ck-page, .rich-text) a:not(.btn):not(.text-link) {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: from-font;
}

/* ---- Page scaffold ---------------------------------------------------------------------- */
.commerce-page { padding: clamp(2rem, 5vw, 3.5rem) 0; }
.commerce-page > .container + .container { margin-top: 2rem; }
.page-head { display: flex; align-items: end; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.page-head h1 { font-size: clamp(1.75rem, 3.5vw, 2.5rem); }
/* `body `: a theme's `p { margin: 0 }` is (0,1,2) and beat the bare rule, so every account page's
   standfirst sat on its heading. `stacked-text` asks this of a heading since it could not before. */
body .page-head p { color: var(--c-muted); margin-top: 0.35rem; }
.c-muted { color: var(--c-muted); }
.c-small { font-size: 0.875rem; }
.c-flow > * + * { margin-top: 1rem; }
.notice { padding: 0.75rem 1rem; border-radius: var(--c-radius-sm); font-size: 0.925rem; border: 1px solid transparent; }
.notice-ok { background: var(--c-ok-bg); color: var(--c-ok); }
.notice-warn { background: var(--c-warn-bg); color: var(--c-warn); }
.notice-danger { background: var(--c-danger-bg); color: var(--c-danger); }
.notice-info { background: var(--c-info-bg); color: var(--c-info); }
.notice ul { margin: 0; padding-left: 1rem; }
.messages-list { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: 0.5rem; }

/* Django's flash messages, emitted by `base.html` for every storefront page and by the delivery
   checkout step. `.store-messages`, `.alert` and `.alert-*` were all defined in exactly one
   place: `static/css/dashboard.css`. So a message that a discount code was rejected, or that an
   address was saved, printed as a bare paragraph in the page's body ink — on all seven themes,
   wherever a view redirected rather than rendering its own copy.

   The signal is carried in the **border**, and the text stays `--c-text` on `--c-soft`, which
   is legible by construction — the same conclusion the pay screens reached.

   `.notice-*` and `.pill-*` below still take a status colour on a tint of itself, which is the
   pairing that measured 2.74:1 to 3.85:1 on the old palettes and 1.51:1 on the one theme that
   never re-pointed `--c-warn-bg`. **That is no longer true**: rebuilding the palettes fixed it,
   and all four pairs now measure 4.51:1 or better on all seven themes
   (`tests/themes/test_status_contrast_browser.py`). Left as they are, because a component that
   passes is not a component to rewrite — but the border-and-body-ink shape above is still the
   safer one for anything new, since it cannot be broken by a palette change. */
.store-messages { display: grid; gap: 0.5rem; margin-block: 1.25rem 0.5rem; }
.alert {
  margin: 0; padding: 0.75rem 1rem;
  border: 1px solid var(--c-line-strong); border-left-width: 3px;
  border-radius: var(--c-radius-sm);
  background: var(--c-soft); color: var(--c-text);
  font-size: 0.925rem;
}
.alert-success { border-left-color: var(--c-ok); }
.alert-info { border-left-color: var(--c-accent); }
.alert-warning { border-left-color: var(--c-warn); }
.alert-error, .alert-danger { border-left-color: var(--c-danger); }
.pill { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.15rem 0.65rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.02em; text-transform: capitalize; background: var(--c-soft); color: var(--c-muted); }
.pill-ok { background: var(--c-ok-bg); color: var(--c-ok); }
.pill-warn { background: var(--c-warn-bg); color: var(--c-warn); }
.pill-danger { background: var(--c-danger-bg); color: var(--c-danger); }
.pill-info { background: var(--c-info-bg); color: var(--c-info); }
.btn-sm { padding: 0.5rem 1rem; font-size: 0.9rem; }
/* A text link. `.btn-link` used to be a second name for this, defined here at weight 600 while
   every theme defines `.text-link` at its own weight and size — so the cart had both on one
   screen and Addresses used one while the order beside it used the other. Two names, two
   renderings, adjacent tabs of the same account. One name now.

   Only the <button> reset lives here: a "Remove" inside a POST form has to look like the "Edit"
   anchor next to it, which is the storefront's version of `a.link` vs `button.link` in the
   admin. Colour and weight stay the theme's. */
button.text-link { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; text-align: inherit; }
/* `body ` is load-bearing. A theme colours `.text-link` from `body.theme-<name> .text-link`,
   which is (0,2,1); a bare `.text-link.danger` is (0,2,0) and loses, so Remove would come out
   the same colour as Edit. With the element selector the two tie and commerce.css, linked after
   the theme, wins. */
/* The palette's `danger` role first: `--c-danger` is a mode-static `#b91c1c`, which is 2.8:1 on a
   dark ground, and the basket's Remove link was that on ten themes. Nothing had measured it,
   because the contrast sweep's customer had an empty basket. */
body .text-link.danger { color: var(--color-danger, var(--c-danger)); }
.btn.btn-full { width: 100%; }

/* ---- Shared form furniture ---------------------------------------------------------------

   `.card`, `.field` and `.input` are reached for by shared storefront templates that no theme
   overrides: the contact form, the support thread's reply box, the review form, the delivery
   checkout step and the tracking page. Every one of them is a real, well-styled class — in
   `static/css/dashboard.css`, the **admin** stylesheet, which no storefront page ever loads.
   Exactly the mistake the learning templates made, in five new places: the contact form was a
   browser-default stack of unstyled boxes on all seven themes from the day `apps/support`
   shipped.

   The sweep that exists to catch this could not see it. `_defined_classes()` pulled class names
   out of the *whole* stylesheet text, comments included, and both names appear in prose — `.card`
   in the block further down this file, `.input` in `static/css/richtext.css`. Two sentences of
   documentation were standing in for two components.

   `.field` is deliberately bare, not `.form .field`: the contact and reply forms carry `.card`,
   not `.form`, so the themes' own `.form .field` rules (all at higher specificity, so they still
   win where they apply) never reached them. */
.card { padding: 1.5rem; border: 1px solid var(--c-line); border-radius: var(--c-radius); background: var(--c-bg); }
.field { display: grid; gap: 0.4rem; margin-bottom: 1.15rem; }
.field > label { font-weight: 600; font-size: 0.9rem; }
.input {
  width: 100%; padding: 0.7rem 0.9rem;
  border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm);
  background: var(--c-bg); color: inherit; font: inherit;
}
textarea.input { resize: vertical; min-height: 5rem; }
.input:focus-visible {
  outline: none; border-color: var(--c-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 35%, transparent);
}
.input::placeholder { color: var(--c-muted); opacity: 1; }

/* ---- Two-column commerce layout (cart, checkout) ------------------------------------------ */
.commerce-grid { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(18rem, 1fr); gap: 2.5rem; align-items: start; }
.summary-card { position: sticky; top: calc(var(--aurora-header-h, 4.25rem) + 1rem); padding: 1.5rem; border: 1px solid var(--c-line); border-radius: var(--c-radius); background: var(--c-bg); box-shadow: var(--c-shadow); }
/* `body ` on all three: a heading's margin is the space under it, and every theme resets
   `h2`/`h3 { margin: 0 }` at (0,1,2) — so `.summary-card h2` at (0,1,1) is a gap that is
   written and never applied. The Review heading sat on its own first line, the checkout's
   section headings on their standfirsts, and a basket line's name on its own size and
   colour, on every theme, for as long as those screens have existed. Nothing could see it
   until the geometry sweep was given a basket: `/cart/` and `/checkout/` were both crawled
   with an empty one, so neither a cart line nor a summary had ever been drawn. */
body .summary-card h2 { font-size: 1.15rem; margin-bottom: 1rem; }
.totals { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; font-size: 0.95rem; }
.totals li { display: flex; justify-content: space-between; gap: 1rem; }
.totals .grand { margin-top: 0.5rem; padding-top: 0.85rem; border-top: 1px solid var(--c-line); font-weight: 700; font-size: 1.1rem; }
.totals .discount { color: var(--c-ok); }
.totals .hint { color: var(--c-muted); font-size: 0.8rem; }
/* The basket's currency, offered only when every line has a price in more than one. */
body .basket-currency { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 1.25rem; }
body .basket-currency select { flex: 1; min-width: 6rem; }
.discount-form { display: flex; gap: 0.5rem; margin-top: 1.25rem; }
.discount-form input { flex: 1; min-width: 0; }
.discount-applied { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin-top: 1.25rem; padding: 0.6rem 0.85rem; border-radius: var(--c-radius-sm); background: var(--c-accent-soft); font-size: 0.9rem; }

/* ---- Cart lines ----------------------------------------------------------------------------- */
.cart-lines { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--c-line); }
.cart-line { display: grid; grid-template-columns: 5rem minmax(0, 1fr) auto; gap: 1.25rem; align-items: center; padding: 1.25rem 0; border-bottom: 1px solid var(--c-line); }
.cart-thumb { width: 5rem; aspect-ratio: 1; border-radius: var(--c-radius-sm); background: var(--c-soft); overflow: hidden; display: grid; place-items: center; color: var(--c-muted); }
.cart-thumb img { width: 100%; height: 100%; object-fit: cover; }
body .cart-line-body h3 { font-size: 1rem; margin-bottom: 0.15rem; }
.cart-line-body h3 a { text-decoration: none; }
.cart-line-meta { font-size: 0.85rem; color: var(--c-muted); margin-top: 0.15rem; }
/* When a line is for a particular time (a table, an appointment, a hire): `{{ line|line_when }}`.
   A span in a list row would run on from the name, so it is a block. */
body span.line-when { display: block; font-size: 0.85rem; font-weight: 400; color: var(--c-muted); margin-top: 0.15rem; }
.cart-line-actions { display: flex; align-items: center; gap: 1rem; margin-top: 0.75rem; flex-wrap: wrap; }
.qty { display: inline-flex; align-items: center; border: 1px solid var(--c-line-strong); border-radius: 999px; overflow: hidden; }
.qty button { width: 2.25rem; height: 2.25rem; border: 0; background: transparent; cursor: pointer; font-size: 1.1rem; color: var(--c-text); }
.qty button:hover { background: var(--c-soft); }
.qty input { width: 3rem; text-align: center; border: 0; background: transparent; padding: 0.4rem 0; font-weight: 600; -moz-appearance: textfield; }
.qty input::-webkit-outer-spin-button, .qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cart-line-price { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cart-line-price small { display: block; font-weight: 400; color: var(--c-muted); }
.cart-actions { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-top: 1.25rem; flex-wrap: wrap; }

/* ---- Empty state ---------------------------------------------------------------------------- */
.empty-state { text-align: center; padding: clamp(3rem, 8vw, 5rem) 1rem; border: 1px dashed var(--c-line-strong); border-radius: var(--c-radius); }
.empty-state .icon { width: 3rem; height: 3rem; margin: 0 auto 1rem; color: var(--c-muted); }
/* `body ` ties a theme's `h2`/`p` margin reset and wins by being linked later; without it the
   "Browse the shop…" sentence rested on the Start shopping button on every theme. */
body .empty-state h2 { margin-bottom: 0.5rem; }
body .empty-state p { color: var(--c-muted); margin-bottom: 1.5rem; }

/* ---- Mini-cart drawer ----------------------------------------------------------------------- */
.drawer-backdrop { position: fixed; inset: 0; z-index: 80; background: color-mix(in srgb, var(--c-text) 45%, transparent); }
.cart-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 90; width: min(26rem, 100%); background: var(--c-bg); box-shadow: var(--c-shadow); display: flex; flex-direction: column; }
.cart-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--c-line); }
.cart-drawer-head h2 { font-size: 1.15rem; }
.cart-drawer-close { border: 0; background: transparent; width: 2.25rem; height: 2.25rem; border-radius: 999px; cursor: pointer; font-size: 1.4rem; line-height: 1; color: var(--c-text); }
.cart-drawer-close:hover { background: var(--c-soft); }
.cart-drawer-body { flex: 1; overflow-y: auto; padding: 0 1.5rem; }
.cart-drawer-body .cart-line { grid-template-columns: 3.5rem minmax(0, 1fr) auto; gap: 0.85rem; padding: 1rem 0; }
.cart-drawer-body .cart-thumb { width: 3.5rem; }
.cart-drawer-foot { padding: 1.25rem 1.5rem; border-top: 1px solid var(--c-line); display: grid; gap: 0.75rem; }
.cart-drawer-foot .subtotal { display: flex; justify-content: space-between; font-weight: 700; }
.cart-drawer-foot .btn { width: 100%; }
.cart-drawer .empty-state { border: 0; padding: 3rem 1rem; }

/* ---- Checkout --------------------------------------------------------------------------------- */
.checkout-steps { display: flex; gap: 0.5rem; list-style: none; margin: 0 0 2rem; padding: 0; counter-reset: step; flex-wrap: wrap; }
.checkout-steps li { counter-increment: step; display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; font-weight: 600; color: var(--c-muted); }
.checkout-steps li::before { content: counter(step); display: grid; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 999px; background: var(--c-soft); font-size: 0.75rem; }
.checkout-steps li.active { color: var(--c-text); }
/* `--c-bg`, not `#fff`: `--c-primary` is the palette's ink, so in dark mode it is near-white and a
   literal white numeral on it vanished — every step read as an empty light dot. The sweep only
   measured text *nodes*, and a counter is `::before` content; it measures generated content now. */
.checkout-steps li.active::before { background: var(--c-primary); color: var(--c-bg); }
/* `.active` is "reached", which a done step and the current one both are — so without this the
   step you are standing on looks exactly like the ones behind it, which is most of what a progress
   bar is for. Weight rather than a colour: the pair is already an AA-guaranteed one, and a third
   shade of it is a contrast figure nobody has measured. `aria-current="step"` carries the same
   fact for anybody who cannot see either. */
.checkout-steps li.is-current { font-weight: 800; }
.checkout-steps li + li { margin-left: 0.75rem; }
.checkout-section { padding: 1.5rem; border: 1px solid var(--c-line); border-radius: var(--c-radius); margin-bottom: 1.5rem; }
body .checkout-section h2 { font-size: 1.15rem; margin-bottom: 0.25rem; }
.checkout-section .lede { color: var(--c-muted); font-size: 0.9rem; margin-bottom: 1.25rem; }
.checkout-section .signed-in { font-size: 0.9rem; color: var(--c-muted); margin-bottom: 1rem; }
/* The address map (`static/js/address_locator.js`). Its actions row is written by the script, so
   with JavaScript off the block is the map (or the consent gate) and nothing that looks pressable.
   `body p.x` because every theme resets `p { margin: 0 }` at (0,1,2). Leaflet's tiles are light
   whatever the scheme, so the frame takes the palette's line and the page keeps its own ground. */
.address-locator { display: grid; gap: 0.6rem; margin-bottom: 1.25rem; }
body p.address-locator-intro { margin: 0; font-size: 0.9rem; color: var(--c-muted); }
.address-map { height: 17rem; border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm); overflow: hidden; z-index: 0; }
body .address-locator > p.hint { margin: 0; font-size: 0.8rem; color: var(--c-muted); }
.address-locator-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem; }
body p.address-locator-status { margin: 0; font-size: 0.85rem; color: var(--c-text); flex: 1 1 14rem; min-width: 0; }
body p.address-locator-status:empty { display: none; }
@media (max-width: 600px) { .address-map { height: 14rem; } }
/* Searching for the address (the same script). The box, its label and the results are all built
   by `address_locator.js` and appear only where the shop has a geocoder, so nothing here is a
   hidden state the page starts in — the `reveal.js` rule, as on the header's search panel, whose
   highlight and ground this copies rather than inventing a second look for one component. */
.address-search { position: relative; display: grid; gap: 0.35rem; }
.address-search-label { font-size: 0.85rem; font-weight: 600; }
.address-search-input { width: 100%; }
.address-search-panel {
  position: absolute;
  inset-inline: 0;
  top: calc(100% + 0.3rem);
  /* Over the map, which Leaflet gives its own stacking context. */
  z-index: 60;
  max-height: min(60vh, 20rem);
  overflow-y: auto;
  padding: 0.3rem;
  background: var(--color-surface, #fff);
  color: var(--color-ink, #0f172a);
  border: 1px solid var(--color-border, #9ca3af);
  border-radius: var(--c-radius, 8px);
  box-shadow: 0 14px 34px rgb(0 0 0 / 22%);
}
.address-search-list { list-style: none; margin: 0; padding: 0; }
.address-search-option {
  padding: 0.5rem 0.6rem;
  border-radius: calc(var(--c-radius, 8px) - 2px);
  font-size: 0.9rem;
  cursor: pointer;
}
.address-search-option.is-active {
  background: color-mix(in srgb, var(--color-ink, #0f172a) 9%, var(--color-surface, #fff));
}
body p.address-search-empty { margin: 0; padding: 0.5rem 0.6rem; font-size: 0.85rem; color: var(--c-muted); }
/* The search's own message, under its own box. The shared `.address-locator-status` sits below
   the map beside *Use my location*, so a sentence about this box landed a screen away from it. */
body p.address-search-note { margin: 0; font-size: 0.85rem; color: var(--c-text); }
/* The local-delivery step's own map (`storefront/delivery/checkout_step.html`). It carried its
   height and radius as an inline `style=`, which is a rule no sweep in this project can read —
   the same reason `payments/_styles.html` had to become a real stylesheet. With the pin already
   dropped on checkout's address map the page states where we are coming instead of asking again,
   and folds this away behind a disclosure; Leaflet needs `invalidateSize()` when that opens. */
.pick-map { height: 21rem; border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm); overflow: hidden; z-index: 0; }
@media (max-width: 600px) { .pick-map { height: 16rem; } }
/* Spaced with `gap`, not with margins on the headings: every theme resets `h2` and `p` to
   `margin: 0` at (0,1,2), so a one-class margin here is a rule that is in the file and never
   applies — the `stacked-text` lesson, on a page no geometry sweep reaches, because rendering it
   takes a placed order. `body .card.delivery-step` to tie a theme's own `.card` rule and win by
   being linked later; the hidden inputs and the CSRF field are `display: none` and so cost no gap. */
body .card.delivery-step { display: grid; gap: 0.75rem; }
body .delivery-step h2 { font-size: 1.15rem; margin: 0.75rem 0 0; }
body .delivery-step h2:first-of-type { margin-top: 0; }
body address.pin-known-address { margin: 0; font-style: normal; line-height: 1.5; }
/* The summary carries `.text-link` for its colour and weight, which is the theme's to decide —
   inventing one here is how two controls that do the same job end up looking unrelated. */
.pin-adjust > summary { cursor: pointer; width: fit-content; padding: 0.15rem 0; font-size: 0.9rem; }
.pin-adjust[open] > summary { margin-bottom: 0.6rem; }
.form .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
.form select, .form textarea { width: 100%; padding: 0.7rem 0.9rem; border-radius: var(--c-radius-sm); border: 1px solid var(--c-line-strong); background: var(--c-bg); font: inherit; color: inherit; }
/* …and the text inputs beside them, which this file never styled: six themes fill the gap with
   a `.form input:not([type=checkbox])` rule of their own and `summit` does not, so on that theme
   every address and register field was a browser-default box next to a styled select. Wrapped in
   `:where()` — zero specificity — because those six rules and this one are otherwise the same
   selector, and commerce.css is linked last, so a plain copy here would silently take the look
   of every checkout form away from the theme that owns it. */
.form :where(input:not([type="checkbox"]):not([type="radio"]):not([type="submit"])) {
  width: 100%; padding: 0.7rem 0.9rem; border-radius: var(--c-radius-sm);
  border: 1px solid var(--c-line-strong); background: var(--c-bg); font: inherit; color: inherit;
}
.form textarea { resize: vertical; min-height: 5rem; }
.form select:focus-visible, .form textarea:focus-visible { outline: none; border-color: var(--c-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 35%, transparent); }
/* `.card` too: the contact page's form is a card, not a `.form`, and asks the newsletter question. */
:is(.form, .card) .field-check { display: flex; align-items: start; gap: 0.6rem; margin-bottom: 1rem; }
:is(.form, .card) .field-check label { font-weight: 500; }
:is(.form, .card) .field-check input { width: 1.05rem; height: 1.05rem; margin-top: 0.2rem; accent-color: var(--c-accent); }
.form .field .hint { font-size: 0.8rem; color: var(--c-muted); }
.form .field-error, .form .errorlist { list-style: none; margin: 0; padding: 0; font-size: 0.85rem; color: var(--c-danger); }
.form .has-error input, .form .has-error select { border-color: var(--c-danger); }
.form .btn { width: auto; margin-top: 0; }

/* ---- Every select in the shop, once ---------------------------------------------------------
   `appearance: none` is the whole rule and it was written in exactly one place: the two header
   switchers, where its absence had already been found once. Everywhere else a `<select>` was
   given a border, a background and a font by a component rule of its own and then painted by the
   **operating system** anyway — grey, rounded, system font, native arrow — on whatever palette
   the merchant chose. Four separate rules had made the same omission: `.form select` above (so
   the province and country boxes on an address), the reason on a return, the three filters on
   `/experiences/`, and `.evt-book-select` — *the date picker a repeating event is booked
   through*, i.e. the control a purchase is made with.

   One rule rather than a fifth, sixth and seventh copy. That is the lesson the currency switcher
   is already the record of: six themes each wrote a private copy of those four rules and the
   seventh wrote none. A component that four rules describe identically has one owner.

   The two switchers are excluded on purpose and the exclusion is not new — several themes pad
   for the native arrow and draw their own chevron over it, so suppressing it from here would
   leave them with neither.

   `body ` is load-bearing. A theme styles its form controls from `body.theme-<name> .form select`
   at (0,2,2), and a bare `select:not()…` is (0,2,1) — so the theme's `background` *shorthand*
   would win and reset `background-image` to none, leaving a select with no arrow at all. At
   (0,2,2) the two tie and commerce.css is linked last. The same load-bearing `body ` that
   `.container.narrow` and `.text-link.danger` needed.

   The chevron is two gradients in `currentColor`, not a background image: a `<select>` cannot
   carry a pseudo-element, and a data-URI SVG would be a colour literal in a file that forbids
   them and would not follow the palette into dark mode. */
body select:not(.currency-select):not(.locale-select) {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.1rem;
  color: var(--c-text);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: right 1.15rem center, right 0.8rem center;
  background-repeat: no-repeat;
}
.option-list { display: grid; gap: 0.6rem; }
.option-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm); cursor: pointer; font-weight: 400; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
.option-card input { width: 1.05rem; height: 1.05rem; accent-color: var(--c-accent); margin: 0; }
.option-card:has(input:checked) { border-color: var(--c-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 25%, transparent); }
.option-card strong { display: block; }
/* `display: block` because a delivery method prints two of these — its lead time and its
   own description — and inline they are one run of text: "Choose a time slot at
   checkoutWe deliver it ourselves". `.pay-method .desc` in `payments.css` is the same
   control one screen further along the same checkout and has always had it. */
.option-card .desc { display: block; font-size: 0.85rem; color: var(--c-muted); }
.option-card .amount { font-weight: 700; white-space: nowrap; }
.option-card .amount.free { color: var(--c-ok); }
.checkout-summary .summary-lines { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.6rem; font-size: 0.9rem; }
.checkout-summary .summary-lines li { display: flex; justify-content: space-between; gap: 1rem; }
.checkout-summary .summary-lines .name { color: var(--c-muted); }
.checkout-summary .summary-lines .name strong { color: var(--c-text); }
/* The button, the legal sentence and the "you'll choose how to pay" line, under the totals.
   Spaced with `gap` rather than margins: every theme resets `p { margin: 0 }` at (0,1,2), so
   `.secure-note`'s own `margin-top` was a rule that never applied and all three sat on each
   other. `.policy-notice` (`policies/_checkout_notice.html`) and `.currency-notice` had no rule
   at all in any stylesheet. */
.place-order { margin-top: 1.25rem; display: grid; gap: 0.85rem; }
.place-order .btn { width: 100%; }
.place-order .policy-notice,
.place-order .currency-notice {
  margin: 0; font-size: 0.8rem; line-height: 1.55; color: var(--c-muted); text-align: center;
}
.place-order .policy-notice a, .place-order .currency-notice strong { color: var(--c-text); }
/* The widget takes a line of its own inside a form laid out as a row.
   It is `hidden` until Cloudflare has a question, and every theme's `.newsletter-form` is a
   wrapping flex row of an input and a button — so the moment it *is* shown it arrives as a third
   item in a row nobody laid out for it: a 300px white branded box (Cloudflare draws none
   narrower) squeezing the email field to its placeholder, on the one press where the visitor has
   to be able to read it. `flex-basis: 100%` on a wrapping row and `grid-column: 1 / -1` on a grid
   put it and its note on their own line wherever a form is either; on a block form both are
   inert. The margin is for `press`, `encore` and `voyage`, whose rows are `gap: 0` because their
   input and button are deliberately attached.
   Giving it the line is also why `turnstile.js` asks for `size: "normal"` and not `flexible`:
   flexible means *fill the parent*, so the two together drew a Cloudflare banner the full width
   of the form. Do not cap that here with a `max-width` — a flex item's line breaking uses its
   flex-basis *clamped by max-width*, so a 300px cap would put it back beside the email field.
   The size belongs to the widget, the line belongs to the form.
   No sweep can see this: the widget is a Cloudflare iframe that loads in no test, and it is
   `hidden` on every page the sweeps do render. */
body .turnstile,
body .turnstile-note {
  flex-basis: 100%;
  grid-column: 1 / -1;
}
body .turnstile:not([hidden]) { margin-block-start: 0.6rem; }
/* What `static/js/turnstile.js` says when a press is held back — it builds the element, so no
   template can be swept for this class. `body p.…` because every theme resets `p { margin: 0 }`
   at (0,1,2); the ink is the page's own rather than `--c-warn`, which this file has already
   measured at 1.51:1 on its own tint, and the weight is what makes it read as an instruction. */
body p.turnstile-note {
  margin: 0.35rem 0 0; font-size: 0.85rem; font-weight: 500; line-height: 1.5; color: var(--c-text);
}
body .place-order p.turnstile-note { margin: 0; text-align: center; }
/* Cloudflare's widget is 300px and draws no narrower, and the bot check is in this column now.
   The shared summary track is `minmax(18rem, …)` — 288px, twelve short — so the checkout grid asks
   for a little more. No sweep can see this: the widget is a Cloudflare iframe that never loads in
   a test. */
body .commerce-grid.checkout-grid { grid-template-columns: minmax(0, 1.6fr) minmax(19rem, 1fr); }
@media (max-width: 900px) { body .commerce-grid.checkout-grid { grid-template-columns: 1fr; } }
.secure-note { display: flex; align-items: center; gap: 0.5rem; justify-content: center; margin: 0; font-size: 0.8rem; color: var(--c-muted); }
.htmx-request .htmx-indicator { opacity: 1; }
.htmx-indicator { opacity: 0; transition: opacity 0.2s ease; font-size: 0.8rem; color: var(--c-muted); }

/* ---- Order complete / detail ------------------------------------------------------------- */
.thanks { text-align: center; max-width: 40rem; margin: 0 auto 2.5rem; }
.thanks .check { width: 4rem; height: 4rem; border-radius: 999px; display: grid; place-items: center; margin: 0 auto 1.25rem; background: var(--c-ok-bg); color: var(--c-ok); }
.thanks .check .icon { width: 2rem; height: 2rem; }
.thanks h1 { margin-bottom: 0.5rem; }
.thanks p { color: var(--c-muted); }
/* (0,2,1): a theme's `p { margin: 0 }` reset is (0,1,2). */
.thanks p.offline-note { margin-top: 0.75rem; color: var(--c-text); }
.thanks .actions { display: flex; gap: 0.75rem; justify-content: center; margin-top: 1.5rem; flex-wrap: wrap; }
.order-panel { border: 1px solid var(--c-line); border-radius: var(--c-radius); padding: 1.5rem; }
/* `body ` ties a theme's `h2`/`p` margin reset at (0,1,2) and wins by being linked later —
   without it every panel heading (Tracking, a booking's service name, "Changed your mind?")
   rested on the box or button under it. `stacked-text` in `tests/system/geometry.py`. */
body .order-panel h2 { font-size: 1.1rem; margin-bottom: 1rem; }
body .order-panel p + p { margin-top: 0.75rem; }
body .card + p, body .order-panel + p { margin-top: 1rem; }
/* A form's own heading over its first field — "Send us a message" on the contact page. */
body :is(h2, h3) + .field { margin-top: 1rem; }
.order-panel + .order-panel { margin-top: 1.5rem; }
.order-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(10rem, 100%), 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.order-meta dt { font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-muted); margin-bottom: 0.2rem; }
.order-meta dd { margin: 0; font-weight: 600; }
.address-block { font-style: normal; line-height: 1.5; font-size: 0.95rem; }

/* ---- Returning something (apps/returns/templates/returns/request_return.html) --------------

   Shared markup no theme overrides, so it is styled once here — the arrangement the product
   sections, the course pages and the booking panel all arrived at. It was written in the
   *admin's* classes (`card`, `card-header`, `card-section`, `input-label`, `btn-primary`) with
   `#ddd` / `#666` / `#f5f5f5` inline beside them, so it rendered as browser defaults on all
   seven themes and its panels were mode-static grey in dark mode. Everything below is on the
   `--c-*` roles, which each theme re-points. */
.return-lines { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.return-item {
  border: 1px solid var(--c-line); border-radius: var(--c-radius-sm);
  padding: 1rem; display: grid; gap: 0.5rem;
}
.return-item-head { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.return-item-meta { margin: 0; color: var(--c-muted); font-size: 0.9rem; }
/* The quantity box is one number, not a full-width field: `.field > .input` is 100% wide, and
   a 12rem cap keeps it reading as "how many" rather than as another address line. */
.return-item-qty { margin-bottom: 0; max-width: 12rem; }
.form-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.5rem; }
.order-lines { list-style: none; margin: 0; padding: 0; }
.order-lines li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid var(--c-line); font-size: 0.95rem; }
.order-lines li:last-child { border-bottom: 0; }
.order-lines .qty-label { color: var(--c-muted); }
.download-box { padding: 1rem 1.25rem; border-radius: var(--c-radius-sm); background: var(--c-accent-soft); font-size: 0.925rem; }
.download-box ul { margin: 0.5rem 0 0; padding-left: 1.1rem; }
.tracking { display: grid; gap: 0.5rem; }
.tracking-item { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; padding: 0.75rem 1rem; border-radius: var(--c-radius-sm); background: var(--c-soft); font-size: 0.925rem; }

/* ---- Account --------------------------------------------------------------------------------
   A quiet list, not a filled pill: the current item is a 2px accent rule at the inline start and
   stronger ink, which reads as "you are here" without a block of colour competing with the seven
   other things a `--c-accent-soft` fill is used for elsewhere on the page (a chip, a discount
   banner). The rule sits on every row, transparent until it is the current one, so nothing shifts
   width when it lights up. "Sign out" is set apart below a hairline divider — it is not a fourth
   or fifth destination, it ends the list. */
.account-layout { display: grid; grid-template-columns: 14rem minmax(0, 1fr); gap: 2.5rem; align-items: start; }
.account-nav { display: flex; flex-direction: column; gap: 0.1rem; }
.account-nav a {
  padding: 0.55rem 0.9rem; border-radius: var(--c-radius-sm); text-decoration: none;
  color: var(--c-muted); font-weight: 500; border-inline-start: 2px solid transparent;
}
.account-nav a:hover { background: var(--c-soft); color: var(--c-text); }
.account-nav a[aria-current="page"] { border-inline-start-color: var(--c-accent); color: var(--c-text); font-weight: 600; }
.account-nav form { margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px solid var(--c-line); }
.account-nav button { width: 100%; text-align: left; padding: 0.55rem 0.9rem; border: 0; background: transparent; color: var(--c-muted); font: inherit; font-weight: 500; cursor: pointer; border-radius: var(--c-radius-sm); }
.account-nav button:hover { background: var(--c-soft); color: var(--c-text); }
.order-table { width: 100%; border-collapse: collapse; font-size: 0.925rem; }
.order-table th { text-align: left; font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-muted); padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--c-line); }
.order-table td { padding: 0.85rem 0.75rem; border-bottom: 1px solid var(--c-line); vertical-align: middle; }
.order-table a { font-weight: 600; text-decoration: none; }
.order-table a:hover { text-decoration: underline; }
.table-scroll { overflow-x: auto; }
.address-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr)); gap: 1rem; }
.address-card { border: 1px solid var(--c-line); border-radius: var(--c-radius); padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; }
.address-card .pill { align-self: start; }
.address-card .card-actions { display: flex; gap: 1rem; margin-top: auto; font-size: 0.9rem; }
.auth-card.wide { max-width: 32rem; }

/* ---- Responsive -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .commerce-grid { grid-template-columns: 1fr; gap: 2rem; }
  .summary-card { position: static; }
  /* `minmax(0, 1fr)`, not `1fr`. A grid track's minimum is `auto`, which means *min-content*,
     and `.table-scroll` cannot scroll while the track it is in is happy to grow — so the order
     table's 456px min-content became the width of the column, the column became wider than the
     page, and `/account/` scrolled sideways by 86px on a phone. The wide layout above already
     writes `minmax(0, 1fr)` for exactly this reason; the stacked one was the same declaration
     with the fix left off. It is the page builder's `min-height: 0` lesson turned on its side:
     **every ancestor between the scroller and the edge has to be allowed to be smaller than its
     content**, or the scroller never scrolls and the page does instead. */
  .account-layout { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
  /* The vertical list's inline-start rule has nowhere to sit in a row, so the current item is
     underlined instead — the same "you are here" mark `.text-link`'s tab-strip cousins use — and
     the whole thing scrolls sideways rather than wrapping onto several lines, which is what a
     six-entry menu did on a 360px phone before this. "Sign out" stops being a full-width button
     and becomes one more item in the row, in the same quiet ink as the rest, because a strip has
     no floor to set it apart below. */
  .account-nav {
    flex-direction: row; flex-wrap: nowrap; overflow-x: auto; gap: 0.25rem;
    -webkit-overflow-scrolling: touch; padding-bottom: 0.25rem;
  }
  .account-nav a {
    border-inline-start: 0; border-bottom: 2px solid transparent; white-space: nowrap;
  }
  .account-nav a[aria-current="page"] { border-bottom-color: var(--c-accent); }
  .account-nav form { margin-top: 0; padding-top: 0; border-top: 0; flex: none; }
  .account-nav button { width: auto; white-space: nowrap; }
}
@media (max-width: 600px) {
  .cart-line { grid-template-columns: 4rem minmax(0, 1fr); }
  .cart-line-price { grid-column: 2; text-align: left; }
  .form .field-row { grid-template-columns: 1fr; }
  .checkout-section { padding: 1.15rem; }
  .thanks .actions .btn { width: 100%; }
}

/* ---------------------------------------------------------------- header search
   Shared by every theme (partials/_search_form.html). Token-driven so it inherits the
   merchant's palette; `flex: 1` with a max-width is what centres it between the brand and
   the header actions without needing a grid the themes do not share.

   **The form is the box and the input is the text.** That is the contract, and for a long time
   it had two owners instead: this rule painted a bordered, rounded, `--color-surface` box, and
   every one of the seven themes painted a *second* box on `.site-search-input` inside it —
   which wins, being `body.theme-<name> .site-search-input` at (0,2,1). Measured in Chromium,
   what shipped was a 16px-radius rectangle around a 999px pill (harbour), 22px around 999px
   (petal), a full border wrapped around a bare underline field (press, studio), a doubled
   border on the attached submit button (depot), the magnifier stranded outside the pill on all
   six, and every theme's chosen width overridden by the `flex: 1` below.

   Worse on `voyage`, whose header is dark and whose field is deliberately a translucent wash on
   it: this rule's `--color-surface` ground went *under* that wash, so voyage's near-white input
   ink sat on near-white and **what you typed measured 1.0:1 — in both colour schemes**. The
   same shape as `--c-accent-soft` mixed in the wrong scope: markup and CSS both exactly as
   written, nothing empty, nothing 500ing, and only a browser can see it.

   So a theme restyles `.site-search` and leaves the input bare; a theme that wants a real
   attached submit button (depot) says so on `.site-search-button`.
   `tests/themes/test_search_box_browser.py` measures both halves on all seven, in both modes. */
.site-search {
  /* The suggestions panel is absolutely positioned against this. */
  position: relative;
  display: flex;
  align-items: center;
  gap: 0;
  flex: 1 1 auto;
  max-width: 26rem;
  margin-inline: auto;
  border: 1px solid var(--color-border, #9ca3af);
  border-radius: var(--c-radius, 8px);
  background: var(--color-surface, #fff);
  /* No `overflow: hidden`: it clipped nothing once the input stopped carrying its own corners,
     and it would clip the suggestions panel to the height of the box it drops out of. */
}
.site-search:focus-within {
  border-color: var(--color-accent, #4f46e5);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent, #4f46e5) 25%, transparent);
}
.site-search-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.5rem 0.75rem;
  border: 0;
  background: transparent;
  color: var(--color-ink, #0f172a);
  font: inherit;
  font-size: 0.95rem;
}
.site-search-input::placeholder { color: var(--color-ink-muted, #475569); opacity: 1; }
.site-search-input:focus { outline: none; }
/* Chrome's default clear button is invisible on a dark palette. */
.site-search-input::-webkit-search-cancel-button { filter: invert(0.5); }
.site-search-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 0.7rem;
  border: 0;
  background: transparent;
  color: var(--color-ink-muted, #475569);
  cursor: pointer;
}
/* Muted ink to the header's **own** ink, and `inherit` rather than a named token.
   It was `var(--color-accent)`: an accent is a *fill* colour and several themes' do not
   carry as small text (studio's mid red measures 3.98:1 on its own header). But naming
   `--color-ink` instead is the same mistake pointed the other way — vault's header is a
   dark band in *both* modes, so the page's ink is dark-on-dark there. `inherit` is the
   only answer that is right for a rule shared by seven headers: it takes whatever colour
   that header sets, which each theme has already had to get right. */
/* **Tint the ground, do not move the ink.** This rule is shared by seven headers and
   every named colour has a theme it is wrong on: `var(--color-accent)` is a *fill*
   colour that several accents cannot carry as small text (studio measured 3.98:1),
   `var(--color-ink)` is dark-on-dark wherever a header is a dark band in both modes
   (vault), and `inherit` picks up the wrong parent where the search sits in its own
   input. The ink at rest is already readable — whatever the theme made it — so the
   affordance is a tint of `currentColor` behind it, which is a tint of exactly that
   ink and therefore cannot collide with it. */
.site-search-button:hover { background: color-mix(in srgb, currentColor 14%, transparent); }
.site-search-icon { width: 20px; height: 20px; }

/* The results page's own box: the same component one size up, on its own line rather than
   squeezed between a brand and a row of icons. It used to be a hand-written `<form>` in each of
   six themes' `search.html`, wearing `.newsletter-form` — a *newsletter signup* layout — around
   an unclassed `<input type="search">`, with the margin inline-styled. Six copies of one control
   is how a seventh ships with none, and none of the six could have carried the panel below.

   Written `body .site-search.site-search-page`, and every part of that is load-bearing. A
   theme sizes its *header* box with `body.theme-<name> .site-search` at (0,2,1), so a bare
   `.site-search-page` (0,1,0) — and even `.site-search.site-search-page` (0,2,0) — loses to
   it, and the page box silently comes out at the header's 12–15rem. The leading `body` ties
   the specificity, and `commerce.css` is linked *after* the theme stylesheet, so it wins on
   order. Exactly why `.container.narrow` had to be written `body .container.narrow`. A theme
   that wants a different page box says `body.theme-<name> .site-search.site-search-page`,
   which press, petal and voyage do. */
body .site-search.site-search-page {
  max-width: 34rem;
  /* Aligned with the heading above it, which on five of the seven themes starts at the gutter.
     `.site-search` centres itself with `margin-inline: auto` because in a header it is a flex
     item between the brand and the icons; on a page it is an ordinary block and inheriting that
     would leave it floating under a left-aligned `<h1>`. voyage and petal centre their listing
     hero and centre this with it — a theme's alignment belongs in the theme. */
  margin-inline: 0;
  margin-block-start: 1.1rem;
}
/* Three classes, for the same reason and against `body.theme-<name> .site-search-input`. */
body .site-search.site-search-page .site-search-input { padding: 0.7rem 1rem; font-size: 1rem; }
body .site-search.site-search-page .site-search-button { padding: 0.7rem 0.9rem; }

/* On narrow screens the box drops onto its own row rather than crushing the brand. */
@media (max-width: 720px) {
  .site-search { order: 10; flex-basis: 100%; max-width: none; margin-block-start: 0.6rem; }
}
@media (prefers-reduced-motion: reduce) {
  .site-search, .site-search-button { transition: none; }
}

/* ------------------------------------------------- search suggestions and recent searches
   Built by `static/js/search.js`, which is why none of this markup is in a template: a
   `role="listbox"` with no script behind it promises a keyboard user something that will never
   open. Nothing here is a hidden state the page starts in — the panel does not exist until the
   script makes it — so a blocked or broken script leaves an ordinary GET form, which is the
   whole point (the `reveal.js` rule).

   Owned here rather than per theme for the reason the currency and language pickers are: a
   per-item rule is a thing the eighth theme forgets. A theme tweaks from `body.theme-<name>`. */
.site-search-panel {
  position: absolute;
  /* Anchored to the box's *end* edge and allowed to be wider than it, growing inwards. Five of
     the seven headers give the field 12–15rem, which is fine to type in and far too narrow to
     read a product name in: pinned to the box's width, "Enamel Camp Mug" came out as "Ena…" on
     voyage. Growing from the start edge instead would push it off the side of the page on the
     themes whose box sits hard right. */
  inset-inline-start: auto;
  inset-inline-end: 0;
  width: max(100%, 22rem);
  max-width: 92vw;
  top: calc(100% + 0.4rem);
  /* Above the header's own contents and anything sticky under it, below a modal. */
  z-index: 60;
  max-height: min(70vh, 26rem);
  overflow-y: auto;
  padding: 0.3rem;
  text-align: start;
  background: var(--color-surface, #fff);
  color: var(--color-ink, #0f172a);
  border: 1px solid var(--color-border, #9ca3af);
  border-radius: var(--c-radius, 8px);
  box-shadow: 0 14px 34px rgb(0 0 0 / 22%);
}
.site-search-group + .site-search-group { border-top: 1px solid var(--color-border, #9ca3af); }
.site-search-group-title {
  margin: 0.45rem 0.6rem 0.25rem;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-muted, #475569);
}
.site-search-options { list-style: none; margin: 0; padding: 0; }
.site-search-option {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.45rem 0.6rem;
  border-radius: calc(var(--c-radius, 8px) - 2px);
  cursor: pointer;
}
/* The highlight is a mix of the palette's *own* pair, never a colour of this file's choosing:
   the palettes guarantee ink-on-surface and guarantee nothing about anything else, which is how
   `--c-warn` on `--c-warn-bg` reached 1.51:1. */
.site-search-option.is-active {
  background: color-mix(in srgb, var(--color-ink, #0f172a) 9%, var(--color-surface, #fff));
}
.site-search-thumb {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  object-fit: cover;
  border-radius: calc(var(--c-radius, 8px) - 3px);
  background: color-mix(in srgb, var(--color-ink, #0f172a) 6%, var(--color-surface, #fff));
}
.site-search-option-body { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.site-search-option-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.site-search-option-meta {
  font-size: 0.8rem;
  color: var(--color-ink-muted, #475569);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.site-search-option-price {
  margin-inline-start: auto;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.site-search-recent-icon { color: var(--color-ink-muted, #475569); }
/* Deliberately the page's own ink rather than `--color-accent`: an accent is a colour to *fill*
   with and carries `--color-accent-ink` on it, and using it as text on the surface is the
   `--brand-600` / `--brand-ink` split the admin had to make. Weight and a rule do the work. */
.site-search-more { justify-content: center; font-weight: 600; }
.site-search-empty {
  margin: 0;
  padding: 0.9rem 0.6rem;
  text-align: center;
  color: var(--color-ink-muted, #475569);
}
.site-search-clear {
  display: block;
  width: 100%;
  margin-top: 0.2rem;
  padding: 0.5rem;
  border: 0;
  border-top: 1px solid var(--color-border, #9ca3af);
  background: transparent;
  color: var(--color-ink-muted, #475569);
  font: inherit;
  font-size: 0.82rem;
  text-align: center;
  cursor: pointer;
}
.site-search-clear:hover { color: var(--color-ink, #0f172a); }

/* ---------------------------------------------------------------- currency & language pickers
   `partials/_language_switcher.html` (and the currency picker, removed now that a shop prints
   its own prices and a shopper chooses a currency on the basket), beside the search box in
   every theme's header and overridden by none of them — the same shape as `.site-search` above,
   and it should have been owned here from the start. Instead six themes each wrote their own
   copy of the same four rules and the seventh, `summit`, wrote none: both pickers rendered as
   raw browser `<select>` controls — rounded, grey, system-font, native arrow — in the header of
   a theme built out of hairlines and a serif. A per-item rule is a thing you forget, and theme
   number eight would have forgotten it too.

   Deliberately *not* `appearance: none` here. Four of the six themes keep the native arrow and
   pad accordingly; suppressing it from this file would leave them with no arrow and no room for
   the replacement. A theme that wants the chevron draws it itself, as aurora and signal do.

   The `color` and `option` declarations matter beyond summit: atelier, bloom, campus and marquee
   all set `background: var(--color-surface)` on the select and no `color`, so the text was
   whatever the browser paints by default — near-black on a dark palette's dark surface. A select
   does not inherit `color` on its own, and the dropped-open list is painted by the OS, which
   knows nothing about the page's palette. */
.currency-switcher,
.language-switcher { position: relative; display: flex; align-items: center; gap: 0.4rem; }
/* `appearance: none` is the load-bearing line, and it was missing.
   Without it the *operating system* paints the widget — grey, rounded, system font, native
   arrow — whatever border, background and font the rules below ask for. So both pickers sat in
   the header of every theme looking like a macOS control on a page built out of hairlines and a
   serif, and `test_shared_controls_browser.py` reported them as styled the whole time: it asks
   whether the computed style *differs from a bare select*, and it does differ, in three
   properties that the OS then ignores. "Not identical to a default" is not "styled".
   `tests/themes/test_storefront_rendering_browser.py` asks the question that matters instead. */
.currency-select,
.locale-select {
  appearance: none;
  -webkit-appearance: none;
  padding: 0.4rem 1.85rem 0.4rem 0.6rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--c-radius-sm);
  background: var(--c-bg);
  color: var(--c-text);
  font: inherit;
  font-size: 0.85rem;
  line-height: 1.2;
  cursor: pointer;
}
/* The arrow the OS is no longer drawing. On the wrapping form rather than the select, because a
   `<select>` cannot carry a pseudo-element — and drawn in `currentColor` rather than as a data
   URI, so it follows the palette into dark mode instead of being a colour literal in a
   background image. */
.currency-switcher::after,
.language-switcher::after {
  content: "";
  position: absolute;
  right: 0.7rem;
  top: 50%;
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
  opacity: 0.7;
}
.currency-select:focus-visible,
.locale-select:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
.currency-select option,
.locale-select option { background: var(--c-bg); color: var(--c-text); }

/* The legal links row (`{% policy_links %}`), rendered by every theme's footer. It had no rule
   anywhere: unstyled, it sat outside the footer's own container and hard against the window
   edge. Nobody saw it because a new shop publishes no policies, and the cookie notice — which
   is always linked — is what made it visible. */
.footer-policies {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  /* Both sides: every theme rules a line along the top of `.footer-bottom`, which comes next,
     and with no bottom padding the links sat 2px above it on all twelve. */
  padding-block: 1rem;
  font-size: 0.9rem;
}

.footer-policies a { color: inherit; opacity: 0.75; }
.footer-policies a:hover { opacity: 1; }

/* Three more one-line rules that four of the seven themes happened to write and the rest did
   not, in shared markup none of them override: the footer's attribution line, the "12 products"
   count above a listing, and the sentence under a sign-up heading. Each rendered at full body
   size and full body ink on whichever themes had forgotten it. Baselines, so theme number eight
   inherits them; every theme that already has its own keeps it (all of theirs are scoped to
   `body.theme-<name>`, and this file is linked last). */
/* `--c-muted` is the *page's* muted ink, and three themes put this line in a footer painted
   with the ink colour — so it was page-grey on near-black: 1.96:1. It sits inside
   `.footer-bottom`, which every theme colours, so inheriting is both correct and the only
   value that follows a theme into an inverted footer. Found by axe-core, because the
   hand-rolled sweep was skipping the whole footer as 'deliberately dimmed'. */
.powered-by { margin-top: 0.35rem; font-size: 0.8rem; color: inherit; }
.listing-count { margin: 0; font-size: 0.9rem; color: var(--c-muted); }
.auth-sub { color: var(--c-muted); margin-bottom: 1.5rem; }

/* ---- Product page sections --------------------------------------------------------------

   The description, the course panel, the venue and the reviews — everything below the picture
   and the buy button. The templates (`partials/_product_sections.html`, `_course_panel.html`,
   `reviews/_reviews.html`) are shared by every theme and overridden by none, so their styling
   lives here rather than being re-written six times: one component, one owner, the same
   arrangement cart and checkout already have. Themes steer it through the `--c-*` tokens they
   already re-point, and may override with `body.theme-<name> …` (higher specificity) where the
   look demands it.

   Before this block none of these classes were styled anywhere at all: the reviews sat inside
   the buy column as unstyled headings, a browser-default `<ol>` and a bare form. */
.product-sections {
  margin-top: clamp(2.5rem, 6vw, 4.5rem);
  display: grid;
  gap: clamp(2rem, 5vw, 3.5rem);
}
/* Most themes lay the sections into their product grid, so a booking panel that runs long
   scrolls beside the description and the reviews rather than beside an empty column. There the
   grid's own gap is the space above them, not this margin as well. `body ` is load-bearing:
   several themes set that margin from `body.theme-<name> .product-sections` at (0,2,1). */
body .product-layout > .product-sections { margin-top: 0; min-width: 0; }
.product-section { scroll-margin-top: 5rem; }
/* A merchant's own datasheet section (Moldings, Care). It is *not* `.product-info`: that is the buy
   column's class, every theme positions it and studio makes it sticky, so a section wearing it
   slid over the one below as the page scrolled. Nothing to paint — `.product-section` is the look —
   and the rule exists so the class sweep sees the name is defined on purpose. */
.product-extra { }
/* A hairline between sections, not a rule: `--c-line` is a full-strength border on several themes
   (studio's is a mid grey), which drew a hard line across the column directly over a small heading. */
.product-section + .product-section {
  padding-top: clamp(1.5rem, 4vw, 2.5rem);
  border-top: 1px solid color-mix(in srgb, var(--c-line) 40%, transparent);
}
.product-section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.5rem 1.5rem; flex-wrap: wrap; margin-bottom: 1.25rem;
}
.product-section-title { font-size: clamp(1.2rem, 2.4vw, 1.6rem); margin: 0; }
.product-section-body { max-width: 68ch; }
.product-description { line-height: 1.7; }
.product-description > * + * { margin-top: 0.9rem; }
.product-description h2, .product-description h3 { font-size: 1.05rem; margin-top: 1.5rem; }
.product-description ul, .product-description ol { padding-left: 1.25rem; }

/* The specification list: the facts a shopper checks rather than reads. A row hides itself when
   Alpine finds nothing to put in it (an unselected variant has no SKU), so this never prints a
   label with an empty value — which is exactly what the old stray "SKU" line did. */
.product-specs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), max-content));
  gap: 0.75rem 2.5rem; margin: 1.5rem 0 0; padding-top: 1.25rem;
  border-top: 1px solid color-mix(in srgb, var(--c-line) 40%, transparent);
}
.product-spec dt { font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-muted); }
.product-spec dd { margin: 0.2rem 0 0; font-weight: 600; }

/* A specification table and the room scenes: the shared markup of `_product_sections.html`.
   `body ` on the cell rules is load-bearing for the reason it always is here — every theme resets
   `th`, `td` and `p` from `body.theme-<name>` at (0,1,2) or above. */
.spec-table { width: 100%; max-width: 44rem; border-collapse: collapse; }
body .spec-table th, body .spec-table td { padding: 0.65rem 0.75rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--c-line); overflow-wrap: anywhere; }
body .spec-table th { width: 38%; font-weight: 600; color: var(--c-muted); }
.spec-table tr:first-child th, .spec-table tr:first-child td { border-top: 1px solid var(--c-line); }
.room-scenes { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr)); }
.room-scene figure { margin: 0; }
.room-scene img { display: block; width: 100%; height: auto; border-radius: var(--c-radius); }
body .room-scene figcaption { margin-top: 0.4rem; font-size: 0.85rem; color: var(--c-muted); }

/* ---- Course panel ------------------------------------------------------------------------ */
.course-facts { color: var(--c-muted); font-size: 0.9rem; margin: 0; }
.course-outcomes { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.6rem; }
.course-outcomes li { display: flex; gap: 0.6rem; align-items: baseline; }
.course-outcomes li::before { content: "✓"; color: var(--c-accent); font-weight: 700; flex: none; }
body p.course-cta { margin: 1.5rem 0 0; }

/* ---- Reviews ------------------------------------------------------------------------------ */
.product-reviews { display: grid; gap: 0; }
.reviews-count { color: var(--c-muted); font-size: 0.9rem; margin: 0; }
.reviews-empty { margin: 0; }
.reviews-summary {
  display: flex; flex-wrap: wrap; align-items: center; gap: clamp(1.25rem, 4vw, 3rem);
  padding: 1.25rem 1.5rem; border: 1px solid var(--c-line); border-radius: var(--c-radius);
  background: var(--c-soft);
}
.reviews-score { display: flex; flex-direction: column; gap: 0.25rem; }
.reviews-average { font-size: 2.5rem; font-weight: 700; line-height: 1; margin: 0; font-variant-numeric: tabular-nums; }
.reviews-score .stars-count { display: none; }
.stars { letter-spacing: 0.1em; color: var(--c-accent); }
.star-empty { opacity: 0.3; }
.star-half { opacity: 0.6; }
.stars-count { color: var(--c-muted); font-size: 0.9rem; margin-left: 0.4rem; }

/* Capped, not `flex: 1` — a bar stretched across a 74rem page reads as a progress meter for the
   whole shop rather than a share of four reviews. */
.rating-breakdown { list-style: none; margin: 0; padding: 0; flex: 1 1 16rem; max-width: 26rem; display: grid; gap: 0.3rem; }
.rating-row { display: grid; grid-template-columns: 2.5rem 1fr 2rem; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.rating-row-score { color: var(--c-muted); white-space: nowrap; }
.rating-row-count { text-align: right; color: var(--c-muted); font-variant-numeric: tabular-nums; }
.rating-bar { display: block; height: 0.5rem; border-radius: 999px; background: var(--c-line); overflow: hidden; }
.rating-bar-fill { display: block; height: 100%; background: var(--c-accent); }

.review-list { list-style: none; margin: 1.75rem 0 0; padding: 0; display: grid; gap: 1.5rem; }
.review { padding-bottom: 1.5rem; border-bottom: 1px solid var(--c-line); }
.review:last-child { padding-bottom: 0; border-bottom: 0; }
.review-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; }
.review-author { display: flex; align-items: center; gap: 0.6rem; margin: 0; }
.review-rating { margin: 0; color: var(--c-accent); letter-spacing: 0.1em; }
body h3.review-title { font-size: 1rem; margin: 0.6rem 0 0; }
body p.review-body { margin: 0.4rem 0 0; line-height: 1.65; max-width: 68ch; }
.review-date { margin: 0.5rem 0 0; font-size: 0.8rem; color: var(--c-muted); }
.review-reply {
  margin-top: 0.9rem; padding: 0.9rem 1.1rem; border-radius: var(--c-radius-sm);
  background: var(--c-soft); border-left: 3px solid var(--c-accent); font-size: 0.925rem;
}
.review-reply-head { margin: 0 0 0.3rem; }
.review-reply p + p { margin-top: 0.4rem; }

.review-form { margin-top: 2rem; padding-top: 1.75rem; border-top: 1px solid var(--c-line); max-width: 34rem; }
body h3.review-form-title { font-size: 1.05rem; margin: 0 0 1rem; }
.rating-input { border: 0; padding: 0; margin: 0 0 1.25rem; }
.rating-input legend { padding: 0; font-size: 0.9rem; font-weight: 600; margin-bottom: 0.5rem; }
.rating-input-values { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.rating-star { display: inline-flex; }
.rating-star input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.rating-star span {
  display: inline-flex; align-items: center; gap: 0.1rem; padding: 0.4rem 0.85rem;
  border: 1px solid var(--c-line-strong); border-radius: 999px; cursor: pointer; font-size: 0.9rem;
}
.rating-star input:checked + span { background: var(--c-accent); color: var(--c-bg); border-color: var(--c-accent); }
.rating-star input:focus-visible + span { outline: 2px solid var(--c-accent); outline-offset: 2px; }
/* Same separation as the form it stands in for — without it "Sign in to leave a review" reads
   as one more line of the last review. */
/* `body p.` because this is a `<p>`, and every theme's `body.theme-<name> p { margin: 0 }` is
   (0,1,2): the one-class rule lost, and so did `body .review-blocked` at (0,1,1). The "no reviews
   yet" line rested on this one's top border on all twelve themes. */
body p.review-blocked { margin: 2rem 0 0; padding-top: 1.75rem; border-top: 1px solid var(--c-line); }

/* ---- Learning: the shelf, the curriculum, the lesson --------------------------------------
   `storefront/learning/{my_courses,player,lesson}.html` are shared templates that no theme
   overrides, so — like the product sections and the review furniture above — commerce.css owns
   their styling for all six themes and a theme tweaks them only from `body.theme-<name>`.

   They were written against the *admin* design system: `.card`, `.badge`, `.empty` and
   `.progress` are all defined in `static/css/dashboard.css`, which a storefront page never
   loads, and `.progress-bar` existed in no stylesheet in the project at all. So every course
   page rendered as browser defaults — an unstyled `<ol>` for the curriculum, a bare `<span>`
   where the progress meter should be. The templates now use the storefront's own vocabulary
   (`.order-panel`, `.pill`, `.empty-state`, `.course-outcomes`) and everything genuinely new to
   a course page is defined below. */

/* The meter. A `<span>` with a width, so it needs `display: block` to have a height at all —
   which is why the old markup showed nothing whatsoever rather than merely showing it plain. */
.progress {
  display: block; height: 0.5rem; margin: 0.75rem 0 0.4rem; overflow: hidden;
  border-radius: 999px; background: var(--c-line);
}
.progress-bar { display: block; height: 100%; background: var(--c-accent); border-radius: inherit; }

/* "My learning". Cards, not rows: the shelf is the first thing a learner sees after paying. */
.course-shelf {
  list-style: none; margin: 0; padding: 0; display: grid; gap: 1.25rem;
  grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr));
}
.course-card {
  display: flex; flex-direction: column; gap: 0.35rem; padding: 1.5rem;
  border: 1px solid var(--c-line); border-radius: var(--c-radius); background: var(--c-bg);
}
.course-card h2 { font-size: 1.15rem; margin: 0; }
.course-card h2 a { text-decoration: none; color: inherit; }
.course-card h2 a:hover { color: var(--c-accent); }
.course-card p { margin: 0; }
/* Pushed to the foot so buttons line up across cards whose blurbs differ in length. */
.course-card .btn { margin-top: auto; align-self: flex-start; }

/* "My bookings". An appointment is one line of fact, so it is a stacked list rather than the
   card grid the shelf uses — but it is styled here, by the stylesheet the storefront loads,
   for the same reason the course pages are. */
.booking-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.booking-list h2 { font-size: 1.05rem; margin: 0 0 0.35rem; }
.booking-list h2 a { text-decoration: none; color: inherit; }
.booking-list h2 a:hover { color: var(--c-accent); }
.booking-list p { margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
/* A reservation's row names the booking in an <h3> and the groups of rows ("Coming up",
   "Earlier") are <h2>s. Every theme resets headings to `margin: 0` at (0,1,2), so these are
   written `body …` or they never apply. */
body .booking-list h3 { font-size: 1.05rem; margin: 0 0 0.5rem; }
body .booking-list h3 a { text-decoration: none; color: inherit; }
body .booking-list h3 a:hover { color: var(--c-accent); }
body h2.booking-group { margin: 0 0 0.75rem; }
body .booking-list + h2.booking-group { margin-top: 1.75rem; }

/* The course page's head: what it is, what it is about, the facts, then one box to act from.
   The headings and paragraphs here are written `body p.x` / `body .x h1` on purpose: every theme
   resets `p` and `h1`–`h4` to `margin: 0` at (0,1,2), which beats a one-class rule. That is how
   the title, subtitle, facts and first module used to sit in one undivided stack. */
.course-head {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.5rem 3rem; align-items: start;
  margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid var(--c-line);
}
@media (min-width: 760px) {
  .course-head { grid-template-columns: minmax(0, 1fr) minmax(15rem, 18rem); }
}
body p.course-eyebrow {
  margin: 0 0 0.65rem; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--c-muted);
}
body .course-head h1 { margin: 0; font-size: clamp(2rem, 4.5vw, 3rem); }
body p.course-lead { margin: 0.85rem 0 0; max-width: 40rem; font-size: clamp(1.1rem, 2vw, 1.3rem); color: var(--c-muted); }
.course-meta { list-style: none; margin: 1.35rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.course-meta li { padding: 0.3rem 0.8rem; border: 1px solid var(--c-line); border-radius: 999px; font-size: 0.85rem; }
/* One place to act from: the price and Enrol for a visitor, progress and the next lesson for a
   learner. A grid with a gap, so nothing in it depends on a margin a theme resets. */
.course-action {
  display: grid; gap: 0.75rem; padding: 1.25rem;
  border: 1px solid var(--c-line); border-radius: var(--c-radius);
}
body .course-action p { margin: 0; }
body p.course-price { font-size: 1.6rem; font-weight: 700; line-height: 1.2; }
.course-action .progress { margin: 0.25rem 0 0; }
.course-try { justify-self: center; font-size: 0.9rem; }

/* The curriculum. A module is a part of a syllabus, so it gets air above it rather than a box
   inside the box it already sits in. */
body p.course-note {
  margin: 0 0 1.75rem; padding: 0.75rem 1rem; font-size: 0.925rem; color: var(--c-text);
  background: var(--c-soft); border-left: 3px solid var(--c-accent); border-radius: var(--c-radius-sm);
}
.module + .module { margin-top: 1.75rem; padding-top: 1.75rem; border-top: 1px solid var(--c-line); }
.module-head { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 0.25rem 1rem; }
body .module h3 { font-size: 1.15rem; margin: 0; }
body p.module-summary { margin: 0.35rem 0 0; font-size: 0.925rem; color: var(--c-muted); }
/* `.pill` capitalises because it usually holds a status word; here it holds a phrase, and
   "Not On Your Route" reads as a shout. */
body .lesson-row .pill { text-transform: none; }

.lesson-list { list-style: none; margin: 0.75rem 0 0; padding: 0; display: grid; gap: 0.15rem; }
.lesson-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 0.75rem;
  padding: 0.7rem 0.85rem; border-radius: var(--c-radius-sm);
}
.lesson-row:hover { background: var(--c-soft); }
.lesson-row > a { font-weight: 600; text-decoration: none; }
.lesson-row > a:hover { text-decoration: underline; }
/* The lesson kind ("Video", "Quiz") is the last thing on the row and the least important:
   pushed right so the titles stay a readable column down the left. */
.lesson-row > .c-small:last-child { margin-left: auto; color: var(--c-muted); }
/* A locked lesson is still listed — knowing what is coming is most of why the page exists —
   but it must not look clickable. */
.lesson-locked { color: var(--c-muted); }
.lesson-row:has(.lesson-locked):hover { background: none; }

/* The lesson itself. A measure, because this is the one storefront page that is genuinely read
   rather than scanned. */
.lesson-body { line-height: 1.7; }
.lesson-body > * + * { margin-top: 1rem; }
.lesson-body p, .lesson-body ul, .lesson-body ol { max-width: 68ch; }
.lesson-body ul, .lesson-body ol { padding-left: 1.25rem; }
.lesson-body img { max-width: 100%; height: auto; }
.lesson-body h2, .lesson-body h3 { font-size: 1.1rem; margin-top: 1.75rem; }
.lesson-body img { border-radius: var(--c-radius-sm); }
/* A lesson's table — imported ones are common (a first-aid course's vital signs), and the admin
   editor can now write one. Hairlines from the palette, so it follows light and dark. */
body .lesson-body table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
body .lesson-body th, body .lesson-body td { border: 1px solid var(--c-line); padding: 0.45rem 0.65rem; text-align: left; vertical-align: top; overflow-wrap: anywhere; }
body .lesson-body td p { margin: 0; }

.video-embed { position: relative; aspect-ratio: 16 / 9; border-radius: var(--c-radius-sm); overflow: hidden; background: var(--c-soft); }
.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* "Still being prepared" is a status, not body copy — it should not read as the lesson. */
.video-status {
  margin: 0 0 1rem; padding: 0.75rem 1rem; border-radius: var(--c-radius-sm);
  background: var(--c-soft); color: var(--c-muted); font-size: 0.925rem;
}

.quiz { margin-top: 1.5rem; }
.quiz h2 { font-size: 1.15rem; margin: 0 0 0.4rem; }
.quiz .btn { margin-top: 1.5rem; }
.quiz-question { border: 0; margin: 1.5rem 0 0; padding: 0; }
.quiz-question legend { padding: 0; margin-bottom: 0.6rem; font-weight: 600; }
.quiz-question + .quiz-question { padding-top: 1.5rem; border-top: 1px solid var(--c-line); }
/* A question's own content (a picture, a paragraph of context) and a hint on request. The
   `body` prefix is load-bearing: every theme resets `p` margins at (0,1,2). */
body .quiz-question-body { margin: 0 0 0.75rem; }
body .quiz-question-body img { max-width: 100%; height: auto; }
body .quiz-hint { margin: 0 0 0.75rem; }
body .quiz-hint summary { cursor: pointer; color: var(--c-muted); }
body .quiz-hint p { margin: 0.4rem 0 0; }

/* The author's quiz view: answers marked, nothing to submit. `.quiz-question` above is a
   fieldset for the learner and a plain div here, so the prompt needs its own weight. */
.quiz-prompt { font-weight: 600; margin: 0 0 0.6rem; }
.quiz-answers { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; }
.quiz-answers li { color: var(--c-muted); }
.quiz-answers li.is-correct { color: var(--c-text); font-weight: 600; }
.quiz-answers li.is-correct::before { content: "✓"; color: var(--c-accent); margin-right: 0.4rem; }

/* The marked attempt, worked through. The signal is carried by a left border and the pill, not
   by colouring the text: `--c-ok` on `--c-soft` is the pairing that measures 2.74:1–3.85:1
   across the palettes, and a learner reading why they got something wrong is reading body copy. */
.quiz-review { margin-top: 1.75rem; padding-top: 1.5rem; border-top: 1px solid var(--c-line); }
.quiz-review h3 { font-size: 1rem; margin: 0 0 1rem; }
.quiz-review-row { padding: 0.85rem 1rem; border-left: 3px solid var(--c-line); background: var(--c-soft); border-radius: var(--c-radius-sm); }
.quiz-review-row + .quiz-review-row { margin-top: 0.75rem; }
.quiz-review-row.is-correct { border-left-color: var(--c-ok); }
.quiz-review-row.is-wrong { border-left-color: var(--c-danger); }
.quiz-review-row .quiz-prompt { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.quiz-answers li.is-picked { color: var(--c-text); }
.quiz-explanation { margin: 0.6rem 0 0; font-size: 0.925rem; }

/* Starting again. Set apart from the curriculum below it, because it throws work away and a
   button that does that should not sit flush against the list of lessons. */
.retake-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin: 1.5rem 0; }

/* Work a person marks. The state a learner most needs is "where has my work got to", so each
   attempt is a block carrying its own status and the marker's words. */
.assignment { margin-top: 1.5rem; }
.assignment h2 { font-size: 1.15rem; margin: 0 0 0.75rem; }
.assignment form { margin-top: 1.25rem; }
.submission { padding: 0.85rem 1rem; margin-top: 0.75rem; border: 1px solid var(--c-line); border-radius: var(--c-radius-sm); background: var(--c-soft); }
.submission-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0; }
.submission-text { margin-top: 0.6rem; }
.submission-text > *:last-child { margin-bottom: 0; }
/* The feedback is the part they came back for, so it is set apart from their own words. */
.submission-feedback { margin: 0.75rem 0 0; padding-left: 0.85rem; border-left: 3px solid var(--c-accent); }
.submission-feedback > *:last-child { margin-bottom: 0; }

/* The clock. Reported, not kept — `QuizAttempt.started_at` is the authority — so this is a
   status line rather than a control, and it carries its signal in a border for the reason
   `.alert` does: `--c-warn` on `--c-warn-bg` measures 2.74:1–3.85:1 across the palettes. */
.quiz-clock {
  margin: 0.75rem 0 0; padding: 0.6rem 0.85rem; font-size: 0.925rem;
  border: 1px solid var(--c-line); border-left: 3px solid var(--c-warn);
  border-radius: var(--c-radius-sm); background: var(--c-soft); color: var(--c-text);
}

/* The four question kinds a list of radio buttons cannot ask. All plain form controls on
   purpose: drag-and-drop needs JavaScript to be answerable at all, and a quiz that cannot be sat
   without it is a quiz some learners cannot sit. */
.blank-list, .sort-list, .match-list { list-style: none; margin: 0.75rem 0 0; padding: 0; display: grid; gap: 0.6rem; }
.blank-list .field { margin: 0; }
.sort-list li, .match-list li { display: flex; align-items: center; gap: 0.75rem; }
.match-list li > span:first-child { flex: 1 1 45%; }
.match-list select { flex: 1 1 45%; }
.input-narrow { width: 5rem; flex: none; }

/* What they wrote beside what was wanted. Two columns where there is room, because comparing
   them is the whole point and a wrong answer stacked above the right one reads as a list. */
.quiz-given { display: grid; gap: 1rem; margin-top: 0.5rem; }
@media (min-width: 40rem) { .quiz-given:has(> * + *) { grid-template-columns: 1fr 1fr; } }
.quiz-given p { margin: 0 0 0.3rem; }

/* A fork. Full-width buttons stacked, because these are the sentences the learner is choosing
   between and a row of them would be read as a toolbar. */
.lesson-choices { margin-top: 1.5rem; }
.lesson-choices h2 { font-size: 1.15rem; margin: 0 0 0.75rem; }
.lesson-choice-list { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.6rem; }
.lesson-choice-list form { margin: 0; }
/* A choice offered to somebody who cannot take it (not enrolled, or an arm that ends the
   course while previewing) is still worth showing — it is what they are buying. */
.lesson-choice-list .is-disabled { opacity: 0.6; cursor: default; text-align: center; }

/* Where a quiz leads, shown to an author previewing. */
.lesson-routes { margin-top: 1.5rem; }
.lesson-routes h2 { font-size: 1.15rem; margin: 0 0 0.75rem; }
.lesson-route-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.lesson-route-list li { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem; }

body p.lesson-ending { margin-top: 1.5rem; }
/* A lesson the learner's answers have routed them past. Listed, because knowing the course has
   more in it is most of why the curriculum page exists, but plainly not theirs to work through. */
.lesson-off-route { opacity: 0.65; }

/* Staff previewing a course. A band rather than the fixed bar the theme preview uses: this one
   belongs to a page, not to the whole shop, and a course page is read top to bottom. */
.course-preview {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 0.75rem; margin-bottom: 1.5rem; padding: 0.75rem 1rem;
  border: 1px solid var(--c-line); border-left: 3px solid var(--c-accent);
  border-radius: var(--c-radius-sm); background: var(--c-soft); color: var(--c-text);
  font-size: 0.925rem;
}
.course-preview-note { margin: 0; }
.course-preview-stop { font-weight: 600; }

.lesson-actions { margin-top: 1.5rem; }
/* `justify-content: space-between` alone would centre a lone "next" link; the margin keeps
   "next" on the right when there is no "previous" beside it. */
.lesson-nav {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem;
  margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--c-line);
}
.lesson-nav a { font-weight: 600; text-decoration: none; }
.lesson-nav a:hover { text-decoration: underline; }
.lesson-nav a:only-child:last-child { margin-left: auto; }

@media (max-width: 640px) {
  .reviews-summary { gap: 1rem; padding: 1rem; }
  .product-specs { gap: 0.75rem 1.5rem; }
  .course-card { padding: 1.25rem; }
  .lesson-row { padding: 0.6rem 0.5rem; }
  .lesson-row > .c-small:last-child { margin-left: 0; flex-basis: 100%; }
}

/* ------------------------------------------------------------------ tours & experiences

   The experience partials (`apps/events/templates/events/_experience_*.html`,
   `_booking_panel.html` and `experience_list.html`) are
   shared markup that **no theme overrides**, so they are styled here for all seven — the same
   arrangement the product sections, the review furniture and the course pages arrived at, and
   for the same reason: a component with no owner is a component nobody styles.

   Everything below is written against the `--c-*` tokens, which each theme re-points from
   `body.theme-<name>`. No literal colour appears here except the grade bars' "off" state, which
   is a mix of the ink rather than a hex. A theme tweaks these from `body.theme-<name> …` —
   this file is linked *after* the theme stylesheet, so a bare class of equal specificity in a
   theme is a rule that exists and never applies. */

/* The fact band: duration, grade, group size, languages, ages. Printed as data because that is
   what somebody deciding whether they can do this actually reads. */
.evt-guide-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(7.5rem, 100%), 1fr));
  gap: 0.25rem 1.25rem; margin: 0 0 1.25rem; padding: 0.9rem 0;
  border-top: 1px solid var(--c-line); border-bottom: 1px solid var(--c-line);
}
.evt-guide-fact { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.evt-guide-fact dt {
  font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--c-muted); font-weight: 600;
}
.evt-guide-fact dd { margin: 0; font-weight: 600; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.evt-guide-grade { display: inline-flex; gap: 2px; align-items: flex-end; }
.evt-guide-grade-bar {
  display: block; width: 4px; height: 8px; border-radius: 1px;
  background: color-mix(in srgb, var(--c-text) 18%, transparent);
}
.evt-guide-grade-bar:nth-child(2) { height: 11px; }
.evt-guide-grade-bar:nth-child(3) { height: 14px; }
.evt-guide-grade-bar:nth-child(4) { height: 17px; }
.evt-guide-grade-bar.is-on { background: var(--c-accent); }
.evt-guide-grade-label { font-weight: 600; }
.evt-guide-grade-note { margin: -0.75rem 0 1.25rem; color: var(--c-muted); font-size: 0.9rem; }

/* The booking panel. It replaces the theme's quantity box, so it has to read as the buy
   control on every theme rather than as another content block. */
.evt-book {
  border: 1px solid var(--c-line-strong); border-radius: var(--c-radius);
  background: var(--c-soft); padding: 1.25rem; display: grid; gap: 1rem;
}
.evt-book-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.evt-book-title { margin: 0; font-size: 1.1rem; }
.evt-book-from { margin: 0; display: flex; align-items: baseline; gap: 0.35rem; }
.evt-book-from-label { color: var(--c-muted); font-size: 0.85rem; }
.evt-book-from-price { font-size: 1.35rem; font-weight: 700; }
.evt-book-form { display: grid; gap: 1rem; margin: 0; }
.evt-book-field { display: grid; gap: 0.35rem; }
/* A `<legend>` is not a grid item of its fieldset, so the field's `gap` never reached it: the
   "Who is coming?" label sat on the first ticket row's rule. */
.evt-book-field-label { font-weight: 600; font-size: 0.9rem; padding: 0; margin-bottom: 0.5rem; }
.evt-book-select {
  width: 100%; padding: 0.6rem 0.7rem; border-radius: var(--c-radius-sm);
  border: 1px solid var(--c-line-strong); background: var(--c-bg); color: var(--c-text);
  font: inherit;
}
.evt-book-pax, .evt-book-addons { border: 0; padding: 0; margin: 0; display: grid; gap: 0.5rem; }
.evt-book-pax-row {
  display: grid; grid-template-columns: 1fr auto 4.5rem; align-items: center; gap: 0.75rem;
  padding: 0.5rem 0; border-top: 1px solid var(--c-line);
}
.evt-book-pax-who { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.evt-book-pax-name { font-weight: 600; }
.evt-book-pax-age, .evt-book-pax-note { font-size: 0.82rem; color: var(--c-muted); }
.evt-book-pax-price { font-weight: 600; white-space: nowrap; }
.evt-book-pax-qty {
  width: 100%; padding: 0.45rem 0.5rem; text-align: center; font: inherit;
  border-radius: var(--c-radius-sm); border: 1px solid var(--c-line-strong);
  background: var(--c-bg); color: var(--c-text);
}
.evt-book-addon-row .evt-book-pax-name { font-weight: 500; }
.evt-book-total {
  display: flex; align-items: baseline; justify-content: space-between;
  padding-top: 0.75rem; border-top: 2px solid var(--c-line-strong); font-weight: 700;
}
.evt-book-total-value { font-size: 1.25rem; }
.evt-book-btn { width: 100%; }
.evt-book-terms { margin: 0; font-size: 0.8rem; color: var(--c-muted); }
.evt-book-enquiry, .evt-book-empty { display: grid; gap: 0.75rem; }
.evt-book-enquiry p, .evt-book-empty p { margin: 0; }
.evt-book-enquiry-btn { justify-self: start; }
.evt-book-private { margin: 0; font-size: 0.85rem; color: var(--c-muted); }

/* The content sections. They sit inside `.product-sections`, so they inherit its rhythm and
   only declare what is genuinely theirs. */
.evt-guide-section .product-section-title { margin-top: 0; }
/* A field-guide section ends in a bare list, and the next section opens with its rule: without
   this the last highlight sat 3px above that line. */
.evt-guide-section { padding-bottom: 1.5rem; }
.evt-guide-highlights, .evt-guide-bring { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.5rem; }
.evt-guide-highlights li, .evt-guide-bring li { position: relative; padding-left: 1.4rem; }
.evt-guide-highlights li::before, .evt-guide-bring li::before {
  content: ""; position: absolute; left: 0; top: 0.55em;
  width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c-accent);
}
.evt-guide-itinerary { margin: 0; padding: 0; list-style: none; display: grid; gap: 0; }
.evt-guide-stop { position: relative; padding: 0 0 1.25rem 1.75rem; border-left: 2px solid var(--c-line); }
.evt-guide-stop:last-child { border-left-color: transparent; padding-bottom: 0; }
.evt-guide-stop::before {
  content: ""; position: absolute; left: -0.4rem; top: 0.35rem;
  width: 0.7rem; height: 0.7rem; border-radius: 50%;
  background: var(--c-bg); border: 2px solid var(--c-accent);
}
.evt-guide-stop-day {
  margin: 0 0 0.5rem; font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--c-muted); font-weight: 700;
}
.evt-guide-stop-body { display: grid; gap: 0.25rem; }
.evt-guide-stop-time { font-size: 0.8rem; font-weight: 700; color: var(--c-accent); }
.evt-guide-stop-title { margin: 0; font-size: 1rem; }
.evt-guide-stop-text { margin: 0; color: var(--c-muted); }
.evt-guide-includes { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); gap: 1.5rem; }
/* `body h3.`: "Included" sat directly on its first line on voyage, whose `h3 { margin: 0 }` is
   (0,1,2) and beat this rule's one class. */
body h3.evt-guide-include-title { margin: 0 0 0.5rem; font-size: 0.95rem; }
.evt-guide-include-col ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.4rem; }
.evt-guide-include-col li { position: relative; padding-left: 1.5rem; }
.evt-guide-include-yes li::before {
  content: "\2713"; position: absolute; left: 0; color: var(--c-ok); font-weight: 700;
}
.evt-guide-include-no li::before {
  content: "\2715"; position: absolute; left: 0; color: var(--c-danger); font-weight: 700;
}
.evt-guide-include-no li { color: var(--c-muted); }
/* `body p`: paragraphs, so the theme reset beat the bare classes and the meeting time sat on the
   venue box under it. */
body p:is(.evt-guide-fitness, .evt-guide-meeting, .evt-guide-meeting-time, .evt-guide-pickup) { margin: 0 0 0.75rem; }
.evt-guide-meeting-time, .evt-guide-pickup { color: var(--c-muted); font-size: 0.9rem; }
.evt-guide-faqs { display: grid; gap: 0.5rem; }
.evt-guide-faq { border: 1px solid var(--c-line); border-radius: var(--c-radius-sm); background: var(--c-bg); }
.evt-guide-faq-q { padding: 0.75rem 1rem; cursor: pointer; font-weight: 600; list-style: none; }
.evt-guide-faq-q::-webkit-details-marker { display: none; }
.evt-guide-faq-q::after { content: "+"; float: right; color: var(--c-muted); font-weight: 400; }
.evt-guide-faq[open] .evt-guide-faq-q::after { content: "\2013"; }
.evt-guide-faq-a { padding: 0 1rem 0.9rem; color: var(--c-muted); }
.evt-guide-faq-a > :first-child { margin-top: 0; }

/* `/experiences/` — the index. */
.evt-list { padding: 2rem 0 3rem; }
.evt-list-head { margin-bottom: 1.5rem; }
body .evt-list-head h1 { margin: 0 0 0.4rem; }
.evt-list-lead { margin: 0; color: var(--c-muted); max-width: 46ch; }
.evt-list-filters {
  display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: end;
  padding: 1rem; margin-bottom: 1.5rem;
  border: 1px solid var(--c-line); border-radius: var(--c-radius); background: var(--c-soft);
}
.evt-list-filter { display: grid; gap: 0.3rem; }
.evt-list-filter label { font-size: 0.78rem; font-weight: 600; color: var(--c-muted); }
.evt-list-filter select {
  padding: 0.5rem 0.6rem; border-radius: var(--c-radius-sm); font: inherit;
  border: 1px solid var(--c-line-strong); background: var(--c-bg); color: var(--c-text);
}
.evt-list-filter-clear { align-self: center; color: var(--c-muted); font-size: 0.9rem; }
.evt-list-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr)); gap: 1.5rem;
}
.evt-list-card {
  border: 1px solid var(--c-line); border-radius: var(--c-radius); overflow: hidden;
  background: var(--c-bg); display: flex;
}
.evt-list-card-link { display: flex; flex-direction: column; width: 100%; text-decoration: none; color: inherit; }
.evt-list-card-media { position: relative; aspect-ratio: 4 / 3; background: var(--c-soft); overflow: hidden; }
.evt-list-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.evt-list-card-media-empty { display: block; width: 100%; height: 100%; }
.evt-list-card-flag {
  position: absolute; top: 0.6rem; left: 0.6rem; padding: 0.2rem 0.5rem;
  border-radius: 999px; background: var(--c-danger); color: var(--c-bg);
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
}
.evt-list-card-body { padding: 1rem; display: grid; gap: 0.5rem; flex: 1; }
.evt-list-card-title { margin: 0; font-size: 1.05rem; }
.evt-list-card-summary { margin: 0; color: var(--c-muted); font-size: 0.9rem; }
.evt-list-card-facts { margin: 0; display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; }
.evt-list-card-facts div { display: flex; flex-direction: column; }
.evt-list-card-facts dt {
  font-size: 0.65rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-muted);
}
.evt-list-card-facts dd { margin: 0; font-size: 0.85rem; font-weight: 600; }
.evt-list-card-foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  padding: 0.85rem 1rem; border-top: 1px solid var(--c-line);
}
.evt-list-card-price { font-weight: 700; }
.evt-list-card-from { font-size: 0.75rem; font-weight: 500; color: var(--c-muted); }
.evt-list-card-cta { font-size: 0.85rem; font-weight: 600; color: var(--c-accent); }

/* ------------------------------------------------------------------------------------------
   The date search (`events/_date_search.html`) and `/whats-on/`.

   One control in two places — a panel a theme floats over its hero, and the form above the
   results — so it is styled here for all seven and a theme tweaks it from `body.theme-<name>`,
   the arrangement `.site-search` has. The date and number inputs are the browser's own
   controls, so `color-scheme` on the field is what turns the picker's own chrome dark: without
   it the calendar popup is a white sheet under a dark page. */
.date-search {
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: end;
  padding: 1rem; border: 1px solid var(--c-line); border-radius: var(--c-radius);
  background: var(--c-bg);
  /* `.site-search-panel` (added by `static/js/search.js` when the "What" field is enhanced)
     is `position: absolute`, anchored to whichever ancestor is positioned first — without this,
     that is the header or the body, and the panel renders somewhere that is not this form. */
  position: relative;
}
/* `minmax(0, 1fr)`, not the implicit `auto` track: a date input's minimum is its own content
   (the placeholder and the calendar button), so on a phone the "To" box grew its column and ran
   past the panel's edge — `min-width: 0` on the field alone does not reach the track inside it. */
.date-search-field { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.3rem; flex: 1 1 8rem; min-width: 0; }
.date-search-field-wide { flex: 2 1 12rem; }
.date-search-field-narrow { flex: 0 1 6rem; }
.date-search-field label { font-size: 0.78rem; font-weight: 600; color: var(--c-muted); }
.date-search-field input {
  width: 100%; padding: 0.55rem 0.65rem; font: inherit;
  border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm);
  background: var(--c-bg); color: var(--c-text);
}
.date-search-field input:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
/* The "What" field also carries `.site-search-input` — the selector `static/js/search.js`
   looks for (`form.querySelector(".site-search-input")`) to wire up suggestions, nothing about
   its look. Every theme uses that same class to style its own *header* search box at
   `body.theme-<name> .site-search-input` (0,2,1), which beats `.date-search-field input`
   (0,1,1) and repainted this field in the header's padding, type scale, letter-spacing, border
   and background on every theme (depot's attached-button border and studio's letter-spaced caps
   included) — a field a third of the height of its neighbours, reported against the live
   voyage shop. Restated here at equal specificity, after every theme stylesheet, on the same
   pattern as `body .site-search.site-search-page .site-search-input` above: the class is a
   hook, the ancestor decides the look. */
body .date-search-field-wide .site-search-input {
  width: 100%;
  padding: 0.55rem 0.65rem;
  font: inherit;
  letter-spacing: normal;
  text-transform: none;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--c-radius-sm);
  background: var(--c-bg);
  color: var(--c-text);
}
.date-search-submit { flex: 0 0 auto; }
/* The results page's own copy is the same form in a plainer frame: it sits under a heading
   that has already said what the page is, so a second card around it is a box in a box. */
body .date-search.date-search-page { border: 0; padding: 0; background: none; }

/* `/whats-on/` — dates, grouped by day. */
/* Every theme resets `h1`–`h4` and `p` to `margin: 0` at (0,1,2), so a one-class margin here is
   a rule that is in the file and never applies: each date heading sat directly on its first
   booking box, and the "57 dates" count on the search form above it. The day's spacing is a
   `gap`, which no margin reset can reach; the rest name **the tag and the class under `body`**
   — (0,1,2), which ties the theme's reset and wins by being linked later. `body .evt-when-count`
   is only (0,1,1) and still loses, which is exactly what the first attempt at this did.
   `geometry.py`'s `stacked-text` is the guard. */
.evt-when { padding: 2rem 0 3rem; }
body .evt-when-head { margin-bottom: 1.5rem; }
body .evt-when-head h1 { margin: 0 0 0.4rem; }
body p.evt-when-lead { margin: 0; color: var(--c-muted); max-width: 46ch; }
body p.evt-when-count { margin: 1.5rem 0 0.5rem; }
.evt-when-day { margin-top: 1.5rem; display: grid; gap: 0.65rem; }
body h2.evt-when-date {
  margin: 0; font-size: 0.95rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-muted);
}
.evt-when-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.evt-when-item {
  display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; align-items: baseline;
  padding: 0.75rem 1rem; border: 1px solid var(--c-line); border-radius: var(--c-radius);
  background: var(--c-bg);
}
.evt-when-time { font-weight: 700; font-variant-numeric: tabular-nums; }
.evt-when-body { display: flex; flex-wrap: wrap; gap: 0.2rem 0.75rem; align-items: baseline; flex: 1; }
/* Every theme resets anchors to `color: inherit` at zero specificity, so a link in a list of
   dates has nothing marking it as one. The underline is on hover and focus rather than at rest,
   the way the rest of the storefront's in-text links behave. */
.evt-when-title { font-weight: 600; }
.evt-when-title:hover, .evt-when-title:focus-visible { text-decoration: underline; }
.evt-when-where { font-size: 0.85rem; }
.evt-when-state { font-size: 0.85rem; }

/* ------------------------------------------------------------------------------------------
   The slot picker (`scheduling/_picker.html`) — where a shopper buys a *time*.

   It replaces the quantity box on a service product's page, on all seven themes, and it was
   styled by **nothing**: `.slot-picker`, `.slot-day`, `.slot-times`, `.slot-time` and
   `.slot-paging` appeared in no stylesheet in the project, so every available time rendered as
   a browser-default radio button in a run-on paragraph. The same shape as the course pages and
   the contact form before them, and it survived for the same reason plus one more:
   `tests/themes/test_shared_template_classes.py` sweeps `themes/base/templates/storefront/`,
   and these templates live one directory over, in `themes/base/templates/scheduling/`.
   `tests/scheduling/test_picker_styles.py` is the sweep that covers them.

   A theme tweaks from `body.theme-<name>`: commerce.css is linked after the theme stylesheet,
   so a bare class of equal specificity is a rule that never applies. */
.slot-picker { display: grid; gap: 1rem; }
.slot-lead { margin: 0; }
/* A table is bought by a party and a date, then a time (`reservations/_picker.html`). The two
   choosers sit on one row where they fit. `body ` is what lets it beat a theme's `.field` rules. */
body .reserve-choose { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: flex-end; }
body .reserve-choose .field { margin: 0; flex: 1 1 8rem; }
.slot-staff { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.slot-form { display: grid; gap: 1.25rem; margin: 0; }
/* One day open at a time. A fortnight of half-hour slots laid out in full was a column four to
   nine thousand pixels long on a service's product page, beside a photograph that ended on the
   first screen. Each day is a `<details>` sharing one `name`, so opening a day closes the last
   with no script — and a browser that predates exclusive `<details>` just lets several stay
   open, which is the old page rather than a broken one. */
.slot-days { display: grid; border-top: 1px solid var(--c-line); }
.slot-day { border-bottom: 1px solid var(--c-line); }
.slot-day-title {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.7rem 0.15rem; cursor: pointer; list-style: none;
  font-size: 0.78rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--c-text);
}
.slot-day-title::-webkit-details-marker { display: none; }
.slot-day-title::after {
  content: ""; flex: none; width: 0.45rem; height: 0.45rem; margin-right: 0.25rem;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: translateY(-25%) rotate(45deg);
}
.slot-day[open] > .slot-day-title::after { transform: translateY(25%) rotate(-135deg); }
/* A time chosen on a day that has since been folded away is still the one the form will hold,
   so the day says so. */
.slot-day:has(input:checked) > .slot-day-title { box-shadow: inset 3px 0 0 var(--c-accent); padding-left: 0.65rem; }
.slot-day > .slot-times { padding-bottom: 0.9rem; }
.slot-times { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(6.5rem, 100%), 1fr)); gap: 0.4rem; }
/* A court or a room the customer picks: its name, then the times it is free. The <fieldset>
   is the grouping a screen reader announces, so its browser border and padding go. */
body .slot-resource { border: 0; margin: 0 0 0.9rem; padding: 0; min-width: 0; }
body .slot-resource > legend { padding: 0 0 0.4rem; font-weight: 600; }
/* Each time is a radio wearing a button. The input stays in the accessibility tree and keeps
   its keyboard behaviour; only its default appearance goes. */
.slot-time {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 0.1rem;
  padding: 0.5rem 0.4rem; border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm);
  background: var(--c-bg); color: var(--c-text); font-weight: 600; line-height: 1.2;
  cursor: pointer; text-align: center;
}
.slot-time input { position: absolute; inset: 0; opacity: 0; cursor: pointer; margin: 0; }
.slot-time:hover { border-color: var(--c-accent); }
.slot-time:focus-within { outline: 2px solid var(--c-accent); outline-offset: 2px; }
/* Selected carries its signal in the border and a tinted ground, keeping `--c-text` as the
   ink — the arrangement `.alert` uses. A filled `--c-primary` would have been the obvious
   choice and the wrong one: bloom and campus point that token at their accent rather than at
   their ink, which is exactly how the pay screens' home-made button measured 1.69:1. */
.slot-time:has(input:checked) {
  background: var(--c-accent-soft); border-color: var(--c-accent);
  box-shadow: inset 0 0 0 1px var(--c-accent);
}
.slot-paging { display: flex; gap: 0.5rem; }
/* A room is chosen by name, so its tile is wider than a time of day, and a stay says what it
   comes to under the dates. `body p` for the total: every theme resets a paragraph's margin at
   (0,1,2), which a bare `.slot-total` never beats. */
body .slot-times:has(.slot-room) { grid-template-columns: repeat(auto-fill, minmax(min(11rem, 100%), 1fr)); }
.slot-room { padding-block: 0.7rem; }
body p.slot-total { margin: 0; font-weight: 600; }
body p.slot-note { margin: 0; }
.slot-picker .empty-state { padding: 2rem 1rem; }

/* A ride (`reservations/_ride_form.html`, `_ride_quote.html`): where from, where to and when, then
   the fare. Two addresses are full-width fields and the time and the passengers share a row where
   they fit. `body ` beats a theme's `.field` and paragraph resets, which sit at (0,1,2). */
body .ride-form, body .ride-request { display: grid; gap: 0.9rem; margin: 0; }
body .ride-form .field, body .ride-request .field { margin: 0; }
body .ride-row { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; }
body .ride-row .field { flex: 1 1 9rem; }
body p.ride-note { margin: 0; }
body .ride-quote { display: grid; gap: 0.9rem; }
body .ride-quote h2 { margin: 0; }
body .ride-quote .order-meta { margin-bottom: 0; }

/* A seated show (`reservations/_seat_picker.html`): the plan is rows of real checkboxes wearing
   buttons, the way a time is a radio. The visible label is only the seat's number; a screen
   reader gets the whole sentence from the `.sr-only` span. The plan scrolls sideways inside its
   own box rather than widening the page, and every seat is a focus stop, which is what makes
   that scrolling reachable without a pointer. A taken seat is struck through as well as
   disabled — colour is never the only signal. `body ` on the rules a theme resets (paragraphs
   and lists) is what makes them apply. */
body .seat-legend, body .seat-key { display: flex; flex-wrap: wrap; gap: 0.35rem 1rem; list-style: none; margin: 0; padding: 0; }
body .seat-key li { display: inline-flex; align-items: center; gap: 0.4rem; }
body .seat-section { margin: 0.5rem 0 0; font-weight: 600; font-size: 0.85rem; }
.seat-plan { display: grid; gap: 0.35rem; overflow-x: auto; padding: 0.25rem 0.25rem 0.5rem; min-width: 0; }
.seat-row { display: flex; align-items: center; gap: 0.3rem; width: max-content; min-width: 100%; }
.seat-row-label { flex: none; width: 1.75rem; font-size: 0.8rem; font-weight: 600; color: var(--c-muted); text-align: center; }
.seat {
  position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; width: 2.5rem; height: 2.5rem; padding: 0;
  border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm);
  background: var(--c-bg); color: var(--c-text); font-size: 0.7rem; font-weight: 600; line-height: 1;
}
label.seat { cursor: pointer; }
label.seat input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; margin: 0; }
label.seat:hover { border-color: var(--c-accent); }
label.seat:focus-within { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.seat:has(input:checked) {
  background: var(--c-accent-soft); border-color: var(--c-accent);
  box-shadow: inset 0 0 0 1px var(--c-accent);
}
.seat-taken {
  cursor: not-allowed; color: var(--c-muted);
  background: linear-gradient(to top right, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px)), var(--c-bg);
}
.seat-taken input { cursor: not-allowed; }
.seat-gap, .seat-off { border-color: transparent; background: transparent; visibility: hidden; }
.seat-key .seat { width: 1.1rem; height: 1.1rem; }
@media (pointer: coarse) { .seat { width: 2.75rem; height: 2.75rem; } }

/* The delivery window picker (`storefront/delivery/checkout_step.html`) is the same control
   one step later in the shop, and it was unstyled too — `.slot-grid` and `.slot-option` are on
   the debt list in `tests/themes/test_shared_template_classes.py`. One option is one option, so
   it shares the look above rather than growing a second vocabulary; the two lines inside it
   were renamed off `.slot-day` / `.slot-time`, which mean something else in the picker. */
.slot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(11rem, 100%), 1fr)); gap: 0.5rem; }
.slot-option {
  position: relative; display: flex; flex-direction: column; gap: 0.15rem;
  padding: 0.7rem 0.75rem; border: 1px solid var(--c-line-strong);
  border-radius: var(--c-radius-sm); background: var(--c-bg); color: var(--c-text);
  cursor: pointer; line-height: 1.3;
}
.slot-option input { position: absolute; inset: 0; opacity: 0; cursor: pointer; margin: 0; }
.slot-option:hover { border-color: var(--c-accent); }
.slot-option:focus-within { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.slot-option:has(input:checked) {
  background: var(--c-accent-soft); border-color: var(--c-accent);
  box-shadow: inset 0 0 0 1px var(--c-accent);
}
.slot-option-day { font-weight: 600; }
.slot-option-time { font-variant-numeric: tabular-nums; color: var(--c-muted); }
.slot-option:has(input:checked) .slot-option-time { color: inherit; }

/* The customer's own booking page (`scheduling/booking.html`). */
/* `body p.` for the reason `.order-panel h2` above needs `body `: these are paragraphs, and a
   theme's `p { margin: 0 }` is (0,1,2). */
body p.booking-when { font-size: 1.15rem; margin: 0.75rem 0; }
.booking-instructions { margin: 0 0 0.75rem; color: var(--c-muted); }
.booking-instructions p:last-child { margin-bottom: 0; }
body p.booking-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1rem 0 0; }
/* The "Need to change it?" card, for the same reason and with the same `body ` prefix: a theme
   resets `h2` and `p` to `margin: 0` at (0,1,2), so the heading sat directly on the sentence
   under it and that sentence sat 3px off the Cancel form — both under `geometry.py`'s 6px
   `stacked-text` bar, on the one card a customer reads before calling off an appointment. It
   renders only while `can_cancel`, so which booking the sweep happened to sample decided whether
   anything saw it. */
body h2.booking-change-title { margin: 0 0 0.5rem; }
body p.booking-change-note { margin: 0 0 1rem; }

/* The hire picker (`rentals/_picker.html`) and the customer's own hire page (`rentals/hire.html`).
   Shared markup no theme overrides, so this file owns it for all seven, as it owns the slot picker
   above; a theme tweaks from `body.theme-<name>`. `.rent-` rather than borrowing `.slot-`: a hire
   is not a slot, and one class name over two components in the one stylesheet every theme loads
   is the collision the delivery window picker had to be renamed out of. The error line is the
   shared `.alert`, whose signal is a border rather than a status colour pair. */
.rent-picker { display: grid; gap: 1rem; }
.rent-choose { display: grid; gap: 0.75rem; margin: 0; }
/* 11rem, not 9: a date input will not draw narrower than about 10rem, so at 9 a phone got two
   columns and the date ran across the gap onto the time beside it. Where two do not fit, the
   fields stack, as they already did in a desktop buy panel. `min-width: 0` is the second half: a
   grid item's minimum is its content, so without it any control can still spill out of its track. */
.rent-dates { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr)); gap: 0.75rem; }
.rent-dates .field, .rent-dates .input { min-width: 0; }
.rent-choose .field { margin: 0; }
.rent-quote {
  display: grid; gap: 0.6rem; padding: 1rem; border: 1px solid var(--c-line-strong);
  border-radius: var(--c-radius-sm); background: var(--c-soft); color: var(--c-text);
}
.rent-quote form { margin: 0; }
.rent-when { margin: 0; font-weight: 600; }
.rent-sums { display: grid; gap: 0.25rem; margin: 0; }
.rent-sums div { display: flex; justify-content: space-between; gap: 1rem; }
.rent-sums dt { color: var(--c-muted); }
.rent-sums dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; }
.rent-overview summary { cursor: pointer; font-weight: 600; }
.rent-days {
  list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(6.5rem, 100%), 1fr)); gap: 0.4rem;
}
.rent-day {
  display: flex; flex-direction: column; gap: 0.1rem; padding: 0.45rem 0.5rem;
  border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm);
  background: var(--c-bg); color: var(--c-text); line-height: 1.2;
}
.rent-day-date { font-weight: 600; }
.rent-day-free { font-size: 0.85rem; color: var(--c-muted); }
/* Booked carries its signal in the ground and the word, never a muted ink on a muted ground. */
.rent-day.is-full { background: var(--c-soft); }
.rent-day.is-full .rent-day-free { color: var(--c-text); }
/* How a hire changes hands, where the item offers both. Two plain radios in a fieldset: the choice
   is small, and a segmented control that only works once a script has run is not one. */
.rent-handover { display: grid; gap: 0.35rem; margin: 0; padding: 0; border: 0; min-width: 0; }
.rent-handover legend { padding: 0; margin-bottom: 0.25rem; font-weight: 600; }
.rent-handover label { display: flex; align-items: center; gap: 0.5rem; }

@media (max-width: 40rem) {
  .evt-book-pax-row { grid-template-columns: 1fr auto 3.75rem; }
  .evt-guide-facts { grid-template-columns: repeat(2, 1fr); }
  .slot-times { grid-template-columns: repeat(auto-fill, minmax(min(5rem, 100%), 1fr)); }
}

/* Payments in test mode, emitted on every page by the storefront skeleton. Its colours were an
   inline `style=` attribute — a mode-static amber invisible to every sweep here — and the words
   were set in `system-ui` rather than in the theme's own type. The signal is the border; the
   text is ordinary body contrast by construction. */
.payments-test-banner {
  padding: 0.45rem 1rem;
  text-align: center;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-text);
  background: var(--c-soft);
  border-bottom: 2px solid var(--c-warn);
}

/* --------------------------------------------------------------------------------------------
   The catalogue filter panel (`storefront/partials/_facets.html`).

   Shared markup no theme overrides, so `commerce.css` owns it for all seven — the arrangement
   the currency and language switchers have, and for the reason `summit` taught: six themes each
   writing a private copy is six chances for the seventh to have none. A theme tweaks from
   `body.theme-<name> .facets`, which wins because this file is linked after the theme's.
   -------------------------------------------------------------------------------------------- */
.facets-panel {
  margin-bottom: 1.5rem;
  border: 1px solid var(--c-line);
  border-radius: var(--c-radius);
  background: var(--c-soft);
}
.facets-summary {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.7rem 1.15rem;
  font-size: 0.9rem; font-weight: 600; color: var(--c-text);
  cursor: pointer; list-style: none;
}
.facets-summary::-webkit-details-marker { display: none; }
/* The summary's heading reads as the summary's label. Every theme styles `h2` (depot's rail
   paints one as a banded caption at (0,2,1)), so this names three classes to stay a label. */
body .facets-panel > .facets-summary > h2.facets-title {
  margin: 0; padding: 0; border: 0; background: none;
  font: inherit; letter-spacing: normal; text-transform: none; color: inherit;
}
/* The chevron is drawn, not the browser's triangle, so it turns with the panel and takes the ink. */
.facets-summary::after {
  content: ""; width: 0.45rem; height: 0.45rem; margin-left: auto;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); transition: transform 0.15s ease;
}
.facets-panel[open] > .facets-summary::after { transform: rotate(-135deg); }
.facets-summary-on {
  padding: 0.05rem 0.5rem; border-radius: 999px; font-size: 0.75rem;
  background: var(--c-text); color: var(--c-bg);
}
.facets {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  align-items: start;
  padding: 0.25rem 1.15rem 1rem;
}
.facet { margin: 0; padding: 0; border: 0; min-width: 0; }
.facet legend { padding: 0; margin-bottom: 0.4rem; font-size: 0.85rem; font-weight: 600; color: var(--c-text); }
/* `scrollbar-gutter: stable` reserves the scrollbar's track in the content box before it is
   needed, so `.facet-count`'s `margin-left: auto` below never measures against space the
   scrollbar is about to paint over — without it, a facet with enough values to overflow 12rem
   draws its scrollbar directly on top of the count column. And `grid-template-columns: minmax(0,
   1fr)`, not the implicit `auto` track: a track with no explicit sizing grows to its content's
   min-content width, and a long value ("General admission") can't shrink below its own text —
   so every row was pinned to that width regardless of the box actually available, running the
   count straight through the reserved scrollbar gutter on any facet with a long label. */
.facet-values {
  list-style: none; margin: 0; padding: 0 0.3rem 0 0; display: grid;
  grid-template-columns: minmax(0, 1fr); gap: 0.2rem;
  max-height: 12rem; overflow-y: auto; scrollbar-gutter: stable;
}
/* The whole row is the target, not the 1rem box at the start of it. `min-width: 0` for the same
   reason as the track above: a flex item's default minimum is its content's, and without it the
   label span below can never actually reach its own `overflow: hidden; text-overflow: ellipsis`. */
.facet-value { display: flex; align-items: center; gap: 0.5rem; padding: 0.15rem 0; font-size: 0.9rem; cursor: pointer; min-width: 0; }
.facet-value input { flex: none; }
.facet-value > span:first-of-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.facet-count { margin-left: auto; font-size: 0.78rem; color: var(--c-muted); font-variant-numeric: tabular-nums; }
.facet-range { display: flex; align-items: center; gap: 0.4rem; }
.facet-range .input { min-width: 0; }
.facet-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
@media (max-width: 640px) { .facets { grid-template-columns: 1fr; } }

/* Desktop: the panel is a persistent rail, not a thing to open — no chevron, no pointer, no
   click-to-collapse, because the whole point of always starting `open` is that a shopper never
   meets it closed. A phone keeps the ordinary `<details>` behaviour, since there the open panel
   is taller than the screen. 901px matches the width `depot`'s and `press`'s own rails switch
   their sticky behaviour at, so the two read as one decision about the same width. */
@media (min-width: 901px) {
  .facets-summary { cursor: default; pointer-events: none; }
  .facets-summary::after { display: none; }
}

/* The sort links are one word or three ("Price: low to high"), and every theme lays them in a
   flex row. Left to shrink, a phone broke each label over two lines inside its own link. A
   label stays whole and the row wraps between labels instead. `body` so a theme's own
   `.sort-links` (0,2,1) keeps its gaps and colours while this still reaches the wrap. */
body .sort-links { flex-wrap: wrap; row-gap: 0.35rem; }
body .sort-links a, body .sort-links span { white-space: nowrap; }

/* On a phone the sort order is a dropdown (`partials/_sort_select.html`), as shoppers expect: a
   wrapping paragraph of five sort words is a second menu to read before the products. Every
   theme sets `body.theme-<name> .sort-links { display: flex }` at (0,2,1), so hiding it needs the
   doubled class to tie that and win by being linked later. The label stays visible, because a
   bare select reading "Newest" does not say what it does. */
body .sort-select { display: none; }
@media (max-width: 40rem) {
  body .sort-links.sort-links { display: none; }
  body .sort-select.sort-select { display: flex; align-items: center; gap: 0.5rem; }
  body .sort-select label { font-size: 0.85rem; color: var(--c-muted); white-space: nowrap; }
  /* The shared `select` rule above only draws the chevron: every other part of a select's box
     comes from a theme's form rules, and this one is in no form. Without these it rendered as
     bare words with an arrow beside them. */
  body .sort-select select {
    min-width: 0; max-width: 100%;
    padding: 0.5rem 2.1rem 0.5rem 0.75rem;
    border: 1px solid var(--c-line-strong); border-radius: var(--c-radius-sm);
    background-color: var(--c-bg); color: var(--c-text);
    font: inherit; font-size: 0.9rem; line-height: 1.3;
  }
  body .sort-select select:focus-visible {
    outline: none; border-color: var(--c-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 35%, transparent);
  }
}

/* "Email me when it's back" (`storefront/partials/_back_in_stock.html`). Shared markup no theme
   overrides, so this file owns it for all seven. */
.notify-me {
  margin: 0 0 1.5rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--c-line);
  border-radius: var(--c-radius);
  background: var(--c-soft);
}
.notify-me h3 { margin: 0 0 0.25rem; font-size: 1rem; }
.notify-me p { margin: 0 0 0.75rem; color: var(--c-muted); font-size: 0.9rem; }
.notify-form { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.notify-form .input { flex: 1 1 14rem; min-width: 0; }

/* Merchant-chosen recommendations. `.cross-sells` is on the cart page and `.upsells` on the
   product page; both are shared markup no theme overrides, so this file owns them for all
   seven — the arrangement the reviews block and the course panel already have. */
/* Spaced with `gap`, not a margin on the heading: every theme resets `h2`/`h3` to `margin: 0` at
   (0,1,2), which beat `.upsells h3` at (0,1,1), so "Consider instead" sat flush on its list. */
.cross-sells { display: grid; gap: 1.25rem; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--c-line); }
.cross-sells h2 { margin: 0; }
/* Compact rows, not a second grid of cards: a shopper reading this has already found something
   they were willing to buy, and matching the main product's visual weight competes with it. */
.upsells { display: grid; gap: 0.5rem; margin: 0 0 1.5rem; }
.upsells h3 { margin: 0; font-size: 1rem; }
.upsell-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--c-line); border-radius: var(--c-radius); overflow: hidden; }
.upsell-list li + li { border-top: 1px solid var(--c-line); }
.upsell-list a { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.6rem 0.9rem; text-decoration: none; }
.upsell-list a:hover { background: var(--c-soft); }
.upsell-name { color: var(--c-text); }
.upsell-price { color: var(--c-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Which options are left, on a card (`storefront/partials/_option_pills.html`). One owner for all
   twelve themes; a theme restyles from `body.theme-<name> li.option-pill`.

   `body ul.option-pills` and `body li.option-pill`, because every theme resets `ul`/`li` at
   (0,1,2) — a one-class rule on a list here is a rule that is in the file and never applies,
   which this stylesheet has learned three times.

   **The element in the selector is what keeps this rule reachable at all.** `.option-pill` is two
   components: the buy form's `<button>` in `.option-values`, which picks a variant, and this
   `<li>`, which is a sentence about the product. A theme writing `body.theme-<name> .option-pill`
   for the button is (0,2,1) and beats `body li.option-pill` (0,1,2), so for twelve themes every
   pill in every listing wore the buy form's padding, its `cursor: pointer` and a hover that moved
   its border, and did nothing when pressed. Nothing here could see it — the class is defined, the
   colours are a palette pair and a slightly larger pill still fits its card — and it was reported
   by somebody who tried to click one. A theme names the element on both halves;
   `tests/themes/test_theme_stylesheet_rules.py` and
   `tests/themes/test_option_pills_are_not_interactive_browser.py` are the two guards.

   A sold-out pill is **struck through, not dimmed**: fading it is how a card ends up with text
   below the contrast floor, and the storefront sweep measures whatever is painted. Its words go
   through `--c-muted`, the same pair `.pill` has always used. */
body ul.option-pills {
  display: flex; flex-wrap: wrap; gap: 0.3rem;
  margin: 0.5rem 0 0; padding: 0; list-style: none;
}
body li.option-pill {
  padding: 0.1rem 0.5rem; border: 1px solid var(--c-line);
  border-radius: 999px; font-size: 0.75rem; line-height: 1.5;
  color: var(--c-text); background: var(--c-bg);
}
body li.option-pill.is-sold-out {
  color: var(--c-muted); background: var(--c-soft);
  text-decoration: line-through;
}
body li.option-pill-more { border-style: dashed; color: var(--c-muted); }

/* Save for later (`storefront/partials/_wishlist_button.html`). Included by all three copies of
   the product card, so this file owns it for all seven themes — a theme that adds its own card
   partial and forgets the include loses the control, which `tests/themes/test_wishlist.py`
   fails on. */
.product-card { position: relative; }
.wish-form { position: absolute; top: 0.5rem; right: 0.5rem; z-index: 1; margin: 0; }
.wish-btn {
  display: grid; place-items: center;
  width: 2.1rem; height: 2.1rem; padding: 0;
  border: 1px solid var(--c-line); border-radius: var(--c-radius-pill, 999px);
  background: var(--c-bg); color: var(--c-muted);
  cursor: pointer;
}
.wish-btn:hover { color: var(--c-text); border-color: var(--c-line-strong); }
.wish-btn.is-saved { color: var(--c-danger, var(--c-accent)); border-color: currentColor; }
.wish-btn:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.wish-btn .icon { width: 1.05rem; height: 1.05rem; }
/* On the product page it sits in the flow beside the buy button, not over an image. */
.product-buy .wish-form { position: static; }
/* The header's link to the saved list, beside Cart. */
.saved-link { white-space: nowrap; }

/* ---- Shared shell classes that used to live in one theme ---------------------------------
   `.container.narrow`, the sign-in furniture, `.site-main`, `.cart-link` and `.course-panel`
   are all emitted by templates **no theme overrides** — and every one of them was defined only
   in the old default theme's stylesheet. The pooled half of
   `tests/themes/test_shared_template_classes.py` could not see that (one definition anywhere
   counts as styled for everybody), so replacing the themes is what surfaced it: a class one
   theme happens to define is a class the other six render as browser defaults.

   Shared markup has one owner, and this is it. */
/* `body ` is load-bearing: a theme sets its shell width from `body.theme-<name> .container`,
   which is (0,2,1), and a bare `.container.narrow` is (0,2,0) — so the narrow variant lost and
   the contact form, the sign-in card and the order lookup all ran the full shell width. With
   the element selector the two tie, and commerce.css is linked *after* the theme stylesheet, so
   this wins. Measured by `test_shared_controls_browser.py`, which is why it was caught. */
body .container.narrow { width: min(46rem, calc(100% - 2.5rem)); }
/* A hook rather than a component — but it earns one rule: without it a short page (a 404, an
   empty basket) leaves the footer floating in the middle of the viewport. */
.site-main { display: block; min-height: 50vh; }
.cart-link { position: relative; }

/* The sign-in and register cards. Every theme draws the card's border and ground
   (`body.theme-<name> .auth-card`), and nothing drew the inside: the rule that did lived in a
   retired theme's stylesheet, so on all twelve the heading, the fields and the button sat on the
   border. The spacing is written against the themes' `p` and `h1` margin reset at (0,1,2), and
   the button rule is `.auth-card.form` because the card *is* the form — the old
   `.auth-card .form .btn` looked for a form inside it and matched nothing. */
body .auth-card { padding: clamp(1.5rem, 6vw, 2.25rem); }
body .auth-card h1 { margin: 0 0 0.35rem; }
body .auth-card p.auth-sub { margin: 0 0 1.5rem; }
body .auth-card.form .btn { width: 100%; margin-top: 0.25rem; }
body .auth-card p.auth-foot { margin: 1rem 0 0; }
.auth-section { padding: clamp(2.5rem, 6vw, 4.5rem) 0; display: grid; justify-items: center; }
.auth-section > * { width: min(26rem, 100%); }
.form-error {
  margin-bottom: 1rem; padding: 0.75rem 1rem;
  border: 1px solid var(--c-line-strong); border-left: 3px solid var(--c-danger);
  border-radius: var(--c-radius-sm);
  background: var(--c-soft); color: var(--c-text); font-size: 0.9rem;
}
.form-error ul { margin: 0; padding-left: 1rem; }
.auth-foot { font-size: 0.875rem; color: var(--c-muted); text-align: center; }
.auth-foot a { color: var(--c-accent); font-weight: 600; }

/* `_course_panel.html` is a modifier on `.product-section`, which is styled above — it needs
   only the one thing that is genuinely its own. */
.product-section.course-panel .course-outcomes { margin-top: 0.75rem; }

/* ---- Cart arrangements ------------------------------------------------------------------
   `cart.html` is now a theme's own template, but only the *arrangement* is: the line items and
   the summary stay in `partials/_cart_lines.html` and `_cart_summary.html`, because the htmx
   targets and the Alpine stepper are fiddly, identical for everybody, and seven copies of them
   would be seven places for one to drift. These are the shapes those two parts are put into. */
.cart-stack { display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); }
.cart-stack .summary-card { max-width: 26rem; margin-left: auto; }
.cart-stack-summary-first .summary-card { margin-left: 0; margin-right: auto; }
/* The summary column on the left, which is a different reading order rather than a mirror: the
   totals are what a shopper checks first on these themes. */
.commerce-grid.cart-summary-first { grid-template-columns: 20rem minmax(0, 1fr); }
.commerce-grid.cart-summary-first > .summary-card { order: -1; }
@media (max-width: 860px) {
  .commerce-grid.cart-summary-first { grid-template-columns: 1fr; }
  .cart-stack .summary-card, .cart-stack-summary-first .summary-card { max-width: none; }
}

/* ---- Scroll reveal ------------------------------------------------------------------------
   The hidden state is behind `html.js-reveal`, which `static/js/reveal.js` adds. With
   JavaScript blocked, broken or slow, none of this applies and every element is visible in its
   final position — which is also why the browser sweeps, which strip scripts, measure a page
   with nothing hidden. A reveal written the other way round (hidden here, shown by script) is a
   blank shop whenever anything goes wrong, and a screenshot test cannot see it because the test
   runs the script.

   `prefers-reduced-motion` is belt and braces: the script already refuses to add the class, and
   this makes sure a cached class from a previous visit cannot animate anything either.

   **The revealed rule repeats the hidden selector and appends `.is-revealed`**, rather than
   being one short `.js-reveal .is-revealed`. That short version is (0,2,0) and
   `.js-reveal .builder-page .pb-block` is (0,3,0), so every block on every built home page was
   given the class, kept `opacity: 0`, and stayed invisible for good — the reveal turning into
   the blank shop it is written to avoid. Only a browser can see it, which is what
   `tests/themes/test_reveal_browser.py` is for. */
.js-reveal [data-reveal],
.js-reveal .builder-page .pb-block,
.js-reveal .product-card,
.js-reveal .section-head,
.js-reveal .listing-hero,
.js-reveal .newsletter {
  opacity: 0;
  transform: translateY(var(--reveal-shift, 14px)) scale(var(--reveal-scale, 1));
  transition:
    opacity var(--reveal-duration, 520ms) var(--reveal-ease, cubic-bezier(0.22, 0.61, 0.36, 1)),
    transform var(--reveal-duration, 520ms) var(--reveal-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
  will-change: opacity, transform;
}
.js-reveal [data-reveal].is-revealed,
.js-reveal .builder-page .pb-block.is-revealed,
.js-reveal .product-card.is-revealed,
.js-reveal .section-head.is-revealed,
.js-reveal .listing-hero.is-revealed,
.js-reveal .newsletter.is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}
@media (prefers-reduced-motion: reduce) {
  .js-reveal [data-reveal],
  .js-reveal .builder-page .pb-block,
  .js-reveal .product-card,
  .js-reveal .section-head,
  .js-reveal .listing-hero,
  .js-reveal .newsletter {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---- Motion under the pointer ---------------------------------------------------------
   The one piece of motion every theme in the reference set has, and the one this shop had
   none of: a product photograph that moves a little when you point at it. It is worth having
   for a reason beyond decoration — the card is a link whose whole target area is a picture,
   and the movement is what says so.

   Three things keep it honest:

   - **It is a `transform` on the image and nothing else moves.** Growing the card would
     reflow the grid; growing the image inside a clipped frame does not touch layout, so a
     row of cards cannot jitter as the pointer crosses it.
   - **`@media (hover: hover)`.** On a touch screen `:hover` sticks after a tap, so without
     this the last card somebody touched stays zoomed until they touch another one.
   - **`:focus-within` gets it too**, because the card is reached by keyboard as well, and an
     affordance only pointers can see is one half of the users missing it.

   `--hover-zoom` is per theme, like `--reveal-*`: press barely moves, studio scales, depot
   does not move at all because a parts catalogue is a tool rather than a shop window. A theme
   setting `1` opts out without needing a rule to undo. */
.product-media { overflow: hidden; }
.product-media img {
  transition: transform var(--hover-duration, 620ms)
    var(--hover-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
}
@media (hover: hover) {
  .product-card:hover .product-media img,
  .product-card:focus-within .product-media img {
    transform: scale(var(--hover-zoom, 1.045));
  }
}
@media (prefers-reduced-motion: reduce) {
  .product-media img { transition: none; }
  .product-card:hover .product-media img,
  .product-card:focus-within .product-media img { transform: none; }
}

/* ---- Menu submenus ------------------------------------------------------------------------
   Structural only. Each theme styles `.site-nav` itself — seven headers genuinely do look
   different — so what lives here is the part that is the same everywhere: where the panel sits
   and what opens it. Colours come from the palette tokens, so a submenu follows light and dark
   without any theme writing a rule.

   commerce.css is linked *after* the theme stylesheet, so these win at equal specificity and a
   theme tweaks them from `body.theme-<name> .nav-submenu { … }`.

   **No JavaScript, and that is the design.** `:hover` opens it for a pointer, `:focus-within`
   opens it for a keyboard. A menu that needs a script to open is a menu some people cannot
   open, and this one is above the fold on every page of the shop. */
.site-nav .nav-group { position: relative; display: inline-flex; align-items: center; }
.site-nav .nav-group-label { cursor: default; }
.site-nav .nav-submenu {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 40;
  display: grid;
  min-width: 12rem;
  padding: 0.4rem;
  gap: 0.1rem;
  background: var(--c-bg);
  border: 1px solid var(--c-line);
  border-radius: var(--c-radius-sm);
  box-shadow: var(--c-shadow);
  /* Hidden without `display: none`, so the links stay in the accessibility tree and a screen
     reader can still reach them — and so `:focus-within` has something to focus. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.25rem);
  transition: opacity 120ms ease, transform 120ms ease, visibility 120ms;
}
.site-nav .nav-group:hover .nav-submenu,
.site-nav .nav-group:focus-within .nav-submenu {
  opacity: 1;
  visibility: visible;
  transform: none;
}
.site-nav .nav-submenu a {
  padding: 0.4rem 0.6rem;
  border-radius: var(--c-radius-sm);
  white-space: nowrap;
}
.site-nav .nav-submenu a:hover,
.site-nav .nav-submenu a:focus-visible { background: var(--c-soft); }
/* **The panel owns its ink, and the leading `body` is load-bearing.** A theme colours
   `.site-nav a` for the *bar* — and `voyage`'s bar is a dark chrome band, so its rule is
   `color: var(--color-surface)` at (0,2,2). These links are inside `.site-nav`, so on a panel
   grounded in `--c-bg` that is near-white ink on a near-white card: **1.0:1, in both colour
   schemes**, five of six links invisible, and in dark mode the same thing the other way round.
   Exactly what `.site-search` did on the same theme for the same reason. A bare
   `.site-nav .nav-submenu a` is (0,2,1) and loses; with `body` it ties at (0,2,2) and commerce
   is linked *after* the theme, so it wins. A theme that wants its own panel ink writes
   `body.theme-<name> .site-nav .nav-submenu a`, which is (0,3,2) — and voyage does, because
   the honest fix there is a panel that belongs to the band rather than to the page. */
body .site-nav .nav-submenu a { color: var(--c-text); }

/* **A group inside a panel is one row plus an indented list, not a second dropdown.** It only
   happens in `nav_overflow.js`'s "More" panel, which moves a whole top-level entry in, submenu
   and all. Left with the top-level rules, the group stayed `inline-flex`, so its link was only
   as wide as its word and the hover lit the word rather than the line every other row lights
   (reported on the live voyage shop: "About" in More). Its own panel stayed absolutely placed at
   `top: 100%` and opened whenever More did, since `.nav-group:hover .nav-submenu` matches every
   panel below the hovered one, so it floated over the rows under it. Static and indented is the
   phone drawer's shape, one level up, and it cannot run off the edge of the page the way a
   fly-out would.
   `visibility: inherit`, never `visible`: a child that says `visible` stays focusable and
   clickable inside a closed, transparent outer panel. (0,4,1) outranks every theme's own panel
   rule at (0,3,1), which is what has to lose here: a theme's shadow and ground on the inner
   list would draw a card inside the card. */
body .site-nav .nav-submenu .nav-group { display: grid; }
body .site-nav .nav-submenu .nav-group > .nav-group-label { padding: 0.4rem 0.6rem; }
body .site-nav .nav-submenu .nav-group .nav-submenu {
  position: static;
  opacity: 1;
  visibility: inherit;
  transform: none;
  min-width: 0;
  padding: 0 0 0 0.9rem;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  transition: none;
}

/* The mega panel — the same one level of nesting, laid out in columns.
   `menu()` only builds columns for a parent set to `mega` that has more than three children, so
   this never draws a wide empty box over a short list. `auto-fit` rather than a fixed count so
   a merchant who marks two columns gets two, not four with two empty. */
.site-nav .nav-submenu-mega {
  grid-template-columns: repeat(auto-fit, minmax(min(9.5rem, 100%), 1fr));
  gap: 0.25rem 1.75rem;
  padding: 1.1rem 1.25rem 1.25rem;
  /* **A width, not a minimum.** It opens from its own entry (`left: auto`), so a panel sized by
     its content grew with every column — a menu filled from five top-level categories drew a
     57rem panel from an entry halfway across the header and ran 80px off the page, on every
     page. Fixed at the old minimum, extra columns wrap onto a second row inside it instead,
     which is `auto-fit`'s job. `90vw` keeps it inside a small laptop. */
  left: auto;
  width: min(44rem, 90vw);
}
.site-nav .nav-submenu-mega .nav-column { display: grid; gap: 0.1rem; align-content: start; }
/* The column head is an ordinary link that happens to start a column, so it is marked by weight
   and spacing rather than by being a different kind of thing. */
.site-nav .nav-submenu-mega .nav-column-head {
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 0.72rem;
  color: var(--c-muted);
  margin-bottom: 0.2rem;
}
/* A short flag beside a label. It takes the accent *pair* from the palette — never a colour a
   merchant types — for the reason the builder's tones give: a literal is the same value in
   light and dark, and no contrast test can see a per-shop override. */
.nav-badge {
  display: inline-block;
  margin-inline-start: 0.4rem;
  padding: 0.1rem 0.35rem;
  border-radius: 3px;
  background: var(--color-accent);
  color: var(--color-accent-ink);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  vertical-align: middle;
  white-space: nowrap;
}
/* On a narrow screen there is no room beside the parent and no hover to open it with, so the
   panel becomes an indented list that is simply always there. A dropdown that needs a hover is
   a dropdown a phone cannot open. */
/* **The phone shape is structural, so it outranks the themes.** A media query adds no
   specificity, and every theme now styles its own panel at `body.theme-<name> .site-nav
   .nav-submenu` — (0,3,1) — so a bare `.site-nav .nav-submenu` here would lose and a phone
   would get the desktop card: a shadowed, padded, sometimes inverted box inlined into the
   burger menu, on all seven. `.nav-group` in the middle takes these to (0,3,2), above any
   theme's panel rule; a theme that genuinely needs to tune the phone list has to say so
   explicitly, which is the right way round for a shape that is about the device rather than
   about the design. */
@media (max-width: 720px) {
  body .site-nav .nav-group { display: block; }
  /* A mega panel on a 390px screen is just a long menu, so it becomes one. */
  body .site-nav .nav-group .nav-submenu-mega {
    grid-template-columns: 1fr; width: auto; min-width: 0; max-width: none; padding: 0; gap: 0;
  }
  body .site-nav .nav-group .nav-submenu {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    min-width: 0;
    padding: 0 0 0 0.9rem;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
    transition: none;
  }
}

/* ---------------------------------------------------------------- The shop's logo, twice
   `partials/_brand.html` puts both of a shop's logos in the markup when it has a version for
   dark backgrounds, and this picks one. The choice follows the same two inputs the palette
   does — the merchant's `color_scheme` (stamped on <html> as `data-color-scheme`) and, when that
   is `auto`, the visitor's device — so the logo and the colours behind it can never disagree.

   `data-brand-ground="inverse"` flips it, for a band a theme paints in the *opposite* of the
   page: depot's, voyage's and encore's footers are `--color-ink`, which is dark on a light
   page and light on a dark one. What decides whether a logo is legible is the ground under it,
   not the device, and `tests/themes/test_brand_logo_browser.py` measures that ground.

   `data-brand-ground="accent"` is a chip painted in the accent colour (studio, tally), where
   neither the page nor its opposite is the answer: the palette is. `theme._brand_on_accent`
   emits the two `display` values beside the colour roles, in the same light and dark `:root`
   blocks, and `:root` in those two rules is what lets them outrank the scheme rules above.

   `body` is in every selector so these outrank a theme's `body.theme-<name> .site-brand img`,
   and none of them sets anything but `display`: the size is still the theme's. */
body .site-brand .brand-logo-dark { display: none; }
body .site-brand[data-brand-ground="inverse"] .brand-logo-light { display: none; }
body .site-brand[data-brand-ground="inverse"] .brand-logo-dark { display: inline-block; }
html[data-color-scheme="dark"] body .site-brand .brand-logo-light { display: none; }
html[data-color-scheme="dark"] body .site-brand .brand-logo-dark { display: inline-block; }
html[data-color-scheme="dark"] body .site-brand[data-brand-ground="inverse"] .brand-logo-light { display: inline-block; }
html[data-color-scheme="dark"] body .site-brand[data-brand-ground="inverse"] .brand-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
  html[data-color-scheme="auto"] body .site-brand .brand-logo-light { display: none; }
  html[data-color-scheme="auto"] body .site-brand .brand-logo-dark { display: inline-block; }
  html[data-color-scheme="auto"] body .site-brand[data-brand-ground="inverse"] .brand-logo-light { display: inline-block; }
  html[data-color-scheme="auto"] body .site-brand[data-brand-ground="inverse"] .brand-logo-dark { display: none; }
}
:root body .site-brand[data-brand-ground="accent"] .brand-logo-light { display: var(--brand-logo-on-accent-light, inline-block); }
:root body .site-brand[data-brand-ground="accent"] .brand-logo-dark { display: var(--brand-logo-on-accent-dark, none); }
:where(.site-brand) .brand-logo { vertical-align: middle; }
/* **A wordmark is wider than the name it replaces, so it is capped by width as well.** Every
   theme sizes the logo by height alone (`max-height: 2–3rem; width: auto`), which is right for
   the shop's *name* the headers were designed around and wrong for an 8:1 wordmark: at 2.75rem
   tall that is 350px, which crushed counter's search box to its magnifier, pushed tally's header
   past the window and scrolled linden's sideways on a phone. With both maxima an image scales
   down proportionally, so a typical 4:1 logo is untouched and only a long one gets shorter.
   `vw` rather than `%`: a percentage inside a flex or grid item that sizes to its content
   resolves against a width the image is itself deciding — which is also how voyage's logo came
   out 0x0 on a phone, under every theme's `body.theme-<name> img { max-width: 100% }`. That rule
   is (0,1,2), so this needs real specificity rather than `:where()`, or it is a rule in the file
   that never applies. `tests/themes/test_brand_logo_layout_browser.py` measures every header. */
body .site-brand .brand-logo { max-width: min(calc(15rem * var(--logo-scale, 1)), 45vw); }
/* **The merchant chooses how big the logo is in the header** (Design → `logo_size`, stamped on
   <body> as `data-logo-size`). Every theme writes its logo height as
   `calc(<its own rem> * var(--logo-scale, 1))`, so "Standard" is exactly the design as drawn and
   every other size is a multiple of *that* theme's height rather than one number forced on
   twelve headers of different proportions. The scale is set on `.site-header` only: the width
   cap above scales with it and is still bounded by the window, so a long wordmark grows until
   it would crowd the header and no further. */
body[data-logo-size="small"] .site-header { --logo-scale: 0.8; }
body[data-logo-size="large"] .site-header { --logo-scale: 1.4; }
body[data-logo-size="x-large"] .site-header { --logo-scale: 1.9; }
body[data-logo-size="xx-large"] .site-header { --logo-scale: 2.5; }
/* A phone header is a row shared with the menu button and the basket, so the two largest sizes
   are held back there: at 2.5 times its design height a logo was most of a 390px screen's first
   glance, and the merchant choosing "Extra large" was looking at their desktop. 780px, not 720:
   voyage's own three-track phone header (`theme.css`) switches on at 780px, so a shop between
   721 and 780px wide was drawing the *desktop* scale inside the *phone* layout — the one width
   band where neither theme nor this file had scaled anything back. */
@media (max-width: 780px) {
  body[data-logo-size="x-large"] .site-header { --logo-scale: 1.5; }
  body[data-logo-size="xx-large"] .site-header { --logo-scale: 1.7; }
}
/* **The footer logo is its own setting** (`footer_logo_size` / `data-footer-logo-size`), not a
   copy of the header's. It used to keep the design's drawn size regardless of what the header
   was told, on the reasoning that a logo sized to lead the page is too big for the line of small
   print under it — sound for the *default*, and it left no way for a merchant who wanted more
   weight in the footer to get it. Same variable (`--logo-scale`), scoped to `.site-footer`
   instead of `.site-header`, so every theme's own height calc responds to it without a second
   copy of that arithmetic. In a footer the brand sits in a column of known width, and at 1024px
   linden's is narrower than 15rem — so the column is the cap, a block column rather than the
   auto-sized grid track that made a percentage collapse in voyage's header. */
body[data-footer-logo-size="small"] .site-footer { --logo-scale: 0.8; }
body[data-footer-logo-size="large"] .site-footer { --logo-scale: 1.4; }
body[data-footer-logo-size="x-large"] .site-footer { --logo-scale: 1.9; }
body[data-footer-logo-size="xx-large"] .site-footer { --logo-scale: 2.5; }
body .site-footer .site-brand .brand-logo { max-width: min(calc(15rem * var(--logo-scale, 1)), 100%); }

/* ---------------------------------------------------------------- Social links
   `partials/_social_link.html` puts the network's glyph to the left of its name, in every footer
   and in voyage's utility bar. The glyph is `currentColor`, so it takes whatever ink the theme
   gives the link — hover included — and there is no colour here to go wrong in dark mode.
   `inline-flex` keeps the link's outer behaviour exactly as it was (inline in a row, blockified
   in the footers' grid columns). The doubled class outranks a theme's
   `body.theme-<name> .footer-col a` at (0,2,1) without naming any theme. */
body .social-link.social-link { display: inline-flex; align-items: center; gap: 0.5em; }
body .social-link .social-icon { width: 1.05em; height: 1.05em; flex: none; }
/* The name is its own element (never bare text beside the icon) so a theme with no room for it
   — voyage's phone utility strip — can visually hide just this and keep the glyph, without
   touching the link a screen reader announces. `display: inline` changes nothing here: it is
   what a `<span>` already is, and this line exists so every theme that inherits this partial
   without overriding it has a real rule to style, not the browser default by coincidence. */
body .social-link .social-name { display: inline; }

/* A saved address at checkout, with its Remove beside it. */
body .saved-address { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 0.75rem; }

/* ---------------------------------------------------------------- Page banner
   `{% page_banner %}` (`apps/core/templatetags/banner_tags.py`), first child of `<main>` on a
   page with a header image set — a built page's own, else the shop's default
   (`StoreSettings.header_image`). No overlaid text (the heading right below it says what the
   page is), so this is geometry only: full-bleed, a bounded height so a merchant's portrait
   photograph does not push the page's own content off the first screen, and the picture itself
   cropped to fill it. `object-fit: cover` needs an explicit height on its box or it has nothing
   to fill — the same reasoning `.pb-hero-image` and the carousel's slides already follow. */
.page-banner { width: 100%; height: clamp(10rem, 24vw, 18rem); overflow: hidden; }
.page-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------------------------------------------------------------- Contact form & enquiries
   `storefront/support/_contact_form.html` is the one contact form (`/contact/`, every Contact
   block, the "Enquire with us" dialog); `_contact_dialog.html` is it in a `<dialog>`, and
   `partials/_enquire.html` is the panel a product page offers when it is arranged on request, or
   cannot be bought right now. Shared markup no theme overrides, so this file owns it for every
   theme. Paragraph and heading margins are written `body p.x` / `body .x h2`: every theme resets
   `p` and `h1`–`h4` at (0,1,2), which a one-class rule here would lose to. */
body .field-optional { font-weight: 400; color: var(--c-muted); font-size: 0.85em; }
body p.contact-about { margin: 0 0 1rem; }
/* The chosen option, appended to the line above by Alpine. It is inline text in that line and needs
   no rule of its own; this one exists so the sweep for undefined classes reads it as owned. */
body .contact-about .contact-variant { font-weight: inherit; }
body .contact-card > h2 { margin: 0 0 1rem; }
body .contact-sent { padding: 0.25rem 0; }
body .contact-sent h3.contact-sent-title { margin: 0 0 0.5rem; font-size: 1.1rem; }
body .contact-sent p { margin: 0 0 0.75rem; }

body dialog.contact-dialog {
  width: min(34rem, calc(100vw - 2rem));
  max-height: min(90dvh, 52rem);
  padding: 0;
  border: 1px solid var(--c-line);
  border-radius: var(--c-radius);
  background: var(--c-bg);
  color: var(--c-text);
  box-shadow: var(--c-shadow);
  overflow: hidden;
}
/* A column whose body is the scroller: the dialog has a fixed ceiling, so every box between it
   and the scroller needs `min-height: 0` or the body grows to its content and the form's foot is
   cut off with nothing to scroll (the page builder's panel met this). */
body dialog.contact-dialog[open] { display: flex; flex-direction: column; }
body dialog.contact-dialog::backdrop { background: color-mix(in srgb, var(--c-text) 45%, transparent); }
body .contact-dialog-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1rem 1.25rem; border-bottom: 1px solid var(--c-line);
}
body .contact-dialog-head h2.contact-dialog-title { margin: 0; font-size: 1.15rem; }
body .contact-dialog-close {
  flex: none; width: 2.25rem; height: 2.25rem; display: inline-grid; place-items: center;
  border: 1px solid var(--c-line); border-radius: 999px; background: transparent; color: inherit;
  font-size: 1.35rem; line-height: 1; cursor: pointer;
}
body .contact-dialog-close:hover { background: var(--c-soft); }
body .contact-dialog-close:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
body .contact-dialog-body { min-height: 0; overflow-y: auto; padding: 1.25rem; }
/* The form is one column wherever it sits (a card, a Contact block's grid track, the dialog), and
   may be narrower than its content, or a long hint pushes a phone sideways. */
body form.contact-form { min-width: 0; }
body form.contact-form > .btn { margin-top: 0.25rem; }
/* On a phone the dialog is a sheet across the width, inside the page's 16px gutter. */
@media (max-width: 480px) {
  body dialog.contact-dialog { width: calc(100vw - 2rem); max-height: calc(100dvh - 2rem); }
}

body .enquire {
  margin: 0 0 1.25rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--c-line);
  border-radius: var(--c-radius);
  background: var(--c-soft);
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1rem;
}
body p.enquire-text { margin: 0; flex: 1 1 14rem; min-width: 0; }
body .enquire .btn { flex: none; }
/* `.btn-enquire` is the class `_enquire.html` puts on the button (alongside `.btn`, which does
   the actual painting on every theme). Named here so the shared-template sweep can see it is
   accounted for — a theme adds its own rule only where bare `.btn` is not already the fill
   (voyage's full width, petal's and studio's outline-vs-fill split; see their theme.css). */
body .btn-enquire { }
/* Beside the buy button rather than instead of it: a line, not a panel. */
body .enquire.enquire-alongside { padding: 0; border: 0; background: none; margin-top: 0.75rem; }
/* What the customer told us beyond their message, above the conversation on their own page. */
body dl.contact-answers { display: grid; gap: 0.35rem; margin: 0 0 1.5rem; padding: 1rem 1.15rem; border: 1px solid var(--c-line); border-radius: var(--c-radius); background: var(--c-soft); }
body dl.contact-answers > div { display: grid; grid-template-columns: minmax(0, 12rem) minmax(0, 1fr); gap: 1rem; }
body dl.contact-answers dt { font-weight: 600; }
body dl.contact-answers dd { margin: 0; }
@media (max-width: 480px) { body dl.contact-answers > div { grid-template-columns: minmax(0, 1fr); gap: 0.1rem; } }

/* The refund request's "which items" list (`returns/request_refund.html`): a fieldset of
   `.field-check` rows. `body` because every theme resets `p` and `legend` spacing at (0,1,2). */
body :is(.refund-lines, .refund-payout) { border: 0; padding: 0; margin: 0 0 1rem; min-width: 0; }
body :is(.refund-lines, .refund-payout) legend { padding: 0; margin-bottom: 0.35rem; font-weight: 600; }
/* Where to send the money (`returns/_payout_fields.html`): the sentence above the two boxes is
   `p`, which every theme resets to `margin: 0`, so the gap below it is written on the box. */
body .refund-payout .payout-boxes { display: grid; gap: 0.75rem; margin-top: 0.75rem; }
body .refund-lines p { margin: 0 0 0.75rem; }
body .refund-lines .field-check { margin-bottom: 0.5rem; }

/* Event feedback: where else to review the shop, and the photographs of the date. */
body .review-link-list {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
body .feedback-thanks + .review-elsewhere,
body .card + .card.review-elsewhere {
  margin-top: 1rem;
}
body .review-elsewhere h2 {
  margin: 0 0 0.25rem;
}
body .event-photos {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
  gap: 1rem;
}
body .event-photos img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--c-radius, 0.5rem);
}
body .event-photos p {
  margin: 0.4rem 0 0;
  color: var(--c-muted);
  font-size: 0.9rem;
}
body .past-events { margin-top: 1.5rem; }
body .past-event-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.75rem; }
body .past-event-list li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--c-line);
}
body .past-event-list li:last-child { border-bottom: 0; padding-bottom: 0; }
body .past-event-actions { display: flex; flex-wrap: wrap; gap: 1rem; }


/* ------------------------------------------------------------------- product cards */
/* A card's picture is never taller than a comfortable part of the window. The height came from the
   width: a theme's `aspect-ratio` on a card that is 40rem wide (studio) or one of "2 columns" the
   merchant chose in the page builder is a picture 800px tall, so a listing was a column of
   screen-high photographs — too big to read the name under, and too long to scroll past. The
   ratio still applies on every card that fits under the cap, so a normal grid is unchanged. */
body[class*="theme-"] .product-card .product-media { max-height: min(24rem, 55svh); }


/* ---------------------------------------------------------------------------- picture lightbox */
/* `static/js/lightbox.js` marks a picture it can enlarge and builds the one dialog they share, so
   none of this exists on a page the script never reached. The dialog is drawn over a black wash
   whatever the palette says — it holds somebody else's photograph, and white on black is the only
   pair that reads over all of them (the collection cards and lookbook captions keep the rule). */
.pb-zoomable { cursor: zoom-in; }
.pb-zoom-unit { cursor: zoom-in; }
.pb-zoomable:focus-visible { outline: 3px solid var(--c-primary, currentColor); outline-offset: 3px; }
html.pb-lightbox-open { overflow: hidden; }
body .pb-lightbox {
  position: fixed; inset: 0; box-sizing: border-box; width: 100%; height: 100%;
  max-width: none; max-height: none; margin: 0; padding: 0; border: 0;
  background: rgb(0 0 0 / 0.94); color: #fff;
}
body .pb-lightbox[open] { display: grid; place-items: center; }
body .pb-lightbox::backdrop { background: rgb(0 0 0 / 0.94); }
body .pb-lightbox .pb-lightbox-figure {
  margin: 0; display: grid; justify-items: center; gap: 0.75rem;
  max-width: 100%; max-height: 100%; padding: 4.25rem 1rem 3.25rem; box-sizing: border-box;
  touch-action: pan-y;
}
body .pb-lightbox .pb-lightbox-image {
  display: block; width: auto; height: auto; max-width: 100%;
  max-height: calc(100vh - 9.5rem); max-height: calc(100dvh - 9.5rem);
  object-fit: contain; border-radius: 4px; user-select: none;
}
body .pb-lightbox .pb-lightbox-caption {
  margin: 0; max-width: 46rem; text-align: center; font-size: 0.95rem; line-height: 1.4; color: #fff;
}
body .pb-lightbox .pb-lightbox-caption[hidden] { display: none; }
/* The counter spans the dialog's width and is drawn after the buttons, so on a phone, where the
   arrows sit at the foot, it lay over both and took every tap meant for them. It is only a label. */
body .pb-lightbox .pb-lightbox-count {
  position: absolute; inset-inline: 0; bottom: 0.9rem; margin: 0; text-align: center;
  font-size: 0.8rem; letter-spacing: 0.08em; color: #fff; pointer-events: none;
}
body .pb-lightbox .pb-lightbox-count[hidden] { display: none; }
body .pb-lightbox .pb-lightbox-button {
  position: absolute; display: grid; place-items: center; box-sizing: border-box;
  width: 2.75rem; height: 2.75rem; padding: 0; border-radius: 999px; cursor: pointer;
  border: 1px solid rgb(255 255 255 / 0.5); background: rgb(0 0 0 / 0.55); color: #fff;
  touch-action: manipulation; z-index: 1;
}
body .pb-lightbox .pb-lightbox-button[hidden] { display: none; }
body .pb-lightbox .pb-lightbox-button:hover { background: rgb(255 255 255 / 0.18); }
body .pb-lightbox .pb-lightbox-button:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
body .pb-lightbox .pb-lightbox-close { top: 0.75rem; inset-inline-end: 0.75rem; }
body .pb-lightbox .pb-lightbox-prev { top: 50%; inset-inline-start: 0.75rem; transform: translateY(-50%); }
body .pb-lightbox .pb-lightbox-next { top: 50%; inset-inline-end: 0.75rem; transform: translateY(-50%); }
@media (max-width: 600px) {
  /* On a phone the arrows sit under the picture, out of its way: big enough for a thumb, and lifted
     clear of the home-indicator strip and the browser's own toolbar, where a tap is often taken by
     the system instead. The counter sits on the same line between them. */
  body .pb-lightbox .pb-lightbox-button { width: 3.5rem; height: 3.5rem; }
  body .pb-lightbox .pb-lightbox-prev, body .pb-lightbox .pb-lightbox-next {
    top: auto; bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem); transform: none;
  }
  body .pb-lightbox .pb-lightbox-prev { inset-inline-start: 1rem; }
  body .pb-lightbox .pb-lightbox-next { inset-inline-end: 1rem; }
  body .pb-lightbox .pb-lightbox-count { bottom: calc(env(safe-area-inset-bottom, 0px) + 2.1rem); }
  body .pb-lightbox .pb-lightbox-figure { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 6rem); }
  body .pb-lightbox .pb-lightbox-image { max-height: calc(100vh - 11rem); max-height: calc(100dvh - 11rem); }
  body .pb-lightbox .pb-lightbox-close { width: 3rem; height: 3rem; }
}
@media (prefers-reduced-motion: no-preference) {
  body .pb-lightbox[open] .pb-lightbox-image { animation: pb-lightbox-in 180ms ease-out; }
}
@keyframes pb-lightbox-in { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: none; } }

/* ------------------------------------------------------------------------------- maps */
/* Leaflet numbers its own layers (panes 400, controls 800–1000) and, left alone, those numbers
   take part in the *page's* stacking, so a map scrolled under the pinned header was drawn over it.
   A map is one box: what is inside it stays inside it. */
.leaflet-container { isolation: isolate; }

/* ------------------------------------------------------------------------------- headroom */
/* The header comes back on a scroll up (`static/js/headroom.js`). Everything here is behind
   `html.js-headroom`, which the script adds itself, so a page that never ran it is unchanged.

   `!important` on the position is deliberate and is the one place it is: a theme lays its
   header out at (0,2,x) or more (`position: relative; z-index: 30`, studio's clear header
   `position: absolute`, voyage's `:has()` band at (1,4,1)), and pinning has to win over every
   one of them without this file knowing what they are.

   It is hidden by moving `top`, not by a transform: a transform makes the header the containing
   block of every `position: fixed` descendant, and the basket drawer and its backdrop are fixed
   descendants of the header on every theme. */
html.js-headroom { scroll-padding-top: var(--hr-h, 0px); }
html.js-headroom .hr-spacer { display: block; }
html.js-headroom .hr-fixed {
  position: fixed !important; top: 0 !important; bottom: auto !important;
  inset-inline: 0 !important; z-index: 55 !important;
  transition: top 0.22s ease, box-shadow 0.22s ease;
}
html.js-headroom .hr-fixed:not(.hr-hidden) {
  box-shadow: 0 8px 20px -12px color-mix(in srgb, var(--c-text, currentColor) 45%, transparent);
}
html.js-headroom .hr-fixed.hr-snap { transition: none; }
html.js-headroom .hr-fixed.hr-hidden { top: calc(-1 * var(--hr-h, 0px) - 1.5rem) !important; }
/* A header that is see-through in the flow (over a photograph, or a wash) has only the page's
   own text under it once it is pinned. A theme names its own ground in `--hr-ground`. */
html.js-headroom .hr-fixed.hr-clear { background: var(--hr-ground, var(--color-surface, var(--c-bg))) !important; }
@media (prefers-reduced-motion: reduce) {
  html.js-headroom .hr-fixed { transition: none; }
}
@media print {
  html.js-headroom .hr-fixed { position: static !important; }
  html.js-headroom .hr-spacer { display: none; }
}

/* ---------------------------------------------------------------------------- buy bar
   `static/js/buy_bar.js` builds this from an inert <template> in `partials/_buy_bar.html`, so with
   the script blocked none of it exists and there is nothing here to have gone wrong. Every rule is
   behind `html.js-buy-bar`, a class the script adds itself, for `reveal.js`'s reason. The bar is
   the palette's own ground and ink (the theme restates `--c-*` on `body.theme-<name>`) and its
   button is the theme's own `.btn-buy`, which is how it stays legible on all twelve without a
   colour of its own.

   It is slid in with a transform, which is fine here (nothing fixed lives inside it), and is
   `visibility: hidden` until shown as well, so off screen it is not a stop in the tab order. */
html.js-buy-bar .buy-bar {
  /* `--bb-end` is room a theme keeps at the inline end (counter's docked order rail), and
     `--bb-lift` is the height of the cookie question when one is on screen: it is fixed to the
     same edge, so the bar stands on it. */
  position: fixed; inset: auto var(--bb-end, 0px) var(--bb-lift, 0px) 0; z-index: 40;
  background: var(--c-bg); color: var(--c-text);
  border-top: 1px solid var(--c-line-strong);
  box-shadow: 0 -10px 24px -14px color-mix(in srgb, var(--c-text) 40%, transparent);
  padding: 0.65rem max(1rem, env(safe-area-inset-right)) max(0.65rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
  transform: translateY(100%); visibility: hidden;
  transition: transform 0.22s ease, visibility 0s linear 0.22s;
}
html.js-buy-bar .buy-bar.is-shown { transform: none; visibility: visible; transition: transform 0.22s ease; }
html.js-buy-bar .buy-bar-inner {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  max-width: 76rem; margin-inline: auto;
}
html.js-buy-bar .buy-bar .buy-bar-facts {
  display: flex; flex-direction: column; min-width: 0; margin: 0; line-height: 1.25;
}
html.js-buy-bar .buy-bar-name {
  font-weight: 600; font-size: 0.95rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
html.js-buy-bar .buy-bar-price { font-size: 0.9rem; color: var(--c-text); }
html.js-buy-bar .buy-bar-price:empty { display: none; }
/* `width: auto` and `flex: none`: voyage's `.btn-buy` is full width, and the bar is one line. */
html.js-buy-bar .buy-bar .buy-bar-btn { width: auto; flex: none; white-space: nowrap; }
/* Room at the foot of the page for what is under the bar, only while it is there. */
html.has-buy-bar body { padding-bottom: var(--bb-h, 0px); }
@media (max-width: 480px) {
  html.js-buy-bar .buy-bar-name { font-size: 0.85rem; }
  html.js-buy-bar .buy-bar .buy-bar-btn { padding-inline: 1.1rem; }
}
@media (prefers-reduced-motion: reduce) {
  html.js-buy-bar .buy-bar, html.js-buy-bar .buy-bar.is-shown { transition: none; }
}
@media print { html.js-buy-bar .buy-bar { display: none; } }
