/* ============================================================================
   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. --------------------------- */
  /* Notes and semantic states. Site-overridable. Blue reads as reliable and
     is the accent; green reads as safe/permitted; amber is used sparingly
     because bright warm hues read as alarming rather than authoritative. */
  --note-bg: rgba(74, 222, 128, 0.05);
  --note-rule: rgba(74, 222, 128, 0.35);
  --safe: #4ade80;
  --caution: #fbbf24;

  --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;

  /* -- Datasheet texture (2026-09-13). No-ops by default, so only a site
        that sets them in config.yaml changes. ----------------------------- */
  --grid-line: transparent;
  --heading-tracking: normal;

  /* -- 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);
}

/* Header search (site #4, 2026-09-13). Only sites with `search: true` render
   the form, so these rules do nothing anywhere else. */
.nav-search {
  display: flex;
  align-items: center;
  margin-left: auto;
  margin-right: var(--sp-6);
  padding: 2px 4px 2px var(--sp-4);
  border: 1px solid var(--accent-line);
  border-radius: 999px;
  background: var(--card);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.nav-search:focus-within {
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.nav-search-input {
  width: 12rem;
  padding: 0.35rem 0;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
  outline: none;
}
.nav-search-input::placeholder {
  color: var(--muted);
}
.nav-search-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--green);
  cursor: pointer;
}
.nav-search-btn svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
}
.nav-search ~ .nav-links {
  gap: var(--sp-6);
}
/* Suggestions under the header box, drawn by core/tools/search.js. */
.nav-search {
  position: relative;
}
.nav-search-menu {
  position: absolute;
  top: calc(100% + var(--sp-2));
  right: 0;
  z-index: 110;
  width: min(30rem, calc(100vw - 2rem));
  max-height: min(70vh, 34rem);
  overflow-y: auto;
  margin: 0;
  padding: var(--sp-2);
  list-style: none;
  background: var(--card);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  box-shadow: 0 16px 40px var(--accent-glow), 0 2px 8px var(--accent-glow-soft);
}
.nav-search-opt-link {
  display: block;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-md);
  color: var(--text);
  text-decoration: none;
}
.nav-search-opt-link:hover,
.nav-search-opt.is-active .nav-search-opt-link {
  background: var(--card-hover);
}
.nav-search-opt-top {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
}
.nav-search-opt-kind {
  flex: none;
  min-width: 4rem;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--green);
}
.nav-search-opt-title {
  color: var(--white);
  font-weight: 600;
  font-size: var(--fs-sm);
}
.nav-search-opt-snippet {
  display: block;
  margin-left: calc(4rem + var(--sp-3));
  overflow: hidden;
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.nav-search-opt-title mark,
.search-title mark {
  background: var(--accent-glow);
  color: inherit;
  border-radius: 2px;
}
.nav-search-all {
  margin-top: var(--sp-1);
  padding-top: var(--sp-1);
  border-top: 1px solid var(--border);
}
.nav-search-all .nav-search-opt-link {
  color: var(--green);
  font-size: var(--fs-sm);
  font-weight: 600;
}
.nav-search-empty {
  padding: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--muted);
}
@media (max-width: 1180px) {
  .nav-search-input { width: 8.5rem; }
}
@media (max-width: 900px) {
  .nav-search {
    margin-right: var(--sp-2);
    padding: 0;
    border: 0;
    background: transparent;
  }
  .nav-search:focus-within { box-shadow: none; }
  .nav-search-input { display: none; }
  .nav-search-btn {
    width: 44px;
    height: 44px;
    border: 1px solid var(--accent-line);
    border-radius: var(--r-md);
  }
}

/* The /search/ page: box, live count, results. Built by core/tools/search.js. */
.search-input {
  width: 100%;
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-md);
}
.search-input:focus {
  outline: none;
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.search-count {
  margin: var(--sp-4) 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.search-results {
  display: grid;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.search-hit {
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--card);
  transition: border-color var(--t-fast);
}
.search-hit:hover {
  border-color: var(--accent-line-hover);
}
.search-link {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  color: var(--white);
  font-weight: 600;
  text-decoration: none;
}
.search-link:hover .search-title {
  color: var(--green);
}
.search-kind {
  flex: none;
  min-width: 4.5rem;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--green);
}
.search-snippet {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* 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);
  letter-spacing: var(--heading-tracking);
  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);
  letter-spacing: var(--heading-tracking);
  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;
}

