/* ============================================================================
   Nova Market — stylesheet

   Section order: tokens → base → layout → navbar → buttons → hero → company
   strip → products (toolbar, grid, card) → modal → cart → auth card → form
   fields → features → features (collapsible) → footer → responsive.
   Every section is wrapped in matching BEGIN/END banners.

   Bump the ?v= query on the <link> in index.html after editing this file —
   browsers cache local CSS aggressively, even on a hard reload.
   ============================================================================ */


/* ==== BEGIN: Design tokens ================================================= */

:root {
  /* Sage bands — the navbar and footer share one colour, so it lives in a
     single token both point at. */
  --surface-band: #6f9f6f;
  --surface-nav: var(--surface-band);
  --surface-footer: var(--surface-band);

  /* Creamy-white main section, with two supporting surfaces stepped off it:
     --card sits above the page (raised), --bg-soft below it (inset). */
  --bg: #f8f5ec;             /* main section — creamy white */
  --card: #fffdf7;           /* raised cards — warm white   */
  --bg-soft: #edefe4;        /* inset fields, media boxes   */
  --border: #d7dfd2;         /* hairlines — sage-tinted     */

  /* Ink, not white: this is a light theme. Both clear AA on cream (13.4:1)
     and on the sage bands (4.8:1). */
  --text: #1d2b20;
  --text-dim: #5a6b5c;

  /* Accents. --accent-2 and --danger are darkened from typical dark-theme
     values, which fail contrast as text on cream (1.7:1 and 2.5:1).
     --accent-2 doubles as the company-name colour on product cards. */
  --accent: #6c5ce7;
  /* Second stop of the primary button's gradient. It is a token because a
     company landing page overrides --accent with its own colour, and a
     hardcoded purple second stop then clashes with every one of them. */
  --accent-lift: #8b7cf0;
  --accent-2: #0a7a68;
  --danger: #c0392b;
  --star: #b5811f;           /* rating glyph — warm gold */

  /* Sunset yellow — the navbar monogram badge, gold falling to orange. Warm
     against the sage band rather than tinted from it, which is the point: the
     badge is the one place on the bar that is not a shade of green.

     --brand-ink carries BOTH the "SP" monogram and the wordmark beside it, and
     it is a token because the two have to stay the same ink — a monogram a
     shade off its own wordmark reads as a rendering fault.

     Measured by repainting this gradient to a canvas at the badge's real 38px,
     masking it to the glyphs and reading the pixels actually behind ink — not
     from the stop values, which are optimistic: the glyphs sit in the middle of
     the sweep and never touch the pale corner. Worst glyph pixel 7.4:1, best
     9.0:1, so the monogram clears AAA on every pixel of the plate. The wordmark
     is 5.4:1 on the sage band, against the 4.8:1 plain --text was giving it.

     There is no footer plate any more — the footer heading is the name alone,
     so the navbar's 38px badge is now the only place this gradient carries a
     glyph. The rule the footer used to keep is still worth stating, because it
     is what made resizing the plate free: glyph and box scaled together, so the
     type landed on the same part of the sweep and 7.4:1 / 9.0:1 held without
     re-measuring. That does not generalise — .btn-sunset resizes the box
     WITHOUT the type and its numbers move, see the note there.

     Cream ink — what the badge used over its old purple — measures 1.8:1 on this
     plate and is unreadable. The ink flips dark exactly because the plate went
     light; the two cannot be changed independently. */
  --sunset-1: #ffc24b;
  --sunset-2: #f9a337;
  --sunset-3: #f2802a;
  --brand-ink: #14220f;

  --radius: 14px;
  --shadow: 0 12px 32px rgba(28,46,32,0.10);

  /* Fallback height of the fixed navbar, used before JS runs. The
     useFixedNavHeight hook measures the real height on mount and on resize
     and writes it to this property inline on <html>, which wins over this
     rule — so editing the value here only affects the first paint. */
  --nav-h: 70px;
}

/* ==== END: Design tokens =================================================== */


/* ==== BEGIN: Base / reset ================================================== */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Segoe UI", system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
  scroll-behavior: smooth;
}

#root { min-height: 100vh; }

a { color: inherit; text-decoration: none; }

button { font-family: inherit; cursor: pointer; }

h1, h2, h3 { letter-spacing: -0.2px; }

/* ==== END: Base / reset ==================================================== */


/* ==== BEGIN: Layout — page bands & container =============================== */
/* Each band paints edge to edge; the .container inside it keeps content on one
   shared grid. See the App component in app.jsx. */

.page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

/* Navbar band — pinned to the top of the viewport, so it sits above the page
   content and out of normal flow. A soft shadow separates it from the cream
   below; a hairline border would read as a highlight against the sage. */
.navbar-wrap {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: var(--surface-nav);
  box-shadow:
    0 1px 0 rgba(28,46,32,0.14),
    0 4px 16px rgba(28,46,32,0.08);
}

/* Body band — creamy white, with two very faint green glows behind the hero
   that echo the sage bands. */
.body-wrap {
  flex: 1;
  padding-top: var(--nav-h); /* clears the fixed navbar above */
  background:
    radial-gradient(1000px 500px at 10% -10%, rgba(16,185,129,0.10), transparent),
    radial-gradient(800px 400px at 100% 0%, rgba(0,212,181,0.07), transparent),
    var(--bg);
}

.container {
  max-width: 1160px;
  margin: 0 auto;
  padding: 0 24px;
  width: 100%;
}

/* Anchor targets would otherwise land underneath the fixed navbar when jumped
   to from the hero buttons, or from an #about deep link. */
.products,
.company-strip,
.profile {
  scroll-margin-top: calc(var(--nav-h) + 20px);
}

/* ==== END: Layout — page bands & container ================================= */


/* ==== BEGIN: Navbar ======================================================== */
/* Three zones: brand, search (grows), then the sign-in and cart actions. */

.navbar {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 14px 0;
}

.brand {
  display: flex;
  align-items: center;
  gap: 11px;
  flex-shrink: 0;
}

/* The wordmark, STACKED: "Savannah" over "PHARMACEUTICALS LIMITED" in small
   tracked capitals. Two lines rather than one because the full legal name is
   what the bar should say and one line cannot afford it -- inline, and showing only
   the TRADING name, it measured 286px and left a company-page search field 0px at
   768px, which is why the old bar printed a truncated "Savannah" from 1023px down.
   Stacked, the whole LEGAL name measures 205px: the bar gained "Limited" and lost
   80px at the same time, which is the whole argument for the second line.

   Still ONE flex item, and that is load-bearing: .brand is a flex row with an
   11px gap, so two bare children would open that gap inside the name itself. */
.brand-name {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* nowrap on the column, not the lines: .brand does not shrink, so there is
     nowhere for a THIRD line to go if either of these two wrapped. */
  white-space: nowrap;
}

.brand-line-a {
  font-weight: 800;
  font-size: 1.22rem;
  line-height: 1.04;
  letter-spacing: -0.2px;
  color: var(--brand-ink);
}

/* Hierarchy here is size, weight and tracking ONLY -- deliberately not opacity.
   Dropped to 82% over the sage band this line measures 4.1:1, and at ~9px it is
   the smallest type on the bar, which is the last place to spend contrast. Full
   --brand-ink keeps it at 5.4:1. */
.brand-line-b {
  margin-top: 2px;
  /* Cancels the trailing letter-space the tracking adds after the final "D",
     which would otherwise pad the column 1.4px wider than its glyphs and leave
     the lockup looking left-shifted under "Savannah". */
  margin-right: -0.145em;
  font-weight: 700;
  font-size: 0.575rem;
  line-height: 1.1;
  letter-spacing: 0.145em;
  text-transform: uppercase;
  color: var(--brand-ink);
}

/* The "SP" monogram on its sunset plate. See --sunset-* / --brand-ink for why
   the ink is dark rather than the cream the old purple plate carried. */
.brand-mark {
  width: 38px;
  height: 38px;
  border-radius: 11px;
  background: linear-gradient(145deg, var(--sunset-1) 0%, var(--sunset-2) 52%, var(--sunset-3) 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 0.98rem;
  letter-spacing: 0.5px;
  /* Centres the pair against that tracking: flex centres the whole advance
     width, trailing letter-space included, which sits "SP" a hair to the left. */
  text-indent: 0.5px;
  color: var(--brand-ink);
  flex-shrink: 0;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
  /* The monogram is safe (7.4:1) but the PLATE is not: sampled against the sage
     at its four corners it runs 1.2:1 to 1.9:1, so the square itself has almost
     no edge to stand on and dissolves into the band at a glance. Hence a border
     drawn in shadow rather than none: pale inner rim to lift the top, hairline to
     draw the boundary, soft drop to seat it. This is the shape's contrast, not
     the text's, which is why it is solved with an outline instead of a colour. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.55),
    0 0 0 1px rgba(28,46,32,0.18),
    0 2px 6px rgba(28,46,32,0.22);
}

/* 38px of plate is a big enough target to want feedback, which the old 32px
   square never had. Hovering ANYWHERE on the lockup lifts it -- the trigger is
   the whole link, the thing that moves is only the badge, so the name does not
   shift under the pointer.

   `a.brand`, not `.brand`: the footer heading wears the same class and is an
   <h2>. Unqualified, this would make a non-interactive heading animate under the
   pointer and promise a click that is not there. */
a.brand:hover .brand-mark {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.65),
    0 0 0 1px rgba(28,46,32,0.24),
    0 4px 10px rgba(28,46,32,0.28);
  transform: translateY(-1px);
}

/* -- Search ---------------------------------------------------------------- */

.nav-search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1;
  min-width: 0;
  max-width: 420px;
}

/* Sits over the field, not the sage band, so it takes the input's ink at 55%.
   Larger than the 0.8rem the 🔍 it replaced needed — see .showcase-tile-icon
   for the same correction. */
.nav-search-icon {
  position: absolute;
  left: 12px;
  font-size: 0.95rem;
  color: var(--text);
  opacity: 0.55;
  pointer-events: none;
}

/* The right padding is the Search button's width plus its inset, so a long
   query scrolls under the button rather than behind it. */
.nav-search input {
  width: 100%;
  padding: 10px 84px 10px 36px;
  border-radius: 999px;
  border: 1px solid rgba(28,46,32,0.18);
  background: rgba(255,255,255,0.94);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.nav-search input::placeholder { color: #7b8a7d; }

.nav-search input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(108,92,231,0.18);
}

/* Absolutely positioned inside the pill, like the magnifier opposite it, so
   adding it changed nothing about how the field sizes itself. It carries .btn
   and .btn-primary from <Button /> and overrides only what has to change to fit
   a 40px field. Three of those overrides are load-bearing:

   1. The selector is a DESCENDANT PAIR, not the bare class. .btn is declared
      further down this file, so at equal specificity its own padding and radius
      win on source order and the button renders full height, breaking out of
      the field. Same trap as .footer-to-top's min-width — see the note there.

   2. FLAT --accent, not .btn-primary's gradient. This is the smallest primary
      button on the site (0.78rem against 0.92rem everywhere else), and white on
      the gradient measured 3.69:1 at worst under this label, 4.39:1 at best —
      both under the 4.5:1 AA asks of text this size. Rasterised from the
      composited stack, not read off the two stops. Flat is 4.86:1. Same finding
      and same fix as the hero banner's buttons.

   3. The hover inverts .btn-primary's brightness(1.08), which would lift
      #6c5ce7 straight back under AA. Darkening can only raise the ratio: 5.74:1.

   --accent here is always the site violet, on all eleven pages: companyStyle()
   puts a company's accent on .company-page and the navbar is a sibling band
   outside it. That is why the contrast above is one figure rather than eleven,
   and why .nav-search input:focus can hardcode a violet ring. The found-card
   ring is the opposite case — see it below. */
.nav-search .nav-search-go {
  position: absolute;
  right: 4px;
  padding: 7px 16px;
  border-radius: 999px;
  font-size: 0.78rem;
  background: var(--accent);
  z-index: 1;
}

.nav-search .nav-search-go:hover { filter: brightness(0.9); }

/* Company name beside the brand on a landing page. Full --text, not
   --text-dim: the dim ink only reaches 1.9:1 on the sage band. The separator
   is dimmed with opacity instead, which keeps the same hue. */
.nav-crumb {
  flex-shrink: 0;
  font-size: 0.9rem;
  font-weight: 600;
  white-space: nowrap;
}

/* THE SEPARATOR IS DIMMED BY ITS OWN CLASS, not by a blanket `.nav-crumb span`.
   The blanket form worked only while the crumb held nothing but a chevron and a
   text node: an element placed beside them was dimmed to 0.55 as well, and
   undoing that took a two-class selector — (0,2,0) against (0,1,1) — because a
   single class loses to a class-plus-type wherever in the file it sits. Scoped
   here, anything added inside the crumb opts in rather than having to opt out.
   Labels are bare text nodes, at full contrast with no rule of their own. */
.nav-crumb-sep {
  opacity: 0.55;
  margin-right: 4px;
}

/* -- Actions & cart badge -------------------------------------------------- */

.nav-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
  flex-shrink: 0;
}

/* Who is signed in, beside the sign-out button. Truncates rather than wraps:
   the navbar is a single row here and a long address would push the cart out. */
.nav-account {
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.85rem;
  font-weight: 600;
}

.cart-btn { position: relative; }

/* The sage-coloured ring lifts the badge off both the purple button and the
   band behind it. */
.cart-badge {
  position: absolute;
  top: -7px;
  right: -7px;
  min-width: 21px;
  height: 21px;
  padding: 0 5px;
  border-radius: 999px;
  border: 2px solid var(--surface-band);
  background: var(--danger);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ==== END: Navbar ========================================================== */


/* ==== BEGIN: Buttons ======================================================= */
/* Shared by the navbar, hero, product cards, cart and auth forms. inline-flex
   so the class also works on <a> elements (the hero uses anchor buttons). */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 18px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-size: 0.92rem;
  font-weight: 600;
  text-align: center;
  transition: all 0.15s ease;
}

.btn:hover { border-color: var(--accent); }

.btn-primary {
  background: linear-gradient(135deg, var(--accent), var(--accent-lift));
  border: none;
  color: white;
}

.btn-primary:hover { filter: brightness(1.08); }

/* The navbar plate's fill, promoted to a button variant. TWO callers, and they
   sit on two DIFFERENT backgrounds: "Back to top" on the sage footer band, and
   "All companies" on the cream body of a company page. One class serves both,
   which is only possible because the ring is drawn in --brand-ink: that clears
   1.4.11's 3:1 against each of them (5.4:1 on the sage, 15.2:1 on the cream). The
   white ring the blue Back to top used to carry could not have moved here — on
   cream it measures 1.09:1 and is simply not there.

   THE LABEL IS DARK, and that is not a taste call. White on this fill measures
   2.0:1. --brand-ink, sampled the way the --sunset-* tokens describe but masked
   to each label's own rect, measures 6.6:1 at worst and 9.7:1 at best. The worst
   case is the 154px "All companies", not the 190px "Back to top" (7.0:1): a
   shorter box runs the same three stops over less distance, so proportionally
   more of the dark orange end sits under the label. BOX SIZE MOVES THESE NUMBERS
   and the type does not scale with it, so re-measure rather than assume if either
   button's width changes. (The badge is the opposite case — see .brand-mark.)

   THE RING IS NOT TRIM, for the same reason it was not on the blue: the fill is
   1.2-1.9:1 against the sage and 1.5:1 against the cream, so this button cannot
   be separated from either band by colour alone. Take the ring away and neither
   placement passes. A softer brown ring was the nicer-looking option and is the
   one to avoid: #7a3f0f is 2.7:1 on the sage, under the line. */
.btn-sunset {
  border: 2px solid var(--brand-ink);
  background: linear-gradient(145deg, var(--sunset-1) 0%, var(--sunset-2) 52%, var(--sunset-3) 100%);
  color: var(--brand-ink);
  font-weight: 700;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.55),
    0 8px 20px rgba(28,46,32,0.26);
}

/* Brightens AND lifts. The brighter gradient is safe by construction — every
   stop moves toward white, so dark-ink contrast only goes UP: the two worst
   pixels go 6.6:1 → 7.4:1 and 7.0:1 → 7.8:1. That is the exact opposite of the
   trap the blue had, where a brighter fill walked its white label under AA, and
   it is why this variant may brighten on hover where the blue could not. */
