/* ==========================================================================
   Smoker's Warehouse — site.css
   Gold Standard Solutions · house component library, split-personality skin
   Token block is the ONLY place a color or font is written.
   ========================================================================== */

/* 0. TOKENS ---------------------------------------------------------------- */

/* Every colour below descends from the four values in the client's own brand
   guideline (Brand Kit/SW Brand Guideline.pdf):

     #080707  black      #452219  brown      #701816  red      #B6AA9B  stone

   An earlier version of this file was built on colours sampled out of the
   storefront photographs, which was the right method while no guideline existed
   and the wrong answer the moment one turned up. The four are used at their
   exact specified values; everything else is a tint, shade or desaturation of
   them, tuned only as far as WCAG required and verified by contrast-audit.py
   rather than by eye. No hue enters this file that is not one of the four. */

:root{
  /* Brand — the shared warehouse chassis */
  --brand:        #701816;   /* BRAND RED. Action on light grounds. */
  --brand-dark:   #571110;   /* hover */
  --brand-light:  #D97A66;   /* the red lightened far enough to clear AA as text
                                on the brown humidor ground -- the only place a
                                red link appears on a dark surface. */
  --accent:       #B6AA9B;   /* BRAND STONE. Emphasis, and action on dark. */
  --accent-ink:   #080707;   /* the only text colour legal on --accent */
  --accent-soft:  #E3DCD2;

  /* Surfaces — light / counter-default. Tints of the stone, so the paper the
     site is printed on is the brand's own neutral rather than a generic cream. */
  --bg:           #F5F2EC;
  --surface:      #FCFAF7;
  --surface-alt:  #EAE5DD;
  --ink:          #080707;   /* brand black, for headings */
  --ink-mid:      #452219;   /* brand brown, for body copy */
  --muted:        #6E5A4C;   /* brown desaturated until it clears 4.5:1 on
                                --surface-alt, the darkest light ground */
  --border:       rgba(8,7,7,.16);
  --border-soft:  rgba(8,7,7,.08);

  /* Product art ground. Deliberately NOT redefined by any theme or world scope.
     Packaging is designed to be read against a light shelf, and half of what we
     carry is printed on black -- a Zilla Beast bar on a dark-mode card is a
     silhouette. Pinning one warm light ground behind every packshot keeps the
     product legible in both themes and keeps fourteen photographs from fourteen
     sources reading as one set. It is --surface-alt's light value; it just does
     not follow it into dark mode. */
  --product-ground: #EAE5DD;

  /* World: HUMIDOR — cigars, pipe tobacco, the about page.
     The ground IS the brand brown. The room was always going to be this colour;
     now it is the client's value rather than my approximation of it. */
  --h-bg:         #452219;
  --h-surface:    #573026;
  --h-surface-alt:#2E1710;
  --h-ink:        #EDE7DF;
  --h-ink-mid:    #D2C8BC;
  --h-muted:      #B6AA9B;   /* the stone, at full strength */
  --h-accent:     #B6AA9B;
  --h-accent-ink: #080707;   /* stone is light: its ink is the brand black */
  --h-border:     rgba(182,170,155,.30);
  /* The humidor's pools, and they are NOT the night world's values reused.
     --n-glow-red is the brand red at .55 over #0F0B0A, a ground with forty
     points of lightness to climb into. Over #452219 that same pool has almost
     nowhere to go and paints as a stain on the wall rather than a light on it.
     So the lit pool here is the stone, and the second is the humidor's own
     dark surface used as a floor. No fifth hue: both are already in the four. */
  --h-glow-stone: rgba(182,170,155,.13);
  --h-glow-deep:  rgba(46,23,16,.60);   /* --h-surface-alt, as a shadow */

  /* World: NIGHT — the after-dark scope, added v29, corrected v30.
     EVERY VALUE HERE IS FROM color-system.md. The first version of this world
     shipped a cyan (#6FE3DA) and a violet (#A87CFF) on the argument that the
     client's head-shop cases are lit that way. That was the product lighting,
     not the brand, and the palette is four colours: black, brown, red, stone.
     color-system.md already records one earlier pass being pulled for exactly
     this -- it produced #A83318 and #B8863B, both "defensible readings of the
     photographs", both rejected in favour of the guideline values. Same mistake,
     second time. A photograph is a reference, not a licence to add hues.

     The dark ground did not even need inventing: color-system.md documents one
     at #0F0B0A / #1C1512 / #120D0B. The accent is the stone, which the doc
     already names as "Action on dark grounds", so night and humidor now agree
     rather than each having their own idea. */
  --n-bg:         #0F0B0A;   /* documented dark ground */
  --n-surface:    #1C1512;
  --n-surface-alt:#120D0B;
  --n-ink:        #EDE7DF;   /* the humidor ink, unchanged */
  --n-ink-mid:    #D2C8BC;
  --n-muted:      #B6AA9B;   /* the stone at full strength */
  --n-accent:     #B6AA9B;   /* the stone. Action on dark, per the guideline. */
  --n-accent-ink: #080707;   /* stone is light: its ink is the brand black */
  --n-ember:      #D97A66;   /* --brand-light. The doc reserves this for red as
                                text on a dark ground, which is precisely the
                                statement headline below and nowhere else. */
  --n-border:     rgba(237,231,223,.16);
  /* The glows are the brand red and the stone at low alpha -- depth without a
     fifth hue. */
  --n-glow-red:   rgba(112,24,22,.55);
  --n-glow-stone: rgba(182,170,155,.20);

  /* World: COUNTER — vapes, pouches, cigarettes, accessories.
   Accent is the brand red, so the whole site has ONE action colour. */
  --c-bg:         #F5F2EC;
  --c-surface:    #FCFAF7;
  --c-surface-alt:#EAE5DD;
  --c-ink:        #080707;
  --c-ink-mid:    #452219;
  --c-muted:      #6E5A4C;
  --c-accent:     #701816;
  --c-accent-ink: #FCFAF7;   /* red is dark: its ink is near-white */
  --c-border:     rgba(8,7,7,.16);

  /* Type — the guideline names two families and nothing else.
     Rockwell carries display AND serif duty: it is a slab serif, so the two
     roles this file used to split between a sans and a serif collapse into one
     family, which is a simplification the brand handed us rather than a
     compromise. Both are self-hosted from /assets/fonts (see section 0b), so
     the build has no third-party font origin at all -- that also closes the
     Phase 10 note about four families over fonts.gstatic.com being the biggest
     LCP risk in the build.
     --font-mono is a system stack. The guideline does not specify a monospace,
     and the places this file uses one are letterspaced uppercase micro-labels
     where the system's own mono is indistinguishable at 0.68rem and costs zero
     bytes. Naming a fifth downloaded family for eyebrow text would not be. */
  --font-display: 'Rockwell', 'Rockwell Std', Georgia, 'Times New Roman', serif;
  --font-serif:   'Rockwell', 'Rockwell Std', Georgia, 'Times New Roman', serif;
  --font-body:    'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  --font-mono:    ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;

  /* Geometry */
  --r-card:  14px;
  --r-pill:  50px;
  --r-input: 10px;
  --r-bar:   8px;
  --shadow:  0 4px 32px rgba(36,26,17,.08);
  --shadow-lift: 0 12px 40px rgba(36,26,17,.14);
  --nav-h:   62px;
  --wrap:    1160px;

  /* Motion */
  --ease: cubic-bezier(.2,.7,.3,1);

  color-scheme: light;
}

@media (min-width: 960px){ :root{ --nav-h: 70px; } }

/* Dark — system preference, unless the visitor explicitly chose light */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --bg:          #0F0B0A;
    --surface:     #1C1512;
    --surface-alt: #120D0B;
    --ink:         #EDE7DF;
    --ink-mid:     #D2C8BC;
    --muted:       #A5988B;
    --border:      rgba(237,231,223,.18);
    --border-soft: rgba(237,231,223,.08);
    --shadow:      0 4px 32px rgba(0,0,0,.5);
    --shadow-lift: 0 12px 40px rgba(0,0,0,.62);

    --c-bg:        #0F0B0A;
    --c-surface:   #1C1512;
    --c-surface-alt:#120D0B;
    --c-ink:       #EDE7DF;
    --c-ink-mid:   #D2C8BC;
    --c-muted:     #A5988B;
    --c-border:    rgba(237,231,223,.18);
    --c-accent:    #B6AA9B;
    --c-accent-ink:#080707;

    --h-bg:        #2E1710;
    --h-surface:   #452219;
    --h-surface-alt:#1E0F0A;

    color-scheme: dark;
  }
}

/* Dark — explicit toggle wins in both directions */
:root[data-theme="dark"]{
  --bg:          #0F0B0A;
  --surface:     #1C1512;
  --surface-alt: #120D0B;
  --ink:         #EDE7DF;
  --ink-mid:     #D2C8BC;
  --muted:       #A5988B;
  --border:      rgba(237,231,223,.18);
  --border-soft: rgba(237,231,223,.08);
  --shadow:      0 4px 32px rgba(0,0,0,.5);
  --shadow-lift: 0 12px 40px rgba(0,0,0,.62);

  --c-bg:        #0F0B0A;
  --c-surface:   #1C1512;
  --c-surface-alt:#120D0B;
  --c-ink:       #EDE7DF;
  --c-ink-mid:   #D2C8BC;
  --c-muted:     #A5988B;
  --c-border:    rgba(237,231,223,.18);
  --c-accent:    #B6AA9B;
  --c-accent-ink:#080707;

  --h-bg:        #2E1710;
  --h-surface:   #452219;
  --h-surface-alt:#1E0F0A;

  color-scheme: dark;
}

/* 0b. WEBFONTS -------------------------------------------------------------
   Self-hosted, from the client's own licensed Rockwell files and the Google
   Fonts latin subset of Roboto. Filenames are content-hashed by build.py -- the
   __FONT_*__ tokens below are substituted at build time -- so /assets/fonts can
   be cached immutably for a year alongside the CSS and JS, and a replaced font
   file changes its own URL and the URL of the stylesheet that references it.

   font-display:swap everywhere. This is a shop's opening hours and phone
   number; text that is invisible for three seconds while a slab serif arrives
   is worse than text in Georgia for three seconds.
   -------------------------------------------------------------------------- */

@font-face{
  font-family:'Rockwell';
  src:url('/assets/fonts/rockwell-regular.e9b48b6c9d.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Rockwell';
  src:url('/assets/fonts/rockwell-bold.dfb0d6338d.woff2') format('woff2');
  font-weight:700; font-style:normal; font-display:swap;
}
@font-face{
  /* A real Extra Bold rather than a synthesised one: several headings in this
     file ask for 800, and a browser-smeared 700 on a slab serif is mud. */
  font-family:'Rockwell';
  src:url('/assets/fonts/rockwell-extrabold.4250dd0fc0.woff2') format('woff2');
  font-weight:800; font-style:normal; font-display:swap;
}
@font-face{
  /* One variable file covers 400 through 700, which is why there is no separate
     medium or bold download. */
  font-family:'Roboto';
  src:url('/assets/fonts/roboto-var.0a44e0bb6b.woff2') format('woff2');
  font-weight:100 900; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Roboto';
  src:url('/assets/fonts/roboto-var-italic.a91c64a7a5.woff2') format('woff2');
  font-weight:400; font-style:italic; font-display:swap;
}

/* 1. RESET & BASE ---------------------------------------------------------- */

*,*::before,*::after{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; scroll-behavior:smooth; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto; } }

body{
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font-family:var(--font-body);
  font-size:17px;
  line-height:1.62;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

img,svg,video{ max-width:100%; height:auto; display:block; }
figure{ margin:0; }
a{ color:inherit; }
button{ font:inherit; }

h1,h2,h3,h4{
  font-family:var(--font-display);
  font-weight:700;
  line-height:1.1;
  letter-spacing:-.015em;
  margin:0 0 .5em;
  text-wrap:balance;
}
h1{ font-size:clamp(2.1rem, 5.4vw, 3.9rem); }
h2{ font-size:clamp(1.6rem, 3.4vw, 2.5rem); }
h3{ font-size:clamp(1.15rem, 2vw, 1.45rem); }
p{ margin:0 0 1.1em; max-width:72ch; }

.wrap{ width:100%; max-width:var(--wrap); margin-inline:auto; padding-inline:20px; }
.band{ padding:clamp(48px,6vw,88px) 0; }
.band-alt{ background:var(--surface-alt); }
/* The alternation never rendered, from the first build until now. `.band-alt` and
   `.world-counter` are both a single class, so specificity ties and the one
   further down the sheet wins -- and `.world-counter` sets `background:var(--bg)`
   two hundred lines below this. Every alternating band on the site was therefore
   painting the plain ground, which is a large part of why the page reads as one
   section stacked on the next. Several earlier versions "fixed band alternation"
   by flipping these classes around after inserting sections; that work was real
   but invisible, because none of it was ever painting. Scoping the pair beats
   reordering the sheet: it says what it means and survives a later refactor. */
.world-counter.band-alt{ background:var(--c-surface-alt); }
.world-humidor.band-alt{ background:var(--h-surface-alt); }

.eyebrow{
  font-family:var(--font-mono);
  font-size:.72rem;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--muted);
  margin:0 0 .9em;
}

.lede{ font-size:clamp(1.05rem,1.8vw,1.28rem); color:var(--ink-mid); max-width:64ch; }
.speakable-summary{ font-size:1.06rem; color:var(--ink-mid); max-width:66ch; }

.skip-link{
  position:absolute; left:-9999px; top:0; z-index:9999;
  background:var(--brand); color:#fff; padding:12px 20px;
  border-radius:0 0 var(--r-input) 0; font-weight:600;
}
.skip-link:focus{ left:0; }

:where(a,button,input,select,textarea,summary,[tabindex]):focus-visible{
  outline:3px solid var(--accent);
  outline-offset:3px;
  border-radius:4px;
}

