/* ─────────────────────────────────────────────────────────────────────────
   commerce.css — the buyer-facing commerce surface.
   Loaded ONLY by the pages that have one: the generated product pages
   (wine/<id>.html) and the order page (order.html). Never by index, shop or
   product.html.

   ⛔ WHY A SEPARATE FILE
   css/styles.css is the FROZEN mobile design (projects/website/CLAUDE.md,
   HOSTING-PLAN §14 invariant 5). It is not edited. This is the same pattern
   css/legal.css already uses: a new file, loaded only where it is needed, so
   the three original pages cannot be affected at all — they never load it.

   ⛔ DEAD-CALM ZONE (HOSTING-PLAN §14 invariant 8)
   Checkout, the age gate and the legal pages carry NO motion. There is not a
   single `transition`, `animation` or `transform` in this file, and there must
   never be one. A person about to spend €990 on a 79-year-old bottle is not
   entertained by a sheen; they are trying to read carefully.

   ⚠ Two consequences of that, both deliberate:
     · The site-wide button mechanism in styles.css (`.pill`, `.cta`, `.chip`
       …) animates — a 0.4s box-shadow flood plus a `scale(0.94)` press, and
       motion.css adds more on top. So the commerce controls do NOT use
       `.pill`. `.wb-btn` is a separate, still, un-animated control that
       borrows the same tokens.
     · `prefers-reduced-motion` needs no block here. There is nothing to
       reduce. That is the point.

   ⛔ NOTHING HERE DEPENDS ON AN ANIMATION SUCCEEDING (invariant 9). Every
   element is either visible or removed from the document. No opacity:0
   waiting for a script.

   Design system: "Cellar Light" tokens from styles.css :root, with literal
   fallbacks so a partial stylesheet load still renders legibly.
   ───────────────────────────────────────────────────────────────────────── */

/* ── The load probe ────────────────────────────────────────────────────────
   js/buy.js refuses to render anything unless this rule is in effect.

   On 15 Aug the age gate shipped with its CSS in legal.css, which index, shop
   and product.html do not load — the gate rendered completely unstyled,
   `position: fixed` never applied, and it flowed into the end of the document
   at width 0. The lesson written down at the time was "always both, never
   one". This turns that into something the code can actually check instead of
   something a human has to remember. */
.wb-css-probe {
  position: absolute !important;
  left: -9999px !important;
  width: 1px;
  height: 1px;
  /* buy.js reads exactly this value. Do not change it without changing
     hasCommerceCss() in js/buy.js. */
  outline-color: rgb(93, 26, 38);
}

/* ── The hidden contract ───────────────────────────────────────────────────
   ⛔ LOAD-BEARING. worker/src/availability.ts ships the buy control with the
   `hidden` attribute set and removes that attribute ONLY inside its single
   `state === "available"` branch. If any rule in any stylesheet were to give
   one of these elements a `display`, `hidden` would stop hiding it and a sold
   bottle would show a buy button. `!important` here is not laziness — it is
   the guarantee that a later rule cannot break the fail-safe. */
.wb-buy[hidden],
.wb-availability[hidden],
.wb-panel[hidden],
.wb-order[hidden],
.wb-form[hidden],
.wb-price[hidden],
[data-wb-buy][hidden],
[data-wb-availability][hidden] {
  display: none !important;
}

/* ── ⚠ THE GLOBAL <section> TRAP ──────────────────────────────────────────
   styles.css line 232 sets, for the homepage's full-screen bands:

       section { min-height: 100svh; padding-left: 24px; padding-right: 24px }

   Every <section> on the site inherits it, and styles.css already carries an
   opt-out list (`.shop-head, .shop-grid, .pd-panel, .pd-info { min-height:
   auto }`) with a comment saying it "would leave huge gaps". It would, and it
   did: measured before this rule existed, `.wb-commerce` was forced to
   **714px tall** on a 375×812 screen — about 160px of empty space below the
   health warning — and its children sat at **x=48** while the surrounding page
   sat at x=24, because the section's own 24px padding stacks on top of
   `.legal-body`'s.

   This is the same opt-out, for the same reason. Any NEW <section> added to a
   commerce surface needs to be added here too. */
.wb-commerce,
.wb-panel {
  min-height: auto;
  padding-left: 0;
  padding-right: 0;
}

/* ── The commerce block on a product page ─────────────────────────────────
   Sits inside .legal-body, which already supplies the 24px inline padding and
   the 68ch measure. */
.wb-commerce {
  margin-top: 36px;
  padding-top: 28px;
  border-top: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
}

