/* ============================================================================
   THE stylesheet (SPEC §3, CLAUDE.md rule 1)
   ----------------------------------------------------------------------------
   The operator's name is deliberately not in this header: this file is served
   verbatim from every site in the portfolio, so anything written here is
   published on all of them. Keep it descriptive, never attributive.
   ----------------------------------------------------------------------------
   This is the ONLY authored stylesheet in the factory. Every site in the
   portfolio loads this file unchanged, then a generated tokens.css that
   contains nothing but :root custom-property overrides from config.yaml.

   Rules for editing:
     - No literal colors below :root. Use var(--*).
     - No new effects in page templates. Add a shared class here instead.
     - Effects are CSS-only. No animation libraries, no JS-driven motion.
     - Contrast meets WCAG AA against --bg. Readable on a phone in sunlight
       beats pretty.
   ========================================================================== */

/* ---------------------------------------------------------------- 1. Tokens */

/* ---------------------------------------------------------------------------
   Metric-matched font fallbacks — layout stability during the font swap.

   Google Fonts loads with `display=swap`: text paints in a fallback, then
   re-paints when the web font lands. Different metrics mean the text resizes
   and everything under it moves. Cloudflare Web Analytics measured exactly
   that on 2026-08-02 — CLS 0.114 on `div.grid-auto-narrow`, the /shop product
   grid, pushed down by the `.section-header h2` (weight 900) above it.

   TWO THINGS MAKE THIS EASY TO GET WRONG, and both were hit before this landed:

   1. A `@font-face` with `src: local()` NEVER SYNTHESISES BOLD. Georgia via
      such a face renders at regular weight even at 900, where the plain
      `Georgia` keyword would be faux-bolded ~16% wider. Tuning a single face
      against faux-bold measurements moved the error the WRONG WAY twice
      (-9.1%, then -14.3%). Each weight therefore names the REAL bold font.

   2. One size-adjust cannot serve every weight. Georgia against Fraunces
      measures -5.75% at 400 but +0.4% at 900.

   Every number below was measured in headless Chrome against the live font
   files (canvas TextMetrics, 2026-08-02) and verified after applying. Do not
   hand-adjust: guessed values are worse than no fallback at all. `local()`
   only, so nothing is hosted and no request is added.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: "Fraunces Fallback";
  font-weight: 400;
  src: local("Georgia"), local("Times New Roman");
  size-adjust: 106.1%;
  ascent-override: 92.37%;
  descent-override: 24.51%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Fraunces Fallback";
  font-weight: 700 900;
  src: local("Georgia Bold"), local("Georgia"), local("Times New Roman Bold");
  size-adjust: 99.61%;
  ascent-override: 98.39%;
  descent-override: 26.1%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Outfit Fallback";
  font-weight: 400;
  src: local("Segoe UI"), local("Helvetica Neue"), local("Arial");
  size-adjust: 99.54%;
  ascent-override: 100.47%;
  descent-override: 26.12%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Outfit Fallback";
  font-weight: 700 900;
  src: local("Segoe UI Bold"), local("Helvetica Neue Bold"), local("Arial Bold");
  size-adjust: 97.13%;
  ascent-override: 102.96%;
  descent-override: 26.77%;
  line-gap-override: 0%;
}

:root {
  /* -- Surfaces. Site-overridable via config.yaml -> tokens.css ------------ */
  --bg: #0c0f0a;
  --bg2: #141a0f;
  --card: #1a2114;
  --card-hover: #212b1a;
  --border: rgba(134, 239, 172, 0.08);

  /* -- Accents. Site-overridable ------------------------------------------ */
  --green: #4ade80;
  --green-dim: #22c55e;
  --green-dark: #166534;
  --lime: #a3e635;
  --amber: #fbbf24;
  --purple: #c084fc;
  --rose: #fb7185;

  /* -- Type colors. Site-overridable -------------------------------------- */
  --white: #f0fdf4;
  --text: #c2d6ba;
  /* --muted carries most body copy. The live site's #6b7f64 measured 3.81:1 on
     cards and 3.40:1 on card-hover — below the 4.5:1 AA floor SPEC §3 sets as
     non-negotiable. Same hue, lightened 19%: worst case is now 4.63:1. Do not
     darken this without re-measuring against --card-hover, the lightest
     surface it ever sits on. */
  --muted: #7f9777;

  /* -- Derived accent washes. Kept as tokens so hover/glow states never
        hard-code an rgba() halfway down the file. --------------------------- */
  --accent-wash: rgba(74, 222, 128, 0.06);
  --accent-line: rgba(74, 222, 128, 0.2);
  --accent-line-hover: rgba(74, 222, 128, 0.3);
  --accent-glow: rgba(74, 222, 128, 0.12);
  --accent-glow-soft: rgba(74, 222, 128, 0.08);

  /* Section background loops sit this far below the copy. Anything above ~0.2
     starts costing text contrast, which WCAG AA wins (SPEC §3). */
  --bg-video-opacity: 0.12;
  --shadow-cast: rgba(0, 0, 0, 0.3);
  --shadow-cast-deep: rgba(0, 0, 0, 0.4);
  --danger: #f87171;

  /* -- Fonts --------------------------------------------------------------- */
  --font-body: "Outfit", "Outfit Fallback", system-ui, -apple-system, sans-serif;
  --font-display: "Fraunces", "Fraunces Fallback", Georgia, serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;

  /* -- Type scale ---------------------------------------------------------- */
  --fs-3xs: 0.65rem;
  --fs-2xs: 0.7rem;
  --fs-xs: 0.78rem;
  --fs-sm: 0.85rem;
  --fs-base: 0.95rem;
  --fs-md: 1.05rem;
  --fs-lg: 1.3rem;
  --fs-xl: 1.6rem;
  --fs-hero: clamp(2.8rem, 5.5vw, 4.2rem);
  --fs-h2: clamp(1.8rem, 3.5vw, 2.4rem);

  /* -- Spacing ------------------------------------------------------------- */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.25rem;
  --sp-6: 1.5rem;
  --sp-8: 2rem;
  --sp-12: 3rem;
  --sp-16: 4rem;

  /* -- Radii --------------------------------------------------------------- */
  --r-sm: 6px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 20px;
  --r-pill: 100px;

  /* -- Layout -------------------------------------------------------------- */
  --w-wide: 1200px;
  --w-mid: 1000px;
  --w-narrow: 900px;
  --w-prose: 800px;

  /* -- Motion -------------------------------------------------------------- */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --t-fast: 0.2s;
  --t-mid: 0.3s;
  --t-slow: 0.4s;
}

/* ------------------------------------------------------------- 2. Reset/base */

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  overflow-x: hidden;
}

img,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  color: var(--green);
}