.btn-sunset:hover {
  /* Repeats the ring, because `.btn:hover` would otherwise recolour it to
     --accent and the ring is the whole of this button's edge. */
  border-color: var(--brand-ink);
  background: linear-gradient(145deg, #ffcc63 0%, #ffae44 52%, #f78c33 100%);
  transform: translateY(-2px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.65),
    0 14px 30px rgba(28,46,32,0.32);
}

/* Ink rather than white, so the one rule works on the sage band and the cream body
   both — 5.4:1 and 15.2:1 against those. The 3px offset puts it on the background
   rather than on the plate, so those two are the figures that matter; against the
   plate itself it would still clear, at 6.6:1 worst. */
.btn-sunset:focus-visible {
  outline: 3px solid var(--brand-ink);
  outline-offset: 3px;
}

/* btn-gold and btn-sm lived here for the product card's "Add" button, and the
   gold/gold-light/gold-ink tokens above existed only to colour it. That button
   is now a quantity stepper (qty-stepper), so all five had no remaining user
   and were removed rather than left as dead rules. The star token is separate
   and still colours the rating glyph.

   Class names in this comment are written WITHOUT a leading dot on purpose:
   the class-name audit greps for `.name` and reads a dotted name in a comment
   as a live selector, so it reported all five as dead CSS on every run. */

.btn-block { width: 100%; }

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn:disabled:hover {
  filter: none;
  border-color: var(--border);
}

/* Inline text button — "Forgot password?" and the login/signup switch. */
.link-btn {
  background: none;
  border: none;
  color: var(--accent-2);
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0;
}

/* ==== END: Buttons ========================================================= */


/* ==== BEGIN: Hero banner =================================================== */
/* The panel the hero copy sits on: a background clip, patterned with the ten
   dosage forms. Five layers, z-index 0-4, inside `isolation: isolate` so the
   stack is local to the panel and cannot interact with the fixed navbar's
   z-index: 100.

     0  .hero-video   the background clip, object-fit: cover
     1  ::before      drifting colour wash (the animated part)
     2  ::after       flat veil over the whole panel - the contrast guard
     3  .hero-motifs  ten dosage-form icons, spread over the whole panel and
                      spinning (no company monogram or wordmark - see <Hero />)
     4  .hero         the copy, over its own plate at z-index -1

   THE PANEL IS DARK, and that is the clip's doing rather than a taste call.
   Sampled over the real file - 24 frames x 2 banner crops, 131k pixels inside
   the band the copy occupies - it runs dark: median relative luminance 0.07,
   average RGB (95,78,66). Dark ink on that is unreadable, and a veil light
   enough to keep the old cream panel would need about 0.9 alpha, at which point
   there is no visible video left to have added. So the inks flipped to pale and
   the panel went dark with them.

   Nothing here depends on the clip loading. The panel's own background is the
   same dark, so a failed fetch, an unsupported codec, or prefers-reduced-motion
   (where <Hero /> does not render the video at all) degrades to a plain dark
   panel - never to pale text on cream.

   THE GUARD, layer 2. The clip's median is dark but its HIGHLIGHTS reach 0.767,
   and for pale copy it is the highlights that decide legibility. A flat 0.55
   veil of #101a13 pins the brightest pixel in the copy band at 0.171, which is
   what puts every ink below through AA. Flat and static on purpose: a guarantee
   that animates, or that fades out before the panel edge, is not one - the
   motifs are out there too.

   THE PLATE, inside layer 4. One veil cannot both protect the text and leave the
   motifs visible, because the motifs cross the text: a tint bright enough to
   read against the clip collapses a pale line that drifts over it. Measured at a
   single veil the two demands meet exactly at luminance 0.16 - a knife edge with
   no value satisfying both. The plate splits them by sitting above the motif
   field and below the words. Without it the motif ceiling is opacity 0.20 and
   the icons are a whisper; with it they run at 0.50.

   THE BANNER INKS. The user asked for bright solid colour here, so the copy is
   coloured rather than plain, and every value is a flat colour - the headline
   phrase used to be a gradient clipped to the text, which is neither solid nor
   as legible as the ink at either end of it.

   Measured over the composite (veil 0.55 + plate 0.48), worst case = the clip's
   brightest sampled pixel in the band the copy occupies:

     --hero-ink          #f4f1e6   7.21:1  body paragraph
     --hero-violet-lift  #cdc2ff   4.95:1  headline
     --hero-magenta      #ff9ecb   4.35:1  headline phrase - 46px bold, so AA
                                           Large (3:1) is the bar it has to clear
     --hero-teal         #7fe3cf   5.59:1  slogan, and 4.85:1 as the ticker label
                                           at 0.7rem - AA at any size
     --hero-violet       #4c2ae0           FILL: eyebrow chip, both buttons
     --hero-fill         #0a7a68           FILL: note chip

   Two fills and four inks. The FILLS DID NOT FLIP when the panel did: an opaque
   chip is its own background, so its text is measured against the fill and never
   sees the clip - which is the whole point of setting them solid. White on
   --hero-violet is 7.74:1 and on --hero-fill 5.25:1, both unchanged from the
   cream panel. That also makes the two chips and the primary button the one part
   of this panel that needs no re-measuring if the clip is ever swapped.

   #0a7a68 is AA as a fill but only 4.28:1 as small text on the old ground, so it
   is used one way and not the other. */

.hero-banner {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin: 18px 0 0;
  border: 1px solid var(--border);
  border-radius: 24px;
  background-color: #16221a;
  background-image: linear-gradient(180deg, #1b2a20, #101a13);
  box-shadow: 0 18px 40px rgba(28,46,32,0.06);

  /* Declared here rather than in :root because they are banner copy colours,
     not site tokens - and .hero is a descendant, so it inherits all six. */
  --hero-ink: #f4f1e6;
  --hero-violet-lift: #cdc2ff;
  --hero-magenta: #ff9ecb;
  --hero-teal: #7fe3cf;
  --hero-violet: #4c2ae0;
  --hero-fill: #0a7a68;
}

/* Layer 0 - the clip. `cover` so it fills the panel at any aspect without
   letterboxing, which means the visible crop changes with the breakpoint: the
   panel is far wider than the 1936x1080 source on a desktop, so what shows is
   the vertical middle of the frame, and on a phone the horizontal middle. Both
   extremes were sampled before the veil below was sized.

   Inert - decoration, `aria-hidden` in the JSX, no controls - and not rendered
   at all under prefers-reduced-motion, so there is no paused state to style and
   none of the 4.1 MB is fetched. */
.hero-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* Layer 1 - three soft colour fields, drifting. Inset negatively so the blobs
   can move without their edges ever entering the panel. */
.hero-banner::before {
  content: "";
  position: absolute;
  inset: -22%;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(closest-side, rgba(108,92,231,0.13), transparent) 10% 20% / 48% 54% no-repeat,
    radial-gradient(closest-side, rgba(10,122,104,0.13), transparent) 90% 26% / 44% 52% no-repeat,
    radial-gradient(closest-side, rgba(181,129,31,0.10), transparent) 62% 92% / 42% 48% no-repeat;
  animation: hero-wash 34s ease-in-out infinite alternate;
}

@keyframes hero-wash {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(2.5%, -2.5%, 0) scale(1.07); }
}

/* Layer 2 - the contrast guard. Static on purpose: if it moved with the wash it
   would stop being a guarantee.

   FLAT, and covering the whole panel, where the cream version was a radial that
   faded out by 70%. Two reasons it had to change shape as well as colour: the
   clip reaches the panel edge where the old cream background was uniform, and
   the motifs live out there, so a guard that governs only the middle governs
   neither. 0.55 of #101a13 - see the sampling note above. */
.hero-banner::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: rgba(16,26,19,0.55);
}

/* Layer 3 - the dosage-form field, spread over the whole panel on the brick grid
   HERO_MOTIFS computes. It used to be masked out of the middle so it framed the
   copy; the user asked for even cover including the centre, so the mask is gone
   and every icon now crosses body copy at some point in its drift.

   That is what sets the opacity, and the sum flipped when the panel went dark.
   On cream the icons were the DARKEST thing under the copy and 0.10 was a
   ceiling; on the clip they are the PALEST thing under it, and the same crossing
   pulls the other way. Measured against the composited stack, worst case being
   the palest of the ten tints at the clip's brightest frame:

     0.50  motif 4.26:1 against its backdrop, body copy over it 7.21:1   <- used
     0.20  motif 1.79:1 - a whisper - body copy over it 4.63:1

   0.50 is only available because the plate (.hero::before) sits between the
   field and the words; without it the ceiling is the 0.20 row. Holds at every
   width with no per-breakpoint override. */
.hero-motifs {
  position: absolute;
  inset: 0;
  z-index: 3;
  overflow: hidden;
  pointer-events: none;
}

/* <Icon> draws at 1em, so font-size is the icon's size and the span is exactly
   1em square - which is why the negative margins centre it on its point without
   a translate, leaving `transform` free for the animations below.

   The ink arrives inline from ICON_TONES - the same pair the showcase tiles and
   feature cards use, so a syrup bottle here is the amber it is everywhere else on
   the site. */
.hero-motif {
  position: absolute;
  top: var(--motif-y);
  left: var(--motif-x);
  margin: -0.5em 0 0 -0.5em;
  font-size: clamp(2.4rem, 5.6vw, 4.6rem);
  line-height: 1;
  opacity: 0.5;

  /* Two animations, one job each: a full turn, and a slow float. Longhands
     rather than the shorthand because every value is a per-icon custom property
     and the pairs have to line up in the same order in both lists. */
  animation-name: hero-motif-spin, hero-motif-float;
  animation-duration: var(--motif-spin), var(--motif-float);
  animation-timing-function: linear, ease-in-out;
  animation-iteration-count: infinite, infinite;
  animation-direction: var(--motif-direction), alternate;
  animation-delay: var(--motif-delay), var(--motif-delay);
}

/* A full 360, so direction is a real choice rather than a wobble: half the field
   turns clockwise and half anticlockwise, from `--motif-direction`. */
@keyframes hero-motif-spin {
  to { rotate: 360deg; }
}

/* Separate property from the spin (`translate`, not `transform`), so the two
   animations compose instead of the second overwriting the first. */
@keyframes hero-motif-float {
  from { translate: 0 0; }
  to   { translate: 10px -14px; }
}

/* Both animated parts of the banner, off. The third moving thing - the clip - is
   not listed because <Hero /> does not render it under this preference at all,
   so there is nothing here to pause and nothing to fetch. */
@media (prefers-reduced-motion: reduce) {
  .hero-banner::before,
  .hero-motif { animation: none; }
}

/* ==== END: Hero banner ===================================================== */


/* ==== BEGIN: Hero ========================================================== */
/* Single centred promo column - the auth card that used to sit beside it now
   opens in a modal from the navbar. Horizontal padding is not decoration: inside
   the banner panel the column would otherwise touch the panel's border on a
   phone. */

.hero {
  position: relative;
  z-index: 4;
  text-align: center;
  max-width: 720px;
  margin: 0 auto;
  padding: 56px 24px 52px;
}

/* The copy's own backdrop - see THE PLATE in the banner note above. z-index -1
   inside .hero, which is itself z-index 4: that paints it at the bottom of
   .hero's own stacking context, so it lands above the motif field (layer 3) and
   below these words. It is the one position from which it can darken an icon
   that has drifted under a line without dimming the field anywhere else.

   THE INSET AND THE STOP ARE LOAD-BEARING, and the reason is that this is an
   ELLIPSE over a block of text that is nearly a rectangle. The corners of a wide
   line sit far outside an ellipse drawn to the same box: at the first values
   tried here (inset -12% -8%, solid to 62%) the headline was 521px inside a
   407px solid region, so its first and last words sat at alpha 0.12 instead of
   0.48 and measured 2.41:1. Sized from the text boxes outward instead, the solid
   ellipse now contains every line at every width. If the copy column is ever
   made wider, re-measure - do not assume this still clears.

   0.55 out to 80% of the radius, feathered to nothing after, so it has no
   visible edge. Worst case measured over the clip's brightest frames, each line
   sampled on an 18-point grid, with the palest of the ten tints crossing it:

     body paragraph 7.21:1    slogan 5.59:1    ticker 4.85:1
     headline 4.95:1          headline phrase 4.35:1 (46px bold, AA Large = 3:1)

   The icons are NOT starved by it: 4.26:1 against their own backdrop out at the
   panel corner, where the field is what the eye reads. */
.hero::before {
  content: "";
  position: absolute;
  inset: -35% -30%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    closest-side,
    rgba(12,20,16,0.55),
    rgba(12,20,16,0.55) 80%,
    rgba(12,20,16,0) 100%
  );
}

/* The count chip. Filled solid rather than tinted: it sits at the top of the
   column where small uppercase text would otherwise read against whatever the
   clip and the drift happen to be doing. An opaque fill takes both out of the
   measurement entirely - white on --hero-violet is 7.74:1 wherever the chip
   lands, and that figure survived the panel going dark untouched. */
.eyebrow {
  display: inline-block;
  color: #fff;
  background: var(--hero-violet);
  border: none;
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  margin-bottom: 18px;
  box-shadow: 0 6px 16px rgba(76,42,224,0.28);
}

/* Ambient, cycling company name — see <HeroTicker /> in app.jsx. The name fades
   and lifts in on every remount; the key change on .hero-ticker-name is what
   forces that remount each rotation, same trick as .showcase-progress. */
.hero-ticker {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-bottom: 18px;
  font-size: 0.85rem;
}

/* Solid teal at full strength. It was --text-dim at 0.7 opacity, which put it at
   roughly 3.9:1 on the ground and under AA - faint reads as unfinished next to a
   coloured company name, not as restraint. */
.hero-ticker-label {
  color: var(--hero-teal);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  font-size: 0.7rem;
  font-weight: 700;
}

/* The colour arrives inline from the company's own palette, so the line previews
   that company's page. It reads `tint` rather than `ink` since the panel went
   dark - the same swap the motifs made, and for the same reason: all ten inks
   are deep saturated values that disappear into the clip, while the ten tints
   are pale, carry the same hue, and clear AA over the plate. This rule sets
   weight and size only. */
.hero-ticker-name {
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.2px;
  animation: hero-ticker-in 0.5s ease;
}

/* No plate rule here on purpose: this line is the company name alone, as is the
   navbar crumb — neither carries a company mark, and a photograph was tried in
   both and taken back out. The animal belongs to .company-card-plate and
   .company-hero-plate. */

@keyframes hero-ticker-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-ticker-name { animation: none; }
}

/* Two solid inks, not one ink and one gradient. Both are set here rather than
   inherited from --text: the headline is the brightest thing in the banner and
   near-black would have read as the odd line out once everything around it was
   coloured. */
.hero h1 {
  color: var(--hero-violet-lift);
  font-size: 2.9rem;
  font-weight: 800;
  line-height: 1.1;
  margin: 0 0 18px;
  letter-spacing: -0.5px;
}

/* The phrase inside the headline. It carried a violet-to-teal gradient clipped to
   the glyphs, which meant the middle of the phrase was whatever lay between two
   accents - around 3.5:1 at the crossover, and washed out at that. A flat magenta
   is both brighter and measurable: 5.08:1 over the plate, so it now clears AA at
   any size rather than leaning on AA Large as it did on the cream panel. */
.hero h1 span {
  color: var(--hero-magenta);
}

/* Full ink, not --text-dim. Three coloured lines above it need one plain block
   to push against, and dim grey under bright copy reads as switched off. */
.hero p {
  color: var(--hero-ink);
  font-size: 1.05rem;
  line-height: 1.7;
  max-width: 560px;
  margin: 0 auto 30px;
}

/* The slogan, between the headline and the paragraph. Full --text rather than
   the dim the paragraph below it takes: three sizes of ink down the column -
   headline, slogan, body - is what keeps it a subhead instead of a second
   sentence of the same block. Not uppercased; it arrives in title case with
   full stops, and shouting it would fight the headline.

   Two classes deep on purpose: `.hero p` above sets colour, size and margin, and
   a single class loses to it on specificity - the line would come out --text and
   1.05rem however this rule is written.

   Teal is the third hue of the banner and the only one that carries a whole line
   of small text on the open ground, so it is the lightest of the three inks now
   that the panel is dark - 6.63:1 over the plate, AA at any size, including the
   0.95rem it drops to on a small phone. */
.hero .hero-slogan {
  color: var(--hero-teal);
  font-size: 1.12rem;
  font-weight: 700;
  letter-spacing: 0.2px;
  line-height: 1.5;
  max-width: 520px;
  margin: -2px auto 20px;
}
.hero-actions {
  display: flex;
  gap: 14px;
  align-items: center;
  justify-content: center;
}

/* Both buttons, scoped to the banner so the site-wide ones are untouched.

   The primary button is normally an --accent gradient. Here it is flat
   --hero-violet: it is the one gradient the banner had left once the headline
   went solid, and it was also the lighter violet the eyebrow chip used to be, so
   flattening it settles the colour and the finish in one go. White on it is
   7.74:1, up from about 4.9 over the gradient's first stop.

   The secondary button keeps `.btn`'s white fill but not its hairline border and
   body ink, which are right in a toolbar and much too quiet standing next to a
   filled violet button under a magenta headline - it read as disabled. */
.hero-actions .btn-primary {
  background: var(--hero-violet);
  box-shadow: 0 8px 20px rgba(76,42,224,0.30);
}

.hero-actions .btn-primary:hover { filter: brightness(1.12); }

.hero-actions .btn:not(.btn-primary) {
  border: 2px solid var(--hero-violet);
  color: var(--hero-violet);
  font-weight: 700;
}

/* `border-color` repeats the rule above and is still load-bearing: `.btn:hover`
   sets it to --accent at the same specificity, so dropping this line turns the
   outline the site purple on hover instead of the headline violet. */
.hero-actions .btn:not(.btn-primary):hover {
  border-color: var(--hero-violet);
  background: rgba(76,42,224,0.07);
}

/* Bottom chip, filled to match the eyebrow at the top. It sits at the bottom of
   the column where the plate has begun to feather, so the same reasoning applies
   with more force: its own opaque fill is the background its text is measured
   against (5.25:1), not the clip behind it. */
.hero-note {
  display: inline-block;
  margin-top: 20px;
  padding: 7px 15px;
  border-radius: 999px;
  background: var(--hero-fill);
  color: #fff;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.2px;
  box-shadow: 0 6px 16px rgba(10,122,104,0.24);
}

/* ==== END: Hero ============================================================ */


/* ==== BEGIN: Company directory ============================================= */
/* The ten companies on the home page. Each card is a link to that company's
   landing page, and wears that company's colour set and display face — so the
   directory previews the ten pages instead of listing ten identical rows.
   All the --co-* and --mk-* properties come from companyStyle() inline. */

.company-strip {
  text-align: center;
  padding: 8px 0 44px;
}

.company-strip h2 {
  font-size: 1.5rem;
  margin: 0 0 8px;
}

.company-strip > p {
  color: var(--text-dim);
  font-size: 0.95rem;
  margin: 0 0 24px;
}

