/* ═══ BEGIN WP6-FONTS ═══ */
/* SELF-HOSTED FONTS (was fonts.googleapis.com: a render-blocking third-party
   stylesheet, fonts found only after it). The exact latin woff2 files Google
   served (its hinted Windows build), same descriptors and unicode-range, so the
   loaded page renders identically. Every character on the site is inside the
   range and in each cmap (checked). Inter 500 is not declared: nothing uses it.
   SIL Open Font License 1.1 (openfontlicense.org). Archivo: Copyright 2020 The
   Archivo Project Authors. Inter: Copyright 2016 The Inter Project Authors.
   IBM Plex Mono: Copyright 2017 IBM Corp. */
@font-face{font-family:'Archivo';font-style:normal;font-weight:700;font-stretch:125%;font-display:swap;src:url(/assets/fonts/archivo-wdth125-latin-v25.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Archivo';font-style:normal;font-weight:800;font-stretch:125%;font-display:swap;src:url(/assets/fonts/archivo-wdth125-latin-v25.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url(/assets/fonts/inter-latin-v20.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;src:url(/assets/fonts/inter-latin-v20.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/assets/fonts/ibm-plex-mono-400-latin-v20.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/assets/fonts/ibm-plex-mono-500-latin-v20.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
/* METRIC-MATCHED FALLBACKS: what paints until the files arrive. Local fonts
   scaled (size-adjust = web/fallback width over the site's own strings,
   measured) and given the web font's ascent/descent (overrides divided by
   size-adjust), so the swap no longer re-wraps the wordmark or moves the CTAs.
   Arial Black (Windows, macOS), Arial Bold (iOS), Roboto (Android); mono:
   Consolas (Windows, .5498em), Menlo (Apple; SF Mono via ui-monospace is 3%
   wider). A face whose local() is missing is skipped.
   Never used once the web fonts load: they cover every glyph on the site. */
@font-face{font-family:'Archivo Fallback';font-weight:700;src:local('Arial Black'),local('Arial-Black');size-adjust:115.47%;ascent-override:76.04%;descent-override:18.19%;line-gap-override:0%}
@font-face{font-family:'Archivo Fallback';font-weight:800;src:local('Arial Black'),local('Arial-Black');size-adjust:115.73%;ascent-override:75.87%;descent-override:18.15%;line-gap-override:0%}
@font-face{font-family:'Archivo Fallback Bold';font-weight:700;src:local('Arial Bold'),local('Arial-BoldMT');size-adjust:128.14%;ascent-override:68.52%;descent-override:16.39%;line-gap-override:0%}
@font-face{font-family:'Archivo Fallback Bold';font-weight:800;src:local('Arial Bold'),local('Arial-BoldMT');size-adjust:129.85%;ascent-override:67.62%;descent-override:16.17%;line-gap-override:0%}
@font-face{font-family:'Archivo Fallback Roboto';font-weight:700;src:local('Roboto'),local('Roboto-Regular');size-adjust:140.26%;ascent-override:62.60%;descent-override:14.97%;line-gap-override:0%}
@font-face{font-family:'Archivo Fallback Roboto';font-weight:800;src:local('Roboto'),local('Roboto-Regular');size-adjust:140.74%;ascent-override:62.38%;descent-override:14.92%;line-gap-override:0%}
@font-face{font-family:'Inter Fallback';font-weight:400;src:local('Arial'),local('ArialMT');size-adjust:106.92%;ascent-override:90.61%;descent-override:22.56%;line-gap-override:0%}
@font-face{font-family:'Inter Fallback';font-weight:600;src:local('Arial Bold'),local('Arial-BoldMT');size-adjust:100.90%;ascent-override:96.01%;descent-override:23.91%;line-gap-override:0%}
@font-face{font-family:'Inter Fallback Roboto';font-weight:400;src:local('Roboto'),local('Roboto-Regular');size-adjust:107.59%;ascent-override:90.04%;descent-override:22.42%;line-gap-override:0%}
@font-face{font-family:'Inter Fallback Roboto';font-weight:600;src:local('Roboto'),local('Roboto-Regular');size-adjust:107.80%;ascent-override:89.87%;descent-override:22.38%;line-gap-override:0%}
@font-face{font-family:'IBM Plex Mono Fallback';src:local('Consolas');size-adjust:109.13%;ascent-override:93.93%;descent-override:25.20%;line-gap-override:0%}
@font-face{font-family:'IBM Plex Mono Fallback Menlo';src:local('Menlo-Regular'),local('Menlo');size-adjust:99.66%;ascent-override:102.85%;descent-override:27.59%;line-gap-override:0%}
/* The --display/--body/--mono stacks that name these families are
   set in the WP6-PERF block at the end of this file: a :root token
   override has to come after the design system's own :root. */
/* ═══ END WP6-FONTS ═══ */

/* ═══════════════════════════════════════════════════════════════════════
   Infinity Smoke Shop — design system · LIGHT GROUND
   3034 W New Haven Ave, West Melbourne FL · (321) 327-3722

   "A lit gallery corridor." Same building, lights on. One continuous
   unbroken PAPER ground from nav to footer. No alternating section
   backgrounds, no cards on tinted panels, no colour blocking — separation
   still comes from vertical rhythm and single 1px rules. Photography is
   still cut INTO the ground rather than placed on it; the masks now
   dissolve to PAPER instead of to black.

   The palette is the same metal, re-lit. Every hue below is the same warm
   axis sampled from the client's logo (champagne → brass → bronze), value-
   inverted so the metal now reads as dark carved type on paper instead of
   lit type on black. There is still NO green and NO blue in this system,
   focus rings included.

   ⚠️ THIS IS A VALUE INVERSION, NOT A RECOLOUR. Four token MEANINGS flip.
      Read the INVERTED block before touching any selector.
   ═══════════════════════════════════════════════════════════════════════ */

:root{

  /* ══ ⚠️ INVERTED — these four tokens changed MEANING, not just value ══
     They were light type on a black ground. They are now dark type on a
     paper ground. Any selector that puts one of them ON DARK MEDIA (the
     film, the walkthrough, the hero video) now paints dark-on-dark and the
     text disappears silently. Those selectors must move to the --film-*
     family below. The known list is in the FILM block. */

  --champagne:#241C14;     /* ⚠️ WAS #EFE3BC (light). Display type + primary
                              plate fill. Deep carved bronze-black — the
                              champagne hue held, the value flipped.
                              16.39:1 on --ink · 14.78:1 on --ash          */
  --vapour:#45392F;        /* ⚠️ WAS #C7C4BB (light). Body copy. Warm dark
                              taupe — never pure black, same rule as before
                              in reverse. 10.91:1 on --ink · 9.84 on --ash */
  --muted:#6B5D50;         /* ⚠️ WAS #9A948C. Still the smallest TEXT role.
                              6.20:1 on --ink · 5.59:1 on --ash — passes AA
                              at 12–13px on both surfaces.                 */
  --brass:#8A5F2E;         /* ⚠️ WAS #C0A38A (8.33:1 on black). Primary
                              accent: links, eyebrows, the live dot. The
                              heritage brass darkened until it carries text
                              on paper — still unmistakably brass, not blue.
                              5.46:1 on --ink · 4.92:1 on --ash            */

  /* ── the ground ────────────────────────────────────────────────────── */
  --ink:#FFFCF6;           /* ⚠️ NAME IS NOW A MISNOMER, KEPT ON PURPOSE.
                              This is the PAGE GROUND. It was #0B0A09; it is
                              now warm paper-white. The name is kept because
                              all four call sites (body, .nav, .skip colour,
                              .gb ramp) invert correctly on their own — a
                              rename would be four edits for nothing. Use
                              --paper in new code.                          */
  --paper:var(--ink);      /* preferred alias for the ground, going forward */
  --paper-lift:#FFFFFF;    /* pure white: the ONE step brighter than ground,
                              for a raised plate that must out-white paper  */
  --ash:#F5F0E6;           /* ⚠️ WAS #232323. Surfaces, wells, the map
                              backing, the footer slab. 1.11:1 off ground —
                              a whisper, which is the point.                */

  /* ── the metal, re-lit ─────────────────────────────────────────────── */
  --smoke:#9E9182;         /* ⚠️ WAS #6F6F6F. Hairlines, scrims, the footer
                              wordmark. NON-TEXT only — 3.00:1 exactly, which
                              is the non-text floor and nowhere near body AA. */
  --bronze:#86735D;        /* ⚠️ WAS #7A6E65. The metal body: ghost-button
                              borders, icon strokes, the callout spine.
                              4.43:1 — clears the 3:1 graphics floor with
                              room, so 1.5px icon strokes stay legible.     */
  --specular:#8F5516;      /* Focus rings, and the ONE hover fill. It used to
                              be a brighter champagne because hover on black
                              means "hotter"; on paper hover means "deeper",
                              so it is now a burnished copper. Ring: 5.89:1
                              on ground (floor is 3:1, with a 2px paper gap
                              from any plate). As a fill under --ink type:
                              5.89:1.                                       */
  --metal-deep:#E3D8C4;    /* ⚠️ WAS #443E39. Hairline borders, engraved
                              rules. 1.38:1 — deliberately faint, the shadcn
                              border convention, warmed. Restrained borders,
                              not shadows.                                  */

  --void:#0B0A09;          /* ⚠️ RE-SCOPED, NOT INVERTED. This is now the
                              CINEMA MATERIAL and nothing else: the backing
                              behind full-bleed VIDEO (.film, .walk, .hero)
                              where a dark veil is still correct. It is NO
                              LONGER a page surface. Two call sites must
                              move off it: .foot → var(--ash) and .wall →
                              var(--paper). Leave them and you get two black
                              slabs punched through a white page.           */

  /* ── semantic, sampled from shelves.webp ───────────────────────────── */
  --ember:#A8362A;         /* closed / destructive — fills and borders.
                              6.36:1 on ground; --ink type ON it is 6.36:1  */
  --ember-text:#9A3227;    /* destructive as TEXT — 7.17:1                  */
  --amber:#A57840;         /* the one token that did NOT need to move: it
                              was already a mid-value metal. 3.83:1 — fills
                              and borders only, and --champagne type on it
                              is 4.28:1                                     */
  --amber-text:#85601F;    /* ⚠️ WAS #D8A45E. Warning as TEXT — 5.56:1      */

  --rule:var(--metal-deep);

  /* ══ THE FILM — the ONLY light-on-dark tokens left in the system ══════
     The hero, the scroll-scrubbed film and the walkthrough are real footage
     of a real room: dark shelves, lit product. A white scrim over that
     footage would grey out the merchandise the section exists to show, and
     the veils (.film-veil, .walk-veil, .hero-shade) are already hardcoded
     dark. So the DECISION IS: the media stages stay light-type-on-dark-
     scrim; every other pixel on the site is dark-type-on-paper. The stages
     read as lit windows cut into a white gallery wall, which is the same
     idea the dark build had, reversed.

     These four carry the OLD palette's values unchanged, because those
     values were already correct against #0B0A09.

     ⚠️ REWIRE REQUIRED — these selectors currently resolve to the inverted
     dark tokens and will paint dark-on-dark:
       .film-head / .walk-head / .hero .d-xl   → color:var(--film-display)
       .film-beat .lead / .walk-copy .lead     → color:var(--film-body)
       .film-beat .eyebrow / .walk-copy .eyebrow → color:var(--film-accent)
       .btn-sq-ghost{color:…}                  → var(--film-display)
       .hero-pause / .hero-pause:hover         → var(--film-body)/(--film-accent)
       .hero-pause border, .gsurface-on-media  → var(--film-rule)          */
  --film-ground:#0B0A09;   /* the veil colour the scrims already use        */
  --film-display:#EFE3BC;  /* display type over footage — 15.43:1 on veil   */
  --film-body:#DCD8CF;     /* body over footage — 13.91:1 (raised from the
                              old 11.34: a lit room eats scrim headroom)    */
  --film-accent:#D8BC9E;   /* eyebrows over footage — 10.93:1               */
  --film-rule:#4A423B;     /* hairlines over footage — 2.01:1, non-text     */

  /* ══ SHADCN SEMANTIC LAYER ═══════════════════════════════════════════
     shadcn/ui cannot be installed here — no React, no Tailwind — so what is
     adopted is its TOKEN STRUCTURE and its component discipline: a
     background/foreground pair per surface, restrained borders instead of
     shadows, one --radius, a real visible focus ring. Every value below is
     an alias of a brand token above. Nothing new is invented here.

     ⚠️ ONE DELIBERATE DEVIATION: shadcn's --muted is a SURFACE. In this
     stylesheet --muted has been a TEXT colour since day one and is used as
     `color:` in six places. Redefining it to a surface would silently paint
     six pieces of small print in a near-white and delete them from the page.
     So --muted keeps its brand meaning, --muted-foreground aliases it, and
     shadcn's surface role lives at --muted-surface. If you ever want strict
     shadcn naming, that is a six-call-site rename, not a token swap.        */

  --background:var(--ink);              /* #FFFCF6                          */
  --foreground:var(--vapour);           /* #45392F — 10.91:1 · default text */
  --foreground-strong:var(--champagne); /* #241C14 — 16.39:1 · display tier.
                                           shadcn has no heading token; this
                                           brand has a carved tier, so it is
                                           named rather than hardcoded.      */

  --card:var(--ink);                    /* cards sit ON the ground, not on a
                                           tint — the corridor rule survives */
  --card-foreground:var(--vapour);
  --popover:var(--paper-lift);
  --popover-foreground:var(--vapour);

  --muted-surface:var(--ash);           /* #F5F0E6 — shadcn's --muted role   */
  --muted-foreground:var(--muted);      /* #6B5D50 — 6.20:1 on ground,
                                           5.59:1 on --muted-surface         */

  --border:var(--rule);                 /* #E3D8C4 — 1.38:1                  */
  --input:#DCCFB8;                      /* 1.50:1 — one step stronger than
                                           --border so a field edge reads as
                                           an affordance, not a divider      */
  --ring:var(--specular);               /* #8F5516 — 5.89:1 on ground        */

  --primary:var(--champagne);           /* #241C14 the carved plate          */
  --primary-foreground:var(--ink);      /* #FFFCF6 on it — 16.39:1
                                           ⚠️ .btn-solid and .dock-cta
                                           currently hardcode color:var(--primary-foreground),
                                           which on this plate is 1.1:1 —
                                           invisible. Both MUST become
                                           var(--primary-foreground).        */
  --primary-hover:var(--specular);      /* the plate heats to copper; --ink
                                           type on it is 5.89:1              */

  --secondary:var(--ash);               /* quiet surface                     */
  --secondary-foreground:var(--vapour); /* 9.84:1 on it                      */

  --accent:var(--brass);                /* #8A5F2E — the warm metal accent
                                           COLOUR: links, eyebrows, active
                                           underlines. 5.46:1               */
  --accent-foreground:var(--ink);       /* type on a brass fill — 5.46:1     */
  --accent-wash:#F7EFE2;                /* shadcn uses --accent as a hover
                                           SURFACE; that is this: brass at
                                           ~8%. Body on it 9.79:1, brass on
                                           it 4.90:1                         */

  --destructive:var(--ember);           /* #A8362A · --ink on it 6.36:1      */
  --destructive-foreground:var(--ink);
  --warning:var(--amber);               /* #A57840 · --champagne on it 4.28:1*/
  --warning-foreground:var(--champagne);

  /* ── type — unchanged. The serif is the brand. ─────────────────────── */
  --display:'Cinzel','Marcellus SC',Georgia,'Times New Roman',serif;
  --body:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;

  /* ── rhythm — unchanged ────────────────────────────────────────────── */
  --shell:1240px;
  --gutter:clamp(1.25rem,4vw,2.5rem);
  --section:clamp(5rem,10vw,9rem);
  --radius:3px;            /* squared off. this brand is carved, not soft.
                              shadcn's default is 0.5rem; it is NOT adopted.
                              One radius, and it stays 3px.                 */
}

*,*::before,*::after{box-sizing:border-box}

/* The oversized footer wordmark deliberately runs past the viewport edge, so
   something has to clip it. It must NOT be `overflow-x:hidden` on <body>:
   that makes the body its own scroll container, viewport propagation stops,
   and window.scrollTo/scrollY quietly stop working. `clip` on the root clips
   without creating a scroll container; the hidden fallback is for Safari <16,
   where the root is still the scroller so scrolling survives either way. */
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;overflow-x:hidden}
@supports (overflow:clip){html{overflow-x:clip}}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;
  background:var(--ink);
  color:var(--vapour);
  font-family:var(--body);
  font-size:1.0625rem;                 /* 17px */
  line-height:1.65;
  letter-spacing:.003em;
  -webkit-font-smoothing:antialiased;
}

/* height:auto is load-bearing, not tidiness. The width/height ATTRIBUTES are
   presentational hints that set real CSS width and height. Any rule that sets
   only `width` therefore leaves `height: 660px` (or whatever) still in force,
   and the image renders stretched — which is exactly what happened to the
   storefront shot. Aspect-ratio still comes from the attributes, so this keeps
   the layout-shift protection while letting the height follow the width.
   Rules that genuinely want a fixed height (.wall img) win on specificity. */
img,video{max-width:100%;height:auto;display:block}
a{color:var(--brass);text-decoration-color:color-mix(in srgb,var(--brass) 40%,transparent);text-underline-offset:.2em}
a:hover{color:var(--champagne)}

/* No blue anywhere, focus rings included. */
:focus-visible{outline:2px solid var(--specular);outline-offset:2px;border-radius:2px}
:focus:not(:focus-visible){outline:none}

.shell{max-width:var(--shell);margin-inline:auto;padding-inline:var(--gutter)}
.section{padding-block:var(--section)}
.rule-top{border-top:1px solid var(--rule)}

.skip{
  position:absolute;left:-9999px;top:0;z-index:200;
  background:var(--champagne);color:var(--ink);padding:.75rem 1.25rem;font-weight:600;
}
.skip:focus{left:.5rem;top:.5rem}

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

/* ── type roles ────────────────────────────────────────────────────────
   Cinzel is DISPLAY ONLY — never a paragraph, never under 18px, never more
   than about seven words. It has no true lowercase, so it is set uppercase
   and authors type normally. Tracking goes UP as size comes down: the flared
   serifs clot on a dark ground at small sizes. */
.d-xl,.d-l,.d-m,.d-s{
  font-family:var(--display);font-weight:600;
  text-transform:uppercase;color:var(--champagne);
  margin:0;text-wrap:balance;
}
.d-xl{font-size:clamp(2.75rem,7vw,6rem);line-height:.98;letter-spacing:.02em}
.d-l{font-size:clamp(2rem,4.2vw,3.25rem);line-height:1.05;letter-spacing:.03em}
.d-m{font-size:1.5rem;line-height:1.15;letter-spacing:.04em}
.d-s{font-size:1.125rem;line-height:1.2;letter-spacing:.05em}

.eyebrow{
  font-family:var(--mono);font-weight:500;font-size:.75rem;line-height:1;
  letter-spacing:.18em;text-transform:uppercase;color:var(--brass);
  margin:0 0 1.1rem;
}
.lead{font-size:clamp(1.0625rem,1.6vw,1.25rem);line-height:1.55;max-width:34rem;color:var(--vapour)}
.meta{font-family:var(--mono);font-size:.8125rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.prose{max-width:38rem}
.prose p{margin:0 0 1.15em}
.prose p:last-child{margin-bottom:0}

/* ── buttons ───────────────────────────────────────────────────────────
   Square, carved, no gradients and no shadows. The primary is a champagne
   plate with ink type; the ghost is a bronze hairline. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:.6rem;
  font-family:var(--body);font-weight:600;font-size:.9375rem;line-height:1;
  letter-spacing:.08em;text-transform:uppercase;text-decoration:none;
  padding:1.0625rem 1.75rem;border-radius:var(--radius);
  border:1px solid transparent;cursor:pointer;
  transition:background .22s ease,color .22s ease,border-color .22s ease;
}

.btn-solid{background:var(--champagne);color:var(--primary-foreground);border-color:var(--champagne)}
.btn-solid:hover{background:var(--specular);color:var(--primary-foreground)}
.btn-ghost{background:transparent;color:var(--vapour);border-color:var(--bronze)}
.btn-ghost:hover{color:var(--champagne);border-color:var(--brass);background:rgba(192,163,138,.08)}
.btn-sm{padding:.75rem 1.1rem;font-size:.8125rem}

/* ═══ utility bar + nav ════════════════════════════════════════════════ */
/* The .util bar (top strip with the live dot, hours, address and phone)
   was REMOVED at the client's request - "remove the very top header,
   there's a circle dot thing on the left side". The address and phone
   it carried still appear in the footer and in the nav Call button, so
   no NAP signal was lost. */

.nav{position:sticky;top:0;z-index:60;background:var(--ink);border-bottom:1px solid transparent;transition:border-color .3s ease,background .3s ease}
.nav.is-stuck{background:rgba(255,252,246,.94);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom-color:var(--rule)}
.nav-in{
  max-width:var(--shell);margin-inline:auto;padding:.9rem var(--gutter);
  display:flex;align-items:center;gap:1.5rem;
}
/* Mark as image, wordmark as live Cinzel — the baked lockup was unreadable
   at nav size and 120 KB heavier than the type it was imitating. */
.brand{display:flex;align-items:center;gap:.7rem;flex:0 0 auto;text-decoration:none}
.brand .mark{width:clamp(46px,7vw,60px);height:auto;flex:0 0 auto;overflow:visible}
.brand-name{
  font-family:var(--display);font-weight:600;text-transform:uppercase;
  color:var(--champagne);line-height:1;letter-spacing:.1em;
  font-size:clamp(.9375rem,1.5vw,1.125rem);
}
.brand-name span{
  display:block;margin-top:.32em;
  font-family:var(--mono);font-weight:400;font-size:.5625rem;
  letter-spacing:.22em;color:var(--brass);
}
.brand:hover .brand-name{color:var(--specular)}
.nav-links{display:flex;gap:1.75rem;margin-left:auto}
.nav-links a{
  font-weight:500;font-size:.875rem;line-height:1;letter-spacing:.10em;
  text-transform:uppercase;color:var(--vapour);text-decoration:none;
  padding:.5rem 0;border-bottom:1px solid transparent;
}
.nav-links a:hover,.nav-links a[aria-current="page"]{color:var(--champagne);border-bottom-color:var(--brass)}
.nav .btn{flex:0 0 auto}
.nav-toggle{display:none}

/* ═══ hero ═════════════════════════════════════════════════════════════ */
.hero{position:relative;min-height:min(82vh,760px);display:flex;align-items:flex-end;overflow:hidden;background:var(--void)}
.hero-media{position:absolute;inset:0;z-index:0}
.hero-media video,.hero-media img{width:100%;height:100%;object-fit:cover}
/* The shade does two jobs: it holds the type's contrast, and it stops the
   colourful product wall from fighting the champagne headline. */
.hero-shade{
  position:absolute;inset:0;z-index:1;
  background:
    linear-gradient(90deg,rgb(11 10 9/.82) 0%,rgb(11 10 9/.42) 42%,rgb(11 10 9/.10) 70%,rgb(11 10 9/.3) 100%),
    linear-gradient(180deg,rgb(11 10 9/.6) 0%,transparent 32%,rgb(11 10 9/.88) 100%);
}
.hero-inner{position:relative;z-index:2;width:100%;max-width:var(--shell);margin-inline:auto;padding:clamp(4rem,12vw,7rem) var(--gutter) clamp(3.5rem,8vw,5.5rem)}
.hero-copy{max-width:min(46rem,80%)}
.hero .lead{margin:1.5rem 0 2.25rem}
/* The mascot's baseline is the top edge of this row, and he is ~130px tall,
   so the row needs that much clear air above it or he walks through the copy. */
/* The mascot stands on the top edge of this row, so the row's clearance is
   his rendered height (cell aspect 329/264) plus a little air. Derived, not
   guessed — otherwise resizing him puts him back through the paragraph. */
.hero-cta{
  display:flex;gap:.75rem;flex-wrap:wrap;position:relative;
  margin-top:calc(var(--vc) * 329 / 264 + 20px);
}
.hero-pause[hidden]{display:none}
.hero-pause{
  position:absolute;right:var(--gutter);bottom:var(--gutter);z-index:3;
  width:40px;height:40px;display:grid;place-items:center;
  background:rgba(11,10,9,.6);border:1px solid var(--metal-deep);border-radius:var(--radius);
  color:var(--vapour);cursor:pointer;
}
.hero-pause:hover{color:var(--champagne);border-color:var(--brass)}
.hero-pause svg{width:14px;height:14px;fill:currentColor}
.hero-pause .i-play{display:none}
.hero-pause[aria-pressed="true"] .i-pause{display:none}
.hero-pause[aria-pressed="true"] .i-play{display:block}

/* ═══ the wall ═════════════════════════════════════════════════════════
   The payoff band. One horizontal slice of the real store photo, masked to
   paper on both sides so it has no rectangle. Does exactly one job: convey
   abundance in half a second. */
.wall{position:relative;overflow:hidden;background:var(--paper);min-height:220px}
.wall img{
  width:100%;height:clamp(220px,34vw,340px);object-fit:cover;opacity:.62;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 26%,#000 74%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 26%,#000 74%,transparent);
}
/* LIGHT SCRIM + DARK TYPE — chosen deliberately, not the film's dark scrim.
   The photo is already cut INTO paper: .wall img sits at .62 opacity on
   --paper and is masked to transparent at both ends, so the far left of the
   band already IS paper. A paper veil that holds across the copy column and
   dissolves to the right continues that edge; the old rgba(0,0,0,.96) veil
   punched a black slab through the page and left --champagne/--muted type
   (both INVERTED to dark) sitting on it at 1.14:1 and 2.61:1.
   ⚠️ So the type here uses the PAPER family. Do NOT give it --film-*: those
   are light and would vanish on this veil — the same mismatch, reversed.
   The first declaration is the no-color-mix fallback (paper = 255 252 246). */
.wall-copy{
  position:absolute;inset:0;z-index:2;display:flex;align-items:center;
  max-width:var(--shell);margin-inline:auto;padding-inline:var(--gutter);
  background:linear-gradient(90deg,rgb(255 252 246/.95) 0%,rgb(255 252 246/.88) 34%,rgb(255 252 246/.5) 58%,rgb(255 252 246/0) 82%);
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--paper) 95%,transparent) 0%,
    color-mix(in srgb,var(--paper) 88%,transparent) 34%,
    color-mix(in srgb,var(--paper) 50%,transparent) 58%,
    transparent 82%);
}
.wall-copy > div{max-width:30rem}
.wall-copy .d-l{color:var(--foreground-strong)}
.wall-copy .meta{color:var(--foreground)}   /* one step darker than .meta's
                                               --muted: it sits over a photo */
/* A soft paper halo, not a drop shadow: it lifts the letterforms off any
   bright product label the veil has thinned over, and is invisible on paper. */
.wall-copy .d-l,
.wall-copy .meta{
  text-shadow:0 0 1px color-mix(in srgb,var(--paper) 80%,transparent),
              0 0 16px color-mix(in srgb,var(--paper) 90%,transparent);
}

/* ═══ category tiles ═══════════════════════════════════════════════════ */
.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(1rem,2vw,1.5rem)}
.tile{
  background:transparent;padding:1.75rem 1.5rem 1.5rem;
  display:flex;flex-direction:column;gap:.6rem;min-height:11rem;
}
@media (hover:hover) and (pointer:fine){ .tile:hover .d-s{color:var(--specular)} }
.tile-icon{position:relative;z-index:2;width:28px;height:28px;stroke:var(--bronze);stroke-width:1.5;fill:none;stroke-linecap:round;stroke-linejoin:round}
.tile:hover .tile-icon{stroke:var(--brass)}
.tile p{position:relative;z-index:2;margin:0;font-size:.9375rem;line-height:1.55;color:var(--vapour)}
.tile .d-s{transition:color .25s ease}