/* Keyboard focus must always be visible — AA requirement. */
:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: var(--sp-3) var(--sp-4);
  background: var(--green);
  color: var(--bg);
  font-weight: 700;
  border-radius: 0 0 var(--r-md) 0;
}
.skip-link:focus {
  left: 0;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------- 3. Effect classes */
/* Templates compose these. They never invent their own effects (SPEC §3).   */

/* .glass — frosted translucent surface for overlays and sticky nav */
.glass {
  background: color-mix(in srgb, var(--card) 72%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border: 1px solid var(--border);
}

/* .lift — hover elevation + accent border + inner glow */
.lift {
  position: relative;
  transition: transform var(--t-slow) var(--ease),
    box-shadow var(--t-slow) var(--ease), border-color var(--t-slow) var(--ease),
    background var(--t-slow) var(--ease);
}
.lift:hover {
  transform: translateY(-4px);
  border-color: var(--accent-line-hover);
  box-shadow: 0 0 20px var(--accent-glow), 0 4px 24px var(--shadow-cast),
    inset 0 0 20px var(--accent-wash);
}

/* .shimmer — diagonal light sweep across a card on hover */
.shimmer::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    105deg,
    transparent 40%,
    var(--accent-wash) 45%,
    var(--accent-glow) 50%,
    var(--accent-wash) 55%,
    transparent 60%
  );
  background-size: 200% 100%;
  opacity: 0;
  transition: opacity var(--t-mid);
  pointer-events: none;
  z-index: 1;
}
.shimmer:hover::after {
  opacity: 1;
  animation: shimmer 1.2s ease-in-out;
}

@keyframes shimmer {
  0% {
    background-position: -200% 0;
  }
  100% {
    background-position: 200% 0;
  }
}

/* .fade-in — scroll-reveal.
   Content is visible by DEFAULT and only hides once the script has proved it
   can run the reveal, by putting .js-reveal on <html>. Never make visibility
   depend on a callback firing: if IntersectionObserver is missing, blocked, or
   throws, the page must still render its content rather than go blank. */
.fade-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.7s ease, transform 0.7s ease;
}
.js-reveal .fade-in {
  opacity: 0;
  transform: translateY(30px);
}
.js-reveal .fade-in.visible {
  opacity: 1;
  transform: translateY(0);
}

@keyframes pulse-glow {
  0%,
  100% {
    box-shadow: 0 0 8px transparent, 0 0 20px transparent;
  }
  50% {
    box-shadow: 0 0 12px var(--accent-glow), 0 0 30px var(--accent-glow-soft);
  }
}
.pulse {
  animation: pulse-glow 3s ease-in-out infinite;
}

/* Motion is decoration. Anyone who asks for less gets none of it. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .js-reveal .fade-in,
  .js-reveal .fade-in.visible {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------ 4. Atmosphere */

.atmosphere {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
      ellipse at 15% 20%,
      var(--accent-wash) 0%,
      transparent 50%
    ),
    radial-gradient(
      ellipse at 85% 80%,
      color-mix(in srgb, var(--lime) 4%, transparent) 0%,
      transparent 50%
    ),
    radial-gradient(
      ellipse at 50% 50%,
      color-mix(in srgb, var(--purple) 3%, transparent) 0%,
      transparent 40%
    ),
    var(--bg);
}

.grain {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.3;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
}

/* ------------------------------------------------------------- 5. Age gate */
/* Cannabis profile only (SPEC §4). Rendered by base.html.j2 when
   config.profile.age_gate is true. Blocks the page until acknowledged. */

.age-gate {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.age-gate[hidden] {
  display: none;
}
.age-gate-box {
  max-width: 460px;
  width: 100%;
  padding: var(--sp-8);
  border-radius: var(--r-xl);
  background: var(--card);
  border: 1px solid var(--accent-line);
  box-shadow: 0 0 40px var(--accent-glow-soft), 0 20px 60px var(--shadow-cast-deep);
  text-align: center;
}
.age-gate-box h2 {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 900;
  color: var(--white);
  margin-bottom: var(--sp-3);
}
.age-gate-box p {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.6;
  margin-bottom: var(--sp-6);
}
.age-gate-actions {
  display: flex;
  gap: var(--sp-3);
  justify-content: center;
  flex-wrap: wrap;
}
.age-gate-deny {
  font-size: var(--fs-xs);
  color: var(--muted);
  background: none;
  border: none;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  font-family: var(--font-body);
}
.age-gate-deny:hover {
  color: var(--text);
}

/* ------------------------------------------------------------------ 6. Nav */

.site-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  transition: background var(--t-mid), border-color var(--t-mid);
  border-bottom: 1px solid transparent;
}
/* .scrolled is toggled by base.html.j2; pairs with .glass */
.site-nav.scrolled {
  border-bottom-color: var(--border);
}
.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-8);
}
.logo {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 900;
  color: var(--white);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.logo-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-md);
  background: linear-gradient(135deg, var(--bg), var(--bg2));
  border: 1px solid var(--accent-line);
  padding: 5px;
  font-size: var(--fs-md);
  flex: none;
}
/* Vector mark. Sites without a `logo_svg` render their emoji here instead and
   these rules simply do not apply. Colour comes from a token, so a site that
   overrides --green gets a mark in its own accent for free. */
.logo-mark svg {
  width: 100%;
  height: 100%;
  fill: var(--green);
}
.logo-mark .mark-stem {
  stroke: var(--green);
  stroke-width: 1.1;
  stroke-linecap: round;
  fill: none;
}
.nav-links {
  display: flex;
  gap: var(--sp-8);
  align-items: center;
}
.nav-links a {
  font-size: var(--fs-sm);
  color: var(--muted);
  text-decoration: none;
  font-weight: 500;
  transition: color var(--t-fast);
}
.nav-links a:hover {
  color: var(--green);
}

/* Mobile menu button. Hidden here and revealed below 900px — and only for
   browsers that ran the script, since it is the script that opens the panel.
   44px square is the smallest comfortable tap target on a phone. */
.nav-toggle {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-md);
  color: var(--white);
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.nav-toggle:hover {
  border-color: var(--accent-line-hover);
  background: var(--accent-wash);
}
.nav-toggle-bars {
  position: relative;
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: background var(--t-fast);
}
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--t-fast);
}
.nav-toggle-bars::before {
  top: -6px;
}
.nav-toggle-bars::after {
  top: 6px;
}
/* Open: the bars cross into an X. Middle bar goes transparent, not display
   none, so the two pseudo-elements keep their box to rotate around. */
.nav-toggle.open .nav-toggle-bars {
  background: transparent;
}
.nav-toggle.open .nav-toggle-bars::before {
  transform: translateY(6px) rotate(45deg);
}
.nav-toggle.open .nav-toggle-bars::after {
  transform: translateY(-6px) rotate(-45deg);
}

/* --------------------------------------------------- 6b. Hub path chooser */

/* The three routes off the homepage. Whole card is the link — a small "read
   more" target is the wrong shape on a phone, where these stack. */
.path-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-6);
}
.path-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-8);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
}
.path-card h3 {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  color: var(--white);
  line-height: 1.15;
}
.path-card p {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.6;
  flex: 1;
}
.path-icon {
  font-size: 2rem;
  line-height: 1;
}
.path-cta {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--green);
}
.path-card:hover .path-cta {
  color: var(--lime);
}

/* Route-out link under a homepage teaser section. */
.section-more {
  margin-top: var(--sp-8);
  text-align: center;
}

/* ------------------------------------------------------------ 6c. Game cards */