.visually-hidden{
  position:absolute!important; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* 1b. PAGE-LEVEL WORLD ---------------------------------------------------- */
/* A humidor page carries its chassis (header, breadcrumbs, page ground) in the
   humidor palette so the sticky bar does not sit on the page like a light shelf.
   No heading-font override here: nested .world-* sections still govern their own. */

body.page-humidor{
  --bg:var(--h-bg); --surface:var(--h-surface); --surface-alt:var(--h-surface-alt);
  --ink:var(--h-ink); --ink-mid:var(--h-ink-mid); --muted:var(--h-muted);
  --border:var(--h-border); --border-soft:rgba(184,134,59,.14);
  --accent:var(--h-accent); --accent-ink:var(--h-accent-ink);
  background:var(--bg); color:var(--ink);
}
body.page-humidor .nav-toggle span{ background:var(--h-ink); }

/* 2. WORLD SCOPES ---------------------------------------------------------- */
/* A world remaps the surface tokens for the section it wraps. Everything
   inside keeps using --bg / --surface / --ink, so components are world-agnostic. */

.world-humidor{
  --bg:var(--h-bg); --surface:var(--h-surface); --surface-alt:var(--h-surface-alt);
  --ink:var(--h-ink); --ink-mid:var(--h-ink-mid); --muted:var(--h-muted);
  --border:var(--h-border); --border-soft:rgba(184,134,59,.14);
  --accent:var(--h-accent); --accent-ink:var(--h-accent-ink);
  background:var(--bg); color:var(--ink);
}
.world-humidor h1,.world-humidor h2,.world-humidor h3{
  font-family:var(--font-serif);
  font-weight:600;
  letter-spacing:-.01em;
}
.world-humidor .eyebrow{ color:var(--h-accent); }

.world-counter{
  --bg:var(--c-bg); --surface:var(--c-surface); --surface-alt:var(--c-surface-alt);
  --ink:var(--c-ink); --ink-mid:var(--c-ink-mid); --muted:var(--c-muted);
  --border:var(--c-border); --accent:var(--c-accent); --accent-ink:var(--c-accent-ink);
  background:var(--bg); color:var(--ink);
}
.world-counter h1,.world-counter h2{
  font-family:var(--font-display);
  font-weight:800;
  letter-spacing:-.025em;
  text-transform:uppercase;
}
.world-counter .eyebrow{ color:var(--c-accent); }

/* 3. BUTTONS --------------------------------------------------------------- */

.btn{
  display:inline-flex; align-items:center; gap:.5em;
  padding:13px 26px; min-height:44px;
  border-radius:var(--r-pill);
  font-family:var(--font-display);
  font-weight:700; font-size:.95rem; letter-spacing:.01em;
  text-decoration:none; border:2px solid transparent; cursor:pointer;
  transition:transform .18s var(--ease), background .18s var(--ease), color .18s var(--ease), border-color .18s var(--ease);
}
.btn:hover{ transform:translateY(-2px); }
.btn-solid{ background:var(--brand); color:#fff; }
.btn-solid:hover{ background:var(--brand-dark); }
.btn-accent{ background:var(--accent); color:var(--accent-ink); }
.btn-ghost{ border-color:var(--border); color:var(--ink); background:transparent; }
.btn-ghost:hover{ border-color:var(--ink); }
.btn-sm{ padding:9px 18px; font-size:.86rem; min-height:38px; }

/* 4. HEADER + NAV ---------------------------------------------------------- */

.site-header{
  position:sticky; top:0; z-index:600;
  background:color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter:saturate(1.4) blur(12px);
  border-bottom:1px solid var(--border-soft);
}
.nav{
  height:var(--nav-h);
  max-width:var(--wrap); margin-inline:auto; padding-inline:20px;
  display:flex; align-items:center; gap:18px;
}
.brand{ display:flex; align-items:center; gap:10px; text-decoration:none; margin-right:auto; }
.brand-mark{ width:30px; height:30px; flex:none; }
.brand-word{
  font-family:var(--font-display); font-weight:800;
  text-transform:uppercase; letter-spacing:.02em; font-size:1.12rem; line-height:1;
  color:var(--ink);
}
.brand-word b{ color:var(--brand); font-weight:800; }

.nav-toggle{
  display:grid; place-content:center; gap:5px;
  width:44px; height:44px; background:none; border:0; cursor:pointer; padding:0;
}
.nav-toggle span{ display:block; width:22px; height:2px; background:var(--ink); transition:transform .2s var(--ease), opacity .2s; }
.nav-toggle[aria-expanded="true"] span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2){ opacity:0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

.nav-links{ display:none; }
/* :not(.btn) matters. Without it this rule beats .btn-solid on specificity and
   paints the header CTA's label in ink on the brand fill, which measures 2.6:1.
   It had been failing AA since the header was built. */
.nav-links a:not(.btn){ text-decoration:none; font-weight:600; font-size:.95rem; color:var(--ink); }
.nav-drop{ position:relative; }
.nav-drop-toggle{
  background:none; border:0; cursor:pointer; padding:8px 0;
  font-weight:600; font-size:.95rem; color:var(--ink);
  display:inline-flex; align-items:center; gap:6px;
}
.caret{ font-size:.7em; transition:transform .2s var(--ease); }
.nav-drop-toggle[aria-expanded="true"] .caret{ transform:rotate(180deg); }
.nav-drop-menu{ display:none; }
.nav-drop-label{
  font-family:var(--font-mono); font-size:.68rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted); margin:0 0 .6em;
}
.nav-cta{ display:flex; align-items:center; gap:8px; }
.nav-tel{
  display:none; font-family:var(--font-mono); font-size:.86rem;
  text-decoration:none; color:var(--ink); font-weight:600;
}
.theme-toggle{
  width:38px; height:38px; border-radius:var(--r-pill);
  border:1px solid var(--border); background:transparent; color:var(--ink);
  cursor:pointer; display:grid; place-content:center; font-size:.95rem;
}

@media (min-width: 1000px){
  .nav-toggle{ display:none; }
  .nav-tel{ display:inline-flex; }
  .nav-links{
    display:flex; align-items:center; gap:26px; margin-left:auto;
  }
  .nav-drop-menu{
    display:block; position:absolute; top:calc(100% + 14px); left:-18px;
    min-width:260px; padding:18px; opacity:0; visibility:hidden;
    transform:translateY(-6px);
    background:var(--surface); border:1px solid var(--border);
    border-radius:var(--r-card); box-shadow:var(--shadow);
    transition:opacity .18s var(--ease), transform .18s var(--ease), visibility .18s;
  }
  .nav-drop:hover .nav-drop-menu,
  .nav-drop-toggle[aria-expanded="true"] + .nav-drop-menu{
    opacity:1; visibility:visible; transform:translateY(0);
  }
  .nav-drop-menu a{ display:block; padding:7px 0; font-weight:500; }
  .nav-drop-menu a:hover{ color:var(--brand); }
}

@media (max-width: 999px){
  .nav-links{
    position:fixed; inset:var(--nav-h) 0 0 0; z-index:590;
    display:none; flex-direction:column; align-items:stretch; gap:0;
    background:var(--bg); padding:24px 20px 60px; overflow-y:auto;
  }
  .nav-links.open{ display:flex; }
  .nav-links > a{ padding:15px 0; border-bottom:1px solid var(--border-soft); font-size:1.1rem; }
  .nav-drop-toggle{ width:100%; justify-content:space-between; padding:15px 0; border-bottom:1px solid var(--border-soft); font-size:1.1rem; }
  .nav-drop-menu{ padding:6px 0 16px 14px; }
  .nav-drop-toggle[aria-expanded="true"] + .nav-drop-menu{ display:block; }
  .nav-links .btn{ margin-top:22px; justify-content:center; }
}

/* 5. AGE GATE -------------------------------------------------------------- */

.agegate{
  position:fixed; inset:0; z-index:9000;
  display:grid; place-items:center; padding:20px;
  background:rgba(14,9,5,.94);
  backdrop-filter:blur(6px);
}
.agegate[hidden]{ display:none; }
.agegate .box{
  --ink:var(--h-ink); --ink-mid:var(--h-ink-mid); --muted:var(--h-muted);
  --border:var(--h-border); --accent:var(--h-accent); --accent-ink:var(--h-accent-ink);
  width:100%; max-width:470px; padding:38px 30px;
  background:var(--h-surface); color:var(--h-ink);
  border:1px solid var(--h-border); border-radius:var(--r-card);
  text-align:center;
}
.agegate h2{ font-family:var(--font-serif); font-size:1.7rem; margin-bottom:.4em; }
.agegate p{ color:var(--h-ink-mid); font-size:.95rem; margin-inline:auto; }
.agegate .row{ display:flex; gap:12px; justify-content:center; margin-top:22px; flex-wrap:wrap; }
.agegate .deny{ display:none; font-size:.9rem; color:var(--h-muted); margin-top:18px; }
.agegate.denied .row, .agegate.denied .ag-intro{ display:none; }
.agegate.denied .deny{ display:block; }
.agegate .fine{ font-size:.78rem; color:var(--h-muted); margin-top:20px; }

/* 6. CARDS ----------------------------------------------------------------- */

.grid{ display:grid; gap:20px; }
.grid-2{ grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); }
.grid-3{ grid-template-columns:repeat(auto-fit,minmax(268px,1fr)); }
.grid-4{ grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); }

.card{
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:var(--r-card);
  padding:26px;
  transition:transform .2s var(--ease), box-shadow .2s var(--ease);
}
.card:hover{ transform:translateY(-3px); box-shadow:var(--shadow-lift); }
.card h3,.card .h-sm{ margin-bottom:.35em; }
.h-sm{ font-size:clamp(1.15rem,2vw,1.45rem); }
.card p{ color:var(--ink-mid); font-size:.95rem; margin-bottom:.8em; }
.card-link{ display:block; text-decoration:none; color:inherit; }

.cat-card{ position:relative; overflow:hidden; padding:0; }
.cat-card img{ width:100%; aspect-ratio:4/3; object-fit:cover; border-radius:var(--r-card) var(--r-card) 0 0; }
.cat-card .cat-body{ padding:22px 24px 26px; }
.cat-card .cat-world{
  font-family:var(--font-mono); font-size:.66rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted);
}

.store-card{ display:flex; flex-direction:column; gap:14px; }
.store-card .lp-addr, .store-card .lp-hrs{ font-style:normal; font-size:.98rem; color:var(--ink-mid); }
.store-card .lp-addr strong{ color:var(--ink); }

.fact-row{ display:flex; flex-wrap:wrap; gap:10px; margin:0 0 18px; padding:0; list-style:none; }
.fact{
  font-family:var(--font-mono); font-size:.74rem; letter-spacing:.06em;
  text-transform:uppercase; padding:7px 14px; border-radius:var(--r-pill);
  border:1px solid var(--border); color:var(--ink-mid);
}
.fact-hot{ border-color:var(--accent); color:var(--accent); }

/* 7. HERO ------------------------------------------------------------------ */

.hero{ position:relative; overflow:hidden; padding:clamp(60px,9vw,120px) 0 clamp(50px,7vw,90px); }
.hero-split{ display:grid; gap:34px; align-items:center; }
@media (min-width: 900px){ .hero-split{ grid-template-columns:1.15fr .85fr; gap:52px; } }
.hero h1 .h-line{ display:block; }
.hero .rule{ width:70px; height:4px; background:var(--brand); border-radius:var(--r-bar); margin:0 0 26px; }
.hero-cta{ display:flex; flex-wrap:wrap; gap:12px; margin-top:28px; }

.dual-panel{ display:grid; gap:0; }
@media (min-width: 800px){ .dual-panel{ grid-template-columns:1fr 1fr; } }
.dual-panel > *{ padding:clamp(40px,5vw,72px) clamp(24px,4vw,54px); }

/* 8. TABLES ---------------------------------------------------------------- */

.table-wrap{ overflow-x:auto; border:1px solid var(--border); border-radius:var(--r-card); }
table{ width:100%; border-collapse:collapse; font-size:.95rem; }
th,td{ padding:13px 16px; text-align:left; border-bottom:1px solid var(--border-soft); }
th{ font-family:var(--font-mono); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); }
tbody tr:last-child td{ border-bottom:0; }

/* 9. FAQ ACCORDION --------------------------------------------------------- */

.faq-accordion{ border:1px solid var(--border); border-radius:var(--r-card); overflow:hidden; }
.faq-item{ border-bottom:1px solid var(--border-soft); }
.faq-item:last-child{ border-bottom:0; }
.faq-item summary{
  cursor:pointer; padding:20px 24px; font-weight:700;
  font-family:var(--font-display); font-size:1.02rem;
  display:flex; justify-content:space-between; gap:16px; align-items:baseline;
  list-style:none;
}
.faq-item summary::-webkit-details-marker{ display:none; }
.faq-item summary::after{ content:'+'; font-family:var(--font-mono); color:var(--accent); flex:none; }
.faq-item[open] summary::after{ content:'\2013'; }
.faq-item .faq-a{ padding:0 24px 22px; color:var(--ink-mid); font-size:.97rem; }
.faq-item .faq-a p:last-child{ margin-bottom:0; }

/* 10. FOOTER --------------------------------------------------------------- */

.site-footer{ background:var(--h-surface-alt); color:var(--h-ink); padding-top:clamp(52px,7vw,84px); }
.site-footer a{ color:var(--h-ink-mid); text-decoration:none; }
.site-footer a:hover{ color:var(--h-accent); }
.foot-cols{ display:grid; gap:34px; padding-bottom:44px; }
@media (min-width: 760px){ .foot-cols{ grid-template-columns:1.5fr 1fr 1fr 1.2fr; } }
.foot-cols h3{
  font-family:var(--font-mono); font-size:.7rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--h-muted); margin-bottom:1.1em; font-weight:500;
}
.foot-cols ul{ list-style:none; margin:0; padding:0; display:grid; gap:9px; font-size:.93rem; }
.foot-brand p{ color:var(--h-muted); font-size:.93rem; max-width:34ch; }
.foot-social{ display:flex; gap:14px; margin-top:16px; }

.foot-chips{ display:flex; flex-wrap:wrap; gap:9px; padding:26px 0; border-top:1px solid var(--h-border); }
.foot-chips a{
  font-size:.8rem; padding:7px 15px; border-radius:var(--r-pill);
  border:1px solid var(--h-border); color:var(--h-ink-mid);
}
.foot-chips a:hover{ border-color:var(--h-accent); }

.foot-geo{ padding:26px 0; border-top:1px solid var(--h-border); display:grid; gap:26px; }
@media (min-width: 700px){ .foot-geo{ grid-template-columns:1fr 1fr; } }
.foot-geo h4{
  font-family:var(--font-mono); font-size:.68rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--h-muted); margin:0 0 .9em; font-weight:500;
}
.foot-geo ul{ list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:6px 16px; font-size:.88rem; }

.compliance-bar{
  border-top:1px solid var(--h-border); padding:22px 0;
  font-size:.8rem; color:var(--h-muted); line-height:1.7;
}
.compliance-bar strong{ color:var(--h-ink-mid); font-weight:600; }

.foot-bottom{
  border-top:1px solid var(--h-border); padding:20px 0 30px;
  display:flex; flex-wrap:wrap; gap:10px 22px; align-items:center;
  font-size:.82rem; color:var(--h-muted);
}
.foot-bottom nav{ display:flex; gap:18px; flex-wrap:wrap; }
.foot-powered{ margin-left:auto; color:var(--h-muted); }

/* 11. AI CITATION BLOCK ---------------------------------------------------- */

.ai-cite{
  background:var(--surface-alt); color:var(--muted);
  font-size:.83rem; line-height:1.75; padding:26px 0;
  border-top:1px solid var(--border-soft);
}
.ai-cite p{ max-width:none; margin:0; }