/* ------------------------------------------------ 12b. Datasheet components */
/* Added 2026-09-13 for site #4 (3dgenetic). Opt-in by markup or token: no
   template emitted these classes before, and --grid-line defaults to
   transparent, so every other site renders exactly as it did. */

/* Graph-paper ground behind the hero. On a pseudo-element so the mask can fade
   the grid out toward the edges without fading the hero's own content. The
   mask only reads alpha, so any opaque token serves as its color. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 28px 28px;
  -webkit-mask-image: radial-gradient(ellipse at 65% 45%, var(--white) 35%, transparent 78%);
  mask-image: radial-gradient(ellipse at 65% 45%, var(--white) 35%, transparent 78%);
}

/* A technical drawing in the hero's visual column, in place of the stat stack. */
.hero-figure {
  margin: 0;
  width: 100%;
  max-width: 520px;
}
.hero-figure .figure-svg {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
  box-shadow: 0 1px 2px var(--accent-glow-soft), 0 28px 56px -28px var(--accent-glow);
}

/* Part-number chip: a model number or part code, set in the mono face. */
.part-chip {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 500;
  line-height: 1;
  color: var(--green-dim);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  border-radius: 6px;
  padding: 0.32rem 0.5rem;
  white-space: nowrap;
}
.chip-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
}

/* Fit checks: one sourced relationship per row, verdict on the right. */
.fit-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-3);
}
/* A search result links to #part-<component>; the sticky nav would cover a
   row scrolled flush to the top, and the ring shows which row was meant. */
.fit-row[id] {
  scroll-margin-top: calc(var(--sp-16) + var(--sp-8));
}
.fit-row:target {
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.fit-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.fit-row:hover {
  border-color: var(--accent-line-hover);
  box-shadow: 0 12px 28px -20px var(--accent-glow);
}
.fit-pair {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  color: var(--white);
  font-weight: 600;
}
.fit-brand {
  color: var(--muted);
  font-weight: 500;
}
.fit-plus {
  color: var(--muted);
}
.fit-component {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text);
  font-weight: 500;
}
.fit-verdict {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  justify-self: end;
}
.fit-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 0.3rem 0.7rem;
  border: 1px solid currentColor;
  border-radius: var(--r-pill);
  white-space: nowrap;
}
.fit-badge::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
}
.fit-badge--fits { color: var(--safe); }
.fit-badge--needs_adapter { color: var(--caution); }
.fit-badge--does_not_fit { color: var(--rose); }
.fit-note {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--muted);
}
.fit-source {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.fit-source a {
  color: var(--green);
}
/* Buy button on a fit row (site #4, 2026-09-13). Amazon is a filled pill; a
   maker's own store is the outlined version, so the two read as different
   kinds of link. The part label sits beside it in muted type. */
.fit-buy {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  margin: 0;
}
.fit-buy-btn {
  display: inline-flex;
  align-items: center;
  padding: 0.35rem 0.9rem;
  border: 1px solid var(--green);
  border-radius: 999px;
  background: var(--green);
  color: var(--bg);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.fit-buy-btn:hover {
  background: var(--green-dim);
  border-color: var(--green-dim);
}
.fit-buy-btn--store {
  background: transparent;
  color: var(--green);
  border-color: var(--accent-line-hover);
}
.fit-buy-btn--store:hover {
  background: var(--accent-wash);
  border-color: var(--green);
}
.fit-buy-item {
  font-size: var(--fs-xs);
  color: var(--muted);
}
/* A wrapper, not a second class on the <p>: lint 16 keys on the exact
   class="affiliate-disclosure" attribute. */
.fit-disclosure .affiliate-disclosure {
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--muted);
}
@media (max-width: 600px) {
  .fit-row { grid-template-columns: 1fr; }
  .fit-verdict { justify-self: start; }
  /* The drawing is worth its height on a phone; the stat stack is not. */
  .hero-visual.hero-visual--figure { display: flex; }
}

/* Model pages (generated from compatibility.json). */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-6);
  font-size: 0.85rem;
  color: var(--muted);
}
.crumbs a {
  color: var(--green);
  text-decoration: none;
}
.crumbs a:hover {
  text-decoration: underline;
}
.spec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--sp-3);
  margin: 0 0 var(--sp-8);
}
.spec-cell {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
}
.spec-cell dt {
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin-bottom: var(--sp-2);
}
.spec-cell dd {
  margin: 0;
}
.spec-cell .part-chip {
  font-size: 0.85rem;
}
.model-section {
  margin: var(--sp-8) 0;
}
.model-section > h2 {
  font-family: var(--font-display);
  letter-spacing: var(--heading-tracking);
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--white);
  margin-bottom: var(--sp-4);
}
.part-chip--variant {
  color: var(--white);
  background: var(--bg2);
  border-color: var(--border);
}
.fit-badge--not_covered { color: var(--caution); }
.fit-qualifier {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.9rem;
  color: var(--text);
}
.fit-row--note {
  grid-template-columns: 1fr;
}
.fit-watch {
  margin: 0;
  color: var(--text);
}
.fit-callout {
  margin: var(--sp-8) 0;
  padding: var(--sp-4) var(--sp-5);
  background: var(--note-bg);
  border-left: 3px solid var(--note-rule);
  border-radius: 8px;
  color: var(--text);
}
.fit-callout p {
  margin: 0;
}
.fit-callout p + p {
  margin-top: var(--sp-2);
}
.model-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  list-style: none;
  margin: 0;
  padding: 0;
}
.model-links a {
  display: inline-block;
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--white);
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.model-links a:hover {
  border-color: var(--accent-line-hover);
  color: var(--green);
}