/* ═══ featured piece — asymmetric editorial split ══════════════════════ */
.feature{display:grid;grid-template-columns:5fr 7fr;gap:clamp(2rem,6vw,4.5rem);align-items:center}
.feature-img{position:relative}
.feature-img img{
  width:100%;height:auto;border-radius:var(--radius);
  -webkit-mask-image:radial-gradient(72% 62% at 50% 46%,#000 42%,transparent 100%);
  mask-image:radial-gradient(72% 62% at 50% 46%,#000 42%,transparent 100%);
}

/* ═══ three-column "why" ═══════════════════════════════════════════════ */
.cols3{display:grid;grid-template-columns:repeat(3,1fr);gap:0}
.cols3 > div{padding:0 clamp(1.25rem,3vw,2.25rem)}
.cols3 > div + div{border-left:1px solid var(--rule)}
.cols3 > div:first-child{padding-left:0}
.cols3 > div:last-child{padding-right:0}
.cols3 svg{width:28px;height:28px;stroke:var(--bronze);stroke-width:1.5;fill:none;stroke-linecap:round;stroke-linejoin:round;margin-bottom:1rem}
.cols3 h3{margin:0 0 .6rem}
.cols3 p{margin:0;font-size:.9375rem;line-height:1.6}

/* ═══ visit / hours ════════════════════════════════════════════════════ */
.visit{display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,6vw,4rem);align-items:start}
.hours{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:.9375rem;font-variant-numeric:tabular-nums;margin:1.5rem 0}
.hours th,.hours td{text-align:left;padding:.6rem 0;border-bottom:1px solid var(--rule)}
.hours th{font-weight:400;color:var(--muted);letter-spacing:.06em;text-transform:uppercase;font-size:.8125rem}
.hours td{color:var(--vapour)}
.hours tr[data-today] th,.hours tr[data-today] td{color:var(--champagne)}
.addr{font-style:normal;line-height:1.8}
.addr a{text-decoration:none}

/* A default light Google map on this page is a hole punched in the design.
   The keyless embed is filtered to a dark palette instead of paying 400KB of
   Maps JS for a styled one. */
.visit-media{display:flex;flex-direction:column;gap:1rem}
/* Cut into the ground rather than sat on it: the bottom edge dissolves so the
   photo has no rectangle, same treatment as the rest of the photography. */
.storefront{
  width:100%;border-radius:var(--radius);
  -webkit-mask-image:linear-gradient(180deg,#000 62%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 62%,transparent 100%);
}
.map{position:relative;border:1px solid var(--rule);border-radius:var(--radius);overflow:hidden;aspect-ratio:4/3;background:var(--ash)}
.map iframe{width:100%;height:100%;border:0}
/* The invert/hue-rotate filter was deleted, not retuned. It existed only to
   force a keyless Google embed to match a black page. The page is paper now,
   so the filter renders the map as a dark hole - and its hue-rotate injects
   blue, which this system bans outright. .map{background:var(--ash)} carries it. */

/* ═══ FAQ ══════════════════════════════════════════════════════════════ */
.faq-list{max-width:46rem;margin:0}
.faq-item{border-bottom:1px solid var(--rule);padding:1.6rem 0}
.faq-item:first-child{border-top:1px solid var(--rule)}
.faq-item h3{margin:0 0 .7rem;font-family:var(--display);font-weight:600;text-transform:uppercase;letter-spacing:.04em;font-size:1.125rem;color:var(--champagne)}
.faq-item p{margin:0 0 .8em;font-size:1rem}
.faq-item p:last-child{margin-bottom:0}

.callout{
  border:1px solid var(--rule);border-left:2px solid var(--bronze);
  padding:1.25rem 1.5rem;margin:2rem 0;background:var(--accent-wash);
  font-size:.9375rem;line-height:1.6;
}
.callout strong{color:var(--champagne)}

/* ═══ footer ═══════════════════════════════════════════════════════════ */
.foot{background:var(--ash);border-top:1px solid var(--rule);position:relative;overflow:hidden}
.foot-mark{
  position:absolute;left:50%;bottom:-.18em;transform:translateX(-50%);
  font-family:var(--display);font-weight:700;font-size:clamp(3rem,13vw,9rem);
  line-height:.8;letter-spacing:.06em;color:var(--smoke);
  /* was --metal-deep at .5 — on the light footer that is #E3D8C4 over
     #F5F0E6 and effectively invisible. --smoke at .22 is the same
     whisper, re-struck for a paper ground. */
  white-space:nowrap;pointer-events:none;opacity:.22;user-select:none;
}
.foot-in{position:relative;z-index:1;padding-block:clamp(3rem,7vw,4.5rem) clamp(6rem,14vw,9rem)}
.foot-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem clamp(2rem,6vw,4rem)}
.foot h2{font-family:var(--display);font-weight:600;font-size:.9375rem;letter-spacing:.12em;text-transform:uppercase;color:var(--champagne);margin:0 0 1rem}
.foot ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.foot a{color:var(--vapour);text-decoration:none;font-size:.9375rem}
.foot a:hover{color:var(--champagne)}
.foot-hours{font-family:var(--mono);font-size:.875rem;font-variant-numeric:tabular-nums;line-height:1.9;color:var(--vapour);margin:0}
.foot-legal{
  margin-top:clamp(2.5rem,6vw,3.5rem);padding-top:1.75rem;border-top:1px solid var(--rule);
  font-size:.8125rem;line-height:1.7;color:var(--muted);max-width:62rem;
}
.foot-legal p{margin:0 0 .75em}
.foot-legal p:last-child{margin-bottom:0}
.foot-statute{font-family:var(--mono);font-size:.6875rem;letter-spacing:.05em;text-transform:uppercase;line-height:1.7}
.foot-copy{margin-top:1.5rem;font-family:var(--mono);font-size:.6875rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}

/* ═══════════════════════════════════════════════════════════════════════
   MASCOT RIG
   Ported from the Groundworks "est-guy" rig, which drives a sprite-sheet
   character that pops in, walks the CTA, lands holding a sign, then hops to
   the phone button and dances.

   ⚠️ A cartoon character in nicotine marketing is a youth-appeal exposure.
   It ships on one condition (README, Compliance): it renders only after the
   visitor confirms 21+. The hard interstitial was removed at the client's
   request and the non-modal #agegate card replaced it. site.js starts this
   rig only from that card's confirmation (fresh or remembered) and sets
   html.age21-ok; the CONSENT FIRST rule at the end of this file keeps .mascot
   and .dock display:none without it. VAPE_MASCOT in site.js is the kill
   switch.

   Sprite URLs come from --mascot-walk / --mascot-idle, written onto
   .mascot-btn by build.py with ?v=ASSET_V. The url() fallbacks below are
   unversioned on purpose: a hardcoded ?v= here is the stale-cache bug.

   Sheet geometry (see build_sheets.py):
     vape-walk.png  5 cells, cell 264x329  →  aspect 329/264
     vape-idle.png  2 cells, cell 239x270  →  aspect 270/239
   --vc is the DISPLAYED cell width; every other number derives from it, so
   the sheet can be re-rendered at any resolution without touching the CSS.
   ═══════════════════════════════════════════════════════════════════════ */
.mascot-btn{--vc:100px;position:relative}      /* the ONE mascot number     */
.mascot{
  position:absolute;left:0;bottom:calc(100% - 6px);z-index:3;
  width:var(--vc);height:calc(var(--vc)*329/264);
  opacity:0;pointer-events:none;
  transform:translateX(-6%);
  transform-origin:50% 100%;
}
.mascot::before{
  content:"";position:absolute;inset:0;
  background:var(--mascot-walk, none) no-repeat 0 0;
  background-size:calc(var(--vc)*5) calc(var(--vc)*329/264);
  transform-origin:50% 100%;                   /* lean and bob pivot at the feet */
}


/* walk-on: pop up, stride across, land on the sign pose */
.mascot-btn.play .mascot{
  animation:vapePop .45s ease-out both,
            vapeMove 2.4s .45s ease-in-out forwards;
}
.mascot-btn.play .mascot::before{
  animation:vapeStep .48s .45s linear 5 both,
            vapeBob  .24s .45s linear 10 both,
            vapeLand .45s 2.85s ease-out forwards;
}
.mascot-btn.still .mascot{opacity:1;transform:translateX(56%)}
.mascot-btn.still .mascot::before{background-position-x:calc(var(--vc)*-4)}

@keyframes vapePop{
  0%{opacity:0;transform:translateX(-6%) translateY(46%) scale(.6)}
  70%{opacity:1;transform:translateX(-6%) translateY(-6%) scale(1.04)}
  100%{opacity:1;transform:translateX(-6%) scale(1)}
}
@keyframes vapeMove{from{transform:translateX(-6%)}to{transform:translateX(56%)}}
/* Four contact/passing poses held in quarters — a real leg cycle, not a
   crossfade. steps() would land between cells; explicit quarters do not. */
@keyframes vapeStep{
  0%,24.9%{background-position-x:0}
  25%,49.9%{background-position-x:calc(var(--vc)*-1)}
  50%,74.9%{background-position-x:calc(var(--vc)*-2)}
  75%,100%{background-position-x:calc(var(--vc)*-3)}
}
@keyframes vapeBob{
  0%,100%{transform:translateY(0) rotate(4deg)}
  50%{transform:translateY(-5%) rotate(2.5deg)}
}
@keyframes vapeLand{
  0%{background-position-x:calc(var(--vc)*-4);transform:translateY(-8%) rotate(3deg) scale(1.05)}
  60%{transform:translateY(2%) rotate(0deg) scale(.99)}
  100%{background-position-x:calc(var(--vc)*-4);transform:translateY(0) rotate(0deg) scale(1);opacity:1}
}

/* idle show: hop across to the phone button, then dance with the sign.
   --hx/--hy are measured in JS from the phone button's real position, so one
   animation works whether the CTAs are side by side or stacked on mobile. */
.mascot-btn.hopped .mascot{transform:translateX(56%) translate(var(--hx,0),var(--hy,0));opacity:1}
.mascot-btn.hopped .mascot::before{background-position-x:calc(var(--vc)*-4)}
.mascot-btn.hop .mascot{animation:vapeHop .7s cubic-bezier(.3,.7,.4,1) forwards}
.mascot-btn.hop .mascot::before{background-position-x:calc(var(--vc)*-4)}
@keyframes vapeHop{
  0%{transform:translateX(56%)}
  45%{transform:translateX(56%) translate(calc(var(--hx)*.5),calc(var(--hy)*.5 - 52px)) rotate(-5deg)}
  82%{transform:translateX(56%) translate(var(--hx),var(--hy)) scale(1.06,.9)}
  100%{transform:translateX(56%) translate(var(--hx),var(--hy)) scale(1)}
}
.mascot-btn.dance .mascot::before,.mascot-btn.lift .mascot::before{
  /* Derived, not chosen: vi = vc * (329/264) / (270/239) = vc * 1.10312, so
     the idle silhouette is exactly as tall on screen as the landing pose it
     swaps from. Pick this by eye and the character resizes mid-show. */
  --vi:calc(var(--vc) * 1.10312);
  opacity:1;
  width:var(--vi);height:calc(var(--vi)*270/239);
  background-image:var(--mascot-idle, none);
  background-size:calc(var(--vi)*2) calc(var(--vi)*270/239);
}
.mascot-btn.dance .mascot::before{animation:vapeDanceFrames .5s steps(1,end) 6 both}
.mascot-btn.dance .mascot{animation:vapeDanceMove 1s ease-in-out 3 both}
.mascot-btn.lift .mascot::before{animation:vapeDanceFrames 1.1s steps(1,end) infinite}
/* .lift MUST pin the outer box explicitly. Moving the sprite animations onto
   ::before left .mascot with no rule of its own for this state, so when
   .dance was swapped for .lift the element fell back to the .play rule — and
   because that changes animation-name, the walk-on RESTARTED. The character
   teleported back to the left edge and walked across again, on a loop,
   forever. Verified by sampling the transform through the sequence: it
   dropped from 48px to 1.5px at the dance→lift handoff. */
.mascot-btn.lift .mascot{
  animation:none;
  transform:translateX(56%) translate(var(--hx,0),var(--hy,0));
}
@keyframes vapeDanceFrames{
  0%,49.9%{background-position-x:0}
  50%,100%{background-position-x:calc(var(--vi)*-1)}
}
@keyframes vapeDanceMove{
  0%,100%{transform:translateX(56%) translate(var(--hx),var(--hy)) rotate(0)}
  25%{transform:translateX(56%) translate(calc(var(--hx) - 12px),var(--hy)) rotate(-4deg)}
  75%{transform:translateX(56%) translate(calc(var(--hx) + 12px),var(--hy)) rotate(4deg)}
}

/* corner dock: once the hero CTA scrolls away he drops to the bottom-left
   and becomes a tap-to-call target. */
.dock{
  position:fixed;left:clamp(.75rem,3vw,1.75rem);bottom:clamp(.75rem,3vw,1.5rem);
  z-index:55;display:flex;flex-direction:column;align-items:center;gap:.4rem;
  text-decoration:none;
}
.dock[hidden]{display:none}
.dock img{width:clamp(88px,13vw,124px);height:auto;filter:drop-shadow(0 12px 20px rgba(0,0,0,.6))}
.dock .dock-cta{
  font-family:var(--mono);font-size:.6875rem;letter-spacing:.1em;text-transform:uppercase;
  background:var(--champagne);color:var(--primary-foreground);padding:.5rem .75rem;border-radius:var(--radius);font-weight:600;
}
.dock:hover .dock-cta{background:var(--specular)}
.dock.drop{animation:dockDrop .55s cubic-bezier(.3,.8,.4,1) both}
@keyframes dockDrop{
  0%{opacity:0;transform:translateY(-34px) scale(.9)}
  70%{opacity:1;transform:translateY(4px) scale(1.02)}
  100%{opacity:1;transform:none}
}

@media (prefers-reduced-motion:reduce){
  .mascot-btn.play .mascot,.mascot-btn.hop .mascot,
  .mascot-btn.dance .mascot,.mascot-btn.lift .mascot,
  .dock.drop{animation:none}
  .mascot-btn.play .mascot{opacity:1;transform:translateX(56%)}
  .mascot-btn.play .mascot::before{background-position-x:calc(var(--vc)*-4)}
  .tile:hover{transform:none}
  *{transition-duration:.01ms !important}
}

/* ═══ responsive ═══════════════════════════════════════════════════════ */
@media (max-width:900px){
  .tiles{grid-template-columns:repeat(2,1fr)}
  .feature{grid-template-columns:1fr;gap:2rem}
  .visit{grid-template-columns:1fr}
  .cols3{grid-template-columns:1fr;gap:2rem}
  .cols3 > div{padding:0}
  .cols3 > div + div{border-left:0;border-top:1px solid var(--rule);padding-top:2rem}
  .foot-cols{grid-template-columns:1fr 1fr}
  .hero-copy{max-width:100%}
}

@media (max-width:640px){
  .tiles{grid-template-columns:1fr}
  .foot-cols{grid-template-columns:1fr}
  .util-in{font-size:.6875rem;gap:.5rem}
  .util-right{margin-left:0;width:100%;justify-content:space-between}

  /* Nav collapses to brand + call. The four links move to a scrollable strip
     beneath so no interior page is more than one tap away — a burger menu for
     four links is a tap and a modal for nothing. */
  .nav-in{flex-wrap:wrap;gap:.75rem 1rem}
  .nav .btn{margin-left:auto}
  .nav-links{
    order:3;width:100%;margin-left:0;gap:1.25rem;
    overflow-x:auto;scrollbar-width:none;
    padding-bottom:.15rem;
  }
  .nav-links::-webkit-scrollbar{display:none}
  .nav-links a{flex:0 0 auto;font-size:.8125rem}

  .hero{min-height:auto}
  .hero-inner{padding-top:clamp(3rem,18vw,4rem)}
  .hero-cta .btn{width:100%}
  .hero-pause{right:.75rem;bottom:.75rem}
  .mascot-btn{--vc:74px}
  /* Portrait: the copy is centred over the whole strip, so the veil is a
     vertical paper ramp — same light-scrim/dark-type decision as desktop. */
  .wall-copy{
    background:linear-gradient(180deg,rgb(255 252 246/.72),rgb(255 252 246/.9));
    background:linear-gradient(180deg,color-mix(in srgb,var(--paper) 72%,transparent),color-mix(in srgb,var(--paper) 90%,transparent));
  }
}

@media (max-width:400px){
  .mascot-btn{--vc:62px}
}

/* ═══════════════════════════════════════════════════════════════════════
   REACT BITS EFFECTS — vanilla ports
   Electric Border · Gradual Blur · Glass Surface · Glass Icons · Elastic Mesh

   Tier comes from fx.js as [data-fx="full|lite|still"] on <html>. Every
   effect below is layered ON TOP of a finished fallback, so no guard can
   ever degrade an element to nothing.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── ELECTRIC BORDER ──────────────────────────────────────────────────
   The canvas draws the crawling filament; these layers are the bloom
   around it. The bloom is the component's real cost — a gaussian needs
   ~3x its radius in texture margin on every side and stays a promoted
   compositor layer for the element's whole life — so it is capped here
   and cut roughly in half under 700px. */
[data-fx-electric]{position:relative;isolation:isolate}
.eb-canvas-wrap{position:absolute;pointer-events:none;z-index:2;overflow:visible}
.eb-canvas-wrap canvas{display:block}
/* The resting state, always painted. This is what reduced-motion, Save-Data,
   a lost canvas context and every <480px screen get — a deliberately lit
   edge that reads as the effect at rest, not as a missing feature. */
[data-fx-electric]::after{
  content:"";position:absolute;inset:-1px;border-radius:inherit;z-index:1;
  pointer-events:none;
  border:1px solid rgba(192,163,138,.55);
  box-shadow:0 0 18px -6px rgba(192,163,138,.5), inset 0 0 12px -8px rgba(239,227,188,.6);
}
@supports (border-color: color-mix(in srgb, #fff 50%, transparent)){
  [data-fx-electric]::after{border-color:color-mix(in srgb,var(--brass) 55%,transparent)}
}
@media (max-width:700px){
  [data-fx-electric]::after{box-shadow:0 0 12px -6px rgba(192,163,138,.45)}
}
/* No blur support at all: a crisp brass hairline, the same rule language as
   .rule-top and .tiles. Indistinguishable from an intentional choice. */
@supports not (filter: blur(3px)){
  [data-fx-electric]::after{box-shadow:none;border-color:var(--brass)}
  .eb-canvas-wrap{display:none}
}

/* ── GRADUAL BLUR ─────────────────────────────────────────────────────
   The tint ramp is painted unconditionally and is never removed by any
   code path; fx.js only ever ADDS blur strips on top of it. React Bits'
   own fallback is rgba(0,0,0,.3) at 50% opacity, which on this page's
   #0B0A09 ground is black on black — an invisible element. */
.gb{
  position:absolute;left:0;right:0;z-index:1;pointer-events:none;
  height:var(--gb-h,8rem);
  background:linear-gradient(to bottom,transparent,var(--ink) var(--gb-stop,82%));
}
.gb-bottom{bottom:0}
.gb-l{position:absolute;inset:0;pointer-events:none}
@media (max-width:900px){.gb{--gb-h:6.5rem;--gb-stop:78%}}
@media (max-width:640px){.gb{--gb-h:5rem;--gb-stop:76%}}
@media (prefers-reduced-transparency: reduce){
  .gb-l{display:none}
  .gb{--gb-stop:62%}
}

/* ── GLASS SURFACE ──────────────────────────────────────────────────
   SHIPPING TIER 3 ONLY — the light glass plate. The refraction and the
   frosted tier are deliberately NOT here, and this is the reasoning:

   * `backdrop-filter: url(#filter)` is Chromium-only. Every browser on iOS
     is WebKit, so roughly half this shop's visitors could never see the
     effect by construction.
   * The only placements where refraction shows anything on this page are
     over .hero-media, .wall and .storefront. Two of those carry mask-image,
     which breaks the backdrop root. The third is a playing video, so the
     blur would re-run at 30fps forever on top of decode.
   * Backdrop blur on this page is reserved for glass controls with motion
     behind them — the two landing CTAs and the header CALL over the
     scrubbing film, and the dock chip — plus the stuck nav. This plate
     sits on still paper, where a blur has nothing to act on.
   * And what iOS would actually get — a 14px blur over flat --ink — is
     indistinguishable from this plate. Blurring a flat ground returns a
     flat ground.

   So the plate is not a fallback here. It is the finished look, at zero
   GPU cost. */
/* ⚠️ LIGHT GROUND: the smoked plate was #141311→#0F0E0C under a #000 drop
   shadow, and every token inside it (--champagne, --vapour, --brass, --muted)
   is INVERTED dark type — measured 1.11:1 on "Find us". It is now light
   liquid glass struck from the paper palette: a white specular pool top-left,
   a pane that warms from --paper-lift through --accent-wash to --ash, a fine
   --border rim with a white inner hairline just inside it (the glass edge),
   and a long, low, warm shadow instead of a black one. Still zero GPU cost.
   Darkest point is --ash at the foot: --brass on it is 4.92:1, --muted 5.59. */
.gsurface{
  position:relative;border-radius:var(--radius);
  background:
    radial-gradient(130% 55% at 12% 0%,var(--paper-lift) 0%,transparent 62%),
    linear-gradient(162deg,var(--paper-lift) 0%,var(--accent-wash) 58%,var(--ash) 100%);
  border:1px solid var(--border);
  box-shadow:inset 0 1px 0 var(--paper-lift),
             inset 0 0 0 1px color-mix(in srgb,var(--paper-lift) 55%,transparent),
             inset 0 -1px 0 color-mix(in srgb,var(--bronze) 16%,transparent),
             0 1px 2px color-mix(in srgb,var(--champagne) 6%,transparent),
             0 22px 44px -26px color-mix(in srgb,var(--champagne) 30%,transparent);
}
@media (hover:hover) and (pointer:fine){
  .gsurface:hover{border-color:color-mix(in srgb,var(--bronze) 45%,transparent)}
}

/* ── GLASS ICONS ──────────────────────────────────────────────────────
   Two stacked cards: a tinted plate rotated behind a glass face.

   Two upstream bugs are fixed here rather than ported:
   1. React Bits hides the label at opacity:0 and reveals it on hover, so on
      every phone and tablet the component is unlabelled glyphs with no way
      to discover what they do. The reveal is gated to fine pointers; touch
      gets permanent labels.
   2. It puts backdrop-filter inside `transform-style: preserve-3d` and then
      animates along Z. That is a genuinely under-tested WebKit path whose
      symptoms are an opaque grey block or flicker. No preserve-3d here —
      the depth is done with 2D rotation and shadow, which cannot break. */
.gi{
  --gi-unit:1rem;
  position:relative;display:block;
  padding:calc(var(--gi-unit)*1.6) calc(var(--gi-unit)*1.4);
  text-decoration:none;color:inherit;isolation:isolate;
  border-radius:calc(var(--gi-unit)*.55);
}
/* the tinted plate behind the glass — the "second card" */
.gi::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  /* warm bronze→brass tint; the old second stop was the dark-build
     #443E39, which on paper read as a muddy grey card */
  background:linear-gradient(150deg,color-mix(in srgb,var(--bronze) 26%,transparent),color-mix(in srgb,var(--brass) 8%,transparent));
  transform:rotate(-2.2deg) scale(.965);
  transition:transform .38s cubic-bezier(.2,.7,.2,1),background .38s ease;
}
.gi-face{
  position:absolute;inset:0;z-index:1;border-radius:inherit;pointer-events:none;
  /* Light glass: mostly-white pane thinning in the middle so the tinted
     plate reads THROUGH it, a white specular top edge + inner hairline, and
     a soft warm lift. The old face ended in rgba(0,0,0,.18) and carried a
     champagne specular built for black — grey on paper. */
  background:linear-gradient(160deg,color-mix(in srgb,var(--paper-lift) 94%,transparent),color-mix(in srgb,var(--paper-lift) 72%,transparent) 46%,color-mix(in srgb,var(--accent-wash) 86%,transparent));
  border:1px solid color-mix(in srgb,var(--bronze) 26%,transparent);
  box-shadow:inset 0 1px 0 var(--paper-lift),
             inset 0 0 0 1px color-mix(in srgb,var(--paper-lift) 45%,transparent),
             0 12px 26px -18px color-mix(in srgb,var(--champagne) 34%,transparent);
  transition:transform .38s cubic-bezier(.2,.7,.2,1),border-color .38s ease;
}
/* The blur is opt-in on top of a gradient that is already complete. Over a
   flat ground with a flat plate behind it, blurring the backdrop returns the
   backdrop — the fallback is not a compromise here, it is the same picture. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
  html[data-fx="full"] .gi-live .gi-face{
    -webkit-backdrop-filter:blur(.6em);backdrop-filter:blur(.6em);
  }
}
.gi .d-s{position:relative;z-index:2}
.gi p{position:relative;z-index:2}
@media (hover:hover) and (pointer:fine){
  .gi:hover::before{transform:rotate(-4.5deg) scale(.995);
    background:linear-gradient(150deg,color-mix(in srgb,var(--bronze) 40%,transparent),color-mix(in srgb,var(--brass) 14%,transparent))}
  .gi:hover .gi-face{transform:translateY(-4px);border-color:color-mix(in srgb,var(--bronze) 50%,transparent)}
}
/* -webkit-tap-highlight-color is off site-wide, so touch needs its own press
   affordance or the tiles feel dead. Transform-only: pure compositor work,
   no repaint, no backdrop resample. */
.gi:active .gi-face{transform:scale(.97)}
.gi:active::before{transform:rotate(-1.2deg) scale(.945)}
@media (max-width:640px){.gi{--gi-unit:.9rem}}
@media (max-width:400px){.gi{--gi-unit:.8rem}}
/* Reduced motion must pin the STATES, not just the durations — the site's
   global *{transition-duration:.01ms} alone would leave the plate snapping. */
@media (prefers-reduced-motion:reduce){
  .gi::before{transform:rotate(-2.2deg) scale(.965) !important}
  .gi .gi-face{transform:none !important}
}

/* ── ELASTIC MESH ─────────────────────────────────────────────────────
   Desktop-only WebGL, ONE context per page. The fallback is not "nothing":
   it is this painted lattice panel, which every phone and every
   reduced-motion visitor gets permanently, with the same geometry so it
   reads as the same object standing still. */
.emesh{
  position:relative;overflow:hidden;border-radius:var(--radius);
  /* LIGHT GROUND. This is pure CSS — fx.js no longer paints it (the WebGL
     mesh was deleted), so these three rules are the whole effect. Was
     #151412→#0B0A09 with a #000 inset vignette; the three columns of dark
     type over it measured 1.28:1. Now a warm pane with a white specular
     pool, and fine side rims (the mask fades top and bottom, so the rims
     dissolve with it and read as a glass edge, not a box). */
  background:
    radial-gradient(120% 90% at 20% 0%,var(--paper-lift),transparent 60%),
    linear-gradient(180deg,var(--accent-wash),var(--ash));
  box-shadow:inset 1px 0 0 var(--border),inset -1px 0 0 var(--border);
}
.emesh::before{                                   /* the painted lattice */
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:
    repeating-linear-gradient(0deg,color-mix(in srgb,var(--bronze) 20%,transparent) 0 1px,transparent 1px 34px),
    repeating-linear-gradient(90deg,color-mix(in srgb,var(--bronze) 20%,transparent) 0 1px,transparent 1px 34px);
  -webkit-mask-image:radial-gradient(80% 70% at 50% 45%,#000 40%,transparent 100%);
  mask-image:radial-gradient(80% 70% at 50% 45%,#000 40%,transparent 100%);
  /* The lattice drifts on its own — a slow, barely-there parallax that gives
     the band life without a GPU context. transform only, so it is a pure
     compositor job: no layout, no paint, no per-frame allocation. */
  animation:meshDrift 26s ease-in-out infinite alternate;
}
@keyframes meshDrift{
  from{transform:translate3d(-1.4%,-1%,0) scale(1.06)}
  to  {transform:translate3d(1.4%,1%,0) scale(1.06)}
}
.emesh::after{                                    /* bottom vignette */
  content:"";position:absolute;inset:0;pointer-events:none;
  /* a white specular top edge and a faint warm settle at the foot —
     a black vignette on paper reads as dirt */
  box-shadow:inset 0 1px 0 var(--paper-lift),
             inset 0 -40px 60px -40px color-mix(in srgb,var(--bronze) 16%,transparent);
}
/* When the live mesh is running the painted lattice fades out — the element
   is never empty for a frame in either direction. */
@media (prefers-reduced-motion:reduce){
  .emesh::before{animation:none;transform:scale(1.06)}
}

/* ── effect placement ─────────────────────────────────────────────────── */
.visit-card{padding:clamp(1.5rem,3vw,2.25rem)}

/* The mesh sits behind the three-column band as a contained panel rather
   than a full-bleed background: a bounded element can be measured, capped
   and released, and it keeps the WebGL backing store small. */
.why-mesh{
  position:absolute;inset:auto 0 0 0;height:min(78%,420px);z-index:0;
  opacity:.75;
  /* Fade in from the top so it reads as atmosphere behind the section rather
     than a panel with an edge running through the headline. */
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 22%,#000 82%,transparent);
  mask-image:linear-gradient(180deg,transparent,#000 22%,#000 82%,transparent);
}
.section:has(.why-mesh){position:relative}
.section:has(.why-mesh) > :not(.why-mesh){position:relative;z-index:1}
/* :has() is Safari 15.4+/Chrome 105+. Without it the mesh would sit on top
   of the copy, so it is simply not shown. */
@supports not selector(:has(*)){ .why-mesh{display:none} }
@media (max-width:640px){ .why-mesh{height:min(60%,260px);opacity:.55} }

/* ── SCROLL REVEAL ────────────────────────────────────────────────────
   Nothing here hides anything until fx.js has proved it is running: the
   [data-reveal="on"] attribute is set by an inline script in <head>, so a
   blocked or broken script leaves a completely visible page rather than a
   blank one. */
html[data-reveal="on"] .reveal{
  opacity:0;
  transform:translate3d(0,22px,0);
}
html[data-reveal="on"] .reveal.is-in{
  opacity:1;
  transform:none;
  transition:opacity .72s cubic-bezier(.2,.7,.2,1) var(--r-delay,0ms),
             transform .72s cubic-bezier(.2,.7,.2,1) var(--r-delay,0ms);
}
/* Headlines carry a touch more travel than body copy, so a section arrives
   as a composed group rather than everything sliding the same distance. */
html[data-reveal="on"] .reveal-lead{transform:translate3d(0,34px,0)}

/* The wall band drifts as it passes. animation-timeline runs on the
   compositor, off the main thread — so where it is supported this costs
   nothing per frame, and where it is not, the band is simply static. No
   scroll listener is involved either way. */
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion: no-preference) and (min-width: 900px){
    .wall img{
      animation:wallDrift linear both;
      animation-timeline:view();
      animation-range:entry 0% exit 100%;
    }
    @keyframes wallDrift{
      from{transform:scale(1.10) translate3d(0,-1.2%,0)}
      to  {transform:scale(1.10) translate3d(0,1.2%,0)}
    }
  }
}

@media (prefers-reduced-motion:reduce){
  html[data-reveal="on"] .reveal{opacity:1;transform:none}
}

/* ── GLASS ICON CHIPS ON BUTTONS ──────────────────────────────────────
   A square glass tile carrying the icon, sitting inside the button. Same
   material language as the category tiles: a champagne specular on the top
   edge, a bronze hairline, and the ground showing through.

   Deliberately NOT backdrop-filter. There can be five of these on one
   screen, they sit inside elements that already move on hover, and the
   page's single affordable backdrop slot is spent on the sticky nav. Over
   a flat ground a gradient and a blur render the same thing anyway. */
.btn-ico{
  --chip:2.05em;
  flex:0 0 auto;display:grid;place-items:center;
  width:var(--chip);height:var(--chip);
  margin-left:-.35em;                       /* optical: pull it off the edge */
  border-radius:.28em;
  background:linear-gradient(155deg,rgba(255,248,228,.20),rgba(255,248,228,.05) 45%,rgba(0,0,0,.16));
  border:1px solid rgba(192,163,138,.30);
  box-shadow:inset 0 1px 0 rgba(255,248,228,.30), inset 0 -1px 0 rgba(0,0,0,.28);
  transition:background .25s ease,border-color .25s ease;
}
.btn-ico svg{width:56%;height:56%;display:block}
/* On the champagne plate the chip has to read as recessed metal, not as a
   lighter patch — so it darkens instead of lightening. */
.btn-solid .btn-ico{
  background:linear-gradient(155deg,rgba(16,14,11,.16),rgba(16,14,11,.05) 45%,rgba(16,14,11,.14));
  border-color:rgba(16,14,11,.22);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.45), inset 0 -1px 0 rgba(16,14,11,.16);
}
@media (hover:hover) and (pointer:fine){
  .btn-ghost:hover .btn-ico{border-color:rgba(192,163,138,.55);
    background:linear-gradient(155deg,rgba(255,248,228,.28),rgba(255,248,228,.08) 45%,rgba(0,0,0,.14))}
}
.btn-sm .btn-ico{--chip:1.85em;border-radius:.24em}
/* The old inline phone glyph rule sized bare svgs inside buttons; the chip
   sizes its own, so scope that back to direct children only. */
.btn > svg{width:1.05em;height:1.05em;fill:currentColor;flex:0 0 auto}

/* ═══════════════════════════════════════════════════════════════════════
   THE WALKTHROUGH
   A sticky media stage with three scroll-driven chapters of the real store.
   Scroll advances the LOCATION; playback supplies the MOVEMENT.

   Deliberately NOT a scroll-scrubbed video. Scrubbing does its decoding and
   seeking work at exactly the moment the browser is already busy handling
   scroll, which is where mobile Safari falls over. Chapters bound the motion
   and give every beat a real still-image fallback.
   ═══════════════════════════════════════════════════════════════════════ */
.walk{position:relative;background:var(--void)}
.walk-stage{
  position:sticky;top:0;height:100svh;overflow:hidden;
  display:grid;place-items:stretch;
}
/* The still and the video occupy the same cell. The still is always painted;
   the video fades in over it only once it has a decoded frame, and the still
   returns the instant playback stops. The stage is never empty. */
.walk-still,.walk-vid{
  grid-area:1/1;width:100%;height:100%;object-fit:cover;display:block;
}
.walk-vid{opacity:0;transition:opacity .18s linear}
.walk-vid.is-showing{opacity:1}
.walk-still{transition:opacity .18s linear}

/* The veil holds type contrast on the LEFT only, so the store stays visible.
   The hero's old shade ran 90-97% opacity — tuned to bury a busy AI render —
   and would have obliterated this real footage. */
.walk-veil{
  grid-area:1/1;pointer-events:none;
  background:
    linear-gradient(90deg,rgb(11 10 9/.88) 0%,rgb(11 10 9/.46) 38%,rgb(11 10 9/.06) 68%,transparent 82%),
    linear-gradient(0deg,rgb(11 10 9/.72) 0%,transparent 35%);
}

.walk-steps{position:relative;margin-top:-100svh;z-index:2}
.walk-step{
  min-height:75svh;display:flex;align-items:center;
  max-width:var(--shell);margin-inline:auto;padding-inline:var(--gutter);
}
.walk-copy{max-width:min(34rem,72%)}

/* Type lit BY the room rather than sat on top of it: a sharp champagne core
   with a faint warm halo. Sharp letter + soft glow reads as illuminated; a
   blurred letter just reads as out of focus. */
.walk-head{
  text-shadow:
    0 1px 0 rgb(11 10 9/.55),
    0 0 28px rgb(239 227 188/.22),
    0 0 70px rgb(192 163 138/.14);
}
.walk-copy .lead{
  margin-top:1.1rem;
  text-shadow:0 1px 3px rgb(11 10 9/.85);
}
.walk-copy .eyebrow{text-shadow:0 1px 2px rgb(11 10 9/.9)}

@media (max-width:900px){
  /* Keep the 16:9 composition rather than cover-cropping to portrait — a
     full-height crop throws away most of the selection the shot exists to
     show. Stage on top, copy beneath it. */
  .walk-stage{height:auto;aspect-ratio:16/9;top:0}
  .walk-steps{margin-top:0}
  .walk-step{min-height:0;padding-block:clamp(2rem,7vw,3.5rem)}
  .walk-copy{max-width:100%}
  .walk-veil{background:linear-gradient(0deg,rgb(11 10 9/.75) 0%,transparent 55%)}
}
@media (prefers-reduced-motion:reduce){
  .walk-vid{display:none}          /* stills only — a complete presentation */
  .walk-still{transition:none}
}

/* ═══════════════════════════════════════════════════════════════════════
   THE FILM — scroll-scrubbed hero
   The first thing on the page. Sticky stage, one video, scroll is the
   playhead. See scrub.js for the mechanism.
   ═══════════════════════════════════════════════════════════════════════ */
.film{position:relative;background:var(--void)}
.film-stage{position:sticky;top:0;height:100svh;overflow:hidden}
.film-vid{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  opacity:0;transition:opacity .5s ease;
}
.film-vid.is-ready{opacity:1}
/* The poster carries the stage until the film is ready, so there is never a
   black hole at the top of the page. */
.film-stage{
  background:#000 none center/cover no-repeat;
}
.film-veil{
  position:absolute;inset:0;pointer-events:none;
  background:
    linear-gradient(90deg,rgb(11 10 9/.86) 0%,rgb(11 10 9/.44) 40%,rgb(11 10 9/.06) 70%,transparent 84%),
    linear-gradient(0deg,rgb(11 10 9/.7) 0%,transparent 38%);
}

/* Every beat occupies the same cell; only one is legible at a time and
   scrub position decides which. Stacking them means no layout work happens
   as they cross-fade — it is pure opacity and transform. */
.film-beats{
  position:absolute;inset:0;display:grid;align-items:center;
  max-width:var(--shell);margin-inline:auto;padding-inline:var(--gutter);
  pointer-events:none;
}
.film-beat{
  grid-area:1/1;max-width:min(34rem,80%);
  will-change:opacity,transform;
}
.film-beat > *{pointer-events:auto}
.film-head{
  font-size:clamp(2.5rem,6vw,5rem);
  text-shadow:0 1px 0 rgb(11 10 9/.6),0 0 30px rgb(239 227 188/.20),0 0 80px rgb(192 163 138/.12);
}
.film-beat .lead{margin-top:1.1rem;text-shadow:0 1px 3px rgb(11 10 9/.85)}
.film-beat .eyebrow{text-shadow:0 1px 2px rgb(11 10 9/.9)}
.film-cta{display:flex;gap:.75rem;flex-wrap:wrap;margin-top:2rem}

/* ── the square button ────────────────────────────────────────────────
   "Liquid premium, minimal": square corners, medium weight rather than
   bold, generous tracking, a single hairline, and a fill that arrives as a
   wipe rather than a colour flip. Restraint is the whole effect — nothing
   here is louder than the footage behind it. */
.btn-sq{
  --sq:rgba(239,227,188,.92);
  position:relative;display:inline-flex;align-items:center;gap:.6rem;
  padding:1rem 1.9rem;border-radius:0;
  font-family:var(--body);font-weight:500;font-size:.9375rem;line-height:1;
  letter-spacing:.14em;text-transform:uppercase;text-decoration:none;
  color:#0F0D0A;background:var(--sq);
  border:1px solid var(--sq);
  overflow:hidden;isolation:isolate;
  transition:color .38s cubic-bezier(.4,0,.2,1),border-color .38s ease;
}
.btn-sq::before{
  content:"";position:absolute;inset:0;z-index:-1;background:var(--sq);
  transform:translateY(0);transition:transform .42s cubic-bezier(.4,0,.2,1);
}
.btn-sq-ghost{color:var(--champagne);background:transparent;border-color:rgba(192,163,138,.5)}
.btn-sq-ghost::before{transform:translateY(101%)}
@media (hover:hover) and (pointer:fine){
  .btn-sq:hover{border-color:var(--specular)}
  .btn-sq:hover::before{transform:translateY(101%)}
  .btn-sq:hover{color:var(--champagne)}
  .btn-sq-ghost:hover::before{transform:translateY(0)}
  .btn-sq-ghost:hover{color:#0F0D0A}
}
.btn-sq:active{transform:scale(.985)}

@media (max-width:900px){
  .film-beat{max-width:100%}
  .film-veil{background:linear-gradient(0deg,rgb(11 10 9/.82) 0%,rgb(11 10 9/.2) 60%,transparent 100%)}
  .film-cta{width:100%}
  .btn-sq{flex:1 1 100%;justify-content:center}
}
/* Reduced motion / Save-Data: no video, beats laid out as ordinary stacked
   content rather than overlapping ghosts. */
.film-static .film-stage{position:relative;height:auto;min-height:60svh}
.film-static .film-beats{position:relative;display:block;padding-block:clamp(3rem,9vw,6rem)}
.film-static .film-beat{max-width:38rem}
.film-static .film-beat + .film-beat{margin-top:clamp(2.5rem,6vw,4rem)}
.film-static .film-track{display:none}


/* ══════════════════════════════════════════════════════════════════════
   LIGHT-GROUND OVERRIDES — appended so they win on source order.
   Everything below was written against the paper palette above.
   ══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════
   LIGHT TOKENS — shadcn's structure, this brand's material
   ═══════════════════════════════════════════════════════════════════════
   shadcn's contract is background/foreground pairs, muted + muted-foreground,
   border vs input vs ring kept separate, and one --radius. We adopt that
   contract verbatim and change only what the values are made of.

   There is still no blue and no green in this system. shadcn's stock light
   theme is a cold neutral (hsl 0 0% ...); every neutral below is warmed
   toward the logo's brass, so white paper reads as paper and not as screen.

   Keep these ALONGSIDE the existing dark tokens — the film still needs
   --champagne and --specular. This block goes after the existing :root.  */

/* The global focus ring is --specular, a pale champagne. That is correct on
   the film and invisible on paper, so the ring now comes from a token the
   film re-points. One rule, two grounds. */
:focus-visible{outline:2px solid var(--ring);outline-offset:2px;border-radius:2px}
:focus:not(:focus-visible){outline:none}

/* ═══════════════════════════════════════════════════════════════════════
   .btn-sq — shadcn variants, existing class names
   ═══════════════════════════════════════════════════════════════════════
   Mapping, so no markup changes:
     .btn-sq                    → shadcn `default`
     .btn-sq.btn-sq-ghost       → shadcn `outline`   (kept: it is already a
                                  hairline; renaming it would break every
                                  page and every build.py call site)
     .btn-sq.btn-sq-secondary   → shadcn `secondary`
     .btn-sq.btn-sq-quiet       → shadcn `ghost`
     .btn-sq.btn-sq-danger      → shadcn `destructive`

   The name collision is real and worth stating once: OUR .btn-sq-ghost is
   shadcn's OUTLINE. shadcn's ghost is our .btn-sq-quiet.

   Every variant is four local custom properties and nothing else. That is
   what lets the film restate the whole component in five lines instead of
   redeclaring it.

   🐛 FIXES A LIVE BUG. Today .btn-sq sets `background:var(--sq)` on the
   element AND paints the same colour into ::before. isolation:isolate makes
   .btn-sq a stacking context, so the z-index:-1 pseudo paints ABOVE the
   element's own background — meaning on hover the plate slides away, the
   element background stays champagne, and `color:var(--champagne)` puts
   champagne text on a champagne plate. The label disappears on hover on the
   two hero CTAs. Below, the element background is transparent and the fill
   lives only in ::before, which is what the slide was always assuming. */
.btn-sq{
  --sq-fill:var(--primary);           /* the plate                        */
  --sq-fg:var(--primary-foreground);  /* label while the plate is there   */
  --sq-fg-out:var(--primary);         /* label once the plate has gone    */
  --sq-bd:var(--primary);             /* the hairline                     */

  position:relative;display:inline-flex;align-items:center;justify-content:center;
  gap:.6rem;
  padding:1rem 1.9rem;
  border-radius:var(--radius);        /* 3px. carved, not soft, not round */
  font-family:var(--body);font-weight:500;font-size:.9375rem;line-height:1;
  letter-spacing:.14em;text-transform:uppercase;text-decoration:none;
  color:var(--sq-fg);
  background:transparent;             /* ← the fix. fill is ::before only */
  border:1px solid var(--sq-bd);
  cursor:pointer;
  overflow:hidden;isolation:isolate;
  transition:color .38s cubic-bezier(.4,0,.2,1),border-color .38s ease;
}
.btn-sq::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:var(--sq-fill);
  transform:translateY(0);
  transition:transform .42s cubic-bezier(.4,0,.2,1);
}

/* outline — plate parked below, slides UP to fill */
.btn-sq-ghost{
  --sq-fill:var(--primary);
  --sq-fg:var(--foreground);
  --sq-fg-out:var(--primary-foreground);
  --sq-bd:var(--input);
}
/* secondary — stone plate, ink label both states */
.btn-sq-secondary{
  --sq-fill:var(--secondary);
  --sq-fg:var(--secondary-foreground);
  --sq-fg-out:var(--secondary-foreground);
  --sq-bd:var(--border);
}
/* ghost — no hairline, no plate at rest; a warm tint arrives on hover */
.btn-sq-quiet{
  --sq-fill:var(--accent);
  --sq-fg:var(--muted-foreground);
  --sq-fg-out:var(--accent-foreground);
  --sq-bd:transparent;
}
/* destructive */
.btn-sq-danger{
  --sq-fill:var(--destructive);
  --sq-fg:var(--destructive-foreground);
  --sq-fg-out:var(--destructive);
  --sq-bd:var(--destructive);
}

/* Variants whose plate starts DOWN. Grouped so the direction of the wipe is
   one decision in one place rather than a property on five rules. */
.btn-sq-ghost::before,
.btn-sq-quiet::before{transform:translateY(101%)}

@media (hover:hover) and (pointer:fine){
  /* filled variants: plate leaves, label becomes the plate colour */
  .btn-sq:hover::before{transform:translateY(101%)}
  .btn-sq:hover{color:var(--sq-fg-out);border-color:var(--sq-bd)}
  /* unfilled variants: plate arrives, label becomes the label-on-plate colour */
  .btn-sq-ghost:hover::before,
  .btn-sq-quiet:hover::before{transform:translateY(0)}
  .btn-sq-ghost:hover,
  .btn-sq-quiet:hover{color:var(--sq-fg-out)}
  .btn-sq-ghost:hover{border-color:var(--primary)}
}
.btn-sq:active{transform:scale(.985)}

/* Visible ring, offset so it clears the hairline instead of sitting on it.
   The offset needs a matching shadow-free gap: outline-offset alone leaves
   the ring floating over whatever is behind, which on the film is video. */
.btn-sq:focus-visible{
  outline:2px solid var(--ring);
  outline-offset:2px;
}

/* small — used by the age card */
.btn-sq-sm{padding:.6rem 1rem;font-size:.8125rem;letter-spacing:.1em}

/* disabled — shadcn's convention: half opacity, pointer denied, no wipe */
.btn-sq[disabled],.btn-sq[aria-disabled="true"]{
  opacity:.5;pointer-events:none;
}

@media (prefers-reduced-motion:reduce){
  .btn-sq,.btn-sq::before{transition:none}
  .btn-sq:active{transform:none}
}

/* ── the film keeps the dark palette ───────────────────────────────────
   The component is not redefined here; only the four locals and the ring.
   .btn-sq on the film is champagne-on-black exactly as before. */
.film .btn-sq{
  --sq-fill:rgba(239,227,188,.94);
  --sq-fg:#0F0D0A;
  --sq-fg-out:var(--champagne);
  --sq-bd:rgba(239,227,188,.94);
  --ring:var(--specular);
}
.film .btn-sq-ghost{
  --sq-fill:rgba(239,227,188,.94);
  --sq-fg:var(--champagne);
  --sq-fg-out:#0F0D0A;
  --sq-bd:rgba(192,163,138,.55);
}
@media (hover:hover) and (pointer:fine){
  .film .btn-sq:hover{border-color:var(--specular)}
  .film .btn-sq-ghost:hover{border-color:var(--specular)}
}

/* ═══════════════════════════════════════════════════════════════════════
   THE FILM — centred, with a symmetric scrim
   ═══════════════════════════════════════════════════════════════════════

   DECISION: all four beats centre, not just the first.

   The reason is the veil, not taste. All four beats occupy the same grid
   cell over ONE stage and therefore share ONE scrim. Centring beat 1 while
   beats 2–4 stay left means the scrim has to be dark in the middle AND dark
   on the left, i.e. dark nearly everywhere, which is the flat black rectangle
   the directional veil existed to avoid. A centred composition lets the scrim
   be a real vignette that dissolves at the frame edges, so the footage still
   reads. Mixing alignments mid-crossfade also reads as a rendering fault
   rather than as a choice — the beats dissolve into each other, they do not
   cut.

   THE FILM IS THE ONE DARK SURFACE LEFT IN A LIGHT SITE, deliberately. You
   cannot set dark type over uncontrolled video frames; a light hero would
   mean a scrim heavy enough to grey the footage out. So the film keeps
   champagne-on-black and the page goes white beneath it — the hero reads as
   a lit window cut into the paper, which is the same gesture the original
   dark system used for photography.

   ⚠️ CONSTRAINT: scrub.js writes `style.transform` on every .film-beat every
   frame. Nothing below may centre with transform — it would be overwritten
   ~60×/s. Centring is done with justify-self / margin-inline / text-align
   only.                                                                  */

.film-veil{
  position:absolute;inset:0;pointer-events:none;
  background:
    /* 1. the type's own pool of shade. Ellipse, not circle: the beat block
          is far wider than it is tall, and a circular falloff would leave
          the ends of a two-line h1 brighter than its middle. Anchored at
          46% vertically because the optical centre of the block sits above
          the geometric centre once the CTA row is counted. */
    radial-gradient(ellipse 78% 62% at 50% 46%,
      rgb(11 10 9/.74) 0%,
      rgb(11 10 9/.58) 38%,
      rgb(11 10 9/.22) 72%,
      rgb(11 10 9/0)  100%),
    /* 2. bottom lift. The CTA row sits low and the shop floor is the
          brightest part of the footage; the radial has already faded out
          by the time it gets there. */
    linear-gradient(0deg,rgb(11 10 9/.66) 0%,rgb(11 10 9/.18) 30%,transparent 48%),
    /* 3. top lift, for the sticky nav only — short and shallow so it never
          reaches the h1. */
    linear-gradient(180deg,rgb(11 10 9/.52) 0%,transparent 22%);
}

.film-beats{
  position:absolute;inset:0;display:grid;align-items:center;
  max-width:var(--shell);margin-inline:auto;padding-inline:var(--gutter);
  pointer-events:none;
  justify-items:center;              /* was implicitly `stretch` → left */
}

.film-beat{
  grid-area:1/1;
  /* 40rem rather than 34: uppercase Cinzel at 5rem is a wide face, and a
     centred block wants to break on its own terms rather than be squeezed
     into three lines by the container. text-wrap:balance on .d-xl does the
     rest. */
  max-width:min(40rem,88%);
  text-align:center;
  will-change:opacity,transform;
}

/* .lead carries its own max-width:34rem from the type roles, which without
   this would pin it to the left edge of the centred block. */
.film-beat .lead{margin-inline:auto}

.film-cta{
  display:flex;gap:.75rem;flex-wrap:wrap;
  margin-top:2rem;
  justify-content:center;
}

/* The glow was tuned for type sitting on the dark LEFT third. Centred, it
   sits over the brightest part of the frame, so the hard 1px contact
   shadow does more work and the wide bloom does less — otherwise the
   champagne haloes into the footage and the letterforms go soft. */
.film-head{
  font-size:clamp(2.5rem,6vw,5rem);
  text-shadow:
    0 1px 0  rgb(11 10 9/.75),
    0 2px 12px rgb(11 10 9/.45),
    0 0 44px rgb(239 227 188/.16),
    0 0 96px rgb(192 163 138/.10);
}

@media (max-width:900px){
  .film-beat{max-width:100%}
  .film-cta{width:100%}
  .btn-sq{flex:1 1 100%;justify-content:center}
  /* Portrait kills the ellipse: at 9:16 an 78%-wide radial is a vertical
     stripe, and the top and bottom of the frame stay bright. A symmetric
     vertical ramp is the honest mobile equivalent — bottom-weighted,
     because that is where the copy and the buttons are. */
  .film-veil{
    background:linear-gradient(0deg,
      rgb(11 10 9/.86) 0%,
      rgb(11 10 9/.60) 34%,
      rgb(11 10 9/.30) 62%,
      rgb(11 10 9/.10) 88%,
      transparent 100%);
  }
}

/* Reduced motion / Save-Data: beats become ordinary stacked blocks. They
   still centre — this is the same page, not a different design — but the
   block itself has to be re-centred because `justify-items` no longer
   applies once .film-beats stops being a grid. */
.film-static .film-beat{max-width:38rem;margin-inline:auto;text-align:center}

/* ── 21+ confirmation ──────────────────────────────────────────────────
   shadcn's `popover` surface, not its `dialog`: --popover ground, one
   --border hairline, --radius, and a shadow soft enough to read as
   elevation rather than as decoration. The client killed the full-screen
   gate because it blocked the landing, so this is deliberately a corner
   card — it never covers the hero and it never takes the pointer.       */

/* LOAD-BEARING, NOT BOILERPLATE. In this stylesheet [hidden] loses to any
   later rule that sets `display`, and .agegate sets `display:flex` three
   lines down. Without this the popup would ship visible on every page and
   ignore localStorage entirely. Attribute selector + element selector out-
   specifies the bare class, so it wins without !important.               */
.agegate[hidden]{display:none}

.agegate{
  position:fixed;
  left:max(var(--gutter),env(safe-area-inset-left));
  bottom:max(var(--gutter),env(safe-area-inset-bottom));
  z-index:120;                      /* over the nav (60), under .skip (200) */
  width:min(23rem,calc(100vw - 2 * var(--gutter)));
  display:flex;flex-direction:column;gap:.55rem;
  padding:1.05rem 1.15rem 1.15rem;
  background:var(--popover);
  color:var(--popover-foreground);
  border:1px solid var(--border);
  border-radius:var(--radius);
  /* Two stacked shadows rather than one big blur: a 1px contact shadow
     does the "it is lifted" work, the wide one only softens the edge.
     Warm-tinted because a neutral grey shadow on this paper reads blue. */
  box-shadow:0 1px 2px rgb(35 27 20/.06),0 12px 32px -12px rgb(35 27 20/.18);
}

.agegate-title{
  margin:0;
  font-family:var(--display);font-weight:600;font-size:.9375rem;
  letter-spacing:.09em;text-transform:uppercase;
  color:var(--foreground);
}
.agegate-copy{
  margin:0 0 .35rem;
  font-size:.875rem;line-height:1.5;
  color:var(--muted-foreground);   /* 5.97:1 on white — passes AA at 14px */
}
.agegate-actions{display:flex;flex-wrap:wrap;gap:.5rem}
.agegate-actions .btn-sq{flex:1 1 auto;justify-content:center}

/* Entrance. Transform is safe here — unlike .film-beat, nothing writes an
   inline transform to this element. */
.agegate{
  opacity:0;transform:translateY(.5rem);
  transition:opacity .28s ease,transform .28s cubic-bezier(.4,0,.2,1);
}
.agegate.is-in{opacity:1;transform:none}

@media (prefers-reduced-motion:reduce){
  /* Not `transition:none` alone — that would leave it stuck at opacity:0
     if the class landed in the same frame as the unhide. Neutralise the
     start state too, so the element is correct with or without the class. */
  .agegate{opacity:1;transform:none;transition:none}
}

@media (max-width:520px){
  /* Full-width bar at the bottom on a phone. Still not an interstitial —
     the hero is untouched above it. */
  .agegate{
    left:max(.75rem,env(safe-area-inset-left));
    right:max(.75rem,env(safe-area-inset-right));
    width:auto;
  }
  .agegate-actions .btn-sq{flex:1 1 100%}
}
/* ══════════════════════════════════════════════════════════════════════
   TYPE OVER FOOTAGE — the rewire the value-inversion made mandatory.

   --champagne, --vapour and --brass all INVERTED when the page went to a
   paper ground: they used to be light type on black, they are now dark type
   on paper. Every one of them is correct on the page and WRONG the moment it
   is set over the film, the walkthrough or the hero, because those keep a
   dark scrim by design (the footage is a real lit room; a paper scrim over it
   would wash the store out, which is the opposite of what the client asked).

   Left alone this ships dark-on-dark. Measured on the built page before this
   block existed: .film-head resolved to rgb(36,28,20) over a 0.74 black
   vignette — 2.39:1, under the 3.0 floor for large text.

   The --film-* family is the same palette struck for a dark ground.
   ══════════════════════════════════════════════════════════════════════ */
.film-head,
.walk-head,
.hero .d-xl{color:var(--film-display)}

.film-beat .lead,
.walk-copy .lead,
.hero .lead{color:var(--film-body)}

.film-beat .eyebrow,
.walk-copy .eyebrow,
.hero .eyebrow{color:var(--film-accent)}

.hero-pause{color:var(--film-body);border-color:var(--film-rule)}
.hero-pause:hover{color:var(--film-display);border-color:var(--film-accent)}

/* The ghost CTA sits on the film too, so it takes the film family and a
   film-side border rather than the paper-side --bronze. */
.film-cta .btn-sq-ghost{
  color:var(--film-display);
  border-color:rgb(216 188 158/.55);
}
.film-cta .btn-sq-ghost:hover{border-color:var(--film-accent)}

/* ══════════════════════════════════════════════════════════════════════
   THE NAV OVER THE FILM

   The header sits directly on top of a full-bleed video, so a solid bar
   across the top cuts the frame in half. Over the film it goes transparent
   and lets the footage run under it; once the film has scrolled past and the
   paper page begins, it becomes the solid bar it has to be to stay readable.

   FAILS SAFE. The transparent state is opt-IN (.is-over-film, added by JS
   only when a film is actually behind the nav). Default is the solid paper
   bar, so the sub-pages — which have no film — are correct with no JS, and a
   script failure leaves a readable header rather than an invisible one.

   The type has to invert with it: over footage the links take the --film-*
   family, and the brand mark's metal gradient is overridden to a light
   stroke, because it was re-ramped dark for the paper ground and would
   otherwise disappear into the video.
   ══════════════════════════════════════════════════════════════════════ */
/* The scrim is the nav's own BACKGROUND, not a ::before.
   The first version used a pseudo-element at z-index:-1 with
   isolation:isolate on the parent, and it never painted — a negative-z
   descendant inside a stacking context lands above the element's own
   background but below nothing else useful, and the scrim silently did
   nothing. Measured: the brand sub-line came out at 1.20:1 over the bright
   top of the poster, i.e. invisible. A gradient IS a background, so putting
   it there sidesteps the whole stacking question.

   Strong at the very top where the type sits, gone by the bottom edge so it
   reads as light falling off rather than as a bar. */
.nav.is-over-film{
  /* Stops measured against where the type actually sits. The nav is 80px
     and its content occupies roughly y=25-55, so the scrim has to still
     be strong at 70% of the height; the first version had decayed to .16
     by then and the brand sub-line sat on luma ~160. */
  background:linear-gradient(180deg,rgb(11 10 9/.86) 0%,rgb(11 10 9/.80) 50%,rgb(11 10 9/.62) 78%,rgb(11 10 9/.18) 94%,rgb(11 10 9/0) 100%);
  backdrop-filter:none;-webkit-backdrop-filter:none;
  border-bottom-color:transparent;
}
/* Type over moving footage needs its own separation: the frame behind it
   changes every scroll tick, so the scrim alone cannot be tuned for it. */
.nav.is-over-film .brand-name,
.nav.is-over-film .nav-links a,
.nav.is-over-film .btn{text-shadow:0 1px 3px rgb(11 10 9/.85)}

.nav.is-over-film .brand-name{color:var(--film-display)}
.nav.is-over-film .brand-name span{color:var(--film-body)}
.nav.is-over-film .nav-links a{color:var(--film-body)}
.nav.is-over-film .nav-links a:hover,
.nav.is-over-film .nav-links a[aria-current="page"]{color:var(--film-display)}
.nav.is-over-film .nav-links a[aria-current="page"]{border-bottom-color:var(--film-accent)}

/* The mark was re-ramped to dark metal for paper; over footage that is
   dark-on-dark, so the gradient is overridden with a flat light stroke. */
.nav.is-over-film .mark path,
.nav.is-over-film .mark use{stroke:var(--film-display)}

/* The header CTA is .btn.btn-ghost (there is no .nav-cta — an earlier guess
   at the class name silently matched nothing and left the button sitting at
   --vapour, i.e. dark type on the footage). */
.nav.is-over-film .btn,
.nav.is-over-film .btn-ghost{color:var(--film-display);border-color:rgb(216 188 158/.55)}
.nav.is-over-film .btn:hover,
.nav.is-over-film .btn-ghost:hover{border-color:var(--film-accent)}
.nav.is-over-film .btn-ico{color:var(--film-display)}

@media (prefers-reduced-motion:no-preference){
  .nav{transition:background .35s ease,border-color .35s ease}
}


/* ══════════════════════════════════════════════════════════════════════
   THE FILM RUNS UNDER THE HEADER

   The nav is position:sticky, which means it OCCUPIES its 80px of layout —
   so the film began at y=80 and the top of the page was a band of paper
   with the nav's dark gradient fading out over it. That band, and the hard
   edge where the gradient ended, is the "white gap and header line": the
   header was never over the video at all, it was a bar sitting above it.

   Pulling the film up by exactly the nav's height puts the footage back
   under the bar, so the gradient dissolves into moving picture instead of
   into paper and the join disappears. --nav-h is published by site.js from
   the real measured height; the 80px fallback only matters for the instant
   before script runs.
   ══════════════════════════════════════════════════════════════════════ */
.film{margin-top:calc(-1 * var(--nav-h, 80px))}

/* The sticky stage still pins to the viewport top, so the video is
   full-bleed behind the bar rather than starting under it. */
.film .film-stage{top:0}

/* ══════════════════════════════════════════════════════════════════════
   THE HAIRLINE UNDER THE HEADER

   The client flagged "a header line" under the nav; closing the white gap
   did not remove it. Measured row by row across the seam: ONE pixel row at
   luma 21 (desktop) / 27 (phone) against ~89 / ~135 on either side, sitting
   exactly on the nav's last row. Not a border colour (border-bottom is
   transparent), no box-shadow, no pseudo-element, nothing else ends there.

   It is the scrim gradient WRAPPING INTO THE BORDER. background-origin
   defaults to padding-box, so the gradient tile is sized to the padding box;
   background-clip defaults to border-box and background-repeat to repeat, so
   the 1px transparent border-bottom gets painted with the top of a SECOND
   tile - the gradient's 86%-black first stop. One dark line, full width.

   Sizing the gradient to the border box and not repeating it removes the row
   without changing the nav's height, so --nav-h and the film offset are
   untouched. It must target .nav.is-over-film and come after that rule: its
   `background:` shorthand resets origin and repeat, so a plain .nav rule
   would be silently undone. */
.nav,
.nav.is-over-film{background-origin:border-box;background-repeat:no-repeat}

/* ── repair: glass ── */
/* ══════════════════════════════════════════════════════════════════════
   LIQUID GLASS BUTTONS — one material for every button on the site
   APPENDED LAST. It wins on source order, and pins specificity wherever
   an older context rule would otherwise beat it.

   Classes:  .btn .btn-solid .btn-ghost .btn-sm .btn-ico
             .btn-sq .btn-sq-ghost .btn-sq-quiet .btn-sq-secondary
             .btn-sq-danger .btn-sq-sm  .dock-cta  .hero-pause
   Grounds:  PAPER (page, age card, the light visit card) and FOOTAGE
             (.film-cta, .nav.is-over-film). The dock floats over both.

   LAYERING. The material lives ON THE ELEMENT, never in a negative-z
   pseudo under the tint (that is the bug that killed .btn-sq's hover):
       background-color → the tint        rgb(R G B / literal alpha token)
       box-shadow       → edge hairline (TOP layer, so no rim ever covers
                          it) + lit top rim + shade bottom rim + drop
       backdrop-filter  → the blur, only inside THE BLUR GATE
   The one pseudo is ::after, the specular, at z-index:-1 inside
   isolation:isolate. That lands ABOVE the element's own background and
   BELOW the label, which is exactly where a highlight belongs. It is
   inset 1px so it can never wash out the edge hairline.

   NO MATH INSIDE COLOUR FUNCTIONS. Every alpha is a literal token and the
   blur gate swaps WHICH token is read (--lg-a → --lg-ga). A max()/calc()
   inside rgb() that an engine rejects makes the whole background-color
   invalid at computed-value time: transparent, and the .btn-solid label
   goes #FFFCF6 on #FFFCF6 paper.

   TOKEN RULES (break these and a button silently loses its shadow):
   · a *-hot token holds a LITERAL, never var() of its rest partner, or the
     hover swap forms a cycle, both go invalid and box-shadow drops to none;
   · variants and contexts write only --lg-* tokens, at ≤ 0,2,0; the hover
     swap comes after them all.
   ══════════════════════════════════════════════════════════════════════ */

/* Registered so the specular glides instead of snapping. Without @property
   support the highlight just jumps; nothing else changes. */
@property --lg-mx{syntax:"<percentage>";inherits:true;initial-value:50%}
@property --lg-my{syntax:"<percentage>";inherits:true;initial-value:0%}
@property --lg-spec-a{syntax:"<number>";inherits:true;initial-value:0}

:root{
  --lg-blur:12px;
  --lg-sat:150%;
  --lg-persp:640px;
  --lg-spring:cubic-bezier(.34,1.36,.64,1);   /* one short overshoot */
}
@supports (transition-timing-function:linear(0,1)){
  :root{--lg-spring:linear(0,.21 5%,.58 12%,.87 20%,1.03 29%,1.07 35%,1.05 43%,1.01 55%,.995 70%,1)}
}

/* ── the film beat must not be a Backdrop Root ───────────────────────
   will-change:opacity made every .film-beat a Backdrop Root, so the two
   landing CTAs could never blur the footage. will-change:transform keeps
   the beat on its own layer, and so does scrub.js's inline translate3d.
   This rule does NOT touch transform or opacity; scrub.js still owns both.
   While a beat is mid-fade (opacity < 1) it is a Backdrop Root again, and
   the blur drops out for that moment only. The tint stays. */
.film-beat{will-change:transform}

/* ── kill the legacy wipe plate ─────────────────────────────────────── */
.btn-sq::before{content:none}

/* ── the material · default = PAPER PRIMARY (smoked bronze glass) ───── */
.btn,.btn-sq,.dock-cta,.hero-pause{
  --lg-tint:36 28 20;              --lg-a:.9;    --lg-ga:.84;
  --lg-fg:var(--primary-foreground);
  --lg-edge:rgb(255 244 222/.10);  /* the glass edge, all round (top layer) */
  --lg-hi:rgb(255 244 222/.30);    /* lit top rim, 1px inside the edge      */
  --lg-lo:rgb(0 0 0/.38);          /* shade bottom rim, 1px inside the edge */
  --lg-spec:255 246 228;           --lg-spec-a:.12;
  --lg-shade:0 0 0;                --lg-shade-a:.20;
  --lg-drop:0 1px 1px rgb(35 27 20/.10),0 8px 18px -8px rgb(35 27 20/.42);
  /* hot: literals only */
  --lg-tint-hot:143 85 22;         --lg-a-hot:1;  --lg-ga-hot:1;  /* copper; below ~.92 --ink on it is under 4.5:1 */
  --lg-fg-hot:#FFFCF6;
  --lg-edge-hot:rgb(255 236 206/.16);
  --lg-hi-hot:rgb(255 236 206/.42);
  --lg-lo-hot:rgb(0 0 0/.40);
  --lg-spec-a-hot:.12;   /* measured: .34 under the label put --ink on copper at 3.67:1 */
  --lg-drop-hot:0 2px 2px rgb(35 27 20/.10),0 16px 30px -12px rgb(35 27 20/.48);
  --lg-drop-press:0 1px 1px rgb(35 27 20/.16),0 3px 8px -4px rgb(35 27 20/.30);
  /* motion state: written by hover/active/JS, never by variants */
  --lg-lift:0px;--lg-z:0px;--lg-rx:0deg;--lg-ry:0deg;--lg-nx:0;--lg-ny:0;

  isolation:isolate;overflow:hidden;
  border:0;border-radius:var(--radius);
  color:var(--lg-fg);
  background-image:none;
  background-color:rgb(var(--lg-tint)/var(--lg-a));
  box-shadow:inset 0 0 0 1px var(--lg-edge),inset 0 2px 0 var(--lg-hi),inset 0 -2px 0 var(--lg-lo),var(--lg-drop);
  /* 2D identity at rest: no 3D layer, crisp text. 3D exists only under .lg-on. */
  transform:translateY(var(--lg-lift));
  -webkit-tap-highlight-color:transparent;
  transition:
    transform .42s var(--lg-spring),
    box-shadow .28s cubic-bezier(.2,.7,.2,1),
    background-color .22s ease,color .22s ease,
    --lg-mx .18s ease-out,--lg-my .18s ease-out,--lg-spec-a .28s ease;
}
/* .hero-pause is position:absolute and must stay so. */
.btn,.btn-sq,.dock-cta{position:relative}

/* The old 1px border became the inset edge, so padding takes the pixel
   back and every bordered button keeps its exact old box. (.dock-cta never
   had a border, so it keeps its padding.) */
.btn{padding:calc(1.0625rem + 1px) calc(1.75rem + 1px)}
.btn-sm{padding:calc(.75rem + 1px) calc(1.1rem + 1px)}
.btn-sq{padding:calc(1rem + 1px) calc(1.9rem + 1px)}
.btn-sq-sm{padding:calc(.6rem + 1px) calc(1rem + 1px)}

/* ── the specular: follows the pointer, shades the far side ────────── */
.btn::after,.btn-sq::after,.dock-cta::after,.hero-pause::after{
  content:"";position:absolute;inset:1px;z-index:-1;
  border-radius:inherit;pointer-events:none;
  background:radial-gradient(120% 150% at var(--lg-mx) var(--lg-my),
    rgb(var(--lg-spec)/var(--lg-spec-a)) 0%,
    rgb(var(--lg-spec)/0) 48%,
    rgb(var(--lg-shade)/var(--lg-shade-a)) 100%);
}

/* The icon chip floats a hair above the plate: counter-parallax. */
.btn .btn-ico{
  transform:translate(calc(var(--lg-nx) * -1.5px),calc(var(--lg-ny) * -1.5px));
  transition:transform .42s var(--lg-spring),background .25s ease,border-color .25s ease;
}

/* ══ PAPER VARIANTS ══════════════════════════════════════════════════ */

/* clear glass: .btn-ghost, and .btn-sq-ghost (shadcn "outline").
   The edge is opaque --bronze: 4.43:1 on paper and still over 3:1 on an
   --ash or white card. A ghost is a hairline and a label, so the hairline
   has to carry the WCAG 1.4.11 3:1 non-text floor on its own. */
.btn-ghost,.btn-sq-ghost{
  --lg-tint:255 255 255;           --lg-a:.62;   --lg-ga:.62;
  --lg-fg:var(--foreground);
  --lg-edge:rgb(134 115 93/1);
  --lg-hi:rgb(255 255 255/.95);
  --lg-lo:rgb(134 115 93/.22);
  --lg-spec:255 255 255;           --lg-spec-a:.55;
  --lg-shade:138 95 46;            --lg-shade-a:.10;  /* curvature on white = shade, not light */
  --lg-drop:0 1px 1px rgb(35 27 20/.05),0 6px 14px -8px rgb(35 27 20/.26);
  --lg-tint-hot:255 251 243;       --lg-a-hot:.86; --lg-ga-hot:.86;
  --lg-fg-hot:#241C14;
  --lg-edge-hot:rgb(138 95 46/1);  /* --brass: 5.46:1 */
  --lg-hi-hot:rgb(255 255 255/1);
  --lg-lo-hot:rgb(138 95 46/.28);
  --lg-spec-a-hot:.95;
  --lg-drop-hot:0 2px 2px rgb(35 27 20/.06),0 14px 26px -12px rgb(35 27 20/.34);
  --lg-drop-press:0 1px 1px rgb(35 27 20/.08),0 2px 6px -3px rgb(35 27 20/.22);
}

/* quiet (shadcn "ghost"): no plate at rest, glass condenses on hover */
.btn-sq-quiet{
  --lg-tint:255 255 255;           --lg-a:0;     --lg-ga:0;
  --lg-fg:var(--muted-foreground);
  --lg-edge:rgb(134 115 93/0);
  --lg-hi:rgb(255 255 255/0);
  --lg-lo:rgb(134 115 93/0);
  --lg-spec:255 255 255;           --lg-spec-a:0;
  --lg-shade:138 95 46;            --lg-shade-a:0;
  --lg-drop:0 0 0 transparent;
  --lg-tint-hot:255 255 255;       --lg-a-hot:.80; --lg-ga-hot:.80;
  --lg-fg-hot:#45392F;
  --lg-edge-hot:rgb(134 115 93/.45);
  --lg-hi-hot:rgb(255 255 255/.95);
  --lg-lo-hot:rgb(134 115 93/.20);
  --lg-spec-a-hot:.85;
  --lg-drop-hot:0 1px 1px rgb(35 27 20/.05),0 8px 16px -10px rgb(35 27 20/.26);
  --lg-drop-press:0 1px 2px rgb(35 27 20/.08);
}

/* secondary: stone glass (in the system; no call site yet) */
.btn-sq-secondary{
  --lg-tint:245 240 230;           --lg-a:.90;   --lg-ga:.90;
  --lg-fg:var(--secondary-foreground);
  --lg-edge:rgb(134 115 93/.45);
  --lg-hi:rgb(255 255 255/.90);
  --lg-lo:rgb(134 115 93/.20);
  --lg-spec:255 255 255;           --lg-spec-a:.50;
  --lg-shade:138 95 46;            --lg-shade-a:.08;
  --lg-drop:0 1px 1px rgb(35 27 20/.05),0 6px 14px -8px rgb(35 27 20/.22);
  --lg-tint-hot:240 233 220;       --lg-a-hot:.96; --lg-ga-hot:.96;
  --lg-fg-hot:#45392F;
  --lg-edge-hot:rgb(134 115 93/.60);
  --lg-hi-hot:rgb(255 255 255/1);
  --lg-lo-hot:rgb(134 115 93/.26);
  --lg-spec-a-hot:.85;
  --lg-drop-hot:0 2px 2px rgb(35 27 20/.06),0 14px 26px -12px rgb(35 27 20/.30);
  --lg-drop-press:0 1px 1px rgb(35 27 20/.08),0 2px 6px -3px rgb(35 27 20/.20);
}

/* destructive: ember glass (in the system; no call site yet) */
.btn-sq-danger{
  --lg-tint:168 54 42;             --lg-a:.94;   --lg-ga:.94;
  --lg-fg:var(--destructive-foreground);
  --lg-edge:rgb(255 226 214/.12);
  --lg-hi:rgb(255 226 214/.34);
  --lg-lo:rgb(60 10 5/.45);
  --lg-spec:255 236 228;           --lg-spec-a:.14;
  --lg-shade:0 0 0;                --lg-shade-a:.20;
  --lg-tint-hot:150 46 36;         --lg-a-hot:.97; --lg-ga-hot:.97;
  --lg-fg-hot:#FFFCF6;
  --lg-edge-hot:rgb(255 226 214/.18);
  --lg-hi-hot:rgb(255 226 214/.44);
  --lg-lo-hot:rgb(60 10 5/.50);
  --lg-spec-a-hot:.12;
}

/* ══ OVER FOOTAGE ════════════════════════════════════════════════════
   Paper glass on the film is invisible (clear) or a hole (smoked), so the
   film re-strikes both materials. On dark footage a dark bottom rim and a
   dark drop ground nothing, so the shade rim becomes a dim LIGHT line and
   the hot state adds a faint warm glow. --lg-ga is the thinner tint used
   only when the blur is really running (THE BLUR GATE). */

/* champagne glass: the primary CTA on the film */
:is(.film,.hero,.walk) :is(.btn-sq,.btn-solid){
  --lg-tint:239 227 188;           --lg-a:.90;   --lg-ga:.76;
  --lg-fg:#0F0D0A;
  --lg-edge:rgb(255 250 235/.55);
  --lg-hi:rgb(255 255 255/.85);
  --lg-lo:rgb(110 84 46/.40);
  --lg-spec:255 255 255;           --lg-spec-a:.30;
  --lg-shade:110 80 40;            --lg-shade-a:.22;
  --lg-drop:0 1px 0 rgb(0 0 0/.35),0 12px 28px -10px rgb(0 0 0/.75);
  --lg-tint-hot:249 242 220;       --lg-a-hot:.95; --lg-ga-hot:.86;
  --lg-fg-hot:#0F0D0A;
  --lg-edge-hot:rgb(255 250 235/.80);
  --lg-hi-hot:rgb(255 255 255/.95);
  --lg-lo-hot:rgb(110 84 46/.46);
  --lg-spec-a-hot:.70;
  --lg-drop-hot:0 2px 0 rgb(0 0 0/.30),0 18px 36px -12px rgb(0 0 0/.80),0 0 26px -8px rgb(239 227 188/.35);
  --lg-drop-press:0 1px 0 rgb(0 0 0/.40),0 4px 10px -4px rgb(0 0 0/.60);
  --lg-ring:var(--film-accent);
}

/* smoked glass: the ghost CTA on the film, and the header CALL while the
   nav is transparent over the film. Hover goes darker with a brighter rim,
   never a lighter tint: a light wash over lit footage would drop
   --film-display under 4.5:1. */
:is(.film,.hero,.walk) :is(.btn-sq-ghost,.btn-ghost),
.is-over-film .btn{
  --lg-tint:18 15 12;              --lg-a:.50;   --lg-ga:.40;
  --lg-fg:var(--film-display);
  --lg-edge:rgb(216 188 158/.80);  /* reads against lit footage at the sides too */
  --lg-hi:rgb(239 227 188/.34);
  --lg-lo:rgb(216 188 158/.12);    /* dim LIGHT rim: a dark one vanishes */
  --lg-spec:255 244 214;           --lg-spec-a:.10;
  --lg-shade:0 0 0;                --lg-shade-a:.28;
  --lg-drop:0 8px 22px -10px rgb(0 0 0/.70);
  --lg-tint-hot:30 25 20;          --lg-a-hot:.62; --lg-ga-hot:.52;
  --lg-fg-hot:#EFE3BC;
  --lg-edge-hot:rgb(216 188 158/1);
  --lg-hi-hot:rgb(239 227 188/.56);
  --lg-lo-hot:rgb(216 188 158/.20);
  --lg-spec-a-hot:.12;   /* a light sheen under the label thins the dark plate */
  --lg-drop-hot:0 14px 30px -12px rgb(0 0 0/.80),0 0 22px -10px rgb(216 188 158/.30);
  --lg-drop-press:0 3px 8px -4px rgb(0 0 0/.70);
  --lg-ring:var(--film-accent);
}

/* the hero video's pause control: same smoked glass, square (no markup
   today; kept correct for when it ships) */
.hero-pause{
  --lg-tint:18 15 12;              --lg-a:.55;   --lg-ga:.55;
  --lg-fg:var(--film-body);
  --lg-edge:rgb(74 66 59/.9);      /* --film-rule */
  --lg-hi:rgb(239 227 188/.34);
  --lg-lo:rgb(216 188 158/.12);
  --lg-spec:255 244 214;           --lg-spec-a:.08;
  --lg-shade:0 0 0;                --lg-shade-a:.28;
  --lg-drop:0 6px 16px -8px rgb(0 0 0/.70);
  --lg-tint-hot:30 25 20;          --lg-a-hot:.66; --lg-ga-hot:.66;
  --lg-fg-hot:#EFE3BC;
  --lg-edge-hot:rgb(216 188 158/.60);
  --lg-hi-hot:rgb(239 227 188/.56);
  --lg-lo-hot:rgb(216 188 158/.20);
  --lg-spec-a-hot:.26;
  --lg-drop-hot:0 10px 22px -10px rgb(0 0 0/.80);
  --lg-drop-press:0 2px 6px -3px rgb(0 0 0/.70);
  --lg-ring:var(--film-accent);
}

/* The dock chip floats over paper AND footage: smoked plate plus a
   champagne edge that reads on both.
   ⚠️ The old `.dock .dock-cta{background:var(--champagne)}` is 0,2,0 and
   beat a 0,1,0 base, so this rule re-declares the background itself at
   0,2,0, later in source. */
.dock .dock-cta{
  --lg-ga:.86;
  --lg-edge:rgb(239 227 188/.30);
  --lg-edge-hot:rgb(239 227 188/.50);
  --lg-drop:0 1px 1px rgb(0 0 0/.18),0 8px 18px -8px rgb(0 0 0/.55);
  --lg-drop-hot:0 2px 2px rgb(0 0 0/.18),0 14px 26px -10px rgb(0 0 0/.60);
  --lg-drop-press:0 1px 2px rgb(0 0 0/.30);
  color:var(--lg-fg);
  background-image:none;
  background-color:rgb(var(--lg-tint)/var(--lg-a));
}

/* Two older rules set label colour at 0,3,0 with literal tokens. Pinned so
   --lg-fg is the one source of label colour in every state. */
.film-cta .btn-sq-ghost,
.nav.is-over-film .btn,
.nav.is-over-film .btn-ghost{color:var(--lg-fg)}

/* ══ STATES ══════════════════════════════════════════════════════════ */

/* Sticky :hover on touch. `.btn-solid:hover`, `.btn-ghost:hover`,
   `.dock:hover .dock-cta` and `a:hover` (0,1,1) live OUTSIDE any hover
   media query, so after a tap they would repaint an opaque copper plate or
   a dark label. This re-asserts the glass at equal or higher specificity,
   later in source. On touch the tokens are still the REST tokens, so a
   tapped button simply stays glass. */
.btn:hover,.btn-sq:hover,.hero-pause:hover,
.dock:hover .dock-cta{
  color:var(--lg-fg);
  background-image:none;
  background-color:rgb(var(--lg-tint)/var(--lg-a));
}

/* hover: ONE token swap for every variant and context, mouse only */
@media (hover:hover) and (pointer:fine){
  .btn:hover,.btn-sq:hover,.hero-pause:hover,.dock:hover .dock-cta{
    --lg-tint:var(--lg-tint-hot);  --lg-a:var(--lg-a-hot);  --lg-ga:var(--lg-ga-hot);
    --lg-fg:var(--lg-fg-hot);
    --lg-edge:var(--lg-edge-hot);
    --lg-hi:var(--lg-hi-hot);      --lg-lo:var(--lg-lo-hot);
    --lg-spec-a:var(--lg-spec-a-hot);
    --lg-drop:var(--lg-drop-hot);
    --lg-lift:-2px;
  }
}

/* Keyboard gets the lit state too, minus the pointer-only tilt. The global
   :focus-visible sets border-radius:2px, so the radius is pinned here. */
.btn:focus-visible,.btn-sq:focus-visible,.hero-pause:focus-visible{
  outline:2px solid var(--lg-ring,var(--ring));outline-offset:3px;
  border-radius:var(--radius);
  --lg-edge:var(--lg-edge-hot);
  --lg-spec-a:var(--lg-spec-a-hot);
  --lg-drop:var(--lg-drop-hot);
}
.dock:focus-visible .dock-cta{--lg-edge:var(--lg-edge-hot);--lg-spec-a:var(--lg-spec-a-hot);--lg-drop:var(--lg-drop-hot)}

/* press: the bevel INVERTS (shade moves to the top, so it reads concave),
   the shadow collapses to contact, and the plate sinks. The edge stays on
   top, so the boundary never weakens mid-press. */
.btn:active,.btn-sq:active,.hero-pause:active,.dock:active .dock-cta{
  --lg-lift:1px;--lg-z:-14px;
  box-shadow:inset 0 0 0 1px var(--lg-edge),inset 0 2px 3px var(--lg-lo),inset 0 -2px 0 var(--lg-hi),var(--lg-drop-press);
  transform:translateY(var(--lg-lift)) scale(.97);
  transition:
    transform .12s cubic-bezier(.2,.7,.2,1),
    box-shadow .12s ease-out,
    background-color .22s ease,color .22s ease,
    --lg-mx .18s ease-out,--lg-my .18s ease-out,--lg-spec-a .28s ease;
}

/* .lg-on is set by site.js only while a MOUSE is on the button. It is the
   only state with a 3D transform, and it comes after :active so a press
   under the pointer sinks along Z and keeps its tilt. */
.btn.lg-on,.btn-sq.lg-on,.hero-pause.lg-on,.dock-cta.lg-on,
.dock:active .dock-cta.lg-on{
  transform:perspective(var(--lg-persp)) translateY(var(--lg-lift)) translateZ(var(--lg-z)) rotateX(var(--lg-rx)) rotateY(var(--lg-ry));
}

/* disabled: shadcn convention kept; the glass goes flat */
.btn-sq[disabled],.btn-sq[aria-disabled="true"]{--lg-drop:0 0 0 transparent;--lg-spec-a:0}

/* ══ THE BLUR GATE ═══════════════════════════════════════════════════
   Every button above is already finished glass. The blur is added on top
   ONLY where something moves behind it, so it is visible and worth its
   render surface, and at most ONE backdrop-filter per button:
   · .film-cta buttons: the two landing CTAs, over the scrubbing footage;
   · the header CALL while .nav.is-over-film — that same class switches the
     nav's own blur off, so there is still one blur there; once the nav
     leaves the film it blurs itself and the CALL stops qualifying;
   · the dock chip, which is fixed and has the page scrolling under it.
   Paper buttons never blur: a blur over flat paper returns flat paper.
   Gated on html[data-fx="full"] (fx.js desktop tier; lite, still and no-JS
   never blur, and a watchdog demotion removes it live) and inside
   @supports, so an engine without backdrop-filter keeps the baked tint.
   The gate swaps the READ token to --lg-ga (a literal): no math in rgb(). */
@supports ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){
  html[data-fx="full"]:not([data-fx-transparency="reduce"]) .film-cta .btn-sq,
  html[data-fx="full"]:not([data-fx-transparency="reduce"]) .nav.is-over-film .btn,
  html[data-fx="full"]:not([data-fx-transparency="reduce"]) .dock .dock-cta{
    background-color:rgb(var(--lg-tint)/var(--lg-ga));
    -webkit-backdrop-filter:blur(var(--lg-blur)) saturate(var(--lg-sat));
    backdrop-filter:blur(var(--lg-blur)) saturate(var(--lg-sat));
  }
}

