/* ==========================================================================
   PEPTORIUM - design system
   Direction: analytical instrument. The site should read as laboratory
   documentation, not as wellness commerce. Every number is set in a
   monospace face with tabular figures; the chromatogram is the signature
   motif because it is the actual artefact of peptide purity testing.

   Token names were chosen to map 1:1 onto a WordPress theme.json. That port
   is not happening - see 11-checkout-architecture.md - but the discipline it
   forced is worth keeping on its own merits, and it is what a redesign will
   consume. Do not introduce raw colour or size values below the token layer.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FONTS - NOT YET PRESENT, DELIBERATELY NOT DECLARED

   Self-hosted, never the Google Fonts CDN: serving fonts from Google sends an
   EEA visitor's IP address to Google without consent, which LG München I
   ruled on directly (3 O 17493/20). That decision is why these files have to
   sit in /assets/fonts/ rather than behind a link tag.

   Self-hosted, four files, 108 KB in total, and a browser downloads only the
   subsets it needs. The rules that used to sit here pointed at two woff2
   files that had never existed, so the site rendered in Segoe UI and
   Consolas from the day it was built and every judgement anyone made about
   this design was made against the fallback stack.

   latin-ext is not optional. It carries a c e l n o s z z, and Polish is the
   largest edition.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Archivo';
  src: url('../fonts/archivo-latin-ext.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Archivo';
  src: url('../fonts/archivo-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/jetbrains-mono-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   2. TOKENS
   -------------------------------------------------------------------------- */

:root {
  /* --- Colour -------------------------------------------------------------
     Ground is warm off-white: laboratory printout paper, not screen white.
     Ink is cool near-black so it sits slightly against the warm paper, the
     way toner does. The single accent is a chromatogram trace green.
     This design commits to a light ground. There is no dark theme: a lab
     report is printed on paper, and the concept does not survive inversion.
     -------------------------------------------------------------------- */
  --paper:          #F8F9FB;
  --paper-raised:   #fff;
  --paper-sunk:     #EEF1F5;
  --paper-ink:      #111D33;

  /* --- Ink ---------------------------------------------------------------
     Measured against every light ground this site uses: --paper #F7F6F2,
     --paper-raised #FFFFFF and --paper-sunk #EFEEE8. The worst case is
     always --paper-sunk, so that is the number quoted.

     --ink-mute was #767C82, which measured 3.63:1 and failed AA for normal
     text everywhere it was used - and it is used for 16 pieces of real copy
     including .text-mute, .card__meta, .trust__v and every spec label. It is
     now 4.71:1. That margin over the 4.5 floor is deliberate: a token sitting
     exactly on the line breaks silently the next time a background moves.

     --ink-faint is the 3:1 floor. It is for NON-TEXT and for incidental
     labels inside a graphic - never body copy. Use it for the boundary of
     anything the visitor operates (input, button, chip, variant), because
     WCAG 1.4.11 asks 3:1 of a UI component's edge and --rule-strong measures
     1.47:1. Decorative hairlines between table rows and sections stay on
     --rule / --rule-strong; those are exempt, and darkening them would turn
     a light lab printout into something much heavier.
     -------------------------------------------------------------------- */
  --ink:            #111D33;       /* 15.48:1 */
  --ink-soft:       #3A4A63;       /*  7.60:1 */
  --ink-mute:       #4E5E78;       /*  4.71:1 - AA floor for body text */
  --ink-faint:      #5F6D86;       /*  3.21:1 - non-text and control edges */

  --rule:           #DDE3EB;       /* decorative only */
  --rule-strong:    #7A8BA1;       /* decorative only - 1.47:1, not an edge */

  --signal:         #0B8FBC;       /* trace, graphics, non-text use */
  --signal-deep:    #06718F;       /* text and small UI - AA on paper */
  /* The same accent, dark enough to clear 4.5:1 over the hero video rather
     than over paper. Only the video hero uses it; anywhere else it is
     needlessly heavy. */
  --signal-hero:    #054C61;
  --signal-wash:    #ECF7FA;
  --signal-edge:    #C2E5F1;

  --warn:           #9C5D0A;
  --warn-wash:      #F9F1E4;
  --fail:           #B42318;
  --fail-wash:      #F9EDEC;

  /* --- Type ---------------------------------------------------------------
     One grotesque, one mono. Lab documents do not use serifs.
     Every fallback stack is checked for Polish diacritic coverage.
     -------------------------------------------------------------------- */
  --font-sans: 'Archivo', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace;

  /* Type scale - 1.200 minor third, 16px base. Deliberately tight: a
     datasheet has little dynamic range between heading and body. */
  --t-2xs:  0.694rem;   /* 11.1px - micro labels, legal */
  --t-xs:   0.833rem;   /* 13.3px - captions, table data */
  --t-sm:   0.9375rem;  /* 15px   - secondary body */
  --t-base: 1rem;       /* 16px   - body */
  --t-md:   1.2rem;     /* 19.2px - lead */
  --t-lg:   1.44rem;    /* 23px   - h3 */
  --t-xl:   1.728rem;   /* 27.6px - h2 */
  --t-2xl:  2.074rem;   /* 33.2px - h1 */
  --t-3xl:  2.488rem;   /* 39.8px - display */

  --lh-tight: 1.15;
  --lh-snug:  1.35;
  --lh-body:  1.6;

  --tracking-label: 0.08em;
  --tracking-tight: -0.015em;

  /* --- Space - 4px base, geometric ------------------------------------- */
  --s-1:  0.25rem;
  --s-2:  0.5rem;
  --s-3:  0.75rem;
  --s-4:  1rem;
  --s-5:  1.5rem;
  --s-6:  2rem;
  --s-7:  3rem;
  --s-8:  4rem;
  --s-9:  6rem;

  /* --- Structure -------------------------------------------------------- */
  --measure:      68ch;
  --wrap:         1200px;
  --wrap-narrow:  860px;
  --radius:       2px;      /* near-square. Lab equipment is not rounded. */
  --radius-lg:    3px;
  --border:       1px solid var(--rule);

  --shadow-card:  0 1px 2px rgba(20, 23, 26, 0.04);
  --shadow-lift:  0 2px 8px rgba(20, 23, 26, 0.07);

  --focus: 2px solid var(--signal-deep);
  --focus-offset: 2px;
}

