/* ==========================================================================
   Bohr Materials design system
   Paper-white ground, near-black ink, one accent per element.
   No framework. All codes, CAS, prices, spec values in mono
   with tabular figures.

   Edit rules in place. Do not append comment-delimited "feature vN" patch blocks
   at the end of this file; that habit is how .pdp came to be defined twice with
   conflicting column values, and how ~200 lines of dead rules accumulated.
   ========================================================================== */

:root {
  /* Ground. DO NOT change --ink or --paper: the brand-mark logo image
     (assets/bohr_logo.png) bakes in #191817 and #faf9f7, so changing
     either token desyncs the logo everywhere. */
  --paper:       #faf9f7;
  --paper-2:     #f3f1ec;
  --paper-3:     #ece9e2;
  --ink:         #191817;
  --ink-soft:    #3c3a36;
  --muted:       #6f6b63;
  --rule:        #e4e1db;
  --rule-strong: #c9c5bc;

  /* Single accent, as a pair. A mid-tone cannot pass body contrast on
     paper AND stay visible on ink, so: deep for paper, bright for ink.
     Measured: deep/paper 5.83, deep/paper-2 5.44, deep/tint 5.24,
     bright/ink 9.22, paper/deep 5.83. All pass WCAG AA. */
  --accent:        #0b6e62;
  --accent-bright: #34d399;
  --accent-tint:   #e2f0ee;

  /* Validation only. No error/success colour existed before the account
     forms needed one for a failed sign-in, a refused password, etc. The one
     deliberate addition beyond the ground/accent tokens above; nothing else
     on the account page introduces a new custom property. */
  --danger: #b3261e;

  /* Families. --serif is retired. --mono is KEPT for every spec value,
     CAS, purity and price, which is an existing deliberate convention. */
  --sans:     'Instrument Sans', 'Instrument Sans Fallback', -apple-system,
              BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --display:  var(--sans);
  --wordmark: 'Cinzel', Georgia, serif;
  --mono:     ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas,
              'Liberation Mono', monospace;

  /* Fluid scale: few steps, big jumps. */
  --fs-display: clamp(44px, 7.2vw, 92px);
  --fs-h1:      clamp(34px, 4.6vw, 58px);
  --fs-h2:      clamp(26px, 3.2vw, 40px);
  --fs-h3:      clamp(17px, 1.4vw, 20px);
  --fs-lede:    clamp(17px, 1.5vw, 21px);
  --fs-body:    16.5px;
  --fs-small:   13.5px;
  --fs-label:   11px;

  /* Optical tracking. -0.03em is TOO TIGHT for Instrument Sans: its
     sidebearings are already narrow and its terminals square, so rn/ti/fi
     collide at display size. Do not tighten these further. */
  --tr-display: -0.024em;
  --tr-h1:      -0.021em;
  --tr-h2:      -0.017em;
  --tr-h3:      -0.010em;
  --tr-label:    0.085em;

  /* Rhythm */
  --sp-sec:    clamp(56px, 8vw, 128px);
  --sp-blk:    clamp(18px, 2.2vw, 32px);
  --measure:   66ch;
  --radius:    5px;
  --radius-pill: 999px;
  --radius-lg: 12px;
}

/* Instrument Sans reports line-height 1.22; Arial/Helvetica is 1.150.
   Overriding ascent/descent makes both line boxes identical, so the
   font swap cannot change block height. */
@font-face {
  font-family: 'Instrument Sans Fallback';
  src: local('Helvetica Neue'), local('Helvetica'), local('Arial');
  size-adjust: 96%;
  ascent-override: 97%;
  descent-override: 25%;
  line-gap-override: 0%;
}

/* ---------- Base ---------- */

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--fs-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img { max-width: 100%; height: auto; }

a { color: var(--ink); }
p a, .lede a, li a { color: var(--accent); }

h1, h2, h3, h4 {
  font-family: var(--display);
  color: var(--ink);
  margin: 0 0 var(--sp-blk);
  text-wrap: balance;
  font-synthesis-weight: none;
}

h1 { font-size: var(--fs-h1); font-weight: 500; line-height: 1.03; letter-spacing: var(--tr-h1); }

/* Hero-only voice. T6 adds class="display" to the index.html h1. */
.display {
  font-size: var(--fs-display);
  font-weight: 500;
  line-height: 0.94;
  letter-spacing: var(--tr-display);
  max-width: 17ch;
}

h2 {
  font-size: var(--fs-h2); font-weight: 500; line-height: 1.08;
  letter-spacing: var(--tr-h2);
  padding-bottom: 0; border-bottom: 0;
}

h3 { font-size: var(--fs-h3); font-weight: 600; line-height: 1.25; letter-spacing: var(--tr-h3); margin-bottom: 10px; }

p { margin: 0 0 14px; max-width: var(--measure); }

.lede { font-size: var(--fs-lede); line-height: 1.45; letter-spacing: -0.006em; color: var(--ink-soft); max-width: 44ch; }

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--fs-small); }

code { font-family: var(--mono); font-size: 0.92em; }

/* ---------- Layout shell ---------- */

.wrap {
  max-width: 1240px;
  margin: 0 auto;
  padding-left: 20px;
  padding-right: 20px;
}

main { padding-top: 8px; }

.sec {
  margin-top: var(--sp-sec);
  padding-top: clamp(22px, 2.6vw, 34px);
  border-top: 1px solid var(--rule);
}
/* A page that opens with a .sec must not open with a rule, but it still needs
   its top padding: zeroing that left the h1 only 8px under the header on
   faq/about/terms, while .el-head pages sat at 42px. Keeping padding-top makes
   both 42px at 1440. */
main .sec:first-child { border-top: 0; margin-top: 0; }

/* ---------- Header / nav ---------- */

/* The frosted background lives on a ::before layer, NOT on .site-head itself.
   A non-none backdrop-filter makes the element a containing block for its
   fixed-position descendants -- and .site-nav is a fixed off-canvas drawer
   nested inside this header, so a filtered .site-head would pin the drawer to
   the ~69px header box instead of the viewport, which is exactly the "the four
   subcategories are unreachable on a phone" bug this change set exists to fix.

   This used to be handled by setting backdrop-filter: none on .site-head at
   <=720px, i.e. by relying on the engine to RELEASE the containing block when
   the filter computes to none. That was only ever measured in Chromium and
   there is no iOS device here to check Safari with, so the dependency is
   removed rather than verified: a pseudo-element cannot be the ancestor of
   anything, so no engine's containing-block rules can reach the drawer through
   it. Same reasoning covers filter, transform, will-change and contain.
   .site-head keeps position: sticky, which does NOT create a fixed containing
   block, and which is still what .nav-panel is positioned against. */
.site-head {
  position: sticky; top: 0; z-index: 80;
  border-bottom: 1px solid var(--rule);
}
/* z-index: -1 keeps it behind the header's own content. .site-head has
   z-index: 80 on a sticky box, so it is already a stacking context and the
   negative layer cannot escape it. */
.site-head::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(250, 249, 247, 0.86);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
          backdrop-filter: saturate(180%) blur(12px);
}

/* Solid once the page is scrolled (.is-scrolled, set by assets/nav.js) or
   while the Products panel is open (.is-solid), so an opaque panel never
   hangs off a translucent bar. Two classes rather than one, so a scroll
   ending mid-panel can't clear the panel's opacity.

   Scoped to >=721px: media queries add no specificity, and an unscoped
   .site-head.is-scrolled::before is 0-2-1, which would outrank the <=720px
   opaque rule below (.site-head::before, 0-1-1) regardless of source order.
   That would put the backdrop-filter back on a scrolled phone, re-creating
   the containing-block bug the comment above .site-head::before warns
   about. Wrapping in the media query makes that collision structurally
   impossible instead of order-dependent. */
@media (min-width: 721px) {
  .site-head.is-scrolled::before,
  .site-head.is-solid::before {
    background: var(--paper);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }
  .site-head.is-scrolled {
    box-shadow: 0 1px 0 var(--rule-strong);
  }
}

.head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 30px;
  padding-top: 14px;
  padding-bottom: 14px;
}

.brand {
  font-family: var(--wordmark);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: 0.14em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
.brand-mark { display: block; flex: none; }
/* Research's name beside the shared Bohr Materials mark. flex: none so it
   never shrinks: at narrow widths the mark gives way instead (see the
   max-width rules on .brand-mark below). */
.brand-sub {
  flex: none;
  font-family: var(--sans);
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--muted);
  border-left: 1px solid var(--rule-strong);
  padding-left: 9px;
  line-height: 1.3;
}
/* <=1024px the mark already shrinks to make room (see the two media queries
   below), so beside it .brand-sub would squeeze it to ~54px at 721px. Under
   it instead, the mark keeps the width it had before .brand-sub existed, for
   a few px of header height. */
@media (max-width: 1024px) {
  .head-row .brand { flex-direction: column; align-items: flex-start; gap: 3px; }
  .brand-sub { font-size: 11px; line-height: 1.2; border-left: 0; padding-left: 0; }
}

/* flex: 1 1 auto + justify-content: flex-end lets .search-box claim the slack
   with its own margin-right: auto, which is what puts the search bar hard
   against the brand while Products/About/FAQ stay right-aligned. */
.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 28px;
  flex: 1 1 auto;
  justify-content: flex-end;
  margin-left: auto;
}

.site-nav a {
  font-size: 14px;
  letter-spacing: -0.004em;
  color: var(--ink-soft);
  text-decoration: none;
  padding: 2px 0;
}
.site-nav a:hover { text-decoration: underline; text-underline-offset: 3px; }
/* box-shadow not border-bottom: no baseline shift between states */
.site-nav a[aria-current="page"] {
  color: var(--ink); font-weight: 600; border-bottom: 0;
  box-shadow: inset 0 -2px 0 var(--accent);
}
/* Cart button. Quiet on purpose: it must not read as a second CTA, so no
   border and no fill. The count is a pill that only exists when the cart does,
   so an empty cart leaves the header exactly as it was. The glyph uses
   currentColor and does NOT hardcode --ink.

   Scoped to .head-row, not .site-nav: the cart button is now a direct child of
   .head-row so that it stays in the header bar when .site-nav becomes the
   off-canvas drawer at <=720px. A cart badge you cannot see is a cart badge
   that does not work. */