/* 12. UTILITIES ------------------------------------------------------------ */

.reveal{ opacity:1; }
.js-anim .reveal{ opacity:0; transform:translateY(16px); transition:opacity .6s var(--ease), transform .6s var(--ease); }
.js-anim .reveal.visible{ opacity:1; transform:none; }
@media (prefers-reduced-motion: reduce){
  .js-anim .reveal{ opacity:1; transform:none; transition:none; }
  .btn:hover,.card:hover{ transform:none; }
}

.notice{
  border:1px solid var(--accent); border-left-width:4px;
  border-radius:var(--r-card); padding:18px 22px; background:var(--surface);
  font-size:.93rem; color:var(--ink-mid);
}
.notice strong{ color:var(--ink); }

.breadcrumbs{ font-size:.82rem; color:var(--muted); padding:18px 0 0; }
.breadcrumbs ol{ list-style:none; display:flex; flex-wrap:wrap; gap:8px; margin:0; padding:0; }
.breadcrumbs li + li::before{ content:'/'; margin-right:8px; color:var(--border); }
.breadcrumbs a{ color:var(--muted); text-decoration:none; }
.breadcrumbs a:hover{ color:var(--ink); }

/* 13. NICOTINE WARNING BAR -------------------------------------------------- */
/* Sitewide, above the header. Required-style disclosure, styled as a fact, not
   as a scare banner. It sits above everything including the sticky nav. */

.warning-bar{
  background:var(--h-surface-alt); color:var(--h-ink-mid);
  border-bottom:2px solid var(--h-accent);
  font-family:var(--font-mono); font-size:.72rem; letter-spacing:.08em;
  text-transform:uppercase; text-align:center; padding:10px 20px; line-height:1.5;
}
.warning-bar strong{ color:var(--h-ink); font-weight:600; }

/* 14. HERO SLIDER ----------------------------------------------------------- */
/* Feature callouts. The page H1 deliberately lives BELOW this component in a
   static band: a rotating H1 is a moving target for crawlers and for anyone
   using a screen reader, and the slider's job is promotion, not page identity. */

.slider{ position:relative; overflow:hidden; }
.slider-track{ display:grid; }
.slide{
  grid-area:1/1; display:grid; gap:26px; align-items:center;
  padding:clamp(34px,5vw,64px) 0 clamp(60px,6vw,78px);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .5s var(--ease);
}
.slide.is-active{ opacity:1; visibility:visible; pointer-events:auto; }
.no-js-slider .slide{ opacity:1; visibility:visible; pointer-events:auto; grid-area:auto; }

@media (min-width: 880px){ .slide{ grid-template-columns:1.05fr .95fr; gap:44px; } }

.slide-eyebrow{
  font-family:var(--font-mono); font-size:.74rem; letter-spacing:.2em;
  text-transform:uppercase; color:var(--accent); margin:0 0 .8em; font-weight:500;
}
.slide-h{
  font-family:var(--font-display); font-weight:800;
  text-transform:uppercase; letter-spacing:-.025em; line-height:1.02;
  font-size:clamp(2rem,5vw,3.5rem); margin:0 0 .4em; color:var(--ink);
  text-wrap:balance;
}
.slide-dek{ color:var(--ink-mid); font-size:1.02rem; max-width:44ch; margin:0 0 22px; }
.slide-art{ display:flex; justify-content:center; }
.slide-art img{ width:min(320px,74vw); height:auto; }

.slider-dots{
  position:absolute; left:0; right:0; bottom:22px;
  display:flex; gap:10px; justify-content:center;
}
.slider-dots button{
  width:12px; height:12px; padding:0; border-radius:var(--r-pill);
  border:1.5px solid var(--ink-mid); background:transparent; cursor:pointer;
  transition:background .2s var(--ease), transform .2s var(--ease);
}
.slider-dots button[aria-selected="true"]{ background:var(--ink); transform:scale(1.15); }
.slider-dots button::after{
  content:''; position:absolute; width:44px; height:44px;
  transform:translate(-16px,-16px);
}

/* 15. THE SHELF — product cards + category tabs ----------------------------- */

.shelf-tabs{
  display:flex; flex-wrap:wrap; gap:0; justify-content:center;
  border:1.5px solid var(--border); border-radius:var(--r-pill);
  padding:5px; margin:0 auto 30px; width:fit-content; max-width:100%;
}
.shelf-tab{
  border:0; background:transparent; cursor:pointer;
  font-family:var(--font-display); font-weight:700; font-size:.88rem;
  letter-spacing:.02em; padding:11px 22px; min-height:44px;
  border-radius:var(--r-pill); color:var(--ink-mid);
  transition:background .2s var(--ease), color .2s var(--ease);
}
.shelf-tab[aria-selected="true"]{ background:var(--ink); color:var(--bg); }
.shelf-tab:hover:not([aria-selected="true"]){ color:var(--ink); }

/* One horizontal rail at every width. This used to flip to a five-column
   wrapping grid above 1040px, which put nine pouch brands into two rows of five
   and four -- a layout that reads as "here is the whole list" when the point of
   the component is "keep going, there is more". The rail keeps one behaviour
   everywhere and lets the arrows do the explaining. */
.shelf-rail{
  display:grid; grid-auto-flow:column; grid-auto-columns:minmax(230px,1fr);
  gap:18px; overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  /* Horizontal padding is not decoration: the cards are links, so a focused
     card's outline would be clipped by the overflow without it. scroll-padding
     matches so the snap lands flush rather than a few pixels inside. */
  padding:4px 4px 18px;
  scroll-padding-inline-start:4px;
  scrollbar-width:thin; scrollbar-color:var(--border) transparent;
}
.shelf-rail::-webkit-scrollbar{ height:8px; }
.shelf-rail::-webkit-scrollbar-thumb{ background:var(--border); border-radius:99px; }
.shelf-rail::-webkit-scrollbar-track{ background:transparent; }
.shelf-rail > *{ scroll-snap-align:start; }
@media (min-width: 1040px){
  .shelf-rail{ grid-auto-columns:minmax(250px,1fr); }
}

.pcard{
  display:flex; flex-direction:column;
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--r-card); overflow:hidden; text-decoration:none; color:inherit;
  transition:transform .2s var(--ease), box-shadow .2s var(--ease);
}
.pcard:hover{ transform:translateY(-3px); box-shadow:var(--shadow-lift); }
/* --product-ground, not --surface-alt: see the token. The panel stays light in
   dark mode so the packaging is still readable. */
.pcard-art{ background:var(--product-ground); padding:16px; display:flex; justify-content:center; }
.pcard-art img{ width:100%; max-width:170px; height:auto; }
.pcard-body{ padding:18px 18px 20px; display:flex; flex-direction:column; gap:7px; flex:1; }
.pcard-line{
  font-family:var(--font-mono); font-size:.64rem; letter-spacing:.15em;
  text-transform:uppercase; color:var(--muted);
}
.pcard-brand{ font-family:var(--font-display); font-weight:800; font-size:1.12rem; margin:0; }
.pcard-note{ font-size:.87rem; color:var(--ink-mid); margin:0; flex:1; }
.pcard-price{ font-family:var(--font-mono); font-size:.95rem; color:var(--ink); }
.pcard-cta{
  margin-top:6px; align-self:flex-start;
  font-family:var(--font-display); font-weight:700; font-size:.84rem;
  padding:9px 18px; min-height:38px; display:inline-flex; align-items:center;
  border-radius:var(--r-pill); border:1.5px solid var(--accent); color:var(--accent);
}
.pcard:hover .pcard-cta{ background:var(--accent); color:var(--bg); }

.shelf-foot{
  display:flex; flex-wrap:wrap; gap:12px 22px; align-items:center;
  margin-top:20px; font-size:.87rem; color:var(--muted);
}
.shelf-foot .updated{ font-family:var(--font-mono); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; }

/* 16. CATEGORY TILES -------------------------------------------------------- */
/* THE ONE DEPARTMENT COMPONENT. Client instruction 2026-09-23: the homepage
   treatment is the treatment, everywhere a department list appears. It is now
   used on the homepage, /shop/, both location pages and the Franklin County
   page. A department grid anywhere else uses this and nothing else.
   A v52 experiment replaced /shop/ with a second, larger card of its own; it was
   pulled in v53 for exactly the reason you would expect, which is that a site
   with two department components has no department component.

   The copy constraint is real and is the reason this works: .tile p caps at 22ch
   so the text column clears the art disc on the right. Anything longer than about
   two short sentences wraps into a ribbon and the tile stops reading as a tile.
   Write to the tile; do not widen the tile to fit the writing. */

.tile-grid{ display:grid; gap:18px; }
@media (min-width: 700px){ .tile-grid{ grid-template-columns:repeat(2,1fr); } }
@media (min-width: 1000px){ .tile-grid{ grid-template-columns:repeat(3,1fr); } }

.tile{
  position:relative; overflow:hidden; isolation:isolate;
  display:block; text-decoration:none; color:inherit;
  min-height:196px; padding:26px 26px 24px;
  background:var(--surface-alt); border:1px solid var(--border);
  border-radius:var(--r-card);
  transition:transform .2s var(--ease), box-shadow .2s var(--ease);
}
.tile:hover{ transform:translateY(-3px); box-shadow:var(--shadow-lift); }
.tile::before{
  content:''; position:absolute; z-index:-1;
  right:-58px; top:50%; transform:translateY(-50%);
  width:230px; height:230px; border-radius:50%;
  background:var(--surface);
}
.tile-art{
  position:absolute; z-index:-1; right:-4px; top:50%; transform:translateY(-50%);
  width:150px; height:150px; opacity:.96;
  /* Its own disc rather than relying on .tile::before, which is theme-coloured
     and goes near-black. The art is transparent, so without a light ground
     under it a dark-printed pack disappears in dark mode. Sized to sit inside
     the ::before circle, which then reads as a ring rather than a seam. */
  background:var(--product-ground); border-radius:50%;
  object-fit:contain; padding:12px;
}
/* One tile carries a photograph instead of a cut-out product, because the
   client's library has no cut-out CBD product in it -- only shelf scenes. A
   photograph floated with padding would read as a mistake, so it fills the disc
   instead and becomes a deliberate second treatment rather than a broken first
   one. See scripts/make-store-photos.py for how the crop was chosen. */
.tile-art-photo{ object-fit:cover; padding:0; }
.tile h3{ margin:0 0 .4em; font-size:1.3rem; }
.tile p{ font-size:.9rem; color:var(--ink-mid); margin:0; max-width:22ch; }
/* A chip rather than bare text. The CTA sits at the bottom-left of the tile and
   the product art is a disc on the right, so a long label ("Shop 251 tobacco
   products & prices") runs under the art and loses its contrast against a busy
   photograph -- something contrast-audit.py cannot catch, because it measures
   text against a background COLOUR and this is text against a picture. An
   opaque chip gives it its own ground at any length, and it reads as a control,
   which is what it is now that the counts are real. */
.tile-more{
  margin-top:16px; display:inline-block; position:relative;
  padding:7px 13px; border-radius:var(--r-pill);
  background:var(--surface-alt); border:1px solid var(--border);
  font-family:var(--font-mono); font-size:.7rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--accent);
}
.tile:hover .tile-more{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }

/* 17. PRE-COMMERCE NOTICE --------------------------------------------------- */
/* Delete this block the day online ordering goes live. Until then it is the
   sentence that keeps the shelf honest. */

.precommerce{
  display:flex; flex-wrap:wrap; gap:14px 20px; align-items:center;
  background:var(--surface); border:1px solid var(--accent); border-left-width:4px;
  border-radius:var(--r-card); padding:18px 22px; margin-top:26px;
}
.precommerce p{ margin:0; font-size:.92rem; color:var(--ink-mid); flex:1 1 300px; }
.precommerce strong{ color:var(--ink); }

/* 18. SLIDER, SMALL SCREENS ------------------------------------------------- */
@media (max-width: 879px){
  .slide{ gap:14px; padding-bottom:64px; }
  .slide-art img{ width:min(210px,52vw); }
  .slide-dek{ font-size:.97rem; }
}

/* 19. VERTICAL-MEDIA HERO ---------------------------------------------------- */
/* Info left, a stack of vertical (9:16) clips right. The rotating feature
   callouts from the slider live in the left column; the media column is static,
   so nothing moves in two places at once. */

.vhero{ position:relative; overflow:hidden; padding:clamp(28px,4vw,52px) 0 clamp(44px,5vw,64px); }
.vhero-grid{ display:grid; gap:30px; align-items:center; }
@media (min-width: 900px){
  .vhero-grid{ grid-template-columns:minmax(0,1.02fr) minmax(0,1fr); gap:44px; }
}
.vhero .slider-track{ position:relative; }
.vhero .slide{ display:block; padding:0; }
.vhero .slider-dots{ position:static; justify-content:flex-start; margin-top:26px; }

/* Media column: three vertical panels, staggered so the row has a rhythm
   rather than reading as three identical boxes. */
.vstack{
  display:grid; grid-template-columns:repeat(3,1fr); gap:12px;
  align-items:center;
}
.vpanel{
  margin:0;
  position:relative; overflow:hidden;
  border-radius:var(--r-card);
  background:var(--surface-alt);
  aspect-ratio:9/16;
  border:1px solid var(--border);
}
.vpanel:nth-child(1){ transform:translateY(18px); }
.vpanel:nth-child(3){ transform:translateY(-18px); }
.vpanel video,.vpanel img{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover;
}
.vpanel figcaption{
  position:absolute; left:0; right:0; bottom:0; z-index:2;
  padding:26px 12px 10px;
  background:linear-gradient(to top, rgba(14,9,5,.82), rgba(14,9,5,0));
  color:#EDE7DF;
  font-family:var(--font-mono); font-size:.6rem; letter-spacing:.13em;
  text-transform:uppercase; line-height:1.4;
}

@media (max-width: 899px){
  .vstack{ gap:9px; }
  .vpanel:nth-child(1),.vpanel:nth-child(3){ transform:none; }
  .vpanel figcaption{ font-size:.54rem; padding:20px 8px 8px; }
}
@media (prefers-reduced-motion: reduce){
  .vpanel:nth-child(1),.vpanel:nth-child(3){ transform:none; }
}

/* 20. FULL-BLEED VIDEO HERO ------------------------------------------------- */
/* Vertical source in a horizontal hero. Cropping one 9:16 clip to a 16:9 band
   throws away about two thirds of the frame and beheads the product, so the bed
   is a triptych instead: three vertical panels side by side, each about 480px
   wide on desktop, which crops far less. Mobile shows only the first panel,
   where 9:16 is the native fit and nothing is cropped at all. */

.vhero-full{
  position:relative; isolation:isolate; overflow:hidden;
  min-height:clamp(520px, 72vh, 700px);
  display:grid; align-items:end;
  /* This band is dark whatever the page theme is doing. */
  --ink:#EDE7DF; --ink-mid:#D2C8BC; --muted:#B6AA9B;
  --accent:#B6AA9B; --accent-ink:#080707; --border:rgba(237,231,223,.28);
  color:var(--ink);
  background:#0F0B0A;
}