.company-directory {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(285px, 1fr));
  gap: 14px;
  text-align: left;
}

/* The count spans both columns on its own row, so a long legal name wrapping
   to three lines cannot push it out of alignment with the neighbouring cards. */
.company-card {
  display: grid;
  grid-template-columns: 60px 1fr;
  grid-template-areas:
    "mono  body"
    "count count";
  gap: 0 14px;
  align-items: center;
  padding: 16px;
  border-radius: var(--radius);
  border: 1px solid var(--co-edge);
  background: var(--card);
  color: var(--text);
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

.company-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
  border-color: var(--co-ink);
}

.company-card:focus-visible {
  outline: 2px solid var(--co-ink);
  outline-offset: 2px;
}

/* The animal photograph's frame, in the grid cell the monogram tile used to
   fill — same 60px square, same radius, same tinted border, so the directory's
   geometry is untouched. `overflow: hidden` is what the photo needs and the
   letters did not: without it the image's square corners sit outside the
   rounded frame. The --mk-* font properties are gone with the letters. */
.company-card-plate {
  grid-area: mono;
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 12px;
  background: var(--co-tint);
  border: 1px solid var(--co-edge);
}

.company-card-body {
  grid-area: body;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The wordmark is set in the company's own display face — same idea as the
   brand mark on a product card: type standing in for a logo that has no file.

   overflow-wrap is load-bearing, not defensive. "PHARMACEUTICALS" in Segoe
   Script measures 173px against a 125px body column on a 264px-wide viewport,
   and one unbreakable word pushed the whole page into horizontal scroll.

   `anywhere` rather than `break-word`, and the difference is the whole fix:
   both break a word that cannot fit on its own line, but only `anywhere`
   reduces the element's min-content width. `break-word` left min-content at the
   full word, so a flex item sized to fit-content still resolved wider than its
   container and overflowed anyway. Neither breaks a word that fits, so normal
   widths are unaffected either way. */
.company-card-mark {
  overflow-wrap: anywhere;
  font-family: var(--mk-font);
  font-weight: var(--mk-weight);
  font-style: var(--mk-style);
  letter-spacing: var(--mk-tracking);
  text-transform: var(--mk-transform);
  font-size: calc(1.2rem * var(--mk-scale));
  line-height: 1.15;
  color: var(--co-ink);
}

/* The motto pill, where the PPB number used to sit. `align-self: flex-start`
   keeps it hugging its text rather than stretching the body column, exactly as
   before — but the tracking drops from 0.6px to 0.2px, because 0.6px was set
   for a five-character code and spaces out a sentence like a shop sign. Long
   mottos wrap inside the pill; the count below it spans its own grid row, so a
   two-line pill cannot pull the cards out of alignment. */
.company-card-motto {
  align-self: flex-start;
  margin-top: 8px;
  padding: 3px 9px;
  border-radius: 6px;
  background: var(--co-tint);
  color: var(--co-ink);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.2px;
  line-height: 1.35;
}

/* -- The animal photographs ------------------------------------------------ */
/* One rule for every logo on the site: the directory card's 60px plate and all
   five hero plates. The files are opaque landscape photographs of very
   different proportions — 813x450 for the lion, 318x331 for the rhino — so
   `cover` is doing real work here, not sitting in as a default. It fills the
   square from the centre of the frame, which is where the animal is in all ten.

   `display: block` because an inline image sits on the text baseline and would
   leave a few pixels of tint under it inside a plate that has no text at all. */
.company-logo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.company-card-count {
  grid-area: count;
  margin-top: 13px;
  padding-top: 11px;
  border-top: 1px solid var(--border);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-dim);
}

/* -- Company search hits --------------------------------------------------- */
/* Searching a company name surfaces its page, not only its products. Without
   this, the one search term most likely to find a company whose catalogue is
   still empty returns nothing at all. */

.company-hits {
  padding: 0 0 30px;
  text-align: left;
}

.company-hits h2 {
  margin: 0 0 12px;
  font-size: 1.05rem;
}

.company-hit-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.company-hit {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px 7px 8px;
  border-radius: 999px;
  border: 1px solid var(--co-edge);
  background: var(--co-tint);
  color: var(--co-ink);
  font-size: 0.8rem;
  font-weight: 700;
}

.company-hit:hover { filter: brightness(0.97); }

.company-hit span {
  width: 24px;
  height: 24px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card);
  font-family: var(--mk-font);
  font-size: calc(0.68rem * var(--mk-scale));
  line-height: 1;
}

/* ==== END: Company directory =============================================== */


/* ==== BEGIN: Showcase ====================================================== */
/* Home-page browse aid: dosage forms and therapeutic categories, with a
   spotlight that rotates through whichever panel is open.

   The category tiles and the spotlight take their colour from the --tc-* set
   that <Showcase /> puts on each <li> — the same family colours the product
   plates use, so a colour learned here still means the same thing in the grid.
   Dosage forms carry no --tc-*, which is why every colour below is written as
   var(--tc-x, fallback): categories come out tinted, forms neutral, from one
   rule rather than two. */

.showcase {
  padding: 8px 0 48px;
  text-align: center;
}

.showcase h2 {
  margin: 0 0 8px;
  font-size: 1.5rem;
}

.showcase-lead {
  margin: 0 0 20px;
  color: var(--text-dim);
  font-size: 0.95rem;
}

/* -- Panel switch (a segmented control) ------------------------------------ */

.showcase-tabs {
  display: inline-flex;
  gap: 4px;
  margin-bottom: 20px;
  padding: 4px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-soft);
}

.showcase-tab {
  padding: 8px 17px;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--text-dim);
  font-size: 0.85rem;
  font-weight: 700;
  transition: all 0.15s ease;
}

.showcase-tab:hover { color: var(--text); }

.showcase-tab.active {
  background: var(--card);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(28,46,32,0.12);
}

/* -- Spotlight ------------------------------------------------------------- */
/* No opacity on any text in here. The ink/tint pairs clear AA at 5.6:1 at
   worst, and knocking the detail line back to 0.8 alpha drops it under 4.5:1 —
   size is what separates the two lines instead. */

.showcase-spot {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 18px;
  max-width: 640px;
  margin: 0 auto 20px;
  padding: 20px 24px;
  border-radius: var(--radius);
  border: 1px solid var(--tc-edge, var(--border));
  background: var(--tc-tint, var(--bg-soft));
  color: var(--tc-ink, var(--text));
  text-align: left;
}

.showcase-spot-icon {
  flex-shrink: 0;
  font-size: 2rem;
  line-height: 1;
}

/* Every drawn icon (<Icon />) — ten dosage forms, three feature cards, five
   contact channels. They carry width/height in `em` as attributes, so the box
   follows the font-size wherever they are dropped; this only fixes how they
   sit. display:block removes the baseline gap that would otherwise push an
   icon off centre from its label, and is why none of the spans that host one
   need a display of their own. */
.ui-icon {
  display: block;
  flex-shrink: 0;
}

/* The chip's own 0.76rem would leave the icon noticeably smaller than the
   label it sits beside. */
.filter-chip .ui-icon {
  width: 1.2em;
  height: 1.2em;
}

.showcase-spot-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.showcase-spot-label {
  font-size: 1.05rem;
  font-weight: 700;
}

.showcase-spot-detail {
  font-size: 0.82rem;
  line-height: 1.45;
}

.showcase-spot-count {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 1.3rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.showcase-spot-count small {
  display: block;
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.8px;
}

/* Runs for exactly one rotation. --rot is passed in from SHOWCASE_ROTATE_MS so
   the bar and the timer cannot drift apart, and the element is keyed on the
   spotlight index so remounting restarts the animation on every tick. */
@keyframes showcase-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.showcase-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: currentColor;
  opacity: 0.4;
  transform-origin: left;
  animation: showcase-progress var(--rot) linear forwards;
}

/* Nothing rotates under this preference, so a progress bar would sit at 0
   forever. <Showcase /> skips the timer for the same reason. */
@media (prefers-reduced-motion: reduce) {
  .showcase-progress { display: none; }
}

/* -- Tiles ---------------------------------------------------------------- */

.showcase-tiles {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.showcase-tiles li { display: flex; }

.showcase-tile {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 13px;
  border-radius: 999px;
  border: 1px solid var(--tc-edge, var(--border));
  background: var(--tc-tint, var(--card));
  color: var(--tc-ink, var(--text));
  font-size: 0.8rem;
  font-weight: 600;
  text-align: left;
  transition: all 0.15s ease;
}

/* A stroked icon reads smaller than an emoji of the same em size, so the tile
   runs slightly larger than the 0.95rem the emoji set used to. */
.showcase-tile-icon { font-size: 1.05rem; line-height: 1; }

.showcase-tile-count {
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: rgba(255,255,255,0.72);
  font-size: 0.68rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* Which tile the spotlight is on. A border swap rather than a ring or a nudge:
   this changes every 2.8s beside a wall of other chips, and anything that
   alters size would reflow the whole row on every tick. */
.showcase-tile.spotlit:not(.active) { border-color: var(--tc-ink, var(--accent)); }

button.showcase-tile:hover { border-color: var(--tc-ink, var(--accent)); }

/* Selected — filled. White on every --tc-ink clears AA, and on the --accent
   fallback it is 4.86:1. */
.showcase-tile.active {
  background: var(--tc-ink, var(--accent));
  border-color: transparent;
  color: #fff;
}

.showcase-tile.active .showcase-tile-count {
  background: rgba(255,255,255,0.22);
  color: #fff;
}

/* Rendered as a span while the catalogue is empty — there is nothing to filter,
   so it must not look or behave like a control. */
.showcase-tile.is-static { cursor: default; }

/* ==== END: Showcase ======================================================== */


/* ==== BEGIN: Company landing page ========================================== */
/* One page per company at #/company/<slug>. companyStyle() sets --co-* and
   --mk-* on .company-page and also overrides --accent, which is what recolours
   the buttons, chips, focus rings and pager inside it per company.

   Five hero treatments — split, banner, stack, rule, panel — two companies
   each, and no two sharing a treatment share a colour, a face or an animal.
   The shared pieces are defined once here; each layout-* block only arranges
   them. All five frame the animal photograph in .company-hero-plate and print
   the company's motto where its PPB number used to be. */

.company-page { padding-top: 6px; }

.company-hero {
  margin: 0 0 34px;
  border-radius: var(--radius);
  border: 1px solid var(--co-edge);
  background: var(--card);
  overflow: hidden;
}

.company-hero-text { min-width: 0; }

.company-hero-mark {
  margin: 0;
  /* Same reason as .company-card-mark, including why it is `anywhere` — a
     display face at this size has single words wider than a phone, and the
     collapsed heroes size their text column to fit-content. */
  overflow-wrap: anywhere;
  font-family: var(--mk-font);
  font-weight: var(--mk-weight);
  font-style: var(--mk-style);
  letter-spacing: var(--mk-tracking);
  text-transform: var(--mk-transform);
  font-size: calc(2.5rem * var(--mk-scale));
  line-height: 1.1;
  color: var(--co-ink);
}

.company-hero-status {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--co-tint);
  border: 1px solid var(--co-edge);
  color: var(--co-ink);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.4px;
}

/* One labelled pair on a hero — a <dl> with a small caps label over a value in
   the company's ink. Two callers, and that is why the name is `fact` rather than
   anything more specific: it carries the motto in four of the five layouts, and
   in `panel` a second copy carries the catalogue count. It was `.company-reg`
   until 2026-08-04, which stopped being true when the PPB number came off the
   page — the rule never held a registration number again. */
.company-fact {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 16px 0 0;
}

.company-fact dt {
  margin: 0;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  color: var(--text-dim);
}

/* The motto, not a registration number: 0.5px of tracking was set for
   "PPB/G/1671" and reads as a spaced-out shop sign on a sentence. `line-height`
   is here because a long motto can now wrap, which a code never did. */
.company-fact dd {
  margin: 0;
  font-size: 0.92rem;
  font-weight: 700;
  letter-spacing: 0.1px;
  line-height: 1.4;
  color: var(--co-ink);
}

/* The animal photograph's frame, used by all five hero treatments — the shared
   box the old monogram span sat in, minus the type properties. `overflow` is
   what clips the photo's square corners to the 16px radius. */
.company-hero-plate {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  overflow: hidden;
  width: 118px;
  height: 118px;
  border-radius: 16px;
  background: var(--co-tint);
  border: 1px solid var(--co-edge);
}

/* -- layout: split — photo tile left, copy right -------------------------- */
.company-hero.layout-split {
  display: flex;
  align-items: center;
  gap: 26px;
  padding: 28px;
}

.company-hero.layout-split .company-hero-status { margin-bottom: 12px; }

/* -- layout: banner — tinted band, copy left, photo tile right ------------ */
/* The band used to hold an oversized monogram watermark, absolutely positioned
   and clipped by .company-hero's overflow at 0.15 opacity. A photograph cannot
   play that part — at that opacity it is a grey smudge, not an animal — so the
   watermark is gone and the plate sits on the right instead, mirroring split's
   on the left. The band keeps its own identity through the gradient.
   `space-between` is what puts the plate on the far edge; the two former
   watermark helpers (position: relative here, z-index on the text) went with
   the layer they were stacking against. */
.company-hero.layout-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 26px;
  padding: 34px 30px;
  background: linear-gradient(100deg, var(--co-tint), var(--card) 72%);
}

.company-hero.layout-banner .company-hero-status { margin-bottom: 12px; }

/* -- layout: stack — everything centred ----------------------------------- */
.company-hero.layout-stack {
  padding: 36px 28px;
  text-align: center;
}

.company-hero.layout-stack .company-hero-text {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.company-hero.layout-stack .company-hero-plate { margin-bottom: 18px; }
.company-hero.layout-stack .company-hero-status { margin-top: 14px; }
.company-hero.layout-stack .company-fact { justify-content: center; }

/* -- layout: rule — thick left rule, motto as a stamp --------------------- */
.company-hero.layout-rule {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  /* Left padding is 0 on purpose — the border IS the left edge, and the text
     supplies its own inset. */
  padding: 30px 28px 30px 0;
  border-left: 10px solid var(--co-ink);
}

.company-hero.layout-rule .company-hero-text { padding-left: 26px; }
.company-hero.layout-rule .company-hero-status { margin-top: 14px; }

/* The plate is inside the text column here, so the column's 26px inset off the
   thick rule aligns it with the wordmark below rather than a margin of its own. */
.company-hero.layout-rule .company-hero-plate { margin-bottom: 16px; }

/* The stamp holds a motto now, not a registration number, and that is why
   `flex-shrink` is 1 with a max-width rather than 0: a sentence in a box that
   refuses to shrink pushes the wordmark beside it into overflow, which
   "PPB/BM" never could. The width caps it before it takes half the band. */
.company-hero-stamp {
  flex-shrink: 1;
  max-width: 230px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 20px;
  border: 2px dashed var(--co-edge);
  border-radius: 10px;
  background: var(--co-tint);
  text-align: center;
}

.company-hero-stamp-label {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-dim);
}

/* Same reasoning as .company-fact dd — 1px of tracking was set for a code. It
   also has to wrap now: "All in one, one in all!" is not "PPB/BM". */
.company-hero-stamp-value {
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0.1px;
  line-height: 1.3;
  color: var(--co-ink);
}

/* -- layout: panel — copy left, motto card right -------------------------- */
.company-hero.layout-panel {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 26px;
  align-items: center;
  padding: 30px;
}

.company-hero-panel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 215px;
  padding: 18px 20px;
  border-radius: 12px;
  background: var(--co-tint);
  border: 1px solid var(--co-edge);
}

/* Smaller than the 118px plate the other four layouts use: this one sits inside
   a 215px card beside the motto rather than standing on its own. */
.company-hero-panel .company-hero-plate {
  width: 84px;
  height: 84px;
  border-radius: 12px;
  margin-bottom: 10px;
}

.company-hero-panel .company-fact {
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  margin-top: 10px;
}

/* -- Page footer nav & the pending-catalogue empty state ------------------- */

.company-footer-nav { padding: 0 0 56px; }

/* ==== END: Company landing page ============================================ */


/* ==== BEGIN: Disclosure ==================================================== */
/* The shared show/hide section behind the category rail, the showcase's tile
   list, the fifty-category index and the profile band's four copy rows. One
   button, one region — see <Disclosure /> for why the region is `hidden` rather
   than unmounted. */

/* flex-start rather than space-between, because the chevron is a THIRD child
   now (it used to sit inside disclosure-meta and got clipped there — see the
   comment in the Disclosure component). With three items, space-between would
   strand the summary in the middle of the row; disclosure-meta takes
   margin-left: auto instead, which keeps the heading left and the summary and
   chevron together on the right exactly as before. */
.disclosure-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  padding: 11px 15px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-soft);
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.disclosure-toggle:hover { border-color: var(--accent); }

/* Square off the bottom while open, so the button and the list it reveals read
   as one panel rather than two stacked boxes. */
.disclosure.is-open .disclosure-toggle {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom-color: transparent;
}

.disclosure-heading {
  text-transform: uppercase;
  letter-spacing: 0.9px;
  font-size: 0.72rem;
}

/* Carries the summary — for a filter, the current selection. Allowed to shrink
   and ellipsise, because a long category name must not push the chevron off the
   end of the button. */
/* Holds only the summary text now, so it is a plain block that ellipsises
   rather than a flex row — text-overflow works on a text container and never
   worked on the flex children this used to have. margin-left: auto is what
   pushes it and the chevron to the right edge under flex-start above. */