.head-row .cart-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-left: 6px;
  padding: 9px 10px;
  line-height: 1;
  color: var(--ink);
  border-bottom: 0;
}
/* On /cart, assets/cart.js marks the button with aria-current="page" instead of
   wiring it to the drawer, because the page already lists every line at full
   size. The treatment for that used to come from .site-nav a[aria-current] and
   was lost when the button moved out of .site-nav into .head-row, leaving /cart
   the one page whose header does not say where you are. Same box-shadow idiom
   as the nav links, so there is no baseline shift between states. Declared
   before :hover so hovering still tints it. */
.head-row .cart-btn[aria-current="page"] {
  color: var(--ink);
  box-shadow: inset 0 -2px 0 var(--accent);
}
.head-row .cart-btn:hover { color: var(--accent); text-decoration: none; }
.cart-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}
.cart-btn-ico { display: block; flex: none; }

/* Mono and tabular are set locally rather than via the .mono class, because
   .mono also sets font-size: var(--fs-small) at 13.5px, too big for a badge,
   and beating it by source order would be fragile. */
.cart-count {
  min-width: 20px;
  padding: 2px 6px;
  background: var(--accent);
  color: var(--paper);
  border-radius: var(--radius-pill);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
  .cart-count.is-bumped { animation: cart-bump 260ms cubic-bezier(0.16, 0.84, 0.24, 1); }
}
@keyframes cart-bump { 50% { transform: scale(1.16); } }

/* The <=720px treatment lives with the drawer rules further down (see
   "Products panel and mobile drawer"). The horizontal scroller that used to
   live here is gone: it could not show the subcategory links at all, because
   it was paired with `.menu { display: none !important }`. */

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

.btn {
  display: inline-block;
  padding: 11px 22px;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.002em;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--ink);
  border-radius: var(--radius-pill);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover { background: var(--paper-2); }

.btn-solid {
  background: var(--ink);
  color: var(--paper);
}
.btn-solid:hover { background: var(--ink-soft); color: var(--paper); }

/* Ordering is open: no marketplace or PDP buy button ships with the disabled
   attribute. This rule styles whatever JS disables at runtime instead -- the
   cart's own Checkout button, which assets/cart.js's refreshCheckoutState()
   disables for any of five reasons: an empty cart, an unpriced line, no
   destination chosen, no successful quote yet, or shared-cart mode. One rule
   covers the outlined marketplace buttons and the solid PDP button in case
   either is ever disabled too: faded, not-allowed, and the hover states above
   are cancelled so nothing responds to the pointer. */
.btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn[disabled]:hover { background: transparent; }
.btn-solid[disabled]:hover { background: var(--ink); color: var(--paper); }

.btn-lg { padding: 14px 30px; font-size: 15px; border-radius: var(--radius-pill); }

.btn-block { display: block; text-align: center; }

/* ---------- Hero (home) ---------- */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
  gap: clamp(28px, 5vw, 72px);
  align-items: center;
  padding: 52px 0 12px;
}
.hero-main { min-width: 0; }
@media (max-width: 900px) { .hero { grid-template-columns: 1fr; } }

.hero-eyebrow { font-size: 11.5px; letter-spacing: .14em; color: var(--muted); margin-bottom: 18px; }
/* .hero-main is one of two grid columns (globe takes the other), so the shared
   --fs-display token (up to 92px) overflows this headline past 3 lines. Scale
   it against the narrower column instead. */
.hero-main h1.display { font-size: clamp(41px, 5vw, 70px); }

.hero-cta { display: flex; align-items: flex-start; gap: 24px; margin-top: 30px; flex-wrap: wrap; }
.hero-offer {
  display: inline-block;
  margin-bottom: 18px;
  padding: 7px 16px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  background: var(--accent-tint);
  color: var(--accent);
  font-size: var(--fs-label);
  letter-spacing: var(--tr-label);
}

.globe-stage {
  position: relative;
  width: 100%; max-width: 560px;
  margin-inline: auto;
  aspect-ratio: 1 / 1;
  background: radial-gradient(circle at 50% 46%,
              rgba(243,241,236,0.9) 0%, rgba(243,241,236,0) 62%);
}
/* Pinning both dimensions is what prevents a ResizeObserver feedback loop. */
.globe-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

.gel {
  position: absolute; z-index: 2;
  display: grid; gap: 2px; justify-items: center; text-align: center;
  width: 78px;
  padding: 8px 6px 7px;
  background: rgba(250, 249, 247, 0.94);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: 0 8px 22px -12px rgba(25, 24, 23, 0.28);
  text-decoration: none;
  opacity: 0;
  --s: 1; --o: 1;
  transform: translate3d(0, 14px, 0) scale(0.96);
  animation: gel-in 620ms cubic-bezier(0.16, 0.84, 0.24, 1) forwards;
}
.gel-n    { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 10px; color: var(--muted); }
.gel-sym  { font-family: var(--display); font-weight: 600; font-size: 24px; line-height: 1.1; letter-spacing: -0.01em; color: var(--ink); }
.gel-name { font-size: 9.5px; color: var(--muted); white-space: nowrap; }

/* Depth via three properties moving together: a nearer box is bigger, more
   opaque and casts a larger softer shadow than a farther one. Scale/opacity
   are read from --s/--o inside the keyframe so a static per-box value
   doesn't fight the entrance transform. */
@keyframes gel-in { to { opacity: var(--o); transform: translate3d(0,0,0) scale(var(--s)); } }

/* nth-child offset by 1 because the canvas is the first child. Placement is
   deliberately irregular (not mirrored left/right) and staggers in
   farthest-to-nearest so the group appears to settle toward the viewer. */
.gel:nth-child(2) { animation-delay: 1500ms; top: 36%; right: -3%;  z-index: 3; --s: 0.79; --o: 0.88; box-shadow: 0 5px 12px -12px rgba(25,24,23,0.15); }
.gel:nth-child(3) { animation-delay: 1580ms; top: 13%; right: -11%; z-index: 4; --s: 0.86; --o: 0.92; box-shadow: 0 7px 16px -12px rgba(25,24,23,0.18); }
.gel:nth-child(4) { animation-delay: 1660ms; bottom: 14%; left: 6%; z-index: 4; --s: 0.89; --o: 0.94; box-shadow: 0 8px 18px -12px rgba(25,24,23,0.20); }
/* Se sits at -6% rather than -13%: at -13% its left edge landed 3px from the
   end of "Unbeatable prices." at both 1440 and 1600, which reads as a collision
   even though the boxes never actually intersect. */
.gel:nth-child(5) { animation-delay: 1740ms; top: 44%; left: -6%;   z-index: 5; --s: 0.93; --o: 0.97; box-shadow: 0 10px 22px -12px rgba(25,24,23,0.24); }
.gel:nth-child(6) { animation-delay: 1820ms; bottom: 1%; right: -7%; z-index: 6; --s: 0.97; --o: 1;    box-shadow: 0 13px 28px -12px rgba(25,24,23,0.28); }
.gel:nth-child(7) { animation-delay: 1900ms; top: 3%; left: -9%;    z-index: 6; --s: 1;    --o: 1;    box-shadow: 0 14px 30px -12px rgba(25,24,23,0.30); }

.gel:hover { border-color: var(--accent); box-shadow: 0 10px 26px -10px rgba(25,24,23,0.34); }

/* Between the single-column breakpoint and ~1400px, .globe-stage is
   narrower than its 560px max-width, so the right-side cells' negative
   percentage offsets (sized for the full-width desktop stage) push them
   past the viewport edge. Swap to small fixed pixel offsets in that band;
   above it there is enough margin for the original percentages. */
@media (min-width: 901px) and (max-width: 1400px) {
  .gel:nth-child(2) { right: -3px; }
  .gel:nth-child(3) { right: -9px; }
  .gel:nth-child(6) { right: -6px; }
}

/* 561px to 900px was the one band with no override, and it is the worst case:
   .hero collapses to a single column at 900px, which widens .globe-stage to its
   full 560px max-width inside a narrow viewport, so the negative percentage
   offsets push cells clean off both edges. Measured at 600px before this rule:
   36px of horizontal page overflow, with In 30px and Se 11px past the LEFT edge
   where scrolling cannot reach them. Pin every offset to 0 here and keep all six
   cells; the depth effect survives via --s/--o. */
@media (min-width: 561px) and (max-width: 900px) {
  .gel:nth-child(3), .gel:nth-child(2), .gel:nth-child(6) {
    right: 0; left: auto;
  }
  .gel:nth-child(7), .gel:nth-child(5), .gel:nth-child(4) {
    left: 0; right: auto;
  }
}

@media (max-width: 560px) {
  .gel { width: 64px; padding: 6px 4px 5px; }
  /* Depth is wasted at this width; show four cells flat, two per side. */
  .gel:nth-child(2), .gel:nth-child(4) { display: none; }
  .gel:nth-child(3), .gel:nth-child(5), .gel:nth-child(6), .gel:nth-child(7) {
    --s: 1; --o: 1; z-index: 2;
  }
  .gel:nth-child(7) { top: 6%; left: 0; right: auto; bottom: auto; }
  .gel:nth-child(5) { bottom: 6%; left: 0; top: auto; right: auto; }
  .gel:nth-child(3) { top: 6%; right: 0; left: auto; bottom: auto; }
  .gel:nth-child(6) { bottom: 6%; right: 0; top: auto; left: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .gel { opacity: 1; transform: none; animation: none; }
}

/* ---------- Data tables ---------- */
/* Always wrap tables in .table-scroll so they scroll inside their
   container on narrow screens; the page never scrolls horizontally. */

.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  background: #fff;
}

table.data-table {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
  font-size: 14px;
}

.data-table th {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  text-align: left;
  padding: 10px 12px;
  background: var(--paper-2);
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
}

.data-table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: #fdfcfa; }

.data-table td.mono, .data-table th.mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13.5px;
  white-space: nowrap;
}

/* ---------- Element page header ---------- */

