/* ═══════════════════════════════════════════════════════════════════════
   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, url("/assets/img/vape-walk.png")) 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, url("/assets/img/vape-idle.png"));
  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 url("/assets/img/film-poster.jpg") 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}
}
@keyframes wmSettle{
  from{opacity:0;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}}