.vhero-bed{
  position:absolute; inset:0; z-index:0;
  display:grid; grid-template-columns:1fr;
}
@media (min-width: 900px){
  .vhero-bed{ grid-template-columns:repeat(3,1fr); }
}
.vhero-bed .vpanel{
  border:0; border-radius:0; margin:0; transform:none;
  aspect-ratio:auto; height:100%; background:#0F0B0A;
}
.vhero-bed .vpanel + .vpanel{ box-shadow:inset 1px 0 0 rgba(224,169,78,.22); }
@media (max-width: 899px){
  /* One panel on mobile, at its native 9:16 with nothing cropped. It is the
     SECOND clip, not the first: the first is dark and disappears under the
     scrim on a small screen, where the scrim has to be heavier. */
  .vhero-bed .vpanel:nth-child(1),
  .vhero-bed .vpanel:nth-child(3){ display:none; }
}
.vhero-bed figcaption{
  position:absolute; left:auto; right:14px; top:16px; bottom:auto;
  padding:6px 12px; border-radius:var(--r-pill);
  background:rgba(23,17,11,.55); backdrop-filter:blur(6px);
  color:#EDE7DF; font-size:.58rem; letter-spacing:.16em;
}
@media (max-width: 899px){ .vhero-bed figcaption{ display:none; } }

.vhero-scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    linear-gradient(180deg, rgba(23,17,11,.72) 0%, rgba(23,17,11,.15) 26%, rgba(23,17,11,.55) 72%, rgba(23,17,11,.92) 100%),
    linear-gradient(96deg, rgba(23,17,11,.94) 0%, rgba(23,17,11,.80) 30%, rgba(23,17,11,.30) 52%, rgba(23,17,11,.12) 74%, rgba(23,17,11,.20) 100%);
}
@media (max-width: 899px){
  .vhero-scrim{
    background:linear-gradient(180deg, rgba(23,17,11,.82) 0%, rgba(23,17,11,.62) 22%, rgba(23,17,11,.30) 42%, rgba(23,17,11,.80) 72%, rgba(23,17,11,.95) 100%);
  }
}

.vhero-inner{
  position:relative; z-index:2;
  padding:clamp(84px,11vh,132px) 0 clamp(26px,3.5vh,40px);
  max-width:560px;
}
.vhero-full h1{
  color:var(--ink);
  font-family:var(--font-display); font-weight:800;
  text-transform:uppercase; letter-spacing:-.025em; line-height:1.02;
  font-size:clamp(1.9rem,3.9vw,3rem);
  margin:0 0 .45em;
}
.vhero-full .speakable-summary{ color:var(--ink-mid); max-width:48ch; font-size:1rem; }
.vhero-full .eyebrow{ color:var(--accent); }

/* Rotating feature strip along the bottom of the hero. Small, so it never
   competes with the H1 and never becomes the page's identity. */
.vstrip{
  position:relative; z-index:2;
  border-top:1px solid var(--border);
  padding:18px 0 26px;
  display:flex; flex-wrap:wrap; align-items:center; gap:14px 26px;
}
.vstrip .slider-track{ flex:1 1 420px; min-width:0; }
.vstrip .slide{ display:block; padding:0; }
.vstrip .slide-eyebrow{
  color:var(--accent); margin:0 0 .25em; font-size:.66rem; letter-spacing:.19em;
}
.vstrip .slide-h{
  font-size:1.06rem; text-transform:none; letter-spacing:0; font-weight:700;
  margin:0 0 .2em; color:var(--ink); line-height:1.3;
}
.vstrip .slide-h br{ display:none; }
.vstrip .slide-dek{ font-size:.9rem; color:var(--muted); margin:0; max-width:62ch; }
.vstrip .slide-dek a{ color:var(--accent); }
.vstrip .slider-dots{ position:static; margin:0; }
.vstrip .slider-dots button{ border-color:var(--muted); }
.vstrip .slider-dots button[aria-selected="true"]{ background:var(--accent); border-color:var(--accent); }

/* 21. SPLIT HERO WITH A CYCLING VIDEO STAGE --------------------------------- */
/* 21b. FULL-BLEED VIDEO HERO -------------------------------------------------
   Client instruction 2026-09-29: mimic the hero on nyelitecannabis.com with the
   new brand film. One muted clip full-bleed, a dark scrim, centred copy on top.

   The .hero-cycle block below is NOT dead. hero-media.json `mode` flips between
   this backdrop and the four-clip stage that block styles, and the stage is the
   fallback if the film ever has to come down. Delete both together or neither.

   The scrim is doing real accessibility work and is written in two layers. A
   single flat tint either washes out the picture or leaves the copy unreadable
   over the bright frames, and this film has both a neon OPEN sign and a white
   pouch wall in it. So: a vertical gradient that is heaviest at the top, where
   the header sits, and at the bottom, where the section meets the next band; and
   a radial that darkens the middle where the copy actually is. contrast-audit
   measures text against a background COLOUR and reads the section's #0F0B0A, so
   it cannot see this. The scrim is what makes its answer true. */

.hero-video{
  position:relative; isolation:isolate; overflow:hidden;
  display:flex; align-items:center;
  min-height:min(72vh, 700px);
  padding:clamp(50px,8vh,92px) 0 clamp(54px,8vh,96px);
  background:#0F0B0A;
  /* Same dark-world token override as .hero-cycle, so the buttons, the eyebrow
     and .cta-terms all inherit correctly without a single hero-specific rule. */
  --ink:#EDE7DF; --ink-mid:#D2C8BC; --muted:#B6AA9B;
  --accent:#B6AA9B; --accent-ink:#080707; --border:rgba(237,231,223,.28);
  --surface:#1C1512; --surface-alt:#120D0B;
  color:var(--ink);
}
.hero-video-bg{
  position:absolute; inset:0; z-index:-2;
  width:100%; height:100%; object-fit:cover; object-position:center;
}
/* LIGHTENED 2026-09-29 on the client's note that the film read too dark. Two
   things were making it dark and both were changed: the encode now carries a
   gamma lift, and this scrim came down from .74/.40/.52/.86 with a .34-to-.70
   radial. What is left is the minimum that keeps the copy legible over the
   brightest frame in the film, which is the lit ZYN wall at about six seconds.
   The text also carries its own shadow, so the scrim does not have to do all of
   it. If it is lightened further, check that frame specifically. */
.hero-video::before{
  content:''; position:absolute; inset:0; z-index:-1;
  background:
    linear-gradient(180deg, rgba(8,7,7,.56) 0%, rgba(8,7,7,.20) 34%,
                    rgba(8,7,7,.30) 66%, rgba(8,7,7,.68) 100%),
    radial-gradient(ellipse 74% 58% at 50% 52%, rgba(8,7,7,.16) 0%, rgba(8,7,7,.50) 100%);
}
/* The seam the old hero had, kept: it is the one line of brand colour that tells
   you the dark band has ended rather than just run out of picture. */
.hero-video::after{
  content:''; position:absolute; inset:auto 0 0 0; height:1px; z-index:1;
  background:linear-gradient(90deg, transparent, rgba(224,169,78,.45), transparent);
}

.hero-video-inner{ position:relative; text-align:center; max-width:960px; }
.hero-video .eyebrow{ color:var(--accent); }
.hero-video h1{
  color:var(--ink);
  font-family:var(--font-display); font-weight:800;
  text-transform:uppercase; letter-spacing:-.025em; line-height:1.04;
  /* Sized to land on three lines at desktop and never more than five on a
     phone. The first pass used the old split-hero clamp, which was tuned for a
     column half this wide and stacked this headline five lines high with the
     accent line orphaned at the bottom. A centred headline has the full measure,
     so it wants a smaller type size, not a bigger one. */
  font-size:clamp(1.62rem,2.75vw,2.2rem); margin:0 auto .45em;
  max-width:35ch;
  text-shadow:0 2px 30px rgba(8,7,7,.72), 0 1px 4px rgba(8,7,7,.55);
}
/* NY Elite sets their second line in a gold italic serif. This site has neither
   a gold nor an italic serif, and inventing one to match a screenshot is how a
   fifth hue reaches a palette. The stone already in this dark scope does the
   same job: it separates the place from the promise. */
.hero-video-accent{ color:var(--accent); }
/* The callout pair: one bold line, one light line, and that is the whole hero.
   .speakable-summary is styled as the bold one because it is the sentence that
   matters and because the class is a schema selector that has to stay on real
   content. Both carry a shadow of their own so the scrim above can stay light. */
.hero-video .speakable-summary{
  font-family:var(--font-display); font-weight:700;
  font-size:clamp(1.04rem,1.75vw,1.28rem); line-height:1.38;
  color:var(--ink); margin:0 auto 14px; max-width:46ch;
  text-shadow:0 1px 20px rgba(8,7,7,.62), 0 1px 3px rgba(8,7,7,.5);
}
/* --ink, not --ink-mid. It is the secondary line and the size and weight already
   say so; dimming the colour as well put it on the wrong side of legible over the
   brightest frames in the film, which are a lit pink pouch display and a white
   carton wall. Being read is not optional for the line carrying both addresses. */
.hero-video-sub{
  color:var(--ink); font-size:.98rem; line-height:1.5;
  max-width:54ch; margin:0 auto 26px;
  text-shadow:0 1px 20px rgba(8,7,7,.72), 0 1px 3px rgba(8,7,7,.55);
}
.hero-video .hero-cta{ justify-content:center; }
.hero-video .cta-terms{ margin-left:auto; margin-right:auto; max-width:62ch; }

/* MOBILE: THE FILM IS NOT CROPPED. Client instruction 2026-09-29 -- on a phone
   the whole frame should be visible rather than a zoomed slice of it.

   A 1.895:1 clip covering a portrait hero is cropped to about a third of its
   width, which on this film meant a vertical strip of one shelf and no sense of
   the store at all. So below 760px the hero stops being a backdrop and becomes a
   stack: the film plays at its own aspect ratio, full width, nothing cut, and the
   copy sits underneath it on the dark band.

   The scrim goes with it. Its whole job was making text legible ON the picture,
   and there is no text on the picture any more. Leaving it would only dim a film
   the client already said was too dark. */
@media (max-width: 899px){
  .hero-video{
    display:block; min-height:0; align-items:stretch;
    padding:0 0 clamp(30px,5vh,44px);
  }
  .hero-video-bg{
    position:static; z-index:auto;
    width:100%; height:auto; aspect-ratio:1600 / 844; object-fit:cover;
  }
  .hero-video::before{ display:none; }
  .hero-video-inner{
    text-align:left; padding-top:clamp(24px,4vh,34px);
  }
  .hero-video h1{ margin-left:0; margin-right:0; max-width:none; }
  .hero-video .speakable-summary,
  .hero-video-sub,
  .hero-video .cta-terms{ margin-left:0; margin-right:0; max-width:none; }
  .hero-video .hero-cta{ justify-content:flex-start; }
  /* No picture behind the words now, so the shadows are noise. */
  .hero-video h1,
  .hero-video .speakable-summary,
  .hero-video-sub{ text-shadow:none; }
}

/* A still hero for anyone who asked for one. The poster is painted by the video
   element itself once it is paused, so there is nothing else to swap in. */
@media (prefers-reduced-motion: reduce){
  .hero-video-bg{ display:block; }
}

/* The four notes, now a strip. They are four equal items, so no card chrome:
   the band already separates them from what is above and below. */
.hero-strip .hstrip-item{ min-width:0; }
.hero-strip .slide-eyebrow{
  color:var(--accent); font-family:var(--font-mono); font-size:.65rem;
  letter-spacing:.19em; text-transform:uppercase; margin:0 0 .4em;
}
.hero-strip .slide-h{
  font-family:var(--font-display); font-weight:700; font-size:1.05rem;
  line-height:1.25; margin:0 0 .45em; color:var(--ink);
}
.hero-strip .slide-dek{ font-size:.92rem; color:var(--ink-mid); margin:0; }
.hero-strip .slide-dek a{ color:var(--accent); }

/* Words left, one clip at a time right. Supersedes the triptych in §20: a single
   4:5 stage crops the source once, at encode time, instead of asking object-fit
   to guess at every breakpoint. §19 and §20 are kept only for the photo fallback. */

.hero-cycle{
  position:relative; overflow:hidden;
  padding:clamp(40px,6vh,76px) 0 clamp(44px,6vh,80px);
  background:#0F0B0A;
  --ink:#EDE7DF; --ink-mid:#D2C8BC; --muted:#B6AA9B;
  --accent:#B6AA9B; --accent-ink:#080707; --border:rgba(237,231,223,.24);
  --surface:#1C1512; --surface-alt:#120D0B;
  color:var(--ink);
}
.hero-cycle::after{
  content:''; position:absolute; inset:auto 0 0 0; height:1px;
  background:linear-gradient(90deg, transparent, rgba(224,169,78,.45), transparent);
}
.hero-cycle-grid{ display:grid; gap:32px; align-items:center; }
@media (min-width: 900px){
  .hero-cycle-grid{ grid-template-columns:minmax(0,1.08fr) minmax(0,.82fr); gap:56px; }
}
.hero-cycle h1{
  color:var(--ink);
  font-family:var(--font-display); font-weight:800;
  text-transform:uppercase; letter-spacing:-.025em; line-height:1.03;
  font-size:clamp(1.9rem,3.7vw,2.95rem); margin:0 0 .4em;
}
.hero-cycle .eyebrow{ color:var(--accent); }
.hero-cycle .speakable-summary{ color:var(--ink-mid); max-width:50ch; font-size:1rem; }
.hero-cycle .rule{ background:var(--accent); }

/* The rotating line under the summary. Small on purpose: it must never read as
   the page's identity, which is what the H1 above it is for. */
.hero-note{
  border-top:1px solid var(--border); margin-top:26px; padding-top:20px;
  min-height:112px;
}
.hero-note .slider-track{ display:grid; }
.hero-note .slide{
  grid-area:1/1; display:block; padding:0;
  opacity:0; visibility:hidden; transition:opacity .45s var(--ease);
}
.hero-note .slide.is-active{ opacity:1; visibility:visible; }
.no-js-slider .hero-note .slide{ opacity:1; visibility:visible; grid-area:auto; margin-bottom:14px; }
.hero-note .slide-eyebrow{
  color:var(--accent); font-family:var(--font-mono); font-size:.65rem;
  letter-spacing:.19em; text-transform:uppercase; margin:0 0 .35em;
}
.hero-note .slide-h{
  font-family:var(--font-display); font-weight:700; font-size:1.08rem;
  letter-spacing:0; text-transform:none; margin:0 0 .3em; color:var(--ink);
}
.hero-note .slide-dek{ font-size:.92rem; color:var(--muted); margin:0; max-width:56ch; }
.hero-note .slide-dek a{ color:var(--accent); }