.el-head {
  display: flex;
  flex-wrap: wrap;
  gap: 28px 40px;
  justify-content: space-between;
  align-items: flex-start;
  padding-top: 34px;
}

.el-head-main { max-width: 640px; }

/* Right column of .el-head, which was empty on every page that used it.
   .el-head is already flex + space-between + wrap, so this slots in as a
   sibling and drops below .el-head-main once the two no longer fit. */
.el-head-side { flex: 0 1 400px; min-width: 280px; }
/* .promise's own rule is max-width: 72ch, sized for a full-width block. In a
   ~400px column that ch measure is never the binding constraint, but it would
   let the aside stretch past its flex basis on a wide viewport, so pin it. */
.el-head-side .promise { max-width: none; }
.el-head-side .promise .promise-lead { font-size: clamp(16px, 1.3vw, 18px); }
@media (max-width: 900px) {
  .el-head-side { flex-basis: 100%; }
  .el-head-side .promise { max-width: 72ch; }
}

/* The five listing pages (/products and the four subcategories) use .el-head as
   a page-title block that is immediately followed by the listing itself, not as
   a peer section. The full --sp-sec rhythm between the two computes to 102.4px
   at 1280 and reads as dead white, because nothing closes the intro -- on a
   subcategory page the next ink is a filter input, not a heading. Halved here.
   Scoped to the modifier class rather than to .el-head, because cart.html and
   thanks.html use .el-head too and their rhythm is signed off (the two form
   pages are handled separately below);
   --sp-sec itself is untouched, since it governs every section gap site-wide. */
.el-head-listing + .sec { margin-top: calc(var(--sp-sec) / 2); }

/* about/faq/privacy/terms open with a title-only .sec (h1, sometimes a lede)
   immediately followed by the first content .sec. Same dead-white problem as
   .el-head-listing above -- the full --sp-sec rhythm reads as empty space
   with nothing to close the intro. Halved here too. */
.sec-intro + .sec { margin-top: calc(var(--sp-sec) / 2); }

/* /quote and /form open with an .el-head lede and then go straight into the
   form. The full rhythm computes to ~119px, and with the .sec rule and its own
   34px padding on top of that the first label sits ~154px below the lede -- the
   same dead-white problem as the two rules above, and worse here because a form
   wants to read as the continuation of the sentence that introduced it.
   The adjacent-sibling selector is what keeps this off cart.html and
   thanks.html: they use .el-head but are not followed by a .sec, so only the
   two form pages match. */
.el-head + .sec { margin-top: calc(var(--sp-sec) / 2); }

.el-sub {
  font-size: 15.5px;
  color: var(--ink-soft);
  margin-bottom: 14px;
}

/* ---------- Forms (quote page) ---------- */

input[type="text"],
input[type="email"],
input[type="date"],
input[type="number"],
input[type="password"],
select {
  width: 100%;
  padding: 9px 10px;
  font: inherit;
  font-size: 14.5px;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
}

textarea {
  width: 100%;
  padding: 9px 10px;
  font: inherit;
  font-size: 14.5px;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
  min-height: 110px;
  resize: vertical;
}

input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
  border-color: var(--ink);
}

/* No generic <label> rule exists elsewhere on the site (the destination
   fieldset's .cart-dest-label is scoped to the cart drawer), so a bare
   <label> here defaults to inline and sits beside a width:100% field
   instead of stacking above it. Mirrors .cart-dest-label's tokens exactly
   rather than inventing a new label style, scoped by id so it touches
   nothing outside this one form.

   #acct-page is added rather than one id per form: assets/account.js
   renders six different form ids (sign-in, register, forgot, resend,
   reset, prefs) into that one container across its three page states, and
   scoping to the shared container covers all six with the one rule instead
   of six near-duplicates. */
#quote-form label,
#acct-page label {
  display: block;
  margin: 0 0 4px;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- Shared form primitives ---------- */
/* Generic, deliberately un-prefixed: /form (assets/form.js) builds its form
   from these four classes rather than from a bare <p><label>, so another
   page's form can reuse them without duplicating a rule. */

/* One label+control. Replaces the bare <p> the old /quote markup used;
   margin and max-width are copied from the site-wide `p` rule (:137) so
   converting <p> to .form-field changes no spacing or line length that was
   already signed off. */
.form-field {
  margin: 0 0 14px;
  max-width: var(--measure);
}

/* Two controls that describe one value side by side (amount + its unit).
   A flex row rather than an inline-block pair, so both controls share the
   row's width instead of the unit select being sized to its own content
   next to a full-width amount input. */
.form-row {
  display: flex;
  gap: 10px;
}
.form-row > * { flex: 1; }

/* A subject-specific cluster of .form-field elements, hidden by default via
   the `hidden` attribute and revealed by a `change` listener on the subject
   select, which toggles `hidden` and each inner field's `required` together
   -- a hidden group can never leave a stale `required` blocking submission.
   Left rule + inset borrows .note's idiom (:686) so a revealed group reads
   as one cluster without a new border colour or a full card. */
.form-group {
  border-left: 3px solid var(--rule-strong);
  padding-left: 16px;
  margin: 0 0 18px;
}

/* The one-line success/error result of a submit (#quote-status on /quote,
   its /form equivalent). Success and failure share this class and differ
   only by modifier, because at most one is ever shown at once. --danger and
   --accent are each already this site's colour for exactly this bad/good
   polarity elsewhere, so this introduces no new colour. */
.form-status { font-size: 14px; }
.form-status.is-error { color: var(--danger); }
.form-status.is-success { color: var(--accent); }

/* ---------- Prose / notice blocks ---------- */

.note {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule-strong);
  border-radius: 0 3px 3px 0;
  background: #fff;
  padding: 12px 16px;
  font-size: 14px;
  color: var(--ink-soft);
  max-width: 72ch;
}

/* The Bohr Promise. Same border idiom as .note, accent-tinted the way
   .cart-shared is, so it reads as a highlighted notice rather than a new
   card. Appears on index.html, products.html and the four subcategory pages. */
.promise {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  border-radius: 0 3px 3px 0;
  background: var(--accent-tint);
  padding: 16px 20px;
  max-width: 72ch;
}
.promise p { margin: 0; max-width: none; color: var(--ink-soft); }
.promise .promise-lead {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(17px, 1.6vw, 21px);
  line-height: 1.35;
  color: var(--ink);
  margin-bottom: 8px;
}
.promise .promise-terms { font-size: 14px; }

/* ---------- Skeleton loaders ---------- */
/* One primitive plus a few shape modifiers, used on the five surfaces that
   actually wait on the network: the /me probe, order history, the two
   destination selects on /account, and the cart's Shipping and Total rows.
   Deliberately placed here, in the general half of the file, rather than in
   the Account or Cart sections: it is shared by both, and it defines no new
   custom property.

   min-height: 1em is load-bearing. A skeleton standing in for a line of text
   has no text of its own, so without it the block collapses and the real
   sentence pushes the page around when it lands. */
.skel {
  display: block;
  min-height: 1em;
  background: var(--paper-2);
  border-radius: var(--radius);
  color: transparent;
  user-select: none;
}

/* The three shapes. Widths are in ch so they track the copy they stand in
   for rather than a guessed pixel count. */
.skel-line  { width: 100%; }
.skel-short { width: 22ch; max-width: 100%; }
.skel-money { display: inline-block; width: 6ch; vertical-align: middle; }

/* Order history: seven columns, matching ORDER_COLUMNS in assets/account.js
   and .data-table's own cell padding, so the skeleton occupies the same box
   the table will. */
.skel-row {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 12px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--rule);
}
.skel-row:last-child { border-bottom: none; }

/* Motion, and the anti-flash delay, both gated on no-preference so the base
   rule above is already the static, correct thing for a reduced-motion user
   -- by construction, not by an override that has to fire.

   The 1ms skel-in animation is not decoration: with `both`, its backwards
   fill holds opacity 0 through the 180ms delay, so a response faster than
   ~180ms never paints a skeleton at all. This is done in CSS precisely so no
   JS timer is needed; assets/account.js and assets/cart.js must stay free of
   one.

   The trade: under prefers-reduced-motion: reduce the delay does not apply,
   so a very fast response can flash a skeleton for a frame. That is accepted
   over the alternative -- an element that is invisible and then appears --
   which is exactly the surprise those users have asked not to be given. A
   brief static block is the lesser harm. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes skel-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes skel-shimmer {
    from { background-position: 100% 0; }
    to   { background-position: -100% 0; }
  }
  .skel {
    background-image: linear-gradient(90deg,
      var(--paper-2) 0%, var(--paper-3) 50%, var(--paper-2) 100%);
    background-size: 200% 100%;
    animation:
      skel-in 1ms linear 180ms both,
      skel-shimmer 1.4s linear 180ms infinite;
  }
}

/* ---------- Footer ---------- */

/* .site-foot sits OUTSIDE .wrap, so a background here is a full-bleed band. */
.site-foot {
  margin-top: var(--sp-sec);
  padding: clamp(40px, 6vw, 76px) 0 clamp(40px, 6vw, 64px);
  border-top: 1px solid var(--rule);
  background: var(--paper-2);
  font-size: var(--fs-small);
}

.foot-entity { color: var(--ink); font-size: var(--fs-h3); font-weight: 500; letter-spacing: var(--tr-h3); max-width: 34ch; margin-bottom: 4px; }
.foot-ruo { color: var(--muted); max-width: 60ch; margin-bottom: 0; }

.foot-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  margin-top: 26px;
  padding-top: 22px;
  border-top: 1px solid var(--rule-strong);
}
.foot-nav a {
  color: var(--muted);
  text-decoration: none;
  padding: 4px 0;
}
.foot-nav a:hover { color: var(--ink); text-decoration: underline; }

/* Ensure the hidden attribute always wins (quote form is hidden after submit) */
[hidden] { display: none !important; }