/* ══ PREFERENCES ═════════════════════════════════════════════════════ */
@media (prefers-reduced-transparency:reduce){
  .btn,.btn-sq,.hero-pause,.dock .dock-cta,
  .btn:hover,.btn-sq:hover,.hero-pause:hover,.dock:hover .dock-cta{background-color:rgb(var(--lg-tint))}
  .btn::after,.btn-sq::after,.dock-cta::after,.hero-pause::after{content:none}
}
@media (prefers-reduced-motion:reduce){
  .btn,.btn-sq,.hero-pause,.dock-cta,
  .btn:hover,.btn-sq:hover,.hero-pause:hover,.dock:hover .dock-cta,
  .btn:active,.btn-sq:active,.hero-pause:active,.dock:active .dock-cta,
  .btn.lg-on,.btn-sq.lg-on,.hero-pause.lg-on,.dock-cta.lg-on,
  .dock:active .dock-cta.lg-on{--lg-lift:0px;transform:none}
  .btn .btn-ico{transform:none}
}
/* Forced colours drop box-shadow, so the edge would vanish: restore a real
   border and hand back the pixel the padding borrowed. */
@media (forced-colors:active){
  .btn,.btn-sq,.hero-pause,.dock-cta{border:1px solid ButtonText}
  .btn{padding:1.0625rem 1.75rem}
  .btn-sm{padding:.75rem 1.1rem}
  .btn-sq{padding:1rem 1.9rem}
  .btn-sq-sm{padding:.6rem 1rem}
  .dock .dock-cta{padding:calc(.5rem - 1px) calc(.75rem - 1px)}
  .btn::after,.btn-sq::after,.hero-pause::after,.dock-cta::after{content:none}
}