/* --------------------------------------------------------------------------
   3. RESET AND BASE
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

/* The browser's own [hidden] rule is `display: none` at user-agent weight, so
   ANY explicit display in this file beats it. `.card` sets `display: flex`,
   which meant shop.js could set `card.hidden = true` and the card stayed on
   screen: the filter updated its bookkeeping and changed nothing visible.
   It was verified by counting `!card.hidden` in the console, which is the
   property, not the rendering - so the test agreed with the bug.
   This is the standard defensive reset. Do not remove it. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-base);
  line-height: var(--lh-body);
  font-feature-settings: 'kern' 1, 'liga' 1;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, video { max-width: 100%; height: auto; display: block; }

a {
  color: var(--signal-deep);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
a:hover { text-decoration-thickness: 2px; }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus);
  outline-offset: var(--focus-offset);
  border-radius: 1px;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}
h1 { font-size: var(--t-2xl); }
h2 { font-size: var(--t-xl); }
h3 { font-size: var(--t-lg); }
h4 { font-size: var(--t-base); }

p { margin: 0 0 var(--s-4); }
p:last-child { margin-bottom: 0; }

/* Every numeral on this site is tabular. Prices, purities, batch numbers,
   masses and volumes must align in columns without exception. */
.num, table, .mono, input[type="number"], time, .price {
  font-variant-numeric: tabular-nums;
}

.mono {
  font-family: var(--font-mono);
  font-feature-settings: 'zero' 1;   /* slashed zero - instrument convention */
}

/* --------------------------------------------------------------------------
   4. LAYOUT
   -------------------------------------------------------------------------- */

.wrap      { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--s-5); }
.wrap--narrow { max-width: var(--wrap-narrow); }
.measure   { max-width: var(--measure); }
.stack > * + * { margin-top: var(--s-4); }
.stack-lg > * + * { margin-top: var(--s-6); }

.section    { padding-block: var(--s-8); }
.section--tight { padding-block: var(--s-6); }
.section--sunk  { background: var(--paper-sunk); }

.grid { display: grid; gap: var(--s-5); }
.grid--products { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.cluster { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }

/* --------------------------------------------------------------------------
   5. THE LABEL - the small uppercase mono tag used throughout.
   This is the workhorse of the lab-document register.
   -------------------------------------------------------------------------- */

.label {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-mute);
  display: block;
}
.label--signal { color: var(--signal-deep); }

/* Section heading with a rule that runs to the edge - datasheet convention */
.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}
.section-head::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
}

/* --------------------------------------------------------------------------
   6. CHROMATOGRAM
   The signature motif. Rendered from real purity data by chromatogram.js,
   so the graphic is the measurement rather than decoration.
   -------------------------------------------------------------------------- */

.chromatogram {
  display: block;
  width: 100%;
  overflow: visible;
}
.chromatogram .cg-baseline { stroke: var(--rule-strong); stroke-width: 1; }
.chromatogram .cg-trace    { stroke: var(--signal); stroke-width: 1.5; fill: none;
                             vector-effect: non-scaling-stroke; }
.chromatogram .cg-fill     { fill: var(--signal); opacity: 0.10; }
.chromatogram .cg-grid     { stroke: var(--rule); stroke-width: 0.5; stroke-dasharray: 2 3; }
.chromatogram .cg-tick     { font-family: var(--font-mono); font-size: 7px; fill: var(--ink-faint); }

/* Draw-on animation for the hero trace */
@media (prefers-reduced-motion: no-preference) {
  .chromatogram--animate .cg-trace {
    stroke-dasharray: 2000;
    stroke-dashoffset: 2000;
    animation: cg-draw 2.4s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
  }
  .chromatogram--animate .cg-fill {
    opacity: 0;
    animation: cg-fade 1.2s ease-out 1.4s forwards;
  }
}
@keyframes cg-draw { to { stroke-dashoffset: 0; } }
@keyframes cg-fade { to { opacity: 0.10; } }

/* Inline micro-trace used inside purity badges and product cards */
.chromatogram--micro { height: 28px; }
.chromatogram--inline { height: 40px; }

/* --------------------------------------------------------------------------
   7. PURITY BADGE
   Number, method and trace together. Never show a purity figure without
   naming the method that produced it.
   -------------------------------------------------------------------------- */