/* ==========================================================================
   Products panel and mobile drawer

   One set of DOM nodes serves both: .site-nav is the desktop nav row and, at
   <=720px, the off-canvas drawer. Nothing is duplicated, so there is exactly
   one search input, one Products button and one set of category buttons per
   page, and assets/nav.js needs no per-breakpoint branch.

   Open state is expressed as [aria-expanded] on the controlling <button>, and
   assets/nav.js touches nothing else. The `html.nav-js` gate exists so the
   panel still opens on hover/focus when the script has not run: without it a
   no-JS visitor could not reach the eight subcategory links at all, which is
   the exact bug this whole change set removes on mobile.
   ========================================================================== */

/* Screen-reader-only. No such utility existed; the search inputs need a real
   <label>, and a visible one would not fit the header row. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Products button ---------- */

.nav-item { display: inline-flex; align-items: center; }

/* The drawer takes programmatic focus on open (tabindex="-1" in the markup)
   so a screen reader announces the nav landmark before its first control.
   Same reason .cart-panel:focus is silenced. */
.site-nav:focus { outline: none; }

/* Matches .site-nav a exactly (14px, --ink-soft, 2px block padding) so
   Products sits on the same baseline as About and FAQ despite being a button.
   It is a <button> and not a link on purpose: there is no /products/<category>
   page behind it any more. */
.nav-products-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-family: var(--sans);
  font-size: 14px;
  letter-spacing: -0.004em;
  color: var(--ink-soft);
  background: transparent;
  border: 0;
  cursor: pointer;
}
.nav-products-btn:hover { color: var(--ink); }
.nav-products-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}
.nav-products-btn[aria-expanded="true"] { color: var(--ink); font-weight: 600; }
.nav-products-caret {
  display: block;
  font-size: 10px;
  line-height: 1;
  transform: rotate(90deg);
}
@media (prefers-reduced-motion: no-preference) {
  .nav-products-caret { transition: transform 160ms ease; }
}
.nav-products-btn[aria-expanded="true"] .nav-products-caret { transform: rotate(-90deg); }

/* ---------- The full-width panel ---------- */

/* .site-head is position: sticky, so it is the containing block for this
   absolutely positioned descendant, which is what makes left:0/right:0 span
   the viewport rather than the .nav-item. .nav-item must therefore NOT be
   position: relative (it was, for the old 210px dropdown). */
.nav-panel {
  display: none;
  position: absolute;
  top: 100%; left: 0; right: 0;
  z-index: 60;
  background: var(--paper);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule-strong);
  box-shadow: 0 14px 34px rgba(25, 24, 23, 0.10);
}
html.nav-js .nav-products-btn[aria-expanded="true"] + .nav-panel { display: block; }
html:not(.nav-js) .nav-item:hover > .nav-panel,
html:not(.nav-js) .nav-item:focus-within > .nav-panel { display: block; }

/* Two panes: the category list on the left, and the active category's
   subcategories in a pane to its right. .nav-panel-inner is that pane's
   containing block, which is why IT takes position: relative and .nav-item
   still must not: .nav-panel is positioned against the sticky .site-head so
   left:0/right:0 spans the viewport, and .nav-panel-inner is inside .nav-panel,
   so positioning it moves nothing. */
.nav-panel-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  padding-top: 26px;
  padding-bottom: 26px;
  /* Sized for the tallest pane today, whichever category is active.

     The pane is out of flow (see .nav-sub), so it contributes NO height at all:
     the panel's height comes from this number and from the left column only.
     That is exactly what makes swapping panes shift nothing -- the box is the
     same height whichever category is active, so no row and no link ever moves
     out from under the pointer.

     It is also what makes the EMPTY state the same height as the open ones. No
     category is active when the panel opens, so the space to the right of the
     column starts blank and the first hover fills it. Reserving the room up
     front means that first hover costs no reflow either. The blank area on
     open is the deliberate price of that, not an oversight.

     Derived, not guessed. Measured at 1280px: a .nav-sub row is 36.4px (14px
     text at line-height 1.6, plus 7px padding above and below), .nav-sub itself
     adds 6px of padding-top, and this box adds 26px + 26px of its own. It is
     sized for a pane of 2 subcategory rows, a divider, and an All row (both
     of today's panes). So 26 + 6 + (3 x 36.4) + 17 for the divider (a 1px
     rule with 8px clear either side) + 26 = 184.2, rounded up to 185.

     Trade-off. The left column is in flow: two category rows plus the
     All-products link come to 118px, 170px with this box's padding, inside
     this. Giving any category a THIRD subcategory needs this number bumped by
     one row (36.4px), or that pane will overflow the panel. Adding a third
     CATEGORY row (about 40px) takes the column past 185 too; the panel then
     grows to fit the column, so nothing overflows, but bump this to match.

     Rejected twice over: letting the panel size itself to whichever pane is
     showing, or to the category column when none is. Panes of different lengths
     would then push the category rows below them up and down as the pointer
     crosses the list, and an empty panel would jump taller on the very first
     hover, moving a row out from under the cursor mid-gesture in both cases.
     That is the same bug class as the one the hover comment in assets/nav.js
     documents, and a self-sizing box is not worth trading a constant height
     away for. */
  min-height: 185px;
}

/* The left column, deliberately IN NORMAL FLOW: it, not the pane, is what
   gives the panel its content height. */
.nav-cats {
  list-style: none;
  margin: 0; padding: 0;
  flex: none;
  width: 300px;
}

.nav-cat-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 9px 0;
  font-family: var(--display);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: var(--tr-h3);
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
}
.nav-cat-btn:hover { color: var(--accent); }
.nav-cat-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}
/* The chevron is the disclosure affordance. aria-hidden in the markup:
   [aria-expanded] already carries the state for assistive tech. It does NOT
   rotate on desktop: the glyph is a right-pointing &rsaquo; and the pane it
   discloses opens to the right, so rotating it down would point it at nothing.
   Colour alone carries the active state here. The drawer, where the disclosure
   really does open downward, keeps the rotation; see the max-width: 720px
   block. */
.nav-cat-caret {
  flex: none;
  font-size: 17px;
  line-height: 1;
  color: var(--muted);
}
@media (prefers-reduced-motion: no-preference) {
  .nav-cat-caret { transition: transform 160ms ease; }
}
.nav-cat-btn[aria-expanded="true"] .nav-cat-caret { color: var(--accent); }

/* The pane. Absolutely positioned against .nav-panel-inner, so exactly one
   pane is visible and swapping panes cannot reflow the column beside it.
   Both offsets are against the containing block's PADDING box, which is what an
   absolutely positioned box is laid out against, so both carry .nav-panel-inner's
   own padding: top is 26px and not 0, or the pane would float 26px above the
   first category row, and left is .wrap's 20px padding-left plus the 300px
   column plus a 56px gutter, the same gutter the panel used between its old
   side-by-side columns. */
.nav-sub {
  display: none;
  position: absolute;
  top: 26px;
  left: 376px;
  list-style: none;
  margin: 0; padding: 6px 0 0;
}
html.nav-js .nav-cat-btn[aria-expanded="true"] + .nav-sub { display: block; }
/* Unscripted, every pane is shown at once, so they have to go back in flow
   under their own category button: three absolutely positioned panes would
   otherwise stack on top of each other at left: 376px. .nav-cats needs no
   reset here -- it is a plain block list in this layout -- and its 300px width
   is kept on purpose, which is what leaves the unscripted panel a readable
   column instead of 1200px-wide rules. */
html:not(.nav-js) .nav-sub { display: block; position: static; }
.nav-sub a {
  display: block;
  padding: 7px 0 7px 2px;
  font-size: 14px;
  color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 0;
}
.nav-sub a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }

/* "All <Category> →", the only route from the nav to a category page (the
   category button itself is deliberately not a link). Divider only: the
   link inside reuses .nav-sub a above, unrestyled. 8px margin + 1px border +
   8px padding = 17px, exactly the divider allowance .nav-panel-inner's
   min-height comment already reserves. Also used, unmodified, by the mobile
   drawer, where .nav-sub is position: static. */
.nav-sub-all {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
}

/* No align-self here, and its removal was deliberate rather than an oversight.
   It used to be `align-self: flex-end`, which put "All products →" at the
   panel's bottom-right while .nav-panel-inner was still a flex ROW. The same
   declaration means something else now that the inner box is a flex COLUMN
   with align-items: flex-start: flex-end resolves against the cross axis, so
   it moved the link from the bottom-right corner to the far right of the
   category column's own row instead. Dropping it leaves the link where the
   column layout wants it, under the categories and aligned with them. Do not
   put it back to "restore" the old position; in a column container it cannot
   express that position at all. */
.nav-panel-all { margin: 0; }
.nav-panel-all a { font-size: 14px; font-weight: 600; color: var(--ink); text-decoration: none; }
.nav-panel-all a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ---------- Header / page search ---------- */

.search-box { position: relative; flex: 0 1 320px; margin-right: auto; }
.search-box--lg { flex: none; max-width: 520px; margin: 22px 0 0; }

.search-input {
  width: 100%;
  padding: 8px 12px;
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
}
.search-box--lg .search-input { padding: 12px 18px; font-size: 15.5px; }
.search-input::placeholder { color: var(--muted); }

/* role="listbox". Empty and display:none until assets/nav.js has matches, so
   with JS off it never paints and the <form>'s GET to /products stands in. */
.search-results {
  display: none;
  position: absolute;
  top: calc(100% + 6px); left: 0; right: 0;
  z-index: 70;
  max-height: 340px;
  overflow-y: auto;
  list-style: none;
  margin: 0; padding: 5px;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  box-shadow: 0 12px 30px rgba(25, 24, 23, 0.14);
}
.search-results.is-open { display: block; }

.search-opt {
  padding: 8px 12px;
  font-size: 14px;
  color: var(--ink-soft);
  border-radius: 6px;
  cursor: pointer;
}
.search-opt:hover { background: var(--paper-2); color: var(--ink); }
.search-opt[aria-selected="true"] { background: var(--accent-tint); color: var(--ink); }
.search-opt-sub {
  display: block;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--muted);
}
.search-empty { padding: 8px 12px; font-size: 14px; color: var(--muted); }

/* ---------- Hamburger ---------- */