/* ── repair: mascot ── */
/* ═══ MASCOT ON THE FILM + DOCK ═══════════════════════════════════════════
   The wrapper sits INSIDE .film-beat. scrub.js rewrites the beat's inline
   transform/opacity/visibility every frame, so nothing here touches the beat:
   all motion stays on .mascot and .mascot::before, which scrub.js never sees.
   ═══════════════════════════════════════════════════════════════════════ */

/* CONSENT FIRST. The mascot ships on one condition (README, Compliance): it
   renders only after the 21+ confirmation. site.js adds .age21-ok to <html>
   on a fresh "I am 21 or older" click or a remembered one, and only then
   starts the rig. Without it neither the hero sprite nor the dock has a box:
   nothing animates, and a ::before on a display:none element fetches no
   background image, so the sheets are not even requested. !important because
   this is a legal gate and the appended override blocks below this file's
   top are decided by source order; nothing later may re-show him by accident. */
html:not(.age21-ok) .mascot,
html:not(.age21-ok) .dock{display:none !important}

/* Hug the button row, so left:0 is the left edge of "Get directions" and the
   walk-on crosses the buttons. The 2rem that .film-cta carried moves to the
   wrapper, so there is one margin to open.

   HEADROOM FOLLOWS THE MASCOT. Room for him (the sprite's rendered height plus
   air, the old .hero-cta derivation) exists only while he is really on:
   startMascot() adds html.mascot-on after the 21+ confirmation, and never with
   VAPE_MASCOT off. Until then the buttons sit their usual 2rem under the h1,
   so a first visit shows no empty band, and switching him off restores the
   original layout. A remembered confirmation loads with the room already
   there and nothing moves; only a fresh "I am 21 or older" click (which also
   sets html.age21-fresh) eases it open while the walk-on starts. */