.purity {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-4);
  background: var(--signal-wash);
  border: 1px solid var(--signal-edge);
  border-radius: var(--radius);
}
.purity__figure {
  font-family: var(--font-mono);
  font-size: var(--t-lg);
  font-weight: 600;
  color: var(--signal-deep);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.purity__meta { font-size: var(--t-2xs); color: var(--ink-soft); line-height: 1.4; }
.purity__trace { flex: 1; min-width: 60px; max-width: 140px; }

/* --------------------------------------------------------------------------
   8. DATA TABLE - styled as analytical output, not as a web table
   -------------------------------------------------------------------------- */

.data-table { width: 100%; border-collapse: collapse; font-size: var(--t-xs); }
.data-table caption {
  caption-side: top;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-mute);
  padding-bottom: var(--s-2);
}
.data-table th {
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
}
.data-table td {
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.data-table td.num, .data-table th.num { text-align: right; font-family: var(--font-mono); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table--striped tbody tr:nth-child(odd) { background: var(--paper-sunk); }

/* Specification list - the two-column key/value block on product pages */
.spec {
  display: grid;
  grid-template-columns: minmax(120px, 34%) 1fr;
  gap: 0;
  border-top: 1px solid var(--rule-strong);
  font-size: var(--t-sm);
}
.spec dt {
  padding: var(--s-3) var(--s-3) var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-mute);
  font-size: var(--t-xs);
}
.spec dd {
  margin: 0;
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   9. BUTTONS
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 0.72rem var(--s-5);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  text-decoration: none;
  transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.btn--primary {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
.btn--primary:hover { background: #000; border-color: #000; }
.btn--signal {
  background: var(--signal-deep);
  color: #fff;
  border-color: var(--signal-deep);
}
.btn--signal:hover { background: #05586F; border-color: #05586F; }
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink-faint);
}
.btn--ghost:hover { border-color: var(--ink); }
.btn--block { width: 100%; }
.btn--lg { padding: 0.9rem var(--s-6); font-size: var(--t-base); }

/* 41px is above the 24px WCAG AA minimum and below the 44px every touch
   guideline asks for. Raised only where the pointer is a finger, so the
   desktop density is untouched. */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
}

/* --------------------------------------------------------------------------
   10. FORMS
   -------------------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field label { font-size: var(--t-xs); color: var(--ink-soft); font-weight: 500; }
.input, select, textarea {
  font-family: var(--font-sans);
  font-size: var(--t-base);
  padding: 0.65rem var(--s-3);
  background: var(--paper-raised);
  border: 1px solid var(--ink-faint);
  border-radius: var(--radius);
  color: var(--ink);
  width: 100%;
}
.input--mono { font-family: var(--font-mono); letter-spacing: 0.04em; }
.input:focus { border-color: var(--signal-deep); }
.hint { font-size: var(--t-2xs); color: var(--ink-mute); }

/* --------------------------------------------------------------------------
   11. HEADER AND FOOTER
   -------------------------------------------------------------------------- */

/* Skip link (WCAG 2.4.1). Moved off-screen rather than hidden with
   display:none or visibility:hidden, because both of those take it out of
   the focus order and it would never be reachable - which is the usual way
   this control ends up present in the markup and useless in practice.

   Every page opens with a topbar, a masthead, a brand link and a nav of six
   or seven items, so without this a keyboard visitor tabbed through ten
   controls before reaching anything belonging to the page. */
.skiplink {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: var(--s-2) var(--s-3);
  background: var(--paper);
  color: var(--ink);
  border: 2px solid var(--signal-deep);
  border-radius: 0 0 var(--r-1) 0;
  font-weight: 600;
  text-decoration: none;
}
.skiplink:focus {
  left: 0;
}

/* The waitlist honeypot. Off-screen rather than display:none, because some
   bots skip inputs that are not rendered - and a real person must never
   reach it, hence aria-hidden and tabindex="-1" on the markup as well. A
   screen-reader user who filled this in would have their signup silently
   discarded, which is why it is hidden from that tree too. */
.waitlist__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.waitlist { display: grid; gap: var(--s-3); max-width: 34rem; }

/* The target takes tabindex="-1" so the browser actually moves focus there.
   Without it a same-page jump scrolls but leaves focus where it was, and the
   next Tab returns to the nav the visitor just skipped. */
#main:focus { outline: none; }

.topbar {
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.04em;
  padding-block: var(--s-2);
}
.topbar a { color: var(--paper); }
.topbar .cluster { justify-content: space-between; gap: var(--s-4); }

.masthead {
  background: var(--paper-raised);
  border-bottom: 1px solid var(--rule);
  position: sticky;
  top: 0;
  z-index: 40;
}
.masthead__inner {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  min-height: 68px;
}
.brand {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  text-decoration: none;
  color: var(--ink);
  font-weight: 700;
  font-size: var(--t-md);
  letter-spacing: -0.02em;
}
.brand__mark { width: 26px; height: 26px; align-self: center; }
.brand__tld { font-family: var(--font-mono); font-weight: 400; font-size: var(--t-xs); color: var(--ink-mute); }

.nav { display: flex; gap: var(--s-5); margin-left: auto; align-items: center; }
.nav a {
  color: var(--ink-soft);
  text-decoration: none;
  font-size: var(--t-sm);
  font-weight: 500;
  padding-block: var(--s-2);
  border-bottom: 2px solid transparent;
}
.nav a:hover, .nav a[aria-current] { color: var(--ink); border-bottom-color: var(--signal); }

.footer {
  /* Light, with a hairline rule rather than a slab of navy. The dark ground
     is kept for the topbar alone, so the page ends the way a document ends
     instead of dropping into a black band. */
  background: var(--paper-sunk);
  color: var(--ink-soft);
  border-top: 1px solid var(--rule);
  padding-block: var(--s-8) var(--s-6);
  font-size: var(--t-sm);
  margin-top: var(--s-9);
}
.footer h4 { color: var(--ink); font-size: var(--t-xs); font-family: var(--font-mono);
             text-transform: uppercase; letter-spacing: var(--tracking-label); font-weight: 500;
             margin-bottom: var(--s-3); }
.footer a { color: var(--ink-soft); text-decoration: none; }
.footer a:hover { color: var(--signal-deep); text-decoration: underline; }
.footer__legal {
  border-top: 1px solid var(--rule);
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  font-size: var(--t-2xs);
  color: var(--ink-mute);
  line-height: 1.6;
}

/* --------------------------------------------------------------------------
   12. PRODUCT CARD
   -------------------------------------------------------------------------- */

.card {
  background: var(--paper-raised);
  border: var(--border);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  transition: border-color 140ms ease, box-shadow 140ms ease;
}
.card:hover { border-color: var(--rule-strong); box-shadow: var(--shadow-lift); }
.card__media {
  /* Square, because the photographs are. A 4/3 box around a 1/1 image is
     either letterboxing or a crop through the vial's cap. */
  aspect-ratio: 1 / 1;
  background: var(--paper-sunk);
  border-bottom: var(--border);
  display: block;
  position: relative;
  overflow: hidden;
}
.card__media picture, .card__img { display: block; width: 100%; height: 100%; }
.card__img img, .card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The photographs sit on their own pale studio ground, so the card does not
   need to tint underneath them. It leans in a touch on hover, which is the
   only motion on the grid. */
.card__media img { transition: transform 220ms ease; }
.card:hover .card__media img { transform: scale(1.03); }
@media (prefers-reduced-motion: reduce) {
  .card__media img { transition: none; }
  .card:hover .card__media img { transform: none; }
}
.card__eyebrow {
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  color: var(--ink-faint);
}
.card__trace { position: absolute; inset: auto 0 0 0; opacity: 0.5; }
.card__body { padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-2); flex: 1; }
.card__title { font-size: var(--t-base); font-weight: 600; }
.card__title a { color: var(--ink); text-decoration: none; }
.card__title a:hover { text-decoration: underline; }
.card__meta { font-family: var(--font-mono); font-size: var(--t-2xs); color: var(--ink-mute); }
.card__foot {
  margin-top: auto;
  padding-top: var(--s-3);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
}
.price {
  font-family: var(--font-mono);
  font-size: var(--t-md);
  font-weight: 600;
  color: var(--ink);
}
.price__from { font-size: var(--t-2xs); color: var(--ink-mute); font-weight: 400; }

/* --------------------------------------------------------------------------
   13. NOTICES - research-use, stock, shipping
   -------------------------------------------------------------------------- */

.notice {
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--ink-mute);
  background: var(--paper-raised);
  padding: var(--s-4);
  font-size: var(--t-sm);
  border-radius: var(--radius);
}
.notice--ruo { border-left-color: var(--warn); background: var(--warn-wash); }
/* Same colours as --ruo, different meaning: --ruo is a standing statement
   about what the material is, --warn is something that just went wrong. Two
   names because one of them will want to change. */
.notice--warn { border-left-color: var(--warn); background: var(--warn-wash); }
.notice--ok  { border-left-color: var(--signal); background: var(--signal-wash); }
.notice__title {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  margin-bottom: var(--s-2);
  color: var(--ink);
}

.stock { display: inline-flex; align-items: center; gap: var(--s-2); font-size: var(--t-xs); }
.stock::before {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--signal);
}
.stock--low::before  { background: var(--warn); }
.stock--none::before { background: var(--ink-faint); }

/* --------------------------------------------------------------------------
   14. TRUST STRIP - the four-item row of verifiable commitments
   -------------------------------------------------------------------------- */

.trust {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  border: 1px solid var(--rule-strong);
  background: var(--paper-raised);
  border-radius: var(--radius);
}
.trust__item { padding: var(--s-4); border-right: 1px solid var(--rule); }
.trust__item:last-child { border-right: none; }
.trust__k { font-family: var(--font-mono); font-size: var(--t-sm); font-weight: 600; color: var(--ink);
            display: block; margin-bottom: var(--s-1); }