.disclosure-meta {
  display: block;
  margin-left: auto;
  min-width: 0;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.disclosure-chevron {
  flex-shrink: 0;
  font-size: 0.7rem;
  transition: transform 0.2s ease;
}

.disclosure.is-open .disclosure-chevron { transform: rotate(180deg); }

/* No display property of its own, deliberately: the `hidden` attribute's UA
   rule is display:none at very low specificity, and any class rule setting a
   display here would silently beat it and leave the region visible when
   collapsed. Stated explicitly as well, so a later edit cannot reintroduce that. */
.disclosure-region {
  padding: 12px 2px 2px;
}

.disclosure-region[hidden] { display: none; }

/* ==== END: Disclosure ====================================================== */


/* ==== BEGIN: Category rail ================================================= */
/* Filter chips for the page in view, behind a <Disclosure />. Only categories
   that hold a product get a chip — a rail of fifty, forty of them empty, is a
   wall rather than a filter. Counts come from the scope *before* the category
   filter, so selecting one does not zero every other chip. */

.category-rail { margin: 0 0 22px; }

/* An active filter tints the closed header, so a collapsed rail still shows at
   a glance that the grid below it is filtered. The summary text names which. */
.category-rail.is-filtered .disclosure-toggle {
  border-color: var(--accent);
  background: var(--card);
}

.category-rail.is-filtered .disclosure-meta { color: var(--accent); }

.category-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.category-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid var(--tc-edge);
  background: var(--card);
  color: var(--text);
  font-size: 0.8rem;
  font-weight: 600;
  text-align: left;
  transition: all 0.15s ease;
}

.category-chip:hover {
  background: var(--tc-tint);
  color: var(--tc-ink);
}

.category-chip.active {
  background: var(--tc-tint);
  border-color: var(--tc-ink);
  color: var(--tc-ink);
}

/* The "All categories" chip has no family colour of its own, so it borrows the
   neutral border and the sage band for its active state. */
.category-chip.all { border-color: var(--border); }

.category-chip.all.active {
  background: var(--surface-band);
  border-color: var(--surface-band);
  color: var(--text);
}

.category-chip-count {
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--bg-soft);
  font-size: 0.68rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.category-chip.active .category-chip-count { background: rgba(255,255,255,0.72); }

/* ==== END: Category rail =================================================== */


/* ==== BEGIN: Category index (all fifty) ==================================== */
/* What a company page shows in place of a catalogue: the frame the uploaded
   products will be grouped into, already carrying its family colours. Collapsed
   by default — fifty chips open is a lot of page to scroll past. */

.category-index {
  margin: 18px auto 22px;
  max-width: 720px;
  text-align: left;
}

/* The toggle, its meta line and the chevron rotation all moved to .disclosure-*
   when <CategoryIndex /> was rewritten on top of <Disclosure />, which the
   category rail and the showcase tiles also use. */

.category-index-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.category-index-list li {
  padding: 5px 11px;
  border-radius: 999px;
  background: var(--tc-tint);
  border: 1px solid var(--tc-edge);
  color: var(--tc-ink);
  font-size: 0.74rem;
  font-weight: 600;
}

/* ==== END: Category index (all fifty) ====================================== */


/* ==== BEGIN: Category groups =============================================== */
/* Products grouped under therapeutic category headings, inside one page of the
   pager. The heading rule takes the category's family colour, so a group reads
   as the same colour as the plates of the cards beneath it.

   `.category-group` itself has no rule and does not need one: the gap between
   groups comes from this flex column, and the element's job is to carry the
   category's --tc-* custom properties down to its heading. It is one of four
   class names in the JSX with no stylesheet rule, all intentional — the others
   are `.showcase-panel` (tabpanel wrapper, block children), `.showcase-tile-label`
   (a span inside a flex tile) and the runtime-composed `layout-` prefix, which
   lands on the .layout-split/-banner/-stack/-rule/-panel rules further down.
   A class-name audit will list these; they are the known-good set. */

.category-groups {
  display: flex;
  flex-direction: column;
  gap: 30px;
}

.category-group-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 2px solid var(--tc-edge);
}

.category-group-head h3 {
  margin: 0;
  font-size: 1.02rem;
  letter-spacing: -0.1px;
  color: var(--tc-ink);
}

.category-group-count {
  margin-left: auto;
  white-space: nowrap;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-dim);
}

/* ==== END: Category groups ================================================= */


/* ==== BEGIN: Pager ========================================================= */
/* 1 · 2 · 3 · 4 … with Prev and Next. pageNumbers() inserts the ellipses, so a
   forty-page catalogue still renders seven-odd buttons. The active button uses
   --accent, which a company page has overridden with its own colour. */

.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 34px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
}

.pager-list {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pager-num,
.pager-step {
  min-width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 12px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-size: 0.88rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: all 0.15s ease;
}

.pager-num:hover:not(.active),
.pager-step:hover:not(:disabled) { border-color: var(--accent); }

.pager-num.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  cursor: default;
}

.pager-step:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.pager-gap {
  padding: 0 4px;
  font-weight: 700;
  color: var(--text-dim);
}

/* ==== END: Pager =========================================================== */


/* ==== BEGIN: Products — toolbar ============================================ */

.products { padding-bottom: 72px; }

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

/* Flex so the active-filter chip can sit beside the count and wrap under it on
   a narrow toolbar. */
.toolbar-count {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 0.9rem;
  color: var(--text-dim);
}

.toolbar-count strong { color: var(--text); }

/* Shows that a dosage-form filter is on, and clears it. The showcase tile that
   set it is usually scrolled off the top by the time you are reading the grid,
   so its active state is not visible here. */
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--accent);
  background: var(--card);
  color: var(--text);
  font-size: 0.76rem;
  font-weight: 700;
}

.filter-chip:hover { background: var(--bg-soft); }

.filter-chip-x { font-size: 0.7rem; }

.toolbar-controls {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

/* <label> wrapping a caption and a <select>. The chevron is a pseudo-element
   so no image or data URI is needed. */
.select-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.82rem;
  color: var(--text-dim);
  font-weight: 600;
}

.select-wrap::after {
  content: "▾";
  position: absolute;
  right: 12px;
  font-size: 0.7rem;
  color: var(--text-dim);
  pointer-events: none;
}

.select-wrap select {
  appearance: none;
  -webkit-appearance: none;
  padding: 8px 30px 8px 12px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  outline: none;
  cursor: pointer;
}

.select-wrap select:focus { border-color: var(--accent); }

/* ==== END: Products — toolbar ============================================== */


/* ==== BEGIN: Products — grid & card ======================================== */
/* auto-fill, not auto-fit: when a filter narrows the results to one or two
   products the cards keep their size instead of stretching across the row. */

/* 218px min: the card is dense but narrow — the widest fixed content is the
   "Dissolution / 86% at 45 min (buffer stage)" spec row. */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(218px, 1fr));
  gap: 14px;
}

/* container-type makes the card a query container, so its own contents break on
   the CARD's width rather than the viewport's — see the @container block at the
   end of this section. That distinction matters: the same viewport produces very
   different card widths (a 375px phone shows one ~343px card, while a 700px
   tablet shows three ~210px ones), so viewport queries get this backwards. */
.product-card {
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.product-card:hover {
  box-shadow: var(--shadow);
  transform: translateY(-2px);
}

/* -- The card under the pointer -------------------------------------------- */
/* Asked for on 2026-08-15: the whole card inverts to a dark ground with white
   text while the pointer is on it.

   BEHIND @media (hover: hover), which is not optional. A touch browser with no
   pointer fires :hover on tap and then LEAVES IT APPLIED until something else
   is tapped, so on a phone the last card touched would stay inverted while the
   customer scrolled — indistinguishable from a selection state the app does not
   have. The query means a touch device never enters this block at all.

   rgba() over an opaque colour is the request as given, and it does something
   useful: the card ground shows through, so a card on the cream page body and
   the same card on a company landing page settle at slightly different darks
   and each stays keyed to its own page.

   THE PLATE HAS TO BE CLEARED SEPARATELY. It paints --tc-tint over the card's
   own background, so recolouring .product-card alone would have inverted the
   lower two thirds and left the brand plate pastel — the card would have looked
   half-painted rather than hovered.

   Everything below is one of two jobs: something that hardcodes a dark ink and
   now needs white, or something that fills itself with a light surface and now
   needs a translucent one. Nothing here changes a size, a weight or a position,
   so the card cannot reflow under the pointer and the grid cannot twitch. */
@media (hover: hover) {
  .product-card:not(.is-placeholder):hover {
    background: rgba(33, 41, 49, 0.8);
    border-color: rgba(33, 41, 49, 0.8);
    color: #fff;
  }

  /* The tint comes off so the dark shows through the plate too; the rule under
     it stays, at the weight a hairline has on a dark ground. */
  .product-card:not(.is-placeholder):hover .product-plate {
    background: none;
    border-bottom-color: rgba(255,255,255,0.22);
  }

  /* The mark included — so the five BRAND_INKS colours are off while hovered.
     Deliberate, and measured: rgba(33,41,49,0.8) composites to #4c5257 over the
     page body, and against that the five inks run 1.06:1 to 1.35:1 — every one
     of them illegible, the blue worst. White is 7.92:1. A legible white mark
     beats a colour-coded unreadable one, and the colour is back the instant the
     pointer leaves, which is the whole of what a hover state promises. */
  .product-card:not(.is-placeholder):hover .product-brandmark,
  .product-card:not(.is-placeholder):hover .product-company,
  .product-card:not(.is-placeholder):hover .product-generic,
  .product-card:not(.is-placeholder):hover .product-specs dt,
  .product-card:not(.is-placeholder):hover .product-specs dd,
  .product-card:not(.is-placeholder):hover .product-prices dt,
  .product-card:not(.is-placeholder):hover .product-prices dd,
  .product-card:not(.is-placeholder):hover .product-prices dd.is-emphasis,
  .product-card:not(.is-placeholder):hover .release-field-label,
  .product-card:not(.is-placeholder):hover .release-field-input,
  .product-card:not(.is-placeholder):hover .qty-value,
  .product-card:not(.is-placeholder):hover .doc-link,
  .product-card:not(.is-placeholder):hover .product-compare {
    color: #fff;
  }

  /* Blink and WebKit grey a disabled input through -webkit-text-fill-color and
     ignore `color`, so the count on an unsellable card needs both or it stays
     dark grey on the dark ground — see .qty-input:disabled. */
  .product-card:not(.is-placeholder):hover .qty-input,
  .product-card:not(.is-placeholder):hover .qty-input:disabled {
    color: #fff;
    -webkit-text-fill-color: #fff;
  }

  .product-card:not(.is-placeholder):hover .qty-line-total,
  .product-card:not(.is-placeholder):hover .release-field-input::placeholder {
    color: rgba(255,255,255,0.78);
  }

  /* The three hairlines that divide the card's blocks. */
  .product-card:not(.is-placeholder):hover .product-specs,
  .product-card:not(.is-placeholder):hover .product-prices,
  .product-card:not(.is-placeholder):hover .qty-stepper {
    border-color: rgba(255,255,255,0.22);
  }

  /* Chips and controls that fill themselves with a light surface. Each gets a
     translucent white instead, so it stays a distinct object on the dark ground
     without becoming a bright block on it. */
  .product-card:not(.is-placeholder):hover .product-class {
    background: rgba(255,255,255,0.14);
    border-color: rgba(255,255,255,0.34);
    color: #fff;
  }

  .product-card:not(.is-placeholder):hover .product-new {
    background: none;
    border-color: rgba(255,255,255,0.55);
    color: #fff;
  }

  .product-card:not(.is-placeholder):hover .qty-stepper { background: rgba(255,255,255,0.08); }

  .product-card:not(.is-placeholder):hover .qty-btn {
    background: rgba(255,255,255,0.16);
    color: #fff;
  }

  /* .qty-add keeps the accent fill — it is the card's primary action and the
     one thing that should not recede when everything around it darkens. */
  .product-card:not(.is-placeholder):hover .qty-add { background: var(--accent); }

  /* Both typed fields fill with a light surface on their own hover and focus
     (--bg-soft and --card), which under white text is white on near-white. The
     translucent replacements keep the "you can type here" feedback and keep the
     figure readable. The focus outline is untouched: it is drawn in --accent
     outside the box and reads on either ground. */
  .product-card:not(.is-placeholder):hover .release-field-input:hover,
  .product-card:not(.is-placeholder):hover .release-field-input:focus,
  .product-card:not(.is-placeholder):hover .qty-input:hover:not(:disabled),
  .product-card:not(.is-placeholder):hover .qty-input:focus {
    background: rgba(255,255,255,0.16);
  }

  /* Pending-data bars are filled with --bg-soft, which on this ground would be
     a row of bright blocks — louder than the values they stand in for. */
  .product-card:not(.is-placeholder):hover .ph-bar { background: rgba(255,255,255,0.28); }

  /* The one line an incomplete card exists to show. It keeps its own strip so
     it stays set apart from the figures above it.

     This is where the red has to be re-stated rather than inherited. The strip
     composites to #62686d under the pointer, and --danger measures 1.04:1 on
     that — not dim, gone. Nor can the red simply be lightened until it passes:
     the ground is mid-grey, so 4.5:1 needs a luminance around 0.79, which is
     nearly white whatever hue it carries. #ffe0da is the compromise that was
     actually measured — 4.55:1, so it clears AA, while still holding enough of
     a warm cast to read as the same red rather than as plain white. The border
     keeps more saturation than the text because it only has to be SEEN, not
     read: #ffb3a7 is 3.29:1, over the 3:1 that non-text contrast asks for. */
  .product-card:not(.is-placeholder):hover .product-inactive-note {
    background: rgba(255,255,255,0.12);
    border-left-color: #ffb3a7;
    color: #ffe0da;
  }

  /* A pack shot keeps a near-white ground: a photograph letterboxed by
     PRODUCT_IMAGE_FIT needs a neutral behind it, and a translucent box would
     put the dark card through the empty strip beside a portrait carton. */
  .product-card:not(.is-placeholder):hover .pack-image { border-color: rgba(255,255,255,0.3); }
}
/* A listing missing one of REQUIRED_FIELDS. The hover lift goes, because the
   lift is this grid's way of saying "this one is actionable" and this one is not
   — the same reason .is-placeholder drops it.

   THE DARK HOVER GROUND ABOVE STILL APPLIES HERE, and the split is the point:
   the lift claims the card can be acted on, while the ground only says the
   pointer is on this one. An incomplete listing is still a card to be read, and
   in a grid of twelve, being able to see which one you are reading is worth
   more on the card whose text you are about to quote back to a supplier. A
   placeholder card takes neither, having nothing to read either.

   Nothing else about the card is dimmed, and that is a decision rather than an
   omission. It is a real product with a real gap, so every figure the price list
   DID supply has to stay readable; and .ph-bar is filled with --bg-soft, so
   tinting the card surface would have made the pending-data bars — the very
   things an incomplete card exists to show — invisible against it. The state is
   carried by .product-inactive-note and the disabled stepper instead, both of
   which say it in words. */
.product-card.is-inactive:hover {
  box-shadow: none;
  transform: none;
}
/* A real card's missing generic name, as opposed to a placeholder card's. The
   paragraph is otherwise one to three lines of text, so the bar takes a line's
   worth of height and the card keeps its rhythm with the ones beside it. */
.product-generic .ph-bar {
  margin: 3px 0;
}

/* Two pulses on arrival, so the ring is noticed on a page that has just scrolled
   several thousand pixels. Animating the ring's own OFFSET rather than a
   coloured box-shadow is what keeps it right on all eleven pages: a shadow would
   have had to name the violet as a literal rgba(), and this card sits inside
   .company-page where companyStyle() overrides --accent. */
@keyframes product-found {
  from, to { outline-offset: 2px; }
  50%      { outline-offset: 9px; }
}

/* The one card the Search button pointed at — the opposite case to
   .nav-search-go above: this element IS inside the company wrapper, so the ring
   wears that page's ink. Verified on Galaxy: rgb(127,77,13).

   A ring rather than a tint or a scale, because the plate below is already
   tinted by therapeutic category and the mark inside it is the product's only
   picture — nothing may recolour or resize the card without changing what it
   says about the product. outline rather than border, because a border would
   add 2px to a card in a row of uniform-height cards and shift every neighbour.

   It doubles as the focus indicator: <App /> focuses this card as well as
   scrolling to it, and since the card is focusable for exactly as long as it is
   the found one, found and focused are one state — a separate :focus rule would
   draw the same ring twice. */
.product-card.is-found {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--shadow);
  animation: product-found 0.9s ease-out 2;
  /* Joins .products and .company-strip in this rule, or the card lands under
     the fixed navbar. A card is often taller than the window (613px against
     550px measured), so no centring can keep its top clear instead. */
  scroll-margin-top: calc(var(--nav-h) + 20px);
}

/* The ring is the answer and stays put; only the pulse is motion. */
@media (prefers-reduced-motion: reduce) {
  .product-card.is-found { animation: none; }
}

/* -- Brand plate (stands in for the product photo) ------------------------- */
/* The brand name is the product's only picture, so this block is treated like
   pack artwork: the therapeutic-class tint fills it, and the mark is set in
   that product's display face. min-height rather than aspect-ratio keeps it
   only as tall as the mark needs — an aspect box wasted ~60px per card. */
.product-plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 88px;
  padding: 15px 13px 11px;
  background: var(--tc-tint);
  border-bottom: 1px solid var(--tc-edge);
  text-align: center;
}