.game-card {
  display: flex;
  flex-direction: column;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  overflow: hidden;
  text-decoration: none;
  color: var(--text);
  background: var(--card);
}
/* `contain`, not `cover`. These are real game screenshots and their aspect
   ratios are whatever the game is — the puzzle board is portrait, the jumble
   is a wide strip. Cropping them to 16:9 cut the keyboard off the puzzle and
   both ends off the jumble. Letterboxing on the card background shows the
   whole screen and reads as a screenshot rather than a broken crop. */
/* 3:2, not 16:9. Measured against the real screenshots: at 16:9 the portrait
   puzzle board filled 52% of the card width and sat in heavy empty bars. 3:2
   is the ratio that treats all three least badly — 61% / 60% / 83%. */
.game-thumb {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
}
.game-thumb .media-slot,
.game-thumb .media-placeholder,
.game-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.game-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-6);
}
.game-body h3 {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  color: var(--white);
}
.game-body p {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.6;
  flex: 1;
}
.game-card:hover .path-cta {
  color: var(--lime);
}

/* ----------------------------------------------------------------- 7. Hero */

.hero {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: 5rem var(--sp-8) var(--sp-16);
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: var(--sp-16);
  align-items: center;
  position: relative;
  z-index: 1;
}
.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--green);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  margin-bottom: var(--sp-4);
  padding: 0.35rem 0.9rem;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
}
.hero-text h1 {
  font-family: var(--font-display);
  font-size: var(--fs-hero);
  font-weight: 900;
  color: var(--white);
  line-height: 1.08;
  margin-bottom: var(--sp-6);
}
.hero-text h1 .accent {
  background: linear-gradient(135deg, var(--green), var(--lime));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.hero-text p {
  font-size: 1.1rem;
  color: var(--muted);
  line-height: 1.7;
  max-width: 520px;
  margin-bottom: var(--sp-8);
}
.hero-ctas {
  display: flex;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.hero-visual {
  position: relative;
  display: flex;
  justify-content: center;
}
/* The floating stat stack. The stagger is per-COLUMN, not per-child index:
   the old rule offset children 2 and 3 by different amounts, which only
   composed into an even arrangement at exactly four cards. At three it left a
   hole and three different-looking gaps. Keying on :nth-child(even) means the
   right-hand column always sits lower by the same amount, so any number of
   cards reads as deliberate and every gap is identical. */
.stat-stack {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-5);
  transform: rotate(-2deg);
  align-items: start;
}
.stat-card {
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--border);
}
.stat-card:nth-child(even) {
  transform: translateY(1.25rem);
}
/* Sized against the number below it, not as an afterthought — at --fs-xl the
   icon read as a stray character next to a display-face figure. */
.stat-icon {
  font-size: 2rem;
  line-height: 1;
  margin-bottom: var(--sp-3);
}
.stat-number {
  font-family: var(--font-display);
  font-size: 2.2rem;
  font-weight: 900;
  color: var(--white);
  margin-bottom: var(--sp-1);
}
.stat-label {
  font-size: var(--fs-xs);
  color: var(--muted);
  font-weight: 500;
}

/* --------------------------------------------------------------- 8. Buttons */

.btn-primary,
.btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0.9rem var(--sp-8);
  font-family: var(--font-body);
  font-size: var(--fs-base);
  border-radius: var(--r-md);
  text-decoration: none;
  transition: all var(--t-mid);
  cursor: pointer;
}
.btn-primary {
  background: linear-gradient(135deg, var(--green), var(--green-dim));
  color: var(--bg);
  font-weight: 700;
  border: none;
  box-shadow: 0 4px 20px color-mix(in srgb, var(--green) 25%, transparent);
}
.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px color-mix(in srgb, var(--green) 35%, transparent);
}
.btn-ghost {
  background: transparent;
  color: var(--green);
  font-weight: 600;
  border: 1.5px solid var(--accent-line);
}
.btn-ghost:hover {
  background: var(--accent-wash);
  border-color: var(--green);
}

/* -------------------------------------------------------- 9. Section chrome */

.section {
  position: relative;
  z-index: 1;
}
.section-header {
  text-align: center;
  max-width: 700px;
  margin: 0 auto var(--sp-12);
  padding: var(--sp-16) var(--sp-8) 0;
}
.section-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--green);
  margin-bottom: var(--sp-3);
  padding: 0.3rem var(--sp-4);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
}
/* h1 AND h2: the homepage renders its section headings as h2 under the hero's
   h1, while every other template renders the PAGE heading here as the h1
   (2026-08-03 — 30 of 31 pages had no h1 at all). Both must look identical:
   this is the same visual slot, and the level says what the heading IS in the
   document, not how big it looks. */
.section-header h1,
.section-header h2 {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 900;
  color: var(--white);
  margin-bottom: var(--sp-3);
  transition: text-shadow var(--t-slow);
}
.section-header:hover h1,
.section-header:hover h2 {
  text-shadow: 0 0 20px var(--accent-glow-soft);
}
.section-header p {
  font-size: var(--fs-base);
  color: var(--muted);
  line-height: 1.6;
}
.section-body {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: 0 var(--sp-8) var(--sp-16);
}
.section-body.narrow {
  max-width: var(--w-mid);
}
.section-body.prose {
  max-width: var(--w-prose);
}
.highlight {
  color: var(--green);
  font-weight: 600;
}

/* ------------------------------------------------------------- 10. Grids */

.grid-auto-wide {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: var(--sp-5);
}
.grid-auto-narrow {
  display: grid;
  /* auto-FIT, not auto-fill. Both build the same 4 tracks at --w-wide (1200px
     / 260px min / --sp-5 gap), and for a group of 4 they are identical — which
     is every group on /shop. They differ only when a group has FEWER products
     than the row has tracks: auto-fill keeps the empty track, so "At the
     Appointment" (3 products) sat 895px wide in a 1200px row with a 305px dead
     column on the right. auto-fit collapses the empty track and the three share
     the full width.

     THE MAX STAYS 1fr. It was briefly changed to a fixed 400px to stop a
     one-product grid stretching its single card across the whole column — and
     that broke every grid on the site, because **a fixed max changes how many
     tracks auto-fit creates**. With `1fr` the browser packs as many MINIMUM-width
     tracks as fit and then expands them; with a fixed length it packs at the
     MAXIMUM, so an 1136px column fitted two 400px tracks instead of four 269px
     ones and /shop went to two cards a row at every width. Measured, not
     reasoned: 1440, 1280, 1152, 1024 and 900px all returned `400px 400px`.

     The lone-card problem is fixed on the CARD instead, below. */
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-5);
}
.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-5);
}
.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-5);
}
.grid-4 {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-4);
}

/* --------------------------------------------------------- 11. Notice box */
/* Neutral emphasis panel — legal updates, policy notes, callouts. */