.trust__v { font-size: var(--t-2xs); color: var(--ink-mute); line-height: 1.45; }

/* --------------------------------------------------------------------------
   15. UTILITIES
   -------------------------------------------------------------------------- */

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.text-mute { color: var(--ink-mute); }
.text-sm   { font-size: var(--t-sm); }
.text-xs   { font-size: var(--t-xs); }
.lead      { font-size: var(--t-md); line-height: var(--lh-snug); color: var(--ink-soft); }
.scroller  { overflow-x: auto; }

@media (max-width: 780px) {
  :root { --t-2xl: 1.8rem; --t-3xl: 2.05rem; }

  /* The nav was display:none here, with nothing put in its place. A phone had
     no way to reach the shop, the certificates, the calculator or the about
     page - only the brand and a cart link that hides itself while the cart is
     empty. It was presumably meant to become a hamburger and never did.

     It wraps to its own row instead. Four links do not need a disclosure
     widget, and a menu you have to open is a menu most people never open;
     the row scrolls sideways if a longer word arrives in another language. */
  .masthead__inner {
    flex-wrap: wrap;
    min-height: 0;
    padding-block: var(--s-3);
    gap: var(--s-2) var(--s-4);
  }
  .brand { flex: 1 1 auto; }
  .cartlink { margin-left: auto; }
  .nav {
    order: 3;
    flex: 1 0 100%;
    margin-left: 0;
    gap: var(--s-5);
    border-top: 1px solid var(--rule);
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav a {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
    padding-block: 0;
    min-height: 44px;
  }
  .spec { grid-template-columns: 1fr; }
  .spec dt { border-bottom: none; padding-bottom: 0; }
  .spec dd { padding-top: var(--s-1); }
  .trust__item { border-right: none; border-bottom: 1px solid var(--rule); }
  .trust__item:last-child { border-bottom: none; }
}

@media print {
  .masthead, .topbar, .footer, .btn { display: none; }
  body { background: #fff; }
}

/* --------------------------------------------------------------------------
   16. PRODUCT PAGE
   -------------------------------------------------------------------------- */

.product-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(340px, 0.85fr);
  gap: var(--s-7);
  align-items: start;
  padding-bottom: var(--s-6);
}

.product-hero__buy { position: sticky; top: 92px; }

/* Placeholder for the product photograph. Styled as a vial label so the
   prototype communicates intent; replaced by real photography at build. */
.vial-frame {
  /* Was a CSS drawing of a label inside a bordered box. It holds the
     photograph now, and a photograph on its own pale ground needs no second
     frame drawn around it. */
  background: var(--paper-sunk);
  border: var(--border);
  border-radius: var(--radius);
  aspect-ratio: 1 / 1;
  overflow: hidden;
}
.vial-frame picture, .vial-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cg-figure {
  margin: var(--s-5) 0 0;
  padding: var(--s-4);
  background: var(--paper-raised);
  border: var(--border);
  border-radius: var(--radius);
}
.cg-figure figcaption { margin-bottom: var(--s-3); }

.buybox {
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper-raised);
  padding: var(--s-5);
}

/* Price-reduction disclosure. Rendered ONLY when a product is genuinely
   reduced - see omnibus_html() in build_products.py for why an unconditional
   version was a fabricated price history rather than a compliance measure.

   Set at body size in --ink-soft, not as a muted caption. The requirement is
   that the information be clear, and a disclosure typeset to be overlooked is
   the thing the rule exists to prevent. The struck-through previous price is
   the only strikethrough anywhere on this site, and it is only ever applied
   to a price that was really charged. */
.pricehist {
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
  display: grid;
  gap: var(--s-1);
  font-size: var(--t-sm);
  color: var(--ink-soft);
}
.pricehist__row { display: flex; justify-content: space-between; gap: var(--s-4); }
.pricehist__row .mono { font-variant-numeric: tabular-nums; }
.pricehist__was { text-decoration: line-through; color: var(--ink-mute); }
.pricehist__row--now { color: var(--ink); font-weight: 600; }

/* Variant selector as radio cards. The per-mg unit price is shown because
   in this category buyers compare on it, and hiding it reads as evasive. */
/* .picklist is the same control for a choice that is NOT a product
   variant - the delivery method. Same look, different meaning: the
   measurement gate looks for a price on every .variants radio,
   because a product variant that reports the wrong price sends the
   wrong revenue to Smart Bidding. A delivery method has no product
   price to report, and sharing the class made the gate demand one. */
.variants, .picklist { display: grid; gap: var(--s-2); margin-top: var(--s-2); }
.variant, .pick { display: block; cursor: pointer; }

/* The fill sizes when the shop is off. There is nothing to choose between,
   so this is a list rather than a control: no pointer, no hover state, no
   focus ring, and no price column. The sizes themselves are real product
   information and stay. See SELLING in build_products.py. */
.variant--static {
  display: inline-block;
  cursor: default;
  margin: 0 var(--s-2) var(--s-2) 0;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--paper-sunk);
  border-radius: var(--radius);
  background: var(--paper-raised);
}
.variant input, .pick input { position: absolute; opacity: 0; width: 0; height: 0; }
.variant__body, .pick__body {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--ink-faint);
  border-radius: var(--radius);
  transition: border-color 120ms ease, background-color 120ms ease;
}
.variant:hover .variant__body, .pick:hover .pick__body { border-color: var(--ink-mute); }
.variant input:checked + .variant__body, .pick input:checked + .pick__body {
  border-color: var(--signal-deep);
  background: var(--signal-wash);
  box-shadow: inset 0 0 0 1px var(--signal-deep);
}
.variant input:focus-visible + .variant__body, .pick input:focus-visible + .pick__body { outline: var(--focus); outline-offset: var(--focus-offset); }
.variant__size, .pick__size  { font-weight: 600; font-size: var(--t-sm); }
.variant__unit, .pick__unit  { font-size: var(--t-2xs); color: var(--ink-mute); font-family: var(--font-mono); }
.variant__price, .pick__price { font-weight: 600; font-size: var(--t-sm); grid-column: 3; }

.coa-card {
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--signal);
  border-radius: var(--radius);
  background: var(--paper-raised);
  padding: var(--s-5);
}

.badge-ok {
  display: inline-block;
  font-size: var(--t-2xs);
  letter-spacing: var(--tracking-label);
  padding: var(--s-1) var(--s-2);
  color: var(--signal-deep);
  background: var(--signal-wash);
  border: 1px solid var(--signal-edge);
  border-radius: var(--radius);
}