/* The mark itself. Every value here comes from the product's BRAND_MARKS entry
   via cardStyle(); --mk-scale normalises the very different x-heights of these
   faces so no card's mark reads as undersized.

   --bm-ink is the mark's OWN colour and the only thing on the card that carries
   it: five dosage-form and therapeutic-group colours set in brandInk(), falling
   back to the category ink for the listings that take none. The pill below the
   mark deliberately keeps --tc-ink, so the plate still says which therapeutic
   family the product is in even when the mark has been recoloured.

   Every one of the five clears AA against all fourteen CATEGORY_FAMILIES tints
   — computed, not eyeballed, worst case 5.00:1 (the pink on the psych tint
   #e5dff7, which is the darkest of the fourteen and sets the worst case for
   four of the five). Re-measure if either table changes; the mark is the
   largest text on the card and the last thing that should be hard to read. */
.product-brandmark {
  margin: 0;
  font-family: var(--mk-font);
  font-weight: var(--mk-weight);
  font-style: var(--mk-style);
  font-size: calc(1.32rem * var(--mk-scale));
  letter-spacing: var(--mk-tracking);
  text-transform: var(--mk-transform);
  line-height: 1.08;
  color: var(--bm-ink, var(--tc-ink));
  text-wrap: balance;
}

/* Therapeutic class, in the same ink as the mark. */
.product-class {
  padding: 2px 9px;
  border: 1px solid var(--tc-edge);
  border-radius: 999px;
  background: rgba(255,255,255,0.6);
  color: var(--tc-ink);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Discount chip, inline in the meta row beside the struck-through price. It
   used to be a corner badge on the plate, but that overlapped longer brand
   marks — and the mark is the product's photo, so nothing may cover it. */
.product-badge {
  padding: 2px 6px;
  border-radius: 6px;
  background: var(--danger);
  color: #fff;
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.2px;
  font-variant-numeric: tabular-nums;
}

/* "New" chip, for the sixteen Dawa lines the price list flags that way. Outlined
   in the accent rather than filled like the discount chip: a discount is a
   number you act on, this is only a label, and two solid chips side by side
   would compete. Accent, so it recolours per company page. */
.product-new {
  padding: 2px 7px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  color: var(--accent);
  background: var(--card);
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
}

/* -- Card body ------------------------------------------------------------- */

.product-info {
  display: flex;
  flex-direction: column;
  gap: 5px;
  flex: 1;
  padding: 11px 12px 12px;
}

.product-company {
  color: var(--accent-2);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.7px;
  text-transform: uppercase;
}

/* Generic (INN) name, strength and dosage form. Bounded so a long name can't
   stretch the card past its neighbours in the row. */
.product-generic {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-dim);
}

/* Pack size, plus assay and dissolution where a price list supplies them. A
   <dl> because these are label/value pairs; the grid keeps the values in one
   column so they scan down the card. tabular-nums stops the figures jittering
   between cards. */
.product-specs {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 1px 8px;
  margin: 2px 0 0;
  padding-top: 7px;
  border-top: 1px solid var(--border);
  font-size: 0.72rem;
}

.product-specs dt {
  color: var(--text-dim);
  font-weight: 600;
}

.product-specs dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

/* Pack sizes are codes — "10X1's(Mono Pack)", "Vial (Without Water)" — so they
   read better slightly emphasised and are worth not truncating. */
.product-pack { font-weight: 600; }

/* Stands in for a pack size the price list does not carry. Sized to the line it
   replaces so the row keeps its height, and vertically centred because a bar has
   no baseline of its own to sit on. */
.ph-pack {
  width: 58px;
  height: 9px;
  margin: 4px 0;
}

/* -- Editable release figures ---------------------------------------------- */
/* Assay and dissolution, typed per product. Same two-column rhythm as
   .product-specs above, so an editable row lines up with the read-only one. */
.release-fields {
  display: grid;
  gap: 3px;
}

.release-field {
  display: grid;
  grid-template-columns: max-content 1fr;
  align-items: center;
  gap: 8px;
  font-size: 0.72rem;
}

.release-field-label {
  color: var(--text-dim);
  font-weight: 600;
}

/* Flat until touched: twenty-four bordered inputs down a product grid read as a
   data-entry form rather than as a catalogue. The dotted underline is the only
   resting affordance — enough to say "editable", quiet enough to ignore. */
.release-field-input {
  width: 100%;
  min-width: 0;
  padding: 2px 4px;
  border: 1px solid transparent;
  border-bottom: 1px dashed var(--border);
  border-radius: 4px;
  background: none;
  color: var(--text);
  font: inherit;
  font-variant-numeric: tabular-nums;
}

.release-field-input:hover {
  border-bottom-color: var(--accent);
  background: var(--bg-soft);
}

/* A real focus ring, not just the hover treatment: this is the only way a
   keyboard user knows which of the two fields they are in.

   An outline rather than the box-shadow ring the navbar search uses, because
   this input sits inside .company-page, where companyStyle() overrides --accent
   per company. outline: var(--accent) recolours with it; the navbar's
   hardcoded rgba() would stay purple on all ten pages. */
.release-field-input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
  border-bottom-style: solid;
  background: var(--card);
}

/* Dimmer than the value, so an empty field reads as "nothing recorded" rather
   than as a figure. */
.release-field-input::placeholder {
  color: var(--text-dim);
  opacity: 0.62;
  font-style: italic;
}

/* The placeholder card's version — a span, so it has no input affordances at
   all and nothing inside the aria-hidden grid can take focus. */
.release-field-input.is-placeholder {
  border-bottom-color: var(--border);
  color: var(--text-dim);
}

/* Trade and wholesale, one above the other. Same two-column grid as the specs
   above so the two blocks share a label column and the figures line up, but
   right-aligned values: prices are compared down a column, not read across. */
.product-prices {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 2px 8px;
  margin: 0;
  padding-top: 7px;
  border-top: 1px solid var(--border);
  font-size: 0.74rem;
}

.product-prices dt {
  color: var(--text-dim);
  font-weight: 600;
}

.product-prices dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The figure the storefront leads with, set by EMPHASISED_PRICE in app.jsx —
   wholesale. NOT necessarily the figure the cart charges (CART_PRICE_BASIS, currently
   trade): the two are separate settings, so this must not be read as "the price
   you pay". Weight and size carry the emphasis, not colour alone. */
.product-prices dd.is-emphasis {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}

/* States the WHOLESALE_BREAK_QTY rule on the card. Rendered only where the two
   prices actually differ, so it never promises a discount a price list does not
   offer. Set in accent-2 rather than --text-dim: it is a term of sale, not a
   caption, and --text-dim at this size on the card ground is the faintest ink
   in the component. */
/* `.price-break-note` lived here — the card's "Wholesale from 20 packs" line.
   Removed with its markup on 2026-08-01; the rule went too rather than being
   left orphaned, which is what verify-clean.ps1's dead-CSS direction is for.
   WHOLESALE_BREAK_QTY still applies: see the comment on it in app.jsx. */

/* -- An incomplete listing -------------------------------------------------- */
/* Why a card cannot be ordered: it is missing one of REQUIRED_FIELDS. Sits
   directly above the stepper it explains.

   --danger, on the shop's instruction (2026-08-16). That reverses what this
   rule used to say, and the old reasoning is kept here because it still governs
   the WORDING: red was refused while the line read "Not orderable — no trade
   price or wholesale price", on the grounds that nothing was broken and nothing
   the customer did went wrong, so red would have reported a fault in the
   storefront that did not exist. The line now reads "Uploading Stocks..." — a
   live status the shop wants noticed, not a fault it wants excused — so red is
   doing a different job and the objection no longer applies. If the wording
   ever goes back to naming the missing fields, this colour should go back too.

   Measured, not eyeballed: --danger on the --bg-soft strip is 4.68:1, which
   clears AA for body text at this size (0.68rem is ~11px, so the 4.5:1
   threshold applies, not the large-text one). It does give up the 12.74:1 that
   --text had, which makes this the least legible line on the card rather than
   the most — worth knowing, since a purchasing officer still has to read it.
   The border takes the same red so the strip does not read half-changed.

   The dark hover ground needs a different colour entirely: plain --danger
   measures 1.04:1 there and would disappear. See the hover block above. */
.product-inactive-note {
  margin: 8px 0 0;
  padding: 6px 8px;
  border-left: 3px solid var(--danger);
  border-radius: 0 6px 6px 0;
  background: var(--bg-soft);
  color: var(--danger);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.3px;
}

/* Leaflet and product-image links. Two elements wear this class, and which one
   a card gets depends on whether a file exists: the Leaflet link is an <a> to
   leaflets.html#<id> for the 110 rows whose leaflet was supplied, and a <button>
   opening the ProductDocs modal for every other row and for Product images,
   which has no file behind it at all.

   The rules below are therefore written to sit on both. `padding: 0` and the
   three `none`s are what strip a button back to looking like text; an anchor
   needs none of them but is unharmed by them, and keeping one block rather than
   `a.doc-link` / `button.doc-link` is what stops the two drifting apart. */
.product-docs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
}

.doc-link {
  padding: 0;
  border: none;
  background: none;
  color: var(--accent-2);
  font-size: 0.74rem;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.doc-link:hover { color: var(--accent); }

/* -- Leaflet text ---------------------------------------------------------- */
/* The leaflet is TEXT in this page, never a file: there is no PDF linked,
   embedded or served anywhere in the app, so there is nothing to save and no
   viewer chrome offering to. What remains to be discouraged is printing, which
   the block at the end of this section handles. */

/* Sits above the body, so it frames what follows rather than footnoting it. */
.leaflet-disclaimer {
  margin: 0 0 14px;
  padding: 10px 13px;
  border: 1px solid var(--border);
  border-left: 4px solid var(--star);
  border-radius: 8px;
  background: var(--bg-soft);
  color: var(--text);
  font-size: 0.78rem;
  line-height: 1.5;
}

/* Between the disclaimer and the body, and saying a different thing from both.
   The disclaimer frames every leaflet on the site; this one appears only where
   the supplied document declares its own text a draft not cleared for release.
   So it takes the scaffolding treatment .otp-issued uses -- dashed --danger over
   a 5% tint -- for the reason argued there: the one unreliable thing on screen
   must not be the most reassuring.

   Body in --text, not --danger, following .product-inactive-note. Measured on
   the tint this sits on, red is 4.97:1 and the ink is 13.52:1, and a reader who
   has to be warned by a sentence has to be able to read the sentence. The
   border carries the signal, the words carry the meaning -- and the sentence
   opens with the word DRAFT in the document's own voice, so nothing here has to
   label it. */
.leaflet-draft {
  margin: 0 0 14px;
  padding: 10px 13px;
  border: 1px dashed var(--danger);
  border-radius: 8px;
  background: rgba(192,57,43,0.05);
  color: var(--text);
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1.5;
}

/* The scroll lives here rather than on the modal, so the tabs and the product's
   own heading stay put while a long leaflet moves under them. */
.leaflet-body {
  max-height: 52vh;
  overflow-y: auto;
  padding-right: 8px;
}

.leaflet-section + .leaflet-section { margin-top: 15px; }

.leaflet-section h3 {
  margin: 0 0 4px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--accent-2);
}

.leaflet-section p {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.6;
  color: var(--text);
}

/* Printing. The key handler in <ProductDocs /> catches Ctrl/Cmd+P; it cannot
   see the browser's own File > Print, so the leaflet is removed from the
   printed page here as well and says why. Both are deterrents rather than
   enforcement -- the pack insert is the document to act on, and a printout that
   looked like one would be the thing worth avoiding. */
@media print {
  /* .leaflet-draft goes with the body it describes. Left standing it would warn
     a printed page about wording that is no longer on it, and the disclaimer's
     ::after below already says why the page is empty. */
  .leaflet-body,
  .leaflet-draft,
  .leaflet-sections { display: none !important; }

  .leaflet-disclaimer::after {
    display: block;
    margin-top: 8px;
    font-weight: 700;
    content: "This leaflet is not available in printed form. Refer to the direction slip inside the medicine pack.";
  }
}

/* Rating and any discount on one line. margin-top:auto pins this and the
   stepper below it to the bottom, so cards with longer generic names still line
   up along the row. The row is not rendered at all when a product has neither a
   rating nor a discount, so the stepper inherits the pin in that case — hence
   margin-top:auto on .qty-stepper too. */
.product-meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 3px 8px;
  margin-top: auto;
  padding-top: 5px;
}

.product-rating {
  display: flex;
  align-items: center;
  gap: 3px;
  font-size: 0.75rem;
  color: var(--text-dim);
}

.product-star { color: var(--star); }

/* Pushed right so the struck price sits opposite the rating. */
.product-compare {
  margin-left: auto;
  color: var(--text-dim);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}

/* -- Quantity stepper ------------------------------------------------------ */
/* Replaces the old single Add button, so − and + are on every card rather than
   appearing only once a line exists. Three columns with the count in the middle
   on a 1fr, which keeps the two buttons the same size whether the count is 1 or
   12 — a stepper whose buttons move as you click it is hard to click twice.
   The 1fr is also what gives the count field room to be typed into: it holds
   four digits and the line total under them without the buttons moving. */
.qty-stepper {
  display: grid;
  grid-template-columns: 38px 1fr 38px;
  align-items: stretch;
  margin-top: auto;
  /* Room for the line total whether or not one is showing. Without it the
     stepper grows ~15px the moment a card gets a quantity, which pushes
     everything below the grid down and leaves the steppers in a row sitting at
     different heights from each other. */
  min-height: 50px;
  border: 1px solid var(--border);
  /* Tighter than --radius (14px), which is the card's own corner — a control
     inside it wants a smaller one or the two radii fight. */
  border-radius: 9px;
  background: var(--card);
  overflow: hidden;
}