.notice-box {
  padding: 2.5rem;
  border-radius: var(--r-xl);
  background: linear-gradient(
    135deg,
    var(--accent-wash),
    color-mix(in srgb, var(--lime) 4%, transparent)
  );
  border: 1px solid var(--accent-line);
  box-shadow: 0 0 30px var(--accent-glow-soft), 0 4px 40px var(--shadow-cast);
  position: relative;
  overflow: hidden;
}
.notice-box::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse at 30% 40%,
    var(--accent-wash),
    transparent 70%
  );
  pointer-events: none;
}
.notice-box > * {
  position: relative;
}
.notice-title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 900;
  color: var(--white);
  margin-bottom: var(--sp-2);
}
.notice-lede {
  font-size: var(--fs-base);
  color: var(--text);
  margin-bottom: var(--sp-6);
  line-height: 1.7;
}
.notice-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-8);
  text-align: left;
  margin-bottom: var(--sp-6);
}
.notice-cols h4 {
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--green);
  margin-bottom: var(--sp-3);
}
.notice-cols li {
  font-size: var(--fs-sm);
  color: var(--text);
  line-height: 1.7;
  margin-bottom: var(--sp-2);
  list-style: none;
  padding-left: var(--sp-4);
  position: relative;
}
.notice-cols li::before {
  content: "•";
  position: absolute;
  left: 0;
  color: var(--green);
}
.notice-actions {
  display: flex;
  justify-content: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.notice-foot {
  font-size: var(--fs-xs);
  color: var(--muted);
  margin-top: var(--sp-6);
}
.notice-foot strong {
  color: var(--text);
}

/* --------------------------------------------------------- 12. Local cards */

.local-card {
  padding: 1.75rem;
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--border);
  text-align: center;
  overflow: hidden;
}
.local-card .icon {
  font-size: var(--sp-8);
  margin-bottom: var(--sp-3);
}
.local-card h3,
.local-card h4 {
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--white);
  margin-bottom: var(--sp-2);
}
.local-card p {
  font-size: 0.82rem;
  color: var(--muted);
  line-height: 1.5;
}

/* ------------------------------------------------------ 13. Dispensary card */

.disp-card {
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--border);
  overflow: hidden;
}
.disp-card:hover {
  background: var(--card-hover);
}
.disp-card:hover .disp-name {
  text-shadow: 0 0 12px var(--accent-line-hover);
}
.disp-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.disp-name {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--white);
}
.disp-badge {
  font-size: var(--fs-3xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: var(--sp-1) 0.6rem;
  border-radius: var(--r-sm);
  background: var(--accent-glow);
  color: var(--green);
  white-space: nowrap;
}
.disp-neighborhood {
  font-size: 0.72rem;
  color: var(--green-dim);
  font-weight: 600;
  margin-bottom: var(--sp-2);
}
.disp-address {
  font-size: 0.82rem;
  color: var(--muted);
  margin-bottom: var(--sp-2);
}
.disp-address a {
  color: var(--muted);
  text-decoration: none;
}
.disp-address a:hover {
  color: var(--green);
}
.disp-meta {
  display: flex;
  gap: var(--sp-6);
  font-size: var(--fs-xs);
  color: var(--muted);
  flex-wrap: wrap;
}
.disp-meta span {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.star {
  color: var(--amber);
}
.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin-top: var(--sp-3);
}
.tag {
  font-size: 0.68rem;
  padding: 0.2rem 0.6rem;
  border-radius: var(--r-sm);
  background: var(--border);
  color: var(--text);
  font-weight: 500;
}

/* --------------------------------------------------------- 14. Product card */

.prod-card {
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--border);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.prod-card:hover {
  box-shadow: 0 0 25px var(--accent-glow), 0 8px 32px var(--shadow-cast-deep),
    inset 0 0 20px var(--accent-wash);
}
.prod-media {
  /* 360, not 200. With `contain` the rendered width is height x 0.55 for a
     352x640 asset, so a 200px box would have shown the product ~110px wide in
     a 270px card — legible, but smaller than the text beside it. At 360 the
     product renders ~198px wide and fills most of the card. */
  height: 360px;
  background: linear-gradient(135deg, var(--bg2), var(--card));
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}
.prod-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 60%, var(--card));
  z-index: 2;
  pointer-events: none;
}
.prod-badge {
  position: absolute;
  top: var(--sp-4);
  right: var(--sp-4);
  font-size: var(--fs-3xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: 0.3rem var(--sp-3);
  border-radius: var(--r-xl);
  z-index: 3;
}
.badge-best {
  background: var(--green);
  color: var(--bg);
}
/* Dark ink on every accent badge. --white on --purple measured 2.52:1; the
   accents are light colors, so the text on them has to be the dark one. */
.badge-popular {
  background: var(--purple);
  color: var(--bg);
}
.badge-value {
  background: var(--amber);
  color: var(--bg);
}
.prod-body {
  padding: var(--sp-5);
  flex: 1;
  display: flex;
  flex-direction: column;
}
.prod-category {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--green);
  font-weight: 600;
  margin-bottom: var(--sp-2);
}
.prod-name {
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--white);
  margin-bottom: var(--sp-2);
}
.prod-desc {
  font-size: 0.82rem;
  color: var(--muted);
  line-height: 1.5;
  margin-bottom: var(--sp-4);
  flex: 1;
}
.prod-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-3);
}
.prod-price {
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--white);
}
.prod-link {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--green);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  transition: color var(--t-fast);
}
.prod-link:hover {
  color: var(--lime);
}

/* FTC disclosure — required above every affiliate section (CLAUDE.md rule 7) */
.affiliate-disclosure {
  max-width: var(--w-wide);
  margin: 0 auto var(--sp-6);
  padding: var(--sp-3) var(--sp-5);
  border-radius: var(--r-md);
  background: var(--bg2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--amber);
  font-size: var(--fs-xs);
  color: var(--text);
  line-height: 1.6;
}
.affiliate-disclosure a {
  color: var(--amber);
}

/* ------------------------------------------------------------- 15. Law card */

.law-card {
  padding: var(--sp-8);
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--border);
  overflow: hidden;
}
.law-card .icon {
  font-size: var(--sp-8);
  margin-bottom: var(--sp-4);
}
.law-card h3 {
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--white);
  margin-bottom: var(--sp-3);
}
.law-card p {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.6;
}

/* ------------------------------------------------------------ 16. Step card */

.steps {
  counter-reset: step;
}
.step-card {
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--border);
  text-align: center;
  counter-increment: step;
  overflow: hidden;
}
.step-card::before {
  content: counter(step);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-md);
  background: linear-gradient(135deg, var(--green), var(--lime));
  color: var(--bg);
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 900;
  margin: 0 auto var(--sp-4);
  transition: all var(--t-mid);
}
.step-card:hover::before {
  box-shadow: 0 0 20px var(--accent-line-hover);
  transform: scale(1.1);
}
.step-card h3,
.step-card h4 {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--white);
  margin-bottom: var(--sp-2);
}
.step-card p {
  font-size: var(--fs-xs);
  color: var(--muted);
  line-height: 1.5;
}

/* ------------------------------------------------------------------ 17. FAQ */

/* Width only. The FAQ list used to borrow `.section-body prose` for this one
   declaration, which nested a layout container inside itself — see the comment
   in page.html.j2's faq block for what that cost. A class that means "constrain
   to the prose measure" and nothing else cannot do that again. */