/* Comparison table: the model column stays put while the rest scrolls. */
.compat-scroll {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--card);
}
.compat-table {
  width: 100%;
  min-width: 620px;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.95rem;
}
.compat-table th,
.compat-table td {
  padding: 0.8rem 1rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.compat-table thead th {
  background: var(--bg2);
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.compat-table thead th:first-child,
.compat-table tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  box-shadow: 1px 0 0 var(--border);
}
.compat-table tbody th {
  background: var(--card);
}
.compat-table tbody tr:last-child th,
.compat-table tbody tr:last-child td {
  border-bottom: 0;
}
.compat-table a {
  color: var(--white);
  font-weight: 600;
  text-decoration: none;
}
.compat-table a:hover {
  color: var(--green);
}
.count {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--white);
}
.count--zero {
  color: var(--muted);
}

/* Model page: the exact model's listing, one button after the specs. */
.model-buy {
  margin: 0 0 var(--sp-8);
}
.model-buy .affiliate-disclosure {
  margin: 0 0 var(--sp-3);
  font-size: 0.85rem;
  color: var(--muted);
}
.model-buy-btn {
  width: 100%;
  justify-content: center;
  text-align: center;
}
.model-buy-btn + .model-buy-btn {
  margin-top: var(--sp-2);
}

/* Category guides: one card of rows per model. */
.fit-group h2 a {
  color: var(--white);
  text-decoration: none;
}
.fit-group h2 a:hover {
  color: var(--green);
}

/* Fit checker outcomes reuse the fit badge and source line. */
.triage-outcome .fit-badge {
  margin: 0 0 var(--sp-3);
}
.triage-outcome .fit-source {
  margin: var(--sp-2) 0 var(--sp-3);
}

/* RCF calculator (core/tools/rcf.js). */
.calc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-4);
  margin-top: var(--sp-4);
}
.calc-field--wide {
  grid-column: 1 / -1;
}
.calc-field label,
.calc-label {
  display: block;
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--white);
}
.calc-input {
  display: flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--card);
  overflow: hidden;
}
.calc-input:focus-within {
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--accent-wash);
}
.calc-input input {
  flex: 1;
  min-width: 0;
  border: 0;
  padding: 0.75rem 0.9rem;
  font-family: var(--font-mono);
  font-size: 1rem;
  color: var(--white);
  background: transparent;
}
.calc-input input:focus {
  outline: none;
}
.calc-unit {
  padding: 0 0.9rem;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--muted);
}
.calc-field select {
  width: 100%;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--card);
  color: var(--white);
  font-family: var(--font-body);
  font-size: 1rem;
}
.calc-help,
.calc-limit-line {
  margin: var(--sp-2) 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.calc-modes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.calc-mode {
  padding: 0.55rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
}
.calc-mode[aria-pressed="true"] {
  border-color: var(--green);
  background: var(--accent-wash);
  color: var(--green-dim);
}
.calc-result {
  margin-top: var(--sp-6);
  padding: var(--sp-5);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  background: var(--accent-wash);
}
.calc-answer {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 2rem;
  font-weight: 600;
  color: var(--white);
}
.calc-working,
.calc-empty {
  margin: var(--sp-2) 0 0;
  color: var(--text);
}

/* ------------------------------------------------------ 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);
}
/* The home-page guide cards are whole-card links (<a class="prod-card">), and a
   bare anchor underlines every line of text inside it: title, blurb and CTA.
   Found 2026-09-13 on 3dgenetic; the same markup had been underlining Fort
   Myers' guide cards since 2026-08-12. Product cards are <article>, unaffected. */
a.prod-card {
  text-decoration: none;
  color: inherit;
}
.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;
}
/* A note attached to a prose block. NOT .legal-updated, which is the
   "last updated" stamp: uppercase mono at --fs-3xs, unreadable for anything
   longer than a date and wrong for the sentence a page turns on. Added
   2026-08-28 after site #3's transformer guide rendered its key diagnostic as
   tiny mono small-print. Applies to every site; the cannabis notes were
   equally unreadable and nobody had looked at one. */