/* Hidden above 720px; the drawer does not exist there. */
.nav-toggle {
  display: none;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 38px; height: 38px;
  margin-left: 4px;
  background: transparent;
  border: 0;
  color: var(--ink);
  cursor: pointer;
}
.nav-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}
.nav-toggle-box { display: block; width: 20px; height: 14px; position: relative; }
.nav-toggle-bar {
  position: absolute; left: 0;
  width: 100%; height: 1.5px;
  background: currentColor;
  border-radius: 2px;
}
.nav-toggle-bar:nth-child(1) { top: 0; }
.nav-toggle-bar:nth-child(2) { top: 6.25px; }
.nav-toggle-bar:nth-child(3) { top: 12.5px; }
@media (prefers-reduced-motion: no-preference) {
  .nav-toggle-bar { transition: transform 200ms ease, opacity 120ms ease; }
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) { transform: translateY(6.25px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) { transform: translateY(-6.25px) rotate(-45deg); }

.nav-scrim { display: none; }

/* ---------- >=721px: one header row, search absorbs the slack ----------

   Above the drawer breakpoint everything must fit on a single line. Left to
   wrap, .site-nav put the 320px search box on one line and Products/About/FAQ
   on a second, doubling the header height at 768px -- wrapping happens BEFORE
   shrinking, so a flex-shrink on .search-box never got a chance to run. nowrap
   on both rows forces the shrink instead, and flex: none on the nav's own
   items makes .search-box the only thing that gives. */
@media (min-width: 721px) {
  .head-row { flex-wrap: nowrap; }
  .site-nav { flex-wrap: nowrap; }

  /* Only above the drawer breakpoint, where .site-nav is inline and the cart
     button's left neighbour is the FAQ/account link. The cart sat 36px away
     -- .head-row's 30px column gap plus the button's own 6px -- while the nav
     items are 28px apart, so it read as detached from the group it belongs
     to. This brings it to 16px, tighter than the items are to each other,
     which is what makes the account link and the cart read as one cluster.
     Below 721px the brand takes margin-right:auto and the cart is pushed
     against the hamburger instead, so the correction does not apply. */
  .head-row .cart-btn { margin-left: -14px; }
  .site-nav > a,
  .site-nav > .nav-item { flex: none; }
  .search-box { min-width: 120px; }

  /* NO hover bridge, deliberately. There is no overlay over this header at any
     time, in either the scripted or the unscripted path, so nothing can ever
     swallow a click meant for the brand, the search box, About, FAQ or the cart.

     A bridge was tried twice and rejected both times. .nav-panel is positioned
     against .site-head, so it starts 23.7px below the Products button, and
     every category target sits at the FAR LEFT of a full-width panel while the
     button is at the far right. A button-width strip is bypassed after ~4% of
     the move. A full-width strip in the dead band is bypassed too, because the
     pointer exits the button SIDEWAYS at y~37 -- above the band -- so the only
     strip that would actually hold the hover is one covering the whole lower
     header row, which swallows the centre of the search input. Not worth it.

     With the script the problem does not need a bridge at all: assets/nav.js
     watches .site-head for the hover close, and .site-head's box is flush with
     the panel by construction, so there is no gap to bridge.

     Without the script, reachability is delivered by :focus-within instead of
     by hover -- clicking the Products button (or three Tabs) focuses it, which
     latches the panel open regardless of where the pointer then goes, and every
     subcategory link is then a normal click. Verified end to end with
     javaScriptEnabled: false. Hover still previews the panel, it just is not
     the gesture the no-JS path relies on. */
}

/* 721-1024px: tablet and small laptop, where the 237px wordmark, the search
   box, three nav items and the cart all compete and there is no slack. Tighter
   gaps and a smaller search basis buy it; the wordmark stays shrinkable as a
   last resort so a longer nav label can never force a second row. */
@media (min-width: 721px) and (max-width: 1024px) {
  .head-row { column-gap: 20px; }
  .site-nav { column-gap: 20px; }
  .search-box { flex-basis: 240px; }
  .head-row .brand { min-width: 0; }
  .brand-mark { flex: 0 1 auto; max-width: 100%; height: auto; }
}

/* ---------- <=720px: .site-nav becomes the drawer ---------- */

@media (max-width: 720px) {
  /* Purely cosmetic, and deliberately NOT load-bearing any more: the drawer's
     fixed positioning no longer depends on any filter anywhere, because the
     blur is on .site-head::before, which is not an ancestor of anything. An
     opaque header at phone widths is just the look that was signed off, and
     skipping a blur that nothing can see through is free. */
  .site-head::before {
    background: var(--paper);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }

  /* z-index 3 keeps the hamburger above both the scrim and the drawer, so it
     stays hit-testable and morphs into the X in place over the drawer's
     top-right corner. It is the only close affordance besides the scrim and
     Escape, which is why it must not be dimmed. */
  .nav-toggle { display: inline-flex; order: 3; position: relative; z-index: 3; }
  .head-row .cart-btn { order: 2; }
  /* The auto margin goes on the brand, not on the cart button: at this width
     .site-nav is position: fixed (below), taken clean out of .head-row's flex
     flow, leaving only the brand, the cart button and the hamburger competing
     for space. The auto margin puts the spacing on the brand, pushing the
     cart button and hamburger to the row's right edge instead.

     nowrap + a shrinkable brand mark is what keeps brand / cart / hamburger on
     one line: at the inherited 30px column gap the 237px wordmark pushed the
     hamburger onto a second row at 390px, and no width is narrow enough for a
     fixed wordmark at 320px. */
  .head-row { flex-wrap: nowrap; gap: 0 10px; }
  .head-row .brand { min-width: 0; margin-right: auto; }
  .brand-mark { flex: 0 1 auto; max-width: 100%; height: auto; }

  .site-nav {
    position: fixed;
    top: 0; right: 0; bottom: 0;
    z-index: 2;
    width: min(360px, 88vw);
    flex: none;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    margin: 0;
    /* 66px of top padding clears the header bar, under whose right edge the
       hamburger-turned-X floats. */
    padding: 66px 20px 32px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    background: var(--paper);
    border-left: 1px solid var(--rule-strong);
    /* Same idiom as .cart-drawer: visibility, not [hidden], because [hidden]
       is display:none !important above and cannot transition. The delayed
       visibility keeps the drawer out of the tab order while closed. */
    visibility: hidden;
    transform: translateX(100%);
    transition: transform 240ms cubic-bezier(0.16, 0.84, 0.24, 1),
                visibility 0s linear 240ms;
  }
  html.nav-open .site-nav {
    visibility: visible;
    transform: translateX(0);
    transition: transform 240ms cubic-bezier(0.16, 0.84, 0.24, 1),
                visibility 0s linear 0s;
  }

  .nav-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1;
    background: rgba(25, 24, 23, 0.34);
    opacity: 0;
    visibility: hidden;
    transition: opacity 240ms cubic-bezier(0.16, 0.84, 0.24, 1),
                visibility 0s linear 240ms;
  }
  html.nav-open .nav-scrim {
    opacity: 1;
    visibility: visible;
    transition: opacity 240ms cubic-bezier(0.16, 0.84, 0.24, 1),
                visibility 0s linear 0s;
  }

  .search-box { flex: none; width: 100%; margin: 0 0 18px; }
  .search-results { position: static; max-height: none; box-shadow: none; margin-top: 6px; }

  .nav-item { display: block; }
  .nav-products-btn {
    width: 100%;
    justify-content: space-between;
    padding: 12px 0;
    font-size: 16px;
    font-weight: 600;
    color: var(--ink);
    border-bottom: 1px solid var(--rule);
  }
  .nav-products-caret { font-size: 12px; }

  /* Inline accordion, not an overlay: inside the drawer there is nothing to
     overlay. */
  .nav-panel {
    position: static;
    border: 0;
    box-shadow: none;
    background: transparent;
  }
  .nav-panel-inner {
    display: block;
    width: auto;
    max-width: none;
    padding: 0 0 8px 12px;
    /* The desktop min-height sizes an out-of-flow pane. In the drawer the panes
       are in flow, so it would only pad the accordion with dead space. */
    min-height: 0;
  }
  /* The two-pane layout undone: the drawer accordion is untouched by it. */
  .nav-cats { display: block; width: auto; }
  .nav-cat-btn { font-size: 15px; padding: 11px 0; }
  /* Restored here only. In the drawer the pane really does open BELOW the
     button, so the caret has to turn to point at it. Same selector as the
     desktop rule and later in the file, so it wins on order, not specificity. */
  .nav-cat-btn[aria-expanded="true"] .nav-cat-caret { transform: rotate(90deg); }
  .nav-sub { position: static; padding: 2px 0 8px 12px; }
  .nav-panel-all { padding: 10px 0 0 12px; }

  .site-nav > a {
    padding: 12px 0;
    font-size: 16px;
    border-bottom: 1px solid var(--rule);
  }
  .site-nav a[aria-current="page"] { box-shadow: none; color: var(--accent); }

  /* No-JS fallback. Nothing can add html.nav-open without the script, so an
     off-canvas drawer would make the whole nav unreachable on a phone -- the
     exact failure this change set exists to remove. Without `nav-js` the nav
     is simply an in-flow stacked block under the brand and the hamburger is
     not shown. .head-row already wraps, so width: 100% is all it takes. */
  html:not(.nav-js) .nav-toggle,
  html:not(.nav-js) .nav-scrim { display: none; }
  /* wrap is restored here only: with the nav back in flow it needs its own
     row, and the nowrap above exists solely for the brand/cart/hamburger bar. */
  html:not(.nav-js) .head-row { flex-wrap: wrap; }
  html:not(.nav-js) .site-nav {
    position: static;
    visibility: visible;
    transform: none;
    width: 100%;
    padding: 10px 0 4px;
    overflow: visible;
    background: transparent;
    border-left: 0;
    transition: none;
  }
}

html.nav-open { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .site-nav, .nav-scrim, .nav-toggle-bar,
  .nav-cat-caret, .nav-products-caret { transition: none !important; }
}