/* FAQ - disclosure list, ruled like a datasheet rather than boxed */
.faq details {
  border-top: 1px solid var(--rule);
  padding: var(--s-4) 0;
}
.faq details:last-of-type { border-bottom: 1px solid var(--rule); }
.faq summary {
  cursor: pointer;
  font-weight: 600;
  font-size: var(--t-base);
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  align-items: baseline;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: '+';
  font-family: var(--font-mono);
  color: var(--ink-mute);
  font-weight: 400;
  flex: none;
}
.faq details[open] summary::after { content: '\2212'; }
.faq details p { margin: var(--s-3) 0 0; max-width: var(--measure); color: var(--ink-soft); }

.byline {
  border-top: 1px solid var(--rule);
  padding-top: var(--s-4);
}

@media (max-width: 900px) {
  .product-hero { grid-template-columns: 1fr; gap: var(--s-6); }
  .product-hero__buy { position: static; }
}

/* --------------------------------------------------------------------------
   17. HOME PAGE
   -------------------------------------------------------------------------- */

/* Hero sits on the sunk paper with a faint graph rule, so the chromatogram
   reads as sitting on plotting paper rather than floating on a web page. */
.hero {
  background: var(--paper-sunk);
  border-bottom: 1px solid var(--rule-strong);
  background-image:
    linear-gradient(to right, rgba(20,23,26,.028) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(20,23,26,.028) 1px, transparent 1px);
  background-size: 28px 28px;
}
.hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: var(--s-8);
  align-items: center;
  padding-block: var(--s-9);
}
.hero__title {
  font-size: clamp(1.9rem, 3.4vw, var(--t-3xl));
  line-height: 1.1;
  letter-spacing: -0.025em;
}
.hero__copy .lead { max-width: 46ch; }

/* The research-use line: small, mono, uppercase - a legal marker, not a
   sentence of copy. It lived inside the photograph hero, then inside the
   video hero, and each rewrite of those blocks deleted it and left it
   rendering as plain body text at the top of all three homepages. It lives
   out here with the other hero base rules now, where a hero variant being
   replaced cannot take it. */
.hero__ruo {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: 1.6;
  margin-bottom: var(--s-4);
}