/* Stage */
.vstage{
  position:relative; margin:0; overflow:hidden;
  aspect-ratio:var(--stage-aspect, 4 / 5);
  border-radius:var(--r-card);
  background:var(--surface-alt);
  box-shadow:0 24px 60px rgba(0,0,0,.45);
}
.vstage .vmedia{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  opacity:0; transition:opacity .55s var(--ease);
}
.vstage .vmedia.is-live{ opacity:1; }
.vstage-shade{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(180deg, rgba(14,9,5,.55) 0%, transparent 22%, transparent 68%, rgba(14,9,5,.72) 100%);
}

.vbars{
  position:absolute; top:14px; left:14px; right:14px; z-index:3;
  display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:6px;
}
.vbar{
  height:16px; padding:6px 0 7px; border:0; background:none; cursor:pointer;
  display:block;
}
.vbar span{
  display:block; height:3px; border-radius:2px;
  background:rgba(240,231,216,.32); position:relative; overflow:hidden;
}
.vbar span::after{
  content:''; position:absolute; inset:0 auto 0 0; width:var(--p,0%);
  background:var(--accent); border-radius:2px;
}
.vbar[aria-current="true"] span{ background:rgba(240,231,216,.5); }

.vcaps{ position:absolute; left:16px; bottom:14px; z-index:3; display:grid; }
.vcap{
  grid-area:1/1; opacity:0; transition:opacity .45s var(--ease);
  font-family:var(--font-mono); font-size:.63rem; letter-spacing:.16em;
  text-transform:uppercase; color:#EDE7DF;
}
.vcap.is-live{ opacity:1; }

/* The play affordance. Shipped hidden and revealed by shared.js in the three
   states where this stage would otherwise be a still poster with no way in:
   prefers-reduced-motion, a metered or 2G connection, and a refused autoplay.
   It is a real button, centred over the stage, and it has to read against a
   photograph that could be any colour -- hence the solid ground rather than a
   translucent one, and the ring that keeps its edge on a light frame. */
/* This pair is not optional. `display:inline-flex` on a class beats the user
   agent's `[hidden]{display:none}`, so without the second rule the button is on
   screen permanently no matter what the attribute says -- which is exactly how
   v26 shipped, with a Play button sitting over a video that was already
   playing. `.agegate` two hundred lines up carries the same pair for the same
   reason; that one was written correctly and this one was not. Any element in
   this sheet that sets `display` and is toggled with the `hidden` attribute
   needs its own `[hidden]` rule. */
.vplay[hidden]{ display:none; }
.vplay{
  position:absolute; z-index:4;
  top:50%; left:50%; transform:translate(-50%,-50%);
  display:inline-flex; align-items:center; gap:10px;
  padding:13px 22px 13px 18px;
  border:0; border-radius:999px; cursor:pointer;
  background:var(--accent); color:var(--accent-ink);
  box-shadow:0 2px 18px rgba(14,9,5,.45), 0 0 0 1px rgba(240,231,216,.28);
  font-family:var(--font-mono); font-size:.68rem; letter-spacing:.14em;
  text-transform:uppercase;
  /* No transform transition: this button exists FOR people who asked for less
     motion, and animating its arrival would be a poor way to greet them. */
  transition:box-shadow .2s var(--ease);
}
.vplay:hover{ box-shadow:0 4px 22px rgba(14,9,5,.55), 0 0 0 1px rgba(240,231,216,.45); }
.vplay:focus-visible{ outline:3px solid #F0E7D8; outline-offset:3px; }
.vplay-ico{ display:inline-flex; }
.vplay-ico svg{ width:16px; height:16px; display:block; }

@media (max-width: 899px){
  .vstage{ aspect-ratio:1 / 1; }
  .hero-note{ min-height:0; }
}

/* 22. THE REAL LOGO --------------------------------------------------------- */
/* Supplied as white-on-transparent, so two files ship: ink for the light
   chassis, white for dark surfaces and the footer. Only one is ever shown. */

.brand{ gap:0; }
.brand-logo{ width:auto; height:34px; display:block; }
@media (min-width: 960px){ .brand-logo{ height:42px; } }
.brand-logo-light{ display:none; }

/* Dark contexts: humidor pages and dark mode. */
body.page-humidor .brand-logo-ink,
:root[data-theme="dark"] .brand-logo-ink{ display:none; }
body.page-humidor .brand-logo-light,
:root[data-theme="dark"] .brand-logo-light{ display:block; }
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]) .brand-logo-ink{ display:none; }
  :root:not([data-theme="light"]) .brand-logo-light{ display:block; }
}

.foot-logo{ width:auto; height:44px; margin-bottom:16px; }
.ag-logo{ width:auto; height:40px; margin:0 auto 20px; }

/* 23. PROMO BANNER ---------------------------------------------------------- */
/* The client's own artwork, shown at its real aspect and linked. Never
   stretched, never cropped by CSS: it is a designed asset with type in it. */

/* Sits directly under the hero, so the gap above is deliberately tight: the
   banner reads as part of the hero block, not as a separate section. */
.promo-band{ background:var(--surface-alt); padding:16px 0 clamp(26px,3vw,40px); }
.promo{
  /* The whole banner is one link, so its caption inherits the global underline
     for in-content links and renders as three underlined sentences. Suppress it
     here and let .promo-more carry the affordance -- an 820px card that lifts on
     hover does not need an underline to look clickable. */
  text-decoration:none; color:inherit;
  display:block; max-width:820px; margin-inline:auto;
  border-radius:var(--r-card); overflow:hidden;
  border:1px solid var(--border); background:var(--surface);
  transition:transform .2s var(--ease), box-shadow .2s var(--ease);
}
.promo:hover{ transform:translateY(-3px); box-shadow:var(--shadow-lift); }
.promo img{ width:100%; height:auto; display:block; }
.promo-cap{
  display:flex; flex-wrap:wrap; gap:8px 18px; align-items:baseline;
  padding:14px 20px 16px; font-size:.9rem; color:var(--ink-mid);
}
.promo-cap strong{ color:var(--ink); }
.promo-cap .promo-more{
  margin-left:auto; font-family:var(--font-display); font-weight:700;
  font-size:.85rem; color:var(--accent);
}

/* 24. PHOTO BLOCKS ---------------------------------------------------------- */

.photo-figure{ margin:0; }
.photo-figure img{
  width:100%; height:auto; border-radius:var(--r-card);
  border:1px solid var(--border);
}
.photo-figure figcaption{
  margin-top:10px; font-family:var(--font-mono); font-size:.68rem;
  letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
}

.photo-row{ display:grid; gap:14px; margin-top:30px; }
@media (min-width: 700px){ .photo-row{ grid-template-columns:repeat(3,1fr); } }
.photo-row .photo-figure img{ aspect-ratio:3/2; object-fit:cover; }
.photo-row .photo-figure:first-child{ grid-column:1 / -1; }
@media (min-width: 700px){
  .photo-row .photo-figure:first-child{ grid-column:auto; }
}

/* 25. STORE CAROUSEL -------------------------------------------------------- */
/* A native scroll-snap rail, not a JS slider. The whole thing works with
   JavaScript off -- it is an overflow container, so it scrolls and swipes on
   its own -- and shared.js only adds the arrows, the counter and the keyboard
   shortcuts on top. Every slide is in the HTML, so the captions are crawlable
   and nothing is hidden behind a script that may never run.

   The slide width is deliberately less than the rail: the next photograph
   always peeks in from the right, which is the only honest signal that there is
   more to see. Arrows that appear over a full-bleed image are decoration; an
   edge you can see is an affordance. */

.carousel{ margin-top:26px; }

.carousel-rail{
  display:flex; gap:14px;
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  /* No horizontal padding. It is tempting to add some for breathing room, and
     it puts the rail's resting scrollLeft at the padding value rather than 0 --
     which quietly breaks any "are we at the start" test that compares against
     zero. The focus ring belongs to the rail itself, which sits outside the
     scroll area, so nothing needs the room anyway. */
  padding:0 0 10px;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
  scrollbar-color:var(--border) transparent;
}
.carousel-rail::-webkit-scrollbar{ height:8px; }
.carousel-rail::-webkit-scrollbar-thumb{
  background:var(--border); border-radius:99px;
}
.carousel-rail::-webkit-scrollbar-track{ background:transparent; }

.carousel-slide{
  flex:0 0 82%;
  scroll-snap-align:start;
  margin:0;
}
@media (min-width: 700px){ .carousel-slide{ flex-basis:52%; } }
@media (min-width: 1000px){ .carousel-slide{ flex-basis:38%; } }

.carousel-slide img{
  width:100%; aspect-ratio:3/2; object-fit:cover;
  border-radius:var(--r-card); border:1px solid var(--border);
  background:var(--surface-alt);
}
.carousel-slide figcaption{
  margin-top:10px; font-family:var(--font-mono); font-size:.68rem;
  letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
}

/* Controls are hidden until shared.js unhides them, and it only does so when
   the rail actually overflows. Without JS the arrows would be dead buttons; on a
   three-card wall that fits on screen they would be dead too. A dead button is
   worse than no button. */
.carousel-controls{
  display:none;
  align-items:center; gap:10px;
  margin-top:6px;
}
.carousel.is-ready .carousel-controls{ display:flex; }

/* The shelf walls reuse the carousel controls, sitting between the rail and the
   "everything in this wall" footer. */
.shelf-panel .carousel-controls{ margin-top:2px; }

.carousel-btn{
  width:44px; height:44px; flex:none;
  display:inline-flex; align-items:center; justify-content:center;
  border:1px solid var(--border); border-radius:50%;
  background:var(--surface); color:var(--ink);
  cursor:pointer; line-height:1;
  transition:background .18s var(--ease), border-color .18s var(--ease);
}
.carousel-btn:hover:not(:disabled){ background:var(--surface-alt); border-color:var(--accent); }
.carousel-btn:disabled{ opacity:.38; cursor:default; }
.carousel-btn svg{ width:17px; height:17px; display:block; }

.carousel-count{
  font-family:var(--font-mono); font-size:.7rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted); margin-left:4px;
}

@media (prefers-reduced-motion: reduce){
  .carousel-rail{ scroll-behavior:auto; }
}

/* Fine print: legal and rate-disclosure lines that must be present and must not
   compete with the copy around them. Still 4.5:1 on every ground it sits on --
   --muted is the token that was tuned for exactly that. */
.small{ font-size:.82rem; color:var(--muted); max-width:62ch; }

.embed-fallback{
  border-left:3px solid var(--accent); padding:2px 0 2px 16px;
  font-size:.92rem; color:var(--ink-mid);
}

/* 26. THIRD-PARTY SIGNUP PANEL --------------------------------------------- */
/* Host for the client's Klaviyo form. Deliberately a bare dark panel rather than
   a .card: the embed brings its own padding and its own white type, so a card's
   surface colour would only fight it. Nothing in here styles the form itself --
   Klaviyo's class names are randomised on every publish, so any rule targeting
   them is a silent breakage waiting for the next save. */
.signup-panel{
  background:var(--h-surface-alt);
  border:1px solid var(--h-border);
  border-radius:var(--r-card);
  padding:6px 18px;
  min-height:120px;
  /* Anything of ours inside this panel is on a dark ground regardless of which
     world the section is in, so it re-scopes its own ink. Without this the
     fallback message inherits the light band's dark brown and vanishes -- the
     exact bug this panel exists to fix, reintroduced one element lower. */
  --ink:var(--h-ink); --ink-mid:var(--h-ink-mid); --muted:var(--h-muted);
  --accent:var(--h-accent); --accent-ink:var(--h-accent-ink);
  --border:var(--h-border);
  color:var(--h-ink);
}
@media (min-width: 700px){ .signup-panel{ padding:10px 26px; } }

/* 27. SHOP PAGE RAILS ------------------------------------------------------- */
/* Two variations on the carousel from section 25, for the category pages.

   The store carousel's slide is 82/52/38% because a photograph needs to be big
   enough to read. A product card and a carton card do not: they carry one
   object and one word, so the same widths would put four cards on a 1400px
   screen and make a nine-brand wall look like a four-brand wall. These narrow
   the slide instead of duplicating the component. Everything else -- the snap,
   the arrows, the counter, the keyboard handling in shared.js -- is inherited,
   because it keys off [data-carousel] rather than off a class. */

.carousel-rail-cards{
  align-items:stretch;
  /* Cards are links or have focusable children, so unlike the photo rail this
     one needs the horizontal padding that section 25 warns off. shared.js reads
     the resting offset rather than comparing against zero, so it is safe here;
     see originOffset() before removing this. */
  padding:4px 4px 14px;
  scroll-padding-inline-start:4px;
}
.carousel-rail-cards > *{
  flex:0 0 68%;
  scroll-snap-align:start;
  margin:0;
}
@media (min-width: 700px){ .carousel-rail-cards > *{ flex-basis:38%; } }
@media (min-width: 1000px){ .carousel-rail-cards > *{ flex-basis:25%; } }
@media (min-width: 1240px){ .carousel-rail-cards > *{ flex-basis:21%; } }

/* A carton card. The art is a photograph on its own light ground rather than a
   cut-out on a transparent one -- see scripts/make-cartons.py for why keying
   these was abandoned -- so it gets the fixed product ground behind it for the
   same reason .pcard-art does: in dark mode a baked light photo needs a light
   surround or it reads as a lit rectangle floating on near-black. */
.bcard{
  display:flex; flex-direction:column; margin:0;
  background:var(--product-ground);
  border:1px solid var(--border); border-radius:var(--r-card);
  overflow:hidden;
}
.bcard img{
  width:100%; aspect-ratio:3/2; object-fit:cover; display:block;
}
.bcard figcaption{
  padding:11px 14px 13px;
  font-family:var(--font-display); font-weight:700; font-size:.95rem;
  /* Fixed ink, not var(--ink). The caption sits on --product-ground, which is
     deliberately NOT theme-scoped, so a theme-scoped ink would go near-white on
     a light card in dark mode. */
  color:#2E1710;
  background:var(--product-ground);
}

/* A five-up row of square product photographs. Not .photo-row: that one is 3:2
   and promotes its first child to full width, which is right for store
   interiors and wrong for five pieces of glass that are all equals. */
.glass-row{ display:grid; gap:14px; margin-top:26px; grid-template-columns:repeat(2,1fr); }
@media (min-width: 700px){ .glass-row{ grid-template-columns:repeat(5,1fr); } }
.glass-row .photo-figure img{ aspect-ratio:1/1; object-fit:cover; }

.carousel-slide a{ display:block; text-decoration:none; color:inherit; }
.carousel-slide a:focus-visible{ outline:3px solid var(--accent); outline-offset:3px; border-radius:var(--r-card); }