/* Named Bohr */
.about-bohr { display: flex; align-items: flex-start; gap: 20px; max-width: 725px; margin-top: 12px; margin-bottom: -80px; }
.about-bohr p { flex: 1 1 auto; margin: 0; }
.about-bohr-photo { flex: none; width: auto; height: 212px; aspect-ratio: 1456 / 2048; object-fit: cover; border: 1px solid var(--rule-strong); border-radius: 8px; }
@media (max-width: 560px) {
  .about-bohr { flex-direction: column; max-width: none; margin-bottom: 0; }
  .about-bohr-photo { order: -1; width: 140px; height: auto; }
}

/* Why Bohr */
.why-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(20px, 3vw, 40px); margin-top: clamp(20px, 3vw, 32px); }
.why-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 900px) { .why-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px) { .why-grid--2 { grid-template-columns: 1fr; } }
@media (max-width: 560px) { .why-grid { grid-template-columns: 1fr; } }
.why-item { border-top: 2px solid var(--ink); padding-top: 14px; }
.why-h { font-family: var(--display); font-weight: 700; font-size: 16px; color: var(--ink); margin-bottom: 6px; }
.why-item p { margin: 0; font-size: 14.5px; line-height: 1.5; color: var(--ink-soft); max-width: 34ch; }

/* Sourcing */
.sourcing-lede { max-width: 60ch; }
.sourcing-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(24px, 4vw, 56px); margin-top: clamp(20px, 3vw, 32px); max-width: 720px; }
@media (max-width: 640px) { .sourcing-grid { grid-template-columns: 1fr; } }
.sourcing-item { border-top: 2px solid var(--ink); padding-top: 14px; }
.sourcing-fig { font-size: clamp(26px, 3.4vw, 40px); font-weight: 600; line-height: 1; letter-spacing: -0.02em; color: var(--accent); }
.sourcing-item p { margin: 8px 0 0; font-size: 14.5px; color: var(--ink-soft); max-width: 32ch; }


/* about-page v1 */
.why-more { margin-top: 18px; }


/* FAQ accordion. Native details/summary, so the page stays JS-free and the
   answers stay in the DOM for the FAQPage schema in the head. */
.faq-list { border-top: 1px solid var(--rule); margin-top: clamp(14px, 2vw, 20px); max-width: 780px; }
.faq-item { border-bottom: 1px solid var(--rule); }
.faq-item > summary { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; padding: 15px 0; cursor: pointer; list-style: none; }
.faq-item > summary::-webkit-details-marker { display: none; }
.faq-item > summary h3 { margin: 0; }
.faq-item > summary:hover h3 { color: var(--accent); }
.faq-item > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.faq-item > summary::after { content: "+"; flex: none; font-family: var(--mono); font-size: 18px; line-height: 1; color: var(--muted); }
.faq-item[open] > summary::after { content: "\2212"; color: var(--accent); }
.faq-item > p { margin: 0 0 18px; max-width: 68ch; color: var(--ink-soft); }


/* ==========================================================================
   Marketplace cards + pricing panel
   ========================================================================== */

.mkt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
  margin-top: 18px;
}

/* On-page card filter (subcategory pages). assets/nav.js sets [hidden] on the
   non-matching .mkt-card elements, so with JS off the input is inert and every
   card is visible -- which is the correct page. */
.card-filter { margin-top: 26px; max-width: 360px; }
.card-filter-label {
  display: block;
  margin: 0 0 6px;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}
.card-filter-input {
  width: 100%;
  padding: 9px 14px;
  font: inherit;
  font-size: 14.5px;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
}
.card-filter-status { margin: 8px 0 0; font-size: 13.5px; color: var(--muted); }
/* Zero-results state. Same border idiom as .note so it reads as a notice
   rather than an error. */
.card-filter-empty {
  margin-top: 18px;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule-strong);
  border-radius: 0 3px 3px 0;
  background: #fff;
  padding: 14px 18px;
  font-size: 14.5px;
  color: var(--ink-soft);
  max-width: 72ch;
}

/* min-width: 0 defeats the automatic minimum size of a grid item. Without it the
   aluminium card's purity select (min-content 260px, the width of its longest
   option) gives that card a 298px min-content width, which widens the shared
   grid track for every card on the tab. At a 320px viewport that grows the track
   from 280px to 298px and pushes the cards 18px into the page gutter. Inert at
   340px and wider, where the track already exceeds 298px. */
.mkt-card {
  min-width: 0;
  border: 1px solid var(--rule-strong);
  border-radius: 6px;
  background: #fff;
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.mkt-card-head {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.mkt-name {
  font-family: var(--display);
  font-size: 18px;
  font-weight: 700;
  margin: 0;
}

.mkt-amount-select {
  width: auto;
  min-width: 90px;
  padding: 5px 8px;
  font-size: 13px;
}

/* Sized like .mkt-amount-select, NOT like .mkt-purity-select below: form
   labels are short ("Ingot", "Granule", "Foil", "Shot", "Crystal", "Pellet",
   "Powder"), so this select should size to its content instead of being
   squeezed to fit a 30-character grade string the way .mkt-purity-select
   is. */
.mkt-form-select {
  width: auto;
  min-width: 90px;
  padding: 5px 8px;
  font-size: 13px;
}

/* Aluminium only. The option labels are long ("99.99995% (trace metals basis)"),
   so this one is allowed to fill the row and shrink instead of sizing to its
   content, which would push a 300px card wide. */
.mkt-purity-select {
  width: auto;
  min-width: 0;
  max-width: 100%;
  flex: 1 1 auto;
  padding: 5px 6px;
  /* Half a point under .mkt-amount-select. At the grid's 300px minimum track the
     longest option, "99.99995% (trace metals basis)", otherwise runs into the
     dropdown arrow. */
  font-size: 12.5px;
}

.mkt-desc {
  margin: 0;
  font-size: 14px;
  color: var(--ink-soft);
  line-height: 1.5;
}

.mkt-specs {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13.5px;
  color: var(--ink-soft);
}

.mkt-specs li + li { margin-top: 5px; }

/* Must wrap: the label and the pack-size select do not always fit on one line
   at narrow viewport widths or large browser font sizes, and without
   wrapping the row pushed past the card and gave the page horizontal scroll.
   Kept after the custom-amount input was retired -- the row is shorter now,
   but the overflow it guards against is a function of viewport and font
   size, not of how many controls sit in it. */
.mkt-amount-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

/* Wraps for the same reason: when the label and the purity select cannot share a
   line the select drops below it and gets the card's full width, which is what
   keeps the longest option string from being cut off. */
.mkt-purity-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

/* Same rule, one row up: the label and the form select cannot always share a
   line at the grid's 300px minimum track either. */
.mkt-form-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.mkt-specs .mkt-label {
  display: inline-block;
  min-width: 76px;
  font-weight: 600;
  color: var(--ink-soft);
}

.mkt-pricing {
  margin-top: auto;
  padding-top: 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Holds the quantity field and the price. It must wrap: at the 300px minimum
   card width the two do not always fit on one line. */
.mkt-purchase-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 10px;
}

.mkt-price {
  margin-left: auto;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

/* "Request a quote" and friends are messages, not prices. At the price size
   they push themselves onto their own line and make the card taller than its
   neighbours, so they render at the body size instead. */
.mkt-price.is-note {
  font-size: 13.5px;
  color: var(--ink-soft);
}

.mkt-qty-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Matches the label column of .mkt-specs above it so the quantity field lines
   up with the pack-size select. */
.mkt-qty-wrap .mkt-label {
  min-width: 76px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink-soft);
}

/* Quantity is a stepper, not a free-text field: the only reachable values are
   the whole numbers 1-10, so there is no bad input to validate and the card
   cannot be widened by a long number. Above 10 is a bulk order and goes
   through the standing-order email instead. */
/* The white sits on the wrapper, not the buttons: on the buttons it left the
   number showing the off-white page behind it, in two visibly different
   shades. */
.qty-stepper {
  display: inline-flex;
  align-items: center;
  background: #fff;
  border: 1px solid var(--rule-strong);
  border-radius: 5px;
  overflow: hidden;
}

/* 34x33 keeps these comfortably tappable on a phone; below about 30px square
   the minus and plus are easy to miss. */
.qty-step {
  width: 34px;
  padding: 9px 0;
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1;
  color: var(--ink);
  background: transparent;
  border: 0;
  cursor: pointer;
}

.qty-step:hover:not(:disabled) { background: var(--paper); }

/* A disabled step keeps the ink colour so the three characters read as one
   control; the cursor and the disabled attribute carry the state instead. */
.qty-step:disabled { cursor: default; }

.qty-step:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.mkt-qty {
  min-width: 32px;
  padding: 9px 2px;
  font-size: 13px;
  text-align: center;
}

/* Keyboard focus ring. The global input:focus rule draws an ink outline on
   pointer clicks too; this is the keyboard-only state for the controls this
   feature adds. */
.mkt-purity-select:focus-visible, .mkt-form-select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--ink);
}

.mkt-toast {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: 12px 20px;
  border-radius: 6px;
  font-size: 14px;
  box-shadow: 0 10px 30px rgba(25, 24, 23, 0.18);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  max-width: min(560px, calc(100vw - 40px));
  text-align: center;
}

.mkt-toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ==========================================================================
   Cart drawer
   A requisition sheet, not a shopping bag: paper ground, hairline dividers
   between lines, a right-hand mono column of amounts and money, and a
   2px solid ink rule above the subtotal (the same "a column starts here"
   gesture .why-item and .sourcing-item already use). No inner cards, no
   shadows, no pills besides the existing .btn and count badge.
   ========================================================================== */

/* Toggled with visibility, not [hidden]: [hidden] is display: none !important
   above, and an !important display cannot transition, so the panel could
   never slide out. The delayed transition keeps the drawer out of the
   accessibility tree and off-canvas until the close animation finishes. */
.cart-drawer {
  position: fixed;
  inset: 0;
  z-index: 90; /* above .site-head (80), below .mkt-toast (100) */
  visibility: hidden;
  transition: visibility 0s linear 240ms;
}
.cart-drawer.is-open {
  visibility: visible;
  transition: visibility 0s linear 0s;
}

.cart-scrim {
  position: absolute;
  inset: 0;
  background: rgba(25, 24, 23, 0.34);
  opacity: 0;
  transition: opacity 240ms cubic-bezier(0.16, 0.84, 0.24, 1);
}
.cart-drawer.is-open .cart-scrim { opacity: 1; }