.film .mascot-btn{
  width:fit-content;max-width:100%;margin-inline:auto;
  margin-top:2rem;
}
html.mascot-on .film .mascot-btn{margin-top:calc(var(--vc) * 329 / 264 + 20px)}
html.age21-fresh .film .mascot-btn{transition:margin-top .5s cubic-bezier(.2,.7,.2,1)}
@media (prefers-reduced-motion:reduce){
  html.age21-fresh .film .mascot-btn{transition:none}
}
.film .mascot-btn > .film-cta{margin-top:0}

/* The sprite has a black cap, arms and shoes that vanish on the dark scrim.
   A faint champagne rim restores the outline; the dark contact shadow seats
   him on the button. On .mascot, never on the beat. */
.film .mascot{
  filter:drop-shadow(0 0 1px rgb(239 227 188/.55))
         drop-shadow(0 1px 2px rgb(239 227 188/.25))
         drop-shadow(0 10px 14px rgb(11 10 9/.55));
}
@media (max-width:900px){
  .film .mascot-btn{width:100%}
}
/* Stacked CTAs: the phone button is BELOW "Get directions", so hopping onto
   it parks his body across the primary label. site.js writes the hop vector
   inline, so !important on the two properties is the one CSS-side override,
   and an !important cannot be undone later, only not applied: hence the
   min-height. Short landscape screens un-stack the row (below) and keep the
   real hop onto the phone button. */
@media (max-width:900px) and (min-height:521px){
  .film .mascot{--hx:0px !important;--hy:0px !important}
}

/* SHORT SCREENS (landscape phones: 844x390, 740x360, 667x375, 568x320).
   Beat 1 is centred in a stage that runs UNDER the transparent nav, and the
   mascot's headroom made the block tall enough to push the h1 up under it.
   Three moves, all scoped to short viewports so desktop and portrait are
   untouched:
     1. the centring area starts below the nav, so the block can never slide
        under it (padding on the grid, not a transform: scrub.js owns that);
     2. the rig shrinks, which shrinks the derived headroom with it;
     3. the CTAs sit side by side (there is width to spare in landscape), so
        the row is one button tall and the hop onto the phone button is the
        real one again. */
@media (max-height:520px){
  .film .film-beats{padding-top:var(--nav-h,80px)}
  .film .mascot-btn{--vc:52px;width:fit-content}
  .film .mascot-btn .film-cta{width:auto;flex-wrap:nowrap}
  .film .mascot-btn .btn-sq{flex:0 1 auto;padding:.8rem 1.25rem}
}

/* ── the dock ───────────────────────────────────────────────────────── */
/* On paper: a warm contact shadow. A 60% black blur on #FFFCF6 is a smudge. */
.dock img{
  filter:drop-shadow(0 1px 1px rgb(36 28 20/.18))
         drop-shadow(0 10px 14px rgb(36 28 20/.20));
}
/* Over the film (.on-film is set by site.js from the film's rect against the
   dock's own middle, not from the nav, which leaves the film a whole viewport
   later): the sprite takes the hero rim, and the plate takes the film palette.
   The paper plate is #241C14, a near-black slab whose edges vanish into the
   scrim. The film plate is the .btn-sq material: champagne with dark type. */
.dock.on-film img{
  filter:drop-shadow(0 0 1px rgb(239 227 188/.55))
         drop-shadow(0 1px 2px rgb(239 227 188/.25))
         drop-shadow(0 10px 14px rgb(11 10 9/.55));
}
.dock.on-film .dock-cta{
  background:var(--film-display);color:#0F0D0A;
  box-shadow:0 0 0 1px rgb(11 10 9/.35),0 6px 16px -6px rgb(11 10 9/.6);
}
.dock.on-film:hover .dock-cta{background:var(--film-accent)}
.dock.on-film:focus-visible{outline-color:var(--film-display)}


/* APPEND AT THE VERY END OF style.css. It must come after both earlier
   .film-head rules (≈L1088 and ≈L1391) so it wins on source order. */

/* ══════════════════════════════════════════════════════════════════════
   THE WORDMARK — warm chrome over footage

   The client rejected Cinzel on the hero and asked for the name to be
   smaller, "glassy chromium", and "animated a bit". This block answers all
   four and touches nothing outside the film.

   TYPEFACE — Archivo (Omnibus-Type), static instance wdth 88 · wght 700.
   Chrome is drawn as a VERTICAL value ramp, and a ramp needs stroke to live
   in. Cinzel's flared, tapering terminals collapse the ramp to one flat
   colour at every serif, and any hairline face (Bodoni, Playfair) does the
   same thing worse. Archivo is a sign grotesque — low contrast, flat sides,
   square shoulders — so every part of every letter carries the whole ramp.
   And its width axis is what makes "smaller" possible: measured, the full
   name is 11.36em at wdth 88 / .06em tracking against 12.49em at wdth 100
   and 12.11em for Cinzel, so it sits on ONE line inside the 40rem beat.

   SCOPE — the FACE changes on all four .film-head beats so the film speaks
   in one voice. The METAL is on the h1 only (.film-wordmark): chrome on
   "Ask us anything." is WordArt.
   ══════════════════════════════════════════════════════════════════════ */
:root{
  --wordmark:'Archivo','Arial Narrow',Arial,system-ui,sans-serif;

  /* The chrome ramp. Every hue is the brand mark's own isfMetal / isfSheen
     gradient or the --film-* family — no new colours — but the DARK end is
     lifted a long way. Measured behind the h1 after .film-veil: backdrop
     luminance p50 .040, max .088. The mark's #6B4A24 would sit at 1.2:1
     against that and the lower half of every letter would drop out of the
     footage. --chrome-ground is the floor: the darkest value that still
     reads as "reflected ground" against the cream above it. */
  --chrome-spec:#FFF8E4;    /* isfSheen. The specular tip and the edge rims */
  --chrome-sky:#E6D6B0;     /* sky brightening toward the horizon           */
  --chrome-zenith:#BFA47F;  /* upper band: sky darkens away from horizon   */
  --chrome-ground:#9A8266;  /* ⚠️ FLOOR. The reflected ground, just below
                               the horizon. Do not darken — see above.
                               Rendered: 3.16–3.25:1 worst case against
                               the backdrop 2–4px from the glyph.          */
  --chrome-lift:#AE9676;    /* ground lifting toward the viewer           */
  --chrome-bounce:#D8BC9E;  /* = --film-accent. Near-field bounce light     */
  --chrome-rim:#F3E8C8;     /* bottom rim, sitting on the baseline          */
}

/* ── size — all four beats ────────────────────────────────────────────
   Was clamp(2.5rem,6vw,5rem): 76.8px at a 1280 viewport, 80px from 1333 up,
   and at that size the name needed 969px against a 640px beat, so it broke
   onto two lines at every width.
   Now 36px → 52px. One line from ~450px wide upward; below that it breaks
   into the two-unit lockup INFINITY / SMOKE SHOP, never mid-pair.         */
.film-head{
  font-family:var(--wordmark);
  font-weight:700;
  font-stretch:88%;          /* must match the served instance exactly —
                                the link requests wdth 88, not a range    */
  font-size:clamp(2.25rem,1.2rem + 2.8vw,3.25rem);
  line-height:1.08;
  letter-spacing:.06em;
  font-kerning:normal;
}

/* ── the metal — h1 only ──────────────────────────────────────────────
   Two nested spans per lockup unit, and the split is load-bearing:
     .wm-w  takes the settle MOTION (transform/opacity) and the SHADOW
     .wm-m  takes the METAL (background-clip:text) and the GLINT
   WebKit has a long history of dropping background-clip:text on an element
   that is itself composited by a transform animation. Keeping transform
   off the clipped element sidesteps it. And neither span is .film-beat,
   which scrub.js rewrites ~60×/s.                                          */

/* LOAD-BEARING. .film-head carries a four-layer text-shadow, and
   text-shadow INHERITS. On background-clip:text with a transparent fill,
   Chrome paints the shadow ABOVE the clipped background, so the inherited
   champagne bloom would fog the metal flat. Shadows move to filter on the
   outer span instead, which composites underneath. */
.film-wordmark{text-shadow:none;line-height:1.12}

.wm-w{
  display:inline-block;
  white-space:nowrap;        /* "SMOKE SHOP" is one unit on every width  */
  /* Replaces backdrop-filter, deliberately. Backdrop blur is reserved for
     the glass controls and the stuck nav, and a blur sampling a scrubbing
     video re-runs on every decoded frame. A drop-shadow is computed from the
     GLYPHS, not the backdrop, so it cannot shimmer with the footage: it
     presses a tight pool of shade into the frame right behind the letters,
     which is what keeps --chrome-ground legible.
       1. 1px hard contact edge — the underside of the bevel
       2. tight dense halo — the local contrast floor                     */
  filter:
    drop-shadow(0 1px 0 rgb(11 10 9/.9))
    drop-shadow(0 0 .2em rgb(11 10 9/.72));
}

.wm-m{
  display:inline-block;
  line-height:1;             /* the ramp's em stops assume a 1em box     */
  color:var(--film-display); /* FALLBACK: no background-clip:text → the
                                solid champagne this h1 already ships in,
                                15.43:1 on the veil                        */
}

@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .wm-m{
    /* Stops are in EM, not %, so the horizon lands on the CAPS rather than
       on the line box. Archivo in a 1em box: cap top .144em, baseline
       .830em. The horizon sits at .495em, 51% of cap height, a hair above
       the optical middle, where a polished surface under a high sky
       puts it.

       Top to bottom, the thing that makes it read as chrome and not as a
       gradient is the HARD CUT at .495em — sky above, ground below, no
       blend. Everything else is supporting cast.                        */
    background-image:
      /* layer 1 — the glint. Parked off the left edge at rest, so the
         static state is finished metal with no stray highlight in it.
         Angled 105deg so it travels as a raked light, not a wipe.         */
      linear-gradient(105deg,
        rgb(255 255 255/0)   30%,
        rgb(255 255 255/.9)  50%,
        rgb(255 255 255/0)   70%),
      /* layer 2 — the ramp */
      linear-gradient(180deg,
        var(--chrome-spec)    0,
        var(--chrome-spec)   .17em,   /* top rim: a polished edge          */
        var(--chrome-zenith) .25em,   /* upper band                        */
        var(--chrome-sky)    .39em,
        var(--chrome-spec)   .47em,   /* brightest, just above horizon     */
        var(--chrome-spec)   .49em,
        var(--chrome-ground) .50em,   /* ── THE HORIZON ── hard inversion  */
        var(--chrome-lift)   .62em,
        var(--chrome-bounce) .76em,
        var(--chrome-rim)    .83em,   /* bottom rim, on the baseline       */
        var(--chrome-rim)    1em);
    background-size:2.6em 100%,100% 100%;
    background-repeat:no-repeat;
    background-position:-2.6em 0,0 0;
    -webkit-background-clip:text;
            background-clip:text;
    -webkit-text-fill-color:transparent;
  }
  /* A transparent fill selects as nothing, so the highlight would sit over
     the metal with no readable letters in it. */
  .wm-m::selection{background:rgb(239 227 188/.92);-webkit-text-fill-color:#0F0D0A;color:#0F0D0A}
}

/* ── motion — "a bit" ─────────────────────────────────────────────────
   1. SETTLE. On load each unit rises .24em and fades in, 120ms apart.
   2. GLINT. One raked highlight crosses the name, then rests. The pass is
      13% of an 11s cycle, about 1.4s, so what the viewer sees is an
      occasional catch of light, not a loop. The first pass starts at .75s,
      as the words land. SMOKE SHOP runs 1s behind INFINITY, which is
      roughly when the band clears the first word, so on one line it reads
      as a single light travelling left to right.

   Gating: `:not([data-fx="still"])` instead of `[data-fx="full"]`, because
   fx.js loads synchronously AFTER the h1, so data-fx is absent at first
   paint. A positive match would start the fade late and flash the finished
   h1 before hiding it. It still switches off when the frame watchdog
   demotes to still. Everything above this block is the finished, static
   wordmark; nothing here is required for it to read.                      */
@media (prefers-reduced-motion:no-preference){
  html:not([data-fx="still"]) .wm-w{
    animation:wmSettle .9s cubic-bezier(.2,.7,.2,1) both;
  }
  html:not([data-fx="still"]) .wm-w + .wm-w{animation-delay:.12s}

  html:not([data-fx="still"]) .wm-m{
    animation:wmGlint 11s cubic-bezier(.45,.05,.4,1) .75s infinite both;
  }
  html:not([data-fx="still"]) .wm-w + .wm-w .wm-m{animation-delay:1.75s}
}
/* from .01, not 0 (integration fix, mobile pass 2026-09-11): with the fonts
   self-hosted they are in hand at first paint, and beats 2-4 no longer paint
   then either, so the hero name is the largest thing in the first frame. At
   opacity 0 Chrome skips it, and the desktop LCP moved from first paint
   (~0.2 s) to the wmGlint repaint (~1.9 s). 1% is invisible (the glyphs are
   still sliding up under the nav) and makes the first frame count. */
@keyframes wmSettle{
  from{opacity:.01;transform:translateY(.24em)}
  to  {opacity:1;transform:none}
}
/* background-position lists BOTH layers every step; only layer 1 moves.
   calc(100% + 2.6em) parks the band just past the right edge of whichever
   word it is on: 100% resolves against (word − band), so adding the band
   width back puts its left edge exactly at the word's right edge. */
@keyframes wmGlint{
  0%  {background-position:-2.6em 0,0 0}
  13% {background-position:calc(100% + 2.6em) 0,0 0}
  100%{background-position:calc(100% + 2.6em) 0,0 0}
}

/* Windows High Contrast: a transparent fill would render as nothing. */
@media (forced-colors:active){
  .wm-m{background:none;-webkit-text-fill-color:CanvasText;color:CanvasText}
  .wm-w{filter:none}
}


/* ══════════════════════════════════════════════════════════════════════
   TYPE SYSTEM — matched to the REYES ◉ HALE demo site the client pointed at
   (a polidori.dev reel): a wide, heavy grotesk for display, small widely
   tracked mono caps for labels and buttons.

   No exact typeface could be named - that demo site is not public and its
   type is ~5px in the reel - so this is a MEASURED match. Five Google Fonts
   candidates were rendered side by side and confirmed loaded (no silent
   fallback): Archivo at width 125 was the closest to the reel's crisp,
   squared, architectural logo. Unbounded's rounded terminals read softer;
   Anybody and Lexend Zetta were too wide and too light.

   Archivo is also the family the chrome wordmark was verified on. The width
   axis does not change vertical metrics, so its em-based ramp stops (cap top
   .144em, baseline .830em) stay correct at width 125.

   The face is ~27% wider than Cinzel at the same size ("INFINITY SMOKE SHOP":
   15.48em vs 12.11em), so every display size comes down ~0.8x to keep line
   lengths, and tracking comes IN - Cinzel's values were set for serif caps.

   Body copy stays Inter: long-form readability, and the reel never showed
   paragraph type to match.
   ══════════════════════════════════════════════════════════════════════ */
:root{
  --display:'Archivo','Arial Black',system-ui,-apple-system,'Segoe UI',sans-serif;
  --display-stretch:125%;     /* must match the served instance exactly */
  --wordmark:var(--display);
}
.d-xl,.d-l,.d-m,.d-s,.brand-name,.faq-item h3,.foot-mark,.foot h2,
.agegate-title,.film-head{font-family:var(--display);font-stretch:var(--display-stretch)}

.d-xl{font-weight:800;font-size:clamp(2.1rem,5.2vw,4.4rem);line-height:1;letter-spacing:.005em}
.d-l {font-weight:800;font-size:clamp(1.6rem,3.2vw,2.5rem);line-height:1.06;letter-spacing:.01em}
.d-m {font-weight:700;font-size:1.2rem;line-height:1.15;letter-spacing:.02em}
.d-s {font-weight:700;font-size:.95rem;line-height:1.2;letter-spacing:.03em}
.faq-item h3{font-weight:700;font-size:.95rem;letter-spacing:.02em}
.brand-name{font-weight:800;letter-spacing:.04em;font-size:clamp(.8125rem,1.2vw,.9375rem)}
.foot h2{font-weight:700;font-size:.8125rem;letter-spacing:.08em}
.agegate-title{font-weight:700;font-size:.8125rem;letter-spacing:.06em}
.foot-mark{font-weight:800;letter-spacing:.02em}

/* The hero. Measured one-line limit for the full name at width 125 in the
   640px beat is 41.3px, so the cap is 40px - which also answers the client's
   "make them smaller". Below ~450px it breaks into the INFINITY / SMOKE SHOP
   lockup, never mid-pair (.wm-w is nowrap). */
.film-head{font-weight:800;font-size:clamp(1.6rem,1rem + 2.2vw,2.5rem);letter-spacing:.02em}

/* Buttons and nav links take the reel's label voice: "ARRANGE A VIEWING" is
   small, widely tracked mono caps. */
.btn,.btn-sq,.nav-links a{font-family:var(--mono);font-weight:500;letter-spacing:.16em}
.btn,.btn-sq{font-size:.8125rem}
.btn-sm,.btn-sq-sm{font-size:.75rem;letter-spacing:.14em}

/* ══════════════════════════════════════════════════════════════════════
   POST-FIX PASS — from the adversarial verifiers, each measured on the render

   1. THE WALL SEAM. .wall-copy kept max-width:var(--shell) + margin-inline:auto,
      so on any viewport wider than 1240px the paper veil began abruptly at the
      shell edge on top of the photograph: a hard vertical seam (column-to-column
      |dRGB| 107 at x=100 @1440). The veil now spans the band and the COPY is
      padded to the shell instead, so the text still lines up with every other
      section while the veil dissolves as the comment always said it did.

   2. THE DOCK'S GLASS STOPPED PAINTING. `.dock.drop{animation:dockDrop … both}`
      keeps a finished animation filling forever; Chrome then keeps the chip on
      an animated layer and drops its backdrop blur. The computed style still
      reports blur(12px), which is how an earlier computed-style check passed
      it. The 100% keyframe equals the dock at rest, so filling BACKWARDS only
      changes nothing visible - except that the blur paints.

   3. THE 21+ CARD ON PHONES, actually small. It was a stacked block - title,
      two lines of copy, two full-width buttons on two rows - 198px on a 390x844
      phone (23.5%) and 219px on 360x740 (29.6%), over the hero. A first attempt
      moved it 7px because its selectors lost on specificity to
      `.agegate-actions .btn-sq{flex:1 1 100%}`; everything here is anchored on
      #agegate. Now 137px (16.2% / 18.4%), buttons side by side, 44px tap targets,
      decline still a real button, no clipped labels.
   ══════════════════════════════════════════════════════════════════════ */
.wall-copy{max-width:none;padding-inline:max(var(--gutter),calc((100% - var(--shell))/2 + var(--gutter)))}

.dock.drop{animation-fill-mode:backwards}

@media (max-width:560px){
  #agegate.agegate{left:10px;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:auto;max-width:none;padding:12px 14px;gap:6px}
  #agegate .agegate-title{font-size:.6875rem;margin:0}
  #agegate .agegate-copy{font-size:.75rem;line-height:1.35;margin:0}
  #agegate .agegate-actions{flex-wrap:nowrap;gap:8px;margin-top:4px}
  #agegate .agegate-actions .btn-sq{flex:1 1 0;min-width:0;min-height:44px;padding-inline:6px;font-size:.6875rem;letter-spacing:.08em;white-space:nowrap}
}

/* ══════════════════════════════════════════════════════════════════════
   WHAT WE CARRY — category tabs left, photo spiral right
   ══════════════════════════════════════════════════════════════════════
   build.py splits the catTitle section into .cat-split when the spiral
   manifest (img/spiral/spiral.json) has items; with no manifest the
   section is the old three-column block, untouched. spiral.js (a vanilla
   port of React Bits InfiniteSpiral) does the motion; this block lays it
   out and paints it paper.

   LAYOUT
   · ≥901px  the proportions of .feature just below it: 5fr / 7fr on the
             same gap scale. Heading, the tabs (one column) and the kava
             line on the left; the spiral stretches to the left column's
             height and is never under 35rem (560px).
             The tabs become rows there (icon in a left gutter): stacked
             tiles made a 1410px column at 1440 wide and the spiral beside
             it was mostly empty paper.
   · ≤900px  stacked in reading order: heading, tabs (two columns, one at
             ≤640px, their original layout), kava line, then the spiral
             stage at 26.5rem (21.25rem on phones), capped to the screen
             height so a landscape phone still sees the whole stage.
   Every tile class, icon, glass face, hover, press and the reveal stagger
   are the same elements with the same rules; only their grid changes.

   MATERIAL. Cards are paper prints: var(--radius), a 1px light edge, and a
   soft drop in the site's ink hue rgb(35 27 20) — the hue every button
   drop already uses — not upstream's purple rgb(8 6 18). The stage has no
   background at all: the helix floats on the page's own paper, so there is
   nothing dark behind it and no frame edge to slice a card against.

   TUNABLES are --spiral-* custom properties, per breakpoint; spiral.js
   re-reads them on every resize. A data-* attribute on [data-spiral] wins.
   ══════════════════════════════════════════════════════════════════════ */
.spiral{
  --spiral-speed:.55;           /* photos per second (upstream default)     */
  --spiral-card-width:172px;    /* base card, before focus + depth scale    */
  --spiral-card-ratio:.75;      /* height / width: every photo is 4:3       */
  --spiral-radius:200px;
  --spiral-spacing:62px;        /* vertical px per photo along the helix    */
  --spiral-per-turn:7;
  --spiral-center-scale:1.2;
  --spiral-edge-fade:.3;
  --spiral-edge-blur:0px;       /* off: measured, and muddy on paper — PERFORMANCE in spiral.js */
  --spiral-scroll:0;            /* scroll coupling off — MOTION in spiral.js */

  position:relative;display:flex;flex-direction:column;min-width:0;
}
.spiral-stage{position:relative;flex:1 1 auto;min-height:0;overflow:hidden}
.spiral-bar{flex:none;display:flex;justify-content:flex-end;align-items:center;padding-top:.75rem}

@media (min-width:901px){
  .cat-split{
    display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);
    gap:clamp(2rem,6vw,4.5rem);align-items:stretch;
  }
  .cat-split .spiral{min-height:35rem}
  .cat-split .tiles{grid-template-columns:1fr;gap:.75rem}
  .cat-split .tile{
    display:grid;grid-template-columns:28px minmax(0,1fr);
    column-gap:1.1rem;row-gap:.2rem;align-content:center;min-height:0;
  }
  .cat-split .tile-icon{grid-row:1 / span 2;margin-top:.05rem}
}
@media (max-width:900px){
  .spiral{--spiral-card-width:150px;--spiral-radius:170px;--spiral-spacing:54px}
  .cat-split .spiral{margin-top:clamp(2.5rem,7vw,3.5rem)}
  .cat-split .spiral-stage{
    flex:none;
    height:26.5rem;height:min(26.5rem,calc(100vh - 9rem));height:min(26.5rem,calc(100svh - 9rem));
  }
}
@media (max-width:640px){
  .spiral{--spiral-card-width:124px;--spiral-radius:104px;--spiral-spacing:40px}
  .cat-split .spiral-stage{height:21.25rem;height:min(21.25rem,calc(100vh - 9rem));height:min(21.25rem,calc(100svh - 9rem))}
}
/* Landscape phones: a short stage needs smaller photos to show a turn. */
@media (max-width:900px) and (max-height:500px){
  .spiral{--spiral-card-width:112px;--spiral-radius:150px;--spiral-spacing:40px}
}

/* ── the cards ─────────────────────────────────────────────────────── */
.spiral-card{
  position:relative;overflow:hidden;border-radius:var(--radius);
  background:var(--ash);
  border:1px solid rgb(255 255 255/.85);
  box-shadow:0 1px 2px rgb(35 27 20/.08),0 18px 34px -20px rgb(35 27 20/.45);
}
.spiral-card img{
  display:block;width:100%;height:100%;max-width:none;object-fit:cover;
  -webkit-user-select:none;user-select:none;-webkit-user-drag:none;
}
/* No JS: a quiet contact sheet in the same box, never a pile of cards. */
.spiral:not(.is-live) .spiral-stage{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));
  gap:.75rem;align-content:start;padding:2px;
}
.spiral:not(.is-live) .spiral-card{aspect-ratio:4/3}
/* Live: every card sits on the stage centre and moves by transform alone. */
.spiral.is-live .spiral-card{
  position:absolute;top:50%;left:50%;
  width:var(--spiral-w,240px);height:var(--spiral-h,180px);
  transform:translate(-50%,-50%);
  will-change:transform,opacity;
}
.spiral.is-live.is-blur .spiral-card{will-change:transform,opacity,filter}
@media (hover:hover) and (pointer:fine){
  .spiral.is-live .spiral-stage{cursor:grab;-webkit-user-select:none;user-select:none}
  .spiral.is-live.is-dragging .spiral-stage{cursor:grabbing}
}

/* ── the pause toggle: the site's quiet square button, icon only ──────
   Same token contract as the .btn-sq variants above: --lg-* tokens only,
   at 0,1,0, so the shared hover swap (0,2,0) still wins. Quiet means a
   muted glyph on clear glass, plus a faint bronze edge at rest so it can
   be found without hovering. Pressed: play glyph, brass glyph and a firmer
   brass edge. 40px square, 44px on coarse pointers. The shared
   .btn-sq:focus-visible ring applies unchanged. */
.spiral-toggle{
  --lg-tint:255 255 255;           --lg-a:.6;    --lg-ga:.6;
  --lg-fg:var(--muted-foreground);
  --lg-edge:rgb(134 115 93/.42);
  --lg-hi:rgb(255 255 255/.9);
  --lg-lo:rgb(134 115 93/.14);
  --lg-spec:255 255 255;           --lg-spec-a:.4;
  --lg-shade:138 95 46;            --lg-shade-a:.06;
  --lg-drop:0 1px 1px rgb(35 27 20/.04),0 4px 10px -6px rgb(35 27 20/.20);
  --lg-tint-hot:255 255 255;       --lg-a-hot:.86; --lg-ga-hot:.86;
  --lg-fg-hot:#45392F;
  --lg-edge-hot:rgb(138 95 46/.75);
  --lg-hi-hot:rgb(255 255 255/.95);
  --lg-lo-hot:rgb(134 115 93/.20);
  --lg-spec-a-hot:.85;
  --lg-drop-hot:0 1px 1px rgb(35 27 20/.05),0 8px 16px -10px rgb(35 27 20/.26);
  --lg-drop-press:0 1px 2px rgb(35 27 20/.08);
}
/* flex:none — below 900px the site sets .btn-sq{flex:1 1 100%}, which stretched it full width */
.btn-sq.spiral-toggle{flex:none;width:40px;height:40px;padding:0;display:inline-grid;place-items:center}
.spiral-toggle svg{width:14px;height:14px;fill:currentColor}
.spiral-toggle .i-play{display:none}
.spiral-toggle[aria-pressed="true"] .i-pause{display:none}
.spiral-toggle[aria-pressed="true"] .i-play{display:block}
.spiral-toggle[aria-pressed="true"]{--lg-edge:rgb(138 95 46/.9);--lg-fg:var(--brass)}
.btn-sq.spiral-toggle[hidden]{display:none}
@media (pointer:coarse){.btn-sq.spiral-toggle{width:44px;height:44px}}