/* A flat row of sibling links at the foot of a category page. It exists because
   a shop page that only links its two locations and one sibling is a dead end
   for a crawler and for a person who came in from a search for one product and
   would happily look at the next. Deliberately plain: this is navigation, not a
   card grid competing with the content above it. */
.aisle-links{
  display:flex; flex-wrap:wrap; gap:8px 10px;
  margin-top:26px; padding-top:22px; border-top:1px solid var(--border);
}
.aisle-links a{
  font-family:var(--font-mono); font-size:.72rem; letter-spacing:.1em;
  text-transform:uppercase; text-decoration:none;
  padding:9px 14px; min-height:38px; display:inline-flex; align-items:center;
  border:1px solid var(--border); border-radius:var(--r-pill);
  color:var(--ink-mid);
}
.aisle-links a:hover{ border-color:var(--accent); color:var(--accent); }

/* A table caption that reads as a note rather than a title. Used on the corridor
   drive-time tables, where the caption carries the "this is an estimate"
   qualifier -- which belongs above the numbers a reader is about to trust, not
   in a footnote under them. */
.table-caption{
  caption-side:top; text-align:left;
  padding:14px 16px 12px;
  font-size:.84rem; line-height:1.5; color:var(--muted);
  border-bottom:1px solid var(--border-soft);
}

/* 28. APP STORE BADGES ------------------------------------------------------ */
/* Apple's and Google's badge guidelines are both strict, both prohibit editing
   the artwork, and they disagree in small ways. What that means in CSS:

     * Minimum on-screen height is 40px (Apple) and 28px (Google). One height
       above 40 satisfies both, and 44 also clears the 44px touch target.
     * Clear space is one quarter of the badge height on all sides, for both.
       That is the gap and the padding below, not a decorative margin -- it is a
       requirement, so it is expressed as a ratio of the height rather than a
       round number somebody will later "tidy up".
     * Neither badge may be recoloured, rotated, animated or rebuilt. There is
       deliberately no filter, no opacity change and no hover recolour here.
       The only hover affordance is a lift, which touches nothing inside the
       artwork.
     * Google's file carries a large amount of built-in transparent padding and
       Apple's carries none, so an identical CSS height renders Google's black
       box less than half the size of Apple's. The fix belongs here, as a taller
       image with the padding pulled back off by negative margin, NOT in editing
       either file. The measurements it is derived from are recorded with it
       below, so the numbers can be checked rather than re-guessed. */

.app-badges{
  display:flex; flex-wrap:wrap; align-items:center;
  /* one quarter of badge height, per both guidelines */
  gap:11px;
  margin-top:22px; padding:11px 0;
}
.app-badge{
  display:inline-flex; text-decoration:none;
  border-radius:8px;
  transition:transform .2s var(--ease);
}
.app-badge:hover{ transform:translateY(-2px); }
.app-badge:focus-visible{ outline:3px solid var(--accent); outline-offset:4px; }
/* max-width:none is load-bearing, not tidiness, and it is the whole reason the
   widths below are stated outright instead of left to the aspect ratio.

   The site reset carries the usual img{max-width:100%}. Combine that with the
   negative margins below and the two rules eat each other: the link is an
   inline-flex box, so it sizes to the image's MARGIN box -- 160.6 minus the
   12.6px of negative side margin, or 148px -- and then 100% of that 148px
   parent clamps the image itself back to 148px. The height is explicit and
   does not follow, so the badge rendered 148 x 104.4 where its own ratio calls
   for 160.6 x 104.4: an 8% horizontal squash of somebody else's trademark.
   Both guidelines forbid altering the artwork, and a squashed badge is an
   alteration. It is also invisible in a screenshot, which is why
   scripts/verify-badges.py checks the ratio numerically. */
.app-badge img{ display:block; flex:0 0 auto; max-width:none; }
/* Apple's artwork is the badge and nothing else. The shipped SVG is
   119.664 x 40 with zero padding, so the CSS height IS the badge height. */
.app-badge-apple img{ height:44px; width:131.6px; }   /* 44 * 2.9916 */

/* Google's file is the opposite case, and by much more than anyone guesses.
   Measured off the shipped PNG rather than estimated:

     canvas   880 x 572
     badge    811 x 241   occupying x 34..845, y 165..406
     padding  L34  T165  R35  B166
     the badge is 42.1% of the canvas height, not the ~12.5% first assumed

   So to land the black box at 44px the image renders 44 * 572/241 = 104.4px
   tall (scale 0.18257), and the baked-in padding then measures 30.1 top,
   6.4 right, 30.3 bottom, 6.2 left, removed by an equal negative margin.

   Those margins carry the clear space as well as the alignment: they put the
   artwork edge exactly on the margin-box edge, so the 11px flex gap and the
   11px vertical padding above are the true distance to the artwork, which is
   the quarter-of-height both guidelines require. Change the height and all
   four margins must be recomputed from the measurements above. Do not crop the
   file instead -- that is an edit to the artwork, which both forbid. */
.app-badge-google img{
  height:104.4px; width:160.6px;                  /* 104.4 * 880/572 */
  margin:-30.1px -6.4px -30.3px -6.2px;
}

@media (max-width: 460px){
  /* Apple's floor, not below it. */
  .app-badge-apple img{ height:40px; width:119.7px; }
  /* Same arithmetic at a 40px badge: 40 * 572/241 = 94.9px, scale 0.16598. */
  .app-badge-google img{
    height:94.9px; width:146px;
    margin:-27.4px -5.8px -27.6px -5.6px;
  }
  .app-badges{ gap:10px; }
}

/* 29. AFTER DARK, AND BREAKING THE GRID ------------------------------------- */
/* The brief was "hipper, cooler, more futuristic -- it looks stale, one section
   on top of another." That last clause was the accurate half and the one worth
   designing against: every band was the same shape (eyebrow, slab headline,
   lede, content), the same column width, the same padding, nine times down the
   page, and nothing ever touched the edge of the viewport. Colour alone would
   not have fixed that, so this section is two things at once -- a night world,
   and a set of primitives for varying the rhythm.

   What is deliberately NOT here: a webfont change, rounded-everything, or a
   gradient on the brand marks. The slab serif and the oxblood are the client's
   own identity and they are the reason the site does not look like every other
   vape shop. The job is to give them somewhere to breathe. */

.world-night{
  --bg:var(--n-bg); --surface:var(--n-surface); --surface-alt:var(--n-surface-alt);
  --ink:var(--n-ink); --ink-mid:var(--n-ink-mid); --muted:var(--n-muted);
  --border:var(--n-border); --border-soft:rgba(237,231,223,.08);
  --accent:var(--n-accent); --accent-ink:var(--n-accent-ink);
  background:var(--bg); color:var(--ink);
  position:relative; isolation:isolate; overflow:hidden;
}
.world-night.band-alt{ background:var(--n-surface-alt); }
.world-night h1,.world-night h2{
  font-family:var(--font-display); font-weight:800;
  letter-spacing:-.025em; text-transform:uppercase;
}
.world-night .eyebrow{ color:var(--n-accent); }
/* :not(.btn) is load-bearing. `.world-night a` is (0,1,1) and `.btn-accent` is
   (0,1,0), so a blanket link colour here outranks the button's own ink and
   painted cyan text on a cyan pill -- an invisible label on the main call to
   action of the section. Buttons carry their own colour pair; links do not. */
.world-night a:not(.btn){ color:var(--n-accent); }

/* The tubes. Two soft pools of colour, well under the content, standing in for
   the lit cases the photographs were shot in. Pinned with `background-attachment`
   left alone on purpose -- fixed backgrounds are a jank source on mobile Safari
   and the parallax below already carries the depth. */
.world-night::before{
  content:''; position:absolute; inset:-30% -10%; z-index:-1; pointer-events:none;
  background:
    radial-gradient(40% 44% at 16% 20%, var(--n-glow-red) 0%, transparent 70%),
    radial-gradient(34% 40% at 88% 80%, var(--n-glow-stone) 0%, transparent 68%);
  opacity:.75;
}

/* The humidor's version of the same idea, and it is OPT-IN rather than blanket
   on .world-humidor. That class wraps fifteen sections across six pages, six of
   them consecutive on /shop/cigars/; a pool on every one is wallpaper, not
   light, and the effect stops being worth having the moment it is everywhere.
   Add `lit-warm` to the one section on a page that should carry it.

   One pool, not two, and anchored top-right so it does not mirror the night
   band's red at top-left -- the two sections are within a scroll of each other
   on the homepage and reading as the same treatment twice was the thing to
   avoid. The second gradient is a floor rather than a light: the humidor's own
   surface-alt, darkening the foot of the band so it has somewhere to sit. */
.world-humidor.lit-warm{
  position:relative; isolation:isolate; overflow:hidden;
}
.world-humidor.lit-warm::before{
  content:''; position:absolute; inset:-25% -10%; z-index:-1; pointer-events:none;
  background:
    radial-gradient(46% 52% at 84% 12%, var(--h-glow-stone) 0%, transparent 68%),
    radial-gradient(64% 44% at 24% 106%, var(--h-glow-deep) 0%, transparent 72%);
}

/* A hairline that reads as a lit edge rather than a border. */
.rule-neon{
  height:1px; border:0; margin:0;
  background:linear-gradient(90deg, transparent, var(--n-accent), var(--n-ember), transparent);
  opacity:.75;
}

/* ---- breaking the grid -------------------------------------------------- */

/* Escape .wrap and run to both edges. The negative margin is the width of the
   wrap's own gutter plus whatever is left of the viewport, which is what the
   two calc()s work out; 100vw is deliberate rather than 100% because the parent
   is capped at --wrap and 100% would only reach that cap. */
.bleed{
  width:100vw;
  margin-left:calc(50% - 50vw);
  margin-right:calc(50% - 50vw);
  max-width:none;
}
/* A scrollbar makes 100vw wider than the document on desktop Windows/Linux, so
   anything bled has to be clipped by its band or the page scrolls sideways. */
.band-bleed{ overflow-x:clip; }

.bleed-media{
  position:relative; display:block; width:100vw;
  margin-left:calc(50% - 50vw); margin-right:calc(50% - 50vw);
  height:clamp(280px,52vh,560px); overflow:hidden;
  /* A real ground rather than transparent. The photograph covers it and the
     scrim below sits on top of that, so what is actually behind the caption is
     this colour at ~86% -- but a contrast checker walks ANCESTOR backgrounds,
     not sibling overlays, and with this transparent it resolved the caption
     against the page cream and read 1.1:1. Declaring the ground makes the
     measurement match the pixels instead of arguing with the tool. */
  background:#0F0B0A;
}
.bleed-media img,.bleed-media video{
  width:100%; height:100%; object-fit:cover; display:block;
  /* Slightly oversized so the parallax has somewhere to travel without
     revealing an edge. Kept in CSS rather than JS so it holds with JS off. */
  scale:1.12;
}
/* A real scrim, not a text-shadow. The caption sits over a photograph whose
   brightness is not knowable, and a shadow is a hope rather than a contrast
   ratio -- the audit measured this pair at 1.1:1 because a shadow is not a
   background. This paints an actual gradient the caption can be measured
   against. */
.bleed-media::after{
  content:''; position:absolute; inset:auto 0 0 0; height:44%;
  background:linear-gradient(to top, rgba(15,11,10,.88) 0%, rgba(15,11,10,.58) 45%, transparent 100%);
  pointer-events:none;
}
/* A band that OPENS with a full-bleed figure gives up its top padding: the
   picture is the top edge of the section. Leaving the padding in puts a strip
   of the band's own ground above a photograph that runs to both edges, which
   reads as a gap rather than as breathing room, and the previous band's colour
   is then separated from the picture by a sliver of a third colour. */
.band-open-media{ padding-top:0; }
.band-open-media > .wrap > .bleed-media:first-child{ margin-top:0; }

.bleed-media figcaption{
  position:absolute; z-index:1;
  left:max(20px,calc(50vw - var(--wrap)/2 + 20px)); bottom:18px;
  font-family:var(--font-mono); font-size:.63rem; letter-spacing:.16em;
  text-transform:uppercase; color:#EDE7DF;
}

/* Asymmetric two-column. The offset is the point: equal columns that start on
   the same line are what made every section read the same. */
.split-offset{ display:grid; gap:28px; }
@media (min-width: 900px){
  .split-offset{ grid-template-columns:1.25fr .75fr; gap:56px; align-items:start; }
  .split-offset > :nth-child(2){ margin-top:clamp(28px,5vw,84px); }
  .split-offset.flip{ grid-template-columns:.75fr 1.25fr; }
  .split-offset.flip > :nth-child(1){ margin-top:clamp(28px,5vw,84px); }
  .split-offset.flip > :nth-child(2){ margin-top:0; }
}

/* Interlock: pull a block up over the seam so two sections share an edge
   instead of stacking against it. */
.overlap-up{ position:relative; z-index:2; margin-top:clamp(-88px,-6vw,-40px); }

/* One statement size, used sparingly. The point of adding it is the DISTANCE
   from the ordinary h2 -- if this appears more than twice on a page it stops
   being a statement and the page goes back to being flat, just louder. */
.display-xl{
  font-family:var(--font-display); font-weight:800; text-transform:uppercase;
  font-size:clamp(2.6rem,7.4vw,6.2rem); line-height:.92; letter-spacing:-.035em;
  margin:0 0 22px;
}
/* `color` stays a real, passing colour and only `-webkit-text-fill-color` is
   made transparent. Setting `color:transparent` instead renders identically but
   is a lie about the text: it computes as transparent, which is what a contrast
   checker reads, what a forced-colours mode honours, and what the audit caught
   at 1.05:1. This way the computed colour is the cyan the eye actually sees,
   the gradient is painted over it, and a browser that supports neither property
   falls back to solid cyan on its own. */
.display-xl .lit{
  color:var(--n-accent);
  background:linear-gradient(92deg,var(--n-accent) 6%,var(--n-ember) 94%);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
}
@media (forced-colors: active){
  .display-xl .lit{ -webkit-text-fill-color:currentColor; background:none; }
}

/* The humidor's statement size. Worth being explicit about what separates this
   from .display-xl, because --font-serif and --font-display resolve to the SAME
   stack (Rockwell): the difference is not the typeface, it is weight 600 against
   800, mixed case against uppercase, and no gradient span. That is the whole
   distinction and it is enough -- case and weight are what the eye reads first.
   Ceiling stops at 4.2rem rather than 6.2rem: Rockwell at 600 in mixed case
   carries descenders and lowercase counters that caps do not, and the larger
   size closes them up. Same rarity rule as .display-xl -- once per page. */