.faq-list {
  max-width: var(--w-prose);
  margin: 0 auto;
}
/* Cap the CARD, not the track. This is what stops a grid holding one product
   rendering it 1136px wide, without touching how many tracks auto-fit builds.
   A track may be as wide as the column; the card inside it stops at 400px.
   Four items still render ~269px and three ~365px, because neither reaches the
   cap - it only binds when a row is nearly empty, which is exactly the case it
   exists for. */
.grid-auto-narrow > .prod-card { max-width: 400px; }

/* Flush by design: items are separated by the hairline border, not by margin.
   Anything that puts vertical space between .faq-item siblings breaks the
   accordion into floating strips. */
.faq-item {
  border-bottom: 1px solid var(--border);
  padding: var(--sp-6) 0;
}
.faq-q {
  font-size: 1rem;
  font-weight: 700;
  color: var(--white);
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  background: none;
  border: none;
  text-align: left;
  font-family: var(--font-body);
}
.faq-q::after {
  content: "+";
  font-size: 1.4rem;
  color: var(--green);
  transition: transform var(--t-mid);
  flex-shrink: 0;
}
.faq-q[aria-expanded="true"]::after {
  transform: rotate(45deg);
}
.faq-a {
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--t-slow) ease, padding var(--t-slow) ease;
  font-size: 0.88rem;
  color: var(--muted);
  line-height: 1.7;
}
.faq-a.open {
  max-height: 800px;
  padding-top: var(--sp-4);
}
/* Without JS every answer is simply open — content stays readable/indexable. */
.no-js .faq-a {
  max-height: none;
  padding-top: var(--sp-4);
}

/* ------------------------------------------------------------ 18. Daily tip */

.tip-card {
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background: linear-gradient(
    135deg,
    var(--accent-wash),
    color-mix(in srgb, var(--lime) 3%, transparent)
  );
  border: 1px solid var(--accent-line);
}
.tip-eyebrow {
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--green);
  font-weight: 700;
  margin-bottom: var(--sp-2);
}
.tip-text {
  font-size: var(--fs-base);
  color: var(--white);
  line-height: 1.7;
}
.tip-cat {
  display: inline-block;
  margin-top: var(--sp-3);
  font-size: 0.68rem;
  padding: var(--sp-1) var(--sp-3);
  background: var(--accent-wash);
  border-radius: var(--r-xl);
  color: var(--green);
}

/* --------------------------------------------------- 19. Media placeholders */
/* SPEC §6: a slot with no file still renders clean, at the right dimensions. */

.media-slot {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: var(--r-lg);
  background: var(--bg2);
}
.media-slot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.media-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  width: 100%;
  height: 100%;
  min-height: 120px;
  background: linear-gradient(135deg, var(--bg2), var(--card));
  border: 1px dashed var(--accent-line);
  border-radius: var(--r-lg);
  color: var(--muted);
  text-align: center;
  padding: var(--sp-4);
}
.media-placeholder .icon {
  font-size: var(--fs-xl);
  opacity: 0.6;
}
.media-placeholder .label {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: 0.06em;
  word-break: break-all;
}

/* --------------------------------------------------------------- 19b. Video */
/* SPEC §6 allows short muted self-hosted loops only. Everything here is a
   shared class — the pre-rebuild site did this with inline style attributes,
   which rule 1 forbids. Any slot can degrade to its poster with no layout
   shift, so a blocked or unsupported video costs nothing. */

.media-slot video {
  display: block;
  width: 100%;
  height: auto;
}
/* Inside a product card the media box is a fixed height, so the slot fills it
   and the poster/video crop rather than stretch. ::after keeps its gradient on
   top (z-index 2). */
.prod-media .media-slot {
  position: absolute;
  inset: 0;
  border-radius: 0;
  z-index: 1;
}
/* `contain`, not `cover`. The media box is landscape (~270x200 in a four-column
   grid) while every product asset is 352x640 portrait, so `cover` scaled the
   image to the box WIDTH and then cropped away roughly 60% of its height — on
   top of the crop ingest_media had already applied getting a landscape source
   down to portrait. Two crops in series is why the cards showed a corner of a
   bag rather than a bag. `contain` fits the whole asset inside the box; the
   card's own gradient shows either side, which reads as intentional because
   every product asset is the same shape. */
.prod-media .media-slot video,
.prod-media .media-slot img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Section background loops. .section already sets position:relative;z-index:1,
   so this sits behind the content without a stacking-context fight. */
.section-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
}
.section-bg video,
.section-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: var(--bg-video-opacity);
}
.section-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, var(--bg) 0%, transparent 25%, transparent 75%, var(--bg) 100%);
}
/* Lift the content above the background layer. Must exclude .section-bg itself:
   it is a sibling of the content, and this rule would otherwise override its
   position:absolute at equal specificity and collapse it to content size. */
.has-bg-video > *:not(.section-bg) {
  position: relative;
  z-index: 1;
}

/* Motion is decoration here, never information. Anyone who has asked their OS
   to calm down gets the poster frame instead — the JS also skips play(). */
@media (prefers-reduced-motion: reduce) {
  .section-bg video,
  .prod-media video {
    display: none;
  }
}

/* ------------------------------------------------------------ 19c. Legal doc */
/* Long-form compliance prose. Narrow measure, generous leading — these get
   read carefully or not at all, and AA contrast is not negotiable here. */

.legal-doc {
  max-width: 68ch;
  margin: 0 auto;
  color: var(--text);
  line-height: 1.75;
}
/* h2 is the block heading inside a page; h3 is kept because the dispensaries
   block and the product grid render item names at h3 inside the same shell. */
.legal-doc h2,
.legal-doc h3 {
  margin: var(--sp-8) 0 var(--sp-3);
  font-size: var(--fs-lg);
  color: var(--white);
}
.legal-doc h2:first-of-type,
.legal-doc h3:first-of-type {
  margin-top: var(--sp-4);
}
.legal-doc p {
  margin-bottom: var(--sp-4);
}
.legal-doc ul {
  margin: 0 0 var(--sp-4) var(--sp-6);
  padding: 0;
}
.legal-doc li {
  margin-bottom: var(--sp-2);
}
.legal-doc a {
  color: var(--green);
  text-decoration: underline;
}
.legal-updated {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: 0.06em;
  color: var(--muted);
  text-transform: uppercase;
}

/* ---------------------------------------------------------------- 19d. Games */
/* Interactives (CONTENT-PLAN). All game markup is built by JS at runtime, so
   these classes are the whole contract between the games and the design
   system — the games never invent a colour or a font. */