.cart-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: min(440px, 100%);
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border-left: 1px solid var(--rule-strong);
  transform: translateX(100%);
  transition: transform 240ms cubic-bezier(0.16, 0.84, 0.24, 1);
}
.cart-panel:focus { outline: none; }
.cart-drawer.is-open .cart-panel { transform: translateX(0); }

.cart-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--rule);
}
.cart-title { margin: 0; font-size: var(--fs-h3); font-weight: 600; letter-spacing: var(--tr-h3); }
.cart-title-n { color: var(--muted); font-weight: 400; }
.cart-close {
  flex: none;
  width: 30px; height: 30px;
  font-family: var(--sans);
  font-size: 22px;
  line-height: 1;
  color: var(--ink-soft);
  background: transparent;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}
.cart-close:hover { color: var(--ink); }
.cart-close:focus-visible,
.cart-remove:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}

.cart-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 20px;
}

.cart-lines { list-style: none; margin: 0; padding: 0; }

.cart-line {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px 12px;
  padding: 18px 0;
  border-bottom: 1px solid var(--rule);
}
.cart-line-ctl {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
}

.cart-line-main { min-width: 0; }
.cart-line-name { font-weight: 600; color: var(--ink); text-decoration: none; }
.cart-line-name:hover { text-decoration: underline; text-underline-offset: 3px; }

/* max-width: none defeats the global p { max-width: 66ch }: these are
   already narrow inside a 440px panel and must not wrap early. */
.cart-line-spec,
.cart-line-ship,
.cart-flag {
  max-width: none;
  margin: 4px 0 0;
  font-size: 13.5px;
  color: var(--ink-soft);
}
.cart-sep { margin: 0 6px; color: var(--rule-strong); }

.cart-k {
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}

.cart-line-money {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.cart-line-price { display: block; font-size: 15px; font-weight: 600; color: var(--ink); white-space: nowrap; }
.cart-line-price.is-note {
  max-width: 130px;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 400;
  white-space: normal;
  color: var(--ink-soft);
}
.cart-line-each { display: block; margin-top: 2px; font-size: 12.5px; color: var(--muted); }

.cart-qty {
  min-width: 30px;
  padding: 9px 2px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13.5px;
  text-align: center;
}

.cart-remove {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: var(--radius);
  padding: 0;
  cursor: pointer;
}
.cart-remove:hover { color: var(--ink); }

.cart-foot {
  flex: none;
  padding: 16px 20px 20px;
  border-top: 1px solid var(--rule);
}
/* An empty cart shows no $0 subtotal and no dead Checkout button. */
.cart-drawer[data-cart-empty] .cart-foot { display: none; }

.cart-total { margin: 0 0 14px; }
.cart-total-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.cart-total-row dt {
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}
.cart-total-row dd { margin: 0; }
.cart-total-row.is-sum {
  margin-top: 4px;
  padding-top: 12px;
  border-top: 2px solid var(--ink);
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
}
.cart-total-row.is-sum dt { color: var(--ink); }

.cart-foot-note {
  margin: 10px 0 0;
  max-width: none;
  font-size: 13px;
  color: var(--ink-soft);
}
.cart-foot-note a { color: var(--accent); }

.cart-empty {
  padding: 56px 4px 32px;
  text-align: center;
}
.cart-empty-h { margin: 0 0 6px; font-weight: 600; color: var(--ink); }
.cart-empty-p { max-width: none; margin: 0 0 20px; font-size: 14px; color: var(--ink-soft); }

@media (prefers-reduced-motion: reduce) {
  .cart-drawer,
  .cart-scrim,
  .cart-panel {
    transition: none;
  }
}

/* Mobile: a full-width right-hand slide-over on a phone is just a page
   pretending to be temporary, so it becomes a bottom sheet instead. */
@media (max-width: 560px) {
  .cart-panel {
    top: auto; left: 0; right: 0; bottom: 0;
    width: 100%;
    max-height: 88vh;
    max-height: 88dvh;
    border-left: 0;
    border-top: 1px solid var(--rule-strong);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    transform: translateY(100%);
  }
  .cart-drawer.is-open .cart-panel { transform: translateY(0); }
  .cart-foot { padding-bottom: max(20px, env(safe-area-inset-bottom)); }
}

html.cart-open { overflow: hidden; }

/* ==========================================================================
   Cart page (/cart)
   The drawer's requisition-sheet look, spread across a two-column layout: a
   column-headed line list on the left, a sticky summary on the right.
   Extends .cart-line/.cart-line-ctl/.cart-line-money rather than redefining
   them, and reuses the drawer's own data-cart-empty pattern for the empty
   state.
   ========================================================================== */

.cart-page-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: clamp(24px, 4vw, 56px); align-items: start; }

.cart-cols { display: grid; grid-template-columns: minmax(0, 1fr) 168px 120px; gap: 0 16px; padding-bottom: 8px; }
.cart-cols span:last-child { text-align: right; }

.cart-page .cart-lines { border-top: 2px solid var(--ink); }
.cart-page .cart-line { grid-template-columns: minmax(0, 1fr) 168px 120px; padding: 18px 0; }
.cart-page .cart-line-ctl { grid-column: 2; grid-row: 1; flex-direction: column; align-items: flex-start; }
.cart-page .cart-line-money { grid-column: 3; grid-row: 1; }

/* CAS number, page-only: extends .cart-line-spec's existing spacing/size/
   colour/max-width and adds only the mono face. */
.cart-line-cas-v { font-family: var(--mono); }

.cart-summary { position: sticky; top: 92px; border-top: 2px solid var(--ink); padding-top: 16px; }
.cart-page-h { margin: 0 0 14px; font-size: var(--fs-h3); font-weight: 600; letter-spacing: var(--tr-h3); }

/* The destination fieldset: disabled in markup as the no-JS/shared-cart
   fallback; assets/cart.js's setupDestinationFieldset() enables it for
   every other cart visit. Reuses .cart-total-row's label tokens rather than
   introducing new ones. */
.cart-dest { border: 0; margin: 0 0 18px; padding: 0; }
.cart-dest[disabled] { opacity: 0.5; }
.cart-dest-row { margin: 0 0 10px; }
.cart-dest-row label,
.cart-dest-label {
  display: block;
  margin-bottom: 4px;
  padding: 0;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}
.cart-dest-row select {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14px;
}
.cart-dest-speed { display: flex; gap: 16px; }
.cart-dest-speed label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 0;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
}

/* Empty cart: no column heads, no line list, no summary. #cart-empty (in
   the page's own markup, outside .cart-page) is what shows instead. */
.cart-page[data-cart-empty] .cart-cols,
.cart-page[data-cart-empty] .cart-lines,
.cart-page[data-cart-empty] .cart-summary {
  display: none;
}

/* 900px: collapse to the drawer's own two-column line arrangement (name/
   spec left, money right, controls full-width below) rather than the
   three-column requisition layout, which has no room left at this width.
   The column heads label those three columns, so they go with them. */
@media (max-width: 900px) {
  .cart-cols { display: none; }
  .cart-page .cart-line { grid-template-columns: minmax(0, 1fr) auto; }
  .cart-page .cart-line-ctl { grid-column: 1 / -1; grid-row: auto; flex-direction: row; align-items: center; }
  .cart-page .cart-line-money { grid-column: auto; grid-row: auto; }
}

/* 820px, matching .pdp's own breakpoint: one column, summary unstuck so the
   money reads after the lines on a phone instead of pinned beside them. */
@media (max-width: 820px) {
  .cart-page-grid { grid-template-columns: 1fr; }
  .cart-summary { position: static; top: auto; }
}

@media print {
  .site-head,
  .site-foot,
  .cart-drawer,
  .cart-page .cart-line-ctl,
  #cart-checkout-page,
  .cart-summary .cart-foot-note,
  .mkt-toast {
    display: none !important;
  }
  .cart-page .cart-line,
  .cart-summary {
    break-inside: avoid;
  }
  .cart-line-name[href]::after { content: " (research.bohrmaterials.com" attr(href) ")"; font-family: var(--mono); font-size: 10px; color: #555; }
}

/* Share links (Task 6): the shared-cart banner and the page's action row
   (Browse Marketplace / Print / Copy link to this cart). The banner reuses
   the .note idiom (border, left rule, radius) with --accent-tint instead of
   #fff so it reads distinctly from the plain .note blocks elsewhere. */
.cart-shared {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 20px;
  padding: 12px 16px;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  border-radius: 0 3px 3px 0;
  background: var(--accent-tint);
}
.cart-shared p { margin: 0; max-width: none; font-size: 14px; color: var(--ink-soft); }

.cart-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }

@media print {
  .cart-shared,
  .cart-actions {
    display: none !important;
  }
}

/* ==========================================================================
   Account (/account)
   Task 9's assets/account.js renders three mutually exclusive bodies into
   #acct-page: signed-out (tabbed sign-in/create-account), signed-in (the
   email, sign-out and order history), and a single "unknown" note when the
   server could not be asked. Every class here
   is one assets/account.js actually emits (see its SIGNED_OUT_HTML,
   RESET_HTML and SIGNED_IN_HTML template strings).

   Reused rather than redefined: .btn/.btn-solid/.btn-lg for every button,
   .cart-k for the "Signed in as" label, and .table-scroll/.data-table
   (already shared) for order history -- none of those are touched here.
   ========================================================================== */

/* Recommendation cards. assets/account.js emits .acct-recs > h2 + .pcard-grid
   > a.pcard > (img.pcard-img + span.pcard-name), and until now NONE of those
   had a rule: the images rendered at their natural size -- several hundred
   pixels -- with a bare link underneath.

   Deliberately not .mkt-card: that card carries a purity select, a price and
   a lead time, and is a much heavier object. This is a thumbnail and a name,
   which is all the data the generated region holds. */
.acct-recs {
  margin-top: 34px;
}
.acct-recs > h2 {
  font-size: var(--fs-label);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 14px;
}

/* auto-fill, not auto-fit: with two or three cards auto-fit stretches them
   across the full width, which looks like a layout error next to a four-card
   row. auto-fill keeps the track width and leaves the gap. */