/* Batch verification panel - the differentiator, given its own surface */
.verify-panel {
  display: grid;
  grid-template-columns: 1fr minmax(300px, 380px);
  gap: var(--s-7);
  align-items: center;
  padding: var(--s-6);
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius);
}
.verify-panel h2 { color: var(--paper); }
.verify-panel .label { color: var(--signal); }
.verify-panel .text-mute { color: #A9AEB3; }
.verify-form { display: grid; gap: var(--s-3); }
.verify-form label { color: #C6CBD0; }
.verify-form .hint { color: #868C92; }
.verify-form .input { background: #1E2226; border-color: #343A40; color: var(--paper); }
.verify-form .input::placeholder { color: #6B7278; }
.verify-form .input:focus { border-color: var(--signal); }
.verify-panel .btn--primary { background: var(--signal-deep); border-color: var(--signal-deep); color: #fff; }
.verify-panel .btn--primary:hover { background: #05586F; border-color: #05586F; }

/* Method steps. Numbered because the sequence is real: separate, confirm,
   publish. Numbering that encodes nothing would be decoration. */
.method { border-top: 2px solid var(--ink); padding-top: var(--s-4); }
.method__n {
  display: block;
  font-size: var(--t-xs);
  color: var(--signal-deep);
  margin-bottom: var(--s-2);
  letter-spacing: var(--tracking-label);
}
.method h3 { font-size: var(--t-base); margin-bottom: var(--s-2); }
.method p { margin: 0; }

.promo {
  display: grid;
  grid-template-columns: 1fr minmax(0, 40%);
  gap: var(--s-6);
  align-items: center;
  padding: var(--s-6);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--signal);
  border-radius: var(--radius);
  background: var(--paper-raised);
}

@media (max-width: 900px) {
  .hero__inner { grid-template-columns: 1fr; gap: var(--s-6); padding-block: var(--s-7); }
  .verify-panel { grid-template-columns: 1fr; gap: var(--s-5); }
  .promo { grid-template-columns: 1fr; }
  .promo svg { display: none; }
}

/* --------------------------------------------------------------------------
   18. CERTIFICATE OF ANALYSIS
   The report is styled as a document rather than as a web card. It should be
   legible when printed, because researchers do print these.
   -------------------------------------------------------------------------- */

.coa-hero {
  background: var(--paper-sunk);
  border-bottom: 1px solid var(--rule-strong);
  padding-block: var(--s-8);
}

.coa-search {
  display: flex;
  gap: var(--s-3);
  align-items: flex-end;
  flex-wrap: wrap;
  margin-top: var(--s-2);
}
.coa-search .field { min-width: 260px; }

.linkish {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-family: var(--font-mono);
  color: var(--signal-deep);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.coa-report {
  border: 1px solid var(--ink);
  border-top: 4px solid var(--signal);
  border-radius: var(--radius);
  background: var(--paper-raised);
  padding: var(--s-6);
}
.coa-report--withdrawn { border-top-color: var(--warn); }

.coa-report__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--rule-strong);
  margin-bottom: var(--s-5);
}
.coa-report__head > div { display: grid; gap: var(--s-1); }
.coa-report__id {
  font-size: var(--t-xl);
  letter-spacing: -0.01em;
  line-height: 1.1;
}

.coa-report__figure {
  margin: 0 0 var(--s-5);
  padding: var(--s-4);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
}
.coa-report__figure figcaption { margin-bottom: var(--s-3); }

.coa-report__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s-6);
}

/* Verification at source. This is the primary action on a certificate:
   a report served only by the seller it vouches for proves nothing. */
.coa-verify {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  flex-wrap: wrap;
  margin-top: var(--s-5);
  padding: var(--s-5);
  background: var(--signal-wash);
  border: 1px solid var(--signal-edge);
  border-radius: var(--radius);
}
.coa-verify .label { color: var(--signal-deep); }
.coa-verify .btn { flex: none; }

.coa-report__foot {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--rule);
  gap: var(--s-3);
}
.coa-report__foot .text-xs { flex: 1 1 240px; }

.badge-warn {
  display: inline-block;
  font-size: var(--t-2xs);
  letter-spacing: var(--tracking-label);
  padding: var(--s-1) var(--s-2);
  color: var(--warn);
  background: var(--warn-wash);
  border: 1px solid #E2C99F;
  border-radius: var(--radius);
  white-space: nowrap;
}

.coa-empty {
  text-align: center;
  padding: var(--s-8) var(--s-5);
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper-raised);
}

.explain {
  border-left: 2px solid var(--rule-strong);
  padding-left: var(--s-4);
}
.explain h3 { font-size: var(--t-base); margin-bottom: var(--s-2); }
.explain p { margin: 0; max-width: var(--measure); }

@media print {
  .coa-hero, .coa-search, .coa-report__foot .btn { display: none; }
  .coa-report { border: 1px solid #000; }
}

/* --------------------------------------------------------------------------
   19. CALCULATOR
   -------------------------------------------------------------------------- */

.calc {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: 0;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-raised);
}
.calc__inputs {
  padding: var(--s-6);
  border-right: 1px solid var(--rule-strong);
}
.calc__out {
  display: block;
  padding: var(--s-6);
  background: var(--ink);
  color: var(--paper);
}
.calc__out .label { color: var(--signal); }
.calc__out .text-mute { color: #9AA0A6; }

.calc__primary { padding-bottom: var(--s-4); border-bottom: 1px solid #2A2F34; }
.calc__big {
  display: block;
  font-size: clamp(1.9rem, 4vw, 2.6rem);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin-top: var(--s-2);
  font-variant-numeric: tabular-nums;
}

.calc__rows { margin: var(--s-4) 0 0; display: grid; gap: 0; }
.calc__rows > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-3) 0;
  border-bottom: 1px solid #2A2F34;
}
.calc__rows > div:last-child { border-bottom: none; }
.calc__rows dt { font-size: var(--t-xs); color: #9AA0A6; }
.calc__rows dd {
  margin: 0;
  font-size: var(--t-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.calc__warn {
  margin: var(--s-4) 0 0;
  padding: var(--s-3);
  border-left: 3px solid var(--warn);
  background: rgba(154, 91, 18, 0.14);
  color: #E8C08A;
  font-size: var(--t-xs);
  line-height: 1.5;
}

/* Number field with a trailing unit */
.input-unit { position: relative; display: flex; align-items: center; }
.input-unit .input { padding-right: 3.2rem; }
.input-unit__u {
  position: absolute;
  right: var(--s-3);
  font-size: var(--t-xs);
  color: var(--ink-mute);
  pointer-events: none;
}

@media (max-width: 820px) {
  .calc { grid-template-columns: 1fr; }
  .calc__inputs { border-right: none; border-bottom: 1px solid var(--rule-strong); }
}

/* --------------------------------------------------------------------------
   20. CONSENT
   Accept and reject are the same size, in the same row, with the same weight.
   A banner where refusing is harder than agreeing is not consent.
   -------------------------------------------------------------------------- */

.consent {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 90;
  background: var(--ink);
  color: var(--paper);
  border-top: 3px solid var(--signal);
  box-shadow: 0 -2px 16px rgba(20, 23, 26, 0.18);
}
.consent[hidden] { display: none; }
.consent__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  padding: var(--s-5);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-6);
  flex-wrap: wrap;
}
.consent__title {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--signal);
  display: block;
  margin-bottom: var(--s-2);
}
.consent__copy { flex: 1 1 380px; }
.consent__copy p { margin: 0; font-size: var(--t-sm); color: #C6CBD0; max-width: 62ch; }
.consent__copy a { color: var(--paper); }
.consent__actions { display: flex; gap: var(--s-3); flex: none; }
.consent__actions .btn { min-width: 168px; }
/* The reject button's edge was #4A5157, which is 2.23:1 against the banner
   ground - below the 3:1 WCAG 1.4.11 floor for a control boundary. It matters
   more here than anywhere else on the site: this is the REJECT button, and a
   banner where refusing is visually fainter than accepting is exactly what EU
   regulators mean by a dark pattern. #697178 measures 3.63:1. */
.consent .btn--ghost { color: var(--paper); border-color: #697178; }
.consent .btn--ghost:hover { border-color: var(--paper); }
.consent .btn--primary { background: var(--signal-deep); border-color: var(--signal-deep); color: #fff; }
.consent .btn--primary:hover { background: #05586F; border-color: #05586F; }

/* Per-purpose panel. It expands in place rather than replacing the banner, so
   "Wybierz cele" never looks like a way of dismissing the question. */
.consent__panel { border-top: 1px solid #2A3138; background: #0F1215; }
.consent__inner--panel { display: block; padding-block: var(--s-4); }
.consent-purposes { display: grid; gap: var(--s-3); margin-bottom: var(--s-4); }

.consent-purpose {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-3);
  align-items: start;
  cursor: pointer;
  max-width: 78ch;
}
.consent-purpose input {
  margin-top: 0.28em; width: 18px; height: 18px;
  accent-color: #00A86B; cursor: pointer;
}
.consent-purpose__label { display: block; color: var(--paper); font-size: var(--t-sm); font-weight: 600; }
.consent-purpose__why { display: block; color: #C6CBD0; font-size: var(--t-xs); line-height: 1.5; }
/* Strictly-necessary is shown, ticked and disabled: naming it is honest, and
   offering a switch that does nothing would not be. */
.consent-purpose--fixed { cursor: default; opacity: 0.72; }
.consent-purpose--fixed input { cursor: not-allowed; }

.consent-reopen {
  position: fixed;
  left: var(--s-4);
  bottom: var(--s-4);
  z-index: 60;
  background: var(--paper-raised);
  border: 1px solid var(--ink-faint);
  border-radius: var(--radius);
  padding: var(--s-2) var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.04em;
  color: var(--ink-mute);
  cursor: pointer;
  box-shadow: var(--shadow-card);
}
.consent-reopen:hover { color: var(--ink); border-color: var(--ink-mute); }
.consent-reopen[hidden] { display: none; }

@media (max-width: 720px) {
  .consent__inner { gap: var(--s-4); padding: var(--s-4); }
  .consent__actions { width: 100%; }
  .consent__actions .btn { flex: 1; min-width: 0; }
}

@media print { .consent, .consent-reopen { display: none; } }

/* --------------------------------------------------------------------------
   21. CATALOGUE
   -------------------------------------------------------------------------- */

.shop-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  padding-block: var(--s-4);
  border-top: 1px solid var(--rule-strong);
  border-bottom: 1px solid var(--rule);
  margin-top: var(--s-5);
}

.chip {
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: 500;
  padding: var(--s-2) var(--s-4);
  background: transparent;
  border: 1px solid var(--ink-faint);
  border-radius: 100px;
  color: var(--ink-soft);
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}
.chip:hover { border-color: var(--ink-mute); color: var(--ink); }
.chip--on {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
/* The chips are anchors, so the link colour and underline from the base
   stylesheet have to be undone explicitly. */
a.chip { text-decoration: none; display: inline-flex; align-items: center; gap: var(--s-2); }
a.chip:hover { text-decoration: none; }
.chip:focus-visible { outline: 2px solid var(--signal-deep); outline-offset: 2px; }

/* The count sits inside the chip so the visitor knows what a filter will
   yield before spending a click on it. */
.chip__n {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-mute);
}
.chip--on .chip__n { color: #9AA1A7; }

#shop-empty[hidden] { display: none; }

/* --- checkout ---------------------------------------------------------- */

/* A checkbox whose label is a paragraph. The input keeps its own size and
   does not stretch: `.input { width: 100% }` above applies to every form
   control, and a checkbox that fills its column is unusable. */
.checkline { display: flex; gap: var(--s-3); align-items: flex-start; cursor: pointer; }
.checkline input[type="checkbox"] {
  width: 1.15rem; height: 1.15rem; flex: none; margin-top: 0.15rem;
  accent-color: var(--signal-deep);
}

/* The error message, not a red border. Colour alone fails WCAG 1.4.1 and
   fails anyone who cannot distinguish it, so the message carries the meaning
   and the colour only reinforces it. --fail is 6.1:1 on paper. */
.field__err { font-size: var(--t-xs); color: var(--fail); }
.field__err:empty { display: none; }
[aria-invalid="true"] { border-color: var(--fail); }
[aria-invalid="true"]:focus { border-color: var(--fail); outline-color: var(--fail); }

.cart-line {
  display: flex; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-2) 0; font-size: var(--t-sm); color: var(--ink-soft);
}

fieldset { border: 0; padding: 0; margin: 0; }
fieldset legend { padding: 0; margin-bottom: var(--s-3); }

.confirm-lines { list-style: none; padding: 0; margin: 0; line-height: 2; }
.confirm-lines li { border-bottom: 1px solid var(--rule); padding: var(--s-2) 0; }
.confirm-lines li:last-child { border-bottom: 0; }

/* --- hero image slot ---------------------------------------------------- */

/* A photograph with a guaranteed-legible text zone, not a panel with a
   picture in it. Two things make it safe to swap the image later without
   retesting anything:

   1. The scrim is a token, not a value tuned to this particular photograph.
      It is opaque enough at the bottom that white text clears 4.5:1 over any
      image at all, including a white one.
   2. The text sits in a defined safe area. Nothing that carries meaning in
      the photograph may live there, which is a brief for the next image
      rather than a property of this one.

   Mobile is the primary case, not the fallback: most traffic arrives from a
   phone. The portrait asset is a separate crop, not this one squeezed, and
   <picture> chooses between them so the browser never downloads both. */

/* --------------------------------------------------------------------------
   THE VIDEO HERO
   --------------------------------------------------------------------------
   The clip is the background and the words sit on it, and they never reach
   the vial. That is arithmetic, not luck.

   The vial's left edge is at 65.6% of the frame - measured off the poster by
   column variance, sharp transition from 9 to 22 - and it stays there only
   while the clip is not cropped HORIZONTALLY. Cropping vertically is free:
   it moves nothing sideways. So the hero is pinned to the clip's own 16:9,
   and max-height only ever makes the box WIDER than that, which crops top and
   bottom and leaves every horizontal fraction where it was.

   That is what went wrong before. The hero was min-height:66svh, so on a tall
   narrow window the box became narrower than 16:9, cover scaled by height,
   and the vial was dragged left into the paragraph. Every attempt to fix it
   by moving object-position or thickening the veil was treating the symptom.

   With the frame stable, the copy at 46ch clears 65.6% at every width from
   900px up: the widest case is a 1200px wrap centred in a 1920px viewport,
   where the copy starts at 360px and ends at about 860, against a vial that
   starts at 1260.
   -------------------------------------------------------------------------- */

.hero--video {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--s-8);
}

.hero--video .hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* LEFT, not center. cover crops whichever axis has surplus, and this box
     grows taller than the clip whenever the copy is taller than 16:9 of the
     window - a narrow desktop, or a reader with large text. Centred, that
     surplus is taken off both sides and the vial walks left into the words.
     Anchored left, a horizontal crop only ever pushes the vial further
     right, away from the copy. */
  object-position: left center;
  z-index: -2;
  pointer-events: none;
}

/* White, not navy. The clip is a light studio scene, so the words are the
   navy they are everywhere else and nothing is being darkened into mud.
   Ramps to nothing by 64%, just short of the vial. */
.hero--video::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to right,
    rgba(248, 249, 251, 0.88) 0%,
    rgba(248, 249, 251, 0.82) 40%,
    rgba(248, 249, 251, 0.74) 52%,
    rgba(248, 249, 251, 0.52) 58%,
    rgba(248, 249, 251, 0) 64%);
}

/* WCAG 2.2.2 pause control, added by herovideo.js only when a clip actually
   attached. Bottom right, over the part of the frame the copy never reaches -
   the vial's side - so it costs the hero nothing. 44px, because it is a
   control on a page most visitors reach on a phone. */
.hero__motion {
  position: absolute;
  right: var(--s-3);
  bottom: var(--s-3);
  z-index: 2;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 0.9rem;
  line-height: 1;
  color: var(--ink);
  background: rgba(248, 249, 251, 0.92);
  border: 1px solid var(--rule);
  border-radius: 50%;
  cursor: pointer;
}
.hero__motion:hover { background: var(--paper-raised); }

.hero--video .hero__inner {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--s-8);
}
/* MEASURED: the clip puts the vial's glass at 65.5% of the frame
   (hero-wave-poster.jpg, by column variance and mean brightness). At 46ch
   the copy's right edge lands at 38-42% depending on viewport, so on a
   desktop there is a wide clear gap and nothing can collide.

   The `min(46ch, 52vw)` this replaces was misleading: the copy starts at the
   centred wrap's left edge, so the 52vw half would have meant
   `wrap_left + 52vw` and never bound anything. 46ch was doing the work
   alone. Dropping the dead half so the next person does not tune a number
   that has no effect - which is how this rule got adjusted twice already.

   Narrower than 46ch was tried and is worse: at 34ch the two buttons no
   longer fit side by side and the hero grows a hollow middle. */