.game-mount {
  max-width: 30rem;
  margin: 0 auto var(--sp-8);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
}
.sotd-board {
  display: grid;
  gap: 6px;
}
.sotd-row {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 6px;
}
.sotd-cell {
  width: 3.1rem;
  height: 3.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--border);
  border-radius: var(--r-sm);
  font-family: var(--font-body);
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--white);
  text-transform: uppercase;
  background: var(--bg2);
}
.sotd-cell.filled {
  border-color: var(--accent-line-hover);
}
/* AA contrast: dark ink on the light fills, not white. */
.sotd-cell.correct {
  background: var(--green);
  border-color: var(--green);
  color: var(--bg);
}
.sotd-cell.present {
  background: var(--amber);
  border-color: var(--amber);
  color: var(--bg);
}
.sotd-cell.absent {
  background: var(--card);
  border-color: var(--card);
  color: var(--muted);
}
.sotd-keys {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
}
.sotd-key-row {
  display: flex;
  gap: 5px;
  justify-content: center;
}
.sotd-key {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--sp-3) 0;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--card);
  color: var(--white);
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.sotd-key.wide { flex: 1.4 1 auto; }
.sotd-key:hover { border-color: var(--green); }
.sotd-key.correct { background: var(--green); color: var(--bg); border-color: var(--green); }
.sotd-key.present { background: var(--amber); color: var(--bg); border-color: var(--amber); }
.sotd-key.absent { background: var(--bg2); color: var(--muted); }
.sotd-message {
  min-height: 1.4rem;
  margin: 0;
  text-align: center;
  color: var(--white);
  font-size: var(--fs-sm);
  white-space: pre-line;
}
.sotd-stats {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: 0.06em;
  color: var(--muted);
  text-transform: uppercase;
}
.sotd-share { margin-top: var(--sp-2); }

.jumble-word {
  font-family: var(--font-mono);
  font-size: 2rem;
  letter-spacing: 0.35em;
  color: var(--lime);
  text-align: center;
  padding: var(--sp-6) 0;
}
.jumble-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  justify-content: center;
}
.jumble-input {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg2);
  color: var(--white);
  font-family: var(--font-mono);
  font-size: 1rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  width: 9rem;
}
.jumble-input:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }

.run-canvas {
  width: 100%;
  max-width: 22.5rem;
  height: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--bg);
  touch-action: manipulation;
}
.run-controls {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}

/* Quiz */
.quiz-q {
  font-size: var(--fs-lg);
  color: var(--white);
  margin-bottom: var(--sp-5);
  text-align: center;
}
.quiz-options {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  width: 100%;
}
.quiz-option {
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--card);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.quiz-option:hover {
  border-color: var(--green);
  background: var(--card-hover);
}
.quiz-progress {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: 0.06em;
  color: var(--muted);
  text-transform: uppercase;
}
.quiz-result {
  text-align: center;
  color: var(--text);
}
.quiz-result h4 {
  font-size: var(--fs-xl);
  color: var(--white);
  margin-bottom: var(--sp-3);
}

@media (max-width: 480px) {
  .sotd-cell { width: 2.6rem; height: 2.6rem; font-size: 1.25rem; }
  .jumble-word { font-size: 1.5rem; letter-spacing: 0.25em; }
}

/* ------------------------------------------------------------- 19e. Signup */
/* Email capture. The provider renders its own form inside [data-signup-mount],
   so we style only our chrome around it. The no-JS fallback link is hidden
   unless <html> still carries .no-js — i.e. our script never ran. */

/* The provider injects its own markup into [data-signup-mount]. We cannot
   change what it emits, so style it by element rather than by their class
   names, which are not ours to depend on. Harmless if it ever becomes an
   iframe — the rules simply stop matching. */
[data-signup-mount] input[type="email"],
[data-signup-mount] input[type="text"] {
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg2);
  color: var(--white);
  font-family: var(--font-body);
  font-size: var(--fs-base);
}
[data-signup-mount] input::placeholder { color: var(--muted); }
[data-signup-mount] button,
[data-signup-mount] input[type="submit"] {
  padding: var(--sp-3) var(--sp-6);
  border: 0;
  border-radius: var(--r-pill);
  background: var(--green);
  color: var(--bg);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
}
[data-signup-mount] button:hover,
[data-signup-mount] input[type="submit"]:hover { background: var(--green-dim); }
[data-signup-mount] form { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
[data-signup-mount] a { color: var(--muted); }

.signup-nojs { display: none; }
.no-js .signup-nojs { display: flex; }
.no-js .signup-cta { display: none; }

.signup-small {
  margin: 0;
  font-size: var(--fs-2xs);
  color: var(--muted);
}
.signup-small a {
  color: var(--green);
  text-decoration: underline;
}

/* ---------------------------------------------------------- 20. Sister site */

.sister-strip {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-8);
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.sister-strip a {
  color: var(--green);
  font-weight: 600;
  text-decoration: none;
}
.sister-strip a:hover {
  color: var(--lime);
}

/* --------------------------------------------------------------- 21. Footer */

/* Flex, not a fixed grid. The old `grid-template-columns: 1.5fr 1fr 1fr 1fr`
   assumed exactly four children; this site renders five (brand, Navigate,
   Dispensaries, Resources, Legal) and the fifth wrapped onto a second row
   under the brand. Column count is a per-site data question — the umbrella
   renders one — so the layout has to derive it rather than hard-code it. */
.site-footer {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: var(--sp-12) var(--sp-8);
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-8) var(--sp-12);
}
.footer-brand {
  flex: 1 1 17rem;
  min-width: 0;
}
.footer-col {
  flex: 1 1 8rem;
  min-width: 0;
}
.footer-brand p {
  font-size: 0.82rem;
  color: var(--muted);
  line-height: 1.6;
  margin-top: var(--sp-3);
}
.footer-col h4 {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--muted);
  font-weight: 700;
  margin-bottom: var(--sp-4);
}
.footer-col a {
  display: block;
  font-size: var(--fs-sm);
  color: var(--text);
  text-decoration: none;
  margin-bottom: var(--sp-2);
  transition: color var(--t-fast);
}
.footer-col a:hover {
  color: var(--green);
}
.footer-bottom {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-8);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.footer-bottom p {
  font-size: 0.72rem;
  color: var(--muted);
}
.footer-disclaimer {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: 0 var(--sp-8) var(--sp-8);
  font-size: 0.68rem;
  color: var(--muted);
  line-height: 1.6;
}

/* ----------------------------------------------------------- 22. Share bar */

.share-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: var(--sp-6);
}
.share-label {
  font-size: 0.72rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 600;
}
.share-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-md);
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  text-decoration: none;
  transition: all var(--t-fast);
}
.share-btn:hover {
  border-color: var(--green);
  color: var(--green);
  background: var(--accent-wash);
  transform: translateY(-2px);
}
.share-btn svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

/* ------------------------------------------------------------ 23. Responsive */