.wb-commerce h2 {
  /* Matches .legal-body h2 so the section does not read as a foreign object
     dropped into the page. */
  font-family: 'Fraunces', Georgia, serif;
  font-size: 22px;
  line-height: 1.25;
  font-weight: 600;
  color: var(--ink, #1f1d1a);
  margin: 0 0 12px;
}

/* ── Availability notice ──────────────────────────────────────────────────
   `data-wb-availability`. Ships EMPTY and `hidden`; the Worker fills the text
   from D1 and un-hides it. With no transform it is not rendered at all, and
   the page makes no claim about stock — which is the whole design. */
.wb-availability {
  margin: 0 0 14px;
  padding: 12px 14px;
  border-left: 3px solid var(--wine, #5d1a26);
  border-radius: 0 12px 12px 0;
  background: rgba(93, 26, 38, 0.04);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink, #1f1d1a);
}

/* The four states are distinguished by weight and tint only — never by a
   colour that implies urgency, and never with a count. Estonian alcohol
   advertising law limits copy to neutral product facts, and workspace rule 2
   forbids inventing a stock number. */
.wb-availability[data-wb-state='sold'],
.wb-availability[data-wb-state='withdrawn'] {
  border-left-color: var(--muted, #6f6a62);
  background: rgba(31, 29, 26, 0.04);
  color: var(--muted, #6f6a62);
}

/* ── The buy control ──────────────────────────────────────────────────────
   `data-wb-buy`. Ships `hidden`. availability.ts REMOVES this element outright
   for every state except `available`, so a sold bottle's page contains no buy
   button at all — not a disabled one, not a hidden one, none. */
.wb-buy {
  margin: 0 0 6px;
}

.wb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px; /* comfortable touch target on a phone */
  padding: 14px 22px;
  border: 0;
  border-radius: 999px;
  font-family: var(--font-btn, 'Sora', system-ui, sans-serif);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  /* ⛔ Dead-calm. Explicit, so that a future rule in motion.css that widens
     its selector list cannot quietly animate a checkout control. */
  transition: none !important;
  animation: none !important;
}

.wb-btn--wine {
  background: var(--wine, #5d1a26);
  color: #faf7f2;
  box-shadow: none;
}

.wb-btn--quiet {
  background: transparent;
  color: var(--wine, #5d1a26);
  box-shadow: inset 0 0 0 1.5px var(--wine, #5d1a26);
}

.wb-btn[disabled],
.wb-btn[aria-disabled='true'] {
  opacity: 0.55;
  cursor: default;
}

.wb-btn--block {
  display: flex;
  width: 100%;
}

/* The price slot. Ships EMPTY and hidden.
   ⛔ NO PRICE IS WRITTEN INTO THE STATIC HTML. The only authoritative price
   is bottles.price_cents in D1, and the buyer sees it on Stripe's hosted
   checkout page before anything is charged. If a lawyer decides the price must
   appear before the buy button, availability.ts must fill this slot from the
   same D1 read that decides the state — do not let the build put a catalogue
   price here, because a catalogue price and a charged price can drift. */
.wb-price {
  display: block;
  font-family: 'Fraunces', Georgia, serif;
  font-size: 26px;
  font-weight: 600;
  color: var(--ink, #1f1d1a);
  margin: 0 0 10px;
}

.wb-fine {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted, #6f6a62);
}

/* ── The status panel — the 409 lives here ────────────────────────────────
   Every bottle is one-of-one, so two people wanting the same 1975 Branaire is
   the NORMAL case, not an edge case. The loser of that race gets this: a
   designed answer that says what happened, what else exists, and how to be
   told if another arrives. It is not an error message and it must never look
   like one — no red, no alert icon, no "something went wrong". */
.wb-panel {
  margin: 16px 0 0;
  padding: 18px 18px 20px;
  border: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
  border-radius: 16px;
  background: var(--surface, #ffffff);
  /* js/buy.js moves focus here so the answer is read. Focus scrolls the
     element to the top of the viewport, and .topbar--solid is sticky — without
     this the panel's own heading lands underneath the header, so the buyer
     arrives at the middle of the explanation. Not motion: this changes where an
     instant scroll stops, not how it travels. */
  scroll-margin-top: 96px;
}

.wb-panel__title {
  font-family: 'Fraunces', Georgia, serif;
  font-size: 20px;
  line-height: 1.25;
  font-weight: 600;
  color: var(--ink, #1f1d1a);
  margin: 0 0 10px;
}

.wb-panel p {
  margin: 0 0 12px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--muted, #6f6a62);
}

.wb-panel p:last-child {
  margin-bottom: 0;
}

/* The panel is the one thing on the page that must be read, so it takes
   focus. The outline is the site's own :focus-visible treatment. */
.wb-panel:focus {
  outline: none;
}

/* An error that is genuinely an error — a network failure, a 500 — is styled
   the same calm way. The difference is in the words, not the decoration. */
.wb-panel--problem {
  border-color: rgba(93, 26, 38, 0.35);
}

/* ── Alternatives: nearest vintages ───────────────────────────────────────
   Static, always present, honest in every state: these links are useful
   whether or not this bottle is still here. Sold-out years must never be a
   dead end (projects/website/CLAUDE.md, honesty rule 4). */
.wb-alts {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
}

.wb-alts li {
  margin: 0 0 8px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--muted, #6f6a62);
}

.wb-alts a {
  color: var(--ink, #1f1d1a);
  text-decoration: none;
  border-bottom: 1px solid rgba(93, 26, 38, 0.3);
}

.wb-alts a:hover {
  color: var(--wine, #5d1a26);
}

/* ── The enquiry form ─────────────────────────────────────────────────────
   POST /api/enquiry. An enquiry is not a sale, which is why this path is
   lawful today while checkout is not (HOSTING-PLAN §3, §11 step A4).

   ⚠ It collects an email address and an optional message. Nothing else — the
   D1 `enquiries` table has no name, phone or address column and this form does
   not invent one. */
.wb-form {
  margin: 14px 0 0;
}

.wb-field {
  display: block;
  margin: 0 0 14px;
}

.wb-label {
  display: block;
  font-family: var(--font-btn, 'Sora', system-ui, sans-serif);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted, #6f6a62);
  margin: 0 0 6px;
}

.wb-input,
.wb-textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 13px 14px;
  border: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
  border-radius: 12px;
  background: var(--surface, #ffffff);
  font-family: 'Fraunces', Georgia, serif;
  /* 16px minimum: anything smaller makes iOS Safari zoom the page on focus,
     which on a checkout screen reads as the layout lurching. */
  font-size: 16px;
  line-height: 1.4;
  color: var(--ink, #1f1d1a);
  transition: none !important;
}

.wb-textarea {
  min-height: 96px;
  resize: vertical;
}

.wb-input:focus,
.wb-textarea:focus {
  border-color: var(--wine, #5d1a26);
}

.wb-form__note {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted, #6f6a62);
}

/* The reply region. `aria-live="polite"` in the markup, so a screen reader is
   told what happened without the focus being stolen.
   ⛔ It says nothing until the server has answered. The checkout deleted on
   12 Aug printed "Order placed" after throwing every field away; nothing in
   this file may ever claim success that a 2xx did not justify. */
.wb-form__status {
  margin: 12px 0 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink, #1f1d1a);
}

.wb-form__status:empty {
  display: none;
}

.wb-form__status[data-tone='problem'] {
  color: var(--wine, #5d1a26);
}

/* ── The health warning ───────────────────────────────────────────────────
   Legally required site-wide and on every commerce screen (HOSTING-PLAN §14
   invariant 4). It is also in the footer of every page; this is the copy that
   sits inside the transaction itself, where it cannot be scrolled past. */
.wb-warning {
  margin: 18px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted, #6f6a62);
}

/* ── The order page ───────────────────────────────────────────────────────
   /order/<id>. Rendered from D1 by the Worker, never from client state.
   `.wb-order` ships hidden and empty; `.wb-order__missing` ships visible. The
   Worker un-hides the first, fills its slots and removes the second. If the
   Worker does not run, or the lookup fails, the buyer sees only the honest
   "could not be loaded" message and no fabricated order. */
.wb-order {
  margin: 0;
  padding: 20px;
  border: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
  border-radius: 16px;
  background: var(--surface, #ffffff);
}

.wb-order dl {
  margin: 0;
}

.wb-order__row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
}

.wb-order__row:last-child {
  border-bottom: 0;
}

.wb-order dt {
  flex: 0 0 100%;
  font-family: var(--font-btn, 'Sora', system-ui, sans-serif);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted, #6f6a62);
}

.wb-order dd {
  flex: 1 1 auto;
  margin: 0;
  font-size: 16px;
  line-height: 1.45;
  color: var(--ink, #1f1d1a);
}

/* An empty slot renders as an em dash rather than as nothing, so a missing
   field is visibly missing instead of silently absent. */
.wb-order dd:empty::before {
  content: '—';
  color: var(--muted, #6f6a62);
}

.wb-order__missing {
  margin: 0;
  padding: 18px;
  border: 1px solid var(--hairline, rgba(31, 29, 26, 0.14));
  border-radius: 16px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--muted, #6f6a62);
}

/* ── Desktop ──────────────────────────────────────────────────────────────
   Everything above is the phone layout and a phone-width browser never reads
   this block, exactly as css/desktop.css works. Mobile cannot drift. */
@media (min-width: 960px) {
  .wb-commerce h2 {
    font-size: 26px;
  }

  .wb-btn--block {
    display: inline-flex;
    width: auto;
  }

  .wb-order dt {
    flex: 0 0 180px;
  }

  .wb-order dd {
    flex: 1 1 0;
  }
}