.hero--video .hero__copy { max-width: min(38ch, 46vw); }
.hero--video .lead { color: var(--ink-soft); max-width: 36ch; }
/* --signal-deep is 2.3:1 over this clip and no veil short of opaque saves
   it, so the hero has its own darker accent. */
.hero--video .hero__ruo { color: var(--signal-hero); }

@media (min-width: 900px) {
  /* The clip's own ratio. max-height only widens the box further, which
     crops vertically and moves nothing sideways. */
  .hero--video { aspect-ratio: 16 / 9; max-height: 78svh; }
  .hero--video .hero__inner {
    height: 100%;
    padding-block: var(--s-9);
  }
}

@media (max-width: 899px) {
  /* No room to put words beside a vial on a phone. The clip comes first,
     whole and edge to edge, and the copy follows it on paper - so the veil
     is off and the ordinary colours come back. */
  .hero--video { display: flex; flex-direction: column; }
  .hero--video::after { content: none; }
  /* The gutter moves from the wrap to the copy, so the clip is simply
     100% of a full-width parent. It was breaking out with
     width:100vw and margin-inline:calc(50% - 50vw), which depends on the
     wrap's padding, its max-width and the scrollbar all agreeing - and it
     did not come out full width. Nothing here computes a viewport unit. */
  .hero--video .hero__inner {
    padding-inline: 0;
    padding-block: 0 var(--s-8);
  }
  .hero--video .hero__copy { padding-inline: var(--s-5); }
  .hero--video .hero__video {
    position: static;
    order: -1;
    width: 100%;
    margin-inline: 0;
    height: auto;
    /* 4:3, not 16:9. This is the first thing a visitor sees and on a phone
       a 16:9 band is 219px of a 844px screen - a stripe, not a hero. At 4:3
       it is 293px and it leads the page. The crop takes width off a 16:9
       source, so the focal point shifts to keep the vial whole. */
    aspect-ratio: 4 / 3;
    object-position: 60% center;
  }
  .hero--video .hero__copy { max-width: none; }
  .hero--video .lead { max-width: none; }
  .hero--video .hero__ruo { color: var(--signal-deep); }
  .hero--video .hero__title { font-size: clamp(1.7rem, 7.4vw, 2.3rem); }
  .hero--video .cluster { flex-direction: column; align-items: stretch; gap: var(--s-3); }
  .hero--video .cluster .btn { width: 100%; justify-content: center; }
}