.prose-note {
  display: block;
  margin: var(--sp-5) 0;
  padding: var(--sp-5) var(--sp-6);
  border-radius: var(--r-lg);
  border-left: 3px solid var(--note-rule);
  background: var(--note-bg);
  color: var(--text);
  font-size: var(--fs-sm);
  line-height: 1.65;
}
.prose-note strong { color: var(--white); }

/* Line icons (build.py ICONS). Sized in em so a card's own font-size drives
   them, and coloured by currentColor so they inherit the accent. */
.icon-svg {
  width: 1.6em;
  height: 1.6em;
  display: block;
  color: var(--green);
}
.path-icon .icon-svg, .stat-icon .icon-svg { width: 1.4em; height: 1.4em; }

.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%;
}

/* -- Native signup form ------------------------------------------------- */
/* Our own form, replacing the provider's embed. No third-party CSS arrives
   with it, so everything it needs is here. */
.signup-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.signup-label {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.signup-input {
  flex: 1 1 220px;
  min-width: 0;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--bg2);
  color: var(--white);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
}
.signup-input::placeholder { color: var(--muted); }
.signup-input:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
  border-color: var(--green);
}
/* Honeypot. Moved OFF-SCREEN rather than display:none — some browsers skip
   hidden fields when autofilling, and a bot that fills everything still
   fills this one. */
.signup-hp {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}
.signup-status:empty { display: none; }
.signup-status {
  margin-top: var(--sp-3);
  color: var(--green);
  font-size: var(--fs-sm);
}

/* -- Terpene Match ------------------------------------------------------- */
/* auto-fit, NOT auto-fill: a short final row must collapse rather than leave
   dead tracks. Fixing a `max` in minmax() once silently forced every product
   grid to two-a-row (§15, 2026-08-06), so the cap belongs on the CARD. */
.terpene-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: var(--sp-2);
  width: 100%;
}
.terpene-card {
  aspect-ratio: 3 / 4;
  max-width: 160px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--bg2);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  line-height: 1.25;
  text-align: center;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast),
              transform var(--t-fast);
}
.terpene-card:hover:not(:disabled) { border-color: var(--green); }
.terpene-card:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}
.terpene-card.is-up {
  background: var(--card-hover);
  border-color: var(--accent-line);
  color: var(--white);
}
.terpene-card.is-matched {
  border-color: var(--green);
  background: var(--accent-wash);
  color: var(--green);
  cursor: default;
}
/* The flip is the only motion here and it is decorative, so it goes when the
   reader has asked for less of it. */
@media (prefers-reduced-motion: no-preference) {
  .terpene-card.is-up { transform: scale(1.04); }
}
.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 {
  /* Needed by the optional footer background: .section-bg is absolute and
     anchors to the nearest positioned ancestor. Without this it would escape
     to the page and cover the whole document. */
  position: relative;
  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);
}
/* The interview tools, given billing in the brand column. Deliberately the
   same vocabulary as .tool-panel on the tool pages themselves — accent wash,
   accent line, green left rule — so a reader meets one visual idea for "this
   is a thing you use", not two. Every value is an existing token; a site
   without `footer_tools` renders none of this. */