.qty-btn {
  border: none;
  background: var(--bg-soft);
  color: var(--text);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.qty-btn:hover:not(:disabled) {
  background: var(--accent);
  color: #fff;
}

/* Dimmed rather than hidden: the control's shape should not change between a
   card with nothing in the cart and one with something, or the grid twitches. */
.qty-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* The + is the primary action of the card, so it carries the accent from rest
   rather than only on hover. --accent is overridden per company page, so this
   recolours with the rest of the landing page. */
.qty-add {
  background: var(--accent);
  color: #fff;
}

.qty-add:hover:not(:disabled) { filter: brightness(1.08); }

/* The middle column: the count field, and under it the line total. Still sets
   the type, since .qty-input inherits its font rather than restating it.

   4px of vertical padding, down from 7px, and the 3px buys the input its
   height. An <input> box is taller than the bare text node it replaced — 21.3px
   against ~18px, measured — and at the old padding the stack of field + gap +
   line total came to 51.7px, breaking through the 50px min-height that exists
   precisely so a card does not grow when it gets a quantity. Field and total
   now measure 48.4px together, so min-height governs both states again and a
   row of cards stays level whatever is in the cart. */
.qty-value {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 4px;
  font-size: 0.92rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The count is typed, not just clicked to: twenty packs is four clicks past the
   wholesale break and two hundred is not reachable by clicking at all.

   Flat until touched, exactly like the assay and dissolution inputs above it —
   a bordered box here would read as a form control sitting inside a control
   that is already a bordered box, and twelve of those down a grid turn a
   catalogue into a data-entry screen. The dashed underline is the whole resting
   affordance, and it is the same one those two fields use, so "dashed rule
   means you can type here" holds everywhere on the card.

   Capped rather than filling the 1fr column: the column is 167px wide on a
   desktop card and "9999" measures 34px, so a full-width rule would run a
   dashed line five times the width of the figure above it and read as a divider
   between the count and its line total rather than as a field. 74px is a little
   over twice the four-digit width — clearly a box, still an easy target. */
.qty-input {
  width: 100%;
  min-width: 0;
  max-width: 74px;
  padding: 0 2px;
  border: 1px solid transparent;
  border-bottom: 1px dashed var(--border);
  border-radius: 4px;
  background: none;
  color: var(--text);
  font: inherit;
  /* Explicit, because an input's default `normal` resolves through the font's
     own metrics to 20px here — 2px more than the text node it replaced, which
     is most of what pushed the stepper past its min-height. */
  line-height: 1.5;
  text-align: center;
}

.qty-input:hover:not(:disabled) {
  border-bottom-color: var(--accent);
  background: var(--bg-soft);
}

/* Same outline treatment as .release-field-input, and for the same reason: it
   is drawn in --accent, which companyStyle() overrides per landing page, so the
   ring recolours with the page instead of staying purple on all ten. */
.qty-input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
  border-bottom-style: solid;
  background: var(--card);
}

/* Inert on a placeholder card and on a product with no trade price. Stripped
   back to plain text: the dashed rule is an invitation to type and there is
   nothing to type into, and the count must look exactly as it did when it was
   a span or the placeholder grid stops matching the real one.
   -webkit-text-fill-color as well as color because Blink and WebKit grey
   disabled input text through that property and ignore `color` alone. */
.qty-input:disabled {
  border-bottom-color: transparent;
  color: var(--text);
  -webkit-text-fill-color: var(--text);
  opacity: 1;
}

/* Line total, shown only once there is a quantity. Small and dim so it reads as
   a consequence of the count above it rather than as a third price. */
.qty-line-total {
  font-size: 0.66rem;
  font-weight: 600;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.empty-state {
  text-align: center;
  padding: 52px 20px;
  background: var(--card);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}

.empty-state h2 {
  margin: 0 0 10px;
  font-size: 1.2rem;
}

/* Capped and re-centred: the "catalogue pending" copy is a paragraph, not a
   one-liner, and a full-container measure is unreadable at desktop widths. */
.empty-state p {
  margin: 0 auto 16px;
  max-width: 62ch;
  color: var(--text-dim);
}

/* -- Narrow-card breaks ---------------------------------------------------- */
/* Keyed to the card, not the viewport (see container-type on .product-card).
   235px is measured, not picked: "Dissolution" is 59px and the longest value,
   "86% at 45 min (buffer stage)", is 150px, so a side-by-side row needs ~217px
   of content — about a 241px card. Below that the value column squeezes and
   that string breaks mid-parenthetical.

   Stacking gives the value the card's full width, where it fits on one line, so
   the spec block costs the same two lines it did before rather than three. The
   trims below claw back the extra line the short values now take. */
@container (max-width: 235px) {
  .product-specs {
    grid-template-columns: 1fr;
    gap: 0 0;
  }

  .product-specs dd {
    margin-bottom: 3px;
  }

  .product-plate {
    min-height: 76px;
    gap: 5px;
    padding: 12px 9px 9px;
  }

  .product-brandmark { font-size: calc(1.16rem * var(--mk-scale)); }

  .product-info { gap: 4px; padding: 10px 10px 11px; }

  .product-generic { font-size: 0.75rem; }

  /* Let the meta line flow from the left once wrapped — pinning the struck
     price right leaves it stranded on its own row. */
  .product-compare { margin-left: 0; }

  /* The price rows stay side-by-side even here: "Wholesale" plus "KSh 1,865" is
     about 120px, well inside a 235px card's content width. Only the emphasised
     figure comes down, to match the smaller type around it. */
  .product-prices { font-size: 0.71rem; }
  .product-prices dd.is-emphasis { font-size: 0.94rem; }

  /* Narrower buttons, so the count and its line total keep room between them. */
  .qty-stepper { grid-template-columns: 32px 1fr 32px; }

  /* "Dissolution" is 59px of the label column; below this width the input left
     over is too narrow to read a value like "94% at 30 min" in, so stack the
     field the same way .product-specs stacks. */
  .release-field {
    grid-template-columns: 1fr;
    gap: 0;
  }
}

/* Very narrow cards only occur if a future breakpoint packs them tighter; the
   class pill is the first thing to overflow, so let it wrap rather than push
   the plate wider than its column. */
@container (max-width: 175px) {
  .product-class { white-space: normal; }
  .product-brandmark { font-size: calc(1.05rem * var(--mk-scale)); }
}

/* ==== END: Products — grid & card ========================================== */


/* ==== BEGIN: Product card placeholders ===================================== */
/* Empty cards for a landing page whose catalogue has not been uploaded. They
   reuse every .product-* class above, so the geometry and both @container
   breakpoints come for free — only the value slots differ. */

.placeholder-head { margin: 0 0 20px; }

.placeholder-head h2 {
  margin: 0 0 6px;
  font-size: 1.15rem;
}

.placeholder-head p {
  margin: 0;
  max-width: 68ch;
  color: var(--text-dim);
  font-size: 0.86rem;
  line-height: 1.55;
}

/* Dashed, matching the other placeholders in this app (empty-state panels, the
   docs-modal image tiles). The plate borrows the company's own tint via --co-*,
   so a placeholder still reads as that company's page rather than as grey
   chrome. The fallbacks only matter if one is ever rendered outside a
   .company-page, where --co-* is not defined. */
.product-card.is-placeholder {
  --tc-tint: var(--co-tint, var(--bg-soft));
  --tc-edge: var(--co-edge, var(--border));
  --tc-ink: var(--co-ink, var(--text-dim));
  border-style: dashed;
}

/* No lift on hover — there is nothing to open. */
.product-card.is-placeholder:hover {
  box-shadow: none;
  transform: none;
}

.doc-link.is-placeholder {
  color: var(--text-dim);
  text-decoration-style: dashed;
  cursor: default;
}

.ph-bar {
  display: block;
  border-radius: 5px;
  background: var(--bg-soft);
}

/* A --bg-soft bar vanishes against the tinted plate; translucent white reads
   there the same way the category pill does. */
.product-plate .ph-bar {
  background: rgba(255,255,255,0.6);
  border: 1px solid var(--tc-edge);
}

/* Heights match the type they stand in for: the mark is 1.32rem at line-height
   1.08, the generic 0.78rem at 1.35. The pack-size, price and quantity rows are
   not bars — they render their real labels with an em dash, so they need no
   stand-in. */
.ph-company       { width: 52%;   height: 8px; }
.ph-mark          { width: 74%;   height: 22px; }
.ph-pill          { width: 86px;  height: 17px; border-radius: 999px; }
.ph-generic       { width: 100%;  height: 10px; }
.ph-generic.short { width: 58%; }

/* The same measured 235px card break the real cards use, applied to the bars. */
@container (max-width: 235px) {
  .ph-mark  { height: 19px; }
  .ph-pill  { width: 74px; }
}

/* ==== END: Product card placeholders ======================================= */


/* ==== BEGIN: Modal ========================================================= */
/* Hosts both the cart and the auth card. z-index clears the fixed navbar. */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  overflow-y: auto;
  background: rgba(28,46,32,0.45);
  backdrop-filter: blur(2px);
}

.modal-card {
  position: relative;
  width: 100%;
  max-width: 440px;
  margin: auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 24px 60px rgba(28,46,32,0.28);
}

.modal-close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 1;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-soft);
  color: var(--text);
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-close:hover { border-color: var(--accent); }

/* Modal receives focus on open (see the Modal component) purely so the dialog
   is reachable and its label announced — a ring around the whole card would
   just be noise, and every control inside keeps its own focus style. */
.modal-card:focus { outline: none; }

/* The auth card supplies its own surface on the page; inside the modal the
   modal-card is already that surface, so flatten it. The extra top padding
   drops the heading clear of the absolutely-positioned close button. */
.modal-card .auth-card {
  background: none;
  border: none;
  box-shadow: none;
  padding-top: 46px;
}

/* ==== END: Modal =========================================================== */


/* ==== BEGIN: Cart panel ==================================================== */

.cart-panel { padding: 24px 22px 26px; }

.cart-panel h2 {
  margin: 0 0 18px;
  font-size: 1.2rem;
}

.cart-empty {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.9rem;
}

.cart-lines {
  list-style: none;
  margin: 0 0 18px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.cart-line {
  display: grid;
  grid-template-columns: 44px 1fr auto;
  gap: 12px;
  align-items: start;
}

/* Carries the product's therapeutic-class tint, so a cart of mixed classes is
   readable at a glance the same way the grid is. */
.cart-line-thumb {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  background: var(--tc-tint);
  border: 1px solid var(--tc-edge);
}

.cart-line-company {
  color: var(--accent-2);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
}

.cart-line-name {
  font-size: 0.88rem;
  line-height: 1.35;
  margin-top: 2px;
}

/* The line's money column: the total, and under it the basis note that only
   appears once the wholesale break applies. Right-aligned so the totals form a
   column the eye can add up, which a left-aligned note underneath would break. */
.cart-line-money {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  min-width: 0;
}

.cart-line-price {
  font-size: 0.9rem;
  font-weight: 700;
  white-space: nowrap;
}

/* Deliberately NOT white-space: nowrap. `.cart-line` is a
   `44px 1fr auto` grid, so this note's min-content width sets the third
   column — and unwrapped, "wholesale · KSh 210 ea" measures 128px, which at a
   320px viewport pushed the line to 334px and out of a 296px modal. Allowed to
   wrap, its min-content is one word and the column collapses to fit. */
.cart-line-basis {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--accent-2);
  text-align: right;
  text-wrap: balance;
}

.cart-qty {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}

.cart-qty button {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.cart-qty button:hover { border-color: var(--accent); }

/* Wrapper for the count field, matching the card skin's .qty-value so both
   variants render the same tree. It carries no line total — the cart line has
   its own money column two elements away. */
.cart-qty-value {
  display: flex;
  align-items: center;
}

/* A real bordered box here, where the card's is flat. The difference is
   deliberate: a cart line is a place you go to change quantities, so the field
   should look like a field and match the two 26px buttons it sits between,
   whereas twelve bordered boxes down a product grid would read as a form. */
.cart-qty-input {
  width: 46px;
  height: 26px;
  padding: 0 4px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 0.88rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.cart-qty-input:hover { border-color: var(--accent); }

.cart-qty-input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}

.cart-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  font-size: 0.95rem;
}

.cart-total strong { font-size: 1.15rem; }

.cart-shipping {
  margin: 6px 0 16px;
  color: var(--text-dim);
  font-size: 0.8rem;
}

/* Why Checkout is disabled, stated in the layout rather than a tooltip. */
.cart-note {
  margin: 10px 0 0;
  color: var(--text-dim);
  font-size: 0.75rem;
  text-align: center;
}

/* Cart lines carry the generic name under the brand — two brands can be the
   same medicine, so the brand alone is ambiguous. */
.cart-line-generic {
  margin-top: 1px;
  color: var(--text-dim);
  font-size: 0.76rem;
  line-height: 1.35;
}

/* And the pack size under that, because brand plus generic is still ambiguous:
   nine of Square's listings are the same brand and strength at a different pack
   size and a different price. */
.cart-line-pack {
  margin-top: 2px;
  color: var(--text-dim);
  font-size: 0.72rem;
  font-weight: 600;
}

/* ==== END: Cart panel ====================================================== */


/* ==== BEGIN: Product docs (leaflet / images) =============================== */
/* The modal opened by either link on a product card. Two views behind one set
   of tabs, sharing the modal plumbing. */

.docs-panel { padding: 24px 22px 26px; }

/* Tinted by therapeutic class like the card's plate, so the modal reads as the
   same product. Right padding clears the modal close button. */
.docs-head {
  margin: -24px -22px 18px;
  padding: 20px 56px 16px 22px;
  background: var(--tc-tint);
  border-bottom: 1px solid var(--tc-edge);
}

/* Inherits the mark's face from .product-brandmark; only the size differs. */
.docs-head .product-brandmark {
  margin: 3px 0 5px;
  font-size: calc(1.5rem * var(--mk-scale));
  text-align: left;
}

.docs-head .docs-generic { margin-bottom: 9px; }

.docs-generic {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.85rem;
  line-height: 1.45;
}

.docs-tabs {
  display: flex;
  gap: 4px;
  padding: 6px;
  background: var(--bg-soft);
  border-radius: 10px;
  margin-bottom: 18px;
}

.docs-tab {
  flex: 1;
  padding: 8px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--text-dim);
  font-weight: 600;
  font-size: 0.85rem;
  transition: all 0.15s ease;
}

.docs-tab.active {
  background: var(--card);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(28,46,32,0.14);
}

/* -- Leaflet view ---------------------------------------------------------- */

/* Each result sits above the specification it is judged against. */
.spec-table {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 10px 14px;
  margin: 0 0 18px;
  font-size: 0.88rem;
}

.spec-table dt {
  color: var(--text-dim);
  font-weight: 600;
}

.spec-table dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.spec-table dd span {
  display: block;
  margin-top: 2px;
  color: var(--text-dim);
  font-size: 0.75rem;
  font-variant-numeric: normal;
}

.leaflet-sections {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  border-top: 1px solid var(--border);
}

.leaflet-sections li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}

.leaflet-sections span {
  flex-shrink: 0;
  color: var(--text-dim);
  font-size: 0.74rem;
}

/* -- Images view ----------------------------------------------------------- */

/* Two columns, because a listing carries exactly two photographs — primary
   pack and secondary pack, PRODUCT_IMAGE_SLOTS. It was three, for three
   loosely-named placeholder tiles; two real slots at half the modal's width
   are also meaningfully bigger than three were. */
.docs-images {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-bottom: 16px;
}

.docs-image { margin: 0; }

/* Sizing only. The surface, the dashed empty state and the fit rule all live
   on .pack-image, which this composes with — one photograph box, styled once,
   whether it is here or on a card. */
.docs-image-box { width: 100%; }

.docs-image figcaption {
  margin-top: 6px;
  color: var(--text-dim);
  font-size: 0.7rem;
  line-height: 1.3;
  text-align: center;
}

/* What belongs in an empty slot, under the slot's name. Block, so the name
   stays on its own line and reads as the label rather than as the start of a
   sentence the hint finishes. */
.docs-image-hint {
  display: block;
  margin-top: 3px;
  font-size: 0.65rem;
  opacity: 0.85;
}

/* -- Shared footnote ------------------------------------------------------- */

.docs-note {
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 0.75rem;
  line-height: 1.55;
}

/* ==== END: Product docs (leaflet / images) ================================= */


/* ==== BEGIN: Pack photography ============================================== */
/* One box for every pack shot in the interface — the card's plate and the two
   tiles in the docs modal — and the layer a click on one opens. Styled once
   here rather than per caller, so the fit rule below is stated once. */

/* The box. Square by aspect-ratio rather than by a fixed height, so it keeps
   its shape at every width the card's container queries produce, and it agrees
   with the square bound the uploader resizes to (PRODUCT_IMAGE_BOX).

   It is a <button> when the photograph enlarges and a <div> when there is
   nothing to enlarge, so the button reset is part of the base rule — a browser
   default border and centred text would otherwise show through on half the
   instances and on neither of the others. */
.pack-image {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  margin: 0;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--card);
  font: inherit;
  color: inherit;
  text-align: inherit;
  -webkit-appearance: none;
  appearance: none;
}

/* CONTAIN, NEVER COVER — the whole of "critical attributes must not be
   trimmed", in one keyword.

   `cover` would fill the box edge to edge by scaling the photograph up and
   cutting off whatever overflows. On a pack shot that is the edges, and the
   edges are where the strength, the batch number, the expiry and the
   manufacturer's name are printed: a centre crop of a carton keeps the brand
   and loses the dose. `contain` fits the whole photograph and leaves a strip
   of the box empty beside it.

   The empty strip is the cost and it is worth paying. Blank space next to a
   portrait carton reads as blank space; a crop reads as a complete photograph
   of an incomplete pack, and nothing on screen says a figure was cut off. The
   uploader resizes by the same rule — see PRODUCT_IMAGE_FIT. */
.pack-image-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Nothing uploaded to this slot. Dashed, which is this app's placeholder
   language everywhere else — the empty-state panels and the catalogue-less
   card grid all use it — so an unphotographed slot reads as pending rather
   than as broken. */
.pack-image.is-empty {
  border-style: dashed;
  border-color: rgba(28,46,32,0.18);
  background: var(--bg-soft);
}

.pack-image.is-zoomable { cursor: zoom-in; }

.pack-image.is-zoomable:hover { border-color: var(--tc-edge, var(--border)); }

.pack-image.is-zoomable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The corner glyph saying the picture opens. Small, and in the corner rather
   than over the middle, because the middle of a pack shot is the label. It is
   aria-hidden — the button's own label says "Enlarge …", so a screen reader is
   told this in words and does not need the arrow read to it as well. */
.pack-image-cue {
  position: absolute;
  right: 5px;
  bottom: 5px;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  background: rgba(28,46,32,0.62);
  color: #fff;
  font-size: 0.66rem;
  line-height: 1;
}

/* The primary pack on a card's plate, above the brand mark. Capped rather than
   full-width: the plate is a centred column and a photograph stretched to the
   full width of a 300px card would tower over the mark under it and turn the
   plate into a picture with a caption. */
.product-shot {
  width: 100%;
  max-width: 118px;
  margin-bottom: 2px;
}

/* -- The enlarged view ----------------------------------------------------- */

/* Above .modal-overlay's 200, since a zoom opened from the images view is a
   layer over that modal rather than a replacement for it. Fixed and inset: 0.
   Opened from inside the modal it is contained by .modal-overlay instead of by
   the viewport — `backdrop-filter` there makes that element a containing block
   for fixed descendants — but that overlay is itself fixed and inset: 0, so
   the two rectangles are the same one and the layer lands correctly either
   way. Verified in the browser at three viewport widths, not assumed. */
.zoom-layer {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  background: rgba(18,30,20,0.84);
  cursor: zoom-out;
}

/* Focus is moved here on open so the layer sees Escape before the modal
   underneath does; the ring would be a rectangle around the whole screen. */
.zoom-layer:focus { outline: none; }

.zoom-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 100%;
  margin: 0;
}

/* Capped at PRODUCT_IMAGE_BOX's 1000px as well as at the viewport, so the
   photograph is never displayed larger than it is stored and never upscaled
   into softness at the one moment somebody is looking closely. object-fit
   repeats contain for the case where both caps bind at once. */
.zoom-img {
  display: block;
  max-width: min(92vw, 1000px);
  max-height: 82vh;
  object-fit: contain;
  border-radius: 10px;
  background: var(--card);
  box-shadow: 0 30px 70px rgba(0,0,0,0.45);
}

.zoom-caption {
  color: rgba(255,255,255,0.92);
  font-size: 0.82rem;
  text-align: center;
}

/* Top right, clear of the picture. Every click on this layer closes it, so
   this button carries no handler of its own — it is the affordance for a
   behaviour the whole backdrop already has, and the one thing a customer who
   does not know that will look for. */
.zoom-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 34px;
  height: 34px;
  border: 1px solid rgba(255,255,255,0.28);
  border-radius: 50%;
  background: rgba(255,255,255,0.12);
  color: #fff;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.zoom-close:hover { background: rgba(255,255,255,0.22); }

.zoom-close:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* ==== END: Pack photography ================================================ */


/* ==== BEGIN: Auth card ===================================================== */
/* Rendered inside the modal, opened from the navbar's "Sign in" button. */

.auth-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 26px 24px 24px;
  min-width: 0;
  width: 100%;
}

.auth-head { margin-bottom: 20px; }

.auth-head h2 {
  margin: 0 0 6px;
  font-size: 1.3rem;
}

.auth-head p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.88rem;
  line-height: 1.5;
}

.auth-form { margin: 0; }

/* "New to Savannah Pharmaceuticals? Create an account" — the button inside is a .link-btn. */
.auth-switch {
  margin: 18px 0 0;
  text-align: center;
  font-size: 0.88rem;
  color: var(--text-dim);
}

/* Shown after a form validates. --accent-2 at low alpha rather than the old
   dark-theme teal (#00d4b5), which this light theme dropped for failing
   contrast on cream. */