.pcard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 14px;
}

.pcard {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  text-decoration: none;
  color: var(--ink);
  transition: border-color 120ms ease, background 120ms ease;
}
.pcard:hover,
.pcard:focus-visible {
  border-color: var(--rule-strong);
  background: var(--paper-2);
}

/* aspect-ratio + object-fit, not a fixed height: the product photographs are
   not all the same shape, and letting them set their own height is what made
   this section a column of giant images. */
.pcard-img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: calc(var(--radius) - 2px);
  background: var(--paper-2);
}

.pcard-name {
  font-size: 14px;
  line-height: 1.35;
  color: var(--ink);
}

/* The header link's signed-in icon. Sized and stroked to match .cart-btn-ico
   exactly, since the two sit side by side. */
.acct-link-ico {
  display: block;
}
.site-nav a#acct-link[aria-label] {
  display: inline-flex;
  align-items: center;
}

.acct-notice {
  margin: 0 0 22px;
  padding: 12px 16px;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule-strong);
  border-radius: 0 3px 3px 0;
  background: #fff;
  font-size: 14px;
  color: var(--ink-soft);
  max-width: 72ch;
}

/* A consent sentence, not a field caption. The shared
   `#quote-form label, #acct-page label` rule (specificity 1-0-1) makes every
   label in this container uppercase, letter-spaced and block-level, which is
   right for an input's caption and wrong for a sentence the customer reads.
   #acct-page .acct-check label is 1-1-1 and wins with no !important.

   input[type="checkbox"] is not in that Forms selector list, so its
   width: 100% never applied here; flex: none only keeps the box from being
   squeezed by a long label. */
.acct-check { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 14px; }
.acct-check input[type="checkbox"] { flex: none; margin: 3px 0 0; }
#acct-page .acct-check label {
  display: inline;
  margin: 0;
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.45;
  color: var(--ink-soft);
}

.acct-auth { max-width: 460px; }

.acct-tabs {
  display: flex;
  gap: 24px;
  margin-bottom: 22px;
  border-bottom: 1px solid var(--rule-strong);
}
.acct-tab {
  padding: 4px 0 10px;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
}
.acct-tab:hover { color: var(--ink); }
.acct-tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }
.acct-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The forgot-password and resend-verification forms sit stacked below the
   sign-in/register form in the same tab panel; this is what separates them
   from it rather than running straight on. */
.acct-tabpanel form + form {
  margin-top: 22px;
  padding-top: 20px;
  border-top: 1px solid var(--rule);
}

.acct-error {
  margin: 0 0 14px;
  padding: 10px 14px;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--danger);
  border-radius: 0 3px 3px 0;
  background: var(--paper-2);
  font-size: 14px;
  color: var(--danger);
}

.acct-note { font-size: 14px; color: var(--ink-soft); margin: 0 0 14px; }

.acct-reset {
  max-width: 460px;
  margin-top: 24px;
  padding: 20px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: #fff;
}

/* Normalises every heading assets/account.js tags class="acct-h" -- a mix of
   <h2> and <h3> across the three states -- to one size, so the signed-in
   grid does not show two different type sizes for headings at the same
   visual rank. */
.acct-h {
  margin: 0 0 14px;
  font-family: var(--display);
  font-size: var(--fs-h3);
  font-weight: 600;
  letter-spacing: var(--tr-h3);
  color: var(--ink);
}

/* Signed-in layout: one column. minmax(0, 1fr) rather than a plain block so
   the order table's .table-scroll scrolls inside it instead of widening the
   page. */
.acct-grid { display: grid; grid-template-columns: minmax(0, 1fr); }

.acct-who { margin: 0 0 20px; font-size: 15px; color: var(--ink-soft); }
.acct-who .acct-email { color: var(--ink); font-weight: 600; }

.acct-sec { margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--rule); }

.acct-page-h { margin: 0 0 14px; font-size: var(--fs-h3); font-weight: 600; letter-spacing: var(--tr-h3); }

.acct-order-lines { margin: 0; padding: 0; list-style: none; font-size: 13.5px; }
.acct-order-lines li { margin-bottom: 4px; }
.acct-order-lines li:last-child { margin-bottom: 0; }
.acct-order-lines a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--rule-strong);
  text-underline-offset: 2px;
}

@media (max-width: 720px) {
  .mkt-grid { grid-template-columns: 1fr; }
}

/* hero CTA row: Browse Marketplace + co-brand lockup side by side, roomier hero */
.hero .lede { margin-bottom: 0; }
.hero-cta .btn { flex: none; }

/* /products' "Shop by category": category cards in a 2-column grid (image
   slot on top of each). */
.stock-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(18px, 2.5vw, 28px); margin-top: 18px; }
.stock-card { display: block; border: 1px solid var(--rule-strong); border-radius: 6px; background: #fff; text-decoration: none; overflow: hidden; }
.stock-card:hover { border-color: var(--ink); }
.stock-pic { aspect-ratio: 5 / 2; background: var(--paper-2); border-bottom: 1px solid var(--rule); overflow: hidden; }
.stock-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.stock-info { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 14px 16px; }
.stock-info h3 { margin: 0; font-size: 16.5px; }
.stock-more { font-size: 13.5px; color: var(--ink-soft); white-space: nowrap; }
.stock-card:hover .stock-more { text-decoration: underline; text-underline-offset: 3px; }
/* /products' tiles carry one line of copy under the count/link row; the home
   page's own stock-grid never needed this, so it is a plain sibling of
   .stock-info rather than a change to it. */
.stock-desc { margin: 0; padding: 0 16px 14px; font-size: 13.5px; color: var(--ink-soft); }
@media (max-width: 640px) { .stock-grid { grid-template-columns: 1fr; } }

.table-scroll + .note { margin-top: 20px; }

/* product detail page (fai.html pattern) */
.crumbs { margin: 18px 0 0; font-size: 13px; color: var(--muted); }
/* A crumb trail mixes links with plain <span>s -- on the 25 PDPs the category
   crumb is a span, because there is no /products/<category> page behind it any
   more. Both used to be var(--muted) with no underline, so the two were
   indistinguishable and a visitor clicked "High-Purity Inorganics" and nothing
   happened. The links now carry a resting underline and a darker ink; hover
   deepens the rule rather than introducing one. */
.crumbs a {
  color: var(--ink-soft);
  text-decoration: underline;
  text-decoration-color: var(--rule-strong);
  text-underline-offset: 3px;
}
.crumbs a:hover { color: var(--ink); text-decoration-color: currentColor; }
.crumbs span[aria-hidden] { margin: 0 6px; }
.note-inline { font-size: 13.5px; color: var(--muted); }
.pd-uses { margin: 0 0 14px; padding-left: 20px; max-width: 72ch; }
.pd-uses li { margin-bottom: 6px; }
#specs .data-table th[scope="row"] { width: 200px; }

/* Amazon-style PDP layout (fai.html): title top, big image left, buy panel right */
.pdp-title { padding-top: 22px; }
.pdp-title h1 { margin-bottom: 6px; }
.pdp { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 40px; align-items: start; margin-top: 22px; }
@media (max-width: 820px) { .pdp { grid-template-columns: 1fr; gap: 24px; } }
.pdp-photo { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border: 1px solid var(--rule-strong); border-radius: 8px; background: var(--paper-2); }
.pdp-label { flex: none; width: 110px; font-weight: 600; color: var(--ink-soft); }
.pdp-price { font-size: 22px; margin: 4px 0 12px; }
.pdp-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.pdp-qty-wrap { display: flex; align-items: center; gap: 12px; }
.pdp-qty { min-width: 36px; padding: 7px 4px; font-family: var(--mono); font-size: 13.5px; text-align: center; }
.pdp-qty-wrap .qty-step { width: 32px; padding: 7px 0; }

/* PDP option chips (grade / pack size toggles) */
.opt-group { display: flex; align-items: center; gap: 16px; margin: 0 0 14px; flex-wrap: wrap; }
.opt-label { flex: none; width: 80px; font-size: 13px; font-weight: 600; color: var(--ink-soft); }
.opt-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.opt-chip { padding: 8px 18px; font-family: var(--mono); font-size: 13.5px; color: var(--ink-soft); background: #fff; border: 1px solid var(--rule-strong); border-radius: var(--radius-pill); cursor: pointer; }
.opt-chip:hover { border-color: var(--ink); color: var(--ink); }
.opt-chip.active { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* Keyboard focus ring. */
.opt-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--ink);
}

/* hazardous-material badge: small red box beside the product title (DG/hazmat items only) */
.haz-badge { display: inline-block; vertical-align: middle; margin-left: 14px; padding: 3px 9px; background: #fbeaea; color: #b3261e; border: 1px solid #b3261e; border-radius: 4px; font-family: var(--sans); font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; position: relative; top: -3px; }

/* PDP v2: photo left; description with buy-box card beneath it on the right */
.pdp-desc .buybox { max-width: 430px; margin-top: 10px; }
.buybox { padding: 0; }
.buybox .pdp-price { margin: 0 0 14px; }
.buybox .opt-group { flex-direction: column; align-items: flex-start; gap: 8px; }
.buybox .opt-label { width: auto; }
.buybox-ship { display: flex; gap: 10px; align-items: baseline; margin: 14px 0; padding: 10px 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); font-size: 14px; }
.buybox .pdp-actions { flex-direction: column; gap: 10px; }
.buybox .pdp-actions .btn { width: 100%; }

/* marketplace card "See more" link to the product page */
.mkt-more { margin: -6px 0 10px; }
/* See more: outlined mini button */
.mkt-more a { display: inline-block; padding: 5px 13px; border: 1.5px solid var(--ink); border-radius: var(--radius-pill); font-size: 13px; font-weight: 600; color: var(--ink); text-decoration: none; }
.mkt-more a:hover { color: var(--paper); text-decoration: none; text-underline-offset: 3px; background: var(--ink); }

/* smaller hazard badge for table rows and marketplace card names */
.haz-sm { font-size: 9px; padding: 2px 7px; margin-left: 8px; top: -2px; }