.footer-tools {
  margin-top: var(--sp-5);
  padding: var(--sp-4);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  border-left: 3px solid var(--green);
  border-radius: var(--r-md);
}
.footer-tools .tool-eyebrow {
  margin: 0 0 var(--sp-3);
}
.footer-tools a {
  display: block;
  text-decoration: none;
  color: var(--text);
  margin-bottom: var(--sp-3);
  transition: color var(--t-fast);
}
.footer-tools a:last-child {
  margin-bottom: 0;
}
.footer-tools a strong {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.footer-tools a span {
  display: block;
  font-size: 0.78rem;
  color: var(--muted);
  line-height: 1.5;
  margin-top: 2px;
}
.footer-tools a:hover strong {
  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; }
/* `layout: split`: the copy on the left, its one card on the right, heading and
   disclosure across the top. Stacks to one column on narrow screens. */
.product-feature--split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
  column-gap: var(--sp-8);
  align-items: start;
}
/* Rows are explicit: the disclosure comes after the copy in the markup but has
   to sit above both columns, or it pushes the card into a row of its own. A
   panel with no disclosure leaves row 2 empty, which takes no height. */
.product-feature--split > h2 {
  grid-column: 1 / -1;
  grid-row: 1;
}
.product-feature--split > .affiliate-disclosure {
  grid-column: 1 / -1;
  grid-row: 2;
}
.product-feature--split > .product-feature-copy {
  grid-column: 1;
  grid-row: 3;
  margin-bottom: 0;
}
.product-feature--split > .grid-auto-narrow {
  grid-column: 2;
  grid-row: 3;
  grid-template-columns: 1fr;
}
@media (max-width: 820px) {
  .product-feature--split {
    grid-template-columns: 1fr;
  }
  .product-feature--split > .product-feature-copy,
  .product-feature--split > .grid-auto-narrow {
    grid-column: 1;
    grid-row: auto;
  }
  .product-feature--split > .product-feature-copy {
    margin-bottom: var(--sp-5);
  }
}
/* 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); }

/* ---------------------------------------------------------------- triage --
   The interview troubleshooter (core/tools/triage.js). Styled here because
   rule 1 admits exactly one authored stylesheet; every colour is a token, so
   the tool inherits each site's palette without a per-site rule.

   Buttons are the whole interface, so they get a real hit area and a visible
   focus ring: this is operated on a porch, on a phone, one-handed, in the
   dark, by someone already annoyed. */
.triage-mount { margin: 1.5rem 0; }

.triage-step {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1.25rem 1.25rem 1.4rem;
}

.triage-q {
  margin: 0 0 .35rem;
  font-size: 1.15rem;
  line-height: 1.3;
  color: var(--white);
}
.triage-q:focus-visible { outline: 2px solid var(--green); outline-offset: 4px; }

.triage-help,
.triage-finding { margin: .35rem 0 0; color: var(--text); line-height: 1.6; }
.triage-help { color: var(--muted); font-size: .95rem; }

.triage-answers {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  margin-top: 1rem;
}

.triage-answer {
  width: 100%;
  text-align: left;
  padding: .8rem .9rem;
  font: inherit;
  color: var(--white);
  background: var(--bg2);
  border: 1px solid var(--accent-line);
  border-radius: 8px;
  cursor: pointer;
}
.triage-answer:hover { border-color: var(--accent-line-hover); background: var(--card-hover); }
.triage-answer:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }

/* The mains warning. Deliberately not styled as an alert -- it appears on
   several steps, and a red box on every third question stops being read. */
.triage-warn {
  margin: .9rem 0 0;
  padding: .7rem .85rem;
  background: var(--note-bg);
  border-left: 3px solid var(--note-rule);
  border-radius: 6px;
  font-size: .93rem;
  line-height: 1.55;
  color: var(--text);
}

/* A tool offered because the STEP needs it. Quiet on purpose: it sits beneath
   the answers, never above them, so the question is always what the reader
   meets first. */
.triage-tool {
  margin: 1.1rem 0 0;
  padding-top: .9rem;
  border-top: 1px solid var(--border);
}
.triage-tool-why { margin: 0 0 .45rem; font-size: .93rem; color: var(--muted); line-height: 1.55; }
.triage-tool-link { font-weight: 600; color: var(--green); }
.triage-tool-disc { margin: .35rem 0 0; font-size: .8rem; color: var(--muted); }