.display-serif{
  font-family:var(--font-serif);
  font-weight:600;
  font-size:clamp(2.2rem,5.2vw,4.2rem);
  line-height:1.02;
  letter-spacing:-.02em;
  text-transform:none;
  margin:0 0 20px;
  text-wrap:balance;
}
/* A hyphenated compound will break AT the hyphen, and at this size "The walk-"
   over "in humidor" is the first thing the eye lands on in the section. Wrap
   the compound in this and it stays whole. text-wrap:balance does not prevent
   it: an explicit hyphen is a legal break opportunity, not a hyphenation. */
.nobreak{ white-space:nowrap; }

/* Frosted panel, for content sitting over photography on the night world. */
.glass{
  background:rgba(28,21,18,.66);
  border:1px solid var(--n-border);
  border-radius:var(--r-card);
  padding:clamp(20px,3vw,30px);
  box-shadow:0 18px 60px rgba(0,0,0,.45);
}
@supports (backdrop-filter:blur(1px)){
  .glass{ backdrop-filter:blur(14px) saturate(1.2); background:rgba(28,21,18,.46); }
}

/* Vertical rhythm variants, so the padding is not the same number every time. */
.band-tight{ padding-block:clamp(30px,3.6vw,52px); }
.band-tall{ padding-block:clamp(72px,9vw,144px); }

/* A stat row that earns its scale. */
.figure-row{
  display:grid; gap:clamp(18px,3vw,44px); margin-top:34px;
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
}
.figure-n{
  font-family:var(--font-display); font-weight:800; line-height:.9;
  font-size:clamp(2.1rem,4.6vw,3.5rem); letter-spacing:-.03em;
  color:var(--n-accent);
}
.figure-l{
  margin:8px 0 0; font-family:var(--font-mono); font-size:.68rem;
  letter-spacing:.15em; text-transform:uppercase; color:var(--muted);
}

/* ---- motion ------------------------------------------------------------- */
/* Everything here is opt-in per element and every one of them is inert under
   prefers-reduced-motion. shared.js also refuses to start the parallax loop at
   all in that case, so this block is the second line of defence, not the only
   one. */
.parallax-inner{ will-change:transform; }

.sticky-label{
  position:sticky; top:96px; align-self:start;
  font-family:var(--font-mono); font-size:.66rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--muted);
}

@media (prefers-reduced-motion: reduce){
  .parallax-inner{ will-change:auto; transform:none !important; }
  .sticky-label{ position:static; }
}

/* A variant of .split-offset for a form rather than a picture. Two differences,
   both because a form is not decoration: the second column gets a real minimum
   so the embed is never squeezed below a comfortable field width (at the plain
   ratio it landed at 399px, narrower than the phone it was designed for), and
   the vertical offset is cancelled -- dropping a signup form 84px down the page
   reads as a mistake rather than as rhythm. */
@media (min-width: 900px){
  .split-offset.split-form{ grid-template-columns:1fr minmax(340px,470px); }
  .split-offset.split-form > :nth-child(2){ margin-top:0; }
}

/* A bleed sized by its ARTWORK rather than by a fixed strip height. The
   storefront needed this: the generic .bleed-media is a fixed-height band, and
   at a narrow viewport that band becomes taller than the photograph is wide, so
   `cover` switches from cropping the top and bottom to cropping the left and
   right -- the object-position tuned on desktop then does nothing at all, and a
   wide building shot turns into a portrait sliver of its middle. Matching the
   frame to the asset's own ratio means the crop decision is made once, in the
   image file, and holds at every width. The min-height stops it collapsing to a
   letterbox sliver on a phone. */
.bleed-media.bleed-fit{
  height:auto;
  aspect-ratio:1400 / 513;
  min-height:210px;
}
/* Less zoom than the generic bleed. The 12% oversize exists to give the parallax
   somewhere to travel without exposing an edge, and on a loose crop it costs
   nothing. On a crop cut to the building it costs the building: 12% takes 6% off
   every edge, which was enough to slice the entrance off the bottom. 8% with the
   travel dialled back to match keeps the whole storefront in frame. */
.bleed-fit img{ scale:1.08; }

/* 30. SHELF PRICE MENU -----------------------------------------------------
   v39, reworked same day. The first cut was a two-column table straight off the
   POS export and it read like one: on the pouch page "ALP" ran twenty times down
   the left and "$28.50" twenty times down the right, with the flavour and
   strength -- the only part anyone scans for -- buried between them.

   So this is built like a menu. The brand is the heading and carries the price
   whenever the whole group shares one, which across the six departments removes
   about 280 repeated figures. Variants sit under it as a run of text rather than
   as rows, because a list of flavours is a list, not a grid. Only brands that
   genuinely span prices get per-line figures. */

.pricelist{ margin-top:28px }
.pricelist-meta{
  font-size:.92rem; color:var(--ink-mid);
  border-left:3px solid var(--accent);
  padding:2px 0 2px 14px; margin:0 0 20px; max-width:64ch;
}
.pricelist-tools{
  display:flex; flex-wrap:wrap; align-items:center; gap:12px 18px; margin:0 0 26px;
}
.pricelist-filter{ display:block; margin:0; flex:1 1 280px; max-width:420px }
.pricelist-sort{
  display:inline-flex; align-items:center; gap:8px;
  font-size:.86rem; color:var(--ink-mid);
}
.pricelist-sort[hidden]{ display:none }
.pricelist-sort select{
  font:inherit; color:var(--ink); background:var(--surface);
  border:1px solid var(--border); border-radius:var(--r-pill);
  padding:10px 14px; min-height:44px;
}
.pricelist-sort select:focus-visible{ outline:2px solid var(--accent); outline-offset:2px }
.pricelist-filter input{
  width:100%; padding:11px 14px; min-height:44px;
  font-family:var(--font-body); font-size:.95rem;
  color:var(--ink); background:var(--surface);
  border:1px solid var(--border); border-radius:var(--r-pill);
}
.pricelist-filter input::placeholder{ color:var(--muted) }
.pricelist-filter input:focus-visible{
  outline:2px solid var(--accent); outline-offset:2px; border-color:transparent;
}
.pricelist-shelf{
  display:flex; align-items:baseline; justify-content:space-between; gap:16px;
  font-family:var(--font-display); font-size:1.02rem; font-weight:700;
  text-transform:uppercase; letter-spacing:.06em;
  margin:34px 0 4px; padding-bottom:8px;
  border-bottom:2px solid var(--ink);
}
.pricelist-shelf span{
  font-family:var(--font-mono); font-size:.66rem; font-weight:400;
  letter-spacing:.12em; color:var(--muted); white-space:nowrap;
}

/* ---- a brand block ---- */
.menu{ margin-top:4px }
.menu-group{ padding:15px 0 14px; border-bottom:1px solid var(--border) }
.menu-group:last-child{ border-bottom:0 }

/* A brand with a photograph becomes two columns and the picture gets real size.
   Only twelve brands of about a hundred and sixty have art, so rather than
   reserving an empty gutter on the other hundred and forty-eight, the ones with
   a photograph read as featured and the rest keep the full width they had. */