.auth-success {
  padding: 12px 4px 8px;
  text-align: center;
}

.auth-success-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  margin: 0 auto 14px;
  border-radius: 50%;
  background: rgba(10,122,104,0.12);
  border: 1px solid rgba(10,122,104,0.38);
  color: var(--accent-2);
  font-size: 1.3rem;
  font-weight: 700;
}

.auth-success h2 {
  margin: 0 0 8px;
  font-size: 1.15rem;
}

.auth-success p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.88rem;
}

/* -- One-time code step ---------------------------------------------------- */
/* The screen between the form and the session. It reuses .auth-card, .auth-head,
   .auth-form and .auth-switch wholesale — it IS the auth card, on its second
   page — and adds only what is specific to a code: the panel that prints one, and
   a wider tracked field to type it into.

   Scoped under .otp-step rather than given the input its own class, because
   <Field /> takes no className and should not grow one for a single caller. */

/* The code is six digits typed as a group, not prose: tracked, centred and
   larger, which is also what makes a transposed digit visible while typing. */
.otp-step .field input {
  text-align: center;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0.4em;
  /* Half the tracking again on the right, or the trailing letter-space pushes the
     group visibly left of centre. */
  padding-right: calc(13px + 0.4em);
}

/* Deliberately NOT a soft hint colour. This panel stands in for an SMS that was
   never sent, and dressing it as a friendly tip would make the one dishonest
   thing on the screen the most reassuring. --danger at low alpha reads as
   "this is scaffolding", which is exactly what it is. */
.otp-issued {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 0 0 18px;
  padding: 12px 14px;
  border: 1px dashed var(--danger);
  border-radius: 10px;
  background: rgba(192,57,43,0.05);
  text-align: center;
}

.otp-issued-label {
  color: var(--danger);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Tabular so the digits sit on an even pitch — a proportional 1 next to a 0 makes
   a six-digit group look mistyped. */
.otp-code {
  color: var(--text);
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: 0.22em;
  font-variant-numeric: tabular-nums;
}

/* Sits in .auth-switch's slot where the resend button will be, so the row does
   not change height when the countdown gives way to the button. */
.otp-wait { font-variant-numeric: tabular-nums; }

/* Two stacked text buttons in one .auth-switch, which is centred and sized for
   one line of prose. */
.otp-step .auth-switch .link-btn { margin-top: 4px; }

/* ==== END: Auth card ======================================================= */


/* ==== BEGIN: Form fields =================================================== */
/* Drives the Field component plus the checkbox rows in both auth forms. */

.field {
  margin-bottom: 16px;
}

/* .field IS the <label> — the caption is a <span> inside it, so this cannot be
   `.field label`. */
.field-label {
  display: block;
  font-size: 0.82rem;
  color: var(--text-dim);
  margin-bottom: 6px;
  font-weight: 600;
}

/* The composer's message box is the same control from the customer's side as
   an auth input, so it takes the same skin from one rule rather than a copy
   that can drift. Only what is genuinely textarea-only is split out below.
   The county <select> joins them for the same reason — one skin, three tags. */
.field input,
.field textarea,
.field select {
  width: 100%;
  padding: 11px 13px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--bg-soft);
  color: var(--text);
  font-family: inherit;
  font-size: 0.94rem;
  outline: none;
  transition: border-color 0.15s ease;
}

/* Vertical only: a horizontal drag would push the textarea past the modal. */
.field textarea {
  display: block;
  min-height: 118px;
  line-height: 1.5;
  resize: vertical;
}

.field input:focus,
.field textarea:focus,
.field select:focus { border-color: var(--accent); }

/* Keyed to the attribute the Field component actually sets. It used to be
   `.field input.invalid`, a class nothing ever added, so a field that failed
   validation showed its message but kept a neutral border. */
.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"],
.field select[aria-invalid="true"] { border-color: var(--danger); }

/* -- Select field ---------------------------------------------------------- */
/* The county picker. The native arrow is suppressed and redrawn as a
   pseudo-element, the same trick .select-wrap plays in the products toolbar —
   but that class cannot be reused here: it is an inline-flex row sized for a
   toolbar caption, and this one is a block control filling a form. */

.field select {
  appearance: none;
  -webkit-appearance: none;
  /* Right padding clears the chevron; without it a long county name runs
     underneath it. */
  padding-right: 34px;
  cursor: pointer;
}

/* Wraps the <select> ALONE. The chevron cannot be anchored to .field, which is
   the whole label and contains the caption too — centring against that box puts
   the arrow above the control. */
.field-select {
  position: relative;
  display: block;
}

.field-select::after {
  content: "▾";
  position: absolute;
  top: 50%;
  right: 13px;
  transform: translateY(-50%);
  font-size: 0.7rem;
  color: var(--text-dim);
  pointer-events: none;
}

/* -- Field pair ------------------------------------------------------------ */
/* Two fields on one row, for two halves of a single answer — county and city.

   The 170px BASIS is what makes this need no media query of its own: two of them
   plus the gap ask for 352px, which the 440px modal grants side by side and a
   375px phone (314px of form) cannot, so the pair stacks itself exactly where two
   controls stop fitting. A breakpoint would have to guess that width.

   Why 170 and not 150, which also fitted the desktop: at 150 the pair squeezed
   onto one row at 375px too, 151px each, and 151px leaves 104px inside the
   select's padding — measured against "Elgeyo-Marakwet" at ~118px, so the
   longest county names clipped. The basis is set by the WIDEST OPTION, not by
   what merely fits.

   `min-width: 0` because a flex item's default min-width is auto — without it
   the longest county name sets a floor under the <select> and the pair pushes the
   modal past its own max-width instead of wrapping. */
/* row-gap 0, column-gap 12px — and no bottom margin. Measured, not guessed: two
   consecutive .field labels have ZERO between one input's bottom edge and the
   next caption's box, the visible breathing room being the caption's own leading.
   These two are ordinary fields that happen to share a row, so stacked they
   should land on that same rhythm rather than 12px looser than everything around
   them. Only the horizontal gap is real spacing. */
.field-pair {
  display: flex;
  flex-wrap: wrap;
  gap: 0 12px;
}

/* Blockified — a flex item cannot stay inline — and that is what finally brings
   .field's dormant `margin-bottom: 16px` to life, on these two labels only. It
   has to be zeroed here: stacked on a phone it put 28px between county and city
   in a form that puts nothing between any other two fields, and side by side it
   hung 16px of dead space under the row. */
.field-pair .field {
  flex: 1 1 170px;
  min-width: 0;
  margin-bottom: 0;
}

.field-error {
  display: block;
  color: var(--danger);
  font-size: 0.78rem;
  margin-top: 5px;
}

/* -- Field hint ------------------------------------------------------------ */
/* A line under a control that is NOT an error: the problem statement's word
   counter, and the attachment's format-and-size note. Same size as .field-error
   so the two occupy one rhythm, but dim rather than red — it carries no failure.

   `.is-over` is the counter past its limit. It borrows --danger for the colour and
   nothing else: no icon, no weight change, because at that moment nothing is
   broken yet. The refusal comes on submit, from .field-error, and if this looked
   the same as that there would be no way to tell "getting long" from "too long". */
.field-hint {
  display: block;
  color: var(--text-dim);
  font-size: 0.78rem;
  margin-top: 5px;
}

.field-hint.is-over { color: var(--danger); }

/* Right-aligned so it reads as a meter against the box's edge rather than as a
   second caption. Only where a counter is the hint — the attachment's note is a
   sentence and stays left, and it follows an <input>, not a <textarea>.

   ONE general-sibling selector, not the adjacent-sibling pair this started as
   (`textarea + .field-hint` plus `textarea ~ .field-error + .field-hint`): the
   error span appears between the two only when validation has failed, so the
   adjacent form needed a second rule for the failed state and would have needed a
   third for anything else that ever sits in between. `~` does not care what is
   between them. */
.field textarea ~ .field-hint {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* -- File field ------------------------------------------------------------ */
/* The one field that is a <div> rather than a <label> — see <FileField />. Which
   means it is a BLOCK, and .field's `margin-bottom: 16px` (dormant on every other
   field, because a <label> is inline and vertical margins do not apply to one)
   comes to life here and puts this row 16px below the rhythm. Zeroed, like
   .field-pair's children, for exactly the same reason. */
.file-field {
  margin-bottom: 0;
}

/* The input keeps the shared skin from `.field input` above; only what is
   file-specific is set here.

   5px of vertical padding, not the 11px a text input carries, and the figure is
   measurement rather than taste: this control's height is its BUTTON's height plus
   this padding, and the button measures 32.7px at the size set below. 1.33 border
   + 10 padding + 32.7 = 44px against a text input's 43.33px — the two rows read as
   one height. At 8px it stood 50px, 7px taller than every neighbour for no reason
   a reader could see.

   4.5px would land on 43.33px exactly, and is deliberately not used: sub-pixel
   padding rounds differently at other zoom levels, which trades a permanent 0.67px
   for an occasional 1px. Re-measure if the button's padding or font-size moves. */
.field input[type="file"] {
  padding: 5px 10px;
  height: auto;
  cursor: pointer;
  font-size: 0.85rem;
}

/* The browser's own button, restyled to match .btn rather than replaced by a
   label over a hidden input — which is the usual trick and loses the focus ring,
   the keyboard activation and the filename readout all at once. */
.field input[type="file"]::file-selector-button {
  margin-right: 12px;
  padding: 7px 13px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s ease;
}

.field input[type="file"]:hover::file-selector-button { border-color: var(--accent); }

/* Carries the "remember me" checkbox plus, on the login form, the
   "Forgot password?" link — hence space-between, with the label supplying its
   own flex so the box and its text stay together. */
.checkbox-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: -2px 0 18px;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.checkbox-row label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.checkbox-row input { accent-color: var(--accent); }

/* -- Choice group ---------------------------------------------------------- */
/* Drives the ChoiceField component — the sign-up form's "Which of these
   describes you?". Same vertical rhythm as .field above so the two sit in one
   stack without a seam. */

.choice-field {
  /* A <fieldset> ships a border, its own padding and `min-width: min-content`.
     All three have to go: the first two box the group off from the fields above
     and below it, and the third stops it shrinking inside the auth card, which
     would push the whole modal wider than the viewport on a phone. */
  min-width: 0;
  padding: 0;
  border: 0;

  /* 12px, matching the measured gap between two .field labels — NOT the 16px
     those labels declare. A <label> is inline, and vertical margins do not apply
     to a non-replaced inline box, so .field's own margin-bottom has never done
     anything: the 12px comes from the line boxes. A fieldset is a block, its
     margin does apply, and 16px here left this one group sitting 4px lower than
     every other row in the form. */
  margin: 0 0 12px;
}

/* The caption is a <legend> carrying .field-label, and a legend has inline
   padding of its own — left alone it sits 2px right of every other caption in
   the form. */
.choice-field legend { padding: 0; }

/* A GRID, not the wrapping flex row that .contact-tabs uses, and the difference
   is the fifth option. Five flex items with flex-grow leave whichever one lands
   alone on the last row stretched across the full width — with five answers that
   is "Other", so the least likely answer became the biggest target on screen and
   the pills all ended up different widths, which reads as a hierarchy the
   answers do not have.

   auto-fit + minmax gives equal columns instead: two of them wherever the modal
   is wide enough for two 140px pills, one on a very narrow phone, and the widest
   caption ("A pharmacist", ~139px) fits the 140px floor at every size. The last
   row simply ends early. */
.choice-group {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px;
}

/* The pill IS the <label>, so the whole of it is the hit target, not just the
   words. The radio inside stays visible rather than being hidden behind a styled
   pill: it is what says "one of these", and it is what the focus ring lands on.

   inline-flex for the radio-beside-caption alignment; as a grid item it is
   blockified to plain flex and fills its column, which is what makes the pills
   equal-width without a width declaration anywhere. */
.choice {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 13px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-soft);
  color: var(--text-dim);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s ease;
}

.choice:hover { border-color: var(--accent); }

.choice input {
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* Set from ChoiceField rather than by :has(input:checked), matching
   .contact-tab.active — React already holds which one is picked. */
.choice.checked {
  background: var(--card);
  border-color: var(--accent);
  color: var(--text);
}

/* Keyed to the attribute ChoiceField actually sets, the way the text inputs
   are. The group failed, so every pill in it carries the failed border — there
   is no one control to mark, which is the whole difference between validating a
   choice and validating a field. */
.choice-field[aria-invalid="true"] .choice { border-color: var(--danger); }

/* ==== END: Form fields ===================================================== */


/* ==== BEGIN: Features ====================================================== */
/* Marketplace selling points (shipping, vetting, returns). */

.features {
  padding: 0 0 80px;
}

.features h2 {
  text-align: center;
  font-size: 1.6rem;
  margin-bottom: 10px;
}

.features > p {
  text-align: center;
  color: var(--text-dim);
  margin-bottom: 40px;
}

.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 20px;
}

.feature-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px;
}

/* The tinted square behind a feature card's icon. Both colours come from that
   icon's own pair in ICON_TONES, written onto this element as --ic-tint and
   --ic-ink by iconToneStyle(): the square takes the tint, and `color` is what
   the icon strokes itself in, since the feature cards render <Icon plain />.
   The fallbacks keep it looking deliberate if an icon has no tone.

   font-size is the icon's own size, and runs above the 1.15rem the emoji used
   because a stroked icon reads smaller in the same em box. */
.feature-icon {
  width: 42px;
  height: 42px;
  border-radius: 10px;
  background: var(--ic-tint, var(--bg-soft));
  color: var(--ic-ink, var(--text));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  margin-bottom: 14px;
}

.feature-card h3 {
  margin: 0 0 8px;
  font-size: 1.02rem;
}

.feature-card p {
  color: var(--text-dim);
  font-size: 0.9rem;
  line-height: 1.6;
  margin: 0;
}

/* ==== END: Features ======================================================== */


/* ==== BEGIN: Features — collapsible (small screens) ======================== */
/* The .collapsible class is added by the Features component when
   useIsSmallScreen() is true; card padding moves onto the toggle and body. */

.feature-grid.collapsible .feature-card {
  padding: 0;
  overflow: hidden;
}

.feature-toggle {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 16px 20px;
  background: none;
  border: none;
  color: var(--text);
  text-align: left;
}

.feature-toggle .feature-icon {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  margin-bottom: 0;
  font-size: 1.2rem;
}

.feature-toggle h3 {
  flex: 1;
  margin: 0;
  font-size: 1rem;
}

.feature-chevron {
  flex-shrink: 0;
  color: var(--text-dim);
  font-size: 0.9rem;
  transition: transform 0.2s ease;
}

.feature-card.open .feature-chevron {
  transform: rotate(180deg);
}

.feature-grid.collapsible .feature-card p {
  padding: 0 20px 18px;
}

/* ==== END: Features — collapsible (small screens) ========================== */


/* ==== BEGIN: Company profile =============================================== */
/* Four <Disclosure /> rows — mission, vision, the About text, the six values —
   closed on arrival. The rows come styled from the Disclosure block above; what
   is here is the band around them and the prose inside them.

   A capped, centred column rather than the full 1160px container: this is the
   only band on the page that is pure prose, and a 1160px line of body text does
   not read. 820px is about 95 characters at this size — the outer bound for a
   measure, and it is the toggles that set the width, not the text, which is
   padded in to align under its own heading. */

.profile {
  padding: 0 0 80px;
}

/* Same size as .features h2, so the two closing bands read as siblings. */
.profile-head {
  margin: 0 0 24px;
  font-size: 1.6rem;
  text-align: center;
}

.profile-list {
  display: grid;
  gap: 10px;
  max-width: 820px;
  margin: 0 auto;
}

/* Aligns the opened text with the label on the toggle above it (which carries
   15px of its own padding). Scoped to this band: the rail and the category index
   want the near-flush 2px the shared rule gives them, because what they open is
   a row of chips, not a paragraph. */
.profile-list .disclosure-region {
  padding: 14px 15px 18px;
}

/* 0.92rem/1.75 rather than the 0.9/1.6 a feature card takes: these are 130-word
   statements read straight through, not captions. */
.profile-text p {
  margin: 0 0 13px;
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.75;
}

.profile-text p:last-child { margin-bottom: 0; }

/* The sign-off. Full ink above a rule — the one line of the block written to be
   remembered rather than checked. Two classes deep to beat `.profile-text p`. */
.profile-text .profile-closer {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  color: var(--text);
  font-weight: 600;
}

/* A <ul>, so the list resets here. Six is the whole set and it is read as one. */
.profile-values {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 15px;
}

.profile-values h3 {
  margin: 0 0 4px;
  font-size: 0.95rem;
}

.profile-values p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.88rem;
  line-height: 1.65;
}

/* ==== END: Company profile ================================================= */


/* ==== BEGIN: Footer ======================================================== */
/* Same sage as the navbar. Text is full --text, not --text-dim, which only
   reaches 1.9:1 on this band. No top border: the cream-to-sage edge is already
   a hard colour change.

   Four stacked, centred rows — blurb, actions, channels, copyright — which is
   what gives the band its height. Nothing here is a fixed height: the padding
   and the row gaps set it, so the band grows with its content instead of
   clipping it. */

.footer-wrap {
  background: var(--surface-footer);
  padding: 46px 0 26px;
  color: var(--text);
  font-size: 0.85rem;
  text-align: center;
}

.footer-inner {
  display: flex;
  flex-direction: column;
  gap: 26px;
}

/* Margin only. The heading's own font-size used to be 1.3rem and is deliberately
   NOT set here any more: the <h2> is now the lockup, whose two lines carry
   explicit rem sizes, so a font-size on the parent would style nothing and read
   as though it still did. .footer-brand below is where the size lives. */
