/* 420.com storefront — brand tokens pulled from the current live site
   (the live site's own saved source), not guessed. */

/* THE `hidden` ATTRIBUTE ACTUALLY HIDES THINGS.
   Browsers implement `hidden` as `[hidden] { display: none }` in the user-agent stylesheet, which
   ANY author rule setting `display` outranks. This shop has fifteen-odd places that hide an element
   by setting `.hidden = true`, and there was no rule for it here at all — so whether hiding worked
   depended on whether that particular element happened to have a `display` of its own.
   It did not work on checkout: `form.checkout-form { display: grid }` beat it, so an empty basket
   showed "There is nothing in your cart to check out" beside a complete, fillable checkout form.
   One rule, first, so the attribute means what it says everywhere. */
[hidden] { display: none !important; }

/* ---- THE FONTS 420.COM ACTUALLY USES ---------------------------------------------
 *
 * Both self-hosted, because the CSP is `font-src 'self'` — a Google Fonts <link> would be blocked
 * and the page would silently fall back, which is exactly the kind of failure that only shows up in
 * a browser.
 *
 * OBVIOUSLY NARROW BLACK is the display face every heading on 420.com is set in, and it is the
 * single biggest reason the live site looks like itself. It is a licensed commercial font, and it
 * is 420.COM'S OWN COPY: taken from the shop's own Shopify Files
 * (cdn.shopify.com/s/files/1/0696/7803/9175/files/Obviously-NarrBlk.woff2), the same place the
 * product photographs come from, and carried across for the same reason. Worth the owner confirming the
 * licence covers self-hosting on the same domain — it will serve from 420.com after cutover exactly
 * as it does today, so this is a move rather than a new use.
 *
 * INTER is the body face. Free (SIL Open Font License), taken from Google's CDN and stored here.
 */
@font-face {
  font-family: "Obviously Narrow";
  src: url("../fonts/Obviously-NarrBlk.woff2") format("woff2"),
       url("../fonts/Obviously-NarrBlk.woff") format("woff");
  font-weight: 900;
  font-style: normal;
  /* swap, so a heading is readable in the fallback while 43KB arrives rather than invisible. */
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../fonts/inter-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
/* ASSISTANT is the face ONE page uses: the rewards page's headings (storefront/rewards.html), which
   on live are Smile's app blocks falling through to the Dawn theme's body font — Assistant 400,
   served from Shopify's own CDN (cdn/fonts/assistant/assistant_n4…woff2, 12KB; read off the live
   page's font list, 14 Sep 2026). Set in Inter, the same 40px heading wrapped to three lines at 1280
   where live's takes two, and to four on the phone where live's takes three (the seventh QA round,
   store-content-account #4). Free (SIL Open Font License), the Latin subset taken from Google's CDN
   and stored here exactly as Inter was, and declared here beside it — rather than in the rewards
   page's own <style> — because a url() in an inline style resolves against whatever address the
   page is served at, and this one is reached as /rewards and as /pages/loyalty-rewards-program.
   Declared, not downloaded: a browser fetches a face only when a rule uses it, so every other page
   pays nothing. Regular weight only, which is the weight live renders all four headings in. */
@font-face {
  font-family: "Assistant";
  src: url("../fonts/assistant-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---- The palette, read off the live site rather than guessed --------------------
 *
 * Every value here was taken from getComputedStyle on www.420.com, not sampled by eye:
 *
 *     header background   #ffffff      (it was dark green here — the single biggest mismatch)
 *     page background     #ffffff      (it was cream, #f5f2eb)
 *     body text           #201e19
 *     heading text        #121212
 *     buttons             #1a391f, border-radius 9999px, 8px/16px, 14px/500
 *     body font           Inter
 *     heading font        Obviously Narrow, weight 900, UPPERCASE
 */
:root {
  --brand-dark-green: #1a391f;
  --brand-lime: #99c220;
  --brand-lime-light: #ddf3b5;
  --brand-cream: #f5f2eb;
  --brand-white: #ffffff;
  --brand-black: #000000;
  /* The page's real ink and ground, which are not black-on-cream. */
  --brand-ink: #201e19;
  --brand-heading: #121212;
  --brand-page: #ffffff;
  --brand-hairline: #e6e3dc;
  --font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-display: 'Obviously Narrow', 'Arial Narrow', Impact, var(--font-primary);
  --radius: 10px;
  /* 420.com's buttons are fully round-ended, not 10px-cornered. */
  --radius-pill: 9999px;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: var(--font-primary);
  background: var(--brand-page);
  color: var(--brand-ink);
  /* LIVE'S TEXT RENDERING ON APPLE SCREENS. Live's body rule (base-420.css) sets
     -webkit-font-smoothing: antialiased, so on macOS and iOS every glyph is drawn without subpixel
     weighting and reads a shade lighter; ours left the default "auto", so the SAME face at the SAME
     size and weight looked heavier and a hair wider on every page — the 404 button's "Continue
     shopping" 210.9px wide against live's 206.3 at 15px/400, the FAQ chevrons round 10 called
     "bolder than live's" (QA round 11, content-account #3). Not a size or weight difference:
     computed font-size/weight/family matched on every element measured. The -moz- line is live's
     companion declaration and does nothing on Apple screens; neither has any effect on Windows or
     Android, where the two sites already drew alike. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* EVERY HEADING IS THE DISPLAY FACE, HEAVY AND UPPERCASE — "SHOP BY CATEGORY", "JUST DROPPED",
   "SHOP BY BRAND". This one rule is most of what makes a page read as 420.com rather than as a
   generic shop, and it was the largest single difference from the live site. */
h1, h2, h3,
.home-row-head h2,
.home-tiles h2 {
  font-family: var(--font-display);
  font-weight: 900;
  text-transform: uppercase;
  color: var(--brand-heading);
  letter-spacing: 0.01em;
}
/* EXCEPT A PRODUCT'S NAME, which is not a section heading — it just happens to be marked up as one
   so the page has an outline. On 420.com a card's title is Inter 500/20px in the brand green with
   NO transform; the blanket rule above turned every product on the site into a heavy uppercase
   slab. It is a heading for structure, not for typography, and this says so.
   BY CLASS ALONE for the shared card. `.product-card h3` used to sit in this list too, and it is
   the reason one card rendered two ways in the tenth QA round (phone-sweep #2): js/product-card.js
   writes the title as an <h2> on a collection page and an <h3> inside a section that already has
   its own <h2> — the document outline a screen reader navigates by — and an element selector
   (0,1,1) outranks the class (0,1,0), so any later class-only rule reached the h2 cards and not
   the h3 ones. Every card title carries .product-card-title whatever its level; nothing else may
   key off the tag. */
.product-card-title,
.upsell-card h3,
.cart-drawer-line-title {
  font-family: var(--font-primary);
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  color: var(--brand-dark-green);
}

header.site-header {
  background: var(--brand-white);
  color: var(--brand-ink);
  border-bottom: 1px solid var(--brand-hairline);
  /* The header owns its own vertical rhythm; the bar inside handles the horizontal. Padding lives
     on .header-bar rather than here so the mega-menu can span the full width beneath it. */
  padding: 0;
  position: relative;
}
header.site-header a { color: var(--brand-ink); text-decoration: none; }

.header-bar {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 1rem 1.5rem;
  max-width: 1400px;
  margin: 0 auto;
}
/* The mark is now the real logo.svg (see js/header.js), so this only has to place it. The type
   rules that used to draw "420.com" out of the display face are gone: they described a logo this
   shop does not actually have. */
.header-logo { display: inline-flex; align-items: center; line-height: 0; }
.header-logo img { display: block; width: 69px; height: 32px; }
/* THE PHONE MARK IS SMALLER. Live's header swaps to a second copy of the same logo.svg under 768px
   (.header-420__logo-image--mobile: height 24px, width auto → 51.75px) in its 64px phone header;
   the 69×32 is its desktop size from 768 up (measured at 360, 390, 768, 1024 and 1280 on 14 Sep
   2026, round 10 phone-sweep #1). Ours drew 69×32 at every width, a third larger than live's
   beside the icons. Same file, one <img>: the size is the only thing that changes. */
@media (max-width: 767px) {
  .header-logo img { width: 52px; height: 24px; }
}
/* THE NINE NAV ITEMS ARE ONE SIZE, AND IT IS LIVE'S. Read off www.420.com's header with
   getComputedStyle on 13 Sep 2026: every item — the three menu buttons and the six links — is Inter
   17px/24px weight 500 in a 40px-tall box of 8px 12px padding, 4px apart (so ~28px between words).
   Ours set 0.95rem on `.header-nav a, .header-shop` and then `.header-shop { font: inherit }` reset
   the buttons to the 16px root size, so Shop, Effects and Brands rendered at 16px beside links at
   15.2px in the same row (the fifth QA round, store-home #3). One rule now carries the size, the
   weight, the line and the padding for both element kinds, and the button rule below no longer
   says `font: inherit`. The 40px box is also the tap target the icons beside it already have.
   Live steps the nav down to 16px at 1024 (measured: 17px at 1100 and above, 16px at 1024 and
   below) — and at 1100 live's own page scrolls sideways by 6px. The step here comes at 1150,
   because the bar with nine 17px items, the logo and the three icons measures 1107px and must
   never be wider than the page; the second step, with tighter padding, keeps it inside the 941px
   breakpoint where the desktop nav still shows (see the 940px media rule further down).
   THE SECOND STEP COMES AT 1051, NOT 1024. It was 1024 when the 16px bar measured 1002px, and
   the three chevrons the twelfth round put after Shop, Effects and Brands (.header-shop svg, 16px
   plus a 4px gap, three times) widened it: the 16px bar now needs 1051px of viewport (24px of
   padding, the 69px logo, 20px, the 762px nav, 20px, the 131px icons, 24px — measured with the
   self-hosted Inter in headless Chromium, 20 Sep 2026), so from 1024 to 1050 the icons ran into
   the right padding and at 1024, 1025 and 1026 — 1024 being an iPad in landscape and a 1280
   laptop at 125% zoom — the page scrolled sideways by 3px on every page of the shop (the handoff
   ledger's "nav.header-actions runs 3px past the viewport"). The 15px bar needs 982px, so at 1024
   it has 42px to spare; the step is drawn where the wider bar stops fitting. Re-measured at 1440,
   1156, 1150, 1149, 1100, 1051, 1050, 1025, 1024, 1023, 990 and 982 on 20 Sep 2026: no
   horizontal scroll at any of them. The same 60px means the 15px bar no longer fits the 941px
   breakpoint either — between 941 and 981 the icons sit in the right padding and from 941 to
   957 the page scrolls by up to 17px — and the 17px bar (1156px now) takes 6px of the padding
   from 1150 to 1155. Neither band is a device width or a zoom step of one (a 1280 laptop at
   133% is 962: padding eaten, no scroll), and neither is changed here — the 941 band is the
   burger breakpoint's to move, not this rule's. */
.header-nav { display: flex; align-items: center; gap: 4px; }
.header-nav a, .header-shop {
  font-family: var(--font-primary); font-size: 17px; line-height: 24px; font-weight: 500;
  padding: 8px 12px; border-radius: 8px; white-space: nowrap;
}
@media (max-width: 1149px) {
  .header-nav a, .header-shop { font-size: 16px; padding: 8px 8px; }
}
@media (max-width: 1050px) {
  .header-nav a, .header-shop { font-size: 15px; padding: 8px 6px; }
}
/* The Shop control is a <button> because it opens a menu — it goes nowhere on its own, and a link
   that does not navigate is a link a screen reader announces wrongly. Styled to sit beside the real
   links without looking like a form control. (No `font: inherit` here — that shorthand also resets
   font-size, which is how the buttons came to differ from the links; see the nav rule above.) */
.header-shop {
  background: none; border: 0; color: var(--brand-ink); cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px;
}
/* THE OPEN BUTTON WEARS LIVE'S PILL AND ITS CHEVRON TURNS. Live's .header-420__nav-trigger is
   the 17px/24px 8px-12px box the rule above already gives every nav item, with a 16px stroked
   chevron (js/header.js draws live's own path) 4px after the word; while its menu is open the
   button's background is rgba(26,57,31,.08) at the 8px radius and the chevron is rotated 180°
   over 0.2s (getComputedStyle on www.420.com, 15 Sep 2026 — the twelfth QA round, store-home #3).
   Ours kept a transparent background and swapped a text "▾" for "▴", so nothing about the open
   button said which menu was open. The same 8% wash the icons take under the pointer. */
.header-shop svg {
  width: 16px; height: 16px; flex: none; fill: none;
  stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform 0.2s;
}
.header-shop[aria-expanded="true"] { background: rgba(26, 57, 31, 0.08); }
.header-shop[aria-expanded="true"] svg { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .header-shop svg { transition: none; }
}
/* The search field no longer sits in this bar: the magnifier opens a drawer (see "The search
   drawer" below), which is what 420.com's magnifier does. */
.header-actions { display: flex; align-items: center; gap: 0.35rem; margin-left: auto; }
.header-actions a { font-size: 0.95rem; font-weight: 500; }

/* THE THREE ICONS ON THE RIGHT — magnifier, trolley, person, as on 420.com's own header.
   Drawn as stroked SVG rather than a font or an image: they inherit the colour, stay crisp at
   any zoom, and cost no extra request. LIVE'S SIZE, WEIGHT AND COLOUR: its `.header-420__icon-btn`
   is a 40px round button whose svg is 24×24 at stroke-width 2 in the brand green (#1a391f), with
   rgba(26,57,31,.08) under the pointer — read off its stylesheet and re-measured on the magnifier,
   the cart and the person at 1280 and 390, 13 Sep 2026. Ours were 22×22 at 1.7 in the body ink,
   visibly lighter and greyer beside live's on every page (the sixth QA round, store-content-account
   #5). */
.header-icon {
  display: inline-flex; align-items: center; justify-content: center; position: relative;
  width: 40px; height: 40px; padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-pill); color: var(--brand-dark-green);
}
/* The cart and the person are LINKS, and `header.site-header a` (one class, two types) outscores
   `.header-icon` (one class) for their colour — so the magnifier, a button, went green and the two
   links stayed in the ink (measured after the first change). Scoped through the header for the same
   reason the mega-menu's links are, a few rules down. */
header.site-header a.header-icon { color: var(--brand-dark-green); }
.header-icon:hover { background: rgba(26, 57, 31, 0.08); }
.header-icon svg {
  width: 24px; height: 24px; fill: none;
  stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
/* The basket count, as a badge on the bag. It was the text " (3)" trailing the word "Basket";
   with the word gone it has to read as a count on its own. */
header.site-header .cart-link #header-cart-count {
  position: absolute; top: 2px; right: 2px; min-width: 17px; height: 17px;
  padding: 0 4px; border-radius: var(--radius-pill);
  background: var(--brand-dark-green); color: var(--brand-white);
  font-size: 11px; font-weight: 700; line-height: 17px; text-align: center;
  font-variant-numeric: tabular-nums;
}

/* THE MEGA-MENU — the three ways into this catalog: category, effect, brand. */
.megamenu {
  position: absolute; left: 0; right: 0; top: 100%; z-index: 900;
  background: var(--brand-white); color: var(--brand-dark-green);
  border-top: 3px solid var(--brand-lime);
  box-shadow: 0 12px 28px rgba(0,0,0,0.18);
  /* The cap itself is set inline on open (js/header.js) from the panel's measured top, because the
     header's height varies; this pair is what makes the cap scroll instead of clip. */
  max-height: calc(100vh - 96px); overflow-y: auto; overscroll-behavior: contain;
}
.megamenu-inner {
  max-width: 1400px; margin: 0 auto; padding: 1.75rem 1.5rem;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1.5rem 2rem;
}
.megamenu-heading {
  margin: 0 0 0.6rem; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: #5b7a5f;
}
.megamenu-column ul { list-style: none; margin: 0; padding: 0; }
.megamenu-column li + li { margin-top: 0.4rem; }
/* SPECIFICITY MATTERS HERE, and getting it wrong is invisible to every test.
   `header.site-header a` sets white text for the dark bar. It scores one class and two types;
   `.megamenu-column a` scores one class and one type, so the bar's rule WINS and every link in this
   panel rendered white on the panel's white background. The counts stayed readable because they
   carry their own class, which is precisely what made it look like a data bug — headings and
   numbers, no names — rather than a colour one. Scoped through header.site-header so it outranks
   the rule it has to beat, rather than reaching for !important. */
header.site-header .megamenu-column a {
  color: var(--brand-dark-green); font-size: 0.95rem; font-weight: 400;
  display: flex; justify-content: space-between; gap: 0.75rem; padding: 0.15rem 0;
}
header.site-header .megamenu-column a:hover { color: #000; text-decoration: underline; }
/* The count is information, not emphasis — it must not compete with the name it qualifies. */
.megamenu-count { color: #6b8a6f; font-variant-numeric: tabular-nums; }

/* Visible to a screen reader, not to the eye. Used for labels that would otherwise be missing —
   a search field whose only visible cue is a placeholder, which is not a label. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- THE MOBILE DRAWER --------------------------------------------------------------------
   420.com collapses its whole header to a hamburger and a logo below its own breakpoint and puts
   the navigation in a drawer. This shop had no mobile header behaviour at all: nine nav items and
   three mega-menu buttons simply wrapped, so a phone opened every page with four lines of links
   above the content. The desktop bar is unchanged — everything here is inside the query. */
/* DRAWN LIKE THE OTHER THREE. The sixth round recoloured the magnifier, trolley and person to
   live's 2px brand green (.header-icon svg above) and left this one at 22px, 1.8px, in the body
   ink — so on a phone the menu button was the one dark, thinner glyph beside three green ones (the
   seventh QA round, store-content-account #2). Live's four phone icons are all alike: 20×20, 2px,
   rgb(26,57,31), measured on the FAQ page at 390 and again at 600, 749 and 750 on 14 Sep 2026 (its
   desktop trio is the 24px .header-icon already keeps). The 20px phone size for all four is set in
   the collapsed-header query further down, beside the rule that shows this button. */
.header-burger {
  display: none;
  align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-pill); color: var(--brand-dark-green); margin-right: -0.5rem;
}
.header-burger svg {
  width: 20px; height: 20px; fill: none;
  stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
/* ABOVE KLAVIYO'S SIGN-UP POP-UP (a fixed full-screen dialog at z-index 90000), like the cart drawer
   since round 6 and the search drawer since round 7 — see the stack written out at .cart-drawer-scrim.
   At 950/960 the pop-up, which fires six to eight seconds after the first page load, landed ON TOP
   of an open menu: the form over the TOP BRANDS links, every tap on them going to the form until its
   × was found (QA round 7, phone-sweep #2). Live's mobile drawer sits at 2147483647, over the same
   form, so a live visitor meets the pop-up only after closing the menu. 90003/90004: under the search
   drawer (90005/90006), which opens from this header and must draw over it, and under the cart drawer
   (90010/90011). */
.drawer-backdrop {
  position: fixed; inset: 0; z-index: 90003; background: rgba(0, 0, 0, 0.45);
}
.mobile-drawer {
  position: fixed; top: 0; bottom: 0; left: 0; z-index: 90004;
  width: min(86vw, 360px);
  background: var(--brand-white); color: var(--brand-ink);
  display: flex; flex-direction: column;
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.25);
}
/* THE CLOSE IS WHERE THE BURGER WAS: TOP LEFT, IN A BAND. Live's .mobile-drawer__header is a
   65px band across the top of the sheet — 12px of padding round a 40px button, closed by a 1px
   rgba(26,57,31,.08) hairline — with a plain × (24px, stroke 2, in the green, no disc) at its left
   edge, x 12 / y 12, the corner the burger occupied on the page behind; SHOP BY CATEGORY starts
   24px under the hairline (getComputedStyle on www.420.com at 390, 15 Sep 2026). Ours put a
   grey-circled × at the top RIGHT with the first heading straight under it and no divider, so a
   thumb that went back to the burger's corner found nothing (the twelfth QA round, store-home
   #6). Same 40px target, live's placement and drawing. */
.mobile-drawer-head {
  display: flex; justify-content: space-between; align-items: center; flex: none;
  padding: 12px; border-bottom: 1px solid rgba(26, 57, 31, 0.08);
}
.mobile-drawer-close {
  width: 40px; height: 40px; padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-pill); color: var(--brand-dark-green);
  display: inline-flex; align-items: center; justify-content: center;
}
.mobile-drawer-close:hover { background: rgba(26, 57, 31, 0.08); }
.mobile-drawer-close svg {
  width: 24px; height: 24px; fill: none;
  stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
/* The drawer scrolls, the page behind it does not — see .drawer-open below. */
.mobile-drawer-body { flex: 1 1 auto; overflow-y: auto; padding: 0.5rem 1.25rem 2rem; }
.mobile-drawer-section { padding: 1rem 0; border-bottom: 1px solid var(--brand-hairline); }
.mobile-drawer-section:last-child { border-bottom: 0; }
.mobile-drawer-title {
  margin: 0 0 0.25rem; font-size: 0.85rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--brand-dark-green);
}
/* .mobile-drawer-all — the full-width "Shop all" button under each section — is drawn in the
   fix-round-3 block at the end of this file (it was a small underlined word beside the heading). */
.mobile-drawer-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; margin-top: 0.75rem; }
.mobile-drawer-tile {
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
  text-align: center; font-size: 0.75rem; line-height: 1.2; color: var(--brand-ink);
}
.mobile-drawer-tile img {
  width: 100%; aspect-ratio: 1 / 1; object-fit: contain;
  background: #f6f5f2; border-radius: var(--radius);
}
/* The live drawer's category tiles are the same painted cards the home page uses (label in the
   picture, pastel ground of its own), so the tile is the picture: covered, no grey ground behind it,
   and the label kept for a screen reader only (js/header.js marks it visually-hidden). */
.mobile-drawer-tile-art { gap: 0; }
.mobile-drawer-tile-art img { object-fit: cover; background: transparent; border-radius: 12px; }
.mobile-drawer-pills { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
/* Cream pills with the ink in green — the live drawer's chips (Calm, Happy, Focused …) are read off
   its screen at 390px: a pale cream ground, not the lime the filter chips use. */
.mobile-drawer-pills a {
  padding: 0.55rem 1rem; border-radius: var(--radius-pill);
  background: var(--brand-cream); color: var(--brand-dark-green); font-size: 0.95rem;
}
.mobile-drawer-brands {
  list-style: none; margin: 0.75rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem 1rem;
}
.mobile-drawer-links { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.6rem; }
.mobile-drawer-brands a, .mobile-drawer-links a { font-size: 0.95rem; color: var(--brand-ink); }
.mobile-drawer-signin {
  display: block; margin-top: 0.75rem; text-align: center;
  background: var(--brand-dark-green); color: var(--brand-white);
  padding: 0.75rem 1rem; border-radius: var(--radius-pill); font-weight: 700;
}
header.site-header .mobile-drawer-signin { color: var(--brand-white); }
/* THE `hidden` ATTRIBUTE HAS TO BE SPELLED OUT HERE, and forgetting it is a silent, total failure.
   The browser's own `[hidden] { display: none }` comes from the user-agent stylesheet, which loses
   to ANY author rule — so `.mobile-drawer { display: flex }` above would have kept the drawer on
   screen, open, over every page of the shop, on every phone, from first paint. The same trap the
   .megamenu containers avoid by having no display of their own. */
.mobile-drawer[hidden], .drawer-backdrop[hidden] { display: none; }
/* The page behind the drawer must not scroll: a shopper who flicks the backdrop and finds the
   catalogue moving underneath has lost their place in it. */
body.drawer-open { overflow: hidden; }

/* 940px, not 800: the desktop header — logo, nine nav items with their menus, the icons — needs
   about 930px. Between 801 and 940 (a 1280 laptop at 150% zoom is 853) the icons were pushed past
   the right edge and the whole page scrolled sideways (measured 12 Sep 2026: scrollWidth 928 at
   853). Shopify's own themes switch to the burger at 990. */
@media (max-width: 940px) {
  /* flex-wrap stays so the bar can take an extra row if a page adopts its own controls into it
     (see adopt() in js/header.js). */
  .header-bar { flex-wrap: wrap; gap: 0.5rem 0.75rem; padding: 0.75rem 1rem; }
  .header-burger { display: inline-flex; }
  /* Live's collapsed header draws its icons at 20×20 (the desktop's are 24) — the magnifier, the
     trolley and the person the same size as the burger beside them, in the same 40px buttons. */
  .header-icon svg { width: 20px; height: 20px; }
  /* The nav and its three mega-menus live in the drawer on a phone. They are not deleted — the
     markup and every listener stay exactly as they are, so nothing has to know which layout it is
     in and there is no second code path to keep in step. */
  .header-nav, .megamenu { display: none; }
  .megamenu-inner { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); padding: 1.25rem 1rem; }
}
@media (min-width: 941px) {
  /* Belt and braces: the drawer can never appear on a desktop even if its `hidden` attribute is
     removed by something else on the page. */
  .mobile-drawer, .drawer-backdrop { display: none; }
}

main { max-width: 1100px; margin: 0 auto; padding: 1.5rem; }

.age-gate-overlay {
  position: fixed; inset: 0; background: rgba(26,58,30,0.92);
  /* OVER EVERYTHING THE SHOP DRAWS. This was 1000, one step above the cart drawer at 971; the
     drawer moved to 90011 to sit above Klaviyo's sign-up pop-up (see .cart-drawer-scrim), so the
     gate moves up with it — a basket that could be drawn over the 21+ wall would be the wall not
     working. The vendor launchers (Smile, Tidio) are held back by script until the gate is passed
     rather than out-stacked (js/tidio.js), so nothing changes for them. */
  display: flex; align-items: center; justify-content: center; z-index: 90100;
}
.age-gate-box {
  background: var(--brand-white); border-radius: var(--radius); padding: 2rem;
  max-width: 420px; text-align: center;
}
.age-gate-box h2 { color: var(--brand-dark-green); }
.age-gate-box button {
  background: var(--brand-lime); border: none; color: var(--brand-black);
  padding: 0.75rem 1.5rem; border-radius: var(--radius); font-weight: 700; cursor: pointer; margin: 0.25rem;
}
.age-gate-box button.decline { background: var(--brand-cream); }

/* TWO COLUMNS ON A PHONE, THREE ON A TABLET, FOUR ON A DESKTOP — counted off the live grid rather
 * than left to the browser to decide.
 *
 * This was `repeat(auto-fill, minmax(220px, 1fr))`, which sounds equivalent and is not. auto-fill
 * asks "how many 220px columns fit?", and inside this page's own 1100px/1.5rem `main` the answer on a
 * phone is ONE: a 390px viewport leaves 342px of content box, 2x220 + 20 gap needs 460. So every
 * phone got a single column of full-width cards and had to scroll through 16 of them to see one
 * screenful of shop, while www.420.com shows two.
 *
 * Measured in the browser against live's own breakpoints (.product-grid-420 in the collection
 * section's stylesheet: repeat(2, 1fr), then 3 at min-width 768px, then 4 at 1024px):
 *
 *     viewport   375  390  414  430  500  600  700  768  900  1000  1024  1280  1600
 *     live         2    2    2    2    2    2    2    3    3     3     4     4     4
 *     auto-fill    1    1    1    1    1    2    2    3    3     4     4     4     4
 *                  ^^^^^^^^^^^^^^^^^^^^^ every phone                  ^ and one tablet width
 *
 * Fixed counts at live's own two breakpoints rather than a smaller minmax floor, because a floor is
 * a guess that has to be re-guessed every time the container width changes — and because the two
 * numbers that matter here are the two the live shop committed to. Cards get narrower on a small
 * phone instead of fewer, which is exactly the trade live makes. */
/* THE GAP WIDENS AT THE SAME BREAKPOINT THE COLUMN COUNT DOES, because on the live grid it is the
 * same declaration. `.product-grid-420 { gap: var(--space-base) }` then `@media (min-width: 768px)
 * { gap: var(--space-lg) }`, and base-420.css defines those as 16px and 24px — so live runs a
 * 16px gutter on a phone and 24px from a tablet up. Ours was a flat 1.25rem (20px) at every width:
 * 4px too wide where the cards are already narrowest and two across, and 4px too narrow where
 * there are four of them.
 *
 * Worth stating because the column counts directly below were taken off this same live rule and the
 * gap beside them was not — the two halves of one declaration were copied apart. Written in px
 * rather than rem deliberately: this is a gutter between photographs, not text, and live's own
 * tokens are px, so matching the unit is what keeps the two grids the same grid at a zoom level
 * where a rem gutter would have grown and a px one does not. */
.product-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px;
  margin-top: 1.5rem;
}
@media (min-width: 768px) {
  .product-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
}
@media (min-width: 1024px) {
  .product-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* NO CARD CHROME. Measured on 420.com's grid: the CARD has a transparent background, no border and
   no radius — the text sits directly on the page, and only the photograph's own tile (below) is
   painted. Ours were white boxes with a lime hairline, which reads as a different shop at a glance. */
.product-card {
  background: transparent; border-radius: 0; padding: 0 0 1rem;
  text-decoration: none; color: var(--brand-dark-green); display: block;
  border: 0;
}
/* A CREAM TILE BEHIND THE PRODUCT, AND CROPPED TO FILL IT — as 420.com's own grid does.
 *
 * The ground is the shop's cream, not white: live's `.product-card-420__image-container` is
 * rgb(245,242,235) with a 12px radius, read with getComputedStyle on /collections/all at 1280 and
 * 390 in the third QA round (13 Sep 2026). An earlier note here recorded live's tiles as white, and
 * the round before that as transparent; both were read off the CARD, which is transparent, rather
 * than off the image container inside it, which is what a shopper actually sees. The photographs
 * are cut-outs with transparent backgrounds, so whatever is painted here shows THROUGH them — which
 * is exactly why it has to be the same cream as live's and not a brand colour (a lime tile made
 * every card a lime square with a jar floating in it) and not white (a white square on a white page
 * is no tile at all, and the grid read as a different, sparser shop beside live's).
 *
 * The FIT changed from `contain` to `cover`, and this comment used to argue the opposite — that
 * cover crops a cut-out which already carries its own padding. That reasoning was sound and the
 * conclusion was still wrong for this shop, which was settled by measuring the live site rather than
 * reasoning about it: 420.com puts a 616x1027 portrait source into a 299x299 square with
 * `object-fit: cover`, cropping about 20% off each end, on its own collection grid.
 *
 * Contained, our cards drew a small product adrift in a tall white box while the live site's filled
 * the tile edge to edge. Cropping ours costs 12-20% top and bottom depending on the source
 * (960x1280 and 1300x2167 are the two common shapes here) — the same order as live's, and on a
 * cut-out most of what goes is the padding.
 *
 * Worth knowing: this is the one change in the storefront pass that DISCARDS pixels rather than
 * rearranging them. If a product ever photographs badly here, the fix is the crop on that image,
 * not this rule. */
.product-thumb {
  /* display: block — an <img> is inline by default and sits on a text baseline, so the line box
     added ~5px of descender space under the tile that no rule asked for; it was the last 5px
     between our 449px grid card and live's 444 (round 10, after the type was put back). */
  display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 12px;
  /* 12px under the tile — the live card's info block starts 12px below its image (516 → 528 at 1280). */
  margin-bottom: 12px; background: var(--brand-cream);
}
/* A product with NO photograph is a neutral tile, not a brand-coloured one — a missing image should
   read as missing, not as a design choice. */
.product-thumb-placeholder { background: #ece9e1; }
.product-detail-image { width: 100%; max-width: 420px; border-radius: var(--radius); display: block; margin-bottom: 1rem; }
/* Sentence case, as Shopify stores it and as 420.com shows it ("Litty", "Arsn") — it was being
   shouted in letterspaced capitals. 14px on a 28px line in 80% brand green, which is the live
   `.product-card-420__brand` at every width (read 13 Sep 2026). */
.product-card .vendor { color: rgba(26, 57, 31, 0.8); font-size: 14px; line-height: 28px; font-weight: 400; overflow-wrap: anywhere; }
/* There used to be a `.product-card h3 { margin; font-size }` rule here from before the shared card
   (js/product-card.js). Every card title now carries .product-card-title whatever its heading level,
   and that rule's higher specificity quietly overrode the title's own size on every h3 card — the
   home grid and the search results — while the h2 cards on a collection page took the other rule.
   One size, set once on the class, further down. */
/* TWO LINES, ALWAYS — so the prices in a row line up with each other.
 *
 * Card titles here run from "NUG Zoap Flower 3.5g" to "Litty Yetti Blue Razzy Blizzard All In One
 * 3.5g": one line or two depending on the product. With nothing reserving the second line, the price
 * under a short name sat 20px higher than the price beside it, so a row of four cards had prices at
 * two or three different heights and the grid read as misaligned rather than as a grid. Measured on
 * the deployed collection page: title boxes of 41px and 21px in the same row.
 *
 * This is what the live card does, in the same two declarations —
 * `.product-card-420__product-title { -webkit-line-clamp: 2; line-height: 1.2; min-height: 2.4em; }`
 * — so a long name is cut off at two lines with an ellipsis and a short one still occupies two.
 *
 * min-height in em, not px, and the clamp counted in LINES: both scale with the reader's text size
 * instead of clipping a different amount at every zoom level. Same reason the product page's
 * description clamp is written this way.
 *
 * The -webkit- prefixes are not legacy cruft. `-webkit-line-clamp` with `display: -webkit-box` is the
 * only line clamp every current browser actually implements (Chrome, Safari and Firefox all ship it
 * unprefixed-in-spirit under these names); the unprefixed `line-clamp` is still not safe to rely on
 * alone. Both are set so this keeps working when it is. */
.product-card-title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.2;
  min-height: 2.4em;
}
/* THE PRICE IS LIVE'S SPAN, NOT LIVE'S WRAPPER. Round 3 set this to 17px/28px, weight 400, in the
   page's ink — which is what live's `.product-card-420__price` computes, and that is the flex
   WRAPPER around the figure. The figure itself is `.product-card-420__price-current`: 16px, weight
   500, brand green rgb(26,57,31), 18px from 768px up, on the wrapper's 28px line (live's
   stylesheet, re-read 13 Sep 2026 for round 4: `.product-card-420__price-current { color: rgb(26,
   57, 31); font-size: 16px; font-weight: 500 }` and `@media (min-width: 768px) { font-size: 18px
   }`). Side by side at 2× the difference was plain — live's "$47.99" a medium green figure, ours a
   thinner black one — on every card of every collection and every search. .is-sale below keeps
   its red by specificity; the struck compare-at is live's 14px (`.product-card-420__price-compare`),
   pinned here because it was sized as a fraction of this figure and would have grown with it.
   THE SAME TRAP WAS WALKED INTO AGAIN in round 9 (commit b71758c), which appended a block at the
   foot of this sheet setting the title 17px/28px/400 and this price 17px/400 in black — the
   wrapper's numbers once more — and every grid, search and carousel card lost its green medium
   figure for a day. Re-measured on www.420.com on 14 Sep 2026 for round 10, on the collection grid
   (.product-card-420__price-current), the PDP carousel and the home page's Just Dropped strip
   (.product-carousel__price-current), all three the same: 16px/500 rgb(26,57,31) at 390, 18px at
   1280, on the wrapper's 28px line. tests/storefront.test.mjs pins these two rules and refuses any
   later `.product-card .price` or `.product-card-title` rule that sets a size, a weight or a
   colour, so the next fixer who measures the wrapper is stopped by the test rather than by the
   next QA round. */
.product-card .price { font-weight: 500; font-size: 16px; line-height: 28px; color: var(--brand-dark-green); }
@media (min-width: 768px) { .product-card .price { font-size: 18px; } }
.product-card .price .price-was { font-size: 14px; }
/* A PRICE THAT HAS BEEN CUT READS AS CUT. 420.com turns the current price dark red the moment there
   is something struck through beside it (`.product-card-420__price-current--sale { color: #84052c }`)
   and sets the old price in translucent brand green rather than grey. Ours left the new price the
   same green as every other price on the page, so the only signal that a $200 care package was $400
   was the small struck number — the saving was legible but not visible. The five reduced products in
   this catalogue are all care packages, which is what a search for "vape" puts at the top of the
   results, so this is the first card a searching customer sees. */
.product-card .price.is-sale { color: #84052c; }
.product-card .price-was { color: rgba(26, 57, 31, 0.6); }
/* THE CARD THAT CARRIES "Add to cart +" (js/product-card.js, addToCart) is an <article> holding the
   link and the button as siblings — a button inside a link is invalid and would navigate as it adds.
   The inner link inherits everything the plain card had, so the two cards look the same above the
   button. */
/* No foot under the button: live's carousel card (.product-carousel__card, 508px at 1280 and 349 at
   390, re-measured 14 Sep 2026) ends at the button's bottom edge, where its grid card keeps a 16px
   wrapper under the price (the `.product-card` padding above, which the catalogue's foot is built
   on). Without this the strip's cards were 16px taller than live's with the type already right. */
.product-card-buyable { display: flex; flex-direction: column; padding-bottom: 0; }
.product-card-link { display: block; color: inherit; text-decoration: none; flex: 1 1 auto; }
/* Measured off the live card (component-card.css on www.420.com, 13 Sep 2026): a full-width pill,
   #1a391f on white, "Add to cart" at the left and a "+" at the right, 52px tall on the desktop and
   a little taller under a thumb. The label is 16px/500 — re-read on the home page's Just Dropped
   strip and the PDP carousel (.product-carousel__add-to-cart: 16px/28px 500, 12px 16px of padding,
   52px) on 14 Sep 2026; round 9 had it at 15px and then set 16px at weight 400 in a block at the
   foot of the sheet, and live's is 500. The 16px margin above it is live's gap between the price
   line and the button (info block ends at 3594, button starts at 3610 at 1280), and it is what
   makes the card 508px tall like live's. */
.product-card-add {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  width: 100%; min-height: 52px; margin-top: 16px; padding: 0 1.25rem;
  background: var(--brand-dark-green); color: var(--brand-white);
  border: 0; border-radius: var(--radius-pill);
  font: inherit; font-size: 16px; font-weight: 500; cursor: pointer; text-align: left;
}
.product-card-add:hover, .product-card-add:focus-visible { background: #245029; }
.product-card-add-plus { font-size: 1.5rem; line-height: 1; font-weight: 300; }
/* Between the click and the drawer there is one request for the variant; the button says so with its
   state rather than with different words, so nothing jumps. */
.product-card-add.is-adding { opacity: 0.7; cursor: progress; }
/* "Sold out", disabled, when the listing says the product has nothing in stock (in_stock = 0 —
   js/product-card.js). The same 50% the buy box's own disabled Add to cart wears
   (button.primary:disabled), so one greyed pill means one thing everywhere, and no hover darkening
   on a control that does nothing. */
.product-card-add:disabled, .product-card-add:disabled:hover { background: var(--brand-dark-green); opacity: 0.5; cursor: not-allowed; }
.upsell-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem;
  margin: 1rem 0 1.5rem;
}
.upsell-card {
  background: var(--brand-white); border-radius: var(--radius); padding: 0.75rem;
  text-decoration: none; color: inherit; display: block; border: 1px solid var(--brand-lime-light);
}
.upsell-thumb { width: 100%; aspect-ratio: 1 / 1; object-fit: contain; border-radius: var(--radius); margin-bottom: 0.4rem; background: var(--brand-white); }
.upsell-thumb-placeholder { background: #ece9e1; }
.upsell-card .vendor { color: var(--brand-dark-green); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
.upsell-card h4 { margin: 0.25rem 0; font-size: 0.9rem; }
.upsell-card .price { font-weight: 700; color: var(--brand-dark-green); font-size: 0.85rem; }
/* A reduced suggestion reads as reduced, the way the listing's card does two rules up: the price
   in the sale red with the compare-at struck beside it (QA round 6, store-cart-checkout #2). */
.upsell-card .price.is-sale { color: #84052c; }
.upsell-card .price-was { color: rgba(26, 57, 31, 0.6); }
/* THE CARD'S LINK, which had no styling at all and rendered in the browser's default #0000EE with an
   underline — the one piece of unstyled markup on the shop.
   It was invisible for a straightforward reason: this strip only appears when a product HAS upsells,
   and not one of the 702 products had any configured, so the section had never been on screen for
   anybody to notice. The feature being dark hid its own styling gap. */
.upsell-card a { color: inherit; text-decoration: none; display: block; }
.upsell-card a:hover h4 { text-decoration: underline; }
.badge {
  display: inline-block; background: var(--brand-lime-light); color: var(--brand-dark-green);
  font-size: 0.7rem; padding: 0.15rem 0.5rem; border-radius: 999px; margin-top: 0.4rem;
}
/* #variant-select used to be a <select> and this rule drew its box. It is a <fieldset> of pills now
   (see .option-pill further down), and a fieldset's own border and padding are reset there. */

/* The filter panel's container: the pill row, then the tags row, stacked. Its 32px of air above and
   below are the live bar's own (`.filters-420 { margin: 32px 0 }`, 24px on the phone), which is
   what puts the first card at y=220 under a 40px bar. The pills themselves are styled with the
   panel, further down ("The catalog filter panel"). */
.filters { display: block; margin: 32px 0; }
@media (max-width: 767px) { .filters { margin: 24px 0; } }

/* Round-ended, like every control on 420.com. Measured off the live "Shop all": #1a391f, white,
   border-radius 9999px. Kept a little larger than 14px here because these are the page's own
   primary actions (Add to Cart, Place Order) rather than a section's secondary link. */
button.primary {
  background: var(--brand-dark-green); color: var(--brand-white); border: none;
  padding: 0.8rem 1.6rem; border-radius: var(--radius-pill); font-weight: 600; cursor: pointer; font-size: 1rem;
}
button.primary:hover:not(:disabled) { background: #245029; }
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }

/* A control that reads as a link but IS a button. The admin had eleven of these written as
   `<a onclick=...>` with no href, which looks right and is unusable without a mouse: an anchor
   with no href is not focusable, gets no keyboard activation, and is announced as plain text
   rather than as a control — so "Manage variants", "Adjust stock" and the staff promote/deactivate
   actions were mouse-only. A real <button> fixes all of that for free, including the focus ring
   above, and this class puts the link's appearance back on top.
   Underlined rather than colour-only, so it doesn't rely on colour vision to read as a control. */
button.linklike {
  background: none; border: none; padding: 0; margin: 0; font: inherit; text-align: left;
  color: var(--brand-dark-green); text-decoration: underline; cursor: pointer;
}
button.linklike:hover { color: var(--brand-black); }

.cart-row { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid var(--brand-lime-light); }
.cart-total { font-size: 1.3rem; font-weight: 700; margin-top: 1rem; text-align: right; }

form.checkout-form { display: grid; gap: 0.75rem; max-width: 480px; }
/* SELECT TOO. The State field became a <select> (a state the shop cannot ship to is unenterable
   that way) and inherited none of this, so on a phone it sat 24px tall between 43px text fields —
   half the height, a third of the tap target, and the one control on the form that decides tax and
   whether the order can be placed at all. Measured by scripts/qa-interact.mjs on 12 Sep 2026. */
form.checkout-form input, form.checkout-form select { padding: 0.65rem; border-radius: var(--radius); border: 1px solid var(--brand-lime-light); background: #fff; min-height: 44px; }
form.checkout-form input[type="checkbox"], form.checkout-form input[type="radio"] { min-height: 0; padding: 0; }

.error-banner { background: #fdecea; color: #a41c1c; padding: 0.75rem 1rem; border-radius: var(--radius); margin: 1rem 0; }
.notice-banner { background: var(--brand-lime-light); color: var(--brand-dark-green); padding: 0.75rem 1rem; border-radius: var(--radius); margin: 1rem 0; }

/* ---------------------------------------------------------------------------
   Accessibility.
   
   Not a checkbox exercise here: the person who runs this store day to day is
   nearly blind, so the admin — which shares this stylesheet — has to work at
   large text sizes, under a screen reader, and from the keyboard alone. The
   storefront gets the same treatment because customers are no different.
   --------------------------------------------------------------------------- */

/* A visible focus ring on EVERYTHING focusable. Browsers ship one and designs
   routinely remove it; nothing here removes it, and this makes it unmissable
   rather than a faint default. Without it, keyboard and low-vision users have
   no idea where they are on the page. :focus-visible keeps it from appearing
   on ordinary mouse clicks, where it would just look like a bug. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--brand-dark-green);
  outline-offset: 2px;
  border-radius: 2px;
}
/* On the dark header the green ring would be invisible against its own
   background, so invert it there. */
header.site-header a:focus-visible { outline-color: var(--brand-lime); }

/* Let someone jump straight past the navigation to the content. Hidden until
   focused, so it costs sighted users nothing and saves keyboard users from
   tabbing through the header on every single page. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--brand-dark-green);
  color: var(--brand-white);
  padding: 0.75rem 1rem;
  z-index: 100;
  border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus {
  left: 0;
}

/* Visually hidden, but still read aloud. For labels that would be redundant to
   a sighted user next to an obvious control, and for live regions that only
   exist to be announced. Deliberately not `display: none` — that removes it
   from the accessibility tree too, which defeats the point. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Inputs inherit the page's font size rather than the browser's much smaller
   form default, so raising text size in the OS or browser actually enlarges the
   fields someone has to type into — not just the prose around them. */
input, select, textarea, button {
  font-family: inherit;
  font-size: 1rem;
}

/* Honour "reduce motion" rather than assuming everyone wants animation. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Descriptions are plain TEXT, not HTML — the Shopify importer converts Body (HTML) rather than
   storing markup, because rendering migrated HTML would mean an XSS surface across the whole
   catalog (see scripts/import-shopify-csv.mjs). pre-line is what makes the paragraph and list
   breaks that conversion preserves actually show as line breaks instead of collapsing into one
   run-on block. */
.product-description { white-space: pre-line; max-width: 60ch; }
/* THE ONE-LINE SUMMARY ABOVE THE CLAMPED DESCRIPTION — products.short_description (migration 0075),
   which the live page prints as .pdp-420__description-short over its description accordion.

   Slightly larger and heavier than the body copy under it, because of the job it does: the paragraph
   below is CLIPPED, so this is the only complete sentence about the product a shopper reads without
   pressing anything. It is a summary, not a heading, so it stays a weight short of one.

   The same 60ch measure as the description it introduces, so the two read as one block rather than as
   two columns of different widths. */
.product-description-short {
  max-width: 60ch;
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1.5;
  margin: 0 0 0.6rem;
}

/* Visible field labels. Used on checkout and the account forms, where a placeholder alone leaves a
   filled-in form as a column of values with nothing saying which is which — you can't check your
   own address before paying, and magnified or low-vision reading makes that worse, not better. */
label.field { display: block; }
label.field > span {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--brand-dark-green);
  margin-bottom: 0.2rem;
}
label.field > input, label.field > select { width: 100%; }
/* A secondary note inside a field's label — what happens if you leave it blank, what format is
   expected. This belongs in the label rather than the placeholder: a placeholder disappears the
   moment someone starts typing, which is exactly when "blank falls back to the product title"
   stops being visible and starts being the thing they needed to know.

   Kept at #444 rather than a lighter grey: this text carries real information, and the low-vision
   reading this codebase is built for is the case where a "subtle" hint stops being readable at all.
   #444 on the cream background is roughly 9:1, comfortably past WCAG AAA. */
label.field > span > .note { font-weight: 400; color: #444; }
/* The same secondary note outside a label.field — e.g. beside a checkbox, where the guidance
   ("resale certificate on file") matters just as much but there is no field label to sit inside. */
.note { font-weight: 400; color: #444; }

/* `.muted` — the SAME thing under the name the storefront's pages actually write.
 *
 * It was never defined here, and nothing broke loudly: a class that does not exist styles nothing,
 * so rewards.html's explanatory lines, reviews.html's, js/reviews.js's and the account page's
 * rewards note were all rendering as ordinary body copy, indistinguishable from the headings'
 * subject matter around them. That is the exact failure mode this project keeps meeting — correct
 * markup, no error anywhere, and the intended result simply absent.
 *
 * Same #444 as .note and for the same reason spelled out above it: this is secondary text, not
 * decorative text, and a lighter grey stops being readable for the low-vision case this codebase is
 * built for. An alias rather than a second colour, so the two names can never drift apart. */
.muted { font-weight: 400; color: #444; }

/* Collection chips — how a Shopify store is browsed. Wraps rather than scrolling horizontally, so
   every collection is reachable without a gesture some visitors can't make. */
#collection-nav { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* THE CARD TITLE'S SIZE IS LIVE'S AT BOTH ENDS: 16px on a phone (`.product-card-420__product-title`
   at 390 and up to 767), 20px from the tablet breakpoint where the grid goes to three columns.
   Ours was 1.05rem (16.8px) everywhere — smaller than live on a desktop and LARGER than live on a
   phone, and on the phone that difference decided whether a name survived the two-line clamp:
   with 16.8px text in a 163px card the preview clipped 9 of the 16 titles on /collections/all?page=2
   ("Litty Rainbow Cotton Candy All-…", with the 1g that tells it from the 5g gone) against live's
   4 of 16 in 16px on 175px (the third QA round, 13 Sep 2026; the two battery results both read
   "420.COM CARTRIDGE..." at 390). The card width is the other half of that fix — see
   main.catalogue-main further down. Margins are the live title wrapper's: 4px on a phone, 8px up.
   RE-MEASURED 14 Sep 2026 (round 10) after round 9 overrode this with the wrapper's 17px/28px/400
   and a 3.3em reserve: live's leaf title — p.product-card-420__product-title on the grid,
   p.product-carousel__product-title in the PDP carousel and the home strip, one stylesheet rule
   each — is 20px/24px weight 500 rgb(26,57,31) with a 48px two-line reserve at 1280, 16px/19.2px
   with 38.4px at 390, and the <p> itself has no margin; the 8px/4px live here belong to its
   div.product-card-420__title-wrapper / .product-carousel__title-wrapper (margin 8px 0 at 1280,
   4px 0 at 390, measured on both). A tester reading the <p> alone sees "no margins" — the wrapper
   carries them, and without them live's 508px carousel card and 428px grid card do not add up
   (308 + 12 + 28 + 8 + 48 + 8 + 28 + 16 + 52 = 508). The line-height and the reserve are in the
   clamp rule above (1.2 and 2.4em: 24px and 48px of 20px type), the weight and the green in the
   heading exception at the top of this sheet. */
.product-card-title { margin: 4px 0; font-size: 16px; }
@media (min-width: 768px) { .product-card-title { margin: 8px 0; font-size: 20px; } }

/* Standing note on the login page for customers migrated from the old store. Styled as guidance,
   not an error: nothing has gone wrong, there is simply one step to do once. */
.migrated-note {
  background: var(--brand-lime-light);
  border-left: 4px solid var(--brand-dark-green);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  max-width: 480px;
}

/* The site footer (js/footer.js), in 420.com's own shape.
 *
 * It is the ONLY navigation to the policies, which is why it is on every page rather than just the
 * home page: a customer looking for the returns policy is usually already deep in the shop, and a
 * payment processor's reviewer checks that the policies are reachable from anywhere on the site.
 *
 * MEASURED OFF THE LIVE FOOTER (www.420.com, 13 Sep 2026, 1280 and 390): a cream band (#f5f2eb, the
 * page's own --brand-cream) with everything LEFT-aligned in the content column; the newsletter block
 * on the left with three link columns to its right; the policy row under them as one line; then the
 * FDA and shipping notices, the copyright, and the logo at the bottom right. On the phone it is the
 * same in one column, with the policy links stacked one per line and the logo above the notices.
 * Ours was a white, centred block — the same words in a different shop. */
.site-footer {
  margin-top: 3rem;
  padding: 3rem 12px 2.5rem;
  background: var(--brand-cream);
  color: var(--brand-ink);
  font-size: 0.9rem;
  text-align: left;
}
/* Live's footer runs the home page's column — 1290px with 12px sides at 1280 (its newsletter block
   alone is 624px) — so the 48px heading holds two lines. At 1100px it took three. */
.footer-inner { max-width: 1290px; margin: 0 auto; }
.site-footer nav {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.6rem;
  margin: 2.5rem 0 1.75rem;
}
.site-footer nav a { color: #5f6b60; text-decoration: none; }
.site-footer nav a:hover, .site-footer nav a:focus { text-decoration: underline; }
.site-footer p { margin: 0; }

/* The newsletter and the three link columns. The newsletter takes the width of two columns, which
   is the live proportion (its block is 600px of the 1280 column), and the lists sit to its right. */
.footer-columns {
  display: grid; grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr));
  gap: 2rem 2.5rem; align-items: start;
}
.footer-column h2, .footer-newsletter h2 {
  margin: 0 0 1rem; font-size: 1.15rem; font-weight: 900; letter-spacing: 0.01em;
  text-transform: uppercase; color: var(--brand-dark-green); font-family: var(--font-display);
}
.footer-column ul { list-style: none; margin: 0; padding: 0; }
.footer-column li { margin-bottom: 0.9rem; }
.footer-column a { color: var(--brand-dark-green); text-decoration: none; }
.footer-column a:hover, .footer-column a:focus { text-decoration: underline; }
/* REACH OUT's two links carry their icon inline, in the link's own green. */
.footer-reach a { display: inline-flex; align-items: center; gap: 0.6rem; }
.footer-icon { flex: none; }

/* "KEEP YOUR HEAD HIGH AND YOUR INBOX LIFTED" is a display heading on the live footer — 36px, the
   heavy uppercase face — over a single rounded field with the Subscribe pill set INSIDE its right
   end. */
.footer-newsletter h2 { font-size: clamp(1.5rem, 2.6vw, 2.25rem); line-height: 1.05; max-width: 22ch; margin-bottom: 1.5rem; }
/* LIVE'S PILL, TO THE PIXEL. Its .footer__newsletter-input-wrapper at 1280 is 440×66: padding
   8px 8px 8px 20px inside a 1px rgba(26,57,31,.24) border, a 20px gap, the field 17px on a 28px
   line and the Subscribe a 122×48 pill of 17px/24px at 12px 20px in the green. At 390 the same
   wrapper is 366×50 with 8px 4px 8px 12px of padding, the field 17px on a 20px line and a small
   90×32 Subscribe of 13px/16px at 8px 12px (getComputedStyle on www.420.com, 15 Sep 2026 — the
   twelfth QA round, store-home #4). Ours was 54px tall at both widths with a 16px field and a
   122×42 16px button: visibly shorter than live's on the desktop and its button visibly larger on
   the phone. The phone's 32px button is live's own size; the field beside it is the 50px target.
   The colours are live's: the wash of the border is the brand green at 24%. */
.footer-newsletter form {
  display: flex; align-items: center; gap: 20px; max-width: 440px;
  padding: 8px 8px 8px 20px; background: var(--brand-white);
  border: 1px solid rgba(26, 57, 31, 0.24); border-radius: var(--radius-pill);
}
.footer-newsletter input {
  flex: 1 1 auto; min-width: 0; height: 28px; padding: 0; border: 0; background: transparent;
  font: inherit; font-size: 17px; line-height: 28px; color: var(--brand-dark-green);
}
.footer-newsletter input:focus { outline: none; }
.footer-newsletter form:focus-within { border-color: var(--brand-dark-green); }
.footer-newsletter button {
  flex: none; background: var(--brand-dark-green); color: var(--brand-white); border: 0;
  padding: 12px 20px; border-radius: var(--radius-pill); font: inherit; font-size: 17px;
  line-height: 24px; font-weight: 500; cursor: pointer;
}
@media (max-width: 600px) {
  .footer-newsletter form { padding: 8px 4px 8px 12px; gap: 8px; }
  .footer-newsletter input { height: 20px; line-height: 20px; }
  .footer-newsletter button { padding: 8px 12px; font-size: 13px; line-height: 16px; }
}
.footer-newsletter button:hover, .footer-newsletter button:focus-visible { background: #245029; }
.footer-newsletter-status:empty { display: none; }
.footer-newsletter-status { margin: 0.6rem 0 0; color: var(--brand-dark-green); }

/* The notices on the left, the logo on the right, bottom-aligned as the live footer sets them. */
.footer-bottom { display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem; }
.footer-notices { display: grid; gap: 0.9rem; max-width: 60rem; }
.footer-copyright { font-size: 0.8rem; color: #5f6b60; }
.footer-logo { flex: none; display: block; line-height: 0; }
.footer-logo img { width: 69px; height: 32px; display: block; }

@media (max-width: 900px) {
  .footer-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .footer-newsletter { grid-column: 1 / -1; }
}
@media (max-width: 600px) {
  .site-footer { padding: 2.5rem 1rem 2rem; }
  .footer-columns { grid-template-columns: minmax(0, 1fr); gap: 2rem; }
  .footer-newsletter h2 { font-size: 1.75rem; }
  /* One policy link per line at the phone width, as the live footer stacks them. */
  .site-footer nav { flex-direction: column; gap: 0.75rem; margin: 2rem 0 1.5rem; }
  .site-footer nav a { font-size: 1rem; }
  /* The logo comes ABOVE the notices on the phone, as on the live page, and the column is one wide. */
  .footer-bottom { flex-direction: column-reverse; align-items: flex-start; gap: 1.25rem; }
}

/* Named variant options — a Strain control beside a Size control, the way Shopify shows them.
 * Laid out as a row that wraps, so two or three options stay side by side on a desktop and stack
 * on a phone rather than being squeezed. */
.option-controls { display: flex; flex-wrap: wrap; gap: 1rem; margin: 1rem 0; }
.option-control { display: flex; flex-direction: column; gap: 0.25rem; }
.option-control .option-name { font-size: 0.85rem; font-weight: 600; color: var(--brand-dark-green, #1a4d2e); }
.option-control select {
  padding: 0.6rem; border-radius: var(--radius, 8px);
  border: 1px solid var(--brand-lime-light, #cfe3b8); min-width: 160px; background: #fff;
}
.option-control select option:disabled { color: #999; }

/* The catalog filter panel (js/filters.js) — 420.com's own bar, measured rather than guessed.
 *
 * Read off www.420.com/collections/all with getComputedStyle on 13 Sep 2026:
 *     .filters-420__buttons          flex, wrap, gap 10px
 *     .filters-420__trigger          40px tall, 0 12px, radius 9999, #f5f2eb at rest, Inter 15/500 in #1a391f, gap 6px
 *     .filters-420__trigger--active  #ebe8e1 with a 1px #1a391f inset ring (box-shadow 0 0 0 1px inset)
 *     .filters-420__count            20px circle, #1a391f, white 13/500
 *     .filters-420__popover          200px, radius 12px, shadow 0 10px 40px rgba(0,0,0,.15), 8px under the pill
 *     .filters-420__popover-inner    max-height 248px, scrolls, 8px 0
 *     .filters-420__option           48px rows, 10px 16px, gap 12px, Inter 15 in #1a391f
 *     .filters-420__checkbox         18px, radius 4px, 2px rgba(26,57,31,.3); checked fills #1a391f
 *     .filters-420__popover-footer   8px 16px over a 1px rgba(26,57,31,.12) rule; "Clear" at 13px
 *     .filters-420__selected-tags    flex, wrap, gap 16px, 45px tall
 *     .filters-420__tag              17px, 8px 0, 1px rgba(26,57,31,.16) underline, gap 8px, × of 16px
 *     .filters-420__clear-all        17px/500 in #1a391f, no underline
 * Wraps on a phone rather than squeezing, because this is the control a customer uses most and it
 * has to stay usable one-handed. */
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.filter-dropdown { position: relative; }
.filter-pill {
  display: inline-flex; align-items: center; gap: 6px; height: 40px; padding: 0 12px;
  border: 0; border-radius: var(--radius-pill); background: #f5f2eb; color: var(--brand-dark-green);
  font: 500 15px/28px var(--font-primary); cursor: pointer; white-space: nowrap;
}
/* THE PILL HAS THREE SHADES, AND LIVE USES ONLY TWO OF THEM. Live's four rules, dumped from its
   stylesheet and re-measured on 13 Sep 2026 (the fifth and sixth QA rounds, store-collection-search
   #2 and #1/#2):
       .filters-420__trigger                            #f5f2eb            (rest)
       .filters-420__trigger:hover                      #ebe8e1            (hover, no ring)
       .filters-420__dropdown--open .filters-420__trigger  #ebe8e1 + inset 0 0 0 1px #1a391f  (popover open)
       .filters-420__trigger--active                    #ebe8e1 + the same ring  (a filter in force)
   The first reading took the ACTIVE shade for the rest shade; the fifth round fixed rest and active
   and left hover and open at #e2dfd6 — a darker grey live never paints — with no ring on an open
   pill, so on the preview a pill whose list was showing looked no different from its neighbours
   (the sixth round). js/filters.js puts .is-active on the pill whenever its dimension has a value
   selected and aria-expanded="true" on the one whose popover is open; both draw live's ringed
   state, and hovering an active pill keeps #ebe8e1 because live's hover IS #ebe8e1. */
.filter-pill:hover { background: #ebe8e1; }
.filter-pill.is-active, .filter-pill[aria-expanded="true"] { background: #ebe8e1; box-shadow: inset 0 0 0 1px var(--brand-dark-green); }
.filter-pill.is-active:hover, .filter-pill[aria-expanded="true"]:hover { background: #ebe8e1; }
.filter-pill-count {
  display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px;
  border-radius: 50%; background: var(--brand-dark-green); color: var(--brand-white);
  font-size: 13px; font-weight: 500; line-height: 1;
}
.filter-pill-chevron { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; transition: transform 0.15s; }
.filter-pill[aria-expanded="true"] .filter-pill-chevron { transform: rotate(180deg); }
.filter-popover {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 40; width: 200px;
  background: var(--brand-white); border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
  overflow: hidden;
}
/* Hung from the pill's right edge when the left-aligned box would run off the screen — measured
   after opening (js/filters.js setOpenPill). The live popover overflows the phone; this one does not. */
.filter-popover.is-flipped { left: auto; right: 0; }
.filter-popover-list { max-height: 248px; overflow-y: auto; padding: 8px 0; overscroll-behavior: contain; }
.filter-popover-empty { margin: 0; padding: 10px 16px; font-size: 15px; color: rgba(26, 57, 31, 0.64); }
.filter-option {
  display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 10px 16px;
  font-size: 15px; color: var(--brand-dark-green); cursor: pointer;
}
.filter-option:hover { background: #f5f2eb; }
/* A REAL CHECKBOX, drawn to the live box's measurements — the live control is a <button> with a
   painted square, which a screen reader announces as a button with no state. */
.filter-option input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; flex: none; width: 18px; height: 18px; margin: 0;
  border: 2px solid rgba(26, 57, 31, 0.3); border-radius: 4px; background: transparent; cursor: pointer;
  display: inline-grid; place-content: center;
}
.filter-option input[type="checkbox"]:checked { border-color: var(--brand-dark-green); background: var(--brand-dark-green); }
.filter-option input[type="checkbox"]:checked::after {
  content: ""; width: 5px; height: 9px; margin-top: -2px;
  border: solid var(--brand-white); border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.filter-option-label { flex: 1 1 auto; min-width: 0; }
/* The count beside each choice — this shop's own addition, kept because a choice that states what
   it will leave is the difference between a filter and a guess. Quiet, so it does not compete with
   the name. */
.filter-option-count { font-size: 13px; color: rgba(26, 57, 31, 0.55); font-variant-numeric: tabular-nums; }
.filter-popover-foot { padding: 8px 16px; border-top: 1px solid rgba(26, 57, 31, 0.12); }
.filter-popover-clear {
  background: none; border: 0; padding: 0; font: inherit; font-size: 13px;
  color: var(--brand-dark-green); cursor: pointer;
}
.filter-popover-clear:hover { text-decoration: underline; }
/* The sort control — this shop's, not live's (the live bar has none) — drawn as one more pill at
   the row's end so it reads as part of the same bar. The native arrow is replaced by the same
   chevron the other pills carry. THE SAME SHADES AS THE PILLS BESIDE IT: when the fifth round
   lightened the three pills to live's #f5f2eb rest shade this select stayed at #ebe8e1, which by
   then was the ACTIVE shade, so the bar showed three light pills and one darker one that read as
   "selected" — on a phone it sits directly under them (the sixth QA round, store-collection-search
   #1). Live has no sort control, so the pills beside it are the only reference: #f5f2eb at rest,
   #ebe8e1 under the pointer. */
.filter-sort { position: relative; display: inline-flex; align-items: center; margin-left: auto; }
.filter-sort select {
  appearance: none; -webkit-appearance: none; height: 40px; padding: 0 34px 0 12px;
  border: 0; border-radius: var(--radius-pill); background: #f5f2eb; color: var(--brand-dark-green);
  /* 16px, NOT 15: iPhone Safari zooms the page (~1.07×) when a form control under 16px takes focus
     and leaves it zoomed and side-scrollable after the picker closes; this select was the one
     control on the shop under that line, and it sits directly under the pills — which are 16px
     already — on a phone (QA round 11, phone-sweep #3). Live has no sort control to measure against;
     16px is the pills' own size and the smallest Safari leaves alone. The same size at every width
     so the pill does not change shape between a laptop and a phone. */
  font: 500 16px/28px var(--font-primary); cursor: pointer;
}
.filter-sort select:hover { background: #ebe8e1; }
.filter-sort .filter-pill-chevron { position: absolute; right: 12px; pointer-events: none; color: var(--brand-dark-green); }
@media (max-width: 767px) { .filter-sort { margin-left: 0; } }
/* The ticked choices, repeated under the bar as removable tags with a "Clear all". */
.filter-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 16px; }
.filter-tag {
  display: inline-flex; align-items: center; gap: 8px; padding: 8px 0;
  border-bottom: 1px solid rgba(26, 57, 31, 0.16); font-size: 17px; color: var(--brand-dark-green);
}
.filter-tag-remove {
  display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px;
  padding: 0; border: 0; background: none; color: var(--brand-dark-green); cursor: pointer;
}
.filter-tag-remove svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; }
.filter-clear-all {
  background: none; border: 0; padding: 0; font: 500 17px/28px var(--font-primary);
  color: var(--brand-dark-green); cursor: pointer;
}
.filter-clear-all:hover { text-decoration: underline; }
/* THE EMPTY GRID WHEN FILTERS LEFT NOTHING — live's own rule, read off the theme's inline CSS on
   14 Sep 2026 (round 8): `.product-grid-420__empty { text-align: center; padding: var(--space-xl4)
   var(--space-base); grid-column: 1 / -1 }` — ONE full-width centred block, the title over the
   sentence, 48px of air above and below and 16px at the sides, at every width. Two earlier rounds
   had it as two columns (title left, sentence right), measured off a page where the two happened to
   sit in the first two product columns; on a phone that put "NO PRODUCTS FOUND" wrapped in a 175px
   column beside a three-line sentence. Spans the whole grid so it is never squeezed into a card. */
.grid-empty { grid-column: 1 / -1; display: block; text-align: center; padding: 48px 16px; }
/* 16px under the title is live's own (`__empty-title { margin: 0 0 var(--space-base) 0 }`). */
.grid-empty-title { margin: 0 0 16px; font-size: 24px; line-height: 1.3; color: var(--brand-dark-green); }
.grid-empty-text { margin: 0; font-size: 17px; line-height: 28px; color: rgba(0, 0, 0, 0.6); }
/* THE SEARCH RESULTS PAGE'S OWN EMPTY STATE (index.html) — live's `.search-420__no-results`: 48px
   16px of air, everything centred, the display face at 24px, the sentence at 17px in 60% black,
   and "View Collections" as a 44px cream pill. */
.search-no-results { grid-column: 1 / -1; padding: 48px 16px; text-align: center; }
.search-no-results-title { margin: 0 0 16px; font-size: 24px; line-height: 1.3; color: var(--brand-dark-green); }
.search-no-results-text { margin: 0 0 24px; font-size: 17px; line-height: 28px; color: rgba(0, 0, 0, 0.6); }
/* A WORD THAT CANNOT BREAK IS BROKEN ANYWAY — the heading and the no-results sentence both quote
   what was typed, and what was typed can be a pasted product link or a 49-letter code: one
   unbreakable "word" that ran out of the page, clipped at the right edge and panning a 390 phone
   sideways (h1 scrollWidth 510–524 in a 366px box; document 522–536 in a 390 viewport; the sentence
   487 in 334; a 1344-wide page at 1280 — QA round 12, collections-search #1). Measured on live on
   14 Sep 2026: its results title (.search-420__results-title) carries word-break: break-word and
   stays 366 / 1280 wide with the same link and the same 49 letters; its no-results paragraph does
   NOT (word-break normal, scrollWidth 487 in 334, document 515 at 390) and pans exactly as ours
   did — the one place this page is deliberately better than the reference, because a page that
   scrolls sideways is a page the owner cannot read. overflow-wrap: anywhere rather than the
   legacy word-break value: the same break, and the min-content width is allowed to shrink with
   it, which is what keeps the grid's columns honest under the heading. */
h1.search-heading, .search-no-results-text { overflow-wrap: anywhere; }
.search-no-results-link {
  display: inline-flex; align-items: center; height: 44px; padding: 8px 16px; border-radius: var(--radius-pill);
  background: #f0e8df; color: var(--brand-dark-green); font-size: 15px; line-height: 28px; text-decoration: none;
}
.search-no-results-link:hover { background: #e8dfd3; }
/* The results heading, in the live page's own colour: `.search-420__results-title` is the display
   face at 48px in the brand green, where every other h1 on this site is near-black. */
h1.search-heading { color: var(--brand-dark-green); font-size: 32px; line-height: 1.3; margin: 0 0 24px; }
@media (min-width: 768px) { h1.search-heading { font-size: 48px; } }
/* NOTHING SEARCHED YET (bare /search) — live's `.search-420__empty-search`: a block padded 48px 16px,
   everything centred, the h1 `__empty-search-title` at 24px in the brand green with 16px under it,
   the `__empty-search-text` at 17/28 in 60% black. The h1 is the page's own #page-heading (so the
   document keeps one h1) and the sentence sits in the grid, so the 48px is split across the two.
   THE BLOCK DOES NOT START AT THE HEADER. Live's main keeps the same room above this block that it
   keeps above a results heading — 48px at 1280, 24px on the phone — so the heading's text sits
   96px / 72px under the header (measured 14 Sep 2026: header 116 → block 164 → h1 212 at 1280;
   155 → 179 → 227 at 390); ours started the 48px padding at the header and the heading floated
   48px too high (the seventh QA round, store-collection-search #3). The same 24/48 the results
   heading takes (main.catalogue-main h1.search-heading, further down), as a margin. And the
   sentence sits 16px under the heading, not 40: the grid this cell lives in carries 24px of
   margin-top for a grid of cards, which is nothing to keep above one sentence. */
h1.search-empty-title { margin: 24px 0 0; padding: 48px 16px 0; text-align: center; color: var(--brand-dark-green); font-size: 24px; line-height: 1.3; }
@media (min-width: 768px) { h1.search-empty-title { margin-top: 48px; } }
main.catalogue-main:has(.search-empty) .product-grid { margin-top: 0; }
.search-empty { grid-column: 1 / -1; padding: 16px 16px 48px; text-align: center; }
.search-empty-text { margin: 0; font-size: 17px; line-height: 28px; color: rgba(0, 0, 0, 0.6); }

/* ---- Home page merchandising rows (js/home-sections.js) ---------------------------- */
/* A heading, a horizontal strip of products, and a way to see the rest — the shape almost every
   section on 420.com's own home page reduces to. */
/* LIVE'S MEASURE, not a smaller one: the "Just Dropped" heading is the same 48px/48px display face
   the two grids above it use (30px/32px on a phone), with 32px (24px) under the heading row and no
   rule beneath it — ours drew it at 21.6px over a lime hairline (the third side-by-side pass,
   13 Sep 2026, home/measurements.txt). The section sits 32px under the spotlight and 64px over the
   bundle band, which is where live's carousel sits. */
.home-row { margin: 32px 0 64px; }
.home-row-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  margin-bottom: 32px;
}
.home-row-head h2 { margin: 0; font-size: 48px; line-height: 48px; color: var(--brand-dark-green); }
/* On 420.com this is a filled pill sitting on the section heading's baseline — "Shop all",
   "Shop All" — not a text link. 8px/16px, 14px, weight 500, white on #1a391f, 44px tall (the 28px
   line the theme gives every button, which is also what makes it a thumb-sized target). */
.home-row-more {
  background: var(--brand-dark-green); color: var(--brand-white);
  padding: 8px 16px; border-radius: var(--radius-pill);
  font-weight: 500; font-size: 14px; line-height: 28px; white-space: nowrap; text-decoration: none;
}
.home-row-more:hover { background: #245029; }
/* "Shop All" and the prev/next pair sit together at the right of the heading row, as on the live
   "Just Dropped" strip. The arrows are the shared .home-carousel-nav (hidden by js/home-sections.js
   when the strip fits), stripped of the top margin it carries under the spotlight. */
.home-row-controls { display: flex; align-items: center; gap: 0.75rem; flex: none; }
.home-row-controls .home-carousel-nav { margin-top: 0; }
.home-row-blurb { margin: -0.5rem 0 1rem; color: #4a5c4d; font-size: 0.95rem; }

/* Scrolls sideways rather than wrapping, which is what a carousel is and what stops one long row
   from pushing the rest of the page below the fold. scroll-snap so it comes to rest on a card
   instead of halfway through one. */
/* A FIXED card width, not 1fr. With 1fr the column is a share of the container, so a strip's card
   size depended on how many products were in it: the eight-product row drew 158px cards and the
   two-product "Battery" row drew 470px ones directly beneath it — the same card, three times the
   size, on the same screen. A one-product promotion rendered a single billboard. A carousel's cards
   are one size; how many there are decides how far it scrolls, not how big anything is. */
/* WHY EVERY HORIZONTAL SCROLLER ON THIS PAGE IS `position: relative`, which looks like a line
   somebody added for no reason and is the fix for the worst layout bug on the home page.

   A scroll container clips its in-flow content. It does NOT clip an absolutely positioned
   descendant unless it is itself that descendant's containing block — and a container is only
   that if it is positioned. Every product card in these strips, and every spotlight slide, carries
   `<span class="visually-hidden">Regular price </span>` beside a struck-through price, and
   .visually-hidden is `position: absolute`. With an unpositioned track those spans were laid out
   against an ancestor outside the scroller and escaped its clipping.

   What that looked like, measured in a browser against the real catalogue: the spotlight's five
   slides made `document.documentElement.scrollWidth` 4,650px on a 1,600px viewport. The whole page
   could be dragged sideways into an empty white field three screens wide — and on a phone that is a
   page that wanders off under your thumb. `position: relative` on the track: 4,650px → 1,600px.

   It is applied to ALL of them, not only the spotlight, because the others differ from it by luck
   rather than by construction: the Care Packages row already holds five of those spans today and is
   simply not yet the widest thing on the page. */
.home-row-strip {
  display: grid; grid-auto-flow: column; grid-auto-columns: 220px;
  justify-content: start;
  gap: 8px; overflow-x: auto; scroll-snap-type: x proximity;
  padding-bottom: 0.5rem; -webkit-overflow-scrolling: touch;
  position: relative;
}
.home-row-strip > .product-card { scroll-snap-align: start; }
/* THE LIVE CARD IS 308px WIDE with an 8px gap, at every width from 768 up (measured on the live
   .product-carousel__card at 768, 1024, 1280, 1440 and 1920 on 13 Sep 2026): four whole cards
   across the 1280px column and the edge of a fifth. The 250px this used to be was sized for the
   1100px column this page no longer has (see main.home-page); the card a shopper compares directly
   against the one they remember is now the same size. */
@media (min-width: 768px) {
  .home-row-strip { grid-auto-columns: 308px; }
}

/* The strip is keyboard-scrollable and must SHOW that it is focused — a scroll container that can
   be tabbed to with no visible focus ring is a trap for anyone not using a mouse. */
.home-row-strip:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 3px; }

/* Two across on a phone with the edge of a third showing, as live's 167px cards sit in the 366px
   column at 390 (12px + 167 + 8 + 167 leaves 16px of the next card). */
@media (max-width: 767px) {
  .home-row-strip { grid-auto-columns: calc((100% - 24px) / 2); }
}

/* The announcement bar — the shop's own copy, above everything. Absent entirely when there is
   nothing to say, rather than an empty strip. */
/* Measured on the live bar rather than guessed: the text is the DISPLAY face at 900/18px, uppercase,
   in the brand green, on the page's own white — not black on a lime strip, which is what this was.
   43px tall on the live site. */
.announcement-bar {
  background: var(--brand-white);
  color: var(--brand-dark-green);
  text-align: center;
  /* Live's message is padded 10px over and under at 18px on a 1.3 line (its .announcement-bar__message:
     23.4px lines, min-height 38px) — one line is 44px with the hairline, two are 68 (measured at
     1280 and 390, 15 Sep 2026). This was 0.6rem on the face's own line height: 48px at 1280. */
  padding: 10px 3rem;
  font-family: var(--font-display);
  font-size: 18px;
  line-height: 1.3;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  border-bottom: 1px solid var(--brand-hairline);
}
header.site-header .announcement-bar a { color: var(--brand-dark-green); text-decoration: underline; }
/* THE SLIDE ITSELF IS NOT UNDERLINED — the live bar's text is plain bold capitals with an arrow after
   it, underlined only on hover. The rule above is for links written INTO a message; it out-ranked the
   slide rule below and underlined every slide (seen in the owner's screenshot, 12 Sep 2026). */
header.site-header .announcement-bar a.announcement-slide { text-decoration: none; }
header.site-header .announcement-bar a.announcement-slide:hover .announcement-message,
header.site-header .announcement-bar a.announcement-slide:focus-visible .announcement-message { text-decoration: underline; }
/* The previous/next chevrons at the bar's two ends, and the arrow after a linked message — the live
   bar's furniture. The chevrons sit inside the bar's own side padding so the message stays centred. */
.announcement-bar { position: relative; }
.announcement-nav {
  position: absolute; top: 0; bottom: 0; width: 2.75rem; border: 0; background: transparent;
  color: var(--brand-dark-green); cursor: pointer; padding: 0; display: flex; align-items: center;
  justify-content: center; opacity: 0.7;
}
.announcement-nav:hover, .announcement-nav:focus-visible { opacity: 1; }
.announcement-prev { left: 0; }
.announcement-next { right: 0; }
.announcement-arrow { margin-left: 0.45rem; vertical-align: middle; }
/* THE BAR IS AS TALL AS ITS LONGEST MESSAGE, AND NEVER JUMPS — which is how live's bar behaves: its
   slides sit side by side in one flex track, so the tallest sets the height of the strip whatever is
   showing. Ours hid every slide but one with display:none, so the bar stood 48, 76 or 104px tall as
   the messages rotated at 390 and the whole page under it jumped every five seconds (13 Sep 2026);
   the answer then was a FIXED 91px on the phone, live's height on the day its longest slide ran to
   three lines. Live took that slide down and its bar shrank to 68px — two lines — while ours stayed
   91 with 12px of white over and under the words, and every page started 23px lower than live's
   (QA round 13, 15 Sep 2026). Fixed heights measure a message that will be gone next week.
   So: every slide is laid in the SAME grid cell, the hidden ones kept in the layout but invisible
   (visibility, not display), and the cell is the height of the tallest. The bar is content-sized like
   live's — 44px for a row of one-liners, 68 when one wraps to two at 390, 91 again the day somebody
   writes a three-liner — and the page under it never moves. The messages do not need clamping any
   more: nothing overflows a bar that is the size of its longest message. */
.announcement-bar { min-height: 44px; box-sizing: border-box; }
.announcement-slides { display: grid; align-items: center; width: 100%; min-width: 0; }
.announcement-slide { grid-area: 1 / 1; display: block; max-width: 100%; }
/* The slide keeps its box so the cell stays the tallest slide's height, and visibility takes it out
   of view, out of the accessibility tree and out of the tab order exactly as display:none would.
   !important because the first rule of this file makes [hidden] display:none !important everywhere —
   rightly, and this is the one element that has to stay in the layout while hidden; the attribute
   still means "not the message showing", which is what js/header.js toggles. */
.announcement-slide[hidden] { display: block !important; visibility: hidden; }
/* Live's phone column is 292px wide on a 390px screen — 49px each side, its 44px chevron inside a
   20px page gutter — so the messages break onto their second line at the same words as live's. */
@media (max-width: 600px) {
  .announcement-bar { padding: 10px 49px; }
}
/* Somebody who has asked for less motion is shown every message at once, stacked, instead of a strip
   that changes under them every five seconds. */
/* Each slide is now its own element, because each one links somewhere different — the live bar's six
   slides go to six destinations. The rule below has to target the SLIDE: targeting the inner label
   would stack every message inside a link that is still hidden. */
.announcement-slide { color: inherit; }
a.announcement-slide { text-decoration: none; }
a.announcement-slide:hover .announcement-message,
a.announcement-slide:focus-visible .announcement-message { text-decoration: underline; }
@media (prefers-reduced-motion: reduce) {
  /* Out of the shared grid cell and one under another: every message is shown at once here. */
  .announcement-slide { display: block; grid-area: auto; }
  .announcement-message { display: inline; }
  .announcement-nav { display: none; }
  .announcement-bar { height: auto; min-height: 44px; padding: 10px 1rem; }
}

/* ---- Reviews (js/reviews.js) ------------------------------------------------------ */
/* THE PRODUCT PAGE'S OWN COLUMN, NOT A NARROWER ONE. Live's Yotpo widget is 1,216px wide at x=32 on
   a 1280 screen: the page's 1290px/16px container (main.product-page above, .pdp-420__container on
   live) with the reviews section's own 16px inside it. Ours drew the reviews in a 1100px/24px box
   under a 1290px/16px product — the one block on the page in a different column — so the widget was
   1,052 at x=114, the cards 272 against live's 321, and "Was this review helpful?" wrapped to two
   lines on every card (QA round 14, 15 Sep 2026; measured on blue-rizz-premium-thcp-flower-1g).
   Same max-width as main.product-page, 32px of padding for live's 16 + 16. */
.product-reviews { max-width: 1290px; margin: 0 auto; padding: 0 32px 3rem; }
/* LIVE'S 16px GUTTER FOR THE REVIEWS ON A PHONE, which the rest of this product page already keeps
   (main.product-page is 0 16px at every width). 24px applied at 390 too, so Yotpo's widget was
   342px wide at x=24 and — its own .yotpo-base-layout adding 17px each side — the review cards
   308px at x=41 against live's 329 at x=30; "Was this review helpful?" wrapped to two lines where
   live's is one (QA round 13, 15 Sep 2026; measured on blue-rizz-premium-thcp-flower-1g). Round 14
   measured this holding (358 at x=16, the helpful line 22px tall) and left it alone. */
@media (max-width: 767px) {
  .product-reviews { padding-inline: 16px; }
}
.reviews-summary { display: flex; gap: 2.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1.5rem; }
.reviews-average { text-align: center; }
.reviews-average-value { margin: 0.25rem 0 0; font-size: 1.1rem; font-weight: 700; }
.stars { color: #c8a300; letter-spacing: 0.08em; font-size: 1.15rem; }
.reviews-bars { flex: 1 1 260px; min-width: 220px; }
.reviews-bar-row { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.25rem; font-size: 0.85rem; }
.reviews-bar-label { width: 2.2rem; color: #4a5c4d; }
.reviews-bar { flex: 1; height: 8px; background: #e6e0d4; border-radius: 999px; overflow: hidden; }
.reviews-bar-fill { display: block; height: 100%; background: var(--brand-lime); }
.reviews-bar-count { width: 2rem; text-align: right; color: #4a5c4d; font-variant-numeric: tabular-nums; }
.reviews-empty { color: #4a5c4d; }
.review { border-top: 1px solid #e6e0d4; padding: 1rem 0; }
.review-title { margin: 0.3rem 0; font-size: 1rem; }
.review-meta { margin: 0.2rem 0; font-size: 0.9rem; color: #4a5c4d; }
/* "Verified purchase" is the one claim on this page a shopper is entitled to trust — the server
   decides it from order history — so it is given real emphasis rather than a grey footnote. */
.review-verified {
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 700; margin-left: 0.5rem;
}
.review-body { margin: 0.4rem 0 0; white-space: pre-wrap; }
.review-reply { margin: 0.75rem 0 0 1.25rem; padding-left: 0.9rem; border-left: 3px solid var(--brand-lime-light); }
.review-reply-who { margin: 0 0 0.2rem; font-weight: 700; font-size: 0.85rem; }
.review-form-wrap { margin: 1.5rem 0; }
.review-form { display: grid; gap: 0.75rem; max-width: 520px; margin-top: 1rem; }
.review-form label { display: grid; gap: 0.25rem; font-weight: 600; font-size: 0.9rem; }
.review-form input, .review-form select, .review-form textarea {
  padding: 0.5rem; border: 1px solid #cdcdcd; border-radius: var(--radius); font: inherit;
}

/* ---- Home page TEXT hero (js/home-sections.js hero(), driven by store settings) ------------
   Absent entirely when the shop has said nothing — a hero with placeholder text looks like a page
   somebody forgot to finish, which is worse than no hero at all.
   LIVE'S SHAPE, measured on www.420.com in a headless browser on 15 Sep 2026 (its hero_slider
   section, which since 14 Sep is a text slide rather than a banner): a full-bleed band, 800px tall
   at 1280 (the 700px picture column plus 60px over and 40px under), its .content a row of two
   halves 40px apart, padded 60px 20px 40px; the copy left-aligned in the left half — the kicker in
   the display face at clamp(24px, 2.4vw, 30px) 600 white, 0.5px tracking, line-height 1.05; the
   heading at clamp(40px, 4.2vw, 105px) 700 in its accent yellow #e8ff3c, line-height 1.3, 6px
   under the kicker, a 30px yellow glow at 15%; four 90px-wide bullets 34px apart 42px under it
   (a 40px 1.6-stroke icon, 15px + 10px, then 16px/500 caps in white); two 250px-min buttons 18px
   apart 44px under those (16px/700, 16px 32px, radius 30px — solid yellow on #0b0f0c, and outlined
   1px white on nothing); the picture beside it 580x700, contained. Under 1200 the picture column is
   500px and the buttons 200px; under 1024 the row stacks and centres (40px over, the buttons 180px
   and 10px apart, the bullets 15px apart); under 767 the bullet words are 14px; under 500 the
   buttons are 165px at 15px, 12px 13px, and the picture 300px tall (390: the band runs 132–969).
   The ground is the setting's colour (live's photograph is near-black) under the setting's
   photograph, cover, centred — both custom properties written by hero(), each reduced to a hex
   colour or a percent-encoded URL first. Full-bleed and flush with the header and the trust strip
   the same way the banner is (margin-inline: calc(50% - 50vw), margin-block -1.5rem 0). */
.home-hero {
  --hero-bg: #05070a;
  display: block;
  margin-inline: calc(50% - 50vw);
  width: 100vw; max-width: 100vw;
  margin-block: -1.5rem 0;
  background-color: var(--hero-bg);
  background-image: var(--hero-bg-image, none);
  background-size: cover; background-position: center; background-repeat: no-repeat;
  color: #f5f7f5;
  overflow: hidden;
}
.home-hero-inner {
  display: flex; flex-direction: column; align-items: center; gap: 40px;
  padding: 40px 20px; text-align: center;
}
.home-hero-copy { width: 100%; max-width: 100%; }
.home-hero-kicker {
  margin: 0; font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(24px, 2.4vw, 30px); line-height: 1.05; letter-spacing: 0.5px; color: #f5f7f5;
}
.home-hero-heading {
  margin: 6px 0 0; font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(40px, 4.2vw, 105px); line-height: 1.3; color: #e8ff3c;
  text-shadow: 0 0 30px rgba(232, 255, 60, 0.15);
}
.home-hero-sub { margin: 16px auto 0; max-width: 44rem; color: #f5f7f5; font-size: 17px; line-height: 1.5; }
.home-hero-bullets {
  list-style: none; margin: 42px 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 15px; justify-content: center;
}
/* 90px columns that SHRINK to share a phone's width, as live's do (its .feature is width: 90px in a
   row that does not wrap, so at 390 the four measure 73–86px): from a 70px basis they grow to 90 and
   no further, and four of them fit a 350px row; a longer row of six wraps rather than squeezing every
   word into 46px. */
.home-hero-bullet {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 15px;
  flex: 1 1 70px; max-width: 90px; min-width: 0;
  font-size: 14px; font-weight: 500; line-height: 1.3; text-transform: uppercase; letter-spacing: 0; color: #f5f7f5;
}
/* No margin under the icon: live's rule adds 10px under an svg, but its icons are <img>s, which get
   none — its bullets measure 91px at 390 and 97 at 1280 with the 15px gap alone. */
.home-hero-bullet-icon { width: 40px; height: 40px; flex: none; }
.home-hero-ctas { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 44px; justify-content: center; }
.home-hero-cta {
  display: inline-block; box-sizing: border-box; min-width: 165px; text-align: center;
  padding: 12px 13px; border-radius: 30px; border: 2px solid transparent;
  /* A 24px line box at either size — live's buttons measure 52px at 390 and 60 at 1280. */
  font-family: var(--font-primary); font-size: 15px; line-height: 24px; font-weight: 700; text-decoration: none;
  background: #e8ff3c; color: #0b0f0c;
  transition: transform 0.15s ease, background 0.2s ease;
}
.home-hero-cta:hover, .home-hero-cta:focus-visible { transform: translateY(-2px); background: #f4ff6e; text-decoration: none; }
.home-hero-cta-outline { background: transparent; color: #fff; border: 1px solid #fff; }
.home-hero-cta-outline:hover, .home-hero-cta-outline:focus-visible { background: transparent; border-color: #fff; }
/* The picture beside the copy is CONTAINED in a box of live's height — 300px under 501, 500 to 1200,
   700 from 1201 (its .right_image min-heights, background-size: contain) — not shown at its own
   height, which at 390 ran the band 130px past live's. */
.home-hero-side { width: 100%; }
.home-hero-side img { display: block; width: 100%; max-width: 100%; height: 300px; object-fit: contain; object-position: center; }
@media (min-width: 501px) {
  .home-hero-cta { min-width: 180px; padding: 14px 20px; font-size: 16px; }
  .home-hero-side img { height: 500px; }
}
@media (min-width: 768px) {
  .home-hero-bullet { font-size: 16px; }
}
@media (min-width: 1025px) {
  .home-hero-inner { padding: 60px 20px 40px; }
  /* Two halves from here, the copy left-aligned — but only when there IS a picture beside it: with
     none the words stay centred across the band rather than hugging the left half of an empty one. */
  .home-hero-has-side .home-hero-inner { flex-direction: row; align-items: center; justify-content: space-between; text-align: left; }
  .home-hero-has-side .home-hero-copy { min-width: 50%; max-width: 50%; }
  .home-hero-has-side .home-hero-sub { margin-inline: 0; }
  .home-hero-has-side .home-hero-bullets, .home-hero-has-side .home-hero-ctas { justify-content: flex-start; }
  .home-hero-bullets { gap: 20px; }
  .home-hero-ctas { gap: 18px; }
  .home-hero-cta { min-width: 200px; padding: 16px 22px; }
  .home-hero-side { width: 50%; }
}
@media (min-width: 1201px) {
  .home-hero-bullets { gap: 34px; }
  .home-hero-cta { min-width: 250px; padding: 16px 32px; }
  .home-hero-side img { height: 700px; }
}
/* The band is wider than 1400px on a very wide screen only up to live's own .content cap of 85%. */
@media (min-width: 1401px) {
  .home-hero-inner { max-width: 85%; margin: 0 auto; }
}

/* ---- Blog (blog.html, article.html) ------------------------------------------------ */

/* THE BLOG INDEX IS A COLLAGE, NOT A LIST — and this is the live site's own rule, transcribed.
   Ours was a single column of rows, each one a 160×110 thumbnail floated left of a headline. The
   live index (www.420.com/blogs/news, .blog-articles--collage in its section-main-blog.css) is a
   two-column grid of large cards in which EVERY THIRD CARD FROM THE FIRST spans the full width and
   centres its text — and so does a last card that would otherwise sit alone on a row. With live's
   six articles per page that reads: one wide hero, two side by side, another wide hero, two more.
   The selectors and the 750px breakpoint are live's. The gaps are live's too, measured on
   13 Sep 2026 (third round): 8px between the two columns and 63px between rows at 1280 — live's
   card keeps 35px of its own under the excerpt and the grid adds 28px — and 35px between the
   stacked cards on a phone. The card's text is left-aligned under its picture (below); only the
   wide card from 750px centres it, which is live's own collage rule. */
.blog-articles { display: grid; gap: 35px; }
@media (min-width: 750px) {
  .blog-articles { grid-template-columns: 1fr 1fr; gap: 63px 8px; }
  .blog-articles--collage > *:nth-child(3n + 1),
  .blog-articles--collage > *:nth-child(3n + 2):last-child { grid-column: span 2; text-align: center; }
}
.article-card h2 a { color: var(--brand-dark-green); text-decoration: none; }
.article-card h2 a:hover { text-decoration: underline; }
.article-card-media { display: block; }
/* LIVE PINS EVERY CARD PICTURE TO A HEIGHT AND CROPS TO FILL — Dawn's article-card rule, against
   its 10px root: the half-width card is 22rem (220px) on a phone and 30.7rem (307px) from 750px,
   the hero 44rem then 55rem at 990px. Measured on www.420.com/blogs/news with getBoundingClientRect
   on 15 Sep 2026: 366×220 for every card at 390, 604×307 for the two-column cards and 1216×550 for
   the hero at 1280. Ours drew the half-width cards at the pictures' own 1200×628 ratio — 366×192 on
   the phone, 604×316 beside each other — so each phone card was 28px shorter than live's and the
   six-card page 291px shorter (the twelfth QA round, store-content-account #7). The img's
   width/height attributes still describe the file; the height here is the box, so the space is
   reserved even when a picture 404s. */
.article-thumb {
  display: block; width: 100%; height: 220px;
  /* SQUARE, as live draws every card picture on the index (border-radius 0px on all three measured
     at 1280 and 390, 13 Sep 2026 — the seventh QA round, store-content-account #1). This carried the
     shop's card radius, which the blog-faces decision (docs/ADMIN-PARITY-2026-09-12.md, round 4)
     never covered: that decision was about typefaces, not shape or spacing. */
  object-fit: cover; border-radius: 0;
}
/* The hero card's picture is TALLER still — live pins it to 44rem, then 55rem at 990px, and crops
   to fill. 440px and 550px. */
@media (min-width: 750px) {
  .article-thumb { height: 307px; }
  .blog-articles--collage > *:nth-child(3n + 1) .article-thumb,
  .blog-articles--collage > *:nth-child(3n + 2):last-child .article-thumb { height: 440px; }
}
@media (min-width: 990px) {
  .blog-articles--collage > *:nth-child(3n + 1) .article-thumb,
  .blog-articles--collage > *:nth-child(3n + 2):last-child .article-thumb { height: 550px; }
}
.article-meta { margin: 0 0 0.4rem; color: #4a5c4d; font-size: 0.85rem; }
.article-excerpt { margin: 0; color: #3d4a3f; }
.article { max-width: 46rem; margin: 0 auto; }
/* THE HERO IS FULL-BLEED AND SITS ABOVE THE HEADLINE on the live article page, so it is not inside
   the .article reading measure — see article.html. 130rem and the 33/66/82.5rem heights are live's
   own (.article-template__hero-container / __hero-large in its section-blog-post.css). */
.article-hero-container { max-width: 1300px; margin: 0 auto 1.5rem; }
/* SQUARE AND FLUSH UNDER THE HEADER. Live's hero has no radius (0px at 1280 and 390) and its top
   edge IS the header's bottom edge (116/116 at 1280, 155/155 at 390, measured 14 Sep 2026); ours was
   rounded 10px and sat 24px lower, on the top padding every <main> carries. main.article-main below
   drops that padding on this one page — the seventh QA round, store-content-account #1, which the
   sixth round had already named and nobody owned. */
.article-hero { display: block; width: 100%; height: 330px; object-fit: cover; border-radius: 0; }
@media (min-width: 750px) { .article-hero { height: 660px; } }
@media (min-width: 990px) { .article-hero { height: 825px; } }
main.article-main { padding-top: 0; }
/* THE BLOG INDEX RUNS AT LIVE'S PAGE WIDTH, NOT THE 1100px EVERY OTHER <main> KEEPS. Live's
   .main-blog is a Dawn .page-width: 32px of side padding, 36px above and below, so at 1280 the
   heading and the grid are 1216px wide (hero card 1216×550, paired cards 604×307, "News" at x=32);
   ours sat in the 1100px main (1052 inner) — 164px narrower with 82px more margin each side (the
   seventh QA round, store-content-account #5). On the phone live's page-width is 12px a side with
   27px above (cards 366 wide at 390 against our 342), and "News" keeps 40px of margin there where the
   desktop keeps 20 (measured 14 Sep 2026: h1 top 172 = 116 + 36 + 20 at 1280; 222 = 155 + 27 + 40 at
   390). Capped at 1280 so wider screens keep the same 1216 the shop's photographs were sized for. */
main.blog-main { max-width: 1280px; padding: 36px 32px; }
@media (max-width: 749px) { main.blog-main { padding: 27px 12px; } }
/* The numbered pagination live draws under the index (its .pagination__list), centred, with the
   current page held in the row as plain text rather than a link to itself. LIVE'S 44×44 ITEMS:
   every `.pagination__item` on www.420.com/blogs/news measures 44×44 at 390 and at 1280 (re-measured
   13 Sep 2026). Ours were 36×36 — under the 40px floor every other phone control on this site keeps
   (the grid pager went to 40 in the fifth round; this pager is drawn from a different rule and was
   missed — the sixth QA round, store-phone-sweep #1).
   AND THE ROW WRAPS, AS LIVE'S DOES. The 44px items were checked against the six-item row of pages 1
   and 6 (284px) and not against pages 2 to 5, which draw EIGHT — an arrow each end — and at 44px
   with a 4px gap that row is 380px: wider than a 390 screen's 358px column, with the arrows flush to
   the screen edges, and wider than a 360 screen, where "←" lost its left 10px off the screen and the
   whole document scrolled sideways (scrollWidth 370 against 360; the seventh QA round,
   store-phone-sweep #1, a regression of the fix above). Live's .pagination__list is flex-wrap: wrap
   with its items on a 54px stride (44 + 10) and no row gap: on page 3 at 390 and at 360 it draws
   "‹ 1 2 3 4 5" on one 44px row and "6 ›" on a second, never wider than the screen (measured 14 Sep
   2026). Same stride here so the row breaks in the same place. */
.pagination-list {
  list-style: none; margin: 2rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 0 10px;
}
.pagination-item, .pagination-arrow, .pagination-gap {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; height: 44px; padding: 0 0.4rem;
  color: var(--brand-dark-green); text-decoration: none; border-radius: var(--radius);
}
.pagination-item:hover, .pagination-arrow:hover { background: var(--brand-lime-light); }
.pagination-item-current { font-weight: 700; text-decoration: underline; text-underline-offset: 4px; }
/* The article body is sanitised markup (infra/blog.js), so it carries real headings and links and
   needs to read like prose rather than like a form. */
.article-body { line-height: 1.7; }
.article-body h2 { margin-top: 2rem; font-size: 1.35rem; }
.article-body h3 { margin-top: 1.5rem; font-size: 1.1rem; }
.article-body img { max-width: 100%; height: auto; border-radius: var(--radius); }
.article-body a { color: var(--brand-dark-green); }
/* "Back to blog", which live centres under the article with the arrow turned back on itself. The
   face, size, margins and colour are the article block's rules above (live's measured figures);
   this keeps only the centring. */
.article-back { display: flex; justify-content: center; }
@media (max-width: 600px) {
  .article-hero { height: 220px; }
}

/* ---- The cart drawer (js/cart-drawer.js) -------------------------------------------
   The live site keeps you on the page when you add something: a panel slides in from the right with
   what you now have and a Check out button. Reproduced here for the reason in that file's header —
   navigating away to the cart page after every add is a different shop to use, and it costs the
   second item people would otherwise have bought. */
.cart-drawer-scrim {
  /* ABOVE THE PRODUCT PAGE'S STICKY BUY BAR, which is z-index 800.
     The drawer opens automatically on every add to cart, and on a product page the sticky bar is
     exactly where the drawer's own Checkout and View cart controls sit — so at 90/91 the bar was
     drawn ON TOP of the open drawer and could cover the two controls the shopper had just been sent
     there to use. Seen on the deployed site and confirmed by a Codex pass.
     970/971 sat above the sticky bar and the mobile drawer (950/960) and below the age gate.

     AND ABOVE KLAVIYO'S SIGN-UP POP-UP, which is a fixed full-screen dialog at z-index 90000
     (data-kl-scroll-locking-modal; read off the deployed preview's DOM, 13 Sep 2026). It fires six
     to eight seconds after the first page load — exactly when a shopper who added something at
     once is looking at their open basket — and at 971 it was drawn OVER the drawer: the panel
     dimmed behind the form's backdrop and every press on +, −, the bin, an upsell's Add,
     "Checkout • $24.00" and the × landed on the pop-up until its own × was found (QA round 6,
     store-cart-checkout #1). Live's drawer (UpCart, z 2147483640) stays above the same form and the
     form is drawn behind the panel; so now does this one. The launcher rule further down still
     hides the teaser, the Smile pill and the Tidio bubble while the drawer is open — those float
     in the millions and are hidden rather than out-stacked.

     THE STACK, from the top: the age gate (90100 — it must stay over everything, the drawer
     included, so it moved up with the drawer), this drawer (90010/90011), the search drawer
     (90005/90006), the mobile menu (90003/90004), the bundle builder's OPEN sheet (90001,
     css/bundle.css), Klaviyo's pop-up (90000), the header's dropdowns (900), the sticky buy bar and
     the bundle builder's CLOSED bar (800). Round 6 moved only the drawer and the gate; round 7 lifted
     the other three panels over the pop-up too (phone-sweep #2 — the form landed on an open search,
     an open menu and an open bundle sheet exactly as it had on the drawer), keeping their order
     among themselves. Everything that is not a panel a person has opened stays under the pop-up,
     as the rest of the page does. */
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); z-index: 90010;
}
.cart-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(400px, 100%);
  background: var(--brand-white); z-index: 90011;   /* one above its own scrim — see the note there */
  display: flex; flex-direction: column;
  box-shadow: -2px 0 16px rgba(0, 0, 0, 0.18);
}
/* The page behind must not scroll under an open drawer — on a phone that is the difference between
   a drawer and a page that has visibly come apart. */
body.cart-drawer-open { overflow: hidden; }

.cart-drawer-head {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 1rem 1.25rem; border-bottom: 1px solid #e6e0d4;
}
.cart-drawer-head h2 { margin: 0; font-size: 1.05rem; color: var(--brand-dark-green); }
.cart-drawer-close {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  font-size: 1.6rem; line-height: 1; color: var(--brand-dark-green); padding: 0 0.25rem;
}
.cart-drawer-body { flex: 1; overflow-y: auto; padding: 0.5rem 1.25rem; }
.cart-drawer-empty { color: #4a5c4d; padding: 1.5rem 0; }

/* The cart page's empty state. With the discount box, the totals and the Checkout button now hidden
   there is nothing left on the page but this, so it has to carry the shopper back to the shop
   rather than leaving them on a blank screen with a text link. */
.cart-empty { color: #4a5c4d; margin: 1.5rem 0 0.75rem; font-size: 1.05rem; }
/* The same button as everywhere else: dark green, white, fully round-ended. It was lime with black
   text, which is not a control 420.com has — its primary buttons are all #1a391f. */
.primary-link {
  display: inline-block; background: var(--brand-dark-green); color: var(--brand-white);
  padding: 0.7rem 1.4rem; border-radius: var(--radius-pill); font-weight: 600; text-decoration: none;
}
.primary-link:hover { background: #245029; }

/* Three things across, then the remove link under them: what it is, how many, what that costs.
   The line total sits in its own column rather than wrapping under the quantity, so a basket of
   several items reads as a column of prices the eye can add up. */
/* ONE LINE OF THE BASKET, LAID OUT AS THE LIVE DRAWER LAYS IT: the picture at the left, the name
   with its price and quantity stepper beneath, and the bin at the top right. It used to put the
   stepper and a line total BESIDE the name, which squeezed a product name into a 110px column and
   wrapped "420.COM CARTRIDGE BATTERY - BLACK - 510 THREADED" onto four lines (the owner's side-by-side of
   the two drawers, 12 Sep 2026). */
.cart-drawer-line {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.25rem 0.85rem;
  align-items: start; padding: 1rem 0; border-bottom: 1px solid #efeadf;
}
.cart-drawer-thumb {
  width: 84px; height: 84px; object-fit: contain; border-radius: 6px;
  border: 1px solid #efeadf; background: var(--brand-white); display: block;
}
/* `contain`, not `cover`: these are packshots on a plain background, and cropping one to fill a
   square cuts the top off the box the customer is trying to recognise. */
.cart-drawer-thumb-empty { background: #f4f2ec; }
.cart-drawer-line-main { min-width: 0; }
.cart-drawer-line-title { margin: 0; font-weight: 700; font-size: 0.95rem; line-height: 1.3; }
/* The title and the picture are links to the product, as live's drawer's are (QA round 10, cart #2):
   the title in its own ink with no underline until hovered — live's title link is bold black text —
   and the picture's anchor a block so it takes the 84px square and nothing else. */
.cart-drawer-line-link { color: inherit; text-decoration: none; }
.cart-drawer-line-link:hover { text-decoration: underline; }
.cart-drawer-thumb-link { display: block; }
/* 14px, live's size for this line (0.95rem = 15.2px wrapped "(Save $75.00)" as "(Save" / "$75.00)"
   in the 213px the line has at 390 — QA round 9, cart #3); the save token itself is kept whole by
   .cart-drawer-save below. */
.cart-drawer-line-price { margin: 0.3rem 0 0; color: var(--brand-ink); font-size: 14px; }
.cart-drawer-each { color: #4a5c4d; font-size: 0.8rem; }
.cart-drawer-line-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.5rem; }
.cart-drawer-line-foot .cart-drawer-code { margin-left: auto; padding: 0.2rem 0.55rem; font-size: 0.75rem; }
.cart-drawer-qty { display: inline-flex; align-items: center; gap: 0; border: 1px solid #cfc9bb; border-radius: var(--radius); }
.cart-drawer-qty button {
  width: 30px; height: 30px; border: 0; border-radius: 0;
  background: var(--brand-white); cursor: pointer; font-size: 1rem; line-height: 1; padding: 0;
}
.cart-drawer-qty span { min-width: 30px; text-align: center; font-size: 0.9rem; }
/* THE "+" AT ITS CEILING (js/cart-drawer.js, learnLineCeilings): dimmed and read as unavailable
   through aria-disabled rather than `disabled`, so it still takes the focus a redraw hands back to
   it; the reason is the line under the stepper — "Only 4 available" / "Up to 99 per order" — in the
   note face the drawer already uses, beside the control a phone is looking at (QA round 11, home #1). */
.cart-drawer-qty button[aria-disabled="true"] { opacity: 0.4; cursor: default; }
.cart-drawer-line-limit { margin: 0.35rem 0 0; color: #4a5c4d; font-size: 0.8rem; }
.cart-drawer-remove {
  background: none; border: 0; padding: 0.15rem; cursor: pointer; line-height: 0;
  color: #4a5c4d; align-self: start;
}
.cart-drawer-remove:hover, .cart-drawer-remove:focus-visible { color: var(--brand-ink); }

.cart-drawer-foot { padding: 1rem 1.25rem 1.25rem; border-top: 1px solid #e6e0d4; }
.cart-drawer-total { display: flex; justify-content: space-between; font-size: 1.05rem; }
.cart-drawer-note { margin: 0.35rem 0 0.9rem; color: #4a5c4d; font-size: 0.8rem; }
/* DARK GREEN, LIKE EVERY OTHER PRIMARY BUTTON ON THIS SHOP.
   The same fix .primary-link above already carries, and the same reason: 420.com's primary controls
   are all #1a391f, and its live cart drawer's Checkout button is dark green. This one stayed lime
   with black text — the one button on the shop that did not match, on the last screen before
   somebody pays. The owner spotted it comparing the two carts side by side.
   The two share a rule and are never on screen together: Checkout shows with a basket, Continue
   shopping only when it is empty. */
.cart-drawer-checkout, .cart-drawer-continue {
  display: block; width: 100%; text-align: center; box-sizing: border-box;
  background: var(--brand-dark-green); color: var(--brand-white);
  padding: 0.75rem 1rem; border: 0; border-radius: var(--radius);
  font: inherit; font-weight: 700; text-decoration: none; cursor: pointer;
}
/* The same hover .primary-link uses, so the two primary buttons behave identically. */
.cart-drawer-checkout:hover, .cart-drawer-continue:hover { background: #245029; }
/* Subtotal and discount above the estimated total, when there is a discount to show. Quieter than
   the total, which is the number that matters. */
.cart-drawer-subtotal, .cart-drawer-discount { font-size: 0.9rem; color: #4a5c4d; margin-bottom: 0.2rem; }
.cart-drawer-discount { color: var(--brand-dark-green); font-weight: 600; }
/* The code that produced the saving, as a chip — the live cart shows it beside the money so the
   shopper can see WHICH of their codes landed. */
.cart-drawer-code {
  display: inline-block; margin-left: 0.35rem; padding: 0.05rem 0.4rem;
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  border-radius: 9999px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.02em;
}
/* A LINE THE PROMOTION TOUCHED — the live drawer's own shape: "$14.99" struck through in grey, the
   charged price in ink, "(Save $7.49)" in green, and the promotion's tag on its own line under it.
   The struck figure is the FULL line price so the three numbers on one line add up. */
.cart-drawer-was, .cart-row-was, .summary-was { color: #6b7a6e; text-decoration: line-through; margin-right: 0.25rem; }
/* nowrap: "(Save $75.00)" drops to the next line as one piece or not at all — never "(Save" alone
   at the end of a line with "$75.00)" under it (QA round 9, cart #3; live keeps the token whole). */
.cart-drawer-save, .cart-row-save { color: var(--brand-dark-green); font-weight: 650; white-space: nowrap; }
.cart-row-tag {
  display: inline-block; margin-left: 0.35rem; padding: 0.05rem 0.4rem;
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  border-radius: 9999px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.02em;
}

/* "YOU MAY ALSO LIKE", in the drawer — the shape the live 420.com cart uses: picture, name, stars,
   price, and a one-press Add on the right. */
.cart-drawer-upsells:empty { display: none; }
.cart-drawer-upsells { margin-top: 1.25rem; border-top: 1px solid #e6e0d4; padding-top: 0.9rem; }
.cart-drawer-upsells-title {
  margin: 0 0 0.6rem; font-size: 0.95rem; font-weight: 700; color: var(--brand-ink);
}
.cart-drawer-upsell {
  display: grid; grid-template-columns: auto 1fr auto; gap: 0.7rem;
  align-items: center; padding: 0.55rem 0; border-bottom: 1px solid #f2efe8;
}
.cart-drawer-upsell:last-child { border-bottom: 0; }
.cart-drawer-upsell-thumb {
  width: 46px; height: 46px; object-fit: contain; border-radius: 6px;
  border: 1px solid #efeadf; background: var(--brand-white); display: block;
}
.cart-drawer-upsell-main { min-width: 0; }
.cart-drawer-upsell-title {
  display: block; font-size: 0.85rem; font-weight: 600; color: var(--brand-ink);
  text-decoration: none; line-height: 1.2;
  /* Two lines, then ellipsis — a 60-character vape name would otherwise push the Add button off
     the bottom of a row that is meant to be scannable. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.cart-drawer-upsell-title:hover { text-decoration: underline; }
.cart-drawer-upsell-stars { margin: 0.15rem 0 0; font-size: 0.75rem; color: var(--brand-lime); letter-spacing: 0.5px; }
/* THE PARTIAL STAR — live's strip's own construction (UpCart's star--partial): a one-em box holding
   the outline glyph in the flow, with the filled glyph laid over it from the left and clipped to the
   rating's fraction by an inline width (stars() in js/cart-drawer.js writes it). So 4.8 is four
   full stars and a fifth filled four-fifths of the way; rounding drew five (QA round 6,
   store-cart-checkout #5). letter-spacing is reset inside the box so the overlay and the outline
   start at the same x. */
.cart-drawer-upsell-star-partial { display: inline-block; position: relative; width: 1em; overflow: hidden; letter-spacing: 0; vertical-align: bottom; }
.cart-drawer-upsell-star-partial > span:first-child { position: absolute; top: 0; left: 0; overflow: hidden; display: inline-block; }
.cart-drawer-upsell-reviews { color: #4a5c4d; }
.cart-drawer-upsell-price { margin: 0.15rem 0 0; font-size: 0.85rem; font-weight: 700; color: var(--brand-ink); }
/* The compare-at struck beside a reduced suggestion's price — "$400.00 $200.00" on the Vape Care
   Package, as live's strip prints it ("Original price $400.00 Sale price $200.00"). The same grey
   and line-through as a discounted line's .cart-drawer-was; lighter weight so the price the shopper
   pays stays the figure they read first. The checkout's strip shares the rule (QA round 6,
   store-product #1 and store-cart-checkout #2). */
.cart-drawer-upsell-was, .checkout-upsell-was { color: #6b7a6e; font-weight: 400; text-decoration: line-through; margin-right: 0.15rem; }
.cart-drawer-upsell-add {
  background: var(--brand-dark-green); color: var(--brand-white); border: 0;
  border-radius: var(--radius); padding: 0.4rem 0.85rem;
  font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer; white-space: nowrap;
}
.cart-drawer-upsell-add:hover { background: #245029; }

.cart-drawer-viewcart {
  display: block; text-align: center; margin-top: 0.6rem;
  color: var(--brand-dark-green); font-size: 0.9rem;
}

/* ---- The search drawer (js/header.js) ----------------------------------------------
   What the magnifier opens: 420.com's right-hand search drawer, measured on the live site with
   getComputedStyle on 13 Sep 2026 and laid out to those numbers:
       .search-drawer                 fixed, right 0, 560px wide at 1280 and the full screen at 390,
                                      white, shadow 0 10px 20px rgba(23,10,64,.08)
       desktop close                  40px at (1216,24)  -> 24px from the top and right
       .search-drawer__input-container 56px pill (48px on the phone), #f5f2eb, 0 8px 0 24px, gap 8px
       .search-drawer__input          Inter 16px (17 on the phone) in #1a391f, no chrome of its own
       .search-drawer__submit-btn     40px circle, #1a391f, white glyph
       .search-drawer__section-title  display face 24px in #1a391f, uppercase
       .search-drawer__pill           36px, 6px 12px, #f5f2eb, Inter 17/500, gap 8px between
       .search-drawer__all-results    17px in rgba(26,57,31,.64), 8px 0, icon + text at gap 8px
       .search-drawer__product-link   96px rows, 8px, gap 20px, radius 8px; 80px thumb on #f5f2eb r8
       .search-drawer__product-name   17/500 in #1a391f, one line, ellipsis; type 15px at 64%
       .search-drawer__view-all-btn   48px pill, #1a391f, white 17/500, full width, 24px padding around
   The page behind is dimmed and does not scroll, Escape closes, focus lands in the field.
   ABOVE KLAVIYO'S SIGN-UP POP-UP (z 90000), as live's search drawer is (2147483647): at 970/980 the
   form came up over a person mid-search — "battery" typed, results loaded, the "Warehouse Clearance"
   form on top of them (QA round 7, phone-sweep #2, ix-390-search-drawer.png). 90005/90006 keeps the
   old order — over the menu drawer (90003/90004), under the cart drawer (90010/90011), which opens
   over this when something is added from the results; the whole stack is written out at
   .cart-drawer-scrim. */
.search-backdrop { position: fixed; inset: 0; z-index: 90005; background: rgba(0, 0, 0, 0.5); }
.search-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 90006;
  width: min(100vw, 560px);
  background: var(--brand-white); color: var(--brand-dark-green);
  display: flex; flex-direction: column;
  box-shadow: 0 10px 20px rgba(23, 10, 64, 0.08);
}
/* `hidden` spelled out, because `display: flex` above would otherwise beat the user-agent rule and
   leave the drawer open over every page from first paint — the same trap the menu drawer names. */
.search-drawer[hidden], .search-backdrop[hidden] { display: none; }
body.search-open { overflow: hidden; }
.search-drawer-head { display: flex; justify-content: flex-end; padding: 24px 24px 0; }
.search-drawer-close {
  display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px;
  padding: 0; border: 0; background: none; border-radius: var(--radius-pill);
  color: var(--brand-dark-green); cursor: pointer;
}
.search-drawer-close:hover { background: #f2f1ec; }
.search-drawer-close svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.header-search { padding: 12px 24px 24px; }
.search-field {
  display: flex; align-items: center; gap: 8px; height: 56px; padding: 0 8px 0 24px;
  border-radius: var(--radius-pill); background: var(--brand-cream);
}
/* The focus ring goes on the pill, not the field inside it: the live page draws a rectangular
   outline on the inner <input>, inside a round container, which is the browser's default and not a
   design. Same colour and weight, on the shape the eye reads as the control — and ONLY when the
   drawer was opened from the keyboard. The drawer focuses the field the moment it opens, so a rule
   on plain :focus-within ringed the pill in every drawer anybody ever opened with the mouse, where
   live's field shows no ring after a pointer-initiated open (its input's outline computes to none
   there; the fifth QA round, store-collection-search #3). :focus-visible cannot tell the two apart
   — a text input matches it however focus arrived — so js/header.js records how the magnifier was
   pressed on the drawer (data-opened-by="pointer" | "keyboard") and clears it on Tab; the ring is
   drawn unless the record says pointer. */
.search-drawer:not([data-opened-by="pointer"]) .search-field:focus-within { outline: 2px solid var(--brand-dark-green); outline-offset: -2px; }
.search-field input {
  flex: 1 1 auto; min-width: 0; height: 100%; padding: 0; border: 0; background: none; outline: none;
  color: var(--brand-dark-green); font: 400 16px/1.4 var(--font-primary);
}
.search-field input::placeholder { color: rgba(26, 57, 31, 0.64); }
.search-field input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.search-clear {
  display: inline-flex; align-items: center; justify-content: center; flex: none; width: 24px; height: 24px;
  padding: 0; border: 0; background: none; color: var(--brand-dark-green); cursor: pointer;
}
.search-clear svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.search-submit {
  display: inline-flex; align-items: center; justify-content: center; flex: none; width: 40px; height: 40px;
  padding: 0; border: 0; border-radius: var(--radius-pill);
  background: var(--brand-dark-green); color: var(--brand-white); cursor: pointer;
}
.search-submit:hover { background: #245029; }
.search-submit svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* The results area scrolls; the drawer, the field and the foot do not move. */
.search-suggestions { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 24px 24px; }
.search-section-title {
  margin: 0 0 24px; font-family: var(--font-display); font-weight: 900; font-size: 24px; line-height: 1;
  text-transform: uppercase; letter-spacing: 0.01em; color: var(--brand-dark-green);
}
.search-pills { display: flex; flex-wrap: wrap; gap: 8px; }
/* BUTTONS, as live's pills are: a click fills the field and runs the lookup inside the drawer (see
   wireSearch in js/header.js). The <a> form is kept in the selector so a page that still emits one
   looks the same. Font and border reset because a <button> brings its own. */
header.site-header .search-pills a,
header.site-header .search-pills button {
  display: inline-flex; align-items: center; height: 36px; padding: 6px 12px; border: 0; border-radius: var(--radius-pill);
  background: var(--brand-cream); color: var(--brand-dark-green); font: 500 17px/1 var(--font-primary); text-decoration: none; cursor: pointer;
}
header.site-header .search-pills a:hover,
header.site-header .search-pills button:hover { background: #ebe8e1; }
header.site-header .search-all-results {
  display: flex; align-items: center; gap: 8px; padding: 8px 0; margin-bottom: 24px;
  font-size: 17px; color: rgba(26, 57, 31, 0.64); text-decoration: none;
}
header.site-header .search-all-results:hover { color: var(--brand-dark-green); }
.search-all-results svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }
.search-suggestions-empty { margin: 0; font-size: 17px; color: rgba(26, 57, 31, 0.64); }
.search-results { list-style: none; margin: 0; padding: 0; }
header.site-header .search-results a {
  display: flex; align-items: center; gap: 20px; height: 96px; padding: 8px; border-radius: 8px;
  color: var(--brand-dark-green); text-decoration: none;
}
header.site-header .search-results a:hover,
header.site-header .search-results a:focus { background: var(--brand-cream); }
.search-result-thumb {
  display: flex; align-items: center; justify-content: center; flex: none; width: 80px; height: 80px;
  border-radius: 8px; background: var(--brand-cream); overflow: hidden;
}
.search-result-thumb img { display: block; width: 80px; height: 80px; object-fit: cover; }
.search-result-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.search-result-title { font-size: 17px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-result-type { font-size: 15px; color: rgba(26, 57, 31, 0.64); }
.search-drawer-foot { padding: 24px; }
header.site-header .search-view-all {
  display: flex; align-items: center; justify-content: center; height: 48px; border-radius: var(--radius-pill);
  background: var(--brand-dark-green); color: var(--brand-white); font-size: 17px; font-weight: 500; text-decoration: none;
}
header.site-header .search-view-all:hover { background: #245029; }
/* The phone: the whole screen, the close at the LEFT under 12px of padding, a 48px field, and the
   same 12px gutter everywhere — the live drawer's own 390px layout. */
@media (max-width: 940px) {
  .search-drawer-head { justify-content: flex-start; padding: 12px; }
  .header-search { padding: 12px 12px 24px; }
  .search-field { height: 48px; }
  .search-field input { font-size: 17px; }
  .search-suggestions { padding: 0 12px 24px; }
  .search-drawer-foot { padding: 12px; }
}

/* ---- Home: shop by category / shop by effect (js/home-sections.js) -----------------
   The two blocks the live home page leads with. Tiles rather than a list because they are the
   answer to "what do you sell?" from somebody who has just arrived, and a list of eight links in
   body text is something the eye slides straight past. */
/* 48px OVER AND UNDER, as live's .image-grid-section is padded (32px on a phone); the margin this
   used to carry was a third of that, and two consecutive blocks sat 32px apart where live's sit 96. */
.home-tiles { margin: 0; padding: 48px 0; }
/* "SHOP BY CATEGORY" / "SHOP BY EFFECT" are full section headings on the live site — 48px/48px in
   the display face, weight 900, uppercase, in the BRAND GREEN (rgb 26,57,31), 30px/32px on a phone.
   This rule once said 24px and claimed live did too; live measures 48px at 1280 and 30px at 390
   (getComputedStyle on .image-grid__title, 13 Sep 2026, home/measurements.txt), and the page at 24px
   read at half live's scale. Measure, do not trust a comment. */
.home-tiles h2 {
  margin: 0; font-size: 48px; line-height: 48px; font-weight: 900;
  letter-spacing: 0.01em; text-transform: uppercase; color: var(--brand-dark-green);
}
/* THE HEADING ROW, which is a row and not just a heading: every one of the three live blocks puts a
   "Shop all" pill opposite its title, and ours had no way out of the block at all. Wraps rather than
   squeezing, so a long heading on a narrow screen drops the pill to its own line instead of
   crushing it — and the pill keeps the live one's shape: filled, fully rounded, small. 32px under
   it (24px on a phone), live's .image-grid__header margin. */
.home-tiles-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; flex-wrap: wrap; margin: 0 0 32px;
}
/* THE PILL IS THE BRAND GREEN, 85x44: live's .image-grid__shop-all is rgb(26,57,31) at 14px/500,
   8px/16px with a 28px line — the same button as "Shop All" beside Just Dropped. Ours was near-black
   (#121212) at 33px tall from 768px up (the phone sweep at 1024, 13 Sep 2026). */
.home-tiles-all {
  flex: none; padding: 8px 16px; border-radius: 9999px;
  background: var(--brand-dark-green); color: #fff;
  font-size: 14px; line-height: 28px; font-weight: 500; text-decoration: none; white-space: nowrap;
}
.home-tiles-all:hover, .home-tiles-all:focus { background: #245029; }
/* THE PHONE'S "Shop all" IS A FULL-WIDTH BUTTON UNDER THE TILES, and the pill in the heading row is
   gone — which is exactly what live does with its two links (.image-grid__shop-all, desktop only,
   and .image-grid__shop-all-mobile: block, 100% wide, 12px/16px, 16px text, hidden from 768px up).
   Measured at 390 on 13 Sep 2026: a 366x52 dark button under the ten category tiles, under the six
   effect photographs and under the brand strip; ours kept the small pill top-right at every width.
   Same 768px breakpoint as live's, so the two pages change over at the same width. */
.home-tiles-all-mobile { display: none; }
@media (max-width: 767px) {
  .home-tiles-all { display: none; }
  .home-tiles-all-mobile {
    display: block; width: 100%; box-sizing: border-box; margin-top: 1rem;
    padding: 12px 16px; border-radius: 9999px;
    background: var(--brand-dark-green); color: var(--brand-white);
    font-size: 16px; line-height: 28px; font-weight: 500; text-align: center; text-decoration: none;
  }
  .home-tiles-all-mobile:hover, .home-tiles-all-mobile:focus { background: #245029; }
}
/* THE BRAND STRIP (tileStrip in js/home-sections.js): live's brand row is a one-row carousel, six
   tiles in view from 768px up and two on a phone (its carousel-420__item--cols-6 / --mobile-cols-2,
   each `calc((100% - gaps) / n)`), with the arrows beside "Shop All" in the heading. Ours was a static
   2x6 grid that ran 1,252px tall at 390. The pastel tiles are the same .home-tile as the grids; only
   the container changes. Positioned, for the reason spelled out on .home-row-strip. */
.home-tiles-controls { display: flex; align-items: center; gap: 0.75rem; flex: none; }
.home-tiles-controls .home-carousel-nav { margin-top: 0; }
.home-tile-strip {
  display: grid; grid-auto-flow: column;
  grid-auto-columns: calc((100% - 5 * 0.75rem) / 6);
  gap: 0.75rem; overflow-x: auto; scroll-snap-type: x proximity;
  scrollbar-width: none; padding-bottom: 0.25rem; -webkit-overflow-scrolling: touch;
  position: relative;
}
.home-tile-strip::-webkit-scrollbar { display: none; }
.home-tile-strip:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 4px; }
.home-tile-strip > .home-tile { scroll-snap-align: start; }
@media (max-width: 1000px) { .home-tile-strip { grid-auto-columns: calc((100% - 3 * 0.75rem) / 4); } }
@media (max-width: 767px) { .home-tile-strip { grid-auto-columns: calc((100% - 0.75rem) / 2); } }
/* HOW MANY ACROSS IS PER-BLOCK, because it is per-block on the live site: its category grid is five
   wide, its effect grid six, its brand carousel six at a time. One number for all three left the
   sixth effect group ("Charge up") alone on a row of its own, which reads as a layout fault rather
   than a design. Falls back to fewer columns on smaller screens rather than squeezing six in. */
/* AN 8px GAP AND LIVE'S COLUMN COUNTS AT LIVE'S BREAKPOINT. Live keeps five category tiles and six
   effect photographs across from 768px all the way up (measured 147px tiles at 768, 198 at 1024,
   250 at 1280, 252 at 1440 and 1920) and switches to three and two below it; ours dropped to four
   and three on the way down, so a tablet drew a different grid from live's. */
.home-tile-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
.home-tile-grid-6 { grid-template-columns: repeat(6, 1fr); }
/* ON A PHONE: three category tiles across and two effect photographs across, which is what the live
   page draws at 390px (its category grid is 3 × 117px cards, its effect grid 2 × 179px photos). The
   category grid was two across here, so ten tiles ran to five rows where the live page fits them in
   four — and Batteries, the tenth, was a screen lower than a shopper expects it. */
@media (max-width: 767px)  {
  .home-tile-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .home-tile-grid-6 { grid-template-columns: repeat(2, 1fr); gap: 8px; }
}
/* 420.com's category tiles are tall, soft-pastel cards with the name set at the top — not centred
   white boxes with a hairline. The colours cycle so a shop that gains a category gets a tile in the
   same family rather than a hole in the pattern; nothing here names a category, exactly as the
   ranking in js/home-sections.js names none. */
.home-tile {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  min-height: 190px; padding: 1.1rem 1rem 0;
  background: #eaf3dc; border: 0; overflow: hidden;
  border-radius: 14px; color: var(--brand-heading);
  font-weight: 600; font-size: 1.05rem; text-decoration: none;
  transition: transform 0.12s ease;
}
.home-tile-label { display: block; }
/* The product shot sits under the name and fills the bottom of the card, as on the live tiles.
   `contain` because these are packshots on their own white ground — cropping them to fill would cut
   the product. min-height 0 lets the image shrink inside the flex column instead of overflowing. */
.home-tile-img {
  margin-top: auto; width: 100%; min-height: 0; max-height: 130px;
  object-fit: contain; mix-blend-mode: multiply;
}
/* A category with no photograph keeps the centred word it had, rather than a card with a hole. */
.home-tile-plain { justify-content: center; padding-bottom: 1.1rem; }
.home-tile:nth-child(5n+2) { background: #dff1ef; }
.home-tile:nth-child(5n+3) { background: #f3f0d8; }
.home-tile:nth-child(5n+4) { background: #fbe4e8; }
.home-tile:nth-child(5n+5) { background: #f2e6f7; }
.home-tile:hover, .home-tile:focus { transform: translateY(-2px); }
/* THE LIVE ARTWORK TILES (js/home-sections.js, entries with `art`).
   .home-tile-art is 420.com's own category card — a square picture with the pastel ground and the
   label painted in — so the tile is the picture and nothing else: no padding, no pastel of its own,
   the text label kept for a screen reader only. The picture's baked-in corners are rounded at about
   5% of its width, which our 16px radius always covers at these tile sizes.
   .home-tile-photo is the live effect tile: a square photograph with the label set UNDER it in plain
   text, on the page's own ground — not a pastel card. */
.home-tile.home-tile-art, .home-tile.home-tile-photo {
  padding: 0; min-height: 0; background: transparent; border-radius: 16px;
}
.home-tile-art .home-tile-img, .home-tile-photo .home-tile-img {
  margin: 0; width: 100%; max-height: none; aspect-ratio: 1 / 1; object-fit: cover;
  mix-blend-mode: normal; border-radius: 16px; display: block;
}
.home-tile-art .home-tile-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.home-tile-photo { align-items: stretch; text-align: left; overflow: visible; }
/* 14px ON A 28px LINE, 8px under the photograph — live's effect label at every width. The line
   was 1.4 (19.6px), which left each tile 8px shorter than live's — 207 against 215 at 390, 234
   against 243 at 1280 — so the row under the labels sat a touch higher than a returning customer's
   eye expects (the twelfth QA round, store-home #5; live's label re-read with getComputedStyle on
   15 Sep 2026: font-size 14px, line-height 28px, margin-top 8px, height 28). */
.home-tile-photo .home-tile-label {
  margin: 8px 0 0; font-size: 14px; line-height: 28px; font-weight: 400; color: var(--brand-ink);
}
.home-tile-art:hover, .home-tile-art:focus, .home-tile-photo:hover, .home-tile-photo:focus { transform: none; }
.home-tile-art:hover .home-tile-img, .home-tile-photo:hover .home-tile-img { transform: translateY(-2px); }
.home-tile-img { transition: transform 0.12s ease; }
@media (max-width: 640px) {
  .home-tile-art, .home-tile-art .home-tile-img { border-radius: 12px; }
}

/* ---- Product page: quantity ---------------------------------------------------------
   Every product page on the live site has this. Without it, buying three cartridges is three presses
   of Add to Cart — and now that adding opens the drawer, two dismissals as well. */
/* THE BUY ROW: stepper and Add to cart on ONE line, as 420.com has them. The button takes the
   remaining width so it stays the largest target on the page — it is the thing the page exists
   for — while the stepper keeps its natural size. */
.buy-row { display: flex; align-items: center; gap: 0.75rem; margin: 1rem 0; }
/* ONE LINE, ALWAYS. "Add $110.00 to cart" is the longest label the catalogue produces and at 390px
   it broke over two lines ("Add $110.00 to / cart") in a button beside a 130px stepper — the live
   button (/products/420-gummy-care-package at 390) keeps it on one. The stepper is what gives: it
   may shrink, its input narrows on a phone, and the button's side padding drops from 1.6rem to 1rem
   there, which is what the live buy row measures. */
.buy-row #add-to-cart { flex: 1 1 auto; min-width: 0; white-space: nowrap; }
.buy-row .qty-stepper { flex: 0 1 auto; }
@media (max-width: 640px) {
  .buy-row { gap: 0.5rem; }
  .buy-row #add-to-cart { padding-inline: 1rem; }
  .buy-row .qty-stepper input { width: 44px; }
  .buy-row .qty-stepper button { width: 32px; }
}
/* Kept for any page still stacking a labelled quantity field of its own. */
.qty-row { display: flex; align-items: center; gap: 0.75rem; margin: 1rem 0; }
.qty-row label { font-weight: 600; }
.qty-stepper {
  display: inline-flex; align-items: center;
  border: 1px solid #cfc9bb; border-radius: var(--radius); background: var(--brand-white);
}
.qty-stepper button {
  width: 36px; height: 40px; border: 0; background: none; cursor: pointer;
  font-size: 1.1rem; line-height: 1; color: var(--brand-dark-green);
}
.qty-stepper input {
  width: 56px; height: 40px; border: 0; text-align: center;
  font: inherit; background: none; color: var(--brand-black);
  /* The browser's own spinners would sit beside buttons that do the same job. */
  -moz-appearance: textfield; appearance: textfield;
}
.qty-stepper input::-webkit-outer-spin-button,
.qty-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ---- Product page: the image gallery ------------------------------------------------
   A product with five photographs showed one, because only images[0] was rendered. On vape hardware
   the extra shots are the ones that answer the real question — threading, what is in the box, how
   big it is — so they are all reachable. */
.product-gallery { max-width: 420px; margin-bottom: 1rem; }
.product-thumbs { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }
/* The live rail: 56px, 8px radius, the beige tile behind each thumb, a 2px border that is
   transparent until the thumb is the one on show. */
.product-thumb-button {
  width: 56px; height: 56px; padding: 0; cursor: pointer; background: #f7f4eb;
  border: 2px solid transparent; border-radius: 8px; overflow: hidden;
}
.product-thumb-button img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 4px; }
/* The one being shown is marked, or a shopper clicking through five near-identical photographs has
   no way to tell which they are looking at. */
.product-thumb-button:hover,
.product-thumb-button.is-selected { border-color: var(--brand-dark-green); }

/* The cart page's own quantity stepper. Same shape as the drawer's, because they are the same
   control on the same basket and looking different would suggest they do different things. */
.cart-row-title { flex: 1; }
.cart-qty { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0 1rem; }
.cart-qty button {
  width: 28px; height: 28px; border: 1px solid #cfc9bb; border-radius: var(--radius);
  background: var(--brand-white); cursor: pointer; font-size: 1rem; line-height: 1; padding: 0;
}
.cart-row-total { white-space: nowrap; }

/* ---- Checkout ----------------------------------------------------------------------
   Two columns, the way Shopify's checkout is: the form on the left, what is being bought on the
   right. This page asks for a date of birth, a full address and a card, and somebody who cannot see
   what they are paying for while they type all that has every reason to stop and go back to check. */
/* THE SHAPE OF SHOPIFY'S CHECKOUT, which is the shape every 420.com customer already knows: the form
   on a white left half, the order summary on a warm grey right half, the two halves running the full
   height of the page. Read off the live checkout on 12 Sep 2026, the night the owner put a battery
   through both and asked why they looked different. The FIELDS and their ids are unchanged. */
.checkout-page { max-width: none; padding: 0; }
.checkout-page > .notice-banner, .checkout-page > .error-banner, .checkout-page > #checkout-signin {
  max-width: 1100px; margin-left: auto; margin-right: auto;
}
@media (max-width: 1140px) {
  .checkout-page > .notice-banner, .checkout-page > .error-banner, .checkout-page > #checkout-signin {
    margin-left: 1rem; margin-right: 1rem;
  }
}
.checkout-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start;
  border-top: 1px solid var(--brand-lime-light);
}
/* The form sits in a 520px column pushed to the RIGHT edge of the left half, the summary in a 420px
   column at the LEFT edge of the right half — so the two meet in the middle of the page as they do on
   Shopify, whatever the window width. */
.checkout-form {
  justify-self: end; width: min(100%, 520px); box-sizing: border-box;
  padding: 1.5rem 2.5rem 3rem 1rem; max-width: none;
}
/* STICKY, AND ACTUALLY STICKING (QA round 9, cart #1). This carried `min-height: 100%` from the
   days before the grid painted the grey half itself, and a percentage min-height on a grid item
   resolves against the grid area — the whole row, as tall as the form — so the summary was a box
   the height of its container and position: sticky had nowhere to move it: with Place order in
   view at 1280 the item lines and the Total row had scrolled off the top and the right half showed
   the tail of the upsell strip over empty grey. Live's column is pinned at the top of the window
   with "Total USD $24.00" beside Pay now. Content height now, so the column stays at the top of the
   window while the form scrolls under it; the grey half is the grid's gradient below. Capped at the
   window's height and scrollable inside itself so a long basket's totals are still reachable
   beside the button rather than lost under the fold. */
.order-summary {
  justify-self: start; width: min(100%, 460px); box-sizing: border-box;
  background: #f5f4ef; border: 0; border-radius: 0; padding: 1.5rem 1rem 2rem 2.5rem;
  position: sticky; top: 0; max-height: 100vh; overflow-y: auto;
}
/* The grey half runs to the page edge, not just under the summary card. */
.checkout-grid::before { content: none; }
.checkout-grid { background: linear-gradient(to right, var(--brand-white) 50%, #f5f4ef 50%); }
.order-summary h2 { margin: 0 0 0.75rem; font-size: 1rem; }
.checkout-section-title {
  margin: 1.5rem 0 0.25rem; font-size: 1.3rem; font-weight: 600; color: var(--brand-ink);
  font-family: inherit; letter-spacing: 0; text-transform: none;
}
.checkout-form > .checkout-section-title:first-child { margin-top: 0.25rem; }
.field-hint { margin: -0.35rem 0 0.25rem; font-size: 0.8rem; color: #4a5c4d; }
/* "Already have an account? Sign in" — a line under the Contact heading, where Shopify puts its
   "Have an account? Log in", not a banner across the page. */
.checkout-signin { margin: -0.25rem 0 0.5rem; font-size: 0.9rem; color: #4a5c4d; }
.checkout-signin a { color: var(--brand-dark-green); font-weight: 600; }
.checkout-form .notice-banner { margin: 0.25rem 0 0.5rem; }
.field-row.three { grid-template-columns: 1.3fr 1fr 0.9fr; }
/* Two or three fields on one row — first/last name, city/state/ZIP — the live form's layout. */
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.checkbox-row { display: flex; gap: 8px; align-items: center; margin: 0.5rem 0; font-size: 0.92rem; }
form.checkout-form input[readonly] { background: #f5f4ef; color: #4a5c4d; }
/* Shipping methods as boxed rows: name left, price right, the chosen one outlined in the shop's green. */
.shipping-placeholder {
  margin: 0; padding: 0.9rem 1rem; background: #f5f4ef; border-radius: var(--radius);
  color: #4a5c4d; font-size: 0.9rem;
}
.shipping-options:empty { display: none; }
.shipping-option {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.8rem 1rem; margin: 0;
  border: 1px solid #cfc9bb; background: var(--brand-white); cursor: pointer; font-size: 0.92rem;
}
.shipping-option:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.shipping-option:last-child { border-radius: 0 0 var(--radius) var(--radius); }
.shipping-option + .shipping-option { border-top: 0; }
.shipping-option:has(input:checked) { border-color: var(--brand-dark-green); background: var(--brand-lime-light); }
.shipping-option + .shipping-option:has(input:checked) { border-top: 1px solid var(--brand-dark-green); }
.shipping-option-name { flex: 1; }
.shipping-option-price { font-weight: 600; }
/* A rate the carrier could not price this time is offered at the shop's flat price and says so, small,
   under its name — the row itself is unchanged. */
.shipping-option-note { display: block; font-size: 0.78rem; color: #4a5c4d; margin-top: 2px; }
/* The card fields in a panel, the way the gateway draws its own. */
.payment-panel { border: 1px solid #cfc9bb; border-radius: var(--radius); padding: 1rem; display: grid; gap: 0.75rem; background: var(--brand-white); }
.payment-panel-title { margin: 0; font-weight: 600; font-size: 0.95rem; }
.checkout-pay { width: 100%; padding: 1rem; font-size: 1.05rem; margin-top: 0.5rem; }

/* THE SUMMARY LINES. Picture left with the quantity in a dark badge on its corner (Shopify's own
   device for "× 2" without printing "× 2"), the name in the middle, the money on the right. */
.summary-line {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.4rem 0; font-size: 0.9rem;
}
.summary-line-title { flex: 1; }
.summary-line-qty { color: #4a5c4d; }
.summary-item { align-items: flex-start; padding: 0.5rem 0; }
.summary-thumb {
  position: relative; flex: none; width: 64px; height: 64px; border-radius: 8px;
  border: 1px solid #d9d4c7; background: var(--brand-white); overflow: visible;
}
.summary-thumb img { width: 100%; height: 100%; object-fit: contain; border-radius: 8px; }
.summary-thumb-gift { background: var(--brand-lime-light); }
.summary-qty-badge {
  position: absolute; top: -8px; right: -8px; min-width: 22px; height: 22px; padding: 0 6px;
  border-radius: 9999px; background: #4a5c4d; color: var(--brand-white);
  font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center;
  box-sizing: border-box;
}
.summary-line-tag { display: block; margin-top: 0.2rem; font-size: 0.78rem; color: #4a5c4d; }
/* "(−$9.00)" drops to the next line as one piece or not at all — never "QA10CHK10 (−" / "$9.00)",
   which is how the tag split in the phone's narrower foot panel (QA round 10, checkout #2); the same
   rule the drawer's "(Save $75.00)" got in round 9 (.cart-drawer-save). */
.summary-line-tag-off { white-space: nowrap; }
/* 420 REWARDS ON THE CHECKOUT SUMMARY (23 Sep 2026). The same row shape as Discounts — it sits
   directly under it — with the green the rest of the shop uses for money coming back, so the two
   lines read as two different kinds of saving rather than one repeated. */
.summary-line.summary-reward span:last-child { color: var(--brand-dark-green); }
.summary-line.summary-reward .summary-line-qty { opacity: 0.75; }

.summary-line-price { white-space: nowrap; text-align: right; }
.summary-currency { font-size: 0.72rem; font-weight: 500; color: #4a5c4d; margin-right: 0.25rem; }
/* THE FREE GIFT LINE — the hoodie the reward ladder promised, printed at $0.00 among the things the
   shopper chose. Marked rather than restyled: it is a line of the order like any other and must read as
   one, because the whole point is that the shop is actually giving it. What the rule adds is a hairline
   separating it from the basket above, because it is the one line in this list the shopper did NOT put
   there and cannot remove — see the comment in checkout.html on why it is appended rather than merged
   into Cart.get(). */
.summary-line-gift {
  border-top: 1px dashed #d9d4c7;
}
.summary-total {
  border-top: 1px solid #d9d4c7; margin-top: 0.5rem; padding-top: 0.75rem;
  font-size: 1.15rem; font-weight: 700;
}
.summary-note { margin: 0.5rem 0 0; color: #4a5c4d; font-size: 0.8rem; }
#summary-items { border-bottom: 1px solid #d9d4c7; padding-bottom: 0.5rem; margin-bottom: 0.75rem; }
#summary-items:empty { display: none; }

/* WHAT THIS PURCHASE EARNS, and WHAT IT SAVES — the live checkout's own two lines, which this
   summary had neither of.
   The points line sits between the items and the totals, where the live page puts it, and reads as a
   note rather than a money row: it is not part of the arithmetic above or below it, and styling it
   like a line of the total is how a shopper ends up looking for "Points" in the figure they are
   charged. */
.checkout-points {
  margin: 0 0 0.75rem; padding: 0.7rem 0.8rem;
  background: #ecebe3; border: 1px solid #d9d4c7; border-radius: 8px;
  font-size: 0.85rem; font-weight: 650; color: var(--brand-ink);
}
/* TOTAL SAVINGS, in the shop's green and in capitals as the live line is. Not bordered like
   .summary-total: it comes AFTER the total and is not part of it. */
.summary-savings {
  font-weight: 700; color: var(--brand-dark-green); letter-spacing: 0.02em;
}
#checkout-savings:empty { display: none; }
/* .order-summary-body, not .order-summary: on a phone the body can be moved under the bar above
   Place order (see wireSummaryToggle in checkout.html), and the code box must look the same there. */
.order-summary-body .checkout-discount { margin: 0 0 0.75rem; }
.order-summary-body .checkout-discount-row input { background: var(--brand-white); }

/* THE REASSURANCE BADGES under the summary. A list, because that is what they are; stacked rather
   than in a row, because three of them across a ~320px sticky column is three truncated phrases. */
.checkout-trust {
  list-style: none; margin: 1rem 0 0; padding: 0.75rem 0 0;
  border-top: 1px solid #d9d4c7;
  display: grid; gap: 0.4rem;
}
/* Cleared setting, no block at all — not a ruled-off empty gap. */
.checkout-trust:empty { display: none; border-top: 0; margin: 0; padding: 0; }
.checkout-trust-badge {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.82rem; color: var(--brand-ink);
}
.checkout-trust-badge svg { flex: none; color: var(--brand-dark-green); }

/* The checkout's own header carries no navigation on purpose; it still needs the shop's own bar
   layout, or it renders as unstyled text in the corner of the page that matters most. */
.checkout-header .header-actions a { font-weight: 500; }

/* THE FOLDED SUMMARY BAR. Not drawn at all on a desktop, where the summary is a column beside the
   form; below 900px it is the first thing on the page. See the breakpoint below and the markup in
   checkout.html for why it exists. */
.order-summary-toggle { display: none; }

@media (max-width: 899px) {
  /* ONE COLUMN ON A PHONE, the summary ABOVE the form — FOLDED. It is what somebody checks before
     they start typing, and below a card form it is off the bottom of the screen; but unfolded it was
     840px of items, points, discount box, totals, upsells and badges before the word "Contact", and a
     390px phone showed no form at all on arrival (the Email field's top at y=1020 in an 844px
     viewport — QA, 13 Sep 2026). Shopify's checkout, and www.420.com's with it, folds the whole
     summary into one grey bar — "Order summary ⌄" on the left, the total on the right — closed by
     default, and puts Contact / Email within the first screen. This is that bar; the body opens IN
     PLACE beneath it, so the form does not jump.
     899px rather than the 800px this block used to sit at: the bar and the single column have to
     switch together, and the summary column beside a 520px form is already cramped below 900. */
  .checkout-grid { grid-template-columns: minmax(0, 1fr); background: var(--brand-white); }
  /* max-height: none; overflow: visible — the desktop column's cap and inner scroll (above) must not
     reach the phone, where the summary unfolds in the page and the page is what scrolls. */
  .order-summary { order: -1; position: static; width: 100%; padding: 0; min-height: 0; max-height: none; overflow: visible; }
  /* form.checkout-form, not .checkout-form: the base rule at the top of this file
     (`form.checkout-form { max-width: 480px }`) outranks a bare class, and with the desktop's
     justify-self: end still in force a tablet drew the form as a 480px column jammed against the
     right edge under a full-width summary bar. Centred at up to 560px here, full width on a phone. */
  form.checkout-form { justify-self: center; width: 100%; max-width: 560px; padding: 0.5rem 1rem 2rem; }
  .field-row, .field-row.three { grid-template-columns: 1fr 1fr; }
  .field-row.three > .field:first-child { grid-column: 1 / -1; }

  /* The bar: the live checkout's grey strip, the full width, a real button 48px tall (the owner is
     visually impaired — no 24px disclosure triangles), the label in the shop's green with the chevron
     beside it and the money right-aligned in bold. */
  .order-summary-toggle {
    display: flex; align-items: center; justify-content: space-between; gap: 1rem;
    width: 100%; min-height: 48px; padding: 0.75rem 1rem; margin: 0;
    background: #f5f4ef; border: 0; border-bottom: 1px solid #d9d4c7; border-radius: 0;
    color: var(--brand-ink); font: inherit; font-size: 0.95rem; text-align: left; cursor: pointer;
  }
  .order-summary-toggle-label { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--brand-dark-green); font-weight: 600; }
  .order-summary-toggle-chevron { transition: transform 0.15s ease; }
  .order-summary.is-open .order-summary-toggle-chevron { transform: rotate(180deg); }
  .order-summary-toggle-total { font-size: 1.1rem; white-space: nowrap; }
  .order-summary-toggle-total .summary-was { font-size: 0.85rem; font-weight: 400; color: #4a5c4d; margin-right: 0.35rem; }
  /* Closed by default; the button opens it. The body keeps the grey ground so open and closed read as
     one panel, and the items list gets the padding the aside used to carry. */
  .order-summary-body { display: none; padding: 1rem; background: #f5f4ef; border-bottom: 1px solid #d9d4c7; }
  .order-summary.is-open .order-summary-body { display: block; }
}
/* ONE ADDRESS FIELD PER ROW ON A PHONE, as the live checkout has it. Read side by side at 390 on
   14 Sep 2026 (QA round 8): Shopify's checkout draws First name, Last name, City, State and ZIP code
   each the full 360px width, one under the other, while ours kept First name | Last name and
   State | ZIP two-up at 173px each — and the ZIP box, the narrowest of them, is where its red
   "Enter a 5-digit ZIP code" note had to wrap to two lines. At 1280 live is two-up and three-up
   exactly as ours is, so only the phone changes. 749px is Shopify's own small breakpoint (the width
   under which its checkout stacks its fields), and this stylesheet's phone blocks sit at 749/767;
   between 750 and 899 the form has 560px, where two 270px boxes are the better shape and live's
   too. The card row keeps its two-up (MM full width above YYYY | CVV): live's phone checkout keeps
   its expiry beside its security code, and a box for two digits does not need 358px. */
@media (max-width: 749px) {
  .checkout-form .field-row, .checkout-form .field-row.three { grid-template-columns: 1fr; }
  .checkout-form .payment-panel .field-row.three { grid-template-columns: 1fr 1fr; }
}

/* The order confirmation's opening line — the sentence that tells somebody where their order stands.
   Given weight because it is the one thing on the page they came back to read. */
.order-state { font-size: 1.05rem; margin: 0.25rem 0 1.25rem; }
.order-totals { max-width: 32rem; margin-top: 1rem; }
/* WHO, WHERE, HOW, PAID — the live thank-you page's detail panel, under the order's state line.
   Four small blocks in a grey panel, two across where there is room and one column on a phone; the
   headings are the page's display face at a size that reads as a label, and the text is the ordinary
   body size (the owner reads this page — no 12px address lines). */
.order-details {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem 1.5rem;
  max-width: 44rem; margin: 0 0 1.5rem; padding: 1rem 1.25rem;
  background: #f5f4ef; border: 1px solid #d9d4c7; border-radius: var(--radius);
}
.order-detail h2 { margin: 0 0 0.3rem; font-size: 0.9rem; letter-spacing: 0.04em; }
.order-detail p { margin: 0; font-size: 1rem; line-height: 1.5; overflow-wrap: anywhere; }

/* ---- Product page layout -------------------------------------------------------------
   Shopify's shape, and 420.com's own: the photograph on the left, everything you act on beside it.
   Before this the page was one narrow column — title, a 500px image, vendor, description, and only
   then the price and Add to Cart — so on a desktop the buy button was below the fold and buying
   meant scrolling past a picture of the thing you had already decided to buy. Invisible until real
   product images replaced the placeholder blocks in the test data. */
.product-layout {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2.5rem; align-items: start;
}
.product-media { min-width: 0; }
.product-buybox { min-width: 0; }

/* THE STAR RATING UNDER THE PRODUCT TITLE, where 420.com puts it (its .pdp-420__star-rating sits
   directly after the h1). Empty until Yotpo's widget draws into it — and it stays empty on a shop
   with no stars instance configured, so it must take no space at all when it holds nothing.
   :empty rather than a display:none toggle: nothing has to remember to unhide it. */
.product-stars { margin: 0.35rem 0 0.6rem; min-height: 0; }
.product-stars:empty { display: none; }
/* OUR OWN rating row, immediately under the stars host — the visible half of the aggregateRating this
   page now publishes (see renderRating() in product.html for why the two cannot be separated).
   It carries the SAME vertical rhythm as .product-stars because only ever one of the two is on screen
   at a time, and the buy box must not shift depending on which one won. No :empty rule here: this row
   is toggled with the `hidden` attribute because it has content it is deliberately withholding, which
   is a different state from "a container somebody else failed to fill". */
.product-rating { margin: 0.35rem 0 0.6rem; }
.product-rating-link {
  display: inline-flex; align-items: center; gap: 0.45rem;
  text-decoration: none; color: inherit;
}
.product-rating-link:hover .product-rating-count,
.product-rating-link:focus-visible .product-rating-count { text-decoration: underline; }
.product-rating-value { font-weight: 600; }
.product-rating-count { color: #4a5c4d; font-size: 0.9rem; }
/* The vertical rule between "4.9" and "29 Reviews", which is how the live row separates them. In the
   stylesheet rather than in the markup so it is punctuation and not a character between two numbers
   for anything that reads the page aloud — the same reasoning as .breadcrumb li + li::before. */
.product-rating-count::before { content: "|"; margin-right: 0.45rem; color: #a8a69c; }
/* THE PHOTOGRAPH IS THE STICKY COLUMN, NOT THE BUY BOX — which is the live page's arrangement
   (`.pdp-420__media { position: sticky; top: 24px; align-self: start }` from 768px up) and the
   arrangement that lets the floating buy bar do its job. This used to pin the BUY BOX, so on a
   desktop the real Add to cart never left the screen, the IntersectionObserver watching it never
   fired, and the bar 420.com keeps at the foot of every product page at 1280 never appeared here.
   With the picture pinned, the buy box scrolls away under a long description and the bar takes
   over, exactly as on the live page. */
@media (min-width: 900px) {
  .product-media { position: sticky; top: 1.5rem; align-self: start; }
}
/* The buybox vendor is styled ONCE, further down beside the h1 it sits above. There used to be a
   rule here as well, and the later one overrode its colour, size and margin but NOT its
   `text-transform: uppercase` — so the brand rendered "420.COM" where the live site shows
   "420.com", and no single rule was wrong. A partial override is worse than a duplicate: it leaves
   a declaration in force that nobody can see is in force. */
.product-buybox h1 { margin: 0 0 0.75rem; font-size: 1.75rem; line-height: 1.2; }
/* THE PRICE IS IN THE DISPLAY FACE AND THE GREEN, as live's `.pdp-420__price-current`: 24px/28px
   weight 700 in "Obviously" in #1a391f at 1280 and 390 (re-measured 13 Sep 2026). Ours was 24px 700
   in Inter in the body ink — a plainer, darker "$74.99" under the badges (the sixth QA round,
   store-product #6). The shop's files carry ONE cut of Obviously, the Narrow Black the headings use
   (see the @font-face at the top), so that is the face here: the same family as live, narrower and
   heavier than live's regular 700 cut, which is not in the shop's files. The struck was-price beside
   it keeps its own rule further down (.price-was). */
.product-buybox .price {
  font-family: var(--font-display); font-size: 24px; line-height: 28px; font-weight: 700;
  color: var(--brand-dark-green); margin: 0 0 0.35rem;
}
.product-buybox #variant-stock { margin: 0 0 1rem; color: #4a5c4d; }
.product-buybox .product-description { margin-top: 1.75rem; line-height: 1.65; }

/* The gallery fills its column now rather than being pinned to 420px, and the image is CONTAINED
   rather than stretched: these photographs are product shots on white with their own padding, and
   cropping them to fill a box cuts the product. Capped in height so a tall one cannot push the buy
   box off the screen on a laptop. */
/* THUMBNAILS TO THE LEFT, running down the side of the main image — which is where 420.com's
   product gallery puts them. They were a horizontal strip underneath, so on a tall product shot the
   thumbnails fell below the fold and the page looked like it had one photograph.
   The main image comes FIRST in the markup (it is the important one, and that is the order to read
   and to tab through); `order` moves the rail to the left visually without reordering the DOM. */
.product-gallery {
  max-width: none;
  /* 56px thumbnails and a 16px gutter, the live rail's measurements. */
  display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 1rem; align-items: start;
}
/* PLACED EXPLICITLY, NOT ORDERED.
 *
 * These two lines used to be `order: 1` / `order: 2` and nothing else, and that quietly broke every
 * product with a SINGLE photograph — which, on this catalogue, is most of them. `order` changes the
 * sequence grid auto-placement walks in; it does not say WHICH column anything lands in. With a
 * thumbnail rail present there are two items and they fall into the two columns by luck. With one
 * image there is only one item, so it auto-placed into column 1 — the 72px thumbnail rail — and a
 * 560x560 product photograph rendered at 72x72.
 *
 * Nothing errored, the image loaded fine, and the page looked deliberate. Naming the column is the
 * fix, and it does not depend on how many images a product happens to have. */
.product-gallery .product-thumbs { grid-column: 1; order: 1; }
/* .product-slides holds every picture (product.html): the first alone on a desktop, where the rail
 * swaps it; all of them side by side on a phone, swiped — see the phone rule below. */
.product-gallery .product-slides { grid-column: 2; order: 2; min-width: 0; }
.product-slides .product-slide-more { display: none; }
/* No rail to reserve room for when there is only one photograph, so the image takes the full width
 * rather than sitting beside an empty 72px gutter. Browsers without :has() still show the image at
 * full size in column 2 — they just keep the gutter, which is a cosmetic loss, not a broken page. */
.product-gallery:not(:has(.product-thumbs)) { grid-template-columns: minmax(0, 1fr); }
.product-gallery:not(:has(.product-thumbs)) .product-slides { grid-column: 1; }
/* The phone's dots — drawn only in the phone rule below. */
.product-dots { display: none; }
/* THE BEIGE TILE BEHIND THE PHOTOGRAPH, which is what makes a cut-out read as a product shot
   rather than as a small object adrift on white. Measured on the live gallery on 2026-09-13:
   `.gallery-420__main { background: #F7F4EB; border-radius: 12px }` — rgb(247,244,235) — with the
   slide's image at width 100%, natural height on a desktop (the Kush Mints pouch, a 1200x2000
   source, renders 604x886 at 1280), and a 1:1 square cropped with object-fit: cover on a phone.
   These photographs are PNG cut-outs with transparent backgrounds, so the tile shows through them
   and a portrait pouch keeps its full height instead of being capped at 520px and contained on
   white, which is what this rule did and what made every preview photo look smaller than live's. */
.product-detail-image {
  width: 100%; max-width: none; max-height: none; height: auto; object-fit: contain;
  background: #f7f4eb; border-radius: 12px; margin-bottom: 0;
}
.product-thumbs {
  display: flex; flex-direction: column; gap: 0.5rem; flex-wrap: nowrap; margin-top: 0;
}
@media (max-width: 899px) {
  .product-layout { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
  /* A square on the phone, as the live gallery is (`aspect-ratio: 1 / 1`, image `object-fit:
     cover`): the transparent padding is what the crop takes, and the product fills the tile. */
  .product-detail-image { aspect-ratio: 1 / 1; object-fit: cover; }
  /* ON A PHONE THE GALLERY IS LIVE'S SWIPE CAROUSEL: no rail, no strip. Live's .gallery-420 hides
     its thumbnails under 900px and lays the pictures side by side in one 358px square that is
     swiped, with 8px dots over the bottom of the picture — the current one solid green, the rest at
     30% — 16px up from its edge, so the title sits directly under the square whatever the number of
     pictures (www.420.com at 390, 15 Sep 2026: media 16,215 358×358, dots at y 549, h1 at 641; the
     twelfth QA round, product-pages #3). Ours put a 52px strip of thumbnails under the picture and
     the title 72px lower on any product with more than one. The box scrolls sideways and snaps a
     slide at a time (mandatory: a slide is the whole width, so there is nothing between two to rest
     on); the scrollbar is hidden as live's is; the dots are the page's, 16px apart as live's, in
     16×24 targets so a thumb can tap one without covering the picture. */
  .product-gallery { grid-template-columns: minmax(0, 1fr); position: relative; }
  /* One column here, so the slides belong in column 1 — the explicit grid-column above would
     otherwise put them in a second column this layout does not have. */
  .product-gallery .product-slides {
    grid-column: 1; order: 1; display: flex; overflow-x: auto; scroll-snap-type: x mandatory;
    scrollbar-width: none; border-radius: 12px; overscroll-behavior-x: contain;
  }
  .product-gallery .product-slides::-webkit-scrollbar { display: none; }
  .product-slides .product-detail-image { flex: 0 0 100%; width: 100%; scroll-snap-align: start; }
  .product-slides .product-slide-more { display: block; }
  .product-gallery .product-thumbs { display: none; }
  .product-dots {
    display: flex; justify-content: center; gap: 0; position: absolute; left: 0; right: 0; bottom: 8px;
    pointer-events: none;
  }
  .product-dot {
    pointer-events: auto; width: 16px; height: 24px; padding: 0; border: 0; background: none;
    cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  }
  .product-dot::before {
    content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--brand-dark-green);
    opacity: 0.3; transition: opacity 0.2s, transform 0.2s;
  }
  .product-dot.is-current::before { opacity: 1; transform: scale(1.25); }
}
/* A product with no photograph is a single column — see product.html. */
.product-layout-no-media { grid-template-columns: minmax(0, 1fr); max-width: 44rem; }


/* ---- Product page: the rows 420.com's buybox carries ------------------------------
   A labelled Category row under the price, and the three promises printed under every Add to cart.
   Both read off the live product page.
   LIVE'S TABLE, TO THE NUMBER (re-measured 14 Sep 2026 at 1280 and 390, round 10 product-pages #5):
   .pdp-420__attr-label is 15px/20px weight 400 in the green at 64% — rgba(26,57,31,.64) — in a
   column exactly as wide as "Certificate of Analysis" (156.19px), every .pdp-420__attr-value 15px/
   20px in the full green 32px to its right (values at x=848 at 1280, x=204 at 390), and the rows
   26px apart — a 20px line and 6px between. Ours were 0.95rem (15.2px) in a warm grey with the
   browser's line height, and that fifth of a pixel is what wrapped "Certificate of Analysis" onto a
   second line in the 156px column on a phone while live keeps it on one; the Net Weight figure was
   ink where every live value is green. */
/* ONE TABLE, ITS LABEL COLUMN AS WIDE AS ITS WIDEST LABEL — which is what live's markup does: the
   labels and the values are two flex COLUMNS 32px apart (.pdp-420__attributes gap: 32px; the label
   column flex 0 0 auto), so the column is exactly as wide as "Certificate of Analysis" (156.19px)
   when that row is present and exactly as wide as "Category" (65.4px) when it is the only row.
   Round 10 imitated the first case with a 156px minimum on every label, which was right on every
   typed product and wrong on the two typeless ones — m-1 and the gummy care package have the one
   "Category  All" row, and live prints "All" at x=757 (1280) / 113 (390) where ours held it at 848 /
   204 with a 91px hole after a 65px word (QA round 11, product-pages #3).
   So the rows are one GRID now: product.html wraps them in .product-meta, each row is
   `display: contents` and its label and value become the grid's cells, and the first track is
   max-content — the widest label on the page, whichever it is, at every width. A row the page has
   hidden (Net Weight, CBD until their figures are painted) is display: none, so it neither shows nor
   widens the column; the [hidden] rule is spelled out because `display: contents` would otherwise
   outrank the browser's own [hidden] { display: none }. The rows keep live's 15px/20px, the 6px
   between them (a 26px stride) and the 64% green label. */
.product-meta {
  display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: 32px; row-gap: 6px;
  align-items: baseline; margin: 6px 0; font-size: 15px; line-height: 20px; color: var(--brand-dark-green);
}
.product-meta-row { display: contents; }
.product-meta-row[hidden] { display: none; }
.product-meta-label { color: rgba(26, 57, 31, 0.64); }
.product-meta-row a { color: var(--brand-dark-green); text-decoration: none; }
.product-meta-row a:hover { text-decoration: underline; }

/* ---- The cannabis badges, and the lab-report link ---------------------------------
   Strain / THC-A / THC-P as pills above the price, and "Certificate of Analysis → View document"
   as a row beneath Net Weight. Both read off the live product page's own stylesheet:

     .pdp-420__badges  { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }
     .pdp-420__badge   { padding:6px 12px; background:#F5F2EB; border-radius:800px;
                         font-size:15px; font-weight:500; line-height:20px; color:#1A391F; }
     .pdp-420__badge--strain { background:#DCF3B5; }
     .pdp-420__coa-link { font-size:15px; font-weight:500; color:#1A391F; text-decoration:underline; }

   Written against this sheet's existing tokens rather than those literals, because they ARE those
   literals: --brand-cream is #f5f2eb, --brand-dark-green is #1a391f, and --brand-lime-light is the
   token already standing for that pale lime. Hard-coding the hexes would give the page two
   definitions of the same colour, which is how a palette drifts.

   The strain pill is the only one with a distinct background on the live site, and that is
   information rather than flourish: it is the one badge that carries an explanation. */
/* THE LOYALTY BANNER, between the star rating and the badges. Measured off the live buy box, where
   Smile's own block renders a full-width tinted strip rather than a pill — the one row in this column
   that is a panel instead of a line, which is what stops a shopper's eye on the way to the price:

     .slp_* wrapper  --secondary-colour:#f8f8f8 background, full column width, ~16px padding,
                     gift glyph then the sentence, the points figure in bold.

   Painted in --brand-cream rather than Smile's #f8f8f8 for the reason the badges are: this sheet
   already has a token for the shop's warm off-white, and a second near-identical grey beside it
   reads as a rendering fault rather than a design. */
.reward-points {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.75rem 1rem; margin: 0 0 1rem; border-radius: 8px;
  background: var(--brand-cream); color: var(--brand-dark-green);
  font-size: 0.9375rem; line-height: 1.35;
}
/* The glyph sits with the first line of a sentence that wraps to two on a phone, so it is aligned to
   the text baseline and never stretched by the row it is in. aria-hidden in the markup: a screen
   reader announcing "wrapped gift" before the offer adds nothing to it. */
.reward-points-icon { flex: 0 0 auto; font-size: 1rem; }
.reward-points a { color: var(--brand-dark-green); text-decoration: underline; }
.reward-points a:hover { text-decoration: none; }

.product-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* :not(:empty) — because the container is rendered even when there is nothing to put in it, the way
   the live site renders <div class="pdp-420__badges"></div> on a battery or a hoodie. An unconditional
   margin-bottom would then leave a 1rem hole between the stars and the price on every non-cannabis
   product. Putting the margin on the CHILDREN instead also works and is worse: it adds to the
   row-gap, so three badges wrapping on a phone get 1.5rem between the rows rather than 0.5rem. */
.product-badges:not(:empty) { margin-bottom: 1rem; }
.product-badge {
  display: inline-flex; align-items: center; gap: 0.375rem;
  padding: 0.375rem 0.75rem; border-radius: 800px;
  background: var(--brand-cream); color: var(--brand-dark-green);
  font-size: 0.9375rem; font-weight: 500; line-height: 1.25rem;
}
.product-badge-strain {
  background: var(--brand-lime-light);
  /* It carries a title= tooltip and is focusable, so it has to LOOK like something to hover or tab
     to. The live site uses cursor:pointer on this badge alone, for the same reason. */
  cursor: help;
}
/* A visible focus ring, because this badge is tabbable so a keyboard user can read its tooltip.
   Without it, tabbing through the buy box moves focus somewhere invisible. */
.product-badge-strain:focus-visible { outline: 2px solid var(--brand-dark-green); outline-offset: 2px; }
/* Underlined, unlike the Category link beside it: that one goes deeper into this shop, this one
   leaves for a PDF on a third-party lab's domain, and the live site distinguishes them the same way. */
.product-coa-link { color: var(--brand-dark-green); text-decoration: underline; }
.product-coa-link:hover { text-decoration: none; }
/* There used to be a ≤600px rule here that let the label take its own width and the row wrap on a
   phone. Live does not: its two columns hold at 390 and 360 (round 9, phone #1), and a value that
   is long for the 170px left of the label ("Vaporizers ready to use") wraps inside its own column,
   which is what live's does too. One rule at every width, above. */

/* LIVE'S ROW, TO THE NUMBER: .pdp-420__guarantees is a wrapping flex row, 16px between lines and 24px
   between items, 20px under it; each .pdp-420__guarantee is 14px weight 500 in the green with 8px
   between its 20px icon and its words (re-measured at 1280 and 390, 13 Sep 2026). Ours were 14.4px
   in a grey with a "✓" before each — the one row of the buy column drawn from a different shop (the
   sixth QA round, store-product #7); the icons themselves are inline in product.html. */
.product-assurances {
  list-style: none; display: flex; flex-wrap: wrap; gap: 16px 24px;
  margin: 0 0 20px; padding: 0; font-size: 14px; font-weight: 500; color: var(--brand-dark-green);
}
.product-assurances li { display: inline-flex; align-items: center; gap: 8px; }
/* The row's 16px top margin went with the tick: live's description block (the summary and its
   "Read more") ends 24px above the row, and here the "Read more" button already carries that
   (10px of padding and 20px under it — measured 29px on live, 30px here). A product with no summary
   has no button, so its description sits directly above the row and keeps live's 24px itself. */
.product-description + .product-assurances { margin-top: 24px; }
/* Live's icon files are 24-unit drawings at stroke 1.5, shown at 20px; the stroke is the green the
   words are in, so the two cannot drift apart. */
.product-assurances svg {
  flex: none; width: 20px; height: 20px; fill: none;
  stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}

/* The product page's own H1 is the display face in the brand green, as on the live site — the
   generic heading rule paints headings #121212, which is right for a section but not for this. */
.product-buybox h1 { color: var(--brand-dark-green); margin: 0.2rem 0 0.6rem; }
.product-buybox .vendor { color: #6b6a63; font-size: 0.95rem; margin: 0; }


/* The product page's breadcrumb — "Home > Battery > 420.COM Cartridge Battery". The separator is
   drawn with ::before on every item but the first, so it is punctuation in the stylesheet rather
   than a character in the markup that a screen reader would read aloud between every step. */
.breadcrumb { margin: 0 0 1rem; }
.breadcrumb ol {
  list-style: none; display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.5rem; margin: 0; padding: 0; font-size: 0.9rem; color: #6b6a63;
}
.breadcrumb li + li::before { content: "›"; margin-right: 0.5rem; color: #a8a69c; }
.breadcrumb a { color: var(--brand-dark-green); text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
/* The page you are on is not a link, and must not look like one. */
.breadcrumb [aria-current="page"] { color: #6b6a63; }


/* ---- The trust strip 420.com runs under its hero ---------------------------------
 *
 * "Fast delivery · Discreet shipping · Lab tested · More for less · Natural ingredients" — live's
 * .hero-details-bar, re-read with getComputedStyle at 1280 and 390 on 14 Sep 2026 (round 10, home
 * #3): a 56px band on the cream rgb(245,242,235) with no rules above or below it; each item a flex
 * row of a 24px line icon, 8px, and the words in Inter 400 at 15px/20px in the brand green (the
 * LABEL span, .hero-details-bar__label, is 15px — the item around it inherits the page's 17px, and
 * a probe that reads the item reports 17); 24px of margin either side of every item from 768px up
 * (48 between neighbours) and 8px below; and the whole band masked so the scroll fades in over the
 * first 10% and out over the last 10% instead of being cut off at the edges. It scrolls slowly
 * sideways there (30s a cycle); here it scrolls the same way, and STOPS for anyone who has asked
 * for less motion, who gets the whole list laid out instead. A strip that moves under a reader who
 * cannot tolerate movement is worse than no strip at all. Ours was a 40px white band between two
 * hairlines with a bold "✓" before each item and a hard edge.
 */
.trust-strip {
  background: var(--brand-cream);
  overflow: hidden;
  /* min-height, not height: with the motion preference the list wraps to as many rows as it
     needs, and a fixed 56px would clip the second row. */
  min-height: 56px; display: flex; align-items: center;
  /* FULL-BLEED, because the live strip spans the whole viewport rather than the content column.
     Breaking out of the centred container this way keeps the markup where it belongs in the
     document — inside <main>, in reading order — instead of hoisting it out to a wrapper that only
     exists for layout. */
  margin-inline: calc(50% - 50vw);
  width: 100vw;
  max-width: 100vw;
  /* THE EDGE FADE. On the band, not on the moving track: a mask on the track would travel with it.
     Both spellings — Safari still wants the prefix. */
  -webkit-mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #000 10%, #000 90%, rgba(0, 0, 0, 0) 100%);
  mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #000 10%, #000 90%, rgba(0, 0, 0, 0) 100%);
}
.trust-track {
  display: flex; width: max-content;
  animation: trust-scroll 30s linear infinite;
}
.trust-strip:hover .trust-track { animation-play-state: paused; }
/* MARGIN ON EACH ITEM, NOT A GAP ON THE ROW — live's own arrangement (margin: 0 24px), and the
   reason the loop is seamless: the track is the five items twice, and the keyframe below slides it
   exactly half its width, so the second copy must start exactly where the first did. With a flex
   gap there is no gap after the last item, the halves are half a gap different, and the strip
   jumped 24px at every loop. */
.trust-track span {
  display: inline-flex; align-items: center; gap: 8px; margin: 0 24px; white-space: nowrap;
  font-size: 15px; line-height: 20px; color: var(--brand-dark-green);
}
.trust-track .trust-icon { display: block; width: 24px; height: 24px; flex: 0 0 auto; }
@media (max-width: 767px) {
  .trust-track span { margin: 0 8px; }
}
@keyframes trust-scroll {
  from { transform: translateX(0); }
  /* -50%, because the list is duplicated exactly once — so the loop point lands on an identical
     frame and the strip never visibly jumps back to the start. */
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .trust-strip { -webkit-mask-image: none; mask-image: none; }
  .trust-track { animation: none; width: auto; flex-wrap: wrap; justify-content: center; margin: 0 auto; padding: 8px 0; }
  /* The duplicate exists only to make the scroll seamless; with no scroll it is the list said twice. */
  .trust-track .trust-duplicate { display: none; }
}


/* ---- The hero banner (js/home-sections.js) ---------------------------------------
   Full-bleed, like 420.com's: the artwork spans the viewport rather than the content column, and
   carries its own copy. Breaks out of the centred container the same way the trust strip does. */
.home-hero-banner {
  display: block;
  margin-inline: calc(50% - 50vw);
  width: 100vw; max-width: 100vw;
  /* FLUSH WITH THE HEADER, as live's banner is. `main` carries 1.5rem of padding on every side and
     this is main's first child, so a 24px white band sat between the header's bottom edge and the
     artwork at every width (measured 13 Sep 2026, both passes: 1280 and 390). The banner pulls
     itself up over that padding rather than the page dropping it, because index.html is ALSO the
     search-results page: with the hero hidden by a ?q= the results heading still wants its gutter.
     The top value is main's own padding and has to move with it.
     AND FLUSH WITH THE TRUST STRIP UNDER IT. Live's strip starts on the hero's last pixel (390: hero
     155–661, strip at 661; 1280: hero 116–916, strip at 916 — re-measured 14 Sep 2026); this carried
     a 1.5rem bottom margin as well, so a 24px white band sat between the dark hero and the cream
     strip at every width and the top of the page read as hero, white line, strip (QA round 11,
     phone-sweep #1). The strip's own top edge is the gap now: none. */
  margin-block: -1.5rem 0;
}
.home-hero-picture { display: block; }
.home-hero-img {
  display: block; width: 100%; height: auto;
  /* The live banner is 1728x800 — a little over 2:1. Reserving that ratio stops the whole page
     jumping down when a 200KB image finally arrives, which is the one thing a hero this size will
     do to a shopper on a phone. */
  aspect-ratio: 1728 / 800; object-fit: cover;
}
/* 800px TALL ON A DESKTOP, cropped to fit, which is what live does: its hero is a `background-size:
   cover` slide whose height is fixed by its content, not by the picture (a 700px min-height column
   plus 100px of padding at 1280: header bottom 117, category grid top 972, the 56px marquee
   between). Ours let the 2400x1141 file set its own height, so at 1280 the band was a 593px
   letterbox with the artwork's copy a third smaller than live's.
   THE THREE STEPS ARE LIVE'S, measured on www.420.com on 13 Sep 2026 rather than taken from its
   stylesheet alone: 800px from 1201px up, 600px from 1025 to 1200 (its column drops to a 500px
   min-height under 1200), 706px from 768 to 1024 (the column stacks under the empty copy block and
   its margins). Below 768 the phone <picture> — a portrait file, shown whole — is untouched. */
@media (min-width: 768px) { .home-hero-img { height: 706px; } }
@media (min-width: 1025px) { .home-hero-img { height: 600px; } }
@media (min-width: 1201px) { .home-hero-img { height: 800px; } }
/* WHEN THERE IS A SEPARATE PHONE BANNER, the reserved ratio above has to stop applying at the width
   the <picture> swaps source — otherwise a portrait banner (420.com's is 1200x1780) is cropped by
   `object-fit: cover` into the same 2:1 letterbox and the phone gets the identical unreadable strip
   the swap existed to avoid.
   767px is 420.com's own breakpoint for this swap, read off its hero stylesheet, so the two sites
   change over at the same width.
   WHAT THIS COSTS, said out loud: below 767px the banner no longer reserves its height, so the page
   shifts down once as it loads. There is no honest way to reserve it — the height of an arbitrary
   URL a merchandiser types into the admin is not knowable from the URL. A brief shift on the first
   paint beats an offer nobody can read, and the class is only present when a phone banner is set,
   so nothing changes for a shop that has not set one. */
@media (max-width: 767px) {
  .home-hero-banner-responsive .home-hero-img { aspect-ratio: auto; object-fit: contain; }
}


/* The pre-sale price beside the current one, struck through — 420.com's grid shows both. Muted and
   a shade smaller so the price a shopper actually pays stays the one they read first. */
.price-was { color: #8a8578; font-weight: 400; margin-left: 0.4rem; font-size: 0.92em; }

/* ---- Product page: the description clamp and the sticky buy bar -------------------
 *
 * Both taken from the live product page: a long description is collapsed behind "Read more", and a
 * buy bar pins to the bottom once the real Add to cart has scrolled away.
 */

/* The collapsed state itself (.product-description.is-collapsed) is defined in the fix-round-3
   block at the foot of this sheet: the body is HIDDEN under its summary until "Read more", which is
   live's accordion. There used to be a six-line -webkit-line-clamp here instead; it showed the
   summary plus two paragraphs cut mid-word, and is gone. */
#description-toggle { margin-top: 0.5rem; }

.sticky-buy {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 800;
  background: var(--brand-white); border-top: 1px solid var(--brand-hairline);
  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.07);
  /* Clear of a phone's home indicator, which otherwise sits on top of the button. */
  padding: 0.7rem 1.5rem calc(0.7rem + env(safe-area-inset-bottom, 0px));
}
.sticky-buy-inner {
  max-width: 1400px; margin: 0 auto;
  display: flex; align-items: center; gap: 1rem;
}
.sticky-buy-title {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  color: var(--brand-dark-green); margin: 0; font-size: 1rem;
  /* One line: the bar is a control, not somewhere to read the product's name in full. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; min-width: 0;
}
.sticky-buy-price { margin: 0; font-weight: 600; white-space: nowrap; }
.sticky-buy .qty-stepper { flex: 0 0 auto; }
.sticky-buy .primary { flex: 0 0 auto; white-space: nowrap; }

@media (max-width: 640px) {
  /* The title is the first thing to go on a narrow screen — the price and the button are what the
     bar is for, and squeezing all four into 360px makes none of them usable. */
  .sticky-buy-title { display: none; }
  .sticky-buy { padding-inline: 1rem; }
}

/* A page with a bar pinned over it needs room to scroll past its own last element, or the footer's
   final line sits permanently underneath the bar.
   THE BAR'S OWN HEIGHT, NOT A ROUND NUMBER. The bar is 74px (12px padding, a 50px stepper, 12px —
   measured at 1280 and 390) PLUS the phone's home-indicator inset, which its padding already adds
   (see .sticky-buy below). This reserved a flat 5rem = 80px, so on a phone with a 34px inset the
   bar stood 108px tall over an 80px reserve and the footer's last line — the copyright — sat
   permanently under it. The reserve now follows the same formula the bar does, with 8px of air. */
body:has(.sticky-buy:not([hidden])) { padding-bottom: calc(74px + env(safe-area-inset-bottom, 0px) + 8px); }

/* AND SO DOES TIDIO'S CHAT LAUNCHER. The bar is pinned to the bottom of the screen and the launcher
   is pinned to the bottom-RIGHT with a z-index in the millions, so the bubble lands exactly on top of
   the Add to Cart button — on the page that sells 85% of this shop's revenue, on the device most of
   that revenue arrives from. It is not a layout bug anyone would notice from a desktop browser: the
   bar only appears on the product page, and the launcher only appears once a chat key is configured,
   so the two are never on screen together in local testing.
   Lifted by the bar's own height plus the phone's home indicator, scoped with :has() so the lift
   happens only while the bar is actually showing. !important because Tidio writes the iframe's
   position inline, and the selector covers both the v4 container and the older bare iframe id. */
body:has(.sticky-buy:not([hidden])) #tidio-chat,
body:has(.sticky-buy:not([hidden])) #tidio-chat-iframe {
  bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px)) !important;
}

/* The FDA statement and the state-shipping notice. Small and quiet, as on the live footer — they are
   required text rather than something the shop is saying, and they sit below the policy links. */
.footer-legal {
  max-width: 60rem; margin: 0; padding: 0;
  font-size: 0.78rem; line-height: 1.5; color: #6b6a63; text-align: left;
}

/* The product page's compliance notice — small, grey and set apart, as on the live page. It is
   required text rather than something the shop is selling with, so it must be readable without
   competing with the price and the button above it. */
.product-compliance {
  margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--brand-hairline);
  font-size: 0.8rem; line-height: 1.55; color: #6b6a63;
}
.product-compliance p { margin: 0 0 0.4rem; }
.product-compliance-heading { font-weight: 700; color: var(--brand-dark-green); }

/* THE EFFECTS BLOCK at the foot of the buy column, drawn as the live product page draws it: a rule,
   then an "Effects" accordion header with a chevron at its right end that starts open, then one
   ICON TILE per effect — a line icon over its name on a cream square. Every value below is live's
   own, read off its stylesheet (.pdp-420__effects* in its base-420.css) and re-measured on the
   Candy Kush page at 390 and 1280 on 14 Sep 2026: the header Inter 20px/28px 500 in the brand green
   with a 24px chevron (stroke 1.5) that turns over when the block is open and 16px under it; the
   tiles in a wrapping row 8px apart, each padded 12px 16px on #F5F2EB with an 8px radius, a 40px
   icon, 8px, then the label at 15px/20px 500 — 92px tall, as wide as the word (80–96px on the phone)
   and at least 115px from 768px up. The icons are live's twelve, inlined in product.html.
   This was a <details> whose <summary> showed the browser's own disclosure triangle beside a bold
   "Effects", then three 27px lime text chips — a different look, and the only sub-40px targets in
   the buy box (the seventh QA round, store-phone-sweep #3). The tiles are still LINKS into the
   catalogue filtered to that effect, as the chips were (live's tiles are plain <div>s that do
   nothing): a tile that takes you to more of the same costs nothing and is worth having, and at
   92px tall it is the largest target in the column. The chevron is drawn by product.html and
   rotated here; the native marker is removed the two ways it needs (list-style for Firefox, the
   -webkit pseudo for Safari and Chrome). 24px between the rule and the header is live's separator
   margin (.pdp-420__separator { margin: 24px 0 }). */
.product-rule { border: 0; border-top: 1px solid var(--brand-hairline); margin: 1.5rem 0 0; }
.product-effects { margin: 24px 0 0; border: 0; }
.product-effects-summary {
  display: flex; align-items: center; justify-content: space-between;
  cursor: pointer; list-style: none; padding: 0; margin: 0;
  font-family: var(--font-primary); font-size: 20px; line-height: 28px; font-weight: 500;
  color: var(--brand-dark-green);
}
.product-effects-summary::-webkit-details-marker { display: none; }
.product-effects-summary::marker { content: ""; }
.product-effects-chevron {
  width: 24px; height: 24px; flex-shrink: 0; color: var(--brand-dark-green);
  transition: transform 0.3s ease;
}
.product-effects[open] > .product-effects-summary .product-effects-chevron { transform: rotate(180deg); }
.product-effects[open] > .product-effects-summary { margin-bottom: 16px; }
.product-effects-grid {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 8px;
}
.product-effect {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 12px 16px; border-radius: 8px; background: var(--brand-cream);
  color: var(--brand-dark-green); text-decoration: none; text-align: center;
}
@media (min-width: 768px) { .product-effect { min-width: 115px; box-sizing: border-box; } }
.product-effect:hover, .product-effect:focus-visible { background: #ece7dc; }
.product-effect-icon { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; flex-shrink: 0; }
.product-effect-icon svg { width: 40px; height: 40px; display: block; }
.product-effect-label { font-family: var(--font-primary); font-size: 15px; line-height: 20px; font-weight: 500; white-space: nowrap; }

/* "You May Also Like" and its "View All", which sit on one line on the live page — the heading on
   the left, the way into the whole category on the right. */
.product-upsells { max-width: 1100px; margin: 0 auto; padding: 0 1.5rem 3rem; }
.upsell-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  flex-wrap: wrap;
}
.upsell-head h2 { margin: 0; }
.upsell-view-all {
  color: var(--brand-dark-green); font-size: 0.9rem; font-weight: 500; text-decoration: underline;
  white-space: nowrap;
}

/* THE NUMBERED PAGER (js/filters.js). Two rows — the numbers, then the count — centred under the
   grid. This replaces the identical `style="display:flex;…"` attribute that both index.html and
   collection.html carried: it had no wrap, so the count sat to the right of the numbers. */
/* Space above AND below the pager (16 Sep 2026, the marketing team's screenshot): the home page's
   "1 2 3 … 14 ›" sat flush against the full-bleed bundle banner that follows it. The bottom margin
   matches the top so the block reads as its own row wherever it is used (home, collection). */
.pagination { margin-top: 1.5rem; margin-bottom: 2.5rem; }
.pager { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.35rem; }
.pager-btn {
  min-width: 38px; height: 38px; padding: 0 0.6rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--brand-hairline); border-radius: var(--radius);
  background: var(--brand-white); color: var(--brand-ink);
  font: inherit; font-size: 0.9rem; font-variant-numeric: tabular-nums;
}
button.pager-btn { cursor: pointer; }
button.pager-btn:hover { background: #f2f1ec; }
.pager-current {
  background: var(--brand-dark-green); border-color: var(--brand-dark-green);
  color: var(--brand-white); font-weight: 700;
}
.pager-gap { border-color: transparent; background: none; min-width: 24px; padding: 0; }
/* .pager-count is gone with the second copy of the product count it styled — the count is stated once
   now, in .filter-summary. See the note in Filters.paginate(). Removed rather than left in place
   because a rule with no markup behind it is something the next reader has to go and check. */

/* The discount code box on the checkout's order summary. The cart has had one all along; this is
   the same control at the point where people actually remember they have a code — the email is
   open in front of them and they are about to pay. */
.checkout-discount { margin-top: 1rem; }
.checkout-discount-row { display: flex; gap: 0.5rem; }
.checkout-discount-row input {
  flex: 1; min-width: 0; padding: 0.55rem 0.7rem;
  border: 1px solid #cfc9bb; border-radius: var(--radius); font: inherit;
}
.checkout-discount .error-banner,
.checkout-discount .notice-banner { margin-top: 0.5rem; }

/* THE PROMO BANNER ACROSS THE TOP OF THE BASKET.
   Colours and type size taken from the live cart's own configuration rather than chosen here:
   background #568930, white bold text at 16px, a small-height strip at the top of the drawer
   (upcartEditorSettings.announcementBackgroundColor / announcementTextFontSizePx / announcementHeight).
   #568930 is NOT in the brand palette above — it is a lighter green than --brand-dark-green and this
   is the one place the shop uses it, so it is written literally with the reason beside it rather than
   approximated with a variable that means something else. */
.cart-announcement {
  background: #568930; color: var(--brand-white);
  padding: 0.55rem 0.9rem; border-radius: 8px; margin: 0 0 0.9rem;
  text-align: center;
}
.cart-announcement-line {
  margin: 0; font-size: 1rem; line-height: 1.3; font-weight: 700;
}
/* The drawer is ~310px wide and these lines are long. 16px bold in that width wraps to three lines
   and pushes the first product below the fold, so the strip is a size smaller inside the panel —
   the same compromise the label-crowding rule below makes, for the same reason. */
.cart-drawer .cart-announcement { border-radius: 0; margin: 0 -1.25rem 0.75rem; }
.cart-drawer .cart-announcement-line { font-size: 0.85rem; }

/* THE REWARD PROGRESS BAR — "Spend $18.40 more for a free 420 hat".
   Colours taken from the live cart's own configuration: the track is white and the fill is
   #ddf3b5, which is this stylesheet's --brand-lime-light. */
.cart-rewards:empty { display: none; }
.cart-rewards { margin: 1rem 0; }
.cart-reward-message { margin: 0 0 0.4rem; font-size: 0.95rem; color: var(--brand-ink); }
.cart-reward-track {
  height: 8px; background: var(--brand-white); border: 1px solid var(--brand-lime-light);
  border-radius: 9999px; overflow: hidden;
}
.cart-reward-bar {
  height: 100%; background: var(--brand-lime-light); border-radius: 9999px;
  transition: width 240ms ease;
}
/* Somebody who has asked for less motion gets the same bar without it sliding. */
@media (prefers-reduced-motion: reduce) { .cart-reward-bar { transition: none; } }
/* THE CONGRATULATION, once every rung is earned. It is the same .cart-reward-message line in the same
   place above the track — the track stays, full, with every rung ticked, as live's does at $400 (QA
   round 7, cart-drawer #5) — so this only colours it the earned green the ticked labels use. It was a
   smaller line on its own, drawn INSTEAD of the ladder (margin-top, 0.85rem); that whole state is
   gone from js/cart-rewards.js, so those measurements go with it. */
.cart-reward-message.cart-reward-earned { color: var(--brand-dark-green); }

/* THE MILESTONES ALONG THE TRACK — the shape the live 420.com cart uses.
   Absolutely positioned by percentage so each marker sits at its real place on the bar, and the
   whole strip is given height for the dot plus its caption. */
.cart-reward-milestones {
  position: relative; height: 40px; margin-top: -9px;
}
.cart-reward-milestone {
  position: absolute; top: 0; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  width: 88px; text-align: center;
}
.cart-reward-dot {
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 9999px;
  background: var(--brand-white); border: 1px solid var(--brand-lime-light);
  color: var(--brand-dark-green);
}
.cart-reward-milestone.is-earned .cart-reward-dot {
  background: var(--brand-dark-green); border-color: var(--brand-dark-green); color: var(--brand-white);
}
.cart-reward-label {
  font-size: 0.68rem; line-height: 1.15; color: var(--brand-ink);
  /* Two lines at most: "Free Priority Shipping!" wraps, and a third line would push the checkout
     button down every time the basket crossed a threshold. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.cart-reward-milestone.is-earned .cart-reward-label { font-weight: 650; color: var(--brand-dark-green); }
/* A CROWDED LABEL IS HIDDEN ONLY WHERE THE CONTAINER IS ACTUALLY NARROW.
   The ladder's markers sit at percentages of the track, so the same shop thresholds are 14% apart
   everywhere — but 14% of the 310px cart DRAWER is ~43px, where two 88px labels print through each
   other, while 14% of the ~860px cart PAGE is ~120px, where they fit. js/cart-rewards.js marks which
   markers are close to a neighbour; only this rule decides whether that matters, because only CSS
   knows the width. The marker keeps its aria-label and title either way, so a hidden label is still
   announced and still available on hover. */
.cart-rewards { container-type: inline-size; }
@container (max-width: 480px) {
  .cart-reward-milestone.is-crowded .cart-reward-label { display: none; }
}
/* Fallback for a browser with no container queries: hide a crowded label on a narrow VIEWPORT, which
   is where the drawer is the whole screen anyway. Without this such a browser prints the overlap. */
@supports not (container-type: inline-size) {
  @media (max-width: 700px) {
    .cart-reward-milestone.is-crowded .cart-reward-label { display: none; }
  }
}

/* The first and last markers would otherwise hang off the ends of the track.
   THE LAST ONE IS PULLED FULLY INSIDE, with its dot against the track's end and its caption
   right-aligned under the dot — the way live's ladder keeps "Free Hat" inside the drawer's side
   margin. At translateX(-75%) the 88px box ran 22px past the end of the track, which is 2px past
   the drawer body's own padding: #cart-drawer-body measured scrollWidth 392 against clientWidth
   390 (362 against 360), a finger dragging the open drawer could shift it sideways, and the caption
   sat flush against the screen's edge (QA round 6, store-phone-sweep #3, 13 Sep 2026). The first
   marker gets the mirror image, though the ladder's even spacing (js/cart-rewards.js) never puts a
   rung at 0% today. */
.cart-reward-milestone[style*="left:0%"] { transform: none; align-items: flex-start; text-align: left; }
.cart-reward-milestone[style*="left:100%"] { transform: translateX(-100%); align-items: flex-end; text-align: right; }

/* ONE LAST SUGGESTION on the checkout, beside the payment form. Deliberately quieter and denser
   than the cart's strip — three product cards next to a card form is a page competing with itself,
   so this is a line each: name, price, and a button. */
.checkout-upsells { margin-top: 1rem; border-top: 1px solid var(--brand-lime-light); padding-top: 0.75rem; }
.checkout-upsells h3 { margin: 0 0 0.5rem; font-size: 0.95rem; }
.checkout-upsell {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.35rem 0; font-size: 0.9rem;
}
/* The picture. Small on purpose — this sits beside a card form — but a product is recognised by
   its box, not by "Litty Baja Blast THCa Cartridge 1g" truncated to fit. flex-shrink:0 so the
   thumbnail keeps its size and the TITLE gives way, which is the one that can ellipsise. */
.checkout-upsell-thumb {
  /* 44, not 32. At 32 the picture is present and useless — you can tell there IS a box, not which
     box, which is the entire job. 44 is still a single-line row beside a payment form. */
  width: 44px; height: 44px; flex: 0 0 44px; border-radius: var(--radius);
  object-fit: contain; background: var(--brand-white); border: 1px solid #e6e2d8; display: block;
}
.checkout-upsell-thumb-placeholder { background: #f1efe9; }
.checkout-upsell-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.checkout-upsell-price { font-weight: 700; color: var(--brand-dark-green); }
.checkout-upsell-add {
  border: 1px solid var(--brand-dark-green); background: var(--brand-white);
  color: var(--brand-dark-green); border-radius: var(--radius-pill, 9999px);
  padding: 0.25rem 0.75rem; font: inherit; font-size: 0.85rem; cursor: pointer;
}
.checkout-upsell-add:hover { background: var(--brand-lime-light); }


/* ---- The rest of 420.com's home page (js/home-sections.js) -------------------------
 *
 * Sections 5 and 7-12 of the live home page: the product spotlight, the bundle promotion, Shop by
 * brand, the featured brand, the testimonials, the unboxing videos and the FAQ. The section map at
 * the top of js/home-sections.js lists all twelve and says which file builds each.
 *
 * Measured against the live sections rather than invented: the spotlight is a dark full-bleed band
 * with the photograph on the right, the bundle block is copy beside artwork, the testimonials scroll
 * sideways like the trust strip, the videos are 9:16 portrait cards, and the FAQ is an accordion
 * beside an illustration. Brand tiles reuse .home-tile — same control, same look, because "shop by
 * brand" is the same journey as "shop by category" and two different-looking grids for one idea is
 * how a page stops reading as one site.
 */

/* The prev/next pair the live carousels carry. Hidden by JavaScript on a track that cannot scroll,
   because a control that moves nothing is a control that lies. */
.home-carousel-nav { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.75rem; }
.home-carousel-btn {
  width: 40px; height: 40px; border-radius: var(--radius-pill);
  border: 1px solid var(--brand-dark-green); background: var(--brand-white);
  color: var(--brand-dark-green); font-size: 1.2rem; line-height: 1; cursor: pointer;
}
.home-carousel-btn:hover { background: var(--brand-lime-light); }
/* AT THE END OF A STRIP THAT STOPS, the arrow pointing past it is greyed (js/home-sections.js
   syncArrows marks it aria-disabled). Live's arrows stay solid and the press simply does nothing;
   a greyed arrow says the same thing without looking broken, which is what the sixth QA round asked
   for (store-home #2 and #4). Still focusable and still the same size — only the look changes. */
.home-carousel-btn[aria-disabled="true"] { opacity: 0.35; cursor: default; }
.home-carousel-btn[aria-disabled="true"]:hover { background: var(--brand-white); }

/* ---- Section 5: the product spotlight --------------------------------------------- */
/* FULL-BLEED AND DARK, as the live slider is — it spans the viewport rather than the content column,
   breaking out the same way the trust strip and the hero banner do so the markup stays in reading
   order inside <main> instead of being hoisted into a layout wrapper. */
.home-spotlight {
  margin-inline: calc(50% - 50vw);
  width: 100vw; max-width: 100vw;
  background: var(--brand-dark-green);
  /* #F5F2EB, which is what fps-slider--dark sets --fps-fg to on the live slider — not pure white. */
  color: var(--brand-cream);
  /* No padding of its own: the SLIDE carries live's 80px over and 60px under its content
     (.fps-slider__slide, measured 13 Sep 2026) and the nav carries the 40px to the band's foot.
     Live's band is 968px tall at 1280 and this one 784: the difference is the subscription copy
     ("Subscribe now for 50% off…", "then $150/order", "Delivers every 30 days") and the feature
     chips, which this platform has no subscription system or field for (see the top of
     js/home-sections.js). Ours was 620px. */
  padding: 0;
  margin-block: 0 0;
}
/* One slide per screenful, scroll-snapped. The track is the scroller, so touch and keyboard work
   with no JavaScript and the arrows are only the mouse affordance. */
.home-spotlight-track {
  display: grid; grid-auto-flow: column; grid-auto-columns: 100%;
  /* PROXIMITY, NOT MANDATORY, on a track whose arrows scroll it programmatically. A `mandatory`
     scroller is re-snapped to its current snap point while a smooth programmatic scroll is still in
     flight, which is a well-known way for a next/prev button to animate and land exactly where it
     started. The arrows move the track by exactly one slide, so it still comes to rest on a slide
     edge; proximity only stops the browser fighting the animation on the way there. It is also what
     the other two scroll containers in this stylesheet already use (.home-row-strip,
     .home-ugc-track), so all three behave the same way. */
  overflow-x: auto; scroll-snap-type: x proximity;
  /* The scrollbar would sit across the bottom of a dark band. The track is still scrollable by every
     other means, and the arrows are always there. */
  scrollbar-width: none;
  /* 1290px, live's .fps-slider__container (20px of side padding inside it, on the slide below). */
  max-width: 1290px; margin: 0 auto;
  /* POSITIONED, AND THAT ONE WORD IS LOAD-BEARING — see the note on the sideways-scrolling page
     under .home-row-strip. Without it the visually-hidden "Regular price " spans inside these slides
     are laid out against an ancestor OUTSIDE this scroller, escape its clipping, and made the whole
     document 4,650px wide on a 1,600px screen: the page could be dragged sideways into an empty
     white field. Measured, both ways, in a browser. */
  position: relative;
}
.home-spotlight-track::-webkit-scrollbar { display: none; }
.home-spotlight-track:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 4px; }
/* COPY LEFT, PICTURE RIGHT, in live's proportions: its .fps-slider__grid is two columns of 1fr and
   1.2fr with an 80px gap (527px and 633px inside the 1240px container at 1280), the copy column
   vertically centred on the picture. Note for the next side-by-side pass: a full-page capture of
   live catches its 0.5s slide transition mid-flight and shows the PREVIOUS slide's picture on the
   left with the next slide's copy on the right and neighbours peeking at both edges — that is the
   animation, not the layout. At rest (live-spot-1280-t6.png, and its own markup's "LEFT: Content /
   RIGHT: Image" comments) it is copy left, picture right, one slide to a screen. */
.home-spotlight-slide {
  scroll-snap-align: start;
  display: grid; grid-template-columns: 1fr 1.2fr; gap: 80px; align-items: center;
  padding: 80px 20px 60px;
}
/* THE BADGE, when a product tag supplies one (spotlightBadge in js/home-sections.js): live's
   .fps-slider__badge — a #DCF3B5 pill, 11px display face, weight 600, letterspaced capitals, 6px/16px,
   20px under it. Nothing is drawn when there is no tag. */
.home-spotlight-badge-row { margin: 0 0 20px; }
.home-spotlight-badge {
  display: inline-block; padding: 6px 16px; border-radius: var(--radius-pill);
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  font-family: var(--font-display); font-size: 11px; font-weight: 600; line-height: 16px;
  letter-spacing: 0.12em; text-transform: uppercase;
}
/* THE EYEBROW: live's .fps-slider__brand — 14px, weight 500, the foreground at 55%, 8px under it,
   sentence case. It read "420.COM" here in lime letterspaced capitals, which is the field's NAME in
   the theme; live prints "Exclusive Curation" in it on every slide. */
.home-spotlight-eyebrow {
  display: inline-block; margin: 0 0 8px; font-size: 14px; font-weight: 500; letter-spacing: 0.02em;
  color: rgba(245, 242, 235, 0.55); text-decoration: none;
}
a.home-spotlight-eyebrow:hover, a.home-spotlight-eyebrow:focus { color: var(--brand-cream); text-decoration: underline; }
/* 64px/64px, live's .fps-slider__title (36px/36px on a phone), 16px under it. Ours peaked at 48px. */
.home-spotlight-title { margin: 0 0 16px; font-size: 64px; line-height: 1; color: var(--brand-cream); }
.home-spotlight-title a { color: inherit; text-decoration: none; }
.home-spotlight-title a:hover { text-decoration: underline; }
/* The tagline is 16px on a 1.55 line at 55% of the cream, 440px wide — live's .fps-slider__tagline;
   ours was pale lime, which read as a second heading. */
.home-spotlight-tagline { margin: 0 0 24px; font-size: 16px; line-height: 1.55; color: rgba(245, 242, 235, 0.55); max-width: 440px; }
/* THE PRICE IS 32px/700 (live's .fps-slider__price), the struck one 16px at 55%, the saving a pale
   lime pill in 12px capitals. Ours ran the whole row at 21.6px. */
.home-spotlight-price { display: flex; align-items: center; gap: 12px; margin: 0 0 24px; font-size: 32px; font-weight: 700; flex-wrap: wrap; }
.home-spotlight-price .price-was { color: rgba(245, 242, 235, 0.55); margin-left: 0; font-size: 16px; font-weight: 400; }
.home-spotlight-save {
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  border-radius: var(--radius-pill); padding: 4px 10px;
  font-size: 12px; line-height: 16px; font-weight: 700;
}
/* THE BUY ROW IS 56px TALL, as live's is: a round stepper on the dark band (cream figures on an 8%
   cream ground, 116px wide) and a PALE lime button — #dcf3b5, live's .fps-slider__btn, with 16px/600
   dark-green text — not the bright lime ours drew at 42px. */
.home-spotlight-buy { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin: 0 0 24px; }
.home-spotlight-buy .qty-stepper {
  height: 56px; min-width: 116px; border: 0; border-radius: var(--radius-pill);
  background: rgba(245, 242, 235, 0.08); color: var(--brand-cream);
}
.home-spotlight-buy .qty-stepper button { color: var(--brand-cream); background: transparent; height: 56px; width: 40px; font-size: 18px; }
.home-spotlight-buy .qty-stepper input { color: var(--brand-cream); background: transparent; height: 56px; width: 36px; font-weight: 500; font-size: 16px; }
.home-spotlight-add {
  flex: 1 1 220px; height: 56px;
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  border: 0; border-radius: var(--radius-pill);
  padding: 0 24px; font: inherit; font-size: 16px; font-weight: 600; cursor: pointer;
}
.home-spotlight-add:hover { filter: brightness(1.03); }
.home-spotlight-soldout { margin: 0 0 24px; font-weight: 700; color: var(--brand-lime-light); }
/* "View full details →", 14px/500 underlined in the cream — live's .fps-slider__details-link. */
.home-spotlight-details {
  display: inline-block; margin: 0;
  font-size: 14px; font-weight: 500;
  color: var(--brand-cream); text-decoration: underline; text-underline-offset: 3px;
}
.home-spotlight-details::after { content: " →"; text-decoration: none; display: inline-block; margin-left: 4px; }
/* The three assurances, 13px at 55% of the cream with a tick before each, 24px under the link. */
.home-spotlight-trust {
  display: flex; flex-wrap: wrap; gap: 8px 24px;
  list-style: none; margin: 32px 0 0; padding: 0;
  font-size: 13px; color: rgba(245, 242, 235, 0.55);
}
.home-spotlight-trust li::before { content: "✓ "; color: var(--brand-cream); font-weight: 700; }
/* A square frame, so a slide does not change height when the next photograph is a different shape —
   which on a full-bleed band means the whole page moving under the reader. Live's frame is 538px
   inside a 560px cap with 20px corners (.fps-slider__image-frame); the photographs are packshots on
   their own white ground, so the frame is white behind them. */
.home-spotlight-art img {
  display: block; width: 100%; max-width: 560px; margin-inline: auto;
  aspect-ratio: 1 / 1; object-fit: cover; border-radius: 20px; background: var(--brand-white);
}
/* THE SLIDER'S CONTROLS ARE CENTRED UNDER THE SLIDE — prev, the dots, next — as live's
   .fps-slider__nav is (44px round outlined arrows on the dark band, 16px apart, the dots between
   them). Ours were the light-band arrows, filled white, huddled bottom-right with no dots at all, so
   nothing on the slide said there were five products or which one this was. The 40px under them is
   the band's foot (live: nav bottom at 3042 = section bottom). */
.home-spotlight .home-carousel-nav { justify-content: center; align-items: center; gap: 16px; margin-top: 0; padding: 0 20px 40px; }
.home-spotlight .home-carousel-btn {
  width: 44px; height: 44px; background: transparent;
  border: 1.5px solid rgba(245, 242, 235, 0.55); color: var(--brand-cream);
}
.home-spotlight .home-carousel-btn:hover { background: rgba(245, 242, 235, 0.08); border-color: var(--brand-cream); }
/* One dot per slide, 10px, the foreground at 55%; the current one is a 28px lime pill
   (live's .fps-slider__dot / __dot--active on the dark variant). */
.home-carousel-dots { display: flex; align-items: center; gap: 8px; }
.home-carousel-dot {
  width: 10px; height: 10px; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(245, 242, 235, 0.55); transition: width 0.3s ease, background 0.3s ease;
}
.home-carousel-dot[aria-current="true"] { width: 28px; border-radius: 10px; background: var(--brand-lime-light); }
@media (prefers-reduced-motion: reduce) { .home-carousel-dot { transition: none; } }
@media (max-width: 900px) {
  /* The photograph goes ON TOP on a phone, as it does on the live slider: the product is what makes
     somebody stop scrolling, and the copy is what they read once they have. Live at 390: a 322px
     square picture, the title at 36px/36px, the buy row stacked (the stepper centred over a
     full-width button), 60px over the slide and 40px under the nav. */
  .home-spotlight-slide { grid-template-columns: 1fr; gap: 40px; padding: 60px 20px 40px; }
  .home-spotlight-art { order: -1; }
  .home-spotlight-art img { max-width: 322px; }
  .home-spotlight-title { font-size: 36px; }
  .home-spotlight-price { font-size: 28px; }
  .home-spotlight-buy { flex-direction: column; align-items: stretch; }
  .home-spotlight-buy .qty-stepper { align-self: center; }
  .home-spotlight-add { flex: 0 0 auto; width: 100%; }
  .home-spotlight-details { display: block; text-align: center; }
  .home-spotlight .home-carousel-nav { padding-bottom: 40px; }
}

/* ---- Section 7: BUILD YOUR BUNDLE -------------------------------------------------
   Read off the live block rather than styled by taste: it is a full-bleed #1a391f band, the heading
   and body are WHITE, and the badge and the call-to-action are both #dcf3b5 pills with dark-green
   text — not a dark button on a light card, which is what this was first drafted as. */
.home-bundle {
  margin-inline: calc(50% - 50vw);
  width: 100vw; max-width: 100vw;
  /* LIVE'S OWN TEXTURED BACKGROUND — its block is `background-color: #1a391f` under
     `background-image: url(bundle-banner.jpg); background-size: cover; background-position: center`,
     a smoky black-to-dark-green wash that is black along the bottom. That is not decoration: the
     product photograph is a PNG on a black ground with a reflection running to its bottom edge, and
     on a flat #1a391f band the photograph's black rectangle showed as a hard-edged box (both
     side-by-side passes, 13 Sep 2026). On live's wash the black meets black and the edge is gone.
     The file is live's, copied home from its CDN (67KB, 1920x767) beside the category and effect
     artwork; the solid colour stays underneath for the moment before it arrives. */
  background: var(--brand-dark-green) url("../img/bundle-banner.jpg") center / cover no-repeat;
  color: var(--brand-white);
  /* 80px over the copy and NOTHING under the artwork, as live's block is padded (padding-top 80px,
     padding-bottom 0, the copy column carrying 40px of its own): the photograph's reflection runs
     to the band's bottom edge instead of floating in it. */
  /* 40px of side padding and a 40px gap, live's .wm-bundle-wrapper (max-width 1400px): at 1280 that
     is two 580px columns with the copy at x=40 and the artwork at x=660, which is where live's are. */
  padding: 80px 40px 0;
  margin-block: 0 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: center;
}
/* ONE COLUMN WHEN THERE IS NO ARTWORK, which is the state every shop is in before a merchandiser
   fills in bundle_promo_image_url — and `bundle_promo_image_url` is "" on the preview today, so this
   is what is actually on the page rather than a hypothetical.
   `grid-template-columns: 1fr 1fr` with one child leaves the second column empty, and because
   .home-bundle-copy is `justify-self: end` the copy hugs the middle of a full-bleed band with the
   whole right half blank dark green — it reads as artwork that failed to load, which is worse than a
   block that was never given any. Exactly the rule .home-faq already carries for the same situation
   (see `.home-faq:not(:has(.home-faq-art))`), written the same way so the two cannot drift.

   THE SECOND DECLARATION IS NOT COSMETIC. Dropping to one column while .home-bundle-copy is still
   `justify-self: end` would move the 550px block from the middle of the band to its far RIGHT edge —
   a worse layout than the one being fixed — so the single-column case re-centres it. */
.home-bundle:not(:has(.home-bundle-art)) { grid-template-columns: 1fr; }
.home-bundle:not(:has(.home-bundle-art)) .home-bundle-copy { justify-self: center; }
/* The band is full-bleed; its CONTENTS still line up with the rest of the page: two 580px columns,
   which is what live's 1400px wrapper gives at 1280. */
.home-bundle > * { max-width: 580px; }
.home-bundle-copy { justify-self: end; width: 100%; padding-bottom: 40px; }
.home-bundle-art { justify-self: start; width: 100%; }
/* "SAVE UP TO 20% OFF": live's .wm-bundle-badge — 12px/800 capitals on a #dcf3b5 pill, 6px/15px,
   15px over the heading. What it says comes from the setting or from the server (bundleSaving in
   js/home-sections.js); this only draws it. */
.home-bundle-badge {
  display: inline-block; margin: 0 0 15px;
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  border-radius: var(--radius-pill); padding: 6px 15px;
  font-size: 12px; font-weight: 800; text-transform: uppercase; line-height: 1.5;
}
/* 48px/48px with 20px under it (28px on a phone), live's .wm-bundle-h2; the copy 16px on a 1.6
   line, 500px wide at most, which is where live's paragraphs wrap. */
.home-bundle h2 { margin: 0 0 20px; font-size: 48px; line-height: 1; color: var(--brand-white); }
.home-bundle p { margin: 0 0 0.9rem; max-width: 500px; font-size: 16px; line-height: 1.6; color: var(--brand-white); }
/* Live's .wm-bundle-link: the words, an 8px gap, then a 12px diagonal arrow in a 20px round seat —
   the arrow is the SVG js/home-sections.js draws into the link. */
.home-bundle-cta {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 0.6rem;
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  padding: 10px 30px; border-radius: var(--radius-pill);
  /* 48px tall, as live's (200×48 at both widths, measured 13 Sep 2026): 14px type on a 28px line
     inside the 10px of padding. This inherited the body's 1.5 line and came out 37px — the one
     control in the section under 44px on a phone, beside a 52px "Shop all" and 56px "Add" buttons
     (the fifth QA round, store-home #2). min-height as well, so a wrapped label can only grow it. */
  font-weight: 700; font-size: 14px; line-height: 28px; min-height: 48px; text-decoration: none;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.home-bundle-cta:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(184, 232, 107, 0.35); }
.home-bundle-cta-arrow { width: 12px; height: 12px; flex: none; }
.home-bundle-art img { display: block; width: 100%; height: auto; }
@media (max-width: 900px) {
  /* The live block collapses to one centred column below its breakpoint (60px over the copy, still
     nothing under the artwork). The copy comes FIRST there — live's mobile order is badge, heading,
     text, button, then the picture — so the photograph's black foot still sits on the band's edge. */
  .home-bundle { grid-template-columns: 1fr; gap: 1.75rem; padding: 60px 20px 0; text-align: center; }
  .home-bundle > * { max-width: none; }
  .home-bundle-copy, .home-bundle-art { justify-self: stretch; }
  .home-bundle-copy { padding-bottom: 0; }
  .home-bundle h2 { font-size: 28px; }
  .home-bundle p { margin-inline: auto; }
  .home-bundle-art { order: 1; }
}

/* ---- Section 9: the featured brand -----------------------------------------------
   LIVE'S CARD, read off .featured-brand on www.420.com (13 Sep 2026, re-measured 14 Sep 2026): the
   band is 480px tall at 1280 with the prev/next pair centred along its top edge. The CREAM (#f5f2eb)
   card is the LEFT 672px only — live's .featured-brand__content, padded 32px — holding "Featured
   brand" in 16px sentence case, the name at 60px/64px in the display face with 20px under it, the
   description 16px on a 1.6 line, 500px wide, then a 52px dark-green pill "Shop <brand> ↗"
   (14px/500, 12px/24px). The RIGHT 608px is the picture's own panel, edge to edge on a white
   ground (.featured-brand__image-desktop: the brand's logo file, which carries its own white,
   covering the panel), and the whole 1280×480 is clipped to a 12px radius (.featured-brand__wrapper,
   overflow hidden). Ours was ONE cream card the full 1280 wide with a small photograph floating in
   the middle of the right half over the cream, so the right half read as mostly empty (QA round 11,
   home #3). The picture stays the product photograph the vendor facet carries — the brand-LOGO
   decision stands (see the note at the top of js/home-sections.js) — contained in live's white
   panel rather than blended onto cream. On a phone the picture goes on top (366x200, its panel
   white too) and the arrows sit beside the label, exactly as live's mobile header row does. The
   section is positioned so the nav can be placed against it. */
.home-brands { margin: 0 0 40px; border-radius: 12px; position: relative; }
.home-brand-track {
  display: grid; grid-auto-flow: column; grid-auto-columns: 100%;
  /* proximity, for the reason spelled out on .home-spotlight-track above. */
  overflow-x: auto; scroll-snap-type: x proximity; scrollbar-width: none;
  /* positioned, for the reason spelled out on .home-row-strip above. */
  position: relative;
  /* The radius on the SCROLL BOX: an overflow other than visible clips to the rounded padding box,
     so the white picture panel's outer corners follow live's 12px without a rule of their own. */
  border-radius: 12px;
}
.home-brand-track::-webkit-scrollbar { display: none; }
.home-brand-track:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 3px; }
.home-brand-slide {
  scroll-snap-align: start;
  /* 21 : 19 is live's split at 1280 — 672 cream, 608 picture — and within a few pixels of it at
     1024 (live 544; this gives 538). Two columns that each paint their own ground, which is why the
     cream moved off the slide and onto the copy column below. */
  display: grid; grid-template-columns: minmax(0, 21fr) minmax(0, 19fr); align-items: stretch;
  min-height: 480px; padding: 0;
}
/* THE CREAM CARD is the copy column, its content centred down the 480px as live's flex column is
   (justify-content: center) — not the whole slide. */
.home-brand-copy {
  display: flex; flex-direction: column; justify-content: center; align-items: flex-start;
  padding: 80px 32px 32px; background: var(--brand-cream); color: var(--brand-dark-green);
}
/* THE PICTURE'S PANEL: white, the full height of the band, the photograph contained inside it with
   32px of air — live's panel is filled edge to edge by a logo on white; a product photograph with
   cover would crop the product, so contain on the same white ground is the honest version. */
.home-brand-art {
  display: flex; align-items: center; justify-content: center;
  padding: 32px; background: var(--brand-white);
}
/* multiply so the photograph's own white ground melts into whatever is behind it — nothing on the
   desktop's white panel, and no white box on the phone's cream card. */
.home-brand-art img {
  display: block; width: 100%; max-height: 416px; margin-inline: auto;
  object-fit: contain; mix-blend-mode: multiply;
}
.home-brand-label {
  margin: 0 0 12px; font-size: 16px; line-height: 28px; font-weight: 400;
  letter-spacing: 0; text-transform: none; color: var(--brand-dark-green);
}
.home-brand-name { margin: 0 0 20px; font-size: 60px; line-height: 64px; color: var(--brand-dark-green); }
.home-brand-about { margin: 0 0 24px; font-size: 16px; line-height: 1.6; color: var(--brand-dark-green); max-width: 500px; }
.home-brand-cta {
  display: inline-flex; align-items: center; gap: 8px; min-height: 52px;
  background: var(--brand-dark-green); color: var(--brand-white);
  padding: 12px 24px; border-radius: var(--radius-pill);
  font-size: 14px; font-weight: 500; line-height: 28px; text-decoration: none;
}
.home-brand-cta:hover { background: #245029; }
.home-brand-cta-arrow { width: 20px; height: 20px; flex: none; }
/* The arrows: 36px, no ring, in the green, along the CREAM CARD's top edge on a desktop — live's
   .featured-brand__nav--desktop sits inside .featured-brand__content at y+24, its right edge 24px
   in from the card's right edge (x 572–648 on the 672px card at 1280; re-measured 14 Sep 2026).
   They were centred on the whole band, which put the Next arrow on the white picture panel once
   the card stopped spanning it. Measured from the right: the picture panel's 47.5% plus the 24px. */
.home-brands .home-carousel-nav {
  position: absolute; top: 24px; right: calc(47.5% + 24px); z-index: 1;
  margin: 0; gap: 4px;
}
.home-brands .home-carousel-btn { width: 36px; height: 36px; border: 0; background: transparent; font-size: 1.5rem; }
.home-brands .home-carousel-btn:hover { background: rgba(26, 57, 31, 0.08); }
@media (max-width: 900px) {
  /* Live at 390: the picture on top, 200px tall; then the label with the arrows at its right; the
     name at 32px/36px; 24px of padding. The picture's 200px stays on the CREAM here — the whole
     366px column is one card on the phone (live's .featured-brand__wrapper is the cream, its mobile
     image box laid over the top of it), and the round-11 side-by-side found this shape already
     matched live's; the white panel is the desktop's, where the picture stands beside the card. */
  .home-brand-slide { grid-template-columns: 1fr; min-height: 0; }
  .home-brand-art { order: -1; padding: 0; background: var(--brand-cream); }
  .home-brand-art img { height: 200px; max-height: none; width: 100%; padding: 16px; }
  .home-brand-copy { padding: 24px; }
  .home-brand-label { margin-bottom: 8px; padding-right: 88px; }
  .home-brand-name { font-size: 32px; line-height: 36px; margin-bottom: 16px; }
  /* The button runs the card's width on a phone (live's 366x52), centred. */
  .home-brand-cta { display: flex; width: 100%; justify-content: center; }
  .home-brands .home-carousel-nav { top: calc(200px + 20px); left: auto; right: 20px; transform: none; }
}

/* ---- Section 10: the testimonial marquee ------------------------------------------
   Built exactly like the trust strip above, for the same reasons and with the same reduced-motion
   behaviour: the list is emitted twice so the loop closes on an identical frame, and somebody who
   has asked for less movement gets the quotes laid out still instead of a strip travelling under
   them. */
/* 60px over and under, live's .testimonials-section. */
.home-testimonials { margin: 0; padding: 60px 0; }
/* NOT ON A PHONE. Live's .testimonials-section is `display: none` and only becomes a flex column
   from 768px up (its --show-mobile modifier is not set on the home page): at 390 the section
   measures 0px tall and the unboxing videos follow the featured brand directly (13 Sep 2026). Ours
   ran the marquee at every width, 251px of it. Hidden by the stylesheet at live's breakpoint rather
   than left out by the script, so the section is still there for anything wider. */
@media (max-width: 767px) { .home-testimonials { display: none; } }
/* 48px/48px, centred, wrapping inside 480px so it breaks as live's does ("DON'T JUST TAKE OUR /
   WORD FOR IT"), 48px over the cards. Ours was 24px. */
.home-testimonials h2 { margin: 0 auto 48px; font-size: 48px; line-height: 48px; max-width: 480px; text-align: center; color: var(--brand-dark-green); }
.home-testimonial-strip {
  overflow: hidden;
  margin-inline: calc(50% - 50vw);
  width: 100vw; max-width: 100vw;
  padding-inline: 1.5rem;
}
/* 50s a loop, live's --testimonials-scroll-speed. */
.home-testimonial-track {
  display: flex; gap: 32px; width: max-content;
  animation: home-testimonial-scroll 50s linear infinite;
}
.home-testimonial-strip:hover .home-testimonial-track { animation-play-state: paused; }
/* The duplicate is a flex child of the same track, so its own cards have to keep flowing in the
   same row rather than stacking inside it. */
.home-testimonial-copy { display: flex; gap: 32px; }
/* TWO CARDS TO A SCREEN, 640x300 with 48px inside, 32px apart — live's .testimonials-section__card
   (13 Sep 2026). Ours were four 320px cards with 20px inside and 15px copy, which is the same five
   quotes at half the size. The author sits at the card's foot, as live's does. */
.home-testimonial {
  flex: 0 0 640px; min-height: 300px;
  display: flex; flex-direction: column; gap: 16px;
  background: var(--brand-cream); border-radius: 16px;
  margin: 0; padding: 48px;
}
/* Five green star icons, 20px each (the SVG js/home-sections.js draws), not gold text glyphs. */
.home-testimonial-stars { display: flex; gap: 4px; color: var(--brand-lime); }
.home-testimonial-stars svg { width: 20px; height: 20px; fill: currentColor; }
.home-testimonial blockquote { margin: 0; font-size: 20px; line-height: 24px; font-weight: 500; color: var(--brand-dark-green); }
.home-testimonial figcaption { margin-top: auto; font-weight: 400; font-size: 15px; color: rgba(0, 0, 0, 0.6); }
@keyframes home-testimonial-scroll {
  from { transform: translateX(0); }
  /* -50%, because the list is duplicated exactly once. */
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .home-testimonial-track { animation: none; width: auto; flex-wrap: wrap; }
  /* The duplicate exists only to make the scroll seamless; with no scroll it is the list said twice. */
  .home-testimonial-copy { display: none; }
}

/* ---- Section 11: the unboxing videos ---------------------------------------------- */
/* Live's .ugc-section: 30px over, 62px under, 20px in from the column's edge (the heading and
   the first card at x=40 on a 1280 screen); 15px / 25px / 18px on a phone. */
.home-ugc { margin: 0; padding: 30px 20px 62px; }
.home-ugc-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; }
.home-ugc-head .home-carousel-nav { margin-top: 0; }
.home-ugc-eyebrow {
  margin: 0 0 0.25rem; font-size: 11px; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--brand-dark-green);
}
/* "SEE WHAT MEMBERS ARE GETTING" at 60px/64px in the green (48px from 768 to 1023, 32px/36px on a
   phone), 50px over the cards — live's .ugc-section h2. Ours was 24px. */
.home-ugc h2 { margin: 0 0 50px; font-size: 60px; line-height: 64px; color: var(--brand-dark-green); }
/* 280x544 cards, 10px apart (200x402 on a phone), on the cream with 10px corners — live's .ugc-card. */
.home-ugc-track {
  display: grid; grid-auto-flow: column; grid-auto-columns: 280px;
  gap: 10px; overflow-x: auto; scroll-snap-type: x proximity;
  padding-bottom: 0.5rem;
  /* positioned, for the reason spelled out on .home-row-strip above. */
  position: relative;
}
.home-ugc-track:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 3px; }
.home-ugc-card {
  scroll-snap-align: start;
  border: 1px solid var(--brand-hairline); border-radius: 10px; overflow: hidden;
  background: var(--brand-cream);
}
/* 9:16, which is what a phone films. aspect-ratio rather than the padding-top trick so the card
   reserves its height before the iframe loads and the row does not jump. */
.home-ugc-video { position: relative; aspect-ratio: 9 / 16; background: #11220f; }
.home-ugc-video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.home-ugc-who { padding: 0.75rem; }
.home-ugc-name { margin: 0 0 0.2rem; font-weight: 700; font-size: 0.9rem; }
.home-ugc-quote { margin: 0; font-size: 0.8rem; color: #4a5c4d; }
@media (max-width: 1023px) {
  .home-ugc h2 { font-size: 48px; line-height: 52px; }
}
@media (max-width: 767px) {
  .home-ugc { padding: 15px 18px 25px; }
  /* 12px under the heading on a phone (live's h2 margin at 390); the head row adds 20px — see the
     unboxing note in the phone-headings block further down. */
  .home-ugc h2 { margin-bottom: 12px; font-size: 32px; line-height: 36px; }
  .home-ugc-track { grid-auto-columns: 200px; }
}

/* ---- Section 12: the FAQ ---------------------------------------------------------- */
.home-faq { display: grid; grid-template-columns: 1fr 1.4fr; gap: 2.5rem; margin: 0 0 2.5rem; }
/* One column when there is no illustration — a 1fr gap holding nothing would push the accordion into
   half the page for no reason. */
.home-faq:not(:has(.home-faq-art)) { grid-template-columns: 1fr; }
.home-faq-art img { display: block; width: 100%; height: auto; border-radius: 16px; }
.home-faq h2 { margin: 0 0 1rem; font-size: 24px; }
.home-faq-item { border-top: 1px solid var(--brand-hairline); }
.home-faq-item:last-of-type { border-bottom: 1px solid var(--brand-hairline); }
.home-faq-item summary {
  cursor: pointer; padding: 0.9rem 0; font-weight: 600;
  /* The browser's default marker is a triangle tucked against the text. A list-item marker with the
     text indented reads as a row, which is what the live accordion looks like. */
  list-style: none; display: flex; justify-content: space-between; gap: 1rem;
}
.home-faq-item summary::-webkit-details-marker { display: none; }
.home-faq-item summary::after { content: "+"; font-weight: 400; font-size: 1.3rem; line-height: 1; }
.home-faq-item[open] summary::after { content: "−"; }
.home-faq-item summary:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 2px; }
.home-faq-item p { margin: 0 0 1rem; color: #4a5c4d; }
.home-faq-more { display: inline-block; margin-top: 1rem; color: var(--brand-dark-green); font-weight: 600; }
@media (max-width: 900px) {
  .home-faq { grid-template-columns: 1fr; gap: 1.5rem; }
}

/* ==============================================================================================
   THE QUANTITY LADDER, AND THE SOLD-OUT WAITING LIST — both live in the product buy box.
   See storefront/product.html (paintQuantityBreaks / paintBackInStock) for what each one says and
   why; this file only decides how it looks.
   ============================================================================================== */

/* Toggled with the `hidden` attribute rather than a :empty rule, because both blocks have content
   they are deliberately withholding (no tiers apply; the option is in stock) — a different state
   from a container somebody failed to fill, and one that must not be confused with it. */
.quantity-breaks { margin: 0 0 1rem; }
.quantity-breaks-title {
  margin: 0 0 0.5rem; font-weight: 600; font-size: 0.95rem; color: var(--brand-dark-green);
}
.quantity-breaks-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
/* A real button that fills its row, so the whole rung is the target rather than the few words in it —
   on a phone the difference between a 44px row and a text link is whether this block gets used. */
.quantity-break {
  width: 100%; display: grid; grid-template-columns: 3.5rem 1fr auto; align-items: center;
  gap: 0.75rem; min-height: 44px; padding: 0.5rem 0.85rem; text-align: left; cursor: pointer;
  background: var(--brand-white); color: inherit; font: inherit;
  border: 1px solid var(--brand-hairline); border-radius: var(--radius);
}
.quantity-break:hover { border-color: var(--brand-lime); }
.quantity-break:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 2px; }
/* The rung the current quantity has actually earned. Marked with a border and a tint, NOT with colour
   alone — the same rung also carries aria-current, so the state is available to somebody who cannot
   see the tint at all. */
.quantity-break.is-active {
  border-color: var(--brand-dark-green); background: var(--brand-lime-light);
}
.quantity-break-qty { font-weight: 700; }
.quantity-break-unit { color: var(--brand-ink); }
.quantity-break-save { font-weight: 600; color: var(--brand-dark-green); white-space: nowrap; }
.quantity-breaks-note { margin: 0.45rem 0 0; font-size: 0.85rem; color: #4a5c4d; }

.back-in-stock { margin: 0 0 1rem; }
.back-in-stock-form {
  padding: 0.9rem 1rem; border: 1px solid var(--brand-hairline); border-radius: var(--radius);
  background: var(--brand-cream);
}
.back-in-stock-title { margin: 0 0 0.25rem; font-weight: 700; }
.back-in-stock-label { display: block; margin: 0 0 0.5rem; font-size: 0.9rem; color: #4a5c4d; }
/* Wraps to two rows on a narrow screen rather than squeezing the field to nothing — an email input a
   shopper cannot read what they typed into is a bounced alert. */
.back-in-stock-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.back-in-stock-row input[type="email"] {
  flex: 1 1 12rem; min-width: 0; min-height: 44px; padding: 0.5rem 0.75rem;
  border: 1px solid var(--brand-hairline); border-radius: var(--radius); font: inherit;
}
.back-in-stock-row button { flex: 0 0 auto; }
/* Reserves no space when empty, so the panel does not jump the moment an answer arrives. */
.back-in-stock-note { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--brand-dark-green); }
.back-in-stock-note:empty { display: none; }

/* ---- PURCHASE OPTIONS: ONE TIME vs SUBSCRIBE & SAVE ------------------------------------------
   The product page's subscribe block (storefront/product.html). Plain CSS, no new font, no new
   host, nothing external — the CSP is untouched by this feature.

   The selected state is driven by a data attribute rather than :has() or :checked + sibling, because
   the block is re-rendered on every variant and quantity change and the attribute is set by the same
   code that writes the radio's `checked` — one source of truth for "which option is chosen" instead
   of two that can disagree for a frame. */
.purchase-options { display: grid; gap: 8px; margin: 0 0 14px; }
.purchase-option {
  display: flex; gap: 10px; align-items: flex-start;
  border: 1.5px solid #e3e3e3; border-radius: 10px; padding: 12px 14px;
  cursor: pointer; background: #fff;
  transition: border-color .12s ease, background-color .12s ease;
}
.purchase-option:hover { border-color: #c9c9c9; }
.purchase-option[data-selected="true"] { border-color: #1f7a3d; background: #f4fbf6; }
.purchase-option input { margin: 3px 0 0; flex: 0 0 auto; }
.purchase-option__body { flex: 1 1 auto; min-width: 0; }
.purchase-option__head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
.purchase-option__title { font-weight: 650; }
.purchase-option__price { font-weight: 650; }
.purchase-option__was { color: #767676; text-decoration: line-through; font-weight: 400; }
.purchase-option__save {
  font-size: .78rem; font-weight: 650; letter-spacing: .02em; text-transform: uppercase;
  background: #1f7a3d; color: #fff; border-radius: 999px; padding: 2px 8px;
}
.purchase-option__note { display: block; font-size: .86rem; color: #555; margin: 4px 0 0; line-height: 1.45; }

/* ---- THE SUBSCRIPTION SUMMARY ON THE CHECKOUT ------------------------------------------------
   What the shopper is agreeing to, stated above the pay button. Not styled as fine print, on
   purpose: a recurring charge somebody does not remember agreeing to is the commonest cause of a
   chargeback, and a chargeback costs the fee, the goods and the merchant account's standing. */
.subscription-terms {
  border: 1.5px solid #1f7a3d; background: #f4fbf6; border-radius: 10px;
  padding: 12px 14px; margin: 0 0 14px;
}
.subscription-terms h3 { margin: 0 0 6px; font-size: 1rem; }
.subscription-terms p { margin: 0; font-size: .9rem; line-height: 1.5; color: #2c2c2c; }

/* ---- SUBSCRIPTIONS ON THE ACCOUNT PAGE -------------------------------------------------------- */
.subscription-card {
  border: 1px solid #e3e3e3; border-radius: 10px; padding: 14px 16px; margin: 0 0 12px; background: #fff;
}
.subscription-card__head { display: flex; flex-wrap: wrap; gap: 10px; align-items: baseline; justify-content: space-between; }
.subscription-card__title { font-weight: 650; margin: 0; }
.subscription-card__status {
  font-size: .76rem; font-weight: 650; letter-spacing: .03em; text-transform: uppercase;
  border-radius: 999px; padding: 2px 9px; background: #eee; color: #333;
}
.subscription-card__status[data-status="active"] { background: #1f7a3d; color: #fff; }
.subscription-card__status[data-status="payment_failed"],
.subscription-card__status[data-status="blocked"] { background: #a6271a; color: #fff; }
.subscription-card__status[data-status="paused"] { background: #8a6d00; color: #fff; }
.subscription-card__status[data-status="cancelled"] { background: #666; color: #fff; }
.subscription-card ul { margin: 8px 0; padding-left: 1.2rem; }
.subscription-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.subscription-card__actions button { font-size: .9rem; }
.subscription-card__warn { color: #a6271a; font-weight: 600; margin: 8px 0 0; }
.subscription-restore { border: 1.5px dashed #1f7a3d; border-radius: 10px; padding: 14px 16px; margin: 0 0 14px; }

@media (prefers-color-scheme: dark) {
  .purchase-option { background: #1a1a1a; border-color: #333; }
  .purchase-option[data-selected="true"] { background: #10241a; border-color: #36a35b; }
  .purchase-option__note { color: #b5b5b5; }
  .purchase-option__was { color: #9a9a9a; }
  .subscription-terms { background: #10241a; border-color: #36a35b; }
  .subscription-terms p { color: #ddd; }
  .subscription-card { background: #1a1a1a; border-color: #333; }
  .subscription-card__status { background: #2a2a2a; color: #ddd; }
}

/* ==============================================================================================
   THE PRODUCT PAGE, MEASURED AGAINST THE LIVE ONE (www.420.com, 2026-09-13) — the option pills,
   the "You May Also Like" carousel, the "Where We Ship" block, the FAQ accordion and the floating
   buy bar. Every figure below was read off the live page's own stylesheet or computed styles rather
   than chosen; the selector each one came from is named beside it.
   ============================================================================================== */

/* ---- The option pills (live: .pdp-420__variant-option) ---------------------------------------
   One <fieldset> per option, its <legend> the option's name in 16px/700, then the values as pills:
   48px minimum, 10px 18px, cream ground, 1.5px transparent border, 800px radius, 15px/500. The
   chosen one is filled dark green; a sold-out one is transparent, 40% ink, and struck through with
   a 1.5px line rotated -8deg — the live theme's own strike, drawn with ::after, not text-decoration,
   so the pill stays a pill. A sold-out pill that is ALSO the chosen one is filled at 35% with a white
   strike, which is the state the live page shows the moment XL is picked. */
.option-controls { display: flex; flex-direction: column; flex-wrap: nowrap; gap: 1.25rem; margin: 0 0 1.5rem; }
.option-control { border: 0; padding: 0; margin: 0; min-width: 0; display: block; }
.option-control .option-name {
  display: block; padding: 0; margin: 0 0 0.6rem;
  font-size: 1rem; font-weight: 700; line-height: 1.25; color: var(--brand-dark-green);
}
.option-pills { display: flex; flex-wrap: wrap; gap: 8px; }
.option-pill {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  min-width: 48px; padding: 10px 18px; box-sizing: border-box;
  background: var(--brand-cream); border: 1.5px solid transparent; border-radius: 800px;
  font-size: 15px; font-weight: 500; line-height: 20px; color: var(--brand-dark-green);
  cursor: pointer; transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.option-pill:hover:not(.is-selected) { border-color: rgba(26, 57, 31, 0.3); }
.option-pill.is-selected { background: var(--brand-dark-green); color: var(--brand-white); border-color: var(--brand-dark-green); }
.option-pill.is-sold-out { background: transparent; color: rgba(26, 57, 31, 0.4); border-color: rgba(26, 57, 31, 0.12); }
.option-pill.is-sold-out::after {
  content: ""; position: absolute; left: 12%; right: 12%; top: 50%; height: 1.5px;
  background: rgba(26, 57, 31, 0.3); transform: rotate(-8deg); pointer-events: none;
}
.option-pill.is-sold-out.is-selected { background: rgba(26, 57, 31, 0.35); color: var(--brand-white); border-color: rgba(26, 57, 31, 0.35); }
.option-pill.is-sold-out.is-selected::after { background: rgba(255, 255, 255, 0.5); }
/* A value that does not exist IN COMBINATION with the other choice (two-option products: Blue Dream
   has no 7g) is NOT struck — a strike says "we stock this and ran out", and there is no such product.
   It is drawn quieter, on a dashed hairline, and stays clickable: choosing it moves the other control
   to a combination that exists (onOptionChange in product.html), which the screen-reader suffix
   ", changes your other choices" says in words. Never both marks at once — see pillState(). */
.option-pill.is-unreachable { background: transparent; color: rgba(26, 57, 31, 0.6); border: 1.5px dashed rgba(26, 57, 31, 0.3); }
.option-pill.is-unreachable:hover { border-color: rgba(26, 57, 31, 0.5); }
/* The radio under the pill is visually hidden, so the pill has to SHOW the keyboard focus the radio
   receives — a row of controls that can be arrowed through with no visible focus is a trap. */
.option-pill:has(input:focus-visible) { outline: 2px solid var(--brand-lime); outline-offset: 2px; }

/* ---- "You May Also Like" as a carousel (live: .product-carousel__*) ----------------------------
   Heading left; "View All" pill and the two 36px chevrons right (.product-carousel__controls, gap
   8px). The track is a scroll-snapped flex row — 308px cards at 8px gaps on a desktop, 167px on a
   phone — with the scrollbar hidden, exactly as the live track is; touch, trackpad and keyboard
   scroll it with no script and the arrows are the mouse affordance. The shared card draws the tile
   on the live carousel's beige (#f5f2eb, .product-carousel__image-container). */
.upsell-controls { display: flex; align-items: center; gap: 8px; }
.upsell-head .upsell-view-all {
  display: inline-block; padding: 8px 16px; background: var(--brand-dark-green); color: var(--brand-white);
  font-size: 14px; font-weight: 500; border-radius: var(--radius-pill); text-decoration: none;
}
.upsell-head .upsell-view-all:hover { opacity: 0.9; }
.upsell-nav-btn {
  width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center;
  color: var(--brand-dark-green); cursor: pointer; background: none; border: 0; padding: 0;
}
.upsell-nav-btn:hover { color: #245029; }
.upsell-nav-btn:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 2px; border-radius: 50%; }
.upsell-head { margin-bottom: 1.5rem; align-items: center; }
.upsell-head h2 {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  color: var(--brand-dark-green); font-size: 30px; line-height: 1.05; letter-spacing: 0.01em;
}
@media (min-width: 768px) { .upsell-head h2 { font-size: 48px; } }
.upsell-track {
  display: flex; gap: 8px; overflow-x: auto; scroll-snap-type: x proximity; scroll-behavior: smooth;
  scrollbar-width: none; -ms-overflow-style: none; -webkit-overflow-scrolling: touch;
  /* position: relative for the same reason .home-row-strip has it — a scroller's contents must
     not be able to widen the page. */
  position: relative; padding-bottom: 0.25rem;
}
.upsell-track::-webkit-scrollbar { display: none; }
.upsell-track > .product-card { flex: 0 0 308px; width: 308px; scroll-snap-align: start; }
.upsell-track > .product-card .product-thumb { background: var(--brand-cream); border-radius: 12px; }
.upsell-track:focus-visible { outline: 2px solid var(--brand-lime); outline-offset: 3px; }
@media (max-width: 767px) {
  .upsell-track > .product-card { flex-basis: 167px; width: 167px; }
}

/* ---- "Where We Ship" (live: .shipping-map-420) ---------------------------------------------
   Text on the left, the map on the right, in a 1fr 1fr grid with a 64px gap from 1024px up; one
   column below that with the MAP FIRST, which is the live order on a phone (order: 1 on the map,
   order: 2 on the text). Title in the display face, 32px rising to 48px; body 15px/1.5 in the
   brand green; the disclaimer 14px with 24px above it. */
.product-shipping { max-width: 1100px; margin: 0 auto; padding: 0 1.5rem 3rem; }
.product-shipping-inner { display: grid; grid-template-columns: 1fr; gap: 16px; }
@media (min-width: 1024px) {
  .product-shipping-inner { grid-template-columns: 1fr 1fr; gap: 64px; align-items: start; }
}
.product-shipping-text { order: 2; max-width: 100%; color: var(--brand-dark-green); font-size: 15px; line-height: 1.5; }
@media (min-width: 1024px) { .product-shipping-text { order: 1; max-width: 480px; margin: auto 0; } }
.product-shipping-text h2 {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  color: var(--brand-dark-green); font-size: 32px; line-height: 1.25; margin: 0 0 16px;
}
@media (min-width: 750px) { .product-shipping-text h2 { font-size: 48px; line-height: 1.5; margin-bottom: 24px; } }
.product-shipping-text p { margin: 0 0 16px; }
.product-shipping-text a { color: var(--brand-dark-green); text-decoration: underline; }
.product-shipping-text a:hover { text-decoration: none; opacity: 0.7; }
.product-shipping-text .product-shipping-disclaimer { font-size: 14px; margin: 24px 0 0; }
.product-shipping-map { order: 1; padding: 16px; }
@media (min-width: 750px) { .product-shipping-map { padding: 40px 64px; } }
@media (min-width: 1024px) { .product-shipping-map { order: 2; } }
.product-shipping-map svg { width: 100%; height: auto; display: block; }

/* ---- The FAQ accordion under it -------------------------------------------------------------
   The same block the home page draws (.home-faq and its items, further up), on the same 1100px
   column every other section of this page uses, so the two are one design. */
.product-faqs { max-width: 1100px; margin: 0 auto; padding: 0 1.5rem 3rem; }
.product-faqs .home-faq h2 {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  color: var(--brand-dark-green); font-size: 32px;
}

/* ---- The floating buy bar, as the live one (.pdp-420__floating) ----------------------------
   Cream (#f5f2eb) not white, a 0 -2px 10px 8% shadow, 12px of vertical padding; the title in the
   display face at 24px uppercase on the left; on the right the price at 17px/500, a 132x50 pill
   stepper with a 24%-ink hairline, and a 48px dark-green pill button 350px wide. On a phone the
   title goes and price, stepper and button share the row, which is the live arrangement. Shown at
   every width now that the photograph is the sticky column (see .product-media above). */
.sticky-buy {
  background: var(--brand-cream); border-top: 0;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
  padding: 12px 1.5rem calc(12px + env(safe-area-inset-bottom, 0px));
}
.sticky-buy-inner { max-width: 1280px; gap: 40px; }
.sticky-buy-title { font-size: 24px; line-height: 24px; font-weight: 900; }
.sticky-buy-price { font-size: 17px; font-weight: 500; line-height: 24px; color: var(--brand-dark-green); }
.sticky-buy-price .price-was { font-size: 14px; color: rgba(26, 57, 31, 0.5); }
.sticky-buy .qty-stepper {
  height: 50px; min-width: 132px; justify-content: space-between;
  border: 1px solid rgba(26, 57, 31, 0.24); border-radius: 800px; background: transparent;
}
.sticky-buy .qty-stepper button { width: 44px; height: 48px; color: var(--brand-dark-green); }
.sticky-buy .qty-stepper #sticky-qty { min-width: 1.5rem; text-align: center; font-weight: 500; color: var(--brand-dark-green); }
/* 17px, like the buy box's own Add: live's .pdp-420__add-btn--floating is 17px/500 (re-read 14 Sep
   2026) and the two bars differed by a point once the buy box went to 17 in round 9. */
.sticky-buy .primary { height: 48px; padding: 0 1.6rem; min-width: 200px; font-size: 17px; font-weight: 500; }
@media (min-width: 900px) { .sticky-buy .primary { width: 350px; } }
@media (max-width: 640px) {
  .sticky-buy-inner { gap: 12px; }
  .sticky-buy .primary { flex: 1 1 auto; min-width: 0; padding: 0 1rem; }
  .sticky-buy .qty-stepper { min-width: 112px; }
  .sticky-buy .qty-stepper button { width: 38px; }
  /* The struck compare-at goes ABOVE the price on a phone: side by side, "$150.00 $110.00" plus
     the stepper pushed the Add button off the right edge of a 390px screen (measured). Stacked, the
     bar keeps all three controls and the saving stays visible. */
  .sticky-buy-price { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.15; }
  .sticky-buy-price .price-was { margin: 0; font-size: 12px; }
}
/* A LOYALTY VENDOR'S REWARDS LAUNCHER WAS LIFTED OVER THIS BAR until 23 Sep 2026, exactly as
   Tidio's chat bubble is further up (see the #tidio-chat rule): it was pinned bottom-right at 20px
   by an inline style, so once the bar was on screen it landed on the Add to cart button. Measured
   at 1280: launcher 146x60 at y=820, bar 74px tall at y=826. 420 Rewards floats nothing, so the
   rule is gone with it and only Tidio's lift remains. */
/* The buy box's own price carries the struck compare-at beside it at 16px, 50% ink — the live
   .pdp-420__price-compare — while the current price keeps its size and weight. In the BODY face:
   the price beside it is in the display face now (see .product-buybox .price), and the shop has only
   the Black cut of that face, so a struck figure inheriting it would come out heavier than the price. */
.product-buybox .price .price-was { font-family: var(--font-primary); font-size: 16px; font-weight: 400; color: rgba(26, 57, 31, 0.5); margin: 0 12px 0 0; }
.sticky-buy-price .price-was { margin: 0 8px 0 0; }

/* ================================================================================================
   CONTENT PAGES, REVIEWS, LAB REPORTS, THE ARTICLE, THE AUTH PAGES — the 13 Sep 2026 live-vs-preview
   audit, content and account topics. Every number below was read off www.420.com with
   getComputedStyle at 1280 and 390 (the live theme's design tokens: --color-primary #1A391F,
   --text-title-2 56/56, --text-h1 40/44, --text-h2 32/32, --text-h3 24/24, --text-body-1 17/28,
   --space-* 8/12/16/24/32/40/48), not chosen. Appended as one block, on purpose: four agents are
   editing this stylesheet today and a block at the end merges where an edit in the middle collides.
   ================================================================================================ */

/* ---- The policy pages (.policy-page on live: 880px measure, everything in the brand green) ----
   Live sets the WHOLE page — heading and body — in --color-primary rgb(26,57,31); ours had a
   near-black heading over a near-black body, which is a different page at a glance. The h1 is the
   theme's title-2 (56/56) from 768px and its h1 (40/44) below; the h2s inside the body are h2
   (32/32) and h3 (24/24) at the same step. The trailing full stop live prints after each title
   ("FAQs.") is the theme's template, not the page's data — so page.html's template adds it
   (headingText there), not the stored title and not a CSS ::after. */
.page { color: var(--brand-dark-green); }
.page h1 {
  color: var(--brand-dark-green);
  font-size: 40px; line-height: 44px; margin: 0 0 12px;
}
.page-body { font-size: 17px; line-height: 28px; }
.page-body h2, .page-body h3 { color: var(--brand-dark-green); }
.page-body h2 { font-size: 24px; line-height: 24px; margin: 32px 0 16px; }
.page-body h2:first-child { margin-top: 0; }
.page-body p { margin: 0 0 16px; }
.page-body ul, .page-body ol { margin: 0 0 16px; padding-left: 24px; }
.page-body li { margin-bottom: 8px; }
.page-body a { color: var(--brand-dark-green); text-decoration: underline; }
.page-body a:hover { opacity: 0.7; }
/* "Shipping Allowed:" — live's .policy-page__content strong is --text-label-1-weight, 500. */
.page-body strong { font-weight: 500; }
@media (min-width: 768px) {
  .page h1 { font-size: 56px; line-height: 56px; margin-bottom: 16px; }
  .page-body h2 { font-size: 32px; line-height: 32px; margin: 40px 0 24px; }
  .page-body h2:first-child { margin-top: 0; }
}

/* ---- Lab Reports (.lab-reports-420 on live) ------------------------------------------------
   The one content page with its own template: a 1290px container with NO side padding from 768px
   (live's heading really does sit at x=0 on a 1280px screen), h1 48px, brand names 32px, and a
   table whose Download is a dark-green PILL BUTTON — ours was a blue underlined link, the browser's
   default. On the phone the combined cell stacks the product name over the strain, the strain at
   64% ink; ours ran them together as "… - Single Sativa". The classes are put on by splitTables()
   in page.html, which recognises the table by its header row rather than by the page's handle. */
.page--wide h1 { font-size: 30px; line-height: 1.3; margin: 0 0 24px; }
.page--wide .page-body h2 { font-size: 20px; line-height: 1.3; margin: 0 0 16px; }
.page--wide .page-body h3 { font-size: 24px; line-height: 1.3; margin: 24px 0 16px; }
.page--wide .page-body p { max-width: 900px; line-height: 1.6; }
@media (min-width: 768px) {
  .page--wide h1 { font-size: 48px; }
  .page--wide .page-body h2 { font-size: 24px; }
  .page--wide .page-body h3 { font-size: 32px; }
}
.page-body table.page-lab-table th {
  font-size: 14px; line-height: 28px; font-weight: 600; color: var(--brand-dark-green);
  padding: 12px 0; border-bottom: 1px solid rgba(26, 57, 31, 0.2);
}
.page-body table.page-lab-table td {
  font-size: 15px; line-height: 28px; color: var(--brand-dark-green);
  padding: 12px 0; vertical-align: middle; border-bottom: 1px solid rgba(26, 57, 31, 0.12);
}
.page-body table.page-lab-table tr:last-child td { border-bottom: 0; }
/* The combined phone cell: name over strain, 4px apart, the strain at 64% ink. */
.page-body .page-col--narrow > div { display: flex; flex-direction: column; gap: 4px; }
.page-body .page-col--narrow > div > span { font-size: 15px; line-height: 1.3; }
.page-body .page-col--narrow > div > span + span { color: rgba(26, 57, 31, 0.64); }
.page-body .page-col--download { width: 120px; min-width: 120px; text-align: right; }
.page-body a.page-download {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--brand-dark-green); color: var(--brand-white);
  border-radius: var(--radius-pill); padding: 8px 16px;
  font-size: 15px; font-weight: 500; line-height: 1.3; text-decoration: none; white-space: nowrap;
  transition: opacity 0.2s ease;
}
.page-body a.page-download:hover { opacity: 0.9; }

/* ---- The reviews page (/pages/reviews on live is a policy page: 880px, title-2 heading, then the
   subtitle "Real reviews from verified buyers and happy customers. Check our vibe.", then Yotpo's
   widget 40px below). Ours had the heading at 32px near-black, no subtitle, and Yotpo's review
   titles came out in CAPITALS because the site-wide heading rule reaches into the widget. */
.reviews-page { max-width: 944px; margin: 0 auto; padding: 24px 12px; color: var(--brand-dark-green); }
.reviews-page h1 { color: var(--brand-dark-green); font-size: 40px; line-height: 44px; margin: 0 0 12px; }
.reviews-subtitle { font-size: 17px; line-height: 28px; margin: 0 0 16px; }
.reviews-page #yotpo-reviews-page { margin-top: 32px; }
@media (min-width: 768px) {
  .reviews-page { padding: 40px 32px; }
  .reviews-page h1 { font-size: 56px; line-height: 56px; margin-bottom: 16px; }
  .reviews-page #yotpo-reviews-page { margin-top: 40px; }
}
/* Sentence case inside the widget: "Great flavor", as Yotpo and the live page render it. Only the
   transform and tracking are reset — the face, weight and colour are Yotpo's own and stay so. */
#yotpo-reviews-page :is(h1, h2, h3, h4, h5, h6) { text-transform: none; letter-spacing: normal; }
/* THE BLANK BAND. Yotpo's widget opens with a .yotpo-head — its own "Customer Reviews" headline,
   already display:none inside — that keeps 70px of margin above and below an empty box: 140px of
   nothing between our subtitle and the rating. The live theme's base.css removes it with exactly
   this rule (`.yotpo-head { display:none!important; visibility:hidden }`, read 13 Sep 2026), which
   is why live's rating sits 100px under its subtitle and ours sat 240px under.
   SITE-WIDE, AS LIVE'S IS — not scoped to the reviews page. Scoped to #yotpo-reviews-page it fixed
   that page and left the same empty band on every product page's widget (#product-reviews): a
   display:block .yotpo-head with min-height 100px and 32px under it (38px above it too on the
   phone), so the "5 · Based on 5 reviews" summary sat 191px below the widget's top at 1280 and
   264px at 390 against live's 59 and 94 — and pressing "5 Reviews" in the buy box scrolled a phone
   to ~170px of nothing (the seventh QA round, product-pages #1). Yotpo's head is its own "Customer
   Reviews" headline, wanted nowhere on this site: both pages head the widget themselves. */
.yotpo-head { display: none !important; visibility: hidden; }

/* ---- The blog index: live heads it "News" in sentence case (Obviously 40/52 at 1280, 30/39 on
   the phone, the theme's near-black), the one heading on the content pages that is NOT in caps. */
.blog-title { text-transform: none; font-size: 30px; line-height: 39px; margin: 20px 0; font-weight: 400; }
@media (min-width: 750px) { .blog-title { font-size: 40px; line-height: 52px; } }
/* 40px above and below on the phone (live's h1 margin there; 20px is the desktop's) — see
   main.blog-main, whose padding this sits inside. Here rather than beside that rule because this
   one has to come after the 20px line above to win. */
@media (max-width: 749px) { .blog-title { margin: 40px 0; } }
/* The index's cards, as live draws them — measured with getComputedStyle and getBoundingClientRect
   at 1280 and 390 on 13 Sep 2026 (third round), against the QA's finding that ours centred every
   card's title, date and excerpt where live LEFT-aligns them under the picture:
       the title       display face, 24px/31.2 (20/26 on the phone), caps, 0.6px tracking, brand
                       green; 17px under the picture (13px on the phone), 18px above the date
       the date        18px/21.6 upper-case, 1.3px tracking, the theme's near-black #121212 (ours
                       was grey and 15px on the phone), 4px of padding above it
       the excerpt     the body's 17/28 in the brand green, 12px under the date; on the wide card
                       it is centred in an 820px measure (live's .blog-articles--collage rule)
   Left-aligned by default; text-align is inherited from the card so the collage rule above, which
   centres the wide card from 750px, still wins there. The second pass had every card centred and
   the wide card's title at 1.6rem — live sets one size for all of them.
   NOTE ON THE FACE. On live these titles, and "News", can appear in a WIDE Obviously. That is the
   Klaviyo sign-up pop-up's Typekit kit injecting lower-case "obviously" faces, which the theme's
   `font-family: Obviously` then matches by name; with the pop-up blocked, live renders the narrow
   black exactly as this stylesheet does. A third party's side effect, not the theme's typography —
   not reproduced, and the CSP's font-src 'self' could not load those faces anyway. */
.article-card { text-align: left; }
.article-card h2 {
  font-family: var(--font-display); font-size: 20px; line-height: 26px; letter-spacing: 0.6px;
  text-transform: uppercase; text-align: inherit; margin: 13px 0 18px;
}
.article-card .article-meta {
  margin: 0; padding-top: 4px; text-align: inherit;
  font-size: 18px; line-height: 21.6px; letter-spacing: 1.3px; text-transform: uppercase; color: var(--brand-heading);
}
.article-card .article-excerpt { margin: 12px 0 0; text-align: inherit; font-size: 17px; line-height: 28px; color: var(--brand-dark-green); }
@media (min-width: 750px) {
  .article-card h2 { font-size: 24px; line-height: 31.2px; margin-top: 17px; }
  .blog-articles--collage > *:nth-child(3n + 1) .article-excerpt,
  .blog-articles--collage > *:nth-child(3n + 2):last-child .article-excerpt { max-width: 820px; margin-left: auto; margin-right: auto; }
}

/* ---- The article (.article-template on live) --------------------------------------------------
   Measured on /blogs/news/best-cannabis-products-for-concerts-festivals-live-music: a 1050px
   header with 32px side padding (986px of text) at 1280, 12px padding on the phone; the headline
   40/52 (30/39 on the phone) in the brand green; the date a 10px letter-spaced caption in the
   display face; a Share control 30px under it; body copy 17/28 in the brand green; in-body h2 24px
   (20px) and an in-body h1 40px (30px). Ours was a 46rem column with a 32px near-black headline,
   a 13.6px grey date, no Share, and 16px near-black body copy. */
.article { max-width: 1050px; padding: 0 32px; margin: 50px auto 0; box-sizing: border-box; }
.article h1 {
  color: var(--brand-dark-green); font-size: 30px; line-height: 39px; margin: 0 0 18px;
}
/* Scoped to the article page: the blog INDEX's cards carry the same class name, and this rule
   unscoped shrank their dates to a 10px caption (the second side-by-side pass, 13 Sep 2026). */
.article .article-meta {
  margin: 0; font-family: var(--font-display); font-size: 10px; line-height: 12px;
  letter-spacing: 1.3px; text-transform: uppercase; color: var(--brand-ink);
}
.article-share { margin: 30px 0 0; position: relative; }
/* "SHARE" AT LIVE'S SIZE, IN INTER FOR NOW. Dawn's .share-button__button is 14px on a 28px line
   at weight 400 with the icon 10px before the word (.icon-share margin-right: 1rem) in a 44px-tall
   summary; ours set it in Inter 500 (QA round 11, content-account #4). Live's face is the regular,
   wide Obviously at 400, which the shop's files do not carry — the display face here is Obviously
   Narrow BLACK, weight 900, and a 14px label in it is not what live shows either — so the rule
   deliberately leaves the body face and takes live's size, weight and colour (the same call as
   "Back to blog", see .article-back). The comment used to say "in the display face" while the rule
   said Inter (the twelfth QA round, store-content-account #8); the rule was right. */
.article-share-btn {
  display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0;
  border: 0; background: none; cursor: pointer; font: inherit;
  font-size: 14px; line-height: 28px; font-weight: 400; letter-spacing: 0; color: var(--brand-heading);
}
.article-share-btn:hover { text-decoration: underline; text-underline-offset: 3px; }
.article-share-btn svg { width: 13px; height: 12px; display: block; }
.article-share-panel {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  margin-top: 8px; padding: 12px; background: #f5f2eb; border-radius: var(--radius);
}
.article-share-panel input {
  flex: 1 1 220px; min-width: 0; min-height: 44px; padding: 0 12px;
  border: 1px solid var(--brand-hairline); border-radius: var(--radius); background: #fff; font: inherit;
}
.article-share-panel .article-share-copy {
  min-height: 44px; padding: 0 16px; border: 0; border-radius: var(--radius-pill); cursor: pointer;
  background: var(--brand-dark-green); color: var(--brand-white); font: inherit; font-weight: 500;
}
.article-share-status { flex-basis: 100%; font-size: 14px; color: var(--brand-dark-green); }
.article-share-status:empty { display: none; }
.article-body { margin-top: 15px; font-size: 17px; line-height: 28px; color: var(--brand-dark-green); }
.article-body p { margin: 0 0 18px; }
.article-body h2, .article-body h3, .article-body h4 { color: var(--brand-dark-green); margin: 0 0 18px; }
.article-body h2 { font-size: 20px; line-height: 26px; }
.article-body h3 { font-size: 18px; line-height: 24px; }
/* The article's own in-body h1 ("Music creates memories."), which the body allowlist reduced to a
   loose span; article.html puts it back as a heading with the live h1's size. */
.article-body .article-body-lead { font-size: 30px; line-height: 39px; }
.article-body strong { font-weight: 700; }
.article-body a { color: var(--brand-dark-green); }
/* SHARE AND BACK TO BLOG, LIVE'S SHAPE (the eleventh QA round, store-content-account #4). Read off
   www.420.com with getComputedStyle, 14 Sep 2026: the Share summary is 14px/28px weight 400 in a
   44px row with its 13×12 icon, no underline; "Back to blog" is a block flex link, 18px/28px weight
   400, rgb(18,18,18), no underline, the 20px arrow icon before the words, in a wrapper with 50px
   above and 32px below, and the footer 32px under the link — the wrapper's own bottom margin and
   nothing else: live's main carries no bottom padding (ours left 100: this wrapper's 32px, the
   main's 24px of padding and the footer's 48px margin). THE FACE IS THE ONE THING NOT MATCHED: live
   sets both in Obviously at weight 400 — the regular, wide face, which the shop's files do not
   carry (the display face here is Obviously Narrow BLACK, weight 900, the headings' face, and a
   14px label in it is not what live shows either). Inter stays for the two labels until that face is
   licensed and added; the sizes, weights and the icon are live's. */
.article-back { margin: 50px 0 32px; display: flex; justify-content: center; }
.article-back a {
  display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%;
  font-size: 18px; line-height: 28px; font-weight: 400; color: var(--brand-heading); text-decoration: none;
}
.article-back a:hover, .article-back a:focus-visible { text-decoration: underline; text-underline-offset: 3px; }
.article-back-arrow { display: inline-flex; }
.article-back-arrow svg { width: 20px; height: 20px; display: block; transform: rotate(180deg); }
/* NO PADDING UNDER THE MAIN. Round 11 gave it 20px to reach a "52" read off live while Dawn's
   animate--slide-in was still holding "Back to blog" 20px low; measured again after the animation
   had finished (scrolled to the foot, 15 Sep 2026) live's cream footer starts 32px under the link at
   1280 and at 390 — the wrapper's margin alone. Ours was 52 (the twelfth QA round,
   store-content-account #4). */
main.article-main { padding-bottom: 0; }
body:has(main.article-main) .site-footer { margin-top: 0; }
@media (min-width: 750px) {
  .article h1 { font-size: 40px; line-height: 52px; }
  .article-body h2 { font-size: 24px; line-height: 31px; }
  .article-body h3 { font-size: 20px; line-height: 26px; }
  .article-body .article-body-lead { font-size: 40px; line-height: 52px; }
}
/* On the phone live runs the hero edge to edge (390×330, square corners) and sets the text 12px
   from the screen edge. <main> carries 24px of padding on every page, so the hero pulls out by that
   and the text column by half of it. */
@media (max-width: 749px) {
  .article { padding: 0; margin: 44px -12px 0; }
  .article-hero-container { margin-left: -24px; margin-right: -24px; }
  .article-hero { height: 330px; border-radius: 0; }
}

/* ---- The auth pages (sign in, create account, forgot / reset password) ----------------------
   Live's sign-in is ONE CENTRED 380px CARD — heading, a grey subline, the fields. Ours put the
   heading at the far left of an 1100px main and the form in the right half (x≈700), an empty half
   page between them, because these forms borrow checkout's `.checkout-form` rule that pins the form
   to the right of checkout's two-column grid. The fields themselves are unchanged. */
.auth-page { max-width: 380px; margin: 0 auto; padding: 48px 16px 64px; }
.auth-page h1 {
  font-family: var(--font-primary); font-weight: 600; font-size: 20px; line-height: 1.3;
  text-transform: none; letter-spacing: 0; color: var(--brand-heading); margin: 0 0 4px;
}
.auth-subline { margin: 0 0 24px; font-size: 14px; color: #6b6b6b; }
.auth-page form.checkout-form {
  justify-self: auto; width: 100%; max-width: none; margin: 0; padding: 0;
}
.auth-page .auth-links { margin: 20px 0 0; font-size: 14px; }
.auth-page .auth-links a { color: var(--brand-dark-green); }
.auth-page .migrated-note { max-width: none; margin-top: 20px; font-size: 14px; }
.auth-page .migrated-note a { color: var(--brand-dark-green); }
.auth-page .error-banner, .auth-page .notice-banner { margin: 0 0 16px; }
/* The code sign-in (13 Sep 2026): the "or" rule between the two doors, and the second door's button
   — live draws its alternative ("Continue with shop") as a full-width secondary button with an "or"
   divider under it; ours mirrors the shape with the password form behind the button. */
.auth-page .auth-hint { margin: 0 0 12px; font-size: 14px; color: #6b6b6b; }
.auth-page .auth-or { display: flex; align-items: center; gap: 12px; margin: 20px 0; font-size: 13px; color: #6b6b6b; text-transform: uppercase; letter-spacing: .06em; }
.auth-page .auth-or::before, .auth-page .auth-or::after { content: ""; flex: 1; border-top: 1px solid var(--p-border, #ddd); }
.auth-page .auth-alt {
  width: 100%; min-height: 44px; padding: 10px 16px; border: 1px solid var(--brand-dark-green); border-radius: 9999px;
  background: #fff; color: var(--brand-dark-green); font: inherit; font-weight: 600; cursor: pointer;
}
.auth-page .auth-alt[aria-expanded="true"] { margin-bottom: 16px; }
.auth-page #code-email[readonly] { background: #f5f2eb; color: #444; }

/* ---- The phone drawer's Sign in button under the Rewards launcher --------------------------
   Smile's floating launcher (about 200×64, bottom right) sat over the right third of the drawer's
   "Sign in" — the last thing in the drawer, so nothing below it could be scrolled to. Room for it
   underneath means the button can always be scrolled clear. The drawer now continues below Sign in
   (live's newsletter and REACH OUT, see the fix-round-3 block), so the room moves to the new foot;
   this section keeps the ordinary spacing. */
.mobile-drawer-account { padding-bottom: 16px; }

/* === fix-round-3: content pages === ---------------------------------------------------------
   The 13 Sep 2026 third round (store-content): the 404 page and the footer's link columns, from
   the phone sweep and the content audit. Numbers read off www.420.com with getComputedStyle at
   1280 and 390 where live has the element, and off this site's own controls where it does not.
   Appended as one block, for the same reason the round-two block above is: several agents edit
   this stylesheet in parallel and a block at the end merges cleanly. */

/* ---- The 404 page (storefront/404.html) ----------------------------------------------------
   The third round drew this shop's own 404 here — the policy pages' measure and green, a 56px
   search pill pre-filled from the dead URL, a "Browse the whole shop" link. The eighth round
   (store-content-account #4) replaced the page with live's own "404 / Page not found / Continue
   shopping", and the rules went with the markup they styled: a rule with nothing behind it is
   something the next reader has to go and check. The page's rules are .not-found, .not-found-code,
   .not-found-title and .not-found-continue in the round-8 block at the foot of this sheet. */

/* ---- The footer's link columns (js/footer.js) -----------------------------------------------
   Measured on www.420.com at 390 and 1280 (13 Sep 2026): TOP BRANDS and CATEGORIES sit SIDE BY
   SIDE on the phone — a two-column grid, 167px each with a 32px gutter, headings at x=12 and
   x=211 on the same line, REACH OUT under them — and every link in those columns is a 36px row:
   display block, 8px of padding over a 20px line of 15px Inter, 8px between rows (a 44px pitch).
   Ours stacked the three columns one under the other at 390 with 17px-tall inline links, which
   made the footer about a screen taller than live's and left the links below a comfortable tap
   height. The REACH OUT links keep their icon row but take the same 36px height. */
.footer-column a { display: block; padding: 8px 0; font-size: 15px; line-height: 20px; }
.footer-column li { margin-bottom: 8px; }
/* Two classes, so the icon row keeps its flex layout whichever order these rules are merged in. */
.footer-column.footer-reach a { display: flex; }
.footer-column.footer-reach li { margin-bottom: 0; }
@media (max-width: 600px) {
  .footer-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2rem; }
  .footer-newsletter, .footer-reach { grid-column: 1 / -1; }
}
/* === fix-round-3: account & sign-in === */

/* ---- The account page's forms sit under their headings ---------------------------------------
   Profile, Change password and the address form borrow checkout's `.checkout-form` rule, which pins
   the form to the RIGHT of checkout's two-column grid (justify-self: end) and pads it for that grid.
   Chrome now honours justify-self on a plain block box, so on this page each 420px form sat at
   x=746 while its heading (PROFILE / CHANGE PASSWORD / SAVED ADDRESSES) was at the far left — an
   empty half page between them at 1280 (13 Sep 2026 QA). The auth pages got .auth-page for exactly
   this; the account page had no override. Same freeing rule, kept at the forms' own 420px. */
.account-page form.checkout-form { justify-self: auto; width: 100%; max-width: 420px; margin: 0; padding: 0; }
/* "You signed in with a code — set a password here": the sentence above the shortened password
   form. Body-sized and #444 like every other secondary line here (see .muted), not a small grey. */
.account-page .auth-hint { margin: 0; font-size: 14px; color: #444; }

/* ---- Subscription actions are real buttons, thumb-sized -------------------------------------
   Skip / Change the date / Pause / Change address / Change card / Cancel were unstyled browser
   defaults, 23px tall — half the 44px the same page gives the address Edit and Delete, and the
   smallest targets on the site (Skip next box 108×23, Pause 58×23 at 390; 13 Sep 2026 QA). The
   same outline pill as the sign-in page's second door (.auth-alt): a shop-green border, white
   inside, 44px tall, so a row of six reads as a row of buttons and each takes a thumb. The restore
   offer's "Restore it" is the same kind of control and gets the same shape. */
.subscription-card__actions button,
.subscription-restore > button {
  min-height: 44px; padding: 0 16px; border: 1px solid var(--brand-dark-green); border-radius: var(--radius-pill);
  background: #fff; color: var(--brand-dark-green); font: inherit; font-size: .95rem; font-weight: 600; cursor: pointer;
}
.subscription-card__actions button:hover:not(:disabled),
.subscription-restore > button:hover:not(:disabled) { background: #f4fbf6; }
.subscription-card__actions button:disabled { opacity: .5; cursor: not-allowed; }
/* The in-card panel an action opens — a question or a control, then Save / Keep. Replaces the
   window.prompt() dialogs ("Use YYYY-MM-DD", "Type a number") with a date input and an address
   list a phone can actually operate. Same field metrics as form.checkout-form's inputs (44px). */
.subscription-panel {
  display: grid; gap: .75rem; max-width: 420px; margin-top: 12px; padding: 12px 14px;
  border: 1px solid var(--brand-lime-light); border-radius: var(--radius); background: #f8fbf3;
}
.subscription-panel__question { margin: 0; font-weight: 600; line-height: 1.45; }
.subscription-panel input, .subscription-panel select {
  padding: .65rem; border-radius: var(--radius); border: 1px solid var(--brand-lime-light); background: #fff;
  min-height: 44px; font: inherit; width: 100%; box-sizing: border-box;
}
.subscription-panel__actions { display: flex; flex-wrap: wrap; gap: 8px; }
.subscription-panel__actions button { min-height: 44px; font: inherit; font-weight: 600; cursor: pointer; }
.subscription-panel__actions button[data-close] {
  padding: 0 16px; border: 1px solid var(--brand-dark-green); border-radius: var(--radius-pill);
  background: #fff; color: var(--brand-dark-green);
}
.subscription-panel__actions button:disabled { opacity: .5; cursor: not-allowed; }
/* What happened, said on the card the button was on — the page-level banners are several screens
   above it on a phone. The same two colours the page's own banners use (.error-banner and the
   green #success), so an outcome reads the same wherever it appears. */
.subscription-card__outcome, .form-outcome { margin: 10px 0 0; padding: 10px 12px; border-radius: var(--radius); font-weight: 600; line-height: 1.45; }
.subscription-card__outcome[data-ok="true"], .form-outcome[data-ok="true"] { background: var(--brand-lime-light); color: var(--brand-dark-green); }
.subscription-card__outcome[data-ok="false"], .form-outcome[data-ok="false"] { background: #fdecea; color: #a41c1c; }
.subscription-card__outcome:focus, .form-outcome:focus { outline: none; }
/* The account forms' own outcome lines (formOutcome in account.html) — "Password changed." under
   the button that changed it, not under the H1 three screens up. */
.account-page form.checkout-form .form-outcome { margin: 0; }
@media (prefers-color-scheme: dark) {
  .subscription-card__actions button, .subscription-restore > button,
  .subscription-panel__actions button[data-close] { background: #1a1a1a; color: #ddf3b5; border-color: #ddf3b5; }
  .subscription-panel { background: #10241a; border-color: #36a35b; }
  .subscription-panel input, .subscription-panel select { background: #1a1a1a; color: #eee; border-color: #333; }
}

/* ---- Room under the sign-in card for the rewards launcher ----------------------------------
   Smile's floating launcher (about 150×60, fixed bottom right) sat over the right 42% of the "Sign
   in" button in the state the page lands a person in after the code request answers 503 — the
   password form opens and its button falls exactly under the launcher at 390×844 (13 Sep 2026 QA);
   a tap on the right half of Sign in opened the rewards panel instead. login.html scrolls the button
   clear of that corner when it opens the form; this is the other half — the same room the phone
   drawer keeps under its own Sign in (.mobile-drawer-account), so the last control on any of the
   four auth pages can always be scrolled out from under the launcher. */
.auth-page { padding-bottom: 128px; }

/* === fix-round-3: home & header ===============================================================
   The third side-by-side pass against www.420.com (13 Sep 2026): the home page rendered at
   roughly half live's scale inside an 1100px column, the desktop menus opened as full-width
   bands where live hangs cards under its buttons, and the phone drawer stopped short of live's
   foot. Every number here was read with getComputedStyle on the live page at 1280 and 390 (and at
   768, 1024, 1440 and 1920 for the column) — fix-store-home-header/live-probe-*.json,
   live-menus.json, live-drawer-390.json — not chosen. Rules that already existed for a home
   section were changed in place further up; this block holds what is new.
   ============================================================================================== */

/* ---- THE HOME PAGE'S COLUMN ---------------------------------------------------------------
   Live's home sections sit in a 1290px container with NO side padding from 768px up — at a 1280
   screen the SHOP BY CATEGORY heading is at x=0 and the five tiles run the full 1280 — and 12px of
   padding below that. Every other page here keeps its own 1100px measure (`main` above), which is
   those pages' live measure; only the home page (index.html marks its <main>) takes this one. The
   vertical padding stays 1.5rem, which the hero's negative top margin and the results heading both
   count on. The filter panel and the catalogue grid under the home sections widen with it, which
   is also what live's /collections/all does at this width: four 308px cards across 1280. */
main.home-page { max-width: 1290px; padding-inline: 0; }
@media (max-width: 767px) {
  main.home-page { padding-inline: 12px; }
  /* The full-bleed bands (hero, spotlight, bundle) already break out with calc(50% - 50vw), so
     the 12px changes nothing for them. */
}

/* ---- THE HERO ON A PHONE: 506px, the artwork CONTAINED, as live's is ----------------------
   Live's .hero-slide is a fixed 506px tall below 600px (measured 320 through 480: 506 every time)
   and 706px from 600 to 767, with the portrait phone banner shown `contain` inside it — a
   341px-wide picture centred in the 390px band with a dark strip either side, its own background
   showing through. Ours let the 1200x1780 file set its own height at the full width: 578px at 390,
   pushing the trust strip and SHOP BY CATEGORY 72px further down (the third pass), and over 1,000px
   at 700. The strip either side is the average of live's — the artwork's own near-black green —
   sampled off its band. */
@media (max-width: 767px) {
  .home-hero-banner-responsive .home-hero-img {
    height: 706px; aspect-ratio: auto; object-fit: contain; background: #071409;
  }
}
@media (max-width: 599px) {
  .home-hero-banner-responsive .home-hero-img { height: 506px; }
}

/* ---- THE FAQ: LIVE'S CARD BESIDE ITS PHOTOGRAPH, on the home page -------------------------
   Live's .faq-section__layout is two 632px columns 16px apart at 1280: the photograph left, its
   own shape (632x850) with 16px corners, and the questions RIGHT in a cream (#f5f2eb) card, 16px
   corners, 40px inside — "FAQs" at 48px/48px in the green with 24px under it, each question a
   20px/24px weight-500 row 16px over and under, a 16px chevron at its right, hairlines at 12% green,
   the answer 15px/24px with 16px under it. On a phone the photograph is cropped to a 366x250
   landscape band above the card and the card has 24px inside, the heading 40px/44px, the questions
   17px/28px. Ours drew a bare list with "+" markers beside a portrait crop, and an "All FAQs" link
   live's section does not have (js/home-sections.js no longer emits it). page.html already
   recreates this card for the FAQ page (its .page-faq rules, read off the same live block) and
   these numbers are the same ones. Scoped to the home page: product.html draws its own FAQ block
   with these classes on its own column. */
.home-page .home-faq { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; margin: 0 0 48px; align-items: start; }
.home-page .home-faq:not(:has(.home-faq-art)) { grid-template-columns: 1fr; }
.home-page .home-faq-art img { display: block; width: 100%; height: auto; border-radius: 16px; }
.home-page .home-faq-body {
  background: var(--brand-cream); border-radius: 16px; padding: 40px; color: var(--brand-dark-green);
}
.home-page .home-faq h2 { margin: 0 0 24px; font-size: 48px; line-height: 48px; color: var(--brand-dark-green); }
.home-page .home-faq-item { border-top: 0; border-bottom: 1px solid rgba(26, 57, 31, 0.12); }
.home-page .home-faq-item:last-of-type { border-bottom: 0; }
.home-page .home-faq-item summary {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 16px 0; font-size: 20px; line-height: 24px; font-weight: 500; color: var(--brand-dark-green);
}
/* The chevron: a 9px square's two edges, turned — the same drawing page.html uses, which is live's
   16px chevron-down icon at the row's right; it turns up when the answer is open. */
.home-page .home-faq-item summary::after {
  content: ""; flex: 0 0 auto; width: 9px; height: 9px; margin: 6px 4px 0 0;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); transition: transform 0.2s ease;
  font-size: 0; line-height: 0;
}
.home-page .home-faq-item[open] summary::after { content: ""; transform: rotate(-135deg); margin-top: 10px; }
.home-page .home-faq-item p { margin: 0; padding-bottom: 16px; font-size: 15px; line-height: 24px; color: var(--brand-dark-green); }
@media (max-width: 1023px) {
  /* One column below 1024, as live stacks it (the photograph 768 wide over the card at 768). */
  .home-page .home-faq { grid-template-columns: 1fr; }
  .home-page .home-faq-art img { height: 500px; object-fit: cover; }
}
@media (max-width: 767px) {
  .home-page .home-faq { margin-bottom: 32px; }
  .home-page .home-faq-art img { height: 250px; }
  .home-page .home-faq-body { padding: 24px; }
  .home-page .home-faq h2 { font-size: 40px; line-height: 44px; margin-bottom: 16px; }
  .home-page .home-faq-item summary { font-size: 17px; line-height: 28px; }
  .home-page .home-faq-item summary::after { margin-top: 8px; }
  .home-page .home-faq-item[open] summary::after { margin-top: 12px; }
  /* THE PHONE'S OWN PICTURE, WHOLE. When the shop holds the landscape phone asset
     (product_faq_image_mobile, 700×497 on live — the same setting the product page's FAQ already
     serves through a <picture>), js/home-sections.js writes the home FAQ as a <picture> too and the
     250px band shows all of it, letterboxed as live's is (`object-fit: contain` on
     .faq-section__image at 390). Scoped to `picture img` on purpose: a shop with only the portrait
     asset gets a bare <img>, and for that the cover-from-the-top crop further down (the store-home
     #3 rule) is still the right answer — contain would shrink a 465×684 portrait to 170px wide in a
     366px band (round 10, home #2). `picture` is inline by default and would leave a line-box gap
     under the image, hence display: block, as the product page's rule has it. */
  .home-page .home-faq-art picture { display: block; }
  .home-page .home-faq-art picture img { object-fit: contain; object-position: center; }
}

/* ---- THE PHONE'S HEADINGS: 30px/32px on the tiles and Just Dropped, 28px on the bundle --------
   Live's .image-grid__title and .product-carousel__title drop from 48px to 30px below 768 (measured
   at 390); the section padding drops from 48px to 32px and the heading row's margin from 32 to 24. */
@media (max-width: 767px) {
  .home-tiles { padding: 32px 0; }
  .home-tiles h2, .home-row-head h2 { font-size: 30px; line-height: 32px; }
  .home-tiles-head, .home-row-head { margin-bottom: 24px; }
  .home-row { margin: 24px 0 48px; }
  /* Live's product carousel keeps only its arrows on a phone — no Shop All pill and no button
     under the cards (its 390 text runs "JUST DROPPED" straight into the first card, and the
     category block's full-width Shop all is a screen above). With the pill gone the heading fits
     its line, as live's does. */
  .home-row-more { display: none; }
  /* THE UNBOXING ARROWS STAY LEVEL WITH THE HEADING, AS LIVE'S DO. Live's .ugc-header is one flex
     row aligned to its foot at every width: at 390 the 32px heading runs two lines in 275px and the
     two arrows sit at its right, level with its last line, with the first card 32px under the
     heading (its 12px margin and the row's 20px). An earlier fix wrapped the row so the arrows
     dropped under the heading — the 30px margin, a 40px row of arrows and 16px more put the cards
     102px under the heading, a screen visibly airier than live's (the sixth QA round, store-home
     #3). Measured after the change at 390: the cards 32px under the heading, the arrows on its last
     line. The heading has 226px beside the two 40px buttons (the owner's floor for a phone control —
     live's arrows are 20px and leave it 275px), so "See What Members Are Getting" runs three lines
     at 32px where live's runs two; the row's shape and the gap under it are live's. */
  .home-ugc-head { flex-wrap: nowrap; align-items: flex-end; gap: 16px; margin-bottom: 20px; }
  .home-ugc-head > div { flex: 1 1 auto; min-width: 0; }
  .home-ugc-head .home-carousel-nav { flex: none; margin: 0; }
}
/* The struck price is the light one; the price itself is 700, as live's .fps-slider__price is. */
.home-spotlight-price strong { font-weight: 700; }

/* ---- THE FOOTER'S NEWSLETTER HEADING, 48px/48px (32px/32px on a phone) --------------------
   Live's .footer__newsletter-heading measures 48px at 1280 and 32px at 390 (footerH in the probes);
   ours was a 33px clamp on the desktop and 28px on the phone, the last of the half-scale headings
   the third pass listed. The measure is live's 624px. */
.footer-newsletter h2 { font-size: 48px; line-height: 48px; max-width: 624px; }
@media (max-width: 600px) {
  .footer-newsletter h2 { font-size: 32px; line-height: 32px; }
}

/* ---- THE DESKTOP MENUS ARE CARDS HUNG UNDER THEIR BUTTONS -----------------------------------
   Live's .header-420__dropdown: a white card 280px wide (CATEGORIES, EFFECTS) with 12px corners,
   24px inside and a 0 10px 15px 10% shadow, opening at its button's left edge 8px above the bar's
   bottom; the heading 32px/32px in the display face and the green, 20px over the list; each link a
   17px/28px weight-400 row in the green, 8px over and under (44px per row — a target, not a line
   of text). TOP BRANDS is the same card at 720px with 32px inside and its sixteen names in two
   columns of eight, 167px wide, 12px apart (live's carries a picture beside them, which this shop
   has no brand asset for). Ours opened a full-width white band under the whole bar with a lime rule
   on top, 0.75rem letterspaced headings and 0.95rem links with counts (the third pass).
   js/header.js sets the card's `left` from the button on open; the width comes from here. */
.megamenu {
  left: auto; right: auto; top: calc(100% - 8px);
  width: 280px; min-width: 280px;
  border-top: 0; border-radius: 12px;
  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
#brands-menu.megamenu { width: 410px; }
.megamenu-inner { display: block; max-width: none; padding: 24px; }
.megamenu-inner-brands { padding: 32px; }
.megamenu-heading {
  margin: 0 0 20px; font-family: var(--font-display); font-size: 32px; line-height: 32px;
  font-weight: 900; letter-spacing: 0.01em; text-transform: uppercase; color: var(--brand-dark-green);
}
/* 8px BETWEEN THE ROWS. Each link is 44px (28px of text, 8px over and under) and live's column
   lays them out with an 8px gap (`.link-list__column { gap: var(--space-xs) }`), so the rows come
   52px apart and the CATEGORIES card stands 614px tall for ten of them; ours had no gap, 44px
   apart, 540px tall — every link a touch closer than a returning customer's hand expects (the
   twelfth QA round, store-home #3; re-measured with getComputedStyle on 15 Sep 2026: link h 44,
   pitch 52, column gap 8px). The margin, not a gap on the list, because the brand card's list is a
   two-column grid with its own gaps. */
.megamenu-column li + li { margin-top: 8px; }
header.site-header .megamenu-column a {
  display: block; padding: 8px 0; font-size: 17px; line-height: 28px; font-weight: 400;
  color: var(--brand-dark-green);
}
header.site-header .megamenu-column a:hover { color: var(--brand-dark-green); text-decoration: underline; }
/* Column-major, as live reads: Boutiq to Litty down the first column, oHHo to Papa & Barkley down
   the second. A seventeenth brand would start a third column rather than be lost. */
.megamenu-inner-brands .megamenu-column ul {
  display: grid; grid-template-columns: repeat(2, 167px); grid-template-rows: repeat(8, auto);
  grid-auto-flow: column; column-gap: 12px; row-gap: 8px;
}
/* The grid's row-gap carries the 8px here: a margin on `li + li` would also land on the top of
   the second column's first name and drop it 8px below the first column's. */
.megamenu-inner-brands .megamenu-column li + li { margin-top: 0; }

/* ---- THE PHONE DRAWER: LIVE'S WIDTH, LIVE'S ROWS, LIVE'S FOOT -------------------------------
   Live's drawer is the whole screen (390px at 390, no dimmed page beside it), its section titles
   the display face at 30–32px in the green, its brand and More-links rows 17px/28px with 8px over
   and under (44px, a thumb's height — ours were 19px text links), and under every section a
   full-width dark-green "Shop all" 52px tall. It continues below Sign in with the newsletter and
   REACH OUT (js/header.js draws them). */
.mobile-drawer { width: min(100vw, 480px); box-shadow: none; }
/* No padding over the first section: its own 24px is what live puts between the head's hairline
   and SHOP BY CATEGORY (heading top 89 = the 65px head + 24). */
.mobile-drawer-body { padding: 0 12px 2rem; }
.mobile-drawer-section { padding: 24px 0; }
.mobile-drawer-title {
  margin: 0 0 16px; font-family: var(--font-display); font-size: 30px; line-height: 32px;
  font-weight: 900; letter-spacing: 0.01em; text-transform: uppercase; color: var(--brand-dark-green);
}
.mobile-drawer-tiles { margin-top: 0; gap: 8px; }
.mobile-drawer-pills { margin-top: 0; }
/* Two columns of five, read down each column as live's list is (Boutiq to Death Row Hemp, then
   Cookies to Nug). */
.mobile-drawer-brands {
  margin: 0; gap: 0 12px; grid-template-rows: repeat(5, auto); grid-auto-flow: column;
}
.mobile-drawer-links { margin: 0; gap: 0; }
header.site-header .mobile-drawer-brands a, header.site-header .mobile-drawer-links a {
  display: block; padding: 8px 0; font-size: 17px; line-height: 28px; color: var(--brand-dark-green);
}
/* The full-width "Shop all" under each section: live's .mobile-drawer__shop-all, 366x52. */
header.site-header .mobile-drawer-all {
  display: block; margin-top: 24px; padding: 12px 16px; border-radius: var(--radius-pill);
  background: var(--brand-dark-green); color: var(--brand-white);
  font-size: 16px; line-height: 28px; font-weight: 500; text-align: center; text-decoration: none;
}
header.site-header .mobile-drawer-signin { margin-top: 0; padding: 12px 16px; font-size: 16px; line-height: 28px; font-weight: 500; }
/* The foot: the newsletter title in the display face (two lines at 32px), a 52px field with the
   Subscribe button set inside its right end, then REACH OUT's two links with the envelope. Room
   underneath for Smile's floating launcher, which sat over the right third of whatever was last. */
.mobile-drawer-foot {
  /* A cream band the width of the drawer (live's .mobile-drawer__footer), the title centred. */
  background: var(--brand-cream); margin: 0 -12px; padding: 32px 12px 96px; border-bottom: 0;
}
.mobile-drawer-newsletter-title {
  margin: 0 0 20px; font-family: var(--font-display); font-size: 32px; line-height: 32px;
  font-weight: 900; letter-spacing: 0.01em; text-transform: uppercase; color: var(--brand-dark-green);
  text-align: center;
}
.mobile-drawer-newsletter-field {
  display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 16px;
  border: 1px solid rgba(26, 57, 31, 0.3); border-radius: var(--radius-pill); background: var(--brand-white);
}
.mobile-drawer-newsletter-field input {
  flex: 1 1 auto; min-width: 0; border: 0; background: none; font: inherit; font-size: 16px;
  height: 38px; color: var(--brand-ink); outline: none;
}
.mobile-drawer-newsletter-field button {
  flex: none; height: 40px; padding: 0 16px; border: 0; border-radius: var(--radius-pill);
  background: var(--brand-dark-green); color: var(--brand-white); font: inherit; font-size: 14px;
  font-weight: 500; cursor: pointer;
}
.mobile-drawer-newsletter-field:focus-within { border-color: var(--brand-dark-green); }
.mobile-drawer-newsletter-status { margin: 8px 0 0; min-height: 1.2em; font-size: 14px; color: var(--brand-dark-green); }
.mobile-drawer-reach { margin-top: 32px; }
.mobile-drawer-reach .mobile-drawer-title { font-size: 20px; line-height: 24px; margin-bottom: 12px; }
.mobile-drawer-reach-links { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
header.site-header .mobile-drawer-reach-links a {
  display: flex; align-items: center; gap: 10px; padding: 8px 0;
  font-size: 15px; line-height: 20px; color: var(--brand-dark-green);
}
.mobile-drawer-reach-links svg { flex: none; }
/* === fix-round-3: grid & product page ==========================================================
   The third side-by-side QA round (13 Sep 2026) measured the collection grid, the search results
   and the product page against www.420.com at 1280 and 390 with getComputedStyle and
   getBoundingClientRect. Everything below is a number read off the live page, not a guess; where a
   rule higher up this sheet is being overridden rather than edited, the reason is stated. */

/* ---- The catalogue's page width ------------------------------------------------------------
   Live's `.collection-420__container` and `.search-420__container` are max-width 1290px with 12px
   of side padding (0 at a desktop for the search page), so at 1280 the grid runs 12–1268: four
   296px cards and three 24px gutters. Ours drew the same grid inside the site's 1100px/1.5rem
   `main`, so the cards were 245px in a 1052px block that started at x=114 — the two pages did not
   look like the same shop at a glance. On a phone the difference decided legibility: 12px gutters
   give a 175px card where our 24px gave 163px, and the title clamp (see .product-card-title) cut
   the size off nine names in sixteen.

   A class on the page's <main> rather than a change to the `main` rule: index.html and
   collection.html are the two pages whose content IS this grid, and the rest of the storefront —
   the basket, checkout, the account pages — keeps the column it was designed in. index.html is
   also the home page, whose sections live also runs at the full 1280 (its own QA item).
   No vertical padding: the filter bar's own 32px margin (24px on a phone) is what puts the first
   pill at live's y=148 under a 116px header, and the pager's margin and the 3rem at the foot keep
   the last row clear of the footer. */
main.catalogue-main { max-width: 1290px; padding: 0 12px 3rem; }
/* The results heading sits in a section padded 48px at the top on live (24px on a phone); its own
   margin is 0. The h1 rule higher up gives it the 24px under; this gives it the air above that the
   `main` padding used to. */
main.catalogue-main h1.search-heading { margin-top: 24px; }
@media (min-width: 768px) { main.catalogue-main h1.search-heading { margin-top: 48px; } }
/* THE BRAND INDEX (/collections/vendors with no brand named — collection.html, brandIndex, 15 Sep
   2026). The page's h1 is shown, in the home page's section-heading face (.home-tiles h2: 48px/48px
   weight 900 uppercase in the brand green, 30px/32px on a phone) with the results heading's air
   above it, and the grid is the home page's tile grid without its own 48px of top padding — the
   heading already has the space. */
main.catalogue-main h1.brand-index-title {
  margin: 24px 0 24px; font-size: 30px; line-height: 32px; font-weight: 900;
  letter-spacing: 0.01em; text-transform: uppercase; color: var(--brand-dark-green);
}
@media (min-width: 768px) { main.catalogue-main h1.brand-index-title { margin-top: 48px; font-size: 48px; line-height: 48px; } }
main.catalogue-main .brand-index { padding-top: 0; }
/* A vendor name is typed by whoever created the product; one with no space in it (the local QA
   fixtures' "CartVendor-crtmtscqwmbamlq") ran out of both sides of a 117px phone tile. Broken inside
   the word when there is no other way, as the admin's review table does — a real brand name wraps
   at its spaces long before this applies. */
main.catalogue-main .brand-index .home-tile-label { overflow-wrap: anywhere; }

/* ---- The product page's width, and the crumb's height ---------------------------------------
   Live's `.pdp-420__container` is max-width 1290px with 16px of side padding, and its two columns
   are 604px each with a 40px gap at 1280 — media 16–620, buy box 660–1264. Ours ran inside the
   1100px main: 506 + 40 + 506 starting at x=114, so the photograph and the buy box were both a
   hundred pixels narrower than live's and the title wrapped differently (its size is below). The
   breadcrumb is a 60px band directly under the header on live (bc 116–176, the grid at 176), which
   20px of padding on a ~20px line reproduces; it had a 1rem margin under it and rode on main's
   padding above. */
main.product-page { max-width: 1290px; padding: 0 16px 3rem; }
main.product-page .breadcrumb { margin: 0; padding: 20px 0; }

/* ---- The product title ------------------------------------------------------------------------
   Live's h1 is the display face at 48px on a 48px line at 1280 ("CAM KUSH MINTS THCA FLOWER 7G"
   wrapping to two lines in a 604px column) and 30px/32px at 390 — `.pdp-420__title`, weight 900, in
   the brand green with no letter-spacing. Ours was 1.75rem (28px) at every width, so on a desktop
   the title sat on one line at little more than half live's size and the whole buy box read as a
   smaller page. The live face is the same Obviously Narrow Black file this sheet self-hosts
   (@font-face at the top), so this is size alone. 16px under it is the gap to the points banner on
   live (h1 bottom 308, banner top 324). */
.product-buybox h1 { font-size: 30px; line-height: 32px; letter-spacing: normal; margin: 4px 0 16px; }
@media (min-width: 768px) { .product-buybox h1 { font-size: 48px; line-height: 48px; } }

/* ---- The description ----------------------------------------------------------------------------
   MARKUP LAYS ITSELF OUT. `.product-description { white-space: pre-line }` higher up exists for the
   CSV importer's plain text, where a newline is a paragraph break. The Shopify mirror stores
   sanitised HTML — "<p>…</p>\n<p>…</p>" — and under pre-line the newline BETWEEN two paragraphs
   became a blank line on top of their margins: 58px between consecutive paragraphs on the care
   package against live's 0px. product.html adds .product-description-html when the body carries
   block tags; that body gets normal whitespace and live's paragraph rhythm — 17px on a 28px line,
   paragraphs butted (their <p> margins are 0 on live; the copy's own line breaks do the spacing). */
.product-description-html { white-space: normal; }
/* IN THE BRAND GREEN, like the short summary above it and like live: `.pdp-420__description` is
   17px/28px 400 in #1A391F (rgb 26,57,31) on every product page measured, and ours was the same
   size and leading in the body ink (rgb 32,30,25) because nothing here set a colour — side by side
   the paragraph read near-black where live's reads dark green (the seventh QA round, product-pages
   #3; pixel-sampled on the T-shirt's copy). */
.product-buybox .product-description { font-size: 17px; line-height: 28px; color: var(--brand-dark-green); }
/* The full column, as on live: the summary and the body both run to the buy box's right edge
   (660–1264 at 1280). The 60ch measure set higher up was written for the 1100px page. */
.product-buybox .product-description, .product-buybox .product-description-short { max-width: none; }
.product-buybox .product-description-html p { margin: 0; }
.product-buybox .product-description-html ul, .product-buybox .product-description-html ol { margin: 0; padding-left: 1.25em; }
/* COLLAPSED IS HIDDEN. Under a summary (.product-description-short) the body starts out of sight
   and "Read more" reveals all of it — live's <details> accordion, which shows the summary sentence
   and nothing of the body until it is opened. The six-line clamp this replaced showed the summary
   PLUS two paragraphs cut mid-word before the control (see product.html). `display: none` rather
   than the hidden attribute because the toggle is a class the button flips, and [hidden] is the
   page-wide `!important` rule at the top of this sheet. */
.product-description.is-collapsed { display: none; }

/* ---- The rows under the price ------------------------------------------------------------------
   "View document" IS UNDERLINED — the rule for it higher up (.product-coa-link) says so and never
   applied: `.product-meta-row a { text-decoration: none }` outranks a lone class, so the COA link
   rendered as plain green text in a row of label/value pairs (computed textDecorationLine "none"
   against live's "underline"). Same specificity as the rule that was winning. */
.product-meta-row .product-coa-link { text-decoration: underline; }
.product-meta-row .product-coa-link:hover { text-decoration: none; }
/* No line at all for a size that is simply in stock — product.html's stockLabel() answers "" for it,
   as live prints nothing there — so the paragraph must take no room. Its 1rem bottom margin
   otherwise opened a gap between the COA row and the buy row. */
.product-buybox #variant-stock:empty { display: none; }

/* ---- The half star ------------------------------------------------------------------------------
   Live's star row (Yotpo's widget) draws four and a half stars beside "4.7"; ours drew five whole
   ones. The five empty glyphs are the track, the five filled ones sit on top clipped to the rating's
   share of the row (product.html sets the width to the nearest half star), so a half is half of the
   same glyph in the same font and nothing depends on a half-star character the device may lack. */
.stars-fractional { position: relative; display: inline-block; white-space: nowrap; line-height: 1; }
.stars-fractional .stars-track { display: inline-block; white-space: nowrap; }
.stars-fractional .stars-fill { position: absolute; left: 0; top: 0; overflow: hidden; white-space: nowrap; }

/* ---- "You May Also Like" ----------------------------------------------------------------------
   THE CAROUSEL RUNS TO THE EDGE, as live's `.product-carousel` does: at 1280 the heading and the
   first card sit at x=0 and the track is the full 1280; at 1600 it is a centred 1290px band; on a
   phone it keeps 12px of side padding (heading at x=12, card at x=12). Ours sat in the 1100px
   column with the cards starting at x=114. Live's section is max-width 1290 with 12px padding that
   drops to 0 from the desktop breakpoint. */
.product-upsells { max-width: 1290px; padding: 0 12px 3rem; }
@media (min-width: 1024px) { .product-upsells { padding-left: 0; padding-right: 0; } }
/* THE ARROWS STAY ON THE HEADING'S ROW ON A PHONE AND "View All" GOES, which is the live header at
   390 (heading 12–229, the two 36px chevrons at 298–378 on the same line, the pill display:none).
   Ours wrapped the pill and both arrows onto a second row under the heading because the row was
   allowed to wrap and the pill made it too wide for 366px. */
@media (max-width: 767px) {
  .upsell-head { flex-wrap: nowrap; }
  .upsell-head .upsell-view-all { display: none; }
}
/* === fix-round-3: cart & drawer === */
/* ================================================================================================
   THE CART DRAWER AND THE CART PAGE — the 13 Sep 2026 live-vs-preview audit, cart topics. Every
   rule here answers a numbered finding in that audit (store-home, store-phone-sweep,
   store-cart-checkout, store-content-account); appended as one block on purpose, because several
   agents edit this stylesheet on the same day and a block at the end merges where an edit in the
   middle collides. The drawer's base rules further up still apply; these override the few numbers
   that were wrong.
   ================================================================================================ */

/* ---- THE FLOATING LAUNCHERS GO AWAY WHILE A DRAWER IS OPEN ---------------------------------
   Klaviyo's "35% Off" teaser is a fixed div whose class is kl-teaser-<formId> at z-index 90000,
   and Tidio's bubble is #tidio-chat. A loyalty vendor's "Rewards" pill used to float here too, at
   z-index 2147483647 — the maximum a browser accepts — and went with the vendor on 23 Sep 2026;
   420 Rewards floats nothing. The rules below kept all three off the drawers and now keep two.
   Both were read off the deployed preview's DOM on 13 Sep 2026, read-only, not guessed; there is no
   fixed element with a klaviyo-form-* class — that family is the pop-up form's own contents.
   The cart drawer was z-index 971 (90011 since round 6 — see .cart-drawer-scrim — which puts it
   above Klaviyo's pop-up but still far below these two), the mobile menu 960 and the search drawer
   980 (90004 and 90006 since round 7, for the same reason and still far below these two), so both
   launchers were drawn ON TOP of every open drawer: the Rewards pill covered the right third of
   "Checkout • $62.00" at 1280 (a tap there opened rewards instead of the checkout) and the teaser
   covered the left 84px of it at 390; on the empty drawer the two bracketed "Continue shopping";
   in the menu they sat over the TOP BRANDS links. Live hides its Smile container while the drawer
   is open (0×0, visibility hidden) and its drawer sits above the teaser.
   Hidden, not lifted: there is nowhere on a 390px screen a 146×60 pill can float without covering
   something in a full-width drawer.
   display:none, NOT visibility:hidden — the round-3 rule used visibility and it did nothing you
   could see (QA round 4, 13 Sep 2026: six reports, the same P2). visibility and pointer-events are
   INHERITED properties, and inheritance is exactly what a child can undo: Smile's own stylesheet
   a vendor's own stylesheet would set its container to visibility:hidden and its iframe back to
   visibility:visible (that is how one SDK showed its pill at all — read off the deployed preview:
   container vis=hidden, iframe vis=visible, drawer open or not), and every descendant of
   Klaviyo's teaser carries kl-private-reset-css-*, whose reset writes visibility:visible and
   pointer-events:auto on each of them. So the containers computed "hidden", the tester's probe
   agreed, and the pill and the teaser stayed painted and — the teaser — tappable: elementFromPoint
   30px into "Checkout • $174.00" at 390 returned the teaser's STRONG, and a tap there opened the
   sign-up form instead of the checkout. display is not inherited and a child of a display:none
   box has no box of its own to set anything on. The old worry about an iframe reloading when it
   is toggled through display:none is a Firefox-3 memory: no current browser reloads an iframe
   that stays in the DOM, and the drawer's stylesheet is the wrong place to guard against one that
   did. Belt and braces, the DESCENDANTS are named as well (the `… *` rule under this one): the
   round-3 rule lost because it hit only the container and each child's own non-!important reset
   re-set the child; an !important rule on the children themselves beats that reset, so should a
   vendor ever write display inline with !important and win the first rule, the pill's iframe and
   the teaser's spans are still unpaintable and untappable one by one.
   Keyed on the three body classes the drawers already toggle (js/cart-drawer.js, js/header.js) so
   no script changed for this. Tidio's chat bubble is pinned to the same corner with a z-index in
   the millions and gets the same treatment for the same reason. !important because every one of
   these positions is written inline by a vendor SDK. Spelled out rather than folded into :is(), so
   the one rule that keeps the Checkout button tappable does not depend on a selector an older
   phone browser drops. */
body.cart-drawer-open [class*="kl-teaser-"], body.drawer-open [class*="kl-teaser-"], body.search-open [class*="kl-teaser-"],
body.cart-drawer-open #tidio-chat, body.drawer-open #tidio-chat, body.search-open #tidio-chat,
body.cart-drawer-open #tidio-chat-iframe, body.drawer-open #tidio-chat-iframe, body.search-open #tidio-chat-iframe {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
body.cart-drawer-open [class*="kl-teaser-"] *, body.drawer-open [class*="kl-teaser-"] *, body.search-open [class*="kl-teaser-"] *,
body.cart-drawer-open #tidio-chat *, body.drawer-open #tidio-chat *, body.search-open #tidio-chat * {
  visibility: hidden !important;
  pointer-events: none !important;
}
/* AND ON THE FOUR SIGN-IN PAGES, where live has no launcher at all — its sign-in is hosted on
   shopify.com, which does not load the shop's Smile widget. Ours did, and at 390×844 the Rewards
   pill sat over the right 42% of the "Sign in" button in the exact state the page lands somebody in
   after asking for a code (store-content-account #2): a tap on the button's right half opened the
   rewards panel. A rewards pill has no business on a page whose only job is a password. Klaviyo's
   teaser goes with it: at 390 it sits 84px tall at the bottom-left (y 740–824) — across the LEFT
   end of that same Sign in button (y 762–808) — and live's sign-in page carries neither widget.
   Round 4 found both still drawn there, over "No account yet? Create one. Forgot your password?
   Reset it." — the row a customer without a password is pointed at — for the reason given above
   the drawer rule; the same display:none closes it. The chat bubble goes too: live's sign-in page
   carries none of the three. Plain comma lists, as above, not :is(); the :has() on the body is
   the one dependency, and every browser that has :has() has the drawers' rule as well. */
body:has(main.auth-page) [class*="kl-teaser-"],
body:has(main.auth-page) #tidio-chat,
body:has(main.auth-page) #tidio-chat-iframe {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
body:has(main.auth-page) [class*="kl-teaser-"] *,
body:has(main.auth-page) #tidio-chat * {
  visibility: hidden !important;
  pointer-events: none !important;
}

/* ---- DRAWER TAP TARGETS ≥ 36px (store-phone-sweep #11) -------------------------------------
   Measured in the open drawer at 390: close × 25×26, the bin 21×21, the upsell Add 53×28, View cart
   17px tall, the stepper 30×30. Live's close is a 40px grey circle and its stepper is taller; the
   owner's floor is 36px for anything a finger has to hit. */
.cart-drawer-close {
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0; border-radius: 50%;
  background: #f2f1ec; font-size: 1.5rem;
}
.cart-drawer-close:hover, .cart-drawer-close:focus-visible { background: #e6e4dc; }
/* The bin keeps its place at the top right of the line — the padding grows the box around the icon
   and the negative margin pulls it back so the glyph does not move. 16px icon + 2×10px = 36px. */
.cart-drawer-remove { padding: 10px; margin: -10px -10px 0 0; }
.cart-drawer-qty button { width: 40px; height: 40px; font-size: 1.1rem; }
.cart-drawer-qty span { min-width: 36px; font-size: 1rem; }
.cart-drawer-upsell-add { min-height: 40px; padding: 0.55rem 1.1rem; font-size: 0.9rem; border-radius: var(--radius-pill); }
/* View cart: a full-width 44px block, not a 17px text line under the checkout button. */
.cart-drawer-viewcart {
  padding: 0.7rem 1rem; min-height: 44px; box-sizing: border-box; margin-top: 0.4rem;
  font-size: 1rem; font-weight: 600; text-decoration: underline; text-underline-offset: 3px;
}
/* The checkout button itself: 52px like live's pill, with the pill's radius. */
.cart-drawer-checkout, .cart-drawer-continue { min-height: 52px; font-size: 1.05rem; border-radius: var(--radius-pill); }

/* ---- NO EMPTY BAND BETWEEN THE LINES AND THE SUGGESTIONS -----------------------------------
   The last line's bottom hairline plus the strip's top hairline, with 1rem + 1.25rem of nothing
   between them, read as an empty 40px box in the middle of the panel. One rule now: the last paid
   line has no border of its own and the strip's border is the divider. The gift lines keep theirs,
   because the paid lines follow them. */
#cart-drawer-items .cart-drawer-line:last-child { border-bottom: 0; }
.cart-drawer-upsells { margin-top: 0.25rem; }

/* ---- THE EMPTY DRAWER (store-cart-checkout #9) ---------------------------------------------
   Live's: the ladder from $0 at the top, "Your cart is empty" centred in the space below it, no
   promo strip. The body becomes a column only in this state (js/cart-drawer.js sets .is-empty), so
   the filled drawer's margins are untouched. */
.cart-drawer.is-empty .cart-drawer-body { display: flex; flex-direction: column; }
/* The items slot takes the room the ladder leaves and the sentence floats to its middle. */
.cart-drawer.is-empty #cart-drawer-items { flex: 1 1 auto; display: flex; flex-direction: column; }
.cart-drawer.is-empty .cart-drawer-empty {
  margin: auto 0; padding: 2rem 0; text-align: center;
  font-size: 1.15rem; color: var(--brand-ink);
}
.cart-drawer.is-empty .cart-drawer-foot { border-top: 0; }

/* ---- THE GIFT LINE, AND THE VARIANT LINE UNDER A TITLE -------------------------------------
   "Size: XL" under the product, quieter than the title; the earned gift has no stepper, so its
   foot holds only the tag. The gift line has no bin either; its grid still reserves the third
   column, which stays empty. */
.cart-drawer-line-variant, .cart-line-variant { margin: 0.15rem 0 0; color: #4a5c4d; font-size: 0.85rem; }
.cart-drawer-line-gift .cart-drawer-line-foot { justify-content: flex-start; }
.cart-drawer-line-gift .cart-drawer-line-foot .cart-drawer-code { margin-left: 0; }

/* ---- THE CART PAGE'S LINES (store-cart-checkout #2, #7; store-phone-sweep #3) --------------
   The drawer's line, at the page's width: picture, then the name with its price under it and the
   stepper under that, Remove beside the stepper. Nothing here is nowrap — the old
   .cart-row-total { white-space: nowrap } put "$47.99 $24.00 (Save $23.99) VAPE50 Remove" on one
   unbreakable line and pushed a 390px page out to 591px. That class is no longer used by a line. */
.cart-line {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.25rem 1rem;
  align-items: start; padding: 1rem 0; border-bottom: 1px solid var(--brand-lime-light);
}
.cart-line-thumb {
  width: 96px; height: 96px; object-fit: contain; border-radius: 8px;
  border: 1px solid #efeadf; background: var(--brand-white); display: block;
}
.cart-line-thumb-empty { background: #f4f2ec; }
/* The name and the picture are links to the product (round 11, cart #1 — the drawer's since round
   10): the name keeps its weight and colour and underlines on hover, the picture's link is a block
   so the grid cell is the picture and nothing else. */
.cart-line-link { color: inherit; text-decoration: none; }
.cart-line-link:hover { text-decoration: underline; }
.cart-line-thumb-link { display: block; }
.cart-line-main { min-width: 0; }
.cart-line-title { margin: 0; font-weight: 700; font-size: 1.05rem; line-height: 1.3; overflow-wrap: anywhere; }
.cart-line-price { margin: 0.35rem 0 0; font-size: 1.05rem; color: var(--brand-ink); }
.cart-line-each { color: #4a5c4d; font-size: 0.85rem; }
.cart-line-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1rem; margin-top: 0.75rem; }
/* The stepper: the drawer's, at the same 40px. Overrides the 28px rule further up. */
.cart-qty { display: inline-flex; align-items: center; gap: 0; margin: 0; border: 1px solid #cfc9bb; border-radius: var(--radius); background: var(--brand-white); }
.cart-qty button {
  width: 40px; height: 40px; border: 0; border-radius: 0;
  background: var(--brand-white); cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0;
}
.cart-qty span { min-width: 36px; text-align: center; font-size: 1rem; }
/* Remove: the bin with the word beside it, 40px tall, in the shop's green — a real control rather
   than a browser-default bevel. Pushed to the right of the foot on a wide page. */
.cart-line-remove {
  display: inline-flex; align-items: center; gap: 0.35rem; min-height: 40px; padding: 0 0.6rem;
  margin-left: auto; background: none; border: 0; border-radius: var(--radius);
  color: var(--brand-dark-green); font: inherit; font-size: 0.95rem; font-weight: 600;
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}
.cart-line-remove:hover, .cart-line-remove:focus-visible { background: var(--brand-lime-light); text-decoration: none; }
.cart-line-gift .cart-line-foot { margin-top: 0.5rem; }
@media (max-width: 480px) {
  .cart-line-thumb { width: 72px; height: 72px; }
  .cart-line-title, .cart-line-price { font-size: 1rem; }
}

/* ---- THE CODE BOX AND APPLY -----------------------------------------------------------------
   The field was styled inline and Apply was not styled at all (26px, grey bevel). Both 44px now,
   Apply in the shop's outline-pill style so the primary Checkout below stays the only solid one. */
.cart-discount-row { display: flex; gap: 0.5rem; margin-top: 1rem; max-width: 420px; }
.cart-discount-row input {
  flex: 1; min-width: 0; min-height: 44px; padding: 0.5rem 0.9rem;
  border-radius: var(--radius-pill); border: 1px solid #cfc9bb; font: inherit; font-size: 1rem;
}
.cart-apply-btn {
  min-height: 44px; padding: 0 1.2rem; border-radius: var(--radius-pill);
  border: 1px solid var(--brand-dark-green); background: var(--brand-white); color: var(--brand-dark-green);
  font: inherit; font-weight: 700; cursor: pointer;
}
.cart-apply-btn:hover { background: var(--brand-lime-light); }

/* ---- "YOU MAY ALSO LIKE" ON THE CART PAGE (store-cart-checkout #11) -------------------------
   Six browser-default "Add to cart" buttons (grey, black border) under cards whose buttons sat at
   different heights because one title ran to three lines. The card is a column now, the link
   grows to fill it, and the button is the shop's dark-green pill at the bottom of every card,
   aligned across the row; 44px tall for a finger. */
.upsell-card { display: flex; flex-direction: column; }
.upsell-card > a { flex: 1 1 auto; }
.add-upsell-btn {
  display: block; width: 100%; min-height: 44px; margin-top: 0.6rem; padding: 0.6rem 0.75rem;
  background: var(--brand-dark-green); color: var(--brand-white); border: 0; border-radius: var(--radius-pill);
  font: inherit; font-size: 0.95rem; font-weight: 700; cursor: pointer;
}
.add-upsell-btn:hover { background: #245029; }
/* The cart page's totals rows keep their hairline; the Discounts chips sit inline with the label. */
.cart-row .cart-row-tag { vertical-align: middle; }

/* === fix-round-4: header, footer & SEO === */

/* ---- THE FOOTER'S COLUMN HEADINGS ARE LIVE'S 24px ------------------------------------------
   Live's .link-list__title — TOP BRANDS, CATEGORIES, REACH OUT — measures 24px/24px, weight 900,
   uppercase, in the display face and the dark green, with 20px under it, at 1280 AND at 390 (read
   off www.420.com's footer, 13 Sep 2026; QA round 4, store-home #5). Ours were 1.15rem (18.4px)
   with 1rem under: the newsletter heading beside them had already been brought up to live's 48px,
   which left these three reading as sub-headings of it rather than as the column titles they are.
   .footer-newsletter h2 keeps its own rule further up (48px, 32px on a phone). */
.footer-column h2 { font-size: 24px; line-height: 24px; margin-bottom: 20px; }

/* ---- THE NEWSLETTER HEADING IS CENTRED ON A PHONE, as live's is --------------------------------
   At 390 live's .footer__newsletter-heading is text-align: center across the full 366px column
   (its form under it is full width too), where at 1280 it is left-aligned in its 624px block. Ours
   stayed left at both widths. The measure cap is lifted with it: a centred line under a 624px cap
   inside a 366px column would centre inside the cap rather than the column. The status line the
   form writes ("Thanks — you're on the list") sits under the field and stays as it is. */
@media (max-width: 600px) {
  .footer-newsletter h2 { text-align: center; max-width: none; }
}

/* === fix-round-4: account === */
/* ---- The adopted Log out link is a 40px target, like the icons beside it -------------------
   js/header.js adopt() copies a page's own header links (account.html's #logout-link) into the
   rebuilt bar and marks each .header-adopted. The copy carried no size rule, so on the signed-in
   account page at 390 it measured 55×19 between a 69×32 logo and three 40×40 icons — a 19px tap
   target, and the only sign-out control on the site (QA round 4, flow-account-portal #5). Same
   height, padding and hover ground as .header-icon; the pill shape says it is a button. */
.header-actions .header-adopted {
  display: inline-flex; align-items: center; min-height: 40px; padding: 0 10px;
  border-radius: var(--radius-pill); text-decoration: none;
}
.header-actions .header-adopted:hover { background: rgba(26, 57, 31, 0.08); }   /* the icons' own hover ground, see .header-icon:hover */
/* Under 375px the bar has 195px for the actions and the padded link made 212: the four controls
   dropped to a second row under the logo at 360 (measured on a 360×800 emulation). The tap box is
   kept — 6px of padding each side, 67×40 — and the same 6px of negative margin gives it back to the
   layout, so the link takes the 55px its word takes and the row stays one row. */
@media (max-width: 374px) {
  .header-actions .header-adopted { padding: 0 6px; margin: 0 -6px; }
}
/* === fix-round-4: launchers, drawer, cart page === */
/* ================================================================================================
   QA ROUND 4 (13 Sep 2026) — the storefront items in store-home, store-phone-sweep,
   store-cart-checkout, store-product, store-collection-search and store-content-account that
   belong to the drawer, the cart page and the two floating launchers. The launcher rule itself is
   corrected in place further up (its round-3 comment records why visibility failed); everything
   else that is NEW lives here, at the end, for the reason the round-3 block gives: several people
   edit this stylesheet on the same day and an appended block merges where an edit in the middle
   collides.
   ================================================================================================ */

/* ---- THE FAQ PHOTOGRAPH KEEPS ITS FACES (store-home #3) --------------------------------------
   The one FAQ image the shop uploads is a portrait (465×684). Below 1024 the band is a fixed
   height with object-fit:cover, and cover crops from the CENTRE by default — so at 390 the 250px
   band showed the couple from the chest down with both heads cut off, and at 768 the 500px band
   did the same. Live serves a landscape crop on a phone and its faces are in the band; ours has
   one asset, so the crop is anchored to the top of the photograph, where the faces are. Scoped to
   the widths where the band is cropped at all; the desktop's two-column layout shows the whole
   photograph and object-position does nothing there. */
@media (max-width: 1023px) {
  .home-page .home-faq-art img { object-position: top; }
}

/* ---- "YOU MAY ALSO LIKE" ON THE CART PAGE: TWO ACROSS AT 360 AS AT 390 (store-phone-sweep #3)
   The grid was minmax(150px, 1fr): at 390 the 342px grid takes two 150px columns with the gap, at
   360 the 312px grid does not (2×150 + 16 = 316), so every card became a full-width 290px square
   and the strip ran to one card per screen. 140px is the widest minimum that still gives two
   columns at 360 (2×140 + 16 = 296 ≤ 312). Scoped to phone widths, because a smaller minimum
   repacks a wider grid too (at 1024 it would fit one more, narrower column) and nothing above
   480px was wrong; at 390 it still packs two, the same as before. */
@media (max-width: 479px) {
  .upsell-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}

/* ---- THE CART PAGE'S LINE FOOT: ONE SHAPE FOR EVERY LINE (store-cart-checkout #6) -------------
   The foot was one wrapping flex row holding the stepper, the promotion's tag and Remove. At 390
   the Litty line's VAPE50 tag took the room Remove needed, so its Remove dropped to a row of its
   own under the stepper while the Mystery line (no tag) kept Remove beside the stepper — two line
   shapes on one page. Now the stepper and the tag share a box of their own (.cart-line-qtyrow in
   cart.html) that wraps INSIDE itself, and Remove is the foot's other child, pinned to the right of
   the stepper's row whatever the tag does: on a phone the tag drops under the stepper and Remove
   stays where the eye expects it; on a wide page all three sit on one row. nowrap on the foot is
   safe because its two children can each shrink — the tag box wraps within itself and Remove is a
   fixed ~100px — where the old single row could not. */
.cart-line-foot { display: flex; flex-wrap: nowrap; align-items: flex-start; justify-content: space-between; gap: 0.5rem 1rem; margin-top: 0.75rem; }
.cart-line-qtyrow { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; min-width: 0; }
.cart-line-remove { flex: none; margin-left: auto; }
/* Under 360 the line's column (the page's padding, a 72px picture and its gap taken off) is
   narrower than the stepper, the gap and Remove together — 118 + 16 + 96 = 230px against ~184px
   at 320 — and nowrap would push the stepper into Remove. There the foot may wrap and Remove goes
   under the stepper on EVERY line, tag or no tag, which is still one shape for the page. */
@media (max-width: 359px) {
  .cart-line-foot { flex-wrap: wrap; }
  .cart-line-remove { margin-left: 0; }
}

/* ---- THE TOTAL ON ONE LINE, AND A CHECKOUT BUTTON THE DRAWER WOULD RECOGNISE ------------------
   "Total: $174.00 (+ tax calculated at checkout)" was one 1.3rem bold string, right-aligned, which
   at 390 wrapped into two ragged lines. It is now the row the other totals are — the word at the
   left, the figure at the right, on one line — with the tax sentence under it in the drawer's
   quieter type, which is how the drawer prints "Estimated total" over "Taxes and shipping
   calculated at checkout." The figure never wraps: it is the one number on the page that must be
   read whole. */
#totals .cart-total {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-top: 0.75rem; padding: 0.5rem 0 0; font-size: 1.25rem; font-weight: 700; text-align: left;
}
#totals .cart-total > strong { white-space: nowrap; flex: none; }
#totals .cart-total-note { margin: 0.35rem 0 0; color: #4a5c4d; font-size: 0.9rem; }
/* The Discounts row's chips wrap under the label when three are in play (VAPE50, FREE HAT, FREE
   420 HOODIE — store-cart-checkout #7, seen in the drawer and true of this row too); the amount at
   the right stays on one line rather than splitting "−" from "$123.97". The label box shrinks, the
   amount does not. Scoped to this page's #totals: .cart-row is also the account, invoice and
   confirmation pages' row, where a long right-hand value must be allowed to wrap — except the
   receipt's and the invoice's TOTALS rows (.order-totals, below with the receipt's rules), whose
   right-hand value is only ever a figure and which split "−" from "$32.25" at 390 the moment the
   Discounts row carried a code chip and a gift chip (QA round 13). */
#totals .cart-row > span:first-child { min-width: 0; }
#totals .cart-row > span:last-child { white-space: nowrap; flex: none; margin-left: 1rem; }
/* Checkout: the drawer's button — full width, 52px, the pill radius, the figure on it — instead of
   a 203×46 pill hugging the left edge under a full-width gift panel and a full-width discount row.
   The row is the <p> around the link in cart.html; the link is a block so the button's whole width
   is the link's. */
.cart-checkout-row { margin: 1rem 0 0; }
.cart-checkout-row > a { display: block; text-decoration: none; }
#checkout-btn { display: block; width: 100%; min-height: 52px; font-size: 1.05rem; font-weight: 700; padding: 0.75rem 1rem; }

/* ---- THE DRAWER'S DISCOUNTS ROW: THE AMOUNT STAYS WHOLE (store-cart-checkout #7) --------------
   Same defect as the page's, same shape of fix: with three chips the label span grew, the flex row
   squeezed the amount span, and "−$123.97" broke after the minus. The chips may wrap under
   "Discounts"; the money may not. */
.cart-drawer-total > span:first-child { min-width: 0; }
.cart-drawer-total > span:last-child, .cart-drawer-total > strong { white-space: nowrap; flex: none; margin-left: 1rem; }
/* === fix-round-4: product page & bundle === */
/* ================================================================================================
   THE PRODUCT PAGE'S LOWER HALF AND ITS SMALL TYPE — the 13 Sep 2026 QA round 4, store-product
   #3–#8. Every rule answers a numbered finding; appended as one block for the reason the round-3
   block gives (several agents edit this sheet on the same day, and a block at the end merges where
   an edit in the middle collides). The product-page rules further up still apply; these override
   the numbers that were wrong. Every figure below was read off www.420.com with getComputedStyle at
   1280 and 390 on 13 Sep 2026, not guessed.
   ================================================================================================ */

/* ---- WHERE WE SHIP RUNS THE PAGE'S OWN COLUMN (store-product #3) ---------------------------
   Live's `.shipping-map-420__container` is max-width 1280px with 24px of side padding (16px on a
   phone), and its grid is two 584px columns 64px apart: the sentence at x=24 in a 480px measure,
   the map 456px wide centred in the right column (736–1192 at 1280). Ours still sat in the old
   1100px/1.5rem main — text at x=114, map 366px wide — so the page stepped IN by a hundred pixels
   right after the full-bleed carousel and stepped out again for the footer. The inner grid, the
   480px measure and the map's 40px/64px padding were already live's; only the container was not. */
.product-shipping { max-width: 1280px; padding: 0 24px 3rem; }
@media (max-width: 749px) { .product-shipping { padding: 0 16px 3rem; } }

/* ---- THE FAQ IS LIVE'S CREAM CARD BESIDE ITS PHOTOGRAPH (store-product #3, #4) --------------
   The same block the home page draws (the `.home-page .home-faq` rules above, read off live's
   `.faq-section`), on the product page's own markup — which already uses these classes, so this is
   the home page's card restated under `.product-faqs` rather than a second design. Restated rather
   than folded into those selectors for the same reason page.html carries its own copy for the FAQ
   page: the home block is scoped to `.home-page` on purpose and is another fixer's to edit today.
   The numbers are the same numbers and must stay so.

   What it is: `.faq-section__layout` is a 1290px band (0–1280 at 1280, centred at 1600) of two
   columns 16px apart — the photograph left with 16px corners, the questions right in a #f5f2eb card
   with 16px corners and 40px inside. "FAQS" is the display face at 48px/48px with 24px under it;
   each question is Inter 20px/24px weight 500 in the GREEN on a 16px-padded row, a 16px chevron
   at its right that turns over when the answer opens, hairlines at 12% green between rows; the
   answer is 15px/24px with 16px under it. On a phone (12px of side padding) the photograph is a
   250px band over the card with object-fit CONTAIN — live's whole photograph, letterboxed, faces
   in frame; cover from the centre showed a torso and shoes (store-product #3) — the card has 24px
   inside, the heading is 40px/44px and the questions are 17px/28px, still 500 and green. Ours drew
   a bare list on a white ground in the 1100px column: "FAQS" at 32px, bold 16px questions and a
   "+" that became "−".

   THE QUESTION'S SIZE WAS MEASURED TWICE, ON TWO ELEMENTS. Live's row is a <button
   class="faq-accordion__trigger"> (17px/28px 400 black — the button's own inherited type) holding a
   <span class="faq-accordion__question"> that carries the words (20px/24px 500 green at 1280,
   17px/28px 500 green at 390). The fifth round measured the button and moved the questions to
   17px/28px 400 black — smaller, lighter and greyer than what a person sees beside live's card
   (store-product #5, then #3 in the sixth round, which re-measured the span on all five product
   pages and on /pages/faqs). The span is the reference; the home page's card and page.html's FAQ
   page already print it. The chevron and the margins from the fifth round stay. */
.product-faqs { max-width: 1290px; margin: 0 auto; padding: 0 12px 3rem; }
@media (min-width: 1024px) { .product-faqs { padding-left: 0; padding-right: 0; } }
.product-faqs .home-faq { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; margin: 0; align-items: start; }
.product-faqs .home-faq:not(:has(.home-faq-art)) { grid-template-columns: 1fr; }
.product-faqs .home-faq-art img { display: block; width: 100%; height: auto; border-radius: 16px; }
.product-faqs .home-faq-body {
  background: var(--brand-cream); border-radius: 16px; padding: 40px; color: var(--brand-dark-green);
}
.product-faqs .home-faq h2 { margin: 0 0 24px; font-size: 48px; line-height: 48px; color: var(--brand-dark-green); }
.product-faqs .home-faq-item { border-top: 0; border-bottom: 1px solid rgba(26, 57, 31, 0.12); }
.product-faqs .home-faq-item:last-of-type { border-bottom: 0; }
.product-faqs .home-faq-item summary {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 16px 0; font-size: 20px; line-height: 24px; font-weight: 500; color: var(--brand-dark-green);
}
/* The chevron: a 9px square's two edges, turned — live's 16px chevron-down icon at the row's right,
   drawn the way the home page and page.html draw it; it turns up when the answer is open. */
.product-faqs .home-faq-item summary::after {
  /* The chevron sits on the line's centre: (24 − 9) / 2 ≈ 6px down on the 24px line when closed,
     10px when turned (the turned square's visual centre moves up); 8px and 12px on the phone's 28px
     line, below. The green is live's own chevron colour, and the question's. */
  content: ""; flex: 0 0 auto; width: 9px; height: 9px; margin: 6px 4px 0 0;
  border-right: 2px solid var(--brand-dark-green); border-bottom: 2px solid var(--brand-dark-green);
  transform: rotate(45deg); transition: transform 0.2s ease;
  font-size: 0; line-height: 0;
}
.product-faqs .home-faq-item[open] summary::after { content: ""; transform: rotate(-135deg); margin-top: 10px; }
.product-faqs .home-faq-item p { margin: 0; padding-bottom: 16px; font-size: 15px; line-height: 24px; color: var(--brand-dark-green); }
@media (max-width: 1023px) {
  /* One column below 1024, as live stacks it. The band shows the WHOLE photograph (contain), as
     live's does: the shop has one portrait asset, and cover cropped the faces out of a 500px band
     at 768 and a 250px band at 390 (store-product #3, the fifth QA round). */
  .product-faqs .home-faq { grid-template-columns: 1fr; }
  .product-faqs .home-faq-art img { height: 500px; object-fit: contain; }
}
@media (max-width: 767px) {
  .product-faqs .home-faq-art img { height: 250px; }
  .product-faqs .home-faq-body { padding: 24px; }
  .product-faqs .home-faq h2 { font-size: 40px; line-height: 44px; margin-bottom: 16px; }
  .product-faqs .home-faq-item summary { font-size: 17px; line-height: 28px; }
  .product-faqs .home-faq-item summary::after { margin-top: 8px; }
  .product-faqs .home-faq-item[open] summary::after { margin-top: 12px; }
}

/* ---- HEADINGS INSIDE SHOP COPY ARE BODY HEADINGS (store-product #5) -------------------------
   The T-shirt's description carries <h3>Features</h3> and <h3>Perfect For</h3>, and the site-wide
   heading rule at the top of this sheet reached into it: "FEATURES" in the uppercase display face at
   19.89px/900. Live prints them as bold sentence-case body headings — 20px/26px weight 700 in the
   heading ink, no transform, 15px over and under — with the list under each at 17px/28px, 20px in
   from the edge with 15px under it. A heading typed into a product's description by whoever wrote
   the copy is prose, not a section of this page; the same exception the card title and the cart
   line already have. Scoped to the HTML body (.product-description-html) because that is the only
   description that can contain a heading at all. */
.product-buybox .product-description-html :is(h1, h2, h3, h4, h5, h6) {
  font-family: var(--font-primary); font-size: 20px; line-height: 26px; font-weight: 700;
  text-transform: none; letter-spacing: normal; color: var(--brand-heading); margin: 15px 0;
}
.product-buybox .product-description-html ul, .product-buybox .product-description-html ol { margin: 0 0 15px 20px; padding: 0; }
.product-buybox .product-description-html li { margin: 0; padding: 0; }

/* ---- THE SUMMARY AND "Read more" AT LIVE'S SIZE (store-product #7) --------------------------
   Live's `.pdp-420__description-short` is 15px/20px weight 500 with 12px under it, and the "Read
   more" summary of its accordion is 13px/16px weight 500, underlined, in the green, 24px above the
   assurances. Ours ran 16.8px/25.2px and 16px, so at 390 the buy column was three lines longer
   before the assurances. The control keeps the owner's 36px floor for anything a finger has to hit
   — 13px text on a 36px-tall button (10px of padding above and below the 16px line), which is what
   puts live's 6px above the words and ~30px below them back in the same place. */
.product-buybox .product-description-short { font-size: 15px; line-height: 20px; font-weight: 500; margin: 0 0 8px; color: var(--brand-dark-green); }
.product-buybox #description-toggle {
  display: inline-flex; align-items: center; min-height: 36px; margin: 0 0 20px; padding: 10px 0;
  font-size: 13px; line-height: 16px; font-weight: 500; text-underline-offset: 2px;
}

/* ---- THE STARS ARE LIME (store-product #8) --------------------------------------------------
   Every star Yotpo draws on live — the row under the title, the reviews block — is #99C121 fill and
   stroke, which is this shop's --brand-lime to within a shade, and the same colour the home page's
   testimonials and the cart drawer's suggestion stars already use. Ours were the gold of a generic
   review widget (#c8a300), on the one page where the badges beside them are lime. The figure beside
   the stars stays in the page's ink: live paints "4.7" in the same lime, which is 1.9:1 on white,
   and a number is there to be read. */
.stars { color: var(--brand-lime); }
/* === fix-round-4: checkout & receipt === */
/* ================================================================================================
   THE CHECKOUT AND THE ORDER CONFIRMATION — the QA round-4 findings (flow-checkout #2 #3 #6 #8,
   store-cart-checkout #2–#5, store-phone-sweep #4, flow-account-portal #2). Appended as one block
   for the reason the round-3 blocks give: several agents edit this stylesheet on the same day and a
   block at the end merges where an edit in the middle collides.
   ================================================================================================ */

/* ---- THE APPLIED CODE WITH ITS REMOVE CONTROL (flow-checkout #2) ---------------------------
   "Code X applied." was a bare sentence; the cart page's box and the live checkout's code tag both
   offer a way to take the code off. The sentence and the button share the green notice, the button a
   real 36px control in the shop's outline style so Apply (solid) stays the box's primary action. */
.checkout-discount-applied { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.checkout-discount-remove {
  min-height: 36px; padding: 0 0.9rem; border-radius: var(--radius-pill);
  border: 1px solid var(--brand-dark-green); background: var(--brand-white); color: var(--brand-dark-green);
  font: inherit; font-size: 0.9rem; font-weight: 700; cursor: pointer;
}
.checkout-discount-remove:hover, .checkout-discount-remove:focus-visible { background: var(--brand-lime-light); }
/* Apply in the cart page's own outline style (.cart-apply-btn) rather than the browser's grey bevel
   it had, and the field beside it the same 40px, so the box reads as the cart's box on this page. */
.checkout-discount-row input, .checkout-discount-row button { min-height: 40px; }
.checkout-discount-row button {
  padding: 0 1.1rem; border-radius: var(--radius); border: 1px solid var(--brand-dark-green);
  background: var(--brand-white); color: var(--brand-dark-green); font: inherit; font-weight: 700; cursor: pointer;
}
.checkout-discount-row button:hover, .checkout-discount-row button:focus-visible { background: var(--brand-lime-light); }

/* ---- THE CHIP ON THE DISCOUNTS ROW AND THE GIFT LINE (flow-checkout #3) ----------------------
   The cart page's .cart-row-tag, on the summary: "Discounts VAPE50 FREE HAT −$48.99" is the row the
   drawer and the cart print, and the checkout printed "Discount −$23.99" with no cause. Same size,
   same green, so the three surfaces read as one basket. */
.summary-chip {
  display: inline-block; margin-left: 0.35rem; padding: 0.05rem 0.4rem; vertical-align: middle;
  background: var(--brand-lime-light); color: var(--brand-dark-green);
  border-radius: 9999px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.02em;
}
.summary-line-tag .summary-chip { margin-left: 0; }
/* The gift's picture where the cart shows one; the lime ground stays for a gift with no photo. */
.summary-thumb-gift img { background: var(--brand-white); }

/* ---- THE RECEIPT (flow-checkout #8, flow-account-portal #2) -----------------------------------
   The totals table was 32rem wide under a full-width items table, so on a desktop Subtotal…Total
   ended halfway across the page while the item prices sat at the right edge; and "Continue shopping"
   was a browser-default blue underlined link under the shop's own styled page. The totals now run the
   width of the items (the same .cart-row hairline rows, right-aligned figures), Total on the same
   right edge, and the link is the shop's dark-green pill (.primary-link, the cart's own Continue
   shopping). The parcels list is the shipments block: one row per box, carrier and tracking link,
   what was in it, when it went. */
.order-totals { max-width: none; margin-top: 1rem; }
.order-totals .cart-total { padding: 0.75rem 0 0; }
/* The checkout's own rule for its #totals, on the receipt's and the invoice's totals: the chips wrap
   under "Discounts", the figure never breaks (15 Sep 2026, QA round 13 at 390). */
.order-totals .cart-row > span:first-child { min-width: 0; }
.order-totals .cart-row > span:last-child { white-space: nowrap; flex: none; margin-left: 1rem; }
.order-items .cart-row > span:last-child { white-space: nowrap; padding-left: 1rem; }
.order-item-note { display: block; font-size: 0.85rem; color: #4a5c4d; }
/* "Still to come" under the parcels of a part-shipped order: the parcel contents' own type, spaced
   as the shipments list is (15 Sep 2026). */
.order-still-to-come { margin: 0 0 1.5rem; }
.order-continue { margin: 1.5rem 0 0; }
.order-shipments { margin: 0 0 1.5rem; padding: 0; list-style: none; display: grid; gap: 0.75rem; }
.order-shipment {
  padding: 0.9rem 1rem; border: 1px solid #d9d4c7; border-radius: var(--radius); background: var(--brand-white);
}
.order-shipment h3 { margin: 0 0 0.25rem; font-size: 1rem; font-family: inherit; letter-spacing: 0; text-transform: none; }
.order-shipment p { margin: 0.15rem 0; line-height: 1.5; overflow-wrap: anywhere; }
.order-shipment a { color: var(--brand-dark-green); font-weight: 600; }
.order-shipment-contents { color: #4a5c4d; font-size: 0.95rem; }
/* The account page's history: the parcels under the row, one line each, not one run-on sentence. */
.order-history-parcels { display: block; margin-top: 0.25rem; font-size: 0.95rem; color: #4a5c4d; }
.order-history-parcels > span { display: block; }
.order-history-parcels a { color: var(--brand-dark-green); font-weight: 600; }
/* THE PRICE NEVER TOUCHES THE DATE. .cart-row is flex with space-between and no gap, which is fine
   while the row is one line (at 1280 there are 250–650px between the text and the amount) — but at
   390 the text wraps to two or three lines, the text span then fills the whole width, and the amount
   sits flush against its first line: "Order #46933 — September 14, 2026$35.00" read as one run on
   six of eight rows (QA round 8, 14 Sep 2026). A gap, and a row that is allowed to wrap: when the
   text and the amount fit side by side they stay side by side, as before; when the text is longer
   than the row the amount drops under it, right-aligned, on a line of its own — the way the drawer
   puts a long line's figure below it — rather than squeezing the text to make room. Scoped to
   #orders: the same class is the invoice's and the receipt's row, which have their own rule. */
#orders .cart-row { flex-wrap: wrap; column-gap: 1rem; row-gap: 0.25rem; align-items: baseline; }
#orders .cart-row > span:last-child { margin-left: auto; white-space: nowrap; }

/* === fix-round-5: home, header, search, content === */
/* ================================================================================================
   QA ROUND 5 (13 Sep 2026) — store-home, store-collection-search, store-content-account and the
   product page's FAQ card. Most of this round's rules are corrected IN PLACE further up (the nav
   size, the filter pills, the search field's ring, the bundle CTA, the product FAQ card), each
   beside the comment that recorded the earlier measurement; what is NEW lives here, at the end, for
   the reason the round-3 block gives: several people edit this stylesheet on the same day and an
   appended block merges where an edit in the middle collides.
   ================================================================================================ */

/* ---- NO MARKETING POP-UP OVER THE SIGN-IN PAGES (store-content-account #1) ------------------
   Round 4 hid Smile's pill, Klaviyo's teaser and Tidio on the four auth pages because live's
   sign-in (hosted on shopify.com) carries none of them. Klaviyo's onsite sign-up FORM was not part
   of that: about eight seconds after /login, /register or /forgot-password opened, "Warehouse
   Clearance — UNLOCK 35% OFF — Join 420.com" came up full-screen over the sign-in form, asking for
   an email address before the person could type it into the field they came for — and Klaviyo
   put klaviyo-prevent-body-scrolling on <body>, so the page behind could not scroll either.

   THE CONTAINER, NOT THE FORM. Read off the deployed preview's DOM on 13 Sep 2026: the pop-up is
   a fixed, full-viewport <div role="dialog" aria-modal data-kl-scroll-locking-modal> at z-index
   90000 (the backdrop), holding data-testid="POPUP", the modal-form-container and, seven levels
   down, <form class="klaviyo-form …" data-testid="klaviyo-form-TcifQj">. Hiding the form's own
   klaviyo-form-* classes would leave the backdrop over the page (the round-3 note above says so),
   so the rule takes the dialog — by the attribute Klaviyo puts on the element that locks the
   scroll, with the aria-modal dialog that contains a Klaviyo form as the fallback should that
   attribute be renamed. The teaser is a sibling `[class*="kl-teaser"]`, already hidden above. The
   four auth pages load js/klaviyo.js for identify() after sign-in, so dropping the script is not
   the fix; js/klaviyo.js takes the scroll-lock class off <body> whenever Klaviyo adds it there,
   because a hidden dialog that still thinks it is open would otherwise freeze the page. The
   cleaner fix is Klaviyo's own URL targeting on the form (exclude /login, /register,
   /forgot-password, /reset-password) — a setting in the Klaviyo account, not a deploy — and that
   is recorded as a decision for the owner; this rule holds either way. The footer's newsletter form is
   this platform's own (js/footer.js), not a Klaviyo form, so it is untouched. */
body:has(main.auth-page) [data-kl-scroll-locking-modal],
body:has(main.auth-page) [role="dialog"][aria-modal]:has(form.klaviyo-form) { display: none !important; }
/* The lock itself: Klaviyo's class sets overflow hidden on <body> (and on some pages position
   fixed). Undone on these pages so a hidden form cannot hold the page still. */
body.klaviyo-prevent-body-scrolling:has(main.auth-page) { overflow: auto !important; position: static !important; }

/* ---- THE EMPTY SEARCH LEAVES LIVE'S ROOM ABOVE THE FOOTER (store-collection-search #4) --------
   Live's empty states — "View Collections" on a search with no results, "Use the search bar above
   to find products." on a bare /search — end 96px above the footer at 1280 and 72px at 390. Ours
   left 168px at both: the block's own 48px foot, then the empty grid's 3rem, then the 3rem the
   listing main keeps under a full grid so its last row clears the footer. With nothing in the grid
   the main's foot has nothing to clear, so it goes; the block keeps live's 48px (24px on a phone)
   and the 24px the footer has above its own content makes up the rest. Scoped to a main that holds
   the empty block, so a grid with results is untouched. */
main.catalogue-main:has(.search-empty), main.catalogue-main:has(.search-no-results) { padding-bottom: 0; }
/* 96px / 72px, not 48 / 24: these two figures were sized to sit on top of the footer's 3rem margin
   (48 + 48 = live's 96, 24 + 48 = live's 72), and round 8's catalogue-foot rule further down took
   that margin away from every page whose main is the catalogue — this one included — so the bare
   /search and the no-results page both ended 48px closer to the cream band than live (52 to live's
   100 at 1280, 28 to live's 76 at 390; QA round 9, collections-search #1, a regression of round 7's
   fix). The block now carries the whole distance itself. */
main.catalogue-main:has(.search-empty) .search-empty { padding-bottom: 96px; }
main.catalogue-main .search-no-results { padding-bottom: 96px; }
@media (max-width: 767px) {
  main.catalogue-main:has(.search-empty) .search-empty { padding-bottom: 72px; }
  main.catalogue-main .search-no-results { padding-bottom: 72px; }
}
/* AND THE FILTERED-EMPTY GRID (the sixth QA round, store-collection-search #4). The round above
   covered the two search empty states and not the collection's `.grid-empty`, which kept the full
   band: the empty pager's 24px top margin, the main's 3rem foot and the footer's 3rem head — 120px
   from "Try adjusting your filters or search term." to the footer at both widths. The block keeps
   16px and the pager and the main keep nothing.
   AND THE FOOTER'S OWN 3rem GOES TOO. The sixth round read live's footer band as starting 64px under
   the sentence — "48px of its own foot and the container's 16px" — and left the footer's margin-top
   in place to make up the 48. Live's own DOM says 16: its main ends at the text's 16px of padding
   and the cream band starts there (text bottom 337, footer top 353 at 1280; 383 → 399 at 390,
   re-measured by the seventh QA round, store-collection-search #1, and pixel-scanned to confirm the
   eye sees what the DOM says). The 48px is not the footer's padding, which paints cream, but its
   3rem margin-top (.site-footer), which paints white above the band — so on a page whose grid is
   empty the footer keeps no margin, and the band begins 16px under the sentence at both widths.
   body:has(), because footer.js appends the footer to <body> and it is not main's next sibling. */
main.catalogue-main:has(.grid-empty) { padding-bottom: 0; }
main.catalogue-main:has(.grid-empty) .pagination { margin-top: 0; }
/* Round 8 (14 Sep 2026): the block itself is live's — 48px of padding above AND below the centred
   title and sentence (`.product-grid-420__empty`, --space-xl4 = 48px in base-420.css) — so the
   16px override that made the round-7 foot is gone; the footer still keeps no margin here. */
body:has(main.catalogue-main .grid-empty) .site-footer { margin-top: 0; }

/* === fix-round-5: cart & checkout === */
/* ================================================================================================
   THE CART, THE CHECKOUT, THE GRID PAGER AND THE PRODUCT PAGE'S BAR — the QA round-5 findings
   (flow-checkout #1 #3 #4 #5, store-cart-checkout #1–#4, store-phone-sweep #1–#4). Appended as one
   block for the reason the round-3 and round-4 blocks give: several agents edit this stylesheet on
   the same day and a block at the end merges where an edit in the middle collides. The rules
   further up still apply; these override the few numbers that were wrong.
   ================================================================================================ */

/* ---- THE CHECKOUT'S "ADD BEFORE YOU PAY" BUTTONS ARE 40px (store-phone-sweep #2) ---------------
   The three upsell Adds were 52×26 — the smallest tappable control on the money page, 14px under
   the floor the cart drawer's upsell Add (64×40) and the cart page's Add to cart (137×44) took in
   rounds 3–4. Same outline pill, the height the drawer's has; the row stays one line because only
   the button's height changes and the title is the thing that gives way. */
.checkout-upsell-add { min-height: 40px; padding: 0 1.1rem; }

/* ---- THE GRID PAGER'S BUTTONS ARE 40px, AS LIVE'S ARE (store-phone-sweep #4) ------------------
   "2", "3", "…", "14" and the arrows measured 38×38 on the home grid and on every collection; live's
   pagination-420__btn is 40×40 and the rest of the phone's controls now stop at 40. The gap marker
   keeps its narrower width (it is not a target) and takes the height from the shared rule. */
.pager-btn { min-width: 40px; height: 40px; }

/* ---- KLAVIYO'S TEASER IS LIFTED OVER THE PRODUCT PAGE'S BAR (store-phone-sweep #3) --------------
   The "35% Off" disc is pinned bottom-left by an inline style (bottom 0, left 0, margin 20px, 84×84 —
   read off the deployed preview, see the launcher rule in the round-3 block), and the sticky buy bar
   is 74px tall along the same edge, so at 390 and 360 the bar's price ("$250.00 struck, $200.00", x
   24–94, y 790–824) sat entirely under the disc (x 20–104, y 740–824): a shopper saw "$" and nothing
   after it. css/bundle.css lifts this same teaser over the bundle bar; this is the same
   lift for the same disc on this page — its 20px margin puts its visible edge 20px above the bar,
   as on /bundle. At every width, because the bar is on screen at every width and the disc is 84px
   tall along the bottom edge wherever it is. The figure is the bar's own height formula (74px plus
   the safe area — the .sticky-buy padding rule and the body's padding-bottom use the same one); any
   other would leave the disc on the bar or floating above nothing. !important because the position
   is written inline by the SDK. */
body:has(.sticky-buy:not([hidden])) [class*="kl-teaser-"] {
  bottom: calc(74px + env(safe-area-inset-bottom, 0px)) !important;
}

/* === fix-round-7: the suggestion strips === */
/* ================================================================================================
   THE SUGGESTION STRIPS — the QA round-7 findings (store-cart-checkout #1 and #3, store-home #2,
   store-product #4, 14 Sep 2026). Appended as one block for the reason every block above gives:
   several agents edit this stylesheet on the same day and a block at the end merges where an edit
   in the middle collides. The rules further up still apply; these add the size select the strips
   now draw and re-shape the checkout's row so the strike no longer costs the name.
   ================================================================================================ */

/* ---- THE SIZE SELECT ON A SUGGESTION (store-cart-checkout #1) ----------------------------------
   A multi-size product was added on one blind press — the six-size T-shirt went in as a Small with
   no size shown anywhere. Live's tile (UpCart) puts a size <select> under the price, S selected,
   beside a narrower Add; the drawer's card, the cart page's card and the checkout's row now draw
   the same control (upsellSizeSelectHtml in js/cart-drawer.js, cart.html and checkout.html; the
   receipt's strip injects its own copy of these rules). The shop's field look — hairline border,
   the page radius, white — at the text size the price beside it is set in, 40px tall so it is a
   finger's target on a phone like every other control in the strips; never wider than its column,
   so a long option name cannot push the Add button out. */
.cart-drawer-upsell-size, .checkout-upsell-size { display: block; margin-top: 0.35rem; max-width: 100%; }
.cart-drawer-upsell-size select, .checkout-upsell-size select, .upsell-card-size select {
  min-height: 40px; padding: 0.35rem 0.6rem; box-sizing: border-box;
  font: inherit; font-size: 0.95rem; color: var(--brand-ink);
  border: 1px solid #cfd6c9; border-radius: var(--radius); background: var(--brand-white);
}
.cart-drawer-upsell-size select, .checkout-upsell-size select { width: 100%; max-width: 11rem; }
/* On the cart page's column card the select is a row of its own between the link and the button,
   the button's full width and the button's 44px, so the two read as one control stack. */
.upsell-card-size { display: block; margin-top: 0.6rem; }
.upsell-card-size select { width: 100%; min-height: 44px; }

/* ---- THE CHECKOUT'S ROW: THE NAME OVER THE PRICE (store-cart-checkout #3) ----------------------
   The row was thumb · title · price · Add on one line, and once the struck compare-at sat beside
   the price (round 6) the title paid for it: "420 Vape Ca… $400.00 $200.00" at 390 and "420 Vap…" —
   71px — at 360, a product a shopper cannot name from the row inviting them to add it; even at 1280
   it read "420 Vape Care Pac…". The title now has the middle column to itself, on up to two lines,
   with the price pair under it (and the size select under that) — the shape the drawer's card and
   the summary's own lines already use — and the Add stays at the right, vertically centred. The
   earlier .checkout-upsell-title rule's nowrap/ellipsis and flex:1 are undone here: the span is no
   longer a flex item of the row but of its own column. */
.checkout-upsell-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.checkout-upsell-title {
  flex: none; white-space: normal; text-overflow: clip; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* === fix-round-8: store-content-3 === */
/* ================================================================================================
   POLISH AND TRUTHFULNESS — the QA round-8 findings for the content pages, the product page, the
   collections and the home page (store-content-account #3 #4, store-product #1 #3 #4 #6,
   store-collection-search #1 #4, store-home #1–#5, 14 Sep 2026). Appended as one block for the
   reason every block above gives: several agents edit this stylesheet on the same day and a block
   at the end merges where an edit in the middle collides. The rules further up still apply; these
   override the numbers that were wrong. Every figure below was re-measured on www.420.com with
   getComputedStyle at 1280 and 390 on 14 Sep 2026 before it was written here — not copied from the
   finding.
   ================================================================================================ */

/* ---- THE 404 PAGE IS LIVE'S (store-content-account #4) ----------------------------------------
   www.420.com/pages/<anything-missing>: a centred column (.template-404, flex column, centred) with
   70px above and below — main 116→483 at 1280 and 155→469 at 390, both 70 + content + 70 — holding
   a "404" eyebrow at 17px/28px, "Page not found" at 40px/52px (30px/39px on the phone) in a plain
   400 weight with 50px over and 30px under (30 and 20 on the phone), and one "Continue shopping"
   button: 47px tall, 0 30px, 15px/18px letter-spaced 1px, white on #121212, square corners, 20px
   above it (10 on the phone). The rounds-3–7 page — the display-face heading, the search box, the
   green link — is gone with its rules' markup (404.html says why); the rules below replace the
   round-3 .not-found ones above outright. Live sets the heading in its body face at 400, so the
   body face here too rather than the shop's display face, which comes in one heavy cut. Black on a
   green shop looks foreign because it is Dawn's default button — and it is what live shows. */
.not-found {
  max-width: none; margin: 0; padding: 70px 12px; color: var(--brand-ink);
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
.not-found-code { margin: 0; font-size: 17px; line-height: 28px; color: var(--brand-ink); }
/* text-transform: none — the sheet's own h1 rule up top capitalises every heading in the display
   face, and live's is "Page not found", mixed case, in the body face at 400. */
/* Assistant 400, the face live's Dawn template renders the HEADING in (the ninth QA round measured
   the Inter heading 288px wide against live's 250 at 1280); the face is self-hosted for the rewards
   page. The heading only: round 9 put the button in Assistant as well and it shrank to 195px, and
   live's `a.button` is the body face — Inter 15px/18px 400, letter-spacing 1px, 211×47 at both
   widths (re-measured 14 Sep 2026, round 10 content-account #3). */
.not-found-title {
  margin: 50px 0 30px; font-family: "Assistant", var(--font-primary); font-size: 40px; line-height: 52px;
  font-weight: 400; color: #121212; letter-spacing: 0; text-transform: none;
}
.not-found-continue {
  display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
  min-height: 47px; margin: 20px 0 0; padding: 0 30px; border: 0; border-radius: 0;
  background: #121212; color: var(--brand-white); font-family: var(--font-primary); font-size: 15px; line-height: 18px;
  font-weight: 400; letter-spacing: 1px; text-decoration: none;
}
.not-found-continue:hover, .not-found-continue:focus-visible { background: #000; }
@media (max-width: 767px) {
  .not-found-title { margin: 30px 0 20px; font-size: 30px; line-height: 39px; }
  .not-found-continue { margin-top: 10px; }
}
/* Live's cream band starts where the 404's main ends (483 → 483 at 1280, 469 → 469 at 390); the
   70px under the button is the page's own. The footer's 3rem margin would put 48px of white on
   top of that. body:has(), because footer.js appends the footer to <body>. */
body:has(main.not-found) .site-footer { margin-top: 0; }

/* ---- THE PRODUCT PAGE'S BUY COLUMN (store-product #1 #3 #4 #6) ---------------------------------
   THE CRUMBS: live's .pdp-420__breadcrumb is 13px in the green at 64% — rgba(26,57,31,.64) on the
   links and the current page alike, the separator too; ours were 14.4px in the full green. THE
   VENDOR LINE ("Cam" above the title, .pdp-420__brand): 15px/20px at weight 500 in the FULL green,
   not the warm grey at 15.2px this had. (The finding read it as 17px; the element measured on
   14 Sep 2026 is 15px/20px/500 — this sheet takes the measurement.) */
main.product-page .breadcrumb ol { font-size: 13px; line-height: 28px; color: rgba(26, 57, 31, 0.64); }
main.product-page .breadcrumb a,
main.product-page .breadcrumb [aria-current="page"],
main.product-page .breadcrumb li + li::before { color: rgba(26, 57, 31, 0.64); }
.product-buybox .vendor { color: var(--brand-dark-green); font-size: 15px; line-height: 20px; font-weight: 500; }

/* THE POINTS BANNER IS SMILE'S OWN (store-product #4). Live's block: a section on #f8f8f8 with 10px
   of padding and a 3px radius, inside it a row at a 10px gap — a 20px gift drawing in a 28px box,
   then "Earn 374 Points when completing this purchase." at 17px/28px, weight 400 throughout, in
   Smile's --primary-colour #121212 — 48px tall on one line (76px when the sentence wraps at 390).
   Ours: the emoji, 15px green text with the figure in bold, the cream, an 8px radius, 46px. The
   glyph is inlined by product.html (paintSmilePoints) and takes this colour. #f8f8f8 is written
   literally rather than as the cream token because it is Smile's grey and not the shop's cream —
   the round-3 note above chose the cream to avoid "a second near-identical grey"; side by side
   with live the cream is the thing that reads as different. */
.reward-points {
  align-items: flex-start; gap: 10px; padding: 10px; margin: 0 0 1rem; border-radius: 3px;
  background: #f8f8f8; color: #121212; font-size: 17px; line-height: 28px; font-weight: 400;
}
.reward-points-icon { display: flex; align-items: center; height: 28px; font-size: 0; }
.reward-points-icon svg { display: block; width: 20px; height: 20px; }

/* THE COMPLIANCE NOTICE (store-product #3): one paragraph, 13px on a 1.6 line (20.8px) in the green
   at 64%, with NO rule above it — the column's single hairline comes AFTER it (#compliance-rule in
   product.html, .product-rule's 24px above and .product-effects' 24px below are live's separator
   margins). Ours drew a border-top with 16px of padding under it and set the text in warm grey at
   12.8px. The heading, when the shop has set one (compliance_notice_heading — live prints none;
   clearing it is a settings decision, recorded in the round's report), keeps its weight but takes
   the same size and colour so the block is one voice. */
.product-compliance {
  margin-top: 1.5rem; padding-top: 0; border-top: 0;
  font-size: 13px; line-height: 1.6; color: rgba(26, 57, 31, 0.64);
}
.product-compliance-heading { color: rgba(26, 57, 31, 0.64); }

/* THE FLOATING BAR AT 390 (store-product #1): live drops the price entirely below 768px
   (.pdp-420__floating-price { display: none }) and gives the row to the 132px stepper and an Add
   to cart that flexes to the rest — 210×48 at x=164 on a 390 screen. Ours printed the price (two
   stacked lines with a compare-at) and left Add to cart 144–149px. The price is still in the buy
   box a scroll above; the bar's job is the button. The bar's sides are live's 16px below 768px
   (.pdp-420__floating .pdp-420__container { padding: 12px 16px }) with 16px between the stepper
   and the button — 16 + 132 + 16 + 210 + 16 = 390; ours kept the desktop's 24px sides and a 12px
   gap, which left the button 198px. The round-3 rule's 24px stands from 768px up. */
@media (max-width: 767px) {
  .sticky-buy { padding-inline: 16px; }
  .sticky-buy-inner { gap: 16px; }
  .sticky-buy-price { display: none; }
  .sticky-buy .qty-stepper { min-width: 132px; }
  .sticky-buy .primary { flex: 1 1 auto; min-width: 0; }
}
/* The FAQ picture is a <picture> now (a landscape source for the phone — product_faq_image_mobile);
   the element is inline by default and would leave a line-box gap under the image. */
.product-faqs .home-faq-art picture { display: block; }

/* ---- THE CATALOGUE'S FOOT (store-collection-search #1) ---------------------------------------
   Live, measured on /collections/all and /collections/clothing at 1280 and 390: the last card's
   wrapper ends 16px under its price, the pager (.pagination-420, 40px tall) sits 32px under that
   wrapper, and the cream footer band begins 16px under the pager — or 16px under the wrapper when
   there is no pager. Footer margin-top 0, main padding-bottom 0. Ours had 96px from the pager to
   the band and 120px from the last card on a page with no pager: main's 3rem foot, the footer's
   3rem head and the pager's 24px top, on every filled collection and search page (the empty-state
   case was fixed in rounds 6–8; this is the rest of the family). The footer keeps no margin on any
   page whose main is the catalogue (the round-6 rule below the empty grid covers only the empty
   one); the main keeps 16px; the pager takes 32px above and nothing when it is empty. body:has(),
   because footer.js appends the footer to <body>. */
main.catalogue-main { padding-bottom: 16px; }
body:has(main.catalogue-main) .site-footer { margin-top: 0; }
.pagination { margin-top: 32px; }
.pagination:empty { margin-top: 0; }
/* The card's own foot: our .product-card pads 16px under the price, which is the 16px live's
   wrapper carries, so price → pager is live's 48 and price → band live's 32 without a change here. */

/* ---- THE SEARCH DRAWER'S PRODUCT ROWS (store-collection-search #4) -----------------------------
   Live's .search-drawer__product-list is a flex column with an 8px gap: 96px rows on a 104px pitch
   (tops 268, 372, 476 … at 1280; 260, 364, 468 … at 390). Ours stacked them with none — pitch 96 —
   so seven rows fitted where live shows six. */
.search-results { display: flex; flex-direction: column; gap: 8px; }

/* ---- THE HOME PAGE (store-home #1–#5) ---------------------------------------------------------
   THE FOOT (store-home #2): live's FAQ section ends 60px above the footer band at 1280 and 24px at
   390 (footer margin-top 0; the section's own foot). Ours was 120 / 104: main's 24px foot + the
   FAQ's 48px (32) margin + the footer's 48px margin. The footer keeps no margin on the home page
   (same body:has() as the catalogue), and the FAQ's margin is what makes up live's figure over
   main's 24px: 36px, and none on the phone. THE FEATURED BRAND → TESTIMONIALS: live's featured
   card ends exactly where .testimonials-section begins (its 60px is padding INSIDE the section);
   .home-brands' 40px margin-bottom was 40px more than that. On the phone the testimonials are
   hidden (live's rule, above) and the unboxing section follows the card directly, with its own 15px
   of padding — live's card → "Real Unboxings" is ~15px at 390. */
body:has(main.home-page) .site-footer { margin-top: 0; }
.home-page .home-faq { margin-bottom: 36px; }
.home-brands { margin-bottom: 0; }
@media (max-width: 767px) {
  .home-page .home-faq { margin-bottom: 0; }
}

/* THE FOOTER'S POLICY ROW AND LEGAL LINES (store-home #3): live's .footer__policy-links are 13px on
   a 20px line in rgba(0,0,0,.6) at both widths — 24px apart in the row at 1280 and stacked on a
   24px pitch (a 4px gap under a 20px line) at 390 with 32px under the block; ours were 14.4px in a
   green-grey (16px on the phone, 32px pitch). The FDA and state notices and the copyright are
   live's .footer__disclaimer: 11px/16px in the same 60% black; ours were 12.48px/18.72px and
   12.8px. The row's 40px margin above stays as it was. */
.site-footer nav { gap: 8px 24px; margin-bottom: 32px; }
.site-footer nav a { font-size: 13px; line-height: 20px; color: rgba(0, 0, 0, 0.6); }
.footer-legal, .footer-copyright { font-size: 11px; line-height: 16px; color: rgba(0, 0, 0, 0.6); }
/* 16px, not 12: live's .footer__disclaimer is one paragraph whose three sentences are separated by a
   blank 16px line — tops 796 → 828 → 860 at 1280, a 32px pitch on the 16px line (QA round 9,
   home #3, re-measured); 12px gave a 28px pitch. */
.footer-notices { gap: 16px; }
@media (max-width: 600px) {
  .site-footer nav { gap: 4px; margin-bottom: 32px; }
  .site-footer nav a { font-size: 13px; line-height: 20px; }
}

/* THE UNBOXING CAPTIONS (store-home #4): live's .ugc-info is a 10px-padded column at a 3.75px gap
   — the name in Obviously 600 at 9.375px/12.5px (8.75px on the phone) in the brand green, the
   quote in Inter at 8.125px/10px (7.5px on the phone) in the green at 55%, on one line — a 46px
   caption under the video. Ours set the name 14.4px/700 in ink and the quote 12.8px in a
   green-grey, which wrapped "Okay, you guys won't believe what just came" to two lines and made
   the caption 74px. THE SIZES ARE LIVE'S RENDERED ONES, and they are small for a reason worth
   writing down: live's theme authored them in rem — 0.9375rem, 0.8125rem, 1rem of padding — on a
   root of 10px (Shopify's 62.5%), so what its designer meant as 15px/13px/16px renders at 9.4/8.1/
   10. Parity is the rule this sheet follows; whether the shop would rather have the intended sizes
   is recorded as a decision for the owner in the round's report, and it is three numbers here.
   The quote is clipped to its line (live's -webkit-line-clamp: 2 at these sizes is one line for
   every quote the shop has) so a long one cannot grow the card. */
.home-ugc-who { display: flex; flex-direction: column; gap: 3.75px; padding: 10px; }
.home-ugc-name {
  margin: 0; font-family: var(--font-display); font-weight: 600; font-size: 9.375px; line-height: 12.5px;
  color: var(--brand-dark-green); text-transform: none;
}
.home-ugc-quote {
  margin: 0; font-family: var(--font-primary); font-size: 8.125px; line-height: 10px;
  color: rgba(26, 57, 31, 0.55);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
@media (max-width: 767px) {
  .home-ugc-name { font-size: 8.75px; }
  .home-ugc-quote { font-size: 7.5px; }
}

/* THE UNBOXING HEAD ROW AT 390 (store-home #1, a round-7 item left open as the owner's call). Live
   runs "SEE WHAT MEMBERS ARE GETTING" on TWO lines at 32px (275px wide, 72px tall) with two 20px
   chevrons at its right — its theme's 2.5rem buttons on the same 10px root as the captions above,
   which is why they are 25px on a desktop and 20px on a phone. Our arrows are 40px, the owner's
   floor for a phone control, and beside them the heading had 226px and ran THREE lines. Keeping
   the 40px AND live's two-line heading means the arrows leave the heading's row: they wrap onto
   their own row under the heading's right edge — where the 1280 layout already puts them, at the
   foot of the heading's margin band — 8px under the heading and 12px above the cards (live's h2
   margin at 390). The round-6 note above measured an earlier wrap at 102px from heading to cards;
   this one is 8 + 40 + 12 = 60. */
@media (max-width: 767px) {
  .home-ugc-head { flex-wrap: wrap; gap: 8px 16px; margin-bottom: 12px; }
  .home-ugc-head > div { flex: 1 1 100%; }
  .home-ugc h2 { margin-bottom: 0; }
  .home-ugc-head .home-carousel-nav { margin-left: auto; }
}

/* THE PHONE'S BODY COPY (store-home #5): live steps three sizes down below 768px that this sheet
   did not — the bundle band's paragraphs 14px/22.4px (ours 16px/25.6px, a block 803px tall
   against live's 704), the featured brand's description 15px/22.5px (ours 16px/25.6px), and its
   "Shop Papa & Barkley" button label UP to 16px/28px inside 12px 20px (14px at 1280 on both sites;
   ours stayed 14 on the phone). The 1280 sizes match already and are untouched. */
@media (max-width: 767px) {
  .home-bundle p { font-size: 14px; line-height: 1.6; }
  .home-brand-about { font-size: 15px; line-height: 1.5; }
  .home-brand-cta { font-size: 16px; line-height: 28px; padding: 12px 20px; }
}

/* ================================================================================================
   ROUND 9 (14 Sep 2026) — THE PRODUCT PAGE'S BUY BOX, ITS FAQ PICTURE, THE SHARED PRODUCT CARD AND
   THE FACTS ROWS ON A PHONE, to live's re-measured numbers (product-pages #2, #4, #5; phone #1).
   ================================================================================================ */
/* THE BUY ROW IS THE FLOATING BAR'S ROW — IN SIZE. Live draws the stepper 128×48 and Add to cart
   468×48 at 1280 (222×48 beside the stepper at 390), both full pills, the label 17px at weight 500
   — the same numbers the sticky bar below already had, so the page showed two sizes of the same two
   controls (stepper 130×42 with a 10px radius, button 46px with a 16px/600 label).
   BUT NOT IN FILL. Live's buy-box stepper (.pdp-420__quantity, re-read 14 Sep 2026 at 1280 and 390)
   is a SOLID CREAM pill — background rgb(245,242,235), border 0 — where its floating bar's stepper
   is the transparent one with the 24% hairline; round 9 copied the bar's hairline into the buy box,
   so ours was an outlined pill on white beside live's filled one (round 10, product-pages #2). The
   buttons stay 44px wide (live's are 32px inside 8px of padding; 44 is the owner's floor for a
   control a thumb has to hit) and 44 + 40 + 44 is the 128 at every width — the ≤640 rule used to
   narrow it to 112, which started Add at x=138 against live's 152 on a 390 screen. */
.buy-row .qty-stepper {
  height: 48px; min-width: 128px; justify-content: space-between;
  border: 0; border-radius: 800px; background: var(--brand-cream);
}
.buy-row .qty-stepper button { width: 44px; height: 48px; color: var(--brand-dark-green); }
.buy-row .qty-stepper input { height: 48px; width: 40px; font-weight: 500; color: var(--brand-dark-green); }
.buy-row #add-to-cart { height: 48px; padding: 0 1.6rem; font-size: 17px; font-weight: 500; line-height: 1; }
@media (max-width: 640px) {
  .buy-row .qty-stepper { min-width: 128px; }
  .buy-row .qty-stepper button { width: 44px; }
  .buy-row .qty-stepper input { width: 40px; }
  .buy-row #add-to-cart { padding: 0 1rem; }
}
/* THE FAQ PICTURE ON THE PRODUCT PAGE AT DESKTOP WIDTH: live crops the portrait asset to 632×850
   (object-fit cover in a 16px-radius wrapper) and starts the cream card 32px below the picture's
   top; ours drew the asset at its full 632×930 and the card level with it. */
@media (min-width: 768px) {
  .product-faqs .home-faq-art { border-radius: 16px; overflow: hidden; max-height: 850px; }
  .product-faqs .home-faq-art img, .product-faqs .home-faq-art picture img { height: 850px; object-fit: cover; border-radius: 0; }
  .product-faqs .home-faq-body { margin-top: 32px; }
}
/* THE SHARED PRODUCT CARD'S TYPE IS NOT SET HERE ANY MORE. Round 9 put five lines at this spot
   that made the title 17px/28px/400 with a 3.3em reserve, the price 17px/400 in black and the
   button label weight 400, on the reading that those were live's — they were live's title and
   price WRAPPERS (div.product-carousel__title-wrapper / __price, which inherit the 17px/28px
   body text), the trap the price rule's own comment at `.product-card .price` had already
   described. For a day every grid, search and carousel card lost its green medium name and figure
   and grew 27px (1280) / 41px (390), and the h2 cards on a collection page came out at weight 400
   while the h3 cards on the search page kept 500 (round 10: collections-search #1, product-pages
   #1, home #5, phone-sweep #2 — all P2/P3 regressions of this block). The measured leaf values —
   20px/24px/500 green with a 48px reserve, 16px/19.2px at 390; price 18px/500 green, 16px at 390;
   button 16px/500 — live in the rules that always carried them: the heading exception at the top
   of the sheet, `.product-card-title` (clamp + margins/size), `.product-card .price` and
   `.product-card-add`, each with the round-10 measurement in its comment, and
   tests/storefront.test.mjs now refuses a later rule that re-sizes them. */
/* THE FACTS ROWS (Category / Net Weight / CBD / Certificate) are one rule now — `.product-meta-row`
   and `.product-meta-label` further up carry live's two columns at every width, so the phone block
   that stood here in round 9 (nowrap, a 156px label column) is folded into them. It is there that
   round 10's residue is fixed too: the label was 0.95rem = 15.2px where live's is 15px, and that
   fifth of a pixel is what wrapped "Certificate of Analysis" onto two lines in the 156px column
   (product-pages #5). */

/* THE ACCOUNT PAGE'S "SET AS DEFAULT" TICK-BOX: a 20px box in a 44px row a thumb can hit, like the
   storefront's filter checkboxes — it was the browser's 13px box in a 20px label, the one control on
   the page under 44px (round 9, 14 Sep 2026). */
form.checkout-form label.check-row { display: flex; align-items: center; gap: 12px; min-height: 44px; cursor: pointer; }
form.checkout-form label.check-row input[type="checkbox"] { width: 20px; height: 20px; min-height: 20px; margin: 0; accent-color: var(--brand-dark-green); }

/* ================================================================================================
   QA ROUND 9 (14 Sep 2026) — the checkout's total at the button, the billing block, the empty
   checkout, the grid pager and the home FAQ picture. Every number here was re-measured on
   www.420.com the same day; the checkout figures come off its own checkout, reached through the
   drawer's Checkout button with nothing typed.
   ================================================================================================ */

/* ---- THE BAR ABOVE PLACE ORDER (cart #1, P2) ------------------------------------------------
   Live's phone checkout draws a second summary bar directly above Pay now — a 46px button, 14px
   above the pay button, the newest item's picture at the left, "Total" over "1 item", the full
   price struck through over "USD $24.00" at the right, a chevron — and a tap unfolds the whole
   summary in place above the button. Ours had nothing between the billing box and the button, and
   the only figure on the page was the top bar, 983px above the screen. Not drawn on a desktop, like
   the top bar: there the column beside the form is pinned (see .order-summary above). 48px rather
   than live's 46 — the owner's floor for a phone control, the same as the top bar. */
.order-summary-foot { display: none; }
/* The phone's "Add discount" and the slot under the button are phone-only; on a desktop the summary
   column carries the box and the badges (the 899px block below draws both). */
.checkout-add-discount { display: none; }
@media (max-width: 899px) {
  .order-summary-foot { display: block; margin-top: 0.25rem; }
  .order-summary-foot-toggle {
    display: flex; align-items: center; gap: 0.75rem;
    width: 100%; min-height: 48px; padding: 0.25rem 0; margin: 0;
    background: none; border: 0; border-radius: 0;
    color: var(--brand-ink); font: inherit; text-align: left; cursor: pointer;
  }
  .order-summary-foot-thumb {
    flex: none; width: 44px; height: 44px; border-radius: 8px;
    border: 1px solid #d9d4c7; background: var(--brand-white); overflow: hidden;
  }
  .order-summary-foot-thumb:empty { display: none; }
  .order-summary-foot-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
  .order-summary-foot-label { display: flex; flex-direction: column; gap: 0.1rem; line-height: 1.25; }
  .order-summary-foot-label strong { font-size: 1.05rem; }
  .order-summary-foot-count { font-size: 0.85rem; color: #4a5c4d; }
  .order-summary-foot-total {
    margin-left: auto; display: flex; flex-direction: column; align-items: flex-end;
    gap: 0.1rem; line-height: 1.25; white-space: nowrap; font-size: 1.1rem;
  }
  .order-summary-foot-total .summary-was { font-size: 0.85rem; font-weight: 400; color: #4a5c4d; margin: 0; }
  .order-summary-foot-total .summary-currency { margin-right: 0.3rem; }
  .order-summary-foot .order-summary-toggle-chevron { flex: none; transition: transform 0.15s ease; }
  .order-summary-foot.is-open .order-summary-toggle-chevron { transform: rotate(180deg); }
  /* The summary, unfolded under the bar: the same body the top bar shows, boxed so it reads as one
     panel with the bar above it and separate from the button below. */
  .order-summary-foot.is-open .order-summary-body {
    display: block; border: 1px solid #d9d4c7; border-radius: var(--radius); margin-bottom: 0.25rem;
  }
  /* 14px from the bar to the button, as live has it: the form's 12px grid gap plus 2. */
  .order-summary-foot + .checkout-pay { margin-top: 0.125rem; }
  /* THE FIGURE ON THE BUTTON IS FOR THE SCREEN READER HERE — the bar above carries it for the eye,
     and "Place order • $188.39" under "USD $188.39" would say the same number twice in 60px. On a
     desktop (no bar) it is visible: see .checkout-pay-amount below. */
  .checkout-pay-amount {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }  /* "ADD DISCOUNT" above the bar, live's own control at 390 (an outlined pill with a tag, 126×38,
     measured 14 Sep 2026) at the owner's 40px minimum for a phone control; it sits at the form's
     left like live's rather than stretching across the grid row (QA round 10, cart-drawer #3). */
  .checkout-add-discount {
    display: inline-flex; align-items: center; gap: 0.4rem; justify-self: start;
    min-height: 40px; padding: 0 14px; margin-top: 0.5rem;
    border: 1px solid #d9d4c7; border-radius: 9999px; background: var(--brand-white);
    color: var(--brand-ink); font: inherit; font-size: 0.95rem; font-weight: 600; cursor: pointer;
  }
  .checkout-add-discount:hover { background: #f5f4ef; }
  /* The trust lines under Place order (moved there by checkout.html's wireSummaryToggle): live
     leaves 24px under Pay now before its first badge and rules nothing off. */
  .checkout-trust-foot-slot .checkout-trust { border-top: 0; margin-top: 0.75rem; padding-top: 0; gap: 0.6rem; }
  .checkout-trust-foot-slot .checkout-trust-badge { font-size: 0.9rem; }
}
/* The button's figure on a desktop — the drawer's and the cart page's "Checkout • $174.00", here. */
.checkout-pay { position: relative; }
.checkout-pay-amount { font-weight: 700; }

/* ---- THE DRAWER'S PRICE LINE HAS LIVE'S ROOM (cart #3) ----------------------------------------
   The bin was the line's third grid column, so its 26px (and the 13.6px gap before it) were kept
   free down the WHOLE line — under the price and the stepper too — and the price line had 213px at
   390, where "$225.00 $150.00 (Save $75.00)" at 14px needs about 220 (223 at 1280, where it fits).
   Live's bin only sits in the title's top-right corner and its price line runs the full width under
   it. Same here: the bin is placed in the corner and only the title keeps clear of it; the price and
   the stepper row get the column back (253px at 390). The 36px target from the round above is kept;
   the top offset puts the 16px glyph where the third column used to put it — level with the title,
   inside the line's 1rem of top padding. */
.cart-drawer-line { position: relative; grid-template-columns: auto minmax(0, 1fr); }
.cart-drawer-line-title { padding-right: 32px; }
.cart-drawer-remove { position: absolute; top: calc(1rem - 10px); right: -10px; margin: 0; }

/* ---- THE EMPTY CHECKOUT (cart #4) ------------------------------------------------------------
   "There is nothing in your cart to check out." was a bare grid child: flush against the left edge
   of the window at both widths, with the grid's grey right half still painted beside it — 640×120
   of empty beige at 1280. With no form and no summary there are no halves: one white column, and
   the notice gets the form column's own inset (1rem on a phone). The recovery notice under it is
   the same kind of block and gets the same. */
.checkout-grid:has(#checkout-empty:not([hidden])), .checkout-grid:has(#checkout-restoring:not([hidden])) {
  grid-template-columns: minmax(0, 1fr); background: var(--brand-white);
}
#checkout-empty, #checkout-restoring {
  grid-column: 1 / -1; box-sizing: border-box; width: 100%; max-width: 1100px;
  margin: 0 auto; padding: 1.5rem 1rem 3rem;
}
#checkout-empty .cart-empty, #checkout-restoring .cart-empty { margin-top: 0; }

/* ---- THE BILLING BOX (cart #5, phone-sweep #2) ------------------------------------------------
   The storefront's last native-size checkbox: a 13px box in a 19px label row, under fields that
   are all 44px. Live's is a 20px box in a 44px option row; the admin's round-8 .check-row is a 20px
   box on a 36px row. 40px here — the owner's floor for a phone control, and the height of the
   fields around it — with the box drawn at 20px in the shop's green. The rule below outranks the
   base `form.checkout-form input[type="checkbox"]` reset by specificity, which is why the inline
   width:auto the input used to carry is gone. */
.checkbox-row { min-height: 40px; padding: 0.25rem 0; gap: 12px; font-size: 0.95rem; cursor: pointer; }
form.checkout-form .checkbox-row input[type="checkbox"] {
  width: 20px; height: 20px; min-height: 0; margin: 0; padding: 0; flex: 0 0 auto;
  accent-color: var(--brand-dark-green); cursor: pointer;
}
/* THE RATE RADIOS, DRAWN LIKE THE BILLING BOX (cart-checkout #3, round 11). Ground / USPS Priority
   were the browser's default 13px control in default blue, 280px above a 20px checkbox in the shop's
   green — two native controls on one screen in two palettes; live's checkout draws its rate radios
   and its checkboxes alike (dark, 20px). Same size, same green, in the 44px row they already sit in. */
form.checkout-form .shipping-option input[type="radio"] {
  width: 20px; height: 20px; min-height: 0; margin: 0; padding: 0; flex: 0 0 auto;
  accent-color: var(--brand-dark-green); cursor: pointer;
}

/* ---- THE GRID PAGER, LIVE'S OWN LOOK (collections-search #3) ----------------------------------
   Read off www.420.com/collections/all?page=2 at 1280 and 390, 14 Sep 2026: .pagination-420__btn is
   a 40×40 box (46×40 for the arrows: a 20px chevron with 12px of padding each side), padding 8px
   12px, a 1px border in the brand green at 16%, 8px corners, no fill, the number in the brand green
   at 15px/500 — the current page filled green with a white number, the ellipsis in the SAME
   bordered box, and 8px between boxes (the list's flex gap). Ours drew 40px boxes with a warm-grey
   border, 10px corners, 14.4px/400 numbers in ink, a bare "…" and text arrows. The arrows are the
   chevrons js/filters.js now prints (live's own paths). The hover keeps a light tint of the green
   so a button still answers the pointer. */
.pager { gap: 8px; }
.pager-btn {
  min-width: 40px; height: 40px; padding: 8px 12px; box-sizing: border-box;
  border: 1px solid rgba(26, 57, 31, 0.16); border-radius: 8px;
  background: transparent; color: var(--brand-dark-green);
  font-size: 15px; font-weight: 500; line-height: 1;
}
button.pager-btn:hover { background: rgba(26, 57, 31, 0.06); }
.pager-current { background: var(--brand-dark-green); border-color: var(--brand-dark-green); color: var(--brand-white); font-weight: 500; }
/* The ellipsis is live's .pagination-420__btn--disabled: the same bordered box at HALF opacity, so it
   reads as a dimmed, inert marker and not as a page button that does nothing (round 10,
   collections-search #4; opacity 0.5 read off page 2 and page 7 at both widths). */
.pager-gap { min-width: 40px; padding: 8px 12px; border-color: rgba(26, 57, 31, 0.16); background: transparent; opacity: 0.5; }
.pager-arrow svg { display: block; width: 20px; height: 20px; }
/* A 360px phone has a 336px row: the middle-page set ("‹ 1 … 7 … 40 ›", js/filters.js) is two 46px
   arrows, four 40px boxes and a 45px two-digit box — 337 with 8px between them — and wrapped onto a
   second line at 360 while fitting 390 (round 10, seen in the 360 sweep). The gap gives up 2px
   under 375px (325 in the row) rather than the boxes giving up any of their 40px, which is the
   owner's floor for a phone control. */
@media (max-width: 374px) {
  .pager { gap: 6px; }
}

/* ---- LOAD AS YOU SCROLL (the marketing team's Slack list, 16 Sep 2026) ------------------------
   The block under the grid on collection.html and index.html — see Filters.loadMore in
   js/filters.js. The sentinel is the pixel the observer watches: one pixel tall so every browser
   reports it (a zero-height box is edge-adjacent at best), and the pixel is given back with a
   negative margin so the foot under the grid measures exactly what it did. The button is the
   pager's own family — the same 1px brand-green border and 15px/500 green face as .pager-btn,
   round-ended like every other pill on 420.com, 44px tall for a thumb — and takes the 32px the
   pager keeps above itself (the catalogue's foot rule above), so grid → button → pager keeps live's
   32px rhythm. The status line only takes room when it has something to say; the count under the
   pager takes the pager's bottom margin over when it is there, so the page's foot does not grow by
   both. Nothing here moves: no transition, no animation, nothing for prefers-reduced-motion to
   switch off. */
.load-more { text-align: center; }
.load-more-sentinel { height: 1px; margin-bottom: -1px; }
.load-more-button {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; min-width: 200px; margin-top: 32px; padding: 8px 24px; box-sizing: border-box;
  border: 1px solid rgba(26, 57, 31, 0.16); border-radius: var(--radius-pill);
  background: transparent; color: var(--brand-dark-green);
  font: inherit; font-size: 15px; font-weight: 500; line-height: 1.2; cursor: pointer;
}
.load-more-button:hover { background: rgba(26, 57, 31, 0.06); }
/* Working, not off: the button keeps focus while a page loads (see Filters.loadMore) and says so
   with aria-disabled — dimmed, and the pointer knows. */
.load-more-button[aria-disabled="true"] { opacity: 0.6; cursor: progress; }
.load-more-status { margin: 16px 0 0; font-size: 15px; line-height: 1.4; color: var(--brand-ink); }
.load-more-status:empty { margin: 0; }
.results-count { margin: 0 0 2.5rem; text-align: center; font-size: 15px; line-height: 1.4; color: rgba(32, 30, 25, 0.7); font-variant-numeric: tabular-nums; }
.results-count:empty { margin: 0; }
.pagination:has(+ .results-count:not(:empty)) { margin-bottom: 12px; }

/* ---- THE HOME FAQ PICTURE STAYS BESIDE THE LIST (home #1) -----------------------------------
   Live's .faq-section__image-wrapper is position: sticky; top: 0, so the couple's photograph stays
   pinned at the top of the window while the fifteen questions scroll past it and the left column is
   never blank. Ours let the picture scroll away, leaving 412px of white under it at the section's
   foot. The wrapper, not the image, is what sticks — the grid already aligns the columns to the
   start (align-items: start), which sticky needs.
   FROM 1024px ONLY — THE GRID'S OWN BREAKPOINT. Below 1024 `.home-page .home-faq` is one column
   (the 500px photograph over the card, the rule near line 4114), and round 9 wrote this at 768, so
   from 768 to 1023 the photograph pinned to the top of the window and rode OVER the questions —
   at 853, 300px into the section, it covered "FAQs" and the first three questions (round 10, home
   #1, P2). Live's .faq-section__image-wrapper is position: relative at 853 and sticky at 1024
   (measured 14 Sep 2026): it sticks only where it sits BESIDE the list. */
@media (min-width: 1024px) {
  .home-page .home-faq-art { position: sticky; top: 0; }
}

/* ---- THE SEARCH PAGE'S OWN FOOT (collections-search #2) --------------------------------------
   Live's search template carries more foot than its collection template: the pager sits 48px above
   the cream band at 1280 and 24px at 390 (the collection's 16 at both), and with no pager the last
   card's price sits 64 / 40 above it (the collection's 32). Round 8 gave the search page the
   collection's numbers. main.search-main is set by index.html whenever the address is a search
   (/search, or any ?q=); the card's own 16px foot makes 48 + 16 = 64 and 24 + 16 = 40 without a
   change to it. The two empty states keep their own foot (the rules above, which win on
   specificity). */
main.catalogue-main.search-main { padding-bottom: 48px; }
@media (max-width: 767px) {
  main.catalogue-main.search-main { padding-bottom: 24px; }
}
main.catalogue-main.search-main:has(.search-empty), main.catalogue-main.search-main:has(.search-no-results) { padding-bottom: 0; }

/* ---- THE CART PAGE KEEPS THE LAUNCHERS OFF ITS MONEY (cart #6) --------------------------------
   At 1280×900 with two lines in the basket, the page lands with the Subtotal and Discounts figures
   under Smile's Rewards pill and the "35% Off" teaser over the Subtotal label — the first thing the
   shopper reads on the cart page is "$197.9" cut by a button. Live's cart is the drawer, and live
   hides its Smile container while the drawer is open; the four sign-in pages here already carry
   none of the three. The cart page is the same basket at the page's width, so it follows the
   drawer's rule: the same three, the same way (see the round-3 note on why display and the
   descendants). One rule to revert if the owner wants the pill on this page. */
body:has(main.cart-page) [class*="kl-teaser-"],
body:has(main.cart-page) #tidio-chat,
body:has(main.cart-page) #tidio-chat-iframe {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
body:has(main.cart-page) [class*="kl-teaser-"] *,
body:has(main.cart-page) #tidio-chat * {
  visibility: hidden !important;
  pointer-events: none !important;
}

/* ---- NO SIGN-UP FORM OVER THE CHECKOUT OR THE RECEIPT (15 Sep 2026) ---------------------------
   A real-browser check of the preview found the account's "UNLOCK 35% OFF" pop-up open over the
   order confirmation — over the receipt and over the AgeChecker modal that page runs — where live's
   Shopify thank-you page shows no Klaviyo form at all. The two pages carry <main
   data-klaviyo-forms="off"> and js/klaviyo.js (keepFormsOffMarkedPages) hides what Klaviyo's forms
   module draws the moment it is appended; this is the style half of that guard and the no-script
   belt under it. The attribute rule is what HOLDS: the guard also writes an inline display:none
   !important, but Klaviyo's React re-renders the root's style attribute and an inline declaration
   is one re-render from gone, while an attribute React does not manage stays put. Every form is
   drawn inside one root the module appends to <body> (#dynamic-react-root, read off its Render
   chunk that day); the dialog and teaser markers are the round-5 rule's, kept so a renamed root is
   still caught. The identify() the checkout's email field fires is untouched: only the forms are
   hidden, never the script. */
[data-klaviyo-form-suppressed],
[data-klaviyo-form-suppressed] * {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
body:has(main[data-klaviyo-forms="off"]) #dynamic-react-root,
body:has(main[data-klaviyo-forms="off"]) [data-kl-scroll-locking-modal],
body:has(main[data-klaviyo-forms="off"]) [role="dialog"][aria-modal]:has(form.klaviyo-form),
body:has(main[data-klaviyo-forms="off"]) [class*="kl-teaser-"] { display: none !important; }
/* The lock itself, as on the sign-in pages: a hidden form that still believes it is open must not
   hold the receipt still. js/klaviyo.js takes the class off <body>; this covers the frame before. */
body.klaviyo-prevent-body-scrolling:has(main[data-klaviyo-forms="off"]) { overflow: auto !important; position: static !important; }