.menu-group.has-art{
  display:grid; grid-template-columns:auto 1fr; gap:0 24px;
  align-items:start; padding-block:20px 18px;
}
.menu-shot{
  display:block; width:112px; height:112px;
  background:var(--product-ground, #EAE5DD);
  border-radius:var(--r-card);
  padding:12px;
}
.menu-body{ min-width:0 }

/* The brand line is the menu line: name left, price right, joined by a leader.
   The leader is a border on a flexible spacer rather than a row of dot
   characters, so it never reaches a screen reader and never wraps. */
.menu-brand{
  /* max-width:none is load-bearing. The base sheet sets p{max-width:72ch} for
     readable prose, and this is a <p>, so the flex row was capping at ~730px
     and stranding every price mid-line instead of at the right margin. */
  max-width:none;
  display:flex; align-items:baseline; gap:10px; margin:0;
  font-family:var(--font-display); font-size:1.06rem; font-weight:700;
  letter-spacing:-.01em; color:var(--ink);
}
.menu-name{ flex:0 1 auto }
.menu-note{
  font-family:var(--font-mono); font-size:.62rem; font-weight:400;
  letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
  margin-left:7px; vertical-align:1px;
}
.menu-dots{
  flex:1 1 auto; min-width:18px; height:0;
  border-bottom:1px dotted var(--border); transform:translateY(-4px);
}
.menu-price{
  flex:0 0 auto; font-variant-numeric:tabular-nums; white-space:nowrap;
}
.menu-range{ font-size:.93em; color:var(--ink-mid) }
/* "most lines" qualifies a dominant price; the exceptions are marked inline. */
.menu-most{
  font-family:var(--font-mono); font-size:.6rem; font-weight:400;
  letter-spacing:.12em; text-transform:uppercase; color:var(--muted);
  margin-left:6px;
}
.menu-odd{
  font-weight:600; color:var(--ink); font-variant-numeric:tabular-nums;
  white-space:nowrap;
}

/* Variants under a single-price brand: a run of text, not rows. */
.menu-vars{
  margin:8px 0 0; font-size:.9rem; line-height:1.8; color:var(--ink-mid);
  max-width:78ch;
}
.menu-var:not(:last-child)::after{
  content:" \00b7 "; color:var(--muted); padding:0 3px;
}
.menu-var[hidden]{ display:none }

/* Variants under a mixed-price brand keep their own figure. */
.menu-list{ list-style:none; margin:8px 0 0; padding:0 }
.menu-list li{
  max-width:none;
  display:flex; align-items:baseline; gap:10px;
  font-size:.9rem; color:var(--ink-mid); padding:3px 0;
}
.menu-list li[hidden]{ display:none }
.menu-list .menu-price{ font-weight:600; color:var(--ink) }

.menu-solo .menu-brand{ font-weight:600 }
.menu-group[hidden]{ display:none }

.pricelist-empty{ font-size:.9rem; color:var(--muted); padding:14px 0; margin:0 }
.pricelist-empty[hidden]{ display:none }

.vh{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* 31. BRAND CARDS ---------------------------------------------------------
   Generated from the same data as the price menu, and built to be used rather
   than read: the name and the price sit on one line, and the primary action is
   into the shelf filtered to that brand. "About <brand>" is the secondary link
   and only appears where a brand page exists to go to. */

/* Borders live on the cards, not on the grid. A 1px-gap grid with a coloured
   background is the usual trick for hairline rules, but seven cards in a
   four-column layout leaves an empty cell and that background paints it as a
   grey block. Per-card borders leave the empty cell empty. */
.brandgrid{
  display:grid; gap:12px; margin-top:24px;
  /* start, not stretch: only some brands have art, and stretching every card to
     the tallest in its row opened a dead gap above the button on the ones
     without. Cards now size to their own content. */
  align-items:start;
  grid-template-columns:repeat(auto-fit,minmax(248px,1fr));
}
.brandcard{
  background:var(--surface); padding:18px 20px 16px;
  border:1px solid var(--border); border-radius:var(--r-card);
  display:flex; flex-direction:column; gap:9px;
}
.brandcard-top{
  display:flex; align-items:baseline; justify-content:space-between; gap:12px;
  margin:0; max-width:none;
}
.brandcard-name{
  font-family:var(--font-display); font-weight:700; font-size:1.12rem;
  letter-spacing:-.01em; color:var(--ink);
}
.brandcard-price{
  font-variant-numeric:tabular-nums; white-space:nowrap;
  font-weight:600; color:var(--ink-mid); font-size:.95rem;
}
.brandcard-copy{
  margin:0; font-size:.88rem; line-height:1.55; color:var(--ink-mid);
  max-width:none;
}
.brandcard-links{
  display:flex; flex-wrap:wrap; gap:8px 16px; align-items:baseline;
  margin:2px 0 0; max-width:none;
}
/* The filter link is the primary action, so it reads as a control rather than
   as body-copy link. */
.brandcard-see{
  font-family:var(--font-display); font-weight:700; font-size:.82rem;
  letter-spacing:.02em; text-decoration:none;
  padding:7px 14px; min-height:34px; display:inline-flex; align-items:center;
  border:1px solid var(--accent); border-radius:var(--r-pill);
  color:var(--accent); background:transparent;
}
.brandcard-see:hover,.brandcard-see:focus-visible{
  background:var(--accent); color:var(--accent-ink); text-decoration:none;
}
.brandcard-more{ font-size:.84rem }

/* The same control on a photo card. It inherits .brandcard-see's shape so the
   two entry points into the filtered menu look like one mechanism, and it sits
   at the bottom of the card body because .pcard-note has flex:1 above it. */
.pcard-shop{
  font-family:var(--font-display); font-weight:700; font-size:.82rem;
  letter-spacing:.02em; text-decoration:none; align-self:flex-start;
  margin-top:4px; padding:7px 14px; min-height:34px;
  display:inline-flex; align-items:center;
  border:1px solid var(--accent); border-radius:var(--r-pill);
  color:var(--accent); background:transparent;
}
.pcard-shop:hover,.pcard-shop:focus-visible{
  background:var(--accent); color:var(--accent-ink); text-decoration:none;
}

/* The terms that sit under a "Shop now" button. Small, quiet, and never more
   than two lines: it is a qualifier on the CTA above it, not a second CTA. */
.cta-terms{
  margin:14px 0 0; max-width:54ch;
  font-size:.83rem; line-height:1.5; color:var(--ink-mid);
}

/* The filtered shelf announces what it is showing, so arriving from a card
   never looks like the list simply lost most of its rows. */
.pricelist-active{
  display:flex; flex-wrap:wrap; align-items:center; gap:10px;
  margin:0 0 18px; font-size:.88rem; color:var(--ink-mid);
}
.pricelist-active[hidden]{ display:none }
.pricelist-clear{
  font-family:var(--font-mono); font-size:.66rem; letter-spacing:.1em;
  text-transform:uppercase; padding:6px 12px; min-height:32px;
  display:inline-flex; align-items:center;
  border:1px solid var(--border); border-radius:var(--r-pill);
  background:var(--surface); color:var(--ink); cursor:pointer;
}
.pricelist-clear:hover{ border-color:var(--accent); color:var(--accent) }
.pricelist-clear:focus-visible{ outline:2px solid var(--accent); outline-offset:2px }

/* 32. PRODUCT PHOTOGRAPHY IN THE SHELF ------------------------------------
   Two levels, and the distinction is not cosmetic. Brand art is
   representative and sits on a brand name. Exact art depicts one SKU with its
   strength printed on the tin and sits on that line and no other. A 3mg tin
   beside a 6mg row is a picture of a product the customer is not buying.

   Coverage is thin and honest about it: twelve brands have art out of roughly
   a hundred and sixty, so every one of these has to look deliberate when
   present and leave no hole when absent. Nothing here reserves space for an
   image that does not exist. */

/* ---- brand card ---- */
/* The grey panel holds the image and nothing else, at one fixed size on every
   card. With the caption inside it the panel took its width from the text, so
   a brand with a long "shown" line got a wide block and one without got a small
   square, three different panel widths in a row. */
.brandcard-shotwrap{
  display:flex; flex-direction:column; align-items:flex-start; gap:6px;
  margin:-4px 0 2px;
}
.brandcard-shot{
  display:block; width:84px; height:84px;
  background:var(--product-ground, #EAE5DD);
  border-radius:calc(var(--r-card) - 4px);
  padding:10px;
}
.brandcard-img{
  display:block; width:100%; height:100%; object-fit:contain;
  mix-blend-mode:multiply;
}
@media (prefers-color-scheme: dark){
  /* multiply over a light ground is the point; it must not follow the page
     into a dark theme or the product turns into a silhouette. */
  .brandcard-shot{ background:#EAE5DD }
}

/* ---- menu brand heading ---- */
.menu-img{
  display:block; width:100%; height:100%; object-fit:contain;
  mix-blend-mode:multiply;
}

/* ---- what the photograph actually shows ----
   Brand art is representative, and on twenty ALP lines one tin quietly implies
   it stands for all of them. Where a photograph has been verified against a
   single SKU, this names it. */
.menu-shownline{ margin:2px 0 0 }
.menu-shown,.brandcard-shotwrap .menu-shown{
  font-family:var(--font-mono); font-size:.58rem; font-weight:400;
  letter-spacing:.1em; text-transform:uppercase; color:var(--muted);
  margin-left:8px; white-space:nowrap;
}
.brandcard-shotwrap .menu-shown{ margin-left:0; font-size:.55rem; white-space:normal }

@media (max-width:560px){
  .brandcard-shot{ width:68px; height:68px }
  /* Keep the two columns on a phone rather than stacking: a 112px picture above
     a run of forty flavour names pushes the names off the first screen. */
  .menu-group.has-art{ grid-template-columns:auto 1fr; gap:0 16px }
  .menu-shot{ width:76px; height:76px; padding:8px }
}
@media (prefers-reduced-motion: reduce){
  .brandcard-img,.menu-img,.menu-var-img{ transition:none }
}

/* 33. THE STARTING-PRICE LEAD ---------------------------------------------
   One figure, stated once, on the page somebody lands on from a pouch-price
   search. It sits under the rule rather than inside the H1 because at 72px in
   a 610px column the headline was already four lines and the price took it to
   six.

   The unit is NOT optional and is not small print: a roll is five tins, and
   $23.95 read as one tin is a customer who feels lied to at the counter. So
   the unit sits on the same baseline block as the figure at a readable size,
   never as a footnote.

   The figure itself is generated by <!--PRICEFROM:--> from prices.json. If it
   is ever hand-typed here, it becomes the one number on the site that an
   import cannot correct. */
.price-lead{
  margin:24px 0 26px; display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 14px;
  max-width:none;
}
.price-lead-fig{
  font-family:var(--font-display); font-weight:800; line-height:1;
  font-size:clamp(2.6rem, 6vw, 3.6rem); color:var(--accent);
  font-variant-numeric:tabular-nums; letter-spacing:-.01em;
}
.price-lead-unit{
  font-size:1rem; line-height:1.4; color:var(--ink-mid); max-width:26ch;
}
@media (max-width:560px){
  .price-lead{ gap:2px 10px }
  .price-lead-unit{ font-size:.94rem }
}

/* 34. TYPOGRAPHIC PRODUCT CARD -------------------------------------------
   A live brand with no photograph. VELO is 11 lines on the pouch wall and has
   no art, because the shot list was written against the brand set this build
   asserted before the POS export arrived (CLIENT-QUESTIONS G6).

   The alternative was putting a different brand's tin on the card, and on a
   21+ nicotine product a picture of a thing the customer is not buying is not
   a cosmetic problem. So the card says the name instead, on the same product
   ground the photographs sit on, and reads as a deliberate second treatment
   rather than a failed first one. Delete this the day the art arrives. */
.pcard-art-type{
  display:flex; align-items:center; justify-content:center;
  background:var(--product-ground); text-align:center; padding:18px;
  /* Square, to match the 480x480 packshots sitting either side of it in the
     same rail. Without this the panel collapsed to its text height and the
     card read as broken rather than as a deliberate alternative. */
  aspect-ratio:1 / 1;
}
.pcard-wordmark{
  font-family:var(--font-display); font-weight:800;
  font-size:clamp(1.5rem, 4vw, 2rem); letter-spacing:.02em;
  color:var(--ink); opacity:.34; text-transform:uppercase;
}

/* 34. PRODUCT CARDS -------------------------------------------------------
   Client call 2026-09-29: the shelf menus lost their menthol / light / king
   style lists. What is left per brand is a picture, the brand name set large,
   and the price. A card is split by package form only where the price differs
   (a Du Ports carton and a Du Ports bag are two cards). Every colour here is an
   existing token: no fifth hue. The photo ground is --product-ground in both
   themes for the reason given at the token. */
.prodgrid{
  display:grid; gap:14px; margin:6px 0 30px;
  grid-template-columns:repeat(auto-fill, minmax(168px, 1fr));
}
.prodgrid[hidden]{ display:none }
.prod{
  display:flex; flex-direction:column; gap:4px;
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--r-card); padding:12px 12px 14px;
}
.prod[hidden]{ display:none }
.prod-shot{
  display:flex; align-items:center; justify-content:center;
  aspect-ratio:1 / 1; margin:0 0 8px;
  background:var(--product-ground); border-radius:calc(var(--r-card) - 4px);
  padding:12px;
}
.prod-img{
  display:block; width:100%; height:100%; object-fit:contain;
  mix-blend-mode:multiply;
}
.prod-shot-type{ padding:14px; text-align:center }
.prod-word{
  font-family:var(--font-display); font-weight:800;
  font-size:clamp(.9rem, 2vw, 1.15rem); line-height:1.15; letter-spacing:.01em;
  color:#080707; opacity:.4; text-transform:uppercase; overflow-wrap:normal;
  word-break:normal; hyphens:manual;
}
.prod-name{
  max-width:none; margin:0;
  font-family:var(--font-display); font-weight:800;
  font-size:1.28rem; line-height:1.15; letter-spacing:-.01em; color:var(--ink);
}
.prod-price{
  max-width:none; margin:0;
  font-family:var(--font-mono); font-size:1.02rem; font-weight:600;
  font-variant-numeric:tabular-nums; color:var(--ink);
}
.prod-unit{
  font-family:var(--font-body, inherit); font-size:.8rem; font-weight:400;
  color:var(--muted); margin-left:2px;
}
.prod-count{ max-width:none; margin:0; font-size:.8rem; color:var(--muted) }
@media (max-width:560px){
  .prodgrid{ grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px }
  .prod-name{ font-size:1.08rem }
}
.prod-video{
  margin-top:6px; align-self:flex-start;
  font-family:var(--font-display); font-weight:700; font-size:.84rem;
  padding:8px 14px; min-height:38px; display:inline-flex; align-items:center;
  border-radius:var(--r-pill); border:1.5px solid var(--accent); color:var(--accent);
  text-decoration:none;
}
.prod-video:hover,.prod-video:focus-visible{ background:var(--accent); color:var(--accent-ink) }

/* 35. REWARDS SIGN-UP FORM ------------------------------------------------
   The client's Twinleaf form in a frame. Tall because the form is: fifteen
   fields on a phone. The link under it is always visible. */
.loyalty-embed{ margin-top:22px }
.loyalty-embed iframe{
  display:block; width:100%; height:1180px; border:1px solid var(--border);
  border-radius:var(--r-card); background:#FCFAF7;
}
@media (min-width:760px){ .loyalty-embed iframe{ height:1020px } }
.loyalty-link{ margin:12px 0 0; font-size:.92rem }
.embed-fallback[hidden]{ display:none }

/* Name panels for products with no photograph. Pinned to --muted's light value
   (#6E5A4C, 4.5:1 on the product ground) rather than a translucent black, so the
   name passes contrast in every world: in the humidor and night scopes the
   panel inherited a light ink and measured 1.02:1. */
.prod-shot-type, .pcard-art-type{ color:#6E5A4C }
.prod-word, .pcard-wordmark{ color:#6E5A4C; opacity:1 }

/* 36. CURRENT PROMOTIONS -------------------------------------------------
   From build/data/promos.json. One card per live promo. */
.promo-grid{
  display:grid; gap:18px; margin-top:20px;
  grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
}
.promo-card{
  display:flex; flex-direction:column; overflow:hidden;
  background:var(--surface); border:1px solid var(--border); border-radius:var(--r-card);
}
.promo-card-img{ display:block; width:100%; height:auto }
.promo-card-body{ padding:18px 20px 20px }
.promo-card h3{ margin:4px 0 10px }
.promo-lines{ margin:0 0 12px; padding-left:1.15em }
.promo-lines li{ margin:4px 0; font-weight:600; color:var(--ink) }

/* 37. LIVE STORE STATUS --------------------------------------------------
   Pill written by shared.js from STORE_HOURS. Colour is never the only signal:
   the words say open or closed. Tokens only. */
.store-status{
  display:inline-flex; align-items:center; gap:6px; flex-wrap:wrap;
  font-size:.82rem; font-weight:600; color:var(--ink); line-height:1.3;
}
.ss-dot{ width:8px; height:8px; border-radius:50%; background:var(--muted); flex:0 0 auto }
.store-status.is-open .ss-dot{ background:var(--accent) }
.ss-label{ font-weight:400; color:var(--ink-mid) }
.ss-detail{ font-weight:400; color:var(--ink-mid) }
.nav-cta .store-status{ display:none }
@media (min-width: 1200px){ .nav-cta .store-status{ display:inline-flex; margin-right:6px } }
.site-footer .store-status{ color:inherit }
.site-footer .ss-detail, .site-footer .ss-label{ color:inherit; opacity:.85 }

/* 38. DEALS BAR ------------------------------------------------------------
   One line under the nicotine warning while a promo in promos.json is live. */
.deals-bar{ background:var(--surface-alt); border-bottom:1px solid var(--border) }
.deals-bar a{
  display:block; padding:9px 0; text-align:center; text-decoration:none;
  font-size:.86rem; color:var(--ink); line-height:1.4;
}
.deals-bar a:hover strong, .deals-bar a:focus-visible strong{ text-decoration:underline }

/* 39. MOBILE ACTION BAR ----------------------------------------------------
   Phones only: call either store, or find one. The age gate (z 9000) sits
   above it; the mobile menu panel (z 590) sits below the header but above it. */
.mobile-bar{ display:none }
@media (max-width: 759px){
  .mobile-bar{
    display:grid; grid-template-columns:1fr 1fr 1fr; position:fixed;
    left:0; right:0; bottom:0; z-index:580;
    background:var(--surface); border-top:1px solid var(--border);
    padding-bottom:env(safe-area-inset-bottom);
    box-shadow:0 -4px 16px rgba(8,7,7,.08);
  }
  .mobile-bar a{
    display:flex; align-items:center; justify-content:center; min-height:52px;
    padding:6px 4px; text-align:center; text-decoration:none;
    font-family:var(--font-display); font-weight:700; font-size:.82rem; color:var(--ink);
  }
  .mobile-bar a + a{ border-left:1px solid var(--border) }
  .mobile-bar a:first-child{ background:var(--accent); color:var(--accent-ink) }
  body{ padding-bottom:calc(52px + env(safe-area-inset-bottom)) }
}

/* 40. SMART 404 ---------------------------------------------------------------
   Skin for /smart-404.js (Website features library). */
.s404-path{
  display:inline-block; max-width:100%; overflow-wrap:anywhere;
  font-family:var(--font-mono); font-size:.9rem; padding:4px 10px;
  background:var(--surface-alt); border-radius:6px; color:var(--ink);
}
.s404-list{ list-style:none; margin:14px 0 0; padding:0; display:grid; gap:10px; max-width:760px }
.s404-hit, .s404-result{
  display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 12px; min-height:44px;
  padding:12px 16px; border:1px solid var(--border); border-radius:var(--r-card);
  background:var(--surface); text-decoration:none; color:var(--ink);
}
.s404-hit:hover, .s404-result:hover{ border-color:var(--accent) }
.s404-hit-title, .s404-result-title{ font-weight:700 }
.s404-hit-path, .s404-result-path{ font-family:var(--font-mono); font-size:.8rem; color:var(--muted) }
.s404-hit-type, .s404-result-type{
  font-family:var(--font-mono); font-size:.68rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--muted); margin-left:auto;
}
.s404-search{ display:flex; gap:10px; flex-wrap:wrap; max-width:520px; margin-top:12px }
.s404-search input{
  flex:1 1 240px; min-height:44px; padding:10px 14px; font:inherit; color:var(--ink);
  background:var(--surface); border:1px solid var(--border); border-radius:var(--r-pill);
}
.s404-search input:focus-visible{ outline:2px solid var(--accent); outline-offset:2px }
[data-s404-suggest][hidden], [data-s404-nomatch][hidden]{ display:none }
.deals-short{ display:none }
@media (max-width: 759px){ .deals-long{ display:none } .deals-short{ display:inline } }

.promo-warning{
  margin:14px 0 0; padding:12px 14px; border:2px solid var(--ink);
  font-size:1rem; font-weight:600; line-height:1.4; color:var(--ink); background:var(--surface);
}

/* 41. CARD LINKS, BRAND FACTS, STOCK SUMMARY (2026-10-20) ----------------- */
.prod-name, .prod-price, .prod-count{ display:block }
a.prod{ text-decoration:none; color:inherit; transition:border-color .15s var(--ease), transform .15s var(--ease) }
a.prod:hover{ border-color:var(--accent); transform:translateY(-2px) }
a.prod:focus-visible{ outline:2px solid var(--accent); outline-offset:2px }
.prod-more{ display:block; margin-top:6px; font-size:.8rem; font-weight:700; color:var(--accent); text-decoration:underline }
@media (prefers-reduced-motion: reduce){ a.prod{ transition:none } a.prod:hover{ transform:none } }
.brand-facts{ display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr)); gap:10px; margin:18px 0 8px }
.brand-facts div{ background:var(--surface); border:1px solid var(--border); border-radius:var(--r-card); padding:12px 14px }
.brand-facts dt{ font-family:var(--font-mono); font-size:.68rem; letter-spacing:.12em; text-transform:uppercase; color:var(--muted) }
.brand-facts dd{ margin:4px 0 0; font-weight:700; color:var(--ink) }
.stock-summary{ margin:14px 0 8px; padding-left:1.1em }
.stock-summary li{ margin:6px 0 }
.fact a{ color:inherit; text-decoration:none }
.fact a:hover, .fact a:focus-visible{ text-decoration:underline }

/* Portrait promo artwork sits beside its text on wider screens. */
@media (min-width: 760px){
  .promo-card-tall{ flex-direction:row; align-items:flex-start }
  .promo-card-tall .promo-card-img{ width:38%; max-width:360px; flex:0 0 auto }
  .promo-card-tall .promo-card-body{ flex:1 1 auto }
}