/* "Before you start" — the facts a reader needs before committing to a job,
   at the top where they can still act on them. Quiet by design: it is a
   pre-flight card, not a callout, and it must not compete with the heading
   above it. Every value is an existing token. */
.prep {
  margin: 0 0 var(--sp-6);
  padding: var(--sp-5);
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.prep-head { margin: 0 0 var(--sp-4); font-size: 1rem; }
.prep-facts { margin: 0; display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-8); }
.prep-facts > div { min-width: 0; }
.prep-facts dt {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
  font-weight: 700;
}
.prep-facts dd { margin: 2px 0 0; font-size: var(--fs-sm); }
.prep-ruleout-head { margin: var(--sp-4) 0 var(--sp-2); font-size: var(--fs-sm); font-weight: 600; }
.prep-ruleout { margin: 0; padding-left: 1.1rem; font-size: var(--fs-sm); color: var(--muted); }
.prep-ruleout li { margin-bottom: .3rem; }
/* The exit block. Deliberately looks like prose rather than a call to action:
   a reader whose fix failed is not in the mood to be sold to. */
.next-steps { border-top: 1px solid var(--border); padding-top: var(--sp-5); }

/* The pro-referral block on an outcome that involves mains work. Quieter than
   .triage-tool on purpose: a reader who has reached this point has decided the
   DIY route is over, and the block should read as help rather than as a second
   thing to buy. No new colours; all four values are existing tokens. */
.triage-pro {
  margin: 1.1rem 0 0;
  padding-top: .9rem;
  border-top: 1px solid var(--border);
  font-size: .93rem;
  color: var(--muted);
  line-height: 1.55;
}
.triage-pro p { margin: 0 0 .45rem; }
.triage-pro-head { font-weight: 600; color: var(--text); }
.triage-pro-quote {
  padding-left: .8rem;
  border-left: 2px solid var(--accent-line);
  font-style: italic;
}
.triage-pro-cta { font-weight: 600; color: var(--green); }
.triage-pro-disc { font-size: .8rem; }

.triage-cta {
  display: inline-block;
  margin-top: 1rem;
  padding: .7rem 1.1rem;
  font-weight: 600;
  color: var(--bg);
  background: var(--green);
  border-radius: 8px;
  text-decoration: none;
}
.triage-cta:hover { background: var(--green-dark); }
.triage-cta:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }

.triage-nav { display: flex; gap: .6rem; margin-top: .9rem; }
.triage-back {
  padding: .45rem .8rem;
  font: inherit;
  font-size: .9rem;
  color: var(--muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}
.triage-back:hover { color: var(--white); border-color: var(--accent-line-hover); }
.triage-back:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }

/* -------------------------------------------------- 33. Specification table */
/* The per-model transformer table (manuals.json), added 2026-08-30. Until now
   only the four GROUP rows reached a reader; the 19 model rows and the diode
   column existed in data and appeared nowhere.

   The wrapper scrolls rather than the page: three columns of model names do
   not fit a phone, and a table that widens the document body breaks every
   other block on the page. `-webkit-overflow-scrolling` is not set because
   momentum scrolling has been the default for years and the property is dead
   weight in a file rule 1 keeps deliberately small. */