@media (max-width: 900px) {
  .hero {
    grid-template-columns: 1fr;
    padding: var(--sp-12) var(--sp-6) var(--sp-8);
    gap: var(--sp-8);
  }
  /* Tablet keeps the stats — they answer "what does it cost" and "who
     delivers", which is worth more than the diagonal flourish. Rotation and
     stagger come off so they read as a plain 2x2 under the hero copy. */
  .hero-visual {
    width: 100%;
  }
  .stat-stack {
    transform: none;
  }
  .stat-card:nth-child(even) {
    transform: none;
  }

  /* Tablet and phone: the link row becomes a drop panel behind the button.
     Gated on the script having run — `.nav-links` used to be a flat
     `display: none` here, which left phones and tablets with no navigation at
     all except the footer. A no-JS browser gets the wrapped row below. */
  html:not(.no-js) .nav-toggle {
    display: inline-flex;
  }
  html:not(.no-js) .nav-links {
    display: none;
    position: absolute;
    top: 100%;
    right: var(--sp-6);
    left: var(--sp-6);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--sp-2);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg2);
    box-shadow: 0 18px 40px var(--shadow-cast-deep);
  }
  html:not(.no-js) .nav-links.open {
    display: flex;
  }
  html:not(.no-js) .nav-links a {
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-md);
    color: var(--text);
    font-size: var(--fs-base);
  }
  html:not(.no-js) .nav-links a:hover {
    background: var(--accent-wash);
    color: var(--green);
  }

  /* No script, no panel: keep the links on the page as a wrapped row. */
  .no-js .nav-inner {
    flex-wrap: wrap;
    gap: var(--sp-3);
  }
  .no-js .nav-links {
    flex-wrap: wrap;
    gap: var(--sp-4);
  }
  .grid-2,
  .grid-3,
  .notice-cols {
    grid-template-columns: 1fr;
  }
  /* Tablet: two routes across, third below. One column would push the third
     option — the shop — off the first screen entirely. */
  .path-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .grid-4 {
    grid-template-columns: 1fr 1fr;
  }
  /* Tablet: brand across the top, then every link column on ONE row beneath.
     The basis has to leave room for four columns plus three gaps inside
     768px minus padding — 10rem fit only three and pushed Legal to a third
     row, which is the wrap this change exists to remove. */
  .site-footer {
    gap: var(--sp-8);
  }
  .footer-brand {
    flex-basis: 100%;
  }
  .footer-col {
    flex: 1 1 7rem;
  }
  .section-header {
    padding: var(--sp-12) var(--sp-6) 0;
  }
  .section-body {
    padding: 0 var(--sp-6) var(--sp-12);
  }
  .notice-box {
    padding: var(--sp-6);
  }
}

@media (max-width: 600px) {
  .grid-4 {
    grid-template-columns: 1fr;
  }
  .grid-auto-wide,
  .grid-auto-narrow,
  .path-grid {
    grid-template-columns: 1fr;
  }
  .path-card {
    padding: var(--sp-6);
  }
  /* Phone: the hero was 742px tall, which pushed the three-route chooser to
     y=1088 — a whole screen of scrolling before a reader sees where to go,
     which defeats the point of a hub. Tighter padding, and the share bar goes:
     offering to share a page nobody has read yet is not worth 40px above the
     fold. It returns at tablet width and up. */
  .hero {
    padding: var(--sp-8) var(--sp-6) var(--sp-8);
    gap: var(--sp-6);
  }
  .hero .share-bar {
    display: none;
  }
  /* Phone: the stats go too. Four cards is another full screen before the
     reader reaches the three routes, and the same facts appear further down
     the page anyway. Tablet and desktop keep them. */
  .hero-visual {
    display: none;
  }
  .hero-ctas,
  .notice-actions {
    flex-direction: column;
    align-items: stretch;
  }
  .hero-ctas a,
  .notice-actions a {
    justify-content: center;
  }
  /* Phone: two columns of links, brand full width above them. One column each
     would push the legal links a screen and a half down. */
  .site-footer {
    gap: var(--sp-6);
  }
  .footer-col {
    flex: 1 1 42%;
  }
  .footer-bottom {
    flex-direction: column;
    gap: var(--sp-2);
    text-align: center;
  }
  .nav-inner {
    padding: var(--sp-4) var(--sp-6);
  }
}

/* ------------------------------------------------------------- 24. Print */

@media print {
  .atmosphere,
  .grain,
  .age-gate,
  .site-nav,
  .share-bar,
  /* A printed page cannot be clicked, so anything that only exists to be
     clicked is ink wasted: calls to action, the signup embed, the "see all"
     links, and the footer's whole navigation column. The disclaimer stays. */
  .notice-box,
  .signup-box,
  .section-more,
  .footer-col,
  .footer-brand,
  .sister-strip,
  .affiliate-disclosure {
    display: none !important;
  }
  body {
    background: #fff;
    color: #000;
  }
  /* Ink, not screen. Dark-luxe costs a cartridge and reads worse on paper. */
  main,
  .legal-doc,
  .grid-auto-narrow {
    background: none !important;
    color: #000 !important;
  }
  a {
    color: #000 !important;
    text-decoration: underline;
  }
  h1, h2, h3 {
    color: #000 !important;
    page-break-after: avoid;
  }
  .checklist li,
  .callout {
    page-break-inside: avoid;
  }
  .checklist li::before {
    border-color: #000 !important;
  }
}

/* -------------------------------------------------- 27. Printable checklist */
/* A list to work through, on screen or on paper. The box is drawn rather than
   set as a character: U+2610 renders inconsistently across the platforms this
   factory targets, and an <input type="checkbox"> would announce a form
   control that submits nothing. */

.checklist {
  list-style: none;
  margin: var(--sp-6) auto;
  padding: 0;
}
.checklist li {
  position: relative;
  padding: var(--sp-3) 0 var(--sp-3) var(--sp-8);
  border-bottom: 1px solid var(--accent-line);
  line-height: 1.6;
}
.checklist li:last-child {
  border-bottom: none;
}
.checklist li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.15em;
  width: 1.05rem;
  height: 1.05rem;
  margin-top: -0.55rem;
  border: 2px solid var(--green);
  border-radius: var(--r-sm);
}
.checklist-intro {
  color: var(--muted);
}

/* -------------------------------------------------- 26. Physician directory */
/* Lee County certifying physicians (SPEC §5 pipeline). Deliberately plain:
   this is a reference list of licensed professionals, not a product grid, and
   styling it like one would imply a ranking the data does not support. */

.phys-filter {
  margin: var(--sp-4) 0 var(--sp-6);
}
.phys-label {
  display: block;
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: var(--sp-2);
}
.phys-filter input {
  width: 100%;
  max-width: 26rem;
  padding: var(--sp-3) var(--sp-4);
  font: inherit;
  color: var(--white);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.phys-filter input:focus-visible {
  border-color: var(--accent-line-hover);
}
.phys-count {
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.phys-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-3);
}
.phys-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-4);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
/* Explicit, and load-bearing: the filter toggles the `hidden` ATTRIBUTE rather
   than an inline style (rule 1), and [hidden] does not beat the flex display
   above without this. Same cascade collision as the game controls, 2026-08-03. */
.phys-item[hidden] {
  display: none;
}
.phys-name {
  font-weight: 700;
  color: var(--white);
}
.phys-meta {
  font-size: var(--fs-sm);
  color: var(--muted);
}
.phys-phone {
  font-size: var(--fs-sm);
  width: fit-content;
}

/* --------------------------------------- 28. Full-width promotional banner */
/* A wide product placement used to break a long scroll (the `banner` block).
   It is deliberately framed rather than floated: a bare image in a column of
   text reads as decoration and gets skipped, which is the opposite of what a
   placement is for. The rule above it, the card behind it and the copy below
   it are all doing the same job — saying 'this is a different kind of thing'.

   The image's aspect ratio is declared rather than its height, so the browser
   reserves the box before the file lands. That is what stops the layout shift
   the /shop grid was measured for on 2026-08-02. */