/* ═══ BEGIN WP2-FILM-SCRUB ═══ */
/* ══════════════════════════════════════════════════════════════════════
   MOBILE FILM (WP2): one script on every device
   Phones scrub the same walk as the desktop (scrub.js). Everything here
   either lets them, or is the same on a desktop by construction.
   ══════════════════════════════════════════════════════════════════════ */

/* THE STAGE FILLS THE LARGE VIEWPORT, THE COPY THE SMALL ONE.
   On a phone the toolbar slides away on the first downward swipe. A 100svh
   stage stayed at the small height and left a flat black band under the
   picture; a stage that followed the viewport moved every section below the
   film. 100lvh is the height with the toolbar gone, fixed for the page's life,
   so the picture always reaches the bottom and nothing below it moves. The
   beats keep 100svh, so they centre in what is visible WITH the toolbar.
   scrub.js measures its travel from the stage, so p never moves either.
   On a desktop svh = lvh = the window: the same box as before. The static
   layout (.film-static .film-stage, 0,2,0) still wins over this. The vh and
   100% lines are only for engines without the viewport units (before Safari
   15.4 / Chrome 108), where the base rule left the stage with no height. */
.film-stage{height:100vh;height:100svh;height:100lvh}
.film-beats{bottom:auto;height:100%;height:100svh}
.film-static .film-beats{height:auto}

/* Nothing inside the film may be a scroll anchor. Rotating a phone changes
   the stage's height, and Chromium kept the film TRACK still on screen by
   moving scrollY 427px: the playhead jumped 3.3 s back (p .40 -> .24) the
   moment the phone turned. The film is the top of the page, so there is
   nothing above it to protect; below it, the sections still anchor. */
.film{overflow-anchor:none}

/* THE PHONE POSTER. On phones the poster is the <picture> build.py writes
   under the video: the stage background gave them the 427 KB 16:9 JPEG, and
   Chromium never counted a full-stage background as the largest paint, so
   their LCP waited ~26 s for the video. An <img> is painted from the first
   frame and counts. The desktop keeps the stage background it always had
   (the <picture> resolves to the same film-poster.jpg there, one request):
   an <img> is resampled differently from a background, and the desktop must
   not change by a single pixel.
   ⚠️ The media list is FILM_MEDIA_P + FILM_MEDIA_L from build.py, verbatim;
   build.py refuses to build if they drift apart. */
.film-poster{display:none}
.film-poster,
.film-poster img{position:absolute;inset:0;width:100%;height:100%}
.film-poster{pointer-events:none}
.film-poster img{display:block;object-fit:cover}
@media (orientation: portrait) and (max-width: 899px), (orientation: portrait) and (pointer: coarse), (orientation: landscape) and (max-width: 899px), (orientation: landscape) and (max-height: 520px) and (pointer: coarse){
  .film-stage{background-image:none}
  .film-poster{display:block}
}

/* FIRST PAINT SHOWS BEAT 1 ONLY. Beat visibility used to exist only as inline
   styles from the deferred scrub.js, so until it ran all four beats painted on
   top of each other and a tap on 'Get directions' landed on beat 4's lead.
   scrub.js's inline opacity/visibility override this from its first frame, and
   so does the static (reduced motion / Save-Data) path. */
.film-beats > .film-beat:not(:first-child){opacity:0;visibility:hidden}

/* Below half strength a beat stops taking taps and clicks (scrub.js sets
   .is-mute). The children need it, not the beat: they carry pointer-events:auto. */
.film-beat.is-mute > *{pointer-events:none}

/* A fully faded beat (.is-off, opacity < 0.01) pauses everything animating
   inside it: the wordmark glint and the mascot's endless sign loop kept
   restyling ~73 times a second behind a hidden beat, and at the footer.
   !important because it must beat the mascot's own state rules; it lets go
   the moment the beat fades back in. */
.film-beat.is-off *,
.film-beat.is-off *::before,
.film-beat.is-off *::after{animation-play-state:paused !important}

/* Reduced motion / Save-Data: the stacked beats start below the header. The
   film runs up under the sticky nav (margin-top:-nav-h), and on a phone the
   old 48px top padding put the whole h1 behind a 122px header. On a desktop
   this is max(96, 78 + 16) = 96px, the same as before. */
.film-static .film-beats{padding-top:max(clamp(3rem,9vw,6rem),calc(var(--nav-h,80px) + 1rem))}

/* Short landscape screens: the beats centre between the header and whatever
   fixed layer sits at the bottom (the 21+ bar or the call chip, published as
   --chrome-bottom by the overlays; 0px when there is none). */
@media (max-height:520px){
  .film .film-beats{padding-bottom:var(--chrome-bottom,0px)}
}

/* Touch targets: the two film CTAs were 38.6px tall on a 734x343 landscape
   phone. 44px on any coarse pointer; a desktop mouse keeps its size. */
@media (pointer:coarse){
  .film .btn-sq{min-height:44px}
}
/* ═══ END WP2-FILM-SCRUB ═══ */

/* ═══ BEGIN WP3-FILM-LEGIBILITY ═══ */
/* ══════════════════════════════════════════════════════════════════════
   MOBILE FILM (WP3): the beat copy and the wordmark read over the phone film
   Measured with the text hidden and the backdrop screenshotted (the mean
   colour inside each line box against the declared text colour), over the
   portrait film scrubbed to five moments of every beat, on five phones in
   both engines. Before this block the phone veil put .27-.51 of shade
   behind the copy where the desktop pool puts .67-.72: the eyebrows failed
   4.5:1 on almost every frame, and beat 3 (the lit cases, the brightest
   footage of the walk) read at 1.6-2.0:1.
   A mouse-driven window 900px wide or more matches none of the rules
   below, whatever its shape, so every desktop pixel is as it was. The
   gates follow the film's own buckets (build.py FILM_MEDIA_P / _L): the
   portrait band goes where the portrait film plays.
   ══════════════════════════════════════════════════════════════════════ */

/* THE VEIL IS CHOSEN BY SHAPE, NOT BY WIDTH. The old phone ramp was gated
   on max-width:900px alone, so a landscape phone got the portrait ramp and
   an upright iPad the same thin shade as a phone.
   · Landscape phones (≤900px, wider than tall) take the desktop pool: the
     same three layers as the desktop rule, because their film is the full
     16:9 frame that pool was tuned on. This block only undoes the old ramp;
     the portrait block below wins on source order for anything upright.  */
@media (max-width:900px){
  .film-veil{
    background:
      radial-gradient(ellipse 78% 62% at 50% 46%,
        rgb(11 10 9/.74) 0%,
        rgb(11 10 9/.58) 38%,
        rgb(11 10 9/.22) 72%,
        rgb(11 10 9/0)  100%),
      linear-gradient(0deg,rgb(11 10 9/.66) 0%,rgb(11 10 9/.18) 30%,transparent 48%),
      linear-gradient(180deg,rgb(11 10 9/.52) 0%,transparent 22%);
  }
}

/* · Short landscape phones (≤900 x ≤520): the same pool, placed on the copy.
     There the beats centre between the nav and the bottom chrome (WP2's
     max-height:520px rule pads them by --nav-h and --chrome-bottom), so the
     copy sits at ~60% of the stage and a pool at 46% left the lead's lower
     line at .54-.60. The centre follows the same two variables, 4% above the
     middle of that area exactly as the desktop pool sits 4% above the middle
     of its own: rendered behind the lead .65 or more on the 568x320 SE.     */
@media (max-width:900px) and (max-height:520px){
  .film-veil{
    background:
      radial-gradient(ellipse 78% 62% at 50% calc(46% + (var(--nav-h,80px) - var(--chrome-bottom,0px)) / 2),
        rgb(11 10 9/.74) 0%,
        rgb(11 10 9/.58) 38%,
        rgb(11 10 9/.22) 72%,
        rgb(11 10 9/0)  100%),
      linear-gradient(0deg,rgb(11 10 9/.66) 0%,rgb(11 10 9/.18) 30%,transparent 48%),
      linear-gradient(180deg,rgb(11 10 9/.52) 0%,transparent 22%);
  }
}

/* · Upright stages (phones, an iPad held upright; = FILM_MEDIA_P): a band
     of shade centred on the COPY, as one vertical profile across the width.
     A phone's lines run nearly edge to edge, so the desktop's ellipse (a
     vertical stripe at 9:16) and any narrower pool leave the line ends
     bright; a radial pool plus a separate bottom ramp was also tried and
     left a light seam where the two met (.53 at 86%).
     The beats centre in the small viewport (100svh, the WP2 block), so the
     stops are in svh too: on a real iPhone the stage is 100lvh, taller than
     what shows while the toolbar is out, and stops in % of the stage would
     slide the band below the copy. The copy runs from about 27% (the h1 on
     a 320x568 SE) to 64% (the last lead line) of the small viewport on every
     phone measured, so the band holds .70-.74 from 26svh to 66svh:
       top      .52  the desktop's nav lift
       12svh    .42  the lightest point, under the nav
       26-66svh .70-.74 behind every line of copy (desktop pool: .65-.72)
       84svh    .60  a lighter window onto the floor
       bottom   .70  the ramp under the call chip and the shop floor
     Rendered behind the eyebrow, h2 and lead: .70-.74, so every line clears
     4.5:1 over any frame of the walk, beat 3's white cases included.
     The first declaration is for engines without svh (Safari < 15.4,
     Chrome < 108): the same profile in % of the stage.                     */
@media (orientation: portrait) and (max-width: 899px), (orientation: portrait) and (pointer: coarse){
  .film-veil{
    background:linear-gradient(180deg,
      rgb(11 10 9/.52) 0,
      rgb(11 10 9/.42) 12%,
      rgb(11 10 9/.70) 26%,
      rgb(11 10 9/.74) 46%,
      rgb(11 10 9/.70) 66%,
      rgb(11 10 9/.60) 84%,
      rgb(11 10 9/.70) 100%);
    background:linear-gradient(180deg,
      rgb(11 10 9/.52) 0,
      rgb(11 10 9/.42) 12svh,
      rgb(11 10 9/.70) 26svh,
      rgb(11 10 9/.74) 46svh,
      rgb(11 10 9/.70) 66svh,
      rgb(11 10 9/.60) 84svh,
      rgb(11 10 9/.70) 100%);
  }
}

/* THE STATIC FILM ON PHONES (reduced motion, Save-Data). The beats stack
   down a stage 1,000px or more tall, so a band placed in viewport units
   would sit on the first beat only. An even wash does the same job for a
   column of text: .72 everywhere, with the desktop's top lift kept for the
   nav. The desktop's static film keeps its own veil.                      */
@media (max-width:900px), (orientation: portrait) and (pointer: coarse){
  .film-static .film-veil{
    background:
      linear-gradient(180deg,rgb(11 10 9/.52) 0%,transparent 12rem),
      rgb(11 10 9/.72);
  }
}

/* THE COPY ON PHONES.
   · Eyebrows: 12px tracked mono caps are the smallest text on the film. In
     --film-accent (#D8BC9E) they need a backdrop no lighter than about sRGB
     79 for 4.5:1; in --film-display (#EFE3BC) about 101, which the band
     guarantees over any frame, not only over the frames measured. A wider
     halo (.5em) under the 1px contact shadow keeps an edge on the caps where
     a lit case runs behind them. Size, face and tracking stay the site's.
   · Leads: text-wrap:pretty keeps a lone word off the last line. With the
     non-breaking spaces build.py puts inside "W New Haven" it also keeps the
     street together: 375-430px used to break "on W / New Haven Ave." or
     "W New / Haven Ave.". Phones only: on the desktop it would re-break
     beat 2's "…New Haven / Ave.", which must not move.                     */
@media (max-width:900px), (orientation: portrait) and (pointer: coarse){
  .film-beat .eyebrow{
    color:var(--film-display);
    text-shadow:0 1px 2px rgb(11 10 9/.9),0 0 .5em rgb(11 10 9/.85);
  }
  .film-beat .lead{text-wrap:pretty}
}

/* THE WORDMARK ON UPRIGHT STAGES (= FILM_MEDIA_P). .film-head's clamp
   bottoms out at 25.6px below ~436px, so the hero name was the size of
   every other heading on a phone. The step: 28px at 320, 33.8px at 393,
   38.4px from 447 up (an upright iPad, one line). "SMOKE SHOP" is one
   .wm-w unit, so the name still breaks only into the INFINITY / SMOKE SHOP
   lockup (SMOKE SHOP is 9.07em: 254 of 280px at 320, 335 of 390px at 430).
   Landscape phones keep their size: the stage is 343-430px tall there and
   the h1 already sits between the nav and the CTA row.                     */
@media (orientation: portrait) and (max-width: 899px), (orientation: portrait) and (pointer: coarse){
  .film-wordmark{font-size:clamp(1.75rem,8.6vw,2.4rem)}
}
/* ═══ END WP3-FILM-LEGIBILITY ═══ */

/* ═══ BEGIN WP5-HEADER ═══ */
/* ══════════════════════════════════════════════════════════════════════
   THE PHONE HEADER: one compact row, all four links always on screen

   Measured before this block: at ≤640px the header wrapped into two rows
   (brand + CALL, then the links) and was 122.4px on every portrait phone,
   14.6-21.5% of the screen, sticky, never given back. At 280px it was
   three rows (159px). Landscape phones 561-640px wide got the same 122px
   (38% of a 320px screen) and the 78px desktop bar above that (23% of 343).

   Decided with the client (D6): no burger and no hide-on-scroll. The four
   links stay visible and one tap away at every scroll position. The brand
   name stays wherever it fits; where it does not, the mark stands alone
   and the name is hidden VISUALLY only, so the link keeps its accessible
   name. Two rows only below 320px.

   THE ROW, for ≤640px wide and for touch screens ≤500px tall: 44px
   targets in a 55px bar (5 + 44 + 5 + the 1px hairline border).
     ≥540px    mark + name · four links · CALL with its label
     410-539   mark + name · four links · CALL as a 44px icon
     320-409   mark only   · four links · CALL as a 44px icon
     <320      two rows: mark + name / four links + CALL icon, so the
               visual order is the DOM and Tab order (brand, links, Call)
   The links split the space between the brand and CALL into equal cells
   (flex:1 1 0; never narrower than their word, never wider than 88px), so
   the 44px-tall hit areas tile the strip with no dead gap, and each is at
   least 44px wide at every width from 280px up.

   --nav-h (read by the film pull-up and the short-screen beat padding) is
   FIXED per viewport: nothing here changes height on scroll, so there is
   no layout shift and nothing for WebKit (no scroll anchoring) to jump
   on. site.js still publishes the measured value; the same numbers are
   set below so the film is pulled up by the right amount before site.js
   runs (the 80px fallback was 42px wrong on a phone).

   Desktop is untouched: every rule sits behind a query that no
   fine-pointer window wider than 640px and taller than 500px matches,
   except the scroll-padding, which stops at 900px.
   ══════════════════════════════════════════════════════════════════════ */

/* 1. Touch targets, any coarse pointer (phones, landscape phones, iPad).
   padding-block brings a link box to 44px and the negative margins hand
   the extra back, so the iPad's 78px bar keeps its height and its text
   does not move. The current-page line was the link's border-bottom,
   which would now sit 7px lower; it becomes a text underline at the same
   place (text-underline-offset is measured from the baseline, and lands
   on the old border row in both engines at 12-14px). */
@media (pointer:coarse){
  .brand{min-height:44px}
  .nav-links a{padding-block:15px;margin-block:-7px -6px}
}
@media (pointer:coarse), (max-width:640px){
  .nav-links a{
    border-bottom:0;
    text-decoration-line:underline;text-decoration-style:solid;
    text-decoration-thickness:1px;text-decoration-color:transparent;
    text-underline-offset:10px;
  }
  .nav-links a[aria-current="page"]{text-decoration-color:var(--brass)}
  .nav.is-over-film .nav-links a[aria-current="page"]{text-decoration-color:var(--film-accent)}
}
@media (hover:hover) and (pointer:fine) and (max-width:640px){
  .nav-links a:hover{text-decoration-color:var(--brass)}
}

/* 2. The compact row. */
@media (max-width:640px), (max-height:500px) and (pointer:coarse){
  :root{--nav-h:55px}
  .nav-in{flex-wrap:nowrap;gap:0 8px;padding-block:5px}
  .brand{min-height:44px;min-width:44px;gap:8px}
  .brand .mark{width:46px}
  /* the sub-line was 9px; 11px on phones, tracked in to keep the lockup
     as wide as INFINITY above it (77.5px) */
  .brand-name span{font-size:.6875rem;letter-spacing:.1em}
  /* the strip no longer scrolls (all four fit at every width), so it no
     longer clips the focus ring either */
  .nav-links{
    order:0;flex:1 1 auto;width:auto;min-width:0;justify-content:flex-end;
    margin-left:0;gap:0;overflow:visible;padding-bottom:0;
  }
  /* cells stop growing at 88px, so on a wide landscape row the links sit
     together beside CALL, as on the desktop bar, instead of spreading out */
  .nav-links a{
    flex:1 1 0;display:flex;align-items:center;justify-content:center;
    max-width:5.5rem;height:44px;padding:0;margin:0;white-space:nowrap;
    font-size:.75rem;letter-spacing:.1em;
    text-underline-offset:8px;
  }
  .nav .btn{margin-left:0;height:44px;padding-block:0;padding-inline:calc(.75rem + 1px)}
}
/* wider compact rows (landscape phones, big portrait windows): the old
   phone link size */
@media (min-width:540px) and (max-width:640px), (min-width:540px) and (max-height:500px) and (pointer:coarse){
  .nav-links a{font-size:.8125rem;letter-spacing:.14em}
}

/* 3. CALL as a 44px icon below 540px. The label is hidden visually, not
   removed: the link's accessible name stays "Call". The glass square is
   the button, so the inner chip drops its own frame (a framed chip inside
   a framed square read as a button in a button) and the glyph grows from
   12px to 20px. */
@media (max-width:539px){
  .nav .btn{width:44px;padding:0;gap:0}
  .nav .btn .btn-ico{--chip:26px;margin:0;background:none;border-color:transparent;box-shadow:none}
  .nav .btn .btn-ico svg{width:78%;height:78%}
  .nav .nav-call-label{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;
    overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
  }
}

/* 4. Mark only, 320-409px. The name needs 131.5px with its mark; below
   410px that leaves under 9px between two link words even with the
   tracking pulled in to .06em (measured: 4.7px at 393px, 8.7px at 409px),
   so the name is hidden there, visually only. 410-429px keeps the name
   and pulls the link tracking in to .06em: tightest word gap 9px at 410,
   10px at 414, 13.5px at 428. Every other width keeps 11.8px or more. */
@media (min-width:320px) and (max-width:409px){
  .brand-name{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;
    overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
  }
}
@media (min-width:410px) and (max-width:429px){
  .nav-links a{letter-spacing:.06em}
}
/* the narrowest one-row widths: a tighter gutter and tracking */
@media (min-width:320px) and (max-width:374px){
  .nav-in{padding-inline:16px;column-gap:4px}
  .nav-links a{letter-spacing:.06em}
}

/* 5. Below 320px: two rows. The strip is sized so it cannot share the
   brand's row; CALL follows it on row two. 5 + 44 + 44 + 5 + 1 = 99px. */
@media (max-width:319px){
  :root{--nav-h:99px}
  .nav-in{flex-wrap:wrap;padding-inline:16px}
  .nav-links{flex:1 1 calc(100% - 52px)}
  .nav-links a{letter-spacing:.06em}
}

/* 6. Anchors and focus land clear of the header. The bar is sticky, so a
   #fragment, a Tab or a Shift+Tab put their target's top edge at y=0,
   under it: /faq/#kratom landed with its eyebrow 100% and its h2 42-45%
   under the 122px bar. Longhand only: scroll-padding-bottom belongs to
   the bottom-layer package. Stops at 900px, so desktop stays 'auto'. */
@media (max-width:900px), (max-height:500px) and (pointer:coarse){
  html{scroll-padding-top:calc(var(--nav-h, 78px) + 8px)}
}
/* ═══ END WP5-HEADER ═══ */

/* ═══ BEGIN WP4-OVERLAYS ═══ */
/* ══════════════════════════════════════════════════════════════════════
   MOBILE OVERLAYS (WP4) — the bottom fixed chrome on phones
   The 21+ card and the call dock are the two layers fixed to the bottom of
   the viewport. On desktop they sit in the corner gutter, clear of the
   centred content. On a phone there is no gutter, so they sat on the text
   and, in the dock's case, took taps meant for the links under its
   transparent art. Everything below is layout, size and hit area only:
   the 21+ gate, its copy and markup, the consent rule
   (html:not(.age21-ok) .mascot, .dock{display:none !important}) and
   VAPE_MASCOT are exactly as they were.

   DESKTOP. Nothing below changes a desktop pixel. The rules are scoped to
   phone-sized viewports (max-width:900px / max-height:520px), to coarse
   pointers, or they are hit-testing only (pointer-events), which paints
   nothing. The one desktop behaviour change is intended: below 1480px the
   dock's art stops taking clicks, because there it overlaps the content
   column and a click on a visible link used to dial the shop.
   ══════════════════════════════════════════════════════════════════════ */

/* ── 1. THE BOTTOM RESERVATION ───────────────────────────────────────────
   site.js publishes --chrome-bottom on <html>: the card's height plus its
   offset while the card is up, else the dock chip's height plus offset while
   the dock shows and is not away, else 0px. Focus moves and anchor jumps then
   stop short of whatever is fixed at the bottom. (scroll-padding-top belongs
   to the header; only the bottom longhand is set here.) Desktop keeps auto. */
@media (max-width:900px), (max-height:500px){
  html{scroll-padding-bottom:calc(var(--chrome-bottom,0px) + 8px)}
}

/* ── 2. THE DOCK TAKES TAPS ON ITS CHIP ONLY ─────────────────────────────
   The whole <a> used to be the tel: target, including the 61% of the art's
   box that is fully transparent, so a tap on a visible link beneath it
   dialled the shop. Now the art is see-through to the pointer as well as to
   the eye, and only the painted chip dials. Wherever the art can overlap
   content: every touch screen, and every width under 1480px (the dock's
   124px art only clears the 1240px shell's padding from about 1470px up).
   Wider, with a mouse, the art sits in an empty gutter and still dials. */
@media (max-width:1479.98px), (pointer:coarse){
  .dock{pointer-events:none}
  .dock .dock-cta{pointer-events:auto}
}
/* A 44px chip for fingers (the plate was 34px). line-height, not
   min-height: the chip is text on a padded plate, and 28px + 2 x .5rem keeps
   the label on the plate's centre line. */
@media (pointer:coarse){
  .dock .dock-cta{line-height:28px;min-height:44px}
}

/* ── 3. THE PHONE DOCK: a small mascot on a narrow chip ─────────────────
   Portrait phones (and any viewport up to 900px wide that is taller than
   520px, iPad Mini portrait included). It was 128x163 at bottom-left: a
   quarter of the screen height and a third of its width, on the text column.
   · The art is at most 64px tall and never more than the screen can spare:
     the whole dock stays within 15% of the height (art = 15svh - 40px, so
     45px on a 568px iPhone SE, 59px on an iPhone 15, 64px from 694px up).
   · "Call" / "the shop" on two lines keeps the chip 87-90px wide (28% of a
     320px screen). The label steps up from 11px to 12px.
   · He stands on the chip: his feet overlap its top edge by 6px, in front
     of it, as he stands on "Get directions" in the hero. The art takes no
     taps (section 2), so the chip's top edge still dials under his feet.
   The WebP art is served here by the <picture> in build.py DOCK. */
@media (max-width:900px) and (min-height:521px){
  .dock{gap:0;bottom:max(clamp(.75rem,3vw,1.5rem),env(safe-area-inset-bottom))}
  .dock picture{position:relative;z-index:1;margin-bottom:-6px}
  .dock img{
    width:auto;
    height:clamp(40px,calc(15vh - 40px),64px);
    height:clamp(40px,calc(15svh - 40px),64px);
  }
  /* min-content: build.py writes "the&nbsp;shop", so the only break is after
     "Call" and the chip is exactly as wide as "the shop". */
  .dock .dock-cta{font-size:.75rem;line-height:14px;padding:8px 10px;min-height:44px;text-align:center;width:min-content}
}
/* Short landscape screens: the chip alone. The header CALL is always on
   screen there and the art was half the height of the viewport, on the
   start of every beat's copy. One line, 44px tall, 8px off the bottom like
   the landscape 21+ bar: every pixel of height counts on a 320px screen,
   where the beat copy ends 59px above the bottom edge. */
@media (max-height:520px){
  .dock picture{display:none}
  .dock{bottom:max(8px,env(safe-area-inset-bottom))}
  .dock .dock-cta{font-size:.75rem;line-height:28px;padding:8px 14px;white-space:nowrap}
}
/* Away: site.js sets .is-away while #visit or the footer is on screen; both
   carry their own call buttons, and those are exactly the places the dock
   sat on text and links. A class and a fade, never [hidden], so the drop
   animation does not replay each time. visibility waits for the fade on the
   way out and returns at once on the way back. Phones only, like the JS. */
@media (max-width:900px), (max-height:520px){
  .dock{transition:opacity .2s ease,visibility 0s linear 0s}
  .dock.is-away{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s ease,visibility 0s linear .2s}
}

/* ── 4. THE MASCOT ON PHONES ─────────────────────────────────────────────
   The sprite sheets as WebP (same cells, 98 + 43 KB instead of 287 + 114 KB
   of PNG). build.py writes both pairs onto .mascot-btn with ?v=ASSET_V; this
   points .mascot (and so its ::before, which reads the property) at the WebP
   pair on phones. Desktop is untouched and keeps the PNG pixels. A custom
   property that is never used in a background is never fetched, so each
   device downloads one pair. */
@media (max-width:900px), (max-height:520px){
  .mascot{
    --mascot-walk:var(--mascot-walk-m,none);
    --mascot-idle:var(--mascot-idle-m,none);
  }
}
/* The "still" fx tier (small screens, Save-Data, weak devices) means nothing
   keeps animating. The sign-holding loop was infinite there, and it kept
   running while its beat was hidden and scrolled away (~73 style recalcs a
   second). Four swings, then he rests on the first frame. */
html[data-fx="still"] .mascot-btn.lift .mascot::before{animation-iteration-count:4}

/* ── 5. THE 21+ CARD ON TOUCH AND SMALL SCREENS ─────────────────────────
   Size and position only. The markup, copy, role, aria, buttons, and the
   fail-closed [hidden] default are untouched.
   ⚠️ The card must never be given a `display` without :not([hidden]): in
   this stylesheet [hidden] loses to any later display (see .agegate[hidden]). */

/* Touch: the consent buttons are 44px targets at every width (33px before
   on landscape phones and iPad). */
@media (pointer:coarse){
  #agegate .agegate-actions .btn-sq{min-height:44px}
}

/* Portrait phones: readable type. It was 11px title, 12px copy and 11px
   buttons. The buttons size to their labels (still side by side) so the
   longer "I am 21 or older" never clips at 12px on a 320px screen. The card
   stays under a quarter of the screen height. */
@media (max-width:560px){
  #agegate.agegate{padding:8px 14px;gap:4px}
  #agegate .agegate-title{font-size:.8125rem;line-height:1.2}
  #agegate .agegate-copy{font-size:.8125rem;line-height:1.3}
  #agegate .agegate-actions{margin-top:4px}
  #agegate .agegate-actions .btn-sq{flex:1 1 auto;padding-inline:8px;font-size:.75rem;letter-spacing:.08em}
}

/* Short landscape screens: one bar along the bottom, title and copy on the
   left, both buttons side by side on the right, under 80px. The desktop
   corner card was 157px there, nearly half the screen, over the heading,
   "Get directions" and every beat. */