.table-scroll {
  overflow-x: auto;
  margin: var(--sp-5) 0;
}
.spec-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
  /* Model names must not wrap mid-name into an unreadable column. */
  min-width: 34rem;
}
.spec-table th,
.spec-table td {
  text-align: left;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.spec-table thead th {
  color: var(--white);
  font-weight: 600;
  border-bottom: 1px solid var(--accent-line-hover);
  white-space: nowrap;
}
.spec-table tbody tr:last-child td { border-bottom: 0; }
/* Voltages and yes/no read as data, not prose. */
.spec-table td:nth-child(2),
.spec-table td:nth-child(3) { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The generic `table` block holds sentences, not voltages. Its cells wrap and
   the value columns share the width, or the last column runs off the prose
   measure (the SW350T vs SW380T table lost its SW380T column, 2026-09-14). */
.spec-table--wrap { width: 100%; table-layout: fixed; min-width: 0; }
.spec-table--wrap tr > :first-child { width: 22%; }
.spec-table--wrap thead th,
.spec-table--wrap td:nth-child(2),
.spec-table--wrap td:nth-child(3) { white-space: normal; }

/* ------------------------------------------------- 34. Interactive tool panel */
/* Added 2026-08-30. The troubleshooter and the camera chooser are the only free
   interactive things on the site and they were the least visible part of it:
   `.triage-step` paints `--card`, and the doorbellsecure palette sets `--card`
   and `--bg` to the same #FFFFFF, so the whole interface was white on white
   behind a 10%-opacity border. The dark cannabis palettes concealed it.

   Every value here is a token, so the panel picks up each site's accent without
   a per-site rule (rule 1). The accent tint is what separates it from the
   surrounding prose; the left rule is what survives when a viewer's contrast
   settings flatten the tint away. */
.tool-panel {
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  border-left: 3px solid var(--green);
  border-radius: 12px;
  padding: var(--sp-5) var(--sp-5) var(--sp-6);
  margin: var(--sp-6) 0;
}
.tool-panel .legal-doc {
  background: none;
  border: 0;
  padding: 0;
}
.tool-eyebrow {
  margin: 0 0 var(--sp-3);
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--green);
}
/* Inside the panel the step sits on the page ground, so the card reads as a
   raised surface against the tint instead of vanishing into it. */
.tool-panel .triage-step {
  background: var(--bg);
  border-color: var(--accent-line);
}
/* The answers are the interface. On the tinted ground they need to read as
   controls, so they carry the accent rather than a hairline. */
.tool-panel .triage-answer {
  background: var(--card);
  border-color: var(--accent-line-hover);
}
.tool-panel .triage-answer:hover {
  background: var(--accent-wash);
  border-color: var(--green);
}

/* -------------------------------------------------------- 35. Figures */
/* Added 2026-08-30 with the `figure` block. Two kinds of asset share these
   rules: a photograph at the head of a guide, and an authored SVG diagram of
   the thing the guide tells you to inspect.

   The diagram classes below are the contract the SVG files draw against. They
   exist so no colour is written inside a site's assets folder -- rule 1 keeps
   every colour in this file, and an SVG with hex values baked in would be a
   second stylesheet wearing a disguise. */
.figure {
  margin: var(--sp-6) 0;
}
.figure-media,
.figure-svg { border-radius: 10px; overflow: hidden; }
.figure figcaption {
  margin-top: var(--sp-3);
  font-size: .9rem;
  line-height: 1.55;
  color: var(--muted);
}
/* A diagram is a drawing, not a picture: it sits on a tinted ground so the
   line art has something to read against on a white page. */
.figure--diagram .figure-svg {
  background: var(--bg2);
  border: 1px solid var(--border);
  padding: var(--sp-5);
}
.figure-svg svg { display: block; width: 100%; height: auto; }
/* A diagram must not shrink below the point its labels stop being readable.
   Measured 2026-08-30: a 640-unit drawing in a 285px column on a 375px phone
   scaled to 0.445, rendering 13-unit labels at 5.8px against 15.2px body copy.
   Unreadable, and worse than no diagram, because it looks like information.

   So the drawing holds a floor and the FIGURE scrolls instead -- the same
   trade the spec table makes in section 33. 600px keeps labels near 12px on a
   phone, which is small but legible, and costs a short sideways drag. */
.figure--diagram .figure-svg { overflow-x: auto; }
.figure--diagram .figure-svg svg { min-width: 600px; }

/* ---- diagram drawing contract -------------------------------------------
   `dg-` classes are the only styling an authored diagram may use. Keeping the
   vocabulary this small is deliberate: six diagrams drawn against six ad-hoc
   palettes would drift apart within a month. */
.dg-line   { fill: none; stroke: var(--white); stroke-width: 2; }
.dg-thin   { fill: none; stroke: var(--muted); stroke-width: 1.25; }
.dg-accent { fill: none; stroke: var(--green); stroke-width: 2.5; }
.dg-fill   { fill: var(--card); stroke: var(--white); stroke-width: 2; }
.dg-wash   { fill: var(--accent-wash); stroke: none; }
.dg-label  { fill: var(--text); font-size: 13px; font-family: inherit; }
.dg-label--key { fill: var(--green); font-weight: 600; }
.dg-label--muted { fill: var(--muted); font-size: 12px; }
/* Leader lines from a label to the part it names. Dashed so they read as
   annotation rather than as part of the object being drawn. */
.dg-leader { fill: none; stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