.page-banner {
  /* 68ch, the same column .legal-doc uses, so the banner and its sales copy
     line up with the article text either side of them. --w-narrow (900px) was
     tried first and rendered wider than the prose AND left-aligned against it,
     which read as a mistake rather than as emphasis. The divider rule does the
     breaking-up; the width does not have to. */
  max-width: 68ch;
  margin: var(--sp-12) auto;
  padding-top: var(--sp-8);
  /* the divider: a hairline across the column, not a box edge */
  border-top: 1px solid var(--accent-line);
}
.page-banner-img {
  display: block;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--card);
  transition: border-color var(--t-mid), transform var(--t-mid);
}
a.page-banner-img:hover {
  border-color: var(--green);
  transform: translateY(-2px);
}
.page-banner img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 1;
  object-fit: cover;
}
/* `ratio: scene` — a taller frame for a banner containing a PERSON. 4:1 keeps
   only the middle 44% of a 16:9 source, which is fine for a box photographed
   lying flat and fatal for a scene: the Audible banner's phone, jar and glass
   all sat below the cut, so the one element that said "audio" was never
   visible. 2:1 keeps 89% and the whole story survives. */
.page-banner--scene img { aspect-ratio: 2 / 1; }
.page-banner-copy {
  margin-top: var(--sp-4);
}
.page-banner-copy h3 {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-lg);
}
.page-banner-copy p {
  margin: 0 0 var(--sp-3);
  color: var(--muted);
}
.page-banner-cta {
  font-weight: 600;
  color: var(--green);
  text-decoration: none;
}
.page-banner-cta:hover { text-decoration: underline; }
.page-banner-caption {
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* Phone: 4:1 is a letterbox slit at 375px, so let it grow taller and crop from
   the centre. Every banner prompt is written around this second crop — it is
   twice as destructive as the desktop one. */
@media (max-width: 600px) {
  .page-banner img { aspect-ratio: 2 / 1; }
  /* a scene banner goes to its native 16:9 on a phone rather than cropping
     further — at 375px a 2:1 strip is already only ~185px tall. */
  .page-banner--scene img { aspect-ratio: 16 / 9; }
  .page-banner { margin: var(--sp-8) auto; }
}

/* --------------------------------------------- 29. Framed product feature */
/* A `products` block that sets `heading` or `intro` renders inside this panel
   instead of as a bare grid. Added 2026-08-05 for the appointment kit on
   /florida-medical-card-checklist/, where three cards sat between two prose
   blocks with nothing around them and nothing saying why they were there.

   A BORDERED PANEL, not the hairline `border-top` that .page-banner uses. The
   banner divider works because a banner is one wide image inside a 68ch prose
   column — the rule reads as a break in the text. The product grid is wider
   than the prose and is already made of bordered cards, so a hairline above it
   separates nothing; the cards still float. A box is what actually groups
   them, and it is what "a border around the products" asked for. */
.product-feature {
  margin: var(--sp-12) 0;
  padding: var(--sp-8) var(--sp-6) var(--sp-6);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  /* --bg2, not --card: the cards inside are --card, and matching the panel to
     them flattened the whole thing into one shape with faint internal lines. */
  background: var(--bg2);
}
.product-feature > h2 {
  margin: 0 0 var(--sp-3);
}
/* The copy is constrained to the prose measure while the grid below stays full
   width. Letting the intro run the panel's whole width gave a ~110-character
   line, which is roughly double a comfortable measure. */
.product-feature-copy {
  max-width: 68ch;
  margin-bottom: var(--sp-5);
}
.product-feature-copy p {
  margin: 0 0 var(--sp-3);
  color: var(--muted);
}
.product-feature-copy p:last-child { margin-bottom: 0; }
/* The disclosure keeps its own styling but loses its outer margin here, so it
   reads as part of the panel rather than as a second floating box inside it. */
.product-feature .affiliate-disclosure {
  margin: 0 0 var(--sp-5);
}
@media (max-width: 600px) {
  .product-feature {
    margin: var(--sp-8) 0;
    padding: var(--sp-6) var(--sp-4) var(--sp-4);
  }
}

/* ------------------------------------------ 30. Vertical rhythm between blocks */
/* Every entry in a page's `blocks:` list renders as a direct child of
   .section-body, and most of the components have padding but no MARGIN — so two
   in a row sat flush against each other with a single hairline between them.
   Operator reported it 2026-08-06 on /florida-medical-card-checklist/, where a
   `cta` and a `callout` are both .notice-box; .notice-box alone appears 24 times
   across dist, and .legal-doc, .grid-3, .grid-auto-wide, .path-grid and
   .tip-card have the same gap.

   THE OWL, and why it is at the very END of this file. Specificity here is 0,1,0
   — the same as a plain component class — so file order decides. It has to come
   after .legal-doc (line ~1526), whose `margin: 0 auto` shorthand would
   otherwise reset margin-top to 0 and leave prose blocks still touching. Placing
   it early was tried on paper and leaves exactly that hole.

   The two exclusions are components that set their own, LARGER vertical rhythm
   (--sp-12). Without the :not() this rule would win on file order and quietly
   REDUCE their spacing, which is a regression rather than a fix. Any future
   component that wants its own rhythm belongs in this list. */
.section-body > * + *:not(.product-feature):not(.page-banner) {
  margin-top: var(--sp-8);
}

/* --------------------------------------------- 31. Swipeable product rail */
/* The homepage product teaser, from 2026-08-06. A horizontal scroll-snap row
   instead of a grid, carrying the WHOLE catalogue rather than four picks.

   CSS-ONLY BY DESIGN. Native overflow scrolling already gives a real swipe on
   phone and tablet, two-finger scroll on a trackpad, and keyboard arrows when
   focused — no library (rule 3) and no JS to load or break. `scroll-snap-type`
   makes it land on a card rather than halfway across one.

   The cards keep their own fixed width here: in the grid they stretch to fill
   a track, and a rail has no tracks to fill. */
.prod-rail {
  display: flex;
  gap: var(--sp-5);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* the padding is what stops a card's lift/shadow being clipped by the
     scroll container, and the negative margin keeps the row flush with the
     column edges despite it. */
  padding: var(--sp-2) var(--sp-2) var(--sp-5);
  margin: 0 calc(var(--sp-2) * -1);
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.prod-rail > .prod-card {
  flex: 0 0 280px;
  scroll-snap-align: start;
}
/* Motion-sensitive readers get no snap — a rail that grabs the scroll position
   is exactly the kind of movement `prefers-reduced-motion` is asking us to
   drop. It stays scrollable, it just stops pulling. */
@media (prefers-reduced-motion: reduce) {
  .prod-rail { scroll-snap-type: none; }
}
@media (max-width: 600px) {
  /* just under full width, so the next card peeks and the row reads as
     scrollable without needing an arrow or a hint to say so. */
  .prod-rail > .prod-card { flex-basis: 84%; }
}
/* A card linked from the homepage lands mid-page on /shop; without this the
   sticky header sits on top of the thing the reader just asked to see. */
.prod-card { scroll-margin-top: var(--sp-16); }