@media (max-height:500px) and (min-width:561px){
  #agegate.agegate:not([hidden]){display:grid}
  #agegate.agegate{
    left:max(10px,env(safe-area-inset-left));right:max(10px,env(safe-area-inset-right));
    bottom:max(8px,env(safe-area-inset-bottom));width:auto;max-width:none;
    grid-template-columns:minmax(0,1fr) auto;column-gap:14px;row-gap:2px;
    align-items:center;padding:6px 8px 6px 14px;
  }
  #agegate .agegate-title{grid-column:1;grid-row:1;align-self:end;margin:0;font-size:.75rem;line-height:1.2}
  #agegate .agegate-copy{grid-column:1;grid-row:2;align-self:start;margin:0;font-size:.8125rem;line-height:1.3}
  #agegate .agegate-actions{grid-column:2;grid-row:1 / span 2;flex-wrap:nowrap;gap:8px;margin:0}
  #agegate .agegate-actions .btn-sq{flex:0 0 auto;min-height:44px;padding-inline:14px;font-size:.75rem;letter-spacing:.1em;white-space:nowrap}
}
@media (max-height:500px) and (min-width:561px) and (max-width:659.98px){
  #agegate .agegate-copy{font-size:.75rem}
  #agegate .agegate-actions .btn-sq{padding-inline:10px;letter-spacing:.06em}
}
/* ═══ END WP4-OVERLAYS ═══ */

/* ═══ BEGIN WP7-HOME-LAYOUT-TYPE ═══ */
/* ══════════════════════════════════════════════════════════════════════
   HOME LAYOUT AND TYPE ON PHONES (WP7) — section rhythm, the phone type
   step, display headings, the wall band, the "what we carry" tiles, the
   "how we run the place" columns, the glass photo in landscape, and the
   footer on every page.

   DESKTOP. Nothing here changes a desktop pixel. Section 0 moves five
   inline margins from build.py into classes at the SAME values (a phone
   query cannot override an inline style), and keeps the phone-only copy
   of the footer hours unrendered. Everything else is scoped
   to a phone-sized viewport (max-width 900/640/560/400px), a short
   landscape screen with a coarse pointer, or pointer:coarse.
   Copy, legal text and the 21+ / mascot rules are untouched: sizes, spacing
   and hit areas only.
   ══════════════════════════════════════════════════════════════════════ */

/* ── 0. EVERY WIDTH, SAME PICTURE AS BEFORE ──────────────────────────────
   The former inline margins, at their old values. */
.sec-title{margin-bottom:2.5rem}          /* #catTitle   */
.sec-title-lg{margin-bottom:2.75rem}      /* #whyTitle   */
.tiles-note{margin-top:1.5rem}            /* the kava/kratom line under the tiles */
.feature-prose{margin-top:1.25rem}        /* the glass copy */
.feature-note{margin-top:1.75rem}         /* the glass-and-pipes line */
/* The footer's day-over-time copy of the hours is for the side-by-side
   phone footer only (section 8). Everywhere else it is not rendered at all,
   so neither layout nor the accessibility tree ever has both. */
.foot-hours-stack{display:none}

/* ── 1. THE PHONE TYPE STEP ─────────────────────────────────────────────
   A phone is read at half the distance of a desktop screen, and the platform
   body default there is 16-17px. Body copy that was 15px goes to 16px, the
   legal paragraphs from 13 to 14px, and the statute notice and copyright
   line from 11px uppercase mono to 12px. The statute link inherits its line
   instead of sitting at 15px inside an 11px line, and never splits. Portrait
   phones, and landscape phones (short, coarse pointer): the same hand. */
@media (max-width:560px), (max-height:500px) and (pointer:coarse){
  .tile p,.cols3 p{font-size:1rem}
  .tile p{line-height:1.45}
  .cols3 p{line-height:1.55}
  .foot-legal{font-size:.875rem;line-height:1.5}
  .foot-legal p{margin-bottom:.6em}
  .foot-statute,.foot-copy{font-size:.75rem}
  .foot-statute{line-height:1.5}
  .meta{letter-spacing:.04em}
  .foot-copy a{font-size:inherit;letter-spacing:inherit;white-space:nowrap}
}

/* ── 2. DISPLAY HEADINGS ────────────────────────────────────────────────
   .d-l bottomed out at 25.6px on every phone. Archivo is served at width
   125 only (font-stretch cannot narrow it), so the long section headings
   wrapped to 10-13 characters a line: "You'll get a straight answer, or
   you'll get sent elsewhere" ran six lines at 320px. The size now follows
   the width (20px at 320-384, 20.4 at 393, 22.4 at 430, back to 25.6px by
   492px), which keeps those headings to four lines at 320 and three from
   393. .d-m (the column heads) steps down with it so it stays below .d-l. */
@media (max-width:560px){
  .d-l{font-size:clamp(1.25rem,5.2vw,1.6rem)}
  .d-m{font-size:1.0625rem}
}

/* ── 3. SECTION RHYTHM ──────────────────────────────────────────────────
   80px of padding above and below every rule made each section join 25-33%
   of a phone screen (half a screen in landscape). */
@media (max-width:560px), (max-height:500px) and (pointer:coarse){
  .section{padding-block:clamp(2.25rem,10vw,3.5rem)}
  .sec-title,.sec-title-lg{margin-bottom:1.25rem}
  .tiles-note{margin-top:.875rem}
  .feature{row-gap:1.5rem}
  .feature-prose{margin-top:1rem}
  .feature-note{margin-top:1.25rem}
}
/* A landscape phone is 320-430px tall: 10vw would be 56px there. */
@media (max-height:500px) and (pointer:coarse){ .section{padding-block:2.25rem} }

/* ── 4. THE TILES ────────────────────────────────────────────────────────
   One column of six 176px-minimum cards made "What we carry" three phone
   screens tall. The icon moves up beside the title and the padding comes
   in; the body keeps the full card width (29-42 characters a line).
   Landscape phones up to 900px wide get it on their two-column grid; wider
   ones already have the desktop row layout (min-width:901px). */
@media (max-width:640px), (max-height:500px) and (max-width:900px) and (pointer:coarse){
  .tiles{gap:.625rem}
  .tile{
    display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;
    column-gap:.7rem;row-gap:.35rem;min-height:0;padding:.75rem .875rem .8rem;
  }
  .tile-icon{width:24px;height:24px}
  .tile p{grid-column:1 / -1}
}
@media (max-width:400px){ .tile{padding-inline:.75rem} }
@media (max-width:640px){ .cat-split .spiral{margin-top:1rem} }

/* ── 5. HOW WE RUN THE PLACE ─────────────────────────────────────────────
   Same move as the tiles: the icon beside the heading, tighter joins. Only
   where the columns are stacked (900px and below). */
@media (max-width:640px), (max-height:500px) and (max-width:900px) and (pointer:coarse){
  .cols3{gap:1.125rem}
  .cols3 > div{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:.7rem;align-items:center}
  .cols3 > div + div{padding-top:1.125rem}
  .cols3 svg{width:24px;height:24px;margin-bottom:0}
  .cols3 p{grid-column:1 / -1;margin-top:.45rem}
}
/* A measure where the column runs full width (641-900px, landscape):
   34em is ~72 characters of Inter; 56ch is ~65 in the legal paragraphs and
   51 in the mono statute notice. The one-line copyright keeps the width. */
@media (max-width:900px){
  .cols3 > div p{max-width:34em}
  .foot-legal p{max-width:56ch}
  .foot-legal .foot-copy{max-width:none}
}

/* ── 6. THE WALL BAND ────────────────────────────────────────────────────
   On phones the paper ramp covered the whole band and the mask kept only
   its middle half, so a tenth of the photo's detail survived: a pale strip.
   The paper now pools behind the copy only (it follows the copy block, 14px
   of fade above and below), and the photo reads above and below it and
   nearly edge to edge. The type keeps its paper ground and halo. */
@media (max-width:640px){
  .wall img{
    -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
    mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
  }
  .wall-copy{background:none}
  .wall-copy > div{position:relative;z-index:0;flex:1 1 auto}
  .wall-copy > div::before{
    content:"";position:absolute;z-index:-1;inset:-14px calc(-1 * var(--gutter));
    background:linear-gradient(180deg,rgb(255 252 246/0),rgb(255 252 246/.88) 14px,rgb(255 252 246/.88) calc(100% - 14px),rgb(255 252 246/0));
    background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--paper) 88%,transparent) 14px,color-mix(in srgb,var(--paper) 88%,transparent) calc(100% - 14px),transparent);
  }
}

/* ── 7. THE GLASS PHOTO ON A LANDSCAPE PHONE ─────────────────────────────
   The 725x900 portrait photo rendered 3.2x the height of a 343px landscape
   screen. It is capped to the band under the header, less 2.5rem of air. */
@media (max-height:500px) and (max-width:900px), (max-height:500px) and (pointer:coarse){
  .feature-img img{
    width:auto;margin-inline:auto;
    max-height:calc(100vh - var(--nav-h,78px) - 2.5rem);
    max-height:calc(100svh - var(--nav-h,78px) - 2.5rem);
  }
}

/* ── 8. THE FOOTER ───────────────────────────────────────────────────────
   It was 1.6 screens on an iPhone 15 and 2.1 on a 320px phone: three
   stacked columns, a column of 18px-tall links, and 96px of padding under
   the last legal line for the INFINITY watermark.
   · Up to 900px (and on landscape phones) the site links run in one
     wrapping row across the full width (44px targets on touch, section 9).
   · From 390 to 640px "The shop" and "Hours" sit side by side. The hours
     line (30 characters of mono) cannot fit a half column, and a break
     inserted into it moves desktop glyphs by a subpixel (measured: 90-220
     changed px at 1366), so build.py ships a second, day-over-time copy of
     the same hours that only this layout shows (.foot-hours-stack) while the
     original is hidden. The ZIP is tied to the state with a no-break space.
   · The bottom padding is the watermark's room OR the bottom chrome WP4
     publishes as --chrome-bottom (the 21+ card, the call dock) plus 16px,
     whichever is larger, so the last legal line and the copyright can always
     scroll clear of it. The watermark (font 13vw, line-height .8, bottom
     -.18em) shows .62em = 8.06vw of ink above the footer's edge, so 8.2vw
     (floor 3rem) keeps it off the legal lines: 48px on phones, 64px at 780px.
     Landscape phones: 8.5vw, floor 4rem. */
@media (max-width:640px){
  .foot-in{padding-block:1.5rem max(3rem,8.2vw,calc(var(--chrome-bottom,0px) + 16px))}
  .foot-cols{gap:1.25rem}
  .foot h2{margin-bottom:.5rem}
  .foot-legal{margin-top:1.5rem;padding-top:1.125rem}
  .foot-copy{margin-top:.875rem}
}
@media (max-width:900px), (max-height:500px) and (pointer:coarse){
  .foot-cols > div:nth-child(3){grid-column:1 / -1}
  .foot ul{flex-direction:row;flex-wrap:wrap;gap:0 1.25rem}
}
@media (min-width:390px) and (max-width:640px){
  .foot-cols{grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem}
  .foot-hours{display:none}
  .foot-hours.foot-hours-stack{display:block;font-size:.8125rem;line-height:1.85}
}
@media (min-width:641px) and (max-width:900px){
  .foot-in{padding-bottom:max(3rem,8.2vw,calc(var(--chrome-bottom,0px) + 16px))}
}
@media (max-height:500px) and (pointer:coarse){
  .foot-in{padding-block:2rem max(4rem,8.5vw,calc(var(--chrome-bottom,0px) + 16px))}
  .foot-cols{row-gap:1.25rem}
  .foot h2{margin-bottom:.5rem}
  .foot-legal{margin-top:1.5rem;padding-top:1.125rem}
}

/* ── 9. TOUCH TARGETS ────────────────────────────────────────────────────
   The footer site links were 18px tall on a 36px pitch. Now 44px, touching
   but never overlapping. The statute link and the kratom-notice link under
   the tiles stay inline: vertical padding on an inline box grows its hit
   area to 44px but never enters the line box, so nothing moves (an
   inline-block would also change how WebKit serialises the line's text).
   (The footer address links belong to the visit component, WP8.) */
@media (pointer:coarse){
  .foot ul{row-gap:0}
  .foot ul a{display:inline-flex;align-items:center;min-height:44px}
  .foot-copy a{padding-block:15px}
  .tiles-note a{padding-block:14px}
}
/* ═══ END WP7-HOME-LAYOUT-TYPE ═══ */

/* ═══ BEGIN WP8-VISIT-SUBPAGES ═══ */
/* Visit component (home #visit and /contact/), map facade, storefront, and
   the sub-page button rows. Everything that changes a phone is scoped to
   <= 640px, pointer:coarse or max-height:500px; the unscoped rules below
   only re-home the inline styles build.py used to emit, with the same values,
   so the desktop renders exactly as before. */

/* ── button rows (were inline flex styles on home #visit, about, faq,
   contact and 404) ──────────────────────────────────────────────────── */
.cta-row{display:flex;gap:.75rem;flex-wrap:wrap;margin-top:1.75rem}
.cta-row.cta-row-lg{margin-top:2rem}
.cta-row.cta-row-center{justify-content:center;margin-top:0}

/* ── /contact/ and /404.html layout (were inline styles) ───────────── */
.visit.visit-page{margin-top:2.5rem}
.nf{min-height:52vh;display:grid;place-items:center;text-align:center}
.nf-in{max-width:34rem}
.nf-lead{margin:1.5rem auto 2.5rem}

/* ── map facade ──────────────────────────────────────────────────────
   site.js (MAP FACADE) adds .map-facade and unhides the button on phones
   and touch screens; desktop gets the iframe at load and never sees either.
   .btn is display:inline-flex, which beats the UA [hidden] rule. */
.map-show[hidden]{display:none}
.map.map-facade{aspect-ratio:auto;height:auto;min-height:8rem;display:grid;place-items:center;padding:1.5rem 1rem}
/* No JS: the <noscript> link fills the map box. */
.map-link{
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;
  font-family:var(--mono);font-size:.8125rem;letter-spacing:.08em;text-transform:uppercase;text-align:center;
}
/* Short landscape: a 4:3 map was 148% of a 343px viewport. 65% leaves the
   header and a strip of page in view with it. */
@media (max-height:500px){
  .map{aspect-ratio:auto;height:65vh;height:65svh}
  .map.map-facade{height:auto}
}

/* ── tap targets: the inline address and phone links ─────────────────
   Vertical padding on an INLINE box grows its hit box but not its line box,
   so nothing moves: 20.6 -> 46.6px in the visit card and /contact/, 18.1 ->
   46.1px in the footer (15px type). Where two lines' boxes overlap, the
   later link wins; that band sits in the descenders and the leading. */
@media (pointer:coarse){
  .addr a{padding-block:calc(.75rem + 1px)}
  .foot .addr a{padding-block:.875rem}
}

/* ── phones ───────────────────────────────────────────────────────────── */
@media (max-width:640px){
  /* Paired buttons stack full width at one width, and drop to the ~50px
     height of the film's CTAs: 62px slabs cost a stacked pair 137px. */
  .cta-row .btn{flex:1 1 100%;padding-block:calc(.75rem + 1px)}
  .map .map-show{padding-block:calc(.75rem + 1px)}

  /* /contact/: the buttons move up under the address (they sat below the
     7-row hours table, a full screen down). Visual order only: the table,
     the note and the callout hold no links, so the tab order is unchanged.
     A flex column does not collapse margins, so .meta's own margins go and
     the table's and the callout's carry the same gaps as before. */
  .visit.visit-page{margin-top:1rem}
  .visit-info{display:flex;flex-direction:column}
  .visit-info > *{order:2}
  .visit-info > .addr{order:0}
  .visit-info > .cta-row{order:1;margin-top:1rem}
  .visit-info > .meta{margin-block:0}

  /* 404: min-height:52vh + centring put the buttons under the 21+ card. */
  .nf{min-height:0;padding-top:2rem}
  .nf-lead{margin:1rem auto 1.5rem}
}
/* Short phones (the 568px iPhone SE): header + card leave ~300px, less than
   eyebrow + h1 + lead + two buttons. The way home comes before the apology. */
@media (max-width:640px) and (max-height:600px){
  .nf{padding-top:1rem}
  .nf-in{display:flex;flex-direction:column}
  .nf-in > .cta-row{order:1;margin-top:1.25rem}
  .nf-in > .nf-lead{order:2;margin:1.25rem auto 0}
}
/* ═══ END WP8-VISIT-SUBPAGES ═══ */

/* ═══ BEGIN WP6-PERF ═══ */
/* The @font-face rules this block's token stacks name live in the
   WP6-FONTS block at the very top of this file. */
:root{
  --display:'Archivo','Archivo Fallback','Archivo Fallback Bold','Archivo Fallback Roboto','Arial Black',system-ui,-apple-system,'Segoe UI',sans-serif;
  --body:'Inter','Inter Fallback','Inter Fallback Roboto',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --mono:'IBM Plex Mono','IBM Plex Mono Fallback','IBM Plex Mono Fallback Menlo',ui-monospace,SFMono-Regular,Menlo,monospace;
}
/* meshDrift stops while the band is off screen (fx.js adds .is-off). */
.emesh.is-off::before{animation-play-state:paused}
/* A queued spiral photo (data-src, not yet released by spiral.js) stays out of
   the render, so a card never shows its alt text; without JS the <noscript>
   copy beside it is the photo. */
.spiral-card > img[data-src]{display:none}
/* ═══ END WP6-PERF ═══ */

/* ═══ BEGIN LENTI-PAGES ═══ */
/* ═══════════════════════════════════════════════════════════════════════
   LENTI · PAGES — layout for the house line, the case carousel, the
   distribution page and the about / visit bands (page agent, 2026-09-11).

   Appended to the END of style.css by the integrator, before lenti.css.
   This block positions and sizes CONTAINERS only. The look of a dark
   stage (.media-stage), the lenticular carousel (.lenti*) and the glow
   card (.glow-card / .edge-light / .glow-inner) belong to lenti.css and
   are not touched here. Type that sits on a stage uses the --film-*
   tokens, per the design system's one light-on-dark rule.

   New class names are all prefixed so nothing here collides with the
   existing sheet: .case-*, .teaser-*, .distro-strip*, .exh*, .exi-*,
   .exg*, .pcard*, .dist-*, .photo-band*, .about-*, .visit-band, .btn-row.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── shared ──────────────────────────────────────────────────────────── */
.btn-row{display:flex;gap:.75rem;flex-wrap:wrap;margin-top:2rem}
.callout + .btn-row{margin-top:2rem}
/* a domain name in a button reads as a URL, not as a shout — and the
   uppercase version was 4px too wide for a 320px phone */
.btn.btn-url{text-transform:none;letter-spacing:.04em}

/* ── nav: six links instead of four ──────────────────────────────────────
   Home, Exclusives, Distribution, About, FAQ, Visit. The single-row header
   holds them from 901px up (measured: 1024 has 40px to spare). Between
   641 and 900px the row needs ~840px and has 590–777, so the phone
   treatment (brand + call on top, the links as a scrollable strip below)
   extends up to 900px. Same rules as the existing ≤640 block; if the
   header is restructured, this block goes with it. */
@media (min-width:641px) and (max-width:900px){
  .nav-in{flex-wrap:wrap;gap:.75rem 1rem}
  .nav .btn{margin-left:auto}
  .nav-links{
    order:3;width:100%;margin-left:0;gap:1.5rem;
    overflow-x:auto;scrollbar-width:none;padding-bottom:.15rem;
  }
  .nav-links::-webkit-scrollbar{display:none}
  .nav-links a{flex:0 0 auto}
}

/* ── the case: heading and copy above, the carousel full width below ─── */
.case-sec .case-head{max-width:46rem;margin-bottom:clamp(1.75rem,3.5vw,2.75rem)}
.case-sec .case-head .prose{margin-top:1.25rem}
.case-sec > .meta{margin-top:1.5rem}
/* the pre-carousel fallback: one photo, in the house's soft-edged style */
.case-sec-still .case-still{max-width:34rem}

/* ── home: "Our own label" teaser ────────────────────────────────────── */
.teaser-head{
  display:flex;align-items:flex-end;justify-content:space-between;
  gap:1.5rem 2.5rem;flex-wrap:wrap;
  margin-bottom:clamp(1.75rem,3.5vw,2.5rem);
}
.teaser-head > div{max-width:46rem;min-width:0}
.teaser-head .lead{margin-top:1.25rem}
.teaser-head .btn{flex:0 0 auto;margin-bottom:.25rem}
.teaser-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(.75rem,2vw,1.5rem)}

/* ── product card contents (the glow card itself is lenti.css's) ─────── */
.pcard{min-width:0}
.pcard-img{display:block;width:100%;height:auto;aspect-ratio:1200/1490;object-fit:cover;border-radius:var(--radius)}
.pcard-name{
  margin:.9rem 0 0;
  font-family:var(--display);font-weight:700;text-transform:uppercase;
  font-size:.9375rem;line-height:1.2;letter-spacing:.04em;
  color:var(--film-display);text-wrap:balance;overflow-wrap:anywhere;
}
.pcard-detail{
  margin:.35rem 0 0;
  font-family:var(--mono);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;
  color:var(--film-accent);
}

/* ── home: the wholesale strip (one line, not a full section) ────────── */
.distro-strip{padding-block:clamp(2.25rem,5vw,3.5rem)}
.distro-strip-in{display:flex;align-items:center;justify-content:space-between;gap:1.25rem 2.5rem;flex-wrap:wrap}
.distro-strip-in > div{min-width:0;max-width:44rem}
.distro-strip-in .eyebrow{margin-bottom:.7rem}
.distro-strip-in .btn{flex:0 0 auto}

/* ── /exclusives/: the hero — h1 in the render's dark negative space ──── */
.exh-sec{padding-top:clamp(1rem,3vw,2rem)}
.exh{position:relative;overflow:hidden}
.exh-img{display:block;width:100%;height:auto;border-radius:var(--radius)}
.exh-copy{
  position:absolute;top:0;bottom:0;left:0;z-index:1;
  width:min(46%,34rem);
  display:flex;flex-direction:column;justify-content:center;
  padding:clamp(1.5rem,4vw,3.5rem);
}
.exh-copy .eyebrow{color:var(--film-accent)}
.exh-copy .d-l{color:var(--film-display)}
.exh-copy .lead{color:var(--film-body);margin:1.25rem 0 0}
/* no render: the h1 stands on paper like every other page */
.exh-plain .exh-copy{position:static;width:auto;padding:0}
.exh-plain .eyebrow{color:var(--brass)}
.exh-plain .d-l{color:var(--champagne)}
.exh-plain .lead{color:var(--vapour)}

.exi-sec{padding-block:clamp(3rem,6vw,4.5rem) clamp(2rem,4vw,3rem)}
.exi-prose p:first-child{
  font-family:var(--display);font-weight:700;text-transform:uppercase;letter-spacing:.03em;
  font-size:clamp(1.25rem,2.2vw,1.625rem);line-height:1.2;color:var(--champagne);
  margin-bottom:1.25rem;
}

/* ── /exclusives/: the grids ─────────────────────────────────────────── */
.exg-sec .exg-lead{margin-top:1rem}
.exg-sec .exg-stage{margin-top:clamp(1.5rem,3vw,2.25rem)}
.exg{display:grid;grid-template-columns:repeat(auto-fill,minmax(13rem,1fr));gap:clamp(.75rem,2vw,1.5rem)}
.exg-1{grid-template-columns:minmax(0,22rem)}
.exg-2{grid-template-columns:repeat(2,minmax(0,22rem))}
/* one card: the stage hugs it instead of framing a dark void */
.exg-stage-1{width:fit-content;max-width:100%}
/* five cards: one row at desktop rather than four plus an orphan */
@media (min-width:901px){.exg-5{grid-template-columns:repeat(5,minmax(0,1fr))}}

/* ── /distribution/ ──────────────────────────────────────────────────── */
.dist-intro{padding-bottom:clamp(2rem,4vw,3rem)}
.dist-film-sec{padding-bottom:clamp(2.5rem,5vw,4rem)}
.dist-film-sec > .meta{margin-top:1rem}
.distro-stage{position:relative}
.distro-vid{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius)}

/* ── about + visit: real photos, cut into the paper like the storefront ─ */
.photo-band{display:grid;gap:clamp(.75rem,2vw,1.25rem);margin-top:clamp(2.5rem,5vw,4rem)}
.photo-band-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.photo-band-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.photo-band img{
  width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius);
  -webkit-mask-image:linear-gradient(180deg,#000 70%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 70%,transparent 100%);
}
.about-band .prose,.visit-band .prose{margin-top:1.25rem}
.about-band .feature-img img,.visit-band .feature-img img{aspect-ratio:16/9;object-fit:cover}
.about-stage{align-self:center}
.about-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(.75rem,2vw,1.25rem)}
.about-band-nomedia{grid-template-columns:1fr}

/* ── responsive ──────────────────────────────────────────────────────── */
@media (max-width:900px){
  /* the hero stacks: copy first, on the dark stage, then the render cropped
     to its right half so the products stay large */
  .exh-copy{position:static;width:auto;padding:clamp(1.25rem,4vw,2rem) clamp(1rem,3vw,1.5rem) clamp(1rem,3vw,1.5rem)}
  .exh-img{aspect-ratio:4/3;object-fit:cover;object-position:100% 50%}
  .photo-band-3{grid-template-columns:repeat(2,minmax(0,1fr))}
  .photo-band-3 img:last-child{grid-column:1 / -1;aspect-ratio:2/1}
}
@media (max-width:640px){
  .teaser-head{align-items:flex-start}
  .teaser-head .btn{margin-bottom:0}
  /* three cards keep their size and the strip scrolls sideways with snap;
     the second card peeks in from the edge so the gesture is obvious */
  .teaser-grid{
    /* a share of the strip, not a fixed rem: with 9.5rem cards a 390px phone
       showed cards one and two exactly and nothing of the third, so nothing
       said "this scrolls". 70% leaves the second card peeking at every width
       (85px at 390, 70px at 320); the 16rem cap keeps large phones sane. */
    grid-template-columns:repeat(3,min(16rem,70%));
    overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;
    padding:2px 2px 6px;margin:-2px -2px 0;
  }
  .teaser-grid::-webkit-scrollbar{display:none}
  .teaser-grid > *{scroll-snap-align:start}
  .exg{grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}
  /* fixed max, not 1fr: the stage is width:fit-content, and a 1fr track in a
     fit-content box is sized from its content - which is the heading's text
     until the lazy render loads, then the render itself. Measured at 390px:
     the stage jumped 228 -> 350px wide and 150px taller when the image
     arrived (the integration gate caught the shift). 13rem is the card the
     phone layout was designed around, and it no longer moves. */
  .exg-1{grid-template-columns:minmax(0,13rem)}
  .exg-2{grid-template-columns:repeat(2,minmax(0,1fr))}
  .pcard-name{font-size:.8125rem}
  .pcard-detail{font-size:.6875rem}
  .photo-band{grid-template-columns:1fr}
  .photo-band-3 img:last-child{grid-column:auto;aspect-ratio:16/9}
  .about-cards{gap:.75rem}
  .distro-strip-in .btn{width:100%;justify-content:center}
}
@media (max-width:400px){
  .teaser-grid{grid-template-columns:repeat(3,min(16rem,72%))}
}
/* landscape phones: the hero copy and the render side by side again, and
   both stages crop to a wider band so each fits inside one screen height
   (844x390: 21/9 of a 776px stage is 333px, plus the stage's own frame) */
@media (max-width:900px) and (max-height:500px){
  .exh-copy{position:absolute;top:0;bottom:0;left:0;width:min(46%,34rem);padding:clamp(1.25rem,3vw,2rem)}
  .exh-img{aspect-ratio:21/9;object-position:100% 50%}
  .exh-copy .lead{display:none}
  .distro-vid{aspect-ratio:21/9}
}

/* ═══ END LENTI-PAGES ═══ */

/* ═══ BEGIN LENTI-COMPONENT ═══ */
/* ═══════════════════════════════════════════════════════════════════════
   LENTI — the media stage, the case carousel, the glow border, the
   distribution film. Component CSS for lenti.js. Appended by the integrator;
   every rule here is scoped to .media-stage, .lenti*, .glow-*, .distro-* or
   .media-toggle, so it collides with nothing above it.

   The stage is the site's "lit window cut into the paper wall": dark only
   where the content IS media. Any type on it uses the --film-* tokens.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── LENTI: the media stage ────────────────────────────────────────── */
.media-stage{
  position:relative;
  background:var(--film-ground);
  border-radius:var(--radius);
  padding:clamp(1rem,2.5vw,1.5rem);
  color:var(--film-body);
  overflow:hidden;
}
.media-stage :is(h1,h2,h3,h4,.d-xl,.d-l,.d-m,.d-s){color:var(--film-display)}
.media-stage :is(p,li,.lead,.prose,.meta){color:var(--film-body)}
.media-stage .eyebrow{color:var(--film-accent)}
.media-stage a:not(.btn):not(.btn-sq){color:var(--film-accent)}
.media-stage img,.media-stage video{display:block;max-width:100%}

/* buttons on the stage: the smoked glass the film's pause control uses.
   The edge is brass at .6 (4.4:1 on the veil) so the button box clears the
   3:1 non-text floor on its own; the ring is champagne-brass, not copper. */