.footer-lead h2 {
  margin: 0 0 8px;
}

/* The footer's copy of the navbar lockup — the NAME only. Everything structural
   — the flex row, the stacked column, the ink — comes from .brand and .brand-*
   above, so this block is only what the SIGN-OFF does differently: centred
   rather than flush left, and one size step up, because here the name IS the
   heading instead of sitting beside a search field.

   No monogram here: <Wordmark mark={false} /> in the footer, by request. That is
   why there is no `.footer-brand .brand-mark` override any more — it used to
   scale the plate to 44px and hold its 7.4:1 / 9.0:1, and with no plate to match
   it would be a rule selecting nothing. The navbar's 38px plate is untouched.

   No `gap` either: .brand sets 11px for the plate-to-name join, and with the
   plate gone this row has ONE child, so a gap here would separate nothing while
   reading as though it still did. Nothing here touches colour — this band is the
   same sage as the navbar's, so the 5.4:1 wordmark carries over. */
.footer-brand {
  justify-content: center;
}

.footer-brand .brand-line-a { font-size: 1.45rem; }
.footer-brand .brand-line-b { font-size: 0.66rem; }

.footer-lead p {
  max-width: 520px;
  margin: 0 auto;
  line-height: 1.6;
}

/* Under the wordmark, above the blurb — the lockup line. Two classes deep for
   the same reason as .hero .hero-slogan: `.footer-lead p` would otherwise set
   the margin and it would sit flush against the sentence below it. */
.footer-lead .footer-slogan {
  margin: 0 auto 8px;
  font-weight: 700;
  letter-spacing: 0.3px;
}

/* The row that holds Contacts, and the row that holds Back to top - one button
   each, in two places in the footer. Both are taller than a navbar button and
   given a minimum width, so neither ends up sized by the length of its label.
   (There was an About button here too until the company profile absorbed it.) */
.footer-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}

.footer-action {
  min-width: 156px;
  min-height: 52px;
  gap: 6px;
  font-size: 0.95rem;
}

/* Back to top, and only that one - Contacts above it shares `.footer-action`
   and stays the plain card button.

   GEOMETRY ONLY as of 2026-08-04. Every colour this rule used to set - a #0a63e0
   azure fill, a white label, a white 2px ring and a blue-tinted drop - now comes
   from `.btn-sunset`, which the button carries as `variant="sunset"`. They were
   not left here to be overridden: both selectors are one class deep, this block
   is ~250 lines later in the file, so source order would have handed every one
   of them back to the blue and the variant would have done nothing visible.
   Anything colour-shaped for this button belongs in .btn-sunset now.

   What the blue knew, kept because the sage band still demands it: no flat fill
   is separable from #6f9f6f on its own - the azure measured 1.77:1 and even a
   near-navy #123fbf only reached 2.76:1 - so a ring is mandatory here rather
   than decorative. .btn-sunset draws one in ink for exactly that reason.

   Wider and heavier than its sibling on purpose: the user asked for highly
   visible, and on a band this saturated size does as much work as hue. */
.footer-to-top {
  min-width: 190px;
  font-size: 1rem;
  letter-spacing: 0.2px;
}

/* The hover lift and the focus ring moved to `.btn-sunset` too, and for a reason
   worth keeping: the blue could only be brightened by hand, because every
   lighter blue tried dropped its WHITE label under AA (#1a73f0 is 4.42:1,
   #2b7ef7 is 3.87:1) - which is also why it could not use
   `filter: brightness()`, a filter that would have walked the label under the
   line silently. A dark label inverts that constraint: brightening the fill only
   raises its contrast, so the variant is free to brighten on hover. The one
   thing that stayed behind is the arrow below, which no other caller has. */

/* The arrow is the glyph that says which way, so it gets to be bigger than the
   label and to move with the lift. It carries no class - it is the button's
   aria-hidden span - so it is selected by that rather than by adding one. */
.footer-to-top span[aria-hidden="true"] {
  font-size: 1.15rem;
  line-height: 1;
  transition: transform 0.15s ease;
}

.footer-to-top:hover span[aria-hidden="true"] { transform: translateY(-2px); }

.footer-reach h3 {
  margin: 0 0 12px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
}

/* auto-fit rather than a media query: the five channels sit in one row on a
   desktop and fold to two, then one, on the way down to 320px. */
.footer-channels {
  list-style: none;
  max-width: 660px;
  margin: 0 auto;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: 10px;
}

/* Translucent white rather than --card: on sage it stays part of the band until
   hovered, when it lifts to the full card surface. The alpha went from 0.16 to
   0.55 when the glyphs became coloured marks — a green or a plum ink needs a
   pale ground under it, and at 0.16 the sage was still showing through enough
   to drag every one of them down. */
.footer-channel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: 100%;
  height: 100%;
  padding: 12px 10px;
  border-radius: 12px;
  border: 1px solid rgba(255,255,255,0.6);
  background: rgba(255,255,255,0.55);
  color: var(--text);
  font-family: inherit;
  transition: all 0.15s ease;
}

.footer-channel:hover {
  background: var(--card);
  border-color: var(--accent);
}

.footer-channel-glyph { font-size: 1.45rem; line-height: 1; }

.footer-channel-label { font-size: 0.86rem; font-weight: 600; }

/* The handle can be a full address, and the tile is ~118px at its narrowest. */
.footer-channel-handle {
  font-size: 0.72rem;
  opacity: 0.8;
  overflow-wrap: anywhere;
}

.footer-note {
  margin: 12px 0 0;
  font-size: 0.8rem;
  opacity: 0.85;
}

.footer-bar {
  padding-top: 20px;
  border-top: 1px solid rgba(255,255,255,0.35);
}

/* ==== END: Footer ========================================================== */


/* ==== BEGIN: Contact panel ================================================= */
/* The composer renders inside the modal, so it supplies the padding the
   modal-card has none of — with the same top clearance as .auth-card, which the
   close button sits in.

   The About dialog that used to share these rules is gone, along with the company
   profile it carried — the info-panel block and everything under it went with it,
   and this is the only panel left. (Written without a leading dot on purpose: the
   class-name audit reads any `.name` in this file as a live selector, including
   inside a comment.) */

.contact-panel {
  padding: 46px 24px 24px;
}
/* Channel switcher inside the composer. Same five channels as the footer, as
   pills, because here they are a choice rather than five ways in. */
.contact-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}

/* inline-flex, not the default inline flow: the channel icon is a block-level
   <svg>, and in flow it would sit on a line of its own above the label. */
.contact-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex: 1 1 auto;
  padding: 8px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-soft);
  color: var(--text-dim);
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  transition: all 0.15s ease;
}

.contact-tab:hover { border-color: var(--accent); }

.contact-tab.active {
  background: var(--card);
  border-color: var(--accent);
  color: var(--text);
}

/* Who the message is going to and who it is from — both fixed by this point,
   so they are stated rather than asked for. */
.contact-route {
  margin: 0 0 14px;
  color: var(--text-dim);
  font-size: 0.82rem;
  line-height: 1.6;
}

.contact-form { margin: 0; }

/* The confirmation reuses .auth-success, which already carries its own top
   padding; inside this panel the modal-close clearance is above it. */
.contact-panel .auth-success { padding-top: 0; }

/* ==== END: Contact panel =================================================== */


/* ==== BEGIN: Responsive breakpoints ======================================== */
/* Base rules above are the ~1024px-and-up desktop layout; each block below
   overrides it going down. Verified 320px → 2560px with no horizontal overflow.
   Note --nav-h is not set per breakpoint: useFixedNavHeight measures the real
   navbar height at every width, including when the search bar wraps below. */

/* -- Very large screens (wide monitors, >=1600px) -------------------------- */
@media (min-width: 1600px) {
  .container { max-width: 1360px; }
  .hero { max-width: 800px; }
  .hero h1 { font-size: 3.2rem; }
  .product-grid { grid-template-columns: repeat(auto-fill, minmax(245px, 1fr)); }
}

/* -- Medium screens (tablets, <=1023px) ----------------------------------- */
@media (max-width: 1023px) {
  /* NOTHING here touches the wordmark, and that is the notable part: this block
     used to hide half the company name from 1023px down, because the old inline
     wordmark cost 286px and left a company-page search field 0px of usable width
     at 768px. Stacking it fixed the cause -- the lockup now measures 194px WITH
     "Limited", and the same 768px company page leaves the search field 251px
     (measured, both). There is no longer a width to buy, so the truncation is
     gone rather than merely moved, and the bar says the full legal name at every
     width down to 479px. Do not reintroduce a .brand rule here without measuring
     that field first. */
  .hero { padding: 44px 22px 44px; }
  .hero h1 { font-size: 2.4rem; }
  .product-grid { grid-template-columns: repeat(auto-fill, minmax(205px, 1fr)); }
}

/* -- Small screens (large phones, <=767px) --------------------------------
   The search bar drops to its own row below the brand and actions, which is
   why the navbar height has to be measured rather than hardcoded.
   Keep this width in sync with SMALL_SCREEN_QUERY in app.jsx / index.html. */
@media (max-width: 767px) {
  .navbar { flex-wrap: wrap; }
  .nav-search { order: 3; flex-basis: 100%; max-width: none; }

  /* The crumb pushes the navbar onto a *third* row here (measured: 128px → 178px),
     and it is redundant — the company hero immediately below names the company.
     Hiding it holds the navbar at 111.3px at 390px and 320px alike. */
  .nav-crumb { display: none; }

  .hero h1 { font-size: 2rem; }
  .hero-actions { flex-direction: column; align-items: stretch; gap: 10px; }
  .hero p { font-size: 1rem; }

  /* No opacity override here: the field now crosses the copy at every width, so
     the one value in the base rule is already the measured ceiling. */
  .hero-banner { border-radius: 20px; }

  .toolbar { align-items: flex-start; flex-direction: column; }
  .toolbar-controls { width: 100%; }
  .select-wrap { flex: 1; }
  .select-wrap select { width: 100%; }

  /* Card internals are NOT tuned here — .product-card is a query container, so
     the plate, mark and spec rows break on card width in the @container block
     above. Doing it by viewport shrank the mark on the wide single-column card
     at 375px while leaving the cramped 3-up cards at 700px untouched. */
  .product-grid { grid-template-columns: repeat(auto-fill, minmax(185px, 1fr)); gap: 12px; }

  /* Every hero treatment collapses to one column. The banner's photo tile stops
     being a far-edge item and simply stacks under the copy — `space-between` has
     nothing to space out in a column, and `align-items: flex-start` is what
     stops it stretching to the full width. */
  .company-hero.layout-split,
  .company-hero.layout-banner,
  .company-hero.layout-rule {
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
  }

  .company-hero.layout-panel { grid-template-columns: 1fr; }
  .company-hero-mark { font-size: calc(2rem * var(--mk-scale)); }

  .company-hero-plate { width: 92px; height: 92px; }
  .company-hero-panel .company-hero-plate { width: 76px; height: 76px; }

  .company-hero.layout-rule { padding: 24px 20px 24px 0; }
  .company-hero.layout-rule .company-hero-text { padding-left: 18px; }
  /* Stretched, so `max-width` has to lift with it or the stamp stays 230px wide
     in a column that is asking it to fill the band. */
  .company-hero-stamp { align-self: stretch; max-width: none; margin-left: 18px; }
  .company-hero-panel { min-width: 0; }

  .category-index { max-width: none; }

  /* Two equal halves rather than a wrapping pill row — "Therapeutic
     categories" is too long to sit beside its sibling at this width. */
  .showcase-tabs { display: flex; width: 100%; }
  .showcase-tab { flex: 1; padding: 8px 10px; font-size: 0.8rem; }

  .showcase-spot { flex-direction: column; align-items: flex-start; gap: 12px; }
  .showcase-spot-count { margin-left: 0; text-align: left; }

  .feature-grid { grid-template-columns: 1fr; gap: 12px; }


  /* The footer actions go full width — side by side at this width they are
     narrower than their own minimum. This `min-width: 0` also has to sit after
     .footer-to-top's 190px rather than before it: same specificity, so source
     order is the whole of the cascade here, and the wrong order would leave the
     blue button at a fixed 190px inside a stretched column. */
  .footer-actions { flex-direction: column; align-items: stretch; }
  .footer-action { min-width: 0; }
}

/* -- Very small screens (small phones, <=479px) --------------------------- */
@media (max-width: 479px) {
  .container { padding: 0 16px; }
  .navbar { padding: 12px 0; gap: 12px; }

  /* The lockup stays TWO LINES on a phone, whole legal name included. Collapsing
     it to a bare word here would make the smallest screen the only one that does
     not say what the company does, and it is not necessary: scaled down like
     this it measures 171px against the 348px row a 375px phone has, so it fits
     signed in as well as signed out. The 374px block at the end of this section
     is where it finally gives something up. */
  .brand { gap: 9px; }
  .brand-mark { width: 34px; height: 34px; border-radius: 10px; font-size: 0.9rem; }
  .brand-line-a { font-size: 1.05rem; }
  .brand-line-b { font-size: 0.5rem; letter-spacing: 0.1em; margin-right: -0.1em; }

  .nav-actions { gap: 8px; }
  .nav-actions .btn { padding: 8px 12px; font-size: 0.85rem; }

  .eyebrow { font-size: 0.7rem; }
  .hero { padding: 32px 18px 36px; }
  .hero-banner { margin-top: 12px; border-radius: 18px; }
  .hero h1 { font-size: 1.7rem; }
  .hero p { font-size: 0.95rem; }

  .company-strip { padding-bottom: 32px; }
  .company-strip h2 { font-size: 1.3rem; }
  .company-directory { grid-template-columns: 1fr; }

  .company-hero.layout-split,
  .company-hero.layout-stack,
  .company-hero.layout-panel,
  .company-hero.layout-banner { padding: 22px 18px; }

  /* Drops the wordmark small enough that break-word rarely has to fire — a
     display face broken mid-word is legible but ugly. */
  .company-hero-mark { font-size: calc(1.6rem * var(--mk-scale)); }

  .category-group-head h3 { font-size: 0.95rem; }
  .category-chip { padding: 6px 10px; font-size: 0.76rem; }

  .showcase { padding-bottom: 34px; }
  .showcase h2 { font-size: 1.3rem; }
  .showcase-spot { padding: 16px 18px; }
  .showcase-tile { padding: 7px 11px; font-size: 0.76rem; }

  .pager { gap: 6px; }
  .pager-num,
  .pager-step {
    min-width: 34px;
    height: 34px;
    padding: 0 9px;
    font-size: 0.82rem;
  }

  /* Single column: two columns at this width leaves ~137px per card, too
     narrow for the "Dissolution / 94% at 30 min" spec row to hold one line. */
  .product-grid { grid-template-columns: 1fr; }
  .docs-head { margin: -20px -16px 16px; padding: 18px 52px 14px 16px; }
  .features h2 { font-size: 1.35rem; }
  .feature-card { padding: 20px; }

  /* Still AA at this size — --hero-teal is measured for it. */
  .hero .hero-slogan { font-size: 1rem; }
  .profile-head { font-size: 1.35rem; }
  .profile-list .disclosure-region { padding: 12px 12px 16px; }


  .modal-overlay { padding: 12px; }
  .cart-panel { padding: 20px 16px 22px; }
  .auth-card { padding: 22px 18px 20px; }
  .modal-card .auth-card { padding-top: 44px; }

  .contact-panel { padding: 44px 18px 20px; }

  /* Two pills a row beats five squeezed onto one at 320px. */
  .contact-tab { flex: 1 1 40%; }

  /* The navbar has the search on its own row here; the account name would push
     sign-out and the cart onto a third. The name is in the footer note anyway. */
  .nav-account { display: none; }

  .footer-wrap { padding: 34px 0 22px; }
  .footer-inner { gap: 22px; }

  /* Was `.footer-lead h2 { font-size: 1.15rem }`, which stopped doing anything
     the moment the heading became the lockup — the two lines carry their own
     sizes. Scaled through .footer-brand instead, and it keeps the footer's step
     up over the navbar (which is at 34px / 1.05rem / 0.5rem at this width).

     The plate rule that used to sit here went with the plate itself — see
     .footer-brand in the main block. So did the `gap`, for the same reason it
     went there: one child, nothing to separate. */
  .footer-brand .brand-line-a { font-size: 1.2rem; }
  .footer-brand .brand-line-b { font-size: 0.56rem; }

  .feature-grid.collapsible .feature-toggle { padding: 14px 16px; }
  .feature-grid.collapsible .feature-card p { padding: 0 16px 16px; }
}

/* -- Narrow phones (<=374px) ----------------------------------------------- */
/* The one width that cannot carry the whole legal name, and the only thing this
   block does. "LIMITED" costs the second line 40px, and the failure it causes is
   not overflow -- the bar wraps instead, so nothing sticks out and the navbar
   just silently becomes THREE rows: measured 111px -> 157px at 320px. Growing a
   row is the regression, and a bar that measures fine is exactly how it would
   get missed, which is why the number is written down here.

   Why 374 and not 359. At 360px the full name does fit, but only just: 19px of
   slack signed out, and 10px signed in, because "Sign out" is 9px wider than
   "Sign in". 10px is not a margin, it is a coincidence -- one padding change in
   .nav-actions and 360px silently grows the third row. 375px keeps 25px signed
   in, which is a margin. So 360px Android loses "Limited" it could technically
   hold, on purpose.

   Nothing is actually lost: the lockup still reads "Savannah / PHARMACEUTICALS",
   and the link's aria-label announces the full legal name at every width. */
@media (max-width: 374px) {
  .brand-suffix { display: none; }
}

/* ==== END: Responsive breakpoints ========================================== */