/* Restored. These 76 lines were collateral: deleting the photograph hero
   took a slice from ".hero--image {" to this comment, and the product chips,
   the laboratory-documentation band and the vial caption were sitting inside
   it. Twelve BEM classes lost their rules on up to 135 pages and every gate
   stayed green, because no gate could see that a class in the markup had
   nothing behind it. check_orphan_classes() can now. */

/* Facts under the product name. Each one states a measured figure and opens
   the batch that produced it. Competitors print "GMP", "ISO 9001",
   "Third-party tested" as graphics with nothing behind them; a chip with no
   measurement behind it is simply not rendered, which is why bacteriostatic
   water shows one and BPC-157 shows three. */
.chips {
  display: flex; flex-wrap: wrap; gap: var(--s-2);
  margin-top: var(--s-3);
}
.chip--data {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--signal-wash);
  border: 1px solid var(--signal-edge);
  color: var(--signal-deep);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.02em;
  padding: 5px 10px;
  border-radius: 999px;
  text-decoration: none;
}
.chip--data:hover { background: #DFF1F7; border-color: var(--signal); }
.chip--quiet {
  display: inline-flex; align-items: center;
  border: 1px solid var(--rule);
  color: var(--ink-mute);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  padding: 5px 10px;
  border-radius: 999px;
}

/* The vial photograph is not this batch. Saying so under the image costs one
   line and removes the only implied claim the visual makes. */
.product-hero__caption {
  margin: var(--s-3) 0 0;
  font-size: var(--t-2xs);
  color: var(--ink-mute);
  line-height: 1.5;
}

.buybox__batch {
  margin: var(--s-3) 0 0;
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  color: var(--ink-mute);
}

/* Laboratory documentation gets its own band: the argument on the left, the
   measurement on the right. The chromatogram moved here from the hero, where
   it competed with the buy panel for the same attention. */
.section--band { border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }

.labdoc {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-6);
  align-items: start;
}
@media (min-width: 900px) {
  .labdoc { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: var(--s-7); }
  .labdoc--single { grid-template-columns: minmax(0, 72ch); }
}
.labdoc__figure {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--s-5);
}
.labdoc__figure .cg-figure { margin: 0; }
.labdoc__figure figcaption { margin-bottom: var(--s-3); }
.labdoc .coa-card { margin-top: var(--s-5); }

/* --- masthead, topbar and cart link ------------------------------------- */

.topbar .cluster { align-items: center; }
.topbar__ruo { font-family: var(--font-mono); text-transform: uppercase; }
@media (max-width: 719px) {
  /* On a phone the shipping line is the one that goes. The research-use
     statement is the one that must not, and neither is the switcher: a
     reader who cannot read the page needs it more on a small screen, not
     less. */
  .topbar__ship { display: none; }
}

/* The language switcher. Every edition, with the current one marked rather
   than dropped - see lang_switch_top(). */
.langbar {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: none;
}
.langbar a, .langbar__on {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.06em;
  padding: 3px 7px;
  border-radius: 3px;
  text-decoration: none;
  line-height: 1.4;
}
.langbar a { color: #C6CBD0; }
.langbar a:hover { color: #fff; background: rgba(255, 255, 255, 0.12); }
.langbar a:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
/* The current edition is not a link, so it cannot rely on colour alone to
   say which one it is - the box does that, and it survives a page rendered
   without CSS colours. */
.langbar__on {
  color: var(--paper-raised);
  /* #111D33 under a 16% white veil, worked out once and written down.
     As rgba() the contrast gate could not check it: it verifies that a
     declared background is a colour the stylesheet actually paints, and a
     blend the browser computes at render time is not one. */
  background: #374154;
  font-weight: 600;
}
/* This one does not go to the same page in another language, because there
   isn't one - the Polish guides are about Polish law. It goes to that
   edition's home page, and the dotted rule says the destination is not the
   same as its neighbours' before anyone reads the title. */
.langbar--home { border-bottom: 1px dotted rgba(255, 255, 255, 0.45); }

/* The topbar wraps on a narrow screen. When it does, the research-use line
   takes the first row on its own and the switcher stays reachable rather
   than being pushed off the end of it. */
.topbar__ruo { flex: 1 1 100%; }
@media (min-width: 900px) { .topbar__ruo { flex: 1 1 auto; } }

.brand { gap: var(--s-3); align-items: center; }
.brand__words { display: flex; flex-direction: column; gap: 2px; line-height: 1.1; }
.brand__name { font-weight: 600; letter-spacing: 0.12em; }
.brand__tag {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-mute);
  font-weight: 400;
}
@media (max-width: 560px) { .brand__tag { display: none; } }

/* The badge was painted into an element that existed on no page at all:
   cartui.js has looked for #cart-count since it was written and found
   nothing every time. The whole link hides while the cart is empty, so an
   untouched shop shows no cart chrome rather than a zero. */
.cartlink {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 6px 12px;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  font-size: var(--t-xs);
  color: var(--ink);
  text-decoration: none;
}
.cartlink:hover { border-color: var(--signal-deep); color: var(--signal-deep); }
/* On a narrow masthead the word goes and the number stays. A cart control
   is recognisable from the count alone, and "Cart" next to a brand, a
   tagline and four nav items is the first thing that should give up room. */
@media (max-width: 560px) {
  .cartlink__label { position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .cartlink { padding: 6px 8px; }
}
.cartlink__n {
  font-family: var(--font-mono);
  background: var(--signal-deep);
  color: #fff;
  border-radius: 999px;
  min-width: 20px;
  padding: 1px 6px;
  text-align: center;
  font-size: var(--t-2xs);
}