.media-stage .btn-sq{
  --lg-tint:18 15 12;              --lg-a:.55;   --lg-ga:.55;
  --lg-fg:var(--film-body);
  --lg-edge:rgb(216 188 158/.60);
  --lg-hi:rgb(239 227 188/.34);
  --lg-lo:rgb(216 188 158/.12);
  --lg-spec:255 244 214;           --lg-spec-a:.08;
  --lg-shade:0 0 0;                --lg-shade-a:.28;
  --lg-drop:0 6px 16px -8px rgb(0 0 0/.70);
  --lg-tint-hot:30 25 20;          --lg-a-hot:.66; --lg-ga-hot:.66;
  --lg-fg-hot:#EFE3BC;
  --lg-edge-hot:rgb(216 188 158/1);
  --lg-hi-hot:rgb(239 227 188/.56);
  --lg-lo-hot:rgb(216 188 158/.20);
  --lg-spec-a-hot:.26;
  --lg-drop-hot:0 10px 22px -10px rgb(0 0 0/.80);
  --lg-drop-press:0 2px 6px -3px rgb(0 0 0/.70);
  --lg-ring:var(--film-accent);
  --ring:var(--film-accent);
}
.media-stage .btn-sq:focus-visible{outline-color:var(--film-accent)}

/* ── LENTI: the carousel ───────────────────────────────────────────── */
.lenti{
  --lenti-w:260px;          /* upstream cardWidth */
  --lenti-gap:26px;         /* upstream gap */
  --lenti-strips:16;        /* rib cap for this breakpoint (upstream 56, was 28:
                               every rib is 3 composited layers, and 28 of them
                               is what made the turn drop frames on phones) */
  --lenti-speed:1.35;       /* turn = 1000/speed ms, so ~740ms instead of 1s */
  --lenti-persp:1200px;
  --lenti-turn:1000ms;      /* 1000 / speed, written by lenti.js */
  --lenti-tilt:14deg;
  --lenti-lift:58px;        /* travel × card width × .35, written by lenti.js */
  --lenti-foil:.5;
  --lenti-scale:.9;
  --lenti-dim:.55;
  --lenti-bleed:44px;       /* room above and below a card for its glow */
  --lenti-glow-pad:34px;
  --lenti-ease:cubic-bezier(.22,.61,.36,1);
  --lenti-slide:.55s;
  /* the "print": a warm brass monochrome of the poster, CSS filters only */
  --lenti-plate:grayscale(1) sepia(.72) saturate(1.35) brightness(.86) contrast(1.08);
  padding-bottom:clamp(.75rem,2vw,1rem);
}
.lenti-viewport{
  position:relative;overflow:hidden;
  touch-action:pan-y;                 /* a vertical swipe is the page's */
  outline:0;
}
.lenti-viewport:focus-visible{outline:2px solid var(--film-accent);outline-offset:-2px}

/* the row: without JS a centred, scroll-snapping strip of colour posters */
.lenti-track{
  position:relative;display:flex;gap:var(--lenti-gap);align-items:flex-start;
  margin:0;list-style:none;
  padding:var(--lenti-bleed) calc(50% - var(--lenti-w) / 2);
  overflow-x:auto;overflow-y:hidden;
  scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.lenti-track::-webkit-scrollbar{display:none}
.lenti-slide{
  position:relative;flex:none;width:var(--lenti-w);margin:0;padding:0;
  scroll-snap-align:center;
}
.lenti-label{
  margin:.85rem 0 0;padding:0 .25rem;
  font-family:var(--body);font-size:.9375rem;line-height:1.4;text-align:center;
  color:var(--film-body);
  min-height:2.8em;                   /* two lines, so every slide is one height */
  transition:opacity .4s ease;
}

/* live: the same boxes, moved by transform alone (no layout shift) */
.lenti.is-live .lenti-track{overflow:visible;scroll-snap-type:none}
.lenti.is-live .lenti-slide{
  transition:transform var(--lenti-slide) var(--lenti-ease),opacity var(--lenti-slide) ease,visibility 0s 0s;
}
.lenti.is-live .lenti-slide:not(.is-active){opacity:var(--lenti-dim)}
.lenti.is-live .lenti-slide.is-active{z-index:2}
.lenti.is-live .lenti-slide.is-off{
  visibility:hidden;
  transition:transform var(--lenti-slide) var(--lenti-ease),opacity var(--lenti-slide) ease,visibility 0s var(--lenti-slide);
}
.lenti.is-live.is-dragging .lenti-slide{transition:none}
.lenti.is-live .lenti-slide:not(.is-active) .lenti-label{opacity:0}
@media (hover:hover) and (pointer:fine){
  .lenti.is-live .lenti-viewport{cursor:grab;-webkit-user-select:none;user-select:none}
  .lenti.is-live.is-dragging .lenti-viewport{cursor:grabbing}
  .lenti.is-live .lenti-slide:not(.is-active) .lenti-card{cursor:pointer}
}

/* the card: one layer at rest */
.lenti-card{
  position:relative;aspect-ratio:3/4;overflow:hidden;
  border-radius:calc(var(--radius) - 1px);
  background:#17130F;
}
.lenti-poster{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  -webkit-user-select:none;user-select:none;-webkit-user-drag:none;
}
.lenti.is-live .lenti-card:not(.is-back) .lenti-poster{filter:var(--lenti-plate)}
.lenti-video{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  /* No crossfade. The clip replaces its poster in one frame: a 450ms opacity
     transition over a playing video is a full-card composite every frame on
     the exact frame budget the decode already wants (client, 2026-09-13). */
  opacity:0;transition:none;pointer-events:none;
}
.lenti-card.is-playing .lenti-video{opacity:1}
/* during a turn the ribs ARE the picture; the seams show the card's dark stock */
.lenti-card.is-turning > .lenti-poster,
.lenti-card.is-turning > .lenti-video{visibility:hidden}

/* the ribs: N strips, each a two-faced 3D sliver under one perspective */
.lenti-ribs{
  position:absolute;inset:0;pointer-events:none;
  perspective:var(--lenti-persp);perspective-origin:50% 50%;
}
.lenti-rib{
  position:absolute;top:0;bottom:0;
  transform-style:preserve-3d;
  animation-duration:var(--lenti-flip);
  animation-timing-function:cubic-bezier(.45,.02,.3,1);
  animation-fill-mode:both;
  animation-delay:calc(var(--k) * var(--lenti-lag));
}
.lenti-face{
  position:absolute;inset:0;
  -webkit-backface-visibility:hidden;backface-visibility:hidden;
  background-repeat:no-repeat;
  background-size:var(--lenti-bs) 100%;
  background-position:var(--lenti-bp) 0;
}
.lenti-face-front{filter:var(--lenti-plate)}
.lenti-face-back{transform:rotateY(180deg)}
/* foil: a champagne-to-rose sheen that crosses each face as it catches the light */
.lenti-face::after{
  content:"";position:absolute;inset:0;
  transform:translateZ(.5px);
  opacity:0;
  background:linear-gradient(100deg,transparent 12%,rgb(239 227 188/.85) 46%,rgb(231 166 161/.45) 58%,transparent 86%);
  animation-duration:var(--lenti-flip);
  animation-timing-function:linear;
  animation-fill-mode:both;
  animation-delay:calc(var(--k) * var(--lenti-lag));
}
/* The foil is an extra animated layer on both faces of every rib. On a
   touch device nobody hovers to see it and it doubles the turn's paint, so
   it is dropped there (client: too much lag on devices, 2026-09-13). */
@media (pointer:coarse){
  .lenti-face::after{content:none}
}
.lenti-ribs.to-back .lenti-rib{animation-name:lenti-to-back}
.lenti-ribs.to-front .lenti-rib{animation-name:lenti-to-front}
.lenti-ribs.to-back .lenti-face-front::after,
.lenti-ribs.to-front .lenti-face-back::after{animation-name:lenti-foil-first}
.lenti-ribs.to-back .lenti-face-back::after,
.lenti-ribs.to-front .lenti-face-front::after{animation-name:lenti-foil-second}

/* refraction: the rib slides sideways as it stands on edge, so the picture
   shears across the ribs and settles back — the lens catching the image */
@keyframes lenti-to-back{
  0%{transform:translateX(0) rotateY(0deg)}
  50%{transform:translateX(var(--lenti-ref)) rotateY(90deg)}
  100%{transform:translateX(0) rotateY(180deg)}
}
@keyframes lenti-to-front{
  0%{transform:translateX(0) rotateY(180deg)}
  50%{transform:translateX(var(--lenti-ref)) rotateY(270deg)}
  100%{transform:translateX(0) rotateY(360deg)}
}
@keyframes lenti-foil-first{0%{opacity:0}30%{opacity:var(--lenti-foil)}50%,100%{opacity:0}}
@keyframes lenti-foil-second{0%,50%{opacity:0}70%{opacity:var(--lenti-foil)}100%{opacity:0}}

/* travel + tilt: the whole print lifts toward the viewer and tilts with the
   direction of travel while it turns (--lenti-dir is ±1 from lenti.js) */
.lenti.is-live .lenti-slide.is-turning .glow-card{
  animation:lenti-lift var(--lenti-turn) cubic-bezier(.4,0,.2,1) both;
}
@keyframes lenti-lift{
  0%,100%{transform:perspective(var(--lenti-persp)) translateZ(.01px) rotateY(0deg)}
  50%{transform:perspective(var(--lenti-persp)) translateZ(var(--lenti-lift)) rotateY(calc(var(--lenti-tilt) * var(--lenti-dir,1)))}
}

/* the bar: prev · count · pause · next, on the stage */
.lenti-bar{
  display:flex;align-items:center;justify-content:center;gap:.75rem;
  margin-top:.25rem;
}
.lenti:not(.is-live) .lenti-bar{visibility:hidden}     /* no JS: box kept, controls gone */
.btn-sq.lenti-prev,.btn-sq.lenti-next,.btn-sq.lenti-toggle{
  flex:none;width:40px;height:40px;padding:0;display:inline-grid;place-items:center;
}
.btn-sq.lenti-toggle[hidden]{display:none}
.lenti-prev svg,.lenti-next svg{
  width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;
  stroke-linecap:round;stroke-linejoin:round;
}
.lenti-toggle svg{width:14px;height:14px;fill:currentColor}
.lenti-toggle .i-play{display:none}
.lenti-toggle[aria-pressed="true"] .i-pause{display:none}
.lenti-toggle[aria-pressed="true"] .i-play{display:block}
.lenti-count{
  margin:0;min-width:6ch;text-align:center;
  font-family:var(--mono);font-size:.8125rem;letter-spacing:.12em;text-transform:uppercase;
  color:var(--film-accent);
}
@media (pointer:coarse){
  .btn-sq.lenti-prev,.btn-sq.lenti-next,.btn-sq.lenti-toggle{width:44px;height:44px}
}

/* ── LENTI: the glow border (BorderGlow, warmed) ───────────────────────
   Upstream's layers unchanged: ::before is the mesh-gradient border in a
   cone toward the pointer, ::after a soft-light wash near the edges, the
   .edge-light span the outer halo (plus-lighter, so it lights the dark
   stage). Only the palette moved: champagne, brass, copper and warm rose,
   sampled from the lit cases. Never blue, green or purple. */
.glow-card{
  --edge-proximity:0;
  --cursor-angle:45deg;
  --edge-sensitivity:30;
  --color-sensitivity:calc(var(--edge-sensitivity) + 20);
  --border-radius:var(--radius);
  --glow-padding:var(--lenti-glow-pad,34px);
  --cone-spread:25;
  --fill-opacity:.42;
  --card-bg:#17130F;
  /* upstream glowIntensity ≈ 1.5: the stage is near-black and the halo is
     plus-lighter, so the default 10–60 % steps read as a faint haze */
  --glow-color:hsl(46 62% 84%);
  --glow-color-60:hsl(46 62% 84% / 90%);
  --glow-color-50:hsl(46 62% 84% / 75%);
  --glow-color-40:hsl(46 62% 84% / 60%);
  --glow-color-30:hsl(43 60% 80% / 45%);
  --glow-color-20:hsl(32 52% 68% / 32%);
  --glow-color-10:hsl(27 53% 54% / 18%);
  --gradient-one:radial-gradient(at 80% 55%,#EFE3BC 0px,transparent 50%);
  --gradient-two:radial-gradient(at 69% 34%,#E7A6A1 0px,transparent 50%);
  --gradient-three:radial-gradient(at 8% 6%,#C8834A 0px,transparent 50%);
  --gradient-four:radial-gradient(at 41% 38%,#EFE3BC 0px,transparent 50%);
  --gradient-five:radial-gradient(at 86% 85%,#E7A6A1 0px,transparent 50%);
  --gradient-six:radial-gradient(at 82% 18%,#C8834A 0px,transparent 50%);
  --gradient-seven:radial-gradient(at 51% 4%,#D8BC9E 0px,transparent 50%);
  --gradient-base:linear-gradient(#D8BC9E 0 100%);

  position:relative;
  border-radius:var(--border-radius);
  isolation:isolate;
  transform:translate3d(0,0,.01px);
  display:grid;
  border:1px solid rgb(216 188 158/.18);
  background:var(--card-bg);
  overflow:visible;
  box-shadow:0 1px 2px rgb(0 0 0/.30),0 8px 16px -6px rgb(0 0 0/.35),0 28px 48px -18px rgb(0 0 0/.55);
}
.glow-card::before,
.glow-card::after,
.glow-card > .edge-light{
  content:"";position:absolute;inset:0;border-radius:inherit;
  transition:opacity .25s ease-out;z-index:-1;
}
.glow-card:not(:hover):not(.sweep-active)::before,
.glow-card:not(:hover):not(.sweep-active)::after,
.glow-card:not(:hover):not(.sweep-active) > .edge-light{
  opacity:0;transition:opacity .75s ease-in-out;
}
/* the mesh-gradient border, masked to a cone toward the pointer */
.glow-card::before{
  border:1px solid transparent;
  background:
    linear-gradient(var(--card-bg) 0 100%) padding-box,
    linear-gradient(rgb(255 255 255/0) 0 100%) border-box,
    var(--gradient-one) border-box,
    var(--gradient-two) border-box,
    var(--gradient-three) border-box,
    var(--gradient-four) border-box,
    var(--gradient-five) border-box,
    var(--gradient-six) border-box,
    var(--gradient-seven) border-box,
    var(--gradient-base) border-box;
  opacity:calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
  -webkit-mask-image:conic-gradient(from var(--cursor-angle) at center,
    black calc(var(--cone-spread) * 1%),transparent calc((var(--cone-spread) + 15) * 1%),
    transparent calc((100 - var(--cone-spread) - 15) * 1%),black calc((100 - var(--cone-spread)) * 1%));
  mask-image:conic-gradient(from var(--cursor-angle) at center,
    black calc(var(--cone-spread) * 1%),transparent calc((var(--cone-spread) + 15) * 1%),
    transparent calc((100 - var(--cone-spread) - 15) * 1%),black calc((100 - var(--cone-spread)) * 1%));
}
/* the wash near the edges */
.glow-card::after{
  border:1px solid transparent;
  background:
    var(--gradient-one) padding-box,
    var(--gradient-two) padding-box,
    var(--gradient-three) padding-box,
    var(--gradient-four) padding-box,
    var(--gradient-five) padding-box,
    var(--gradient-six) padding-box,
    var(--gradient-seven) padding-box,
    var(--gradient-base) padding-box;
  -webkit-mask-image:
    linear-gradient(to bottom,black,black),
    radial-gradient(ellipse at 50% 50%,black 40%,transparent 65%),
    radial-gradient(ellipse at 66% 66%,black 5%,transparent 40%),
    radial-gradient(ellipse at 33% 33%,black 5%,transparent 40%),
    radial-gradient(ellipse at 66% 33%,black 5%,transparent 40%),
    radial-gradient(ellipse at 33% 66%,black 5%,transparent 40%),
    conic-gradient(from var(--cursor-angle) at center,transparent 5%,black 15%,black 85%,transparent 95%);
  mask-image:
    linear-gradient(to bottom,black,black),
    radial-gradient(ellipse at 50% 50%,black 40%,transparent 65%),
    radial-gradient(ellipse at 66% 66%,black 5%,transparent 40%),
    radial-gradient(ellipse at 33% 33%,black 5%,transparent 40%),
    radial-gradient(ellipse at 66% 33%,black 5%,transparent 40%),
    radial-gradient(ellipse at 33% 66%,black 5%,transparent 40%),
    conic-gradient(from var(--cursor-angle) at center,transparent 5%,black 15%,black 85%,transparent 95%);
  -webkit-mask-composite:source-out,source-over,source-over,source-over,source-over,source-over;
  mask-composite:subtract,add,add,add,add,add;
  opacity:calc(var(--fill-opacity) * (var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
  mix-blend-mode:soft-light;
}
/* the outer halo */
.glow-card > .edge-light{
  inset:calc(var(--glow-padding) * -1);
  pointer-events:none;
  z-index:1;
  -webkit-mask-image:conic-gradient(from var(--cursor-angle) at center,black 2.5%,transparent 10%,transparent 90%,black 97.5%);
  mask-image:conic-gradient(from var(--cursor-angle) at center,black 2.5%,transparent 10%,transparent 90%,black 97.5%);
  opacity:calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));
  mix-blend-mode:plus-lighter;
}
.glow-card > .edge-light::before{
  content:"";position:absolute;inset:var(--glow-padding);border-radius:inherit;
  box-shadow:
    inset 0 0 0 1px var(--glow-color),
    inset 0 0 1px 0 var(--glow-color-60),
    inset 0 0 3px 0 var(--glow-color-50),
    inset 0 0 6px 0 var(--glow-color-40),
    inset 0 0 15px 0 var(--glow-color-30),
    inset 0 0 25px 2px var(--glow-color-20),
    inset 0 0 50px 2px var(--glow-color-10),
    0 0 1px 0 var(--glow-color-60),
    0 0 3px 0 var(--glow-color-50),
    0 0 6px 0 var(--glow-color-40),
    0 0 15px 0 var(--glow-color-30),
    0 0 25px 2px var(--glow-color-20),
    0 0 50px 2px var(--glow-color-10);
}
.glow-inner{
  display:flex;flex-direction:column;position:relative;z-index:1;
  overflow:hidden;border-radius:calc(var(--border-radius) - 1px);
  min-width:0;
}
.glow-inner > img{width:100%;height:auto}

/* ── LENTI: the distribution film ──────────────────────────────────── */
.distro-stage{padding:0}
.distro-vid{
  display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;
  background:var(--film-ground);
}
.btn-sq.media-toggle{
  position:absolute;right:.75rem;bottom:.75rem;z-index:2;
  flex:none;width:40px;height:40px;padding:0;display:inline-grid;place-items:center;
}
.btn-sq.media-toggle[hidden]{display:none}
.media-toggle svg{width:14px;height:14px;fill:currentColor}
.media-toggle .i-play{display:none}
.media-toggle[aria-pressed="true"] .i-pause{display:none}
.media-toggle[aria-pressed="true"] .i-play{display:block}
@media (pointer:coarse){.btn-sq.media-toggle{width:44px;height:44px}}

/* ── LENTI: reduced motion / still tier ────────────────────────────────
   No ribs (lenti.js swaps the class; the poster crossfades from plate to
   colour), no sliding (the strip re-places itself at once), no glow
   travel, no film lift. */
@media (prefers-reduced-motion:reduce){
  .lenti.is-live .lenti-slide,.lenti.is-live .lenti-slide.is-off{transition:none}
  .lenti-poster{transition:filter .35s ease}
  .lenti-video{transition:none}
  .lenti-label{transition:none}
  .glow-card::before,.glow-card::after,.glow-card > .edge-light{transition:opacity .2s ease}
}
html[data-fx="still"] .lenti.is-live .lenti-slide,
html[data-fx="still"] .lenti.is-live .lenti-slide.is-off{transition:none}
html[data-fx="still"] .lenti-poster{transition:filter .35s ease}

/* ── LENTI: breakpoints ─────────────────────────────────────────────── */
@media (max-width:900px){
  .lenti{--lenti-w:220px;--lenti-gap:20px;--lenti-strips:8;--lenti-bleed:36px;--lenti-glow-pad:28px}
  .lenti-label{font-size:.875rem}
}
@media (max-width:640px){
  .lenti{--lenti-w:200px;--lenti-gap:18px;--lenti-bleed:30px;--lenti-glow-pad:24px}
  .media-stage{padding:.875rem}
  .lenti-bar{gap:.5rem}
  .lenti-count{min-width:5.5ch;font-size:.75rem}
}
@media (max-width:400px){
  .lenti{--lenti-w:176px;--lenti-gap:16px;--lenti-bleed:26px;--lenti-glow-pad:20px}
  .lenti-label{font-size:.8125rem;margin-top:.65rem}
}
/* landscape phones: a short stage, so the print is smaller and the label is one line */
@media (max-width:900px) and (max-height:500px){
  .lenti{--lenti-w:150px;--lenti-gap:16px;--lenti-bleed:22px;--lenti-glow-pad:18px}
  .media-stage{padding:.75rem}
  .lenti-label{min-height:0;margin-top:.5rem;font-size:.8125rem}
  .lenti-bar{margin-top:0}
  .btn-sq.lenti-prev,.btn-sq.lenti-next,.btn-sq.lenti-toggle{width:38px;height:38px}
}

/* ═══ END LENTI-COMPONENT ═══ */

/* ═══ BEGIN EFFECTS ═══ */
/* ── the electric frame (effects.js, ElectricBorder port) ──────────────
   [data-electric] wraps a product render. The wrapper pads the render so
   the filament and its glow have room inside the card's clipped inner box;
   the hero stage has its own padding, so there the pad is 0. Colours are
   the lit-case metals as plain rgba: champagne #EFE3BC for the line and
   the inner glow, brass #D8BC9E for the outer glow and the bloom. Never
   the upstream purple, never blue. */
.efx{
  --efx-pad:10px;
  --efx-color:rgba(216,188,158,1);        /* brass */
  --efx-color-60:rgba(216,188,158,.6);
  --efx-light:rgba(239,227,188,1);        /* champagne */
  --efx-stroke:rgba(239,227,188,.95);
  --efx-line:1;
  display:block;position:relative;padding:var(--efx-pad);
  border-radius:var(--radius);isolation:isolate;min-width:0;
}
.efx > img{position:relative;z-index:1}
.efx-layers{position:absolute;inset:var(--efx-pad);border-radius:var(--radius);pointer-events:none;z-index:0}
.efx-layers > span{position:absolute;inset:0;display:block;border-radius:inherit;box-sizing:border-box;pointer-events:none}
.efx-glow-1{border:2px solid var(--efx-color-60);filter:blur(1px)}
.efx-glow-2{border:2px solid var(--efx-light);filter:blur(4px)}
.efx-glow-bg{
  z-index:-1;transform:scale(1.1);filter:blur(32px);opacity:.3;
  background:linear-gradient(-30deg,var(--efx-light),transparent,var(--efx-color));
}
.efx-canvas{position:absolute;left:0;top:0;display:block;pointer-events:none;z-index:2}
.exh .efx{--efx-pad:0}
@media (max-width:700px){
  .efx{--efx-pad:8px}
  .efx-glow-bg{filter:blur(14px)}
}
@supports not (filter: blur(3px)){
  .efx-layers{display:none}
}

/* ── the scroll blur (effects.js, GradualBlur port) ────────────────────
   One fixed band at the page bottom, stacked backdrop-filter layers that
   effects.js builds and fades with scroll. z-index 50: under the dock
   (55), the nav (60) and the 21+ card (120). At opacity 0 it is hidden,
   so no backdrop is sampled. No grey fallback: without backdrop-filter
   there is nothing to show. */
.efx-blur{
  position:fixed;left:0;right:0;bottom:0;height:6rem;z-index:50;
  pointer-events:none;opacity:0;isolation:isolate;
}
.efx-blur.is-off{visibility:hidden}
.efx-blur > div{position:absolute;inset:0;pointer-events:none}
@media (max-width:700px),(pointer:coarse){
  .efx-blur{height:4rem}
}
@media (prefers-reduced-transparency:reduce){
  .efx-blur{display:none}
}
html[data-fx="still"] .efx-blur{display:none}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .efx-blur{display:none}
}
/* ═══ END EFFECTS ═══ */

/* ═══ BEGIN RELEASE ═══ */
/* Six nav links fit one row only on desktop; at 900 px and below the two
   newest pages are reached from the footer, the homepage and the FAQ. */
@media (max-width:900px){.nav-links a.nav-extra{display:none}}
/* ═══ END RELEASE ═══ */

/* ═══ BEGIN VAPE ═══ */
/* ── the warehouse film on the house vape's screen ─────────────────────
   assets/img/vape-*.webp is a green-screen render with the screen keyed to
   transparency, so the film is simply UNDER the render and shows through the
   hole. The hole's rectangle comes from the alpha channel at build time and
   arrives as --vape-x/y/w/h on .vape, so nothing here is eyeballed.
   The device is the only green on the site, and it is a photograph of a
   product, not UI chrome. */
/* the device is 460px wide, so a full-bleed stage would be mostly empty
   black either side of it */
.vape-stage{display:grid;justify-items:center;padding:clamp(1rem,3vw,2rem) 0;
  max-width:46rem;margin-inline:auto}
.vape{
  position:relative;
  width:min(460px,76vw);
  aspect-ratio:1696/2528;         /* the render's own ratio */
}
.vape-vid{
  position:absolute;
  left:var(--vape-x);top:var(--vape-y);width:var(--vape-w);height:var(--vape-h);
  object-fit:cover;object-position:50% 50%;
  background:#040705;             /* before the first frame: a dark screen, not a hole */
  border-radius:2.2%/1.5%;
}
.vape-shell{
  position:relative;              /* paints over the film; the hole is its alpha */
  display:block;width:100%;height:auto;pointer-events:none;
  -webkit-user-select:none;user-select:none;-webkit-user-drag:none;
}
.vape .btn-sq.media-toggle{
  position:absolute;right:-.35rem;bottom:1.5rem;z-index:2;
}
@media (max-width:640px){
  .vape{width:min(320px,82vw)}
  .vape .btn-sq.media-toggle{right:-.5rem;bottom:1rem}
}
@media (max-width:900px) and (max-height:500px){
  .vape{width:min(210px,40vw)}
}
/* ═══ END VAPE ═══ */

/* ═══ BEGIN DISTRO ═══ */
/* Infinity Distro only. Everything above is the shop's stylesheet, copied
   byte-for-byte so the two sites read as one company; this block adds the
   pieces the shop site has no use for: the wholesale hero, the house-line
   band and the inquiry form. Same tokens, no new hues. */
.dist-hero{padding-top:clamp(3rem,9vw,6.5rem)}
.dist-hero .d-xl{max-width:14ch;margin-bottom:1.25rem}
.dist-hero .lead{margin-bottom:2rem}

.house-stage{padding:clamp(1rem,3vw,1.75rem)}
.house-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(.75rem,2vw,1.25rem)}
.house-grid .pcard{margin:0}
@media (max-width:640px){ .house-grid{grid-template-columns:repeat(2,1fr)} .house-grid .pcard:last-child{display:none} }

.about-stage img{display:block;width:100%;height:auto;border-radius:calc(var(--radius) - 2px)}

/* ── the inquiry form ──────────────────────────────────────────────────
   One column of honest fields. The labels sit above their inputs because a
   shop owner fills this on a phone, one thumb, in a doorway. */
.inq{max-width:46rem}
.inq-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem 1.25rem}
.inq .field{margin:0 0 1.25rem;display:flex;flex-direction:column;gap:.4rem}
.inq-grid .field{margin:0}
.inq label{font:500 .75rem/1.4 var(--mono,ui-monospace,Menlo,monospace);letter-spacing:.12em;
  text-transform:uppercase;color:var(--muted)}
.inq input[type="text"],.inq input[type="tel"],.inq input[type="email"],.inq textarea{
  font:400 1rem/1.5 inherit;color:var(--champagne);background:var(--paper-lift);
  border:1px solid var(--metal-deep);border-radius:calc(var(--radius) - 4px);
  padding:.75rem .85rem;width:100%;transition:border-color .18s ease,box-shadow .18s ease;
}
.inq textarea{resize:vertical;min-height:7rem}
.inq input:focus-visible,.inq textarea:focus-visible{
  outline:none;border-color:var(--specular);box-shadow:0 0 0 3px color-mix(in srgb,var(--specular) 22%,transparent)}
.inq input:user-invalid,.inq textarea:user-invalid{border-color:var(--ember)}
.field-check{flex-direction:row;align-items:flex-start;gap:.65rem}
.field-check input{width:1.15rem;height:1.15rem;margin-top:.15rem;accent-color:var(--specular);flex:0 0 auto}
.field-check label{font:400 .9375rem/1.5 inherit;letter-spacing:0;text-transform:none;color:var(--vapour)}
.inq .hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.inq-status{min-height:1.5rem;margin:.85rem 0 0;font-size:.9375rem;color:var(--muted)}
.inq-status.is-ok{color:var(--champagne)}
.inq-status.is-warn{color:var(--ember-text)}
.inq button[disabled]{opacity:.6;cursor:progress}
@media (max-width:640px){ .inq-grid{grid-template-columns:1fr} }
/* ═══ END DISTRO ═══ */
