/* ==========================================================================
   BUILD MY BOX — THEME LAYER
   ==========================================================================
   Every visual decision (color, font, radius, structural spacing, and the
   image "asset slots") lives in the :root block below. To reskin the app —
   e.g. dropping in Higgsfield-generated art — edit ONLY this block:

     1. COLORS         — swap hex/rgba values; every rule references these.
     2. ASSET SLOTS    — set an image slot from `none` to `url("...")`:
          --bg-image      full-page background art (layers over --bg color)
          --panel-image   card/panel texture (layers over --panel color)
          --button-image  button texture/art (layers over --accent color)
        When an image is set, add sizing in the matching rule if needed
        (e.g. `background-size: cover;` on body) — not preset here so the
        no-image default renders exactly as before.
        There are no icon elements in the app yet; when icons arrive, follow
        the same pattern: one `--icon-*` variable per slot, referenced by the
        rule that places it, never a file path buried in a rule.
     3. FONTS          — family, sizes, weights are all tokens.
     4. SHAPE/SPACING  — radii and structural spacing (page width/padding,
        card padding/gap, control padding) are tokens. Small one-off margins
        INSIDE components (4-18px nudges between rows/labels) are deliberate-
        ly left literal: they're component structure, not theme, and a token
        per arbitrary pixel value would make this file harder to edit, not
        easier.
   The 640px mobile breakpoint is literal because CSS media queries cannot
   read custom properties.
   Copy/text note: user-facing words live in each page's .html file and its
   matching app/*.js (error messages + checklist templates) — not here.
   ========================================================================== */

:root {
  /* --- colors: surfaces --- */
  --bg: #0f1115;            /* page background color */
  --panel: #171a21;         /* card/panel background color */
  --panel-border: #2a2f3a;  /* card, input, table borders */
  --input-bg: #0d0f13;      /* select/input field background */
  --export-bg: #0d0f13;     /* checklist/export text box background */
  --note-bg: rgba(255,255,255,0.03); /* note callout background */

  /* --- colors: text --- */
  --text: #e6e8eb;          /* primary text */
  --text-dim: #9aa1ad;      /* secondary/dim text */
  --button-text: #1a0d07;   /* text on accent-colored buttons */

  /* --- colors: accent + status --- */
  --accent: #ff6a3d;        /* brand accent (buttons, highlights, big results) */
  --accent-dim: #7a3a26;    /* reserved dim accent */
  --ok: #3ecf8e;            /* good/high-confidence */
  --warn: #e8b93f;          /* caution */
  --bad: #e8574a;           /* error/low-confidence */
  --ok-bg: rgba(62,207,142,0.15);   /* badge fill behind --ok */
  --warn-bg: rgba(232,185,63,0.15); /* badge fill behind --warn */
  --bad-bg: rgba(232,87,74,0.15);   /* badge fill behind --bad */

  /* --- asset slots (see header comment; `none` = current flat look) --- */
  /* Full-page reskin pass, 2026-08-26 (Higgsfield, Cory-approved spend).
     Each slot composites its own dark wash INSIDE the variable (gradient +
     url layers) so text contrast is controlled here, not in component rules.
     11-candidate fan-out, judged against text-legibility before wiring:
     bg = subwoofer surround arc rim-lit orange over black (soul_cinematic);
     panel = near-invisible brushed black (nano_banana);
     button = brushed orange metal, hue-washed toward --accent (nano_banana). */
  --bg-image: linear-gradient(180deg, rgba(15,17,21,0.30) 0%, rgba(15,17,21,0.72) 48%, rgba(15,17,21,0.97) 100%), url("../assets/renders/bg-surround-arc.jpg");
  --panel-image: linear-gradient(rgba(23,26,33,0.55), rgba(23,26,33,0.55)), url("../assets/renders/panel-brushed-black.jpg");
  --button-image: linear-gradient(rgba(255,106,61,0.42), rgba(255,106,61,0.42)), url("../assets/renders/button-brushed-orange.jpg");
  --hero-image: url("../assets/renders/hero-garage-culture-21x9.jpg"); /* Higgsfield render, 2026-07-10; compressed from 9.4MB PNG source */
  --card-img-box-builder: url("../assets/renders/card-box-builder.jpg");
  --card-img-speaker-panel: url("../assets/renders/card-speaker-panel.jpg");
  --card-img-net-volume: url("../assets/renders/card-net-volume.jpg");
  --card-img-port-calculator: url("../assets/renders/card-port-calculator.jpg"); /* Higgsfield render, 2026-07-11 */
  --card-img-bandpass: url("../assets/renders/card-bandpass.jpg"); /* Higgsfield render, 2026-07-11 */
  --icon-img-box-builder: url("../assets/renders/icon-box-builder.jpg"); /* Higgsfield render, 2026-07-11 */
  --icon-img-speaker-panel: url("../assets/renders/icon-speaker-panel.jpg"); /* Higgsfield render, 2026-07-11 */
  --icon-img-net-volume: url("../assets/renders/icon-net-volume.jpg"); /* Higgsfield render, 2026-07-11 */
  --icon-img-port-calculator: url("../assets/renders/icon-port-calculator.jpg"); /* Higgsfield render, 2026-07-12 */
  --icon-img-bandpass: url("../assets/renders/icon-bandpass.jpg"); /* Higgsfield render, 2026-07-12 */
  --icon-img-troubleshooting: url("../assets/renders/icon-troubleshooting.jpg"); /* Higgsfield render, 2026-07-12 */
  --card-img-troubleshooting: url("../assets/renders/card-troubleshooting.jpg"); /* Higgsfield render, 2026-07-11 */
  --card-img-door-fitment: url("../assets/renders/card-door-fitment.jpg"); /* Higgsfield render, 2026-07-12 */
  --icon-img-door-fitment: url("../assets/renders/icon-door-fitment.jpg"); /* Higgsfield render, 2026-07-12 */
  --card-img-wiring: url("../assets/renders/card-wiring.jpg"); /* Higgsfield render, 2026-07-12 */
  --icon-img-wiring: url("../assets/renders/icon-wiring.jpg"); /* Higgsfield render, 2026-07-12 */
  --card-img-class-calculator: url("../assets/renders/card-class-calculator.jpg"); /* Higgsfield render, 2026-07-12 */
  --icon-img-class-calculator: url("../assets/renders/icon-class-calculator.jpg"); /* Higgsfield render, 2026-07-12 */
  --card-img-spl-meet-finder: url("../assets/renders/card-spl-meet-finder.jpg"); /* Higgsfield render, 2026-07-12 */
  --icon-img-spl-meet-finder: url("../assets/renders/icon-spl-meet-finder.jpg"); /* Higgsfield render, 2026-07-12 */
  --boxtype-img-sealed: url("../assets/renders/box-type-sealed.jpg"); /* Higgsfield render, 2026-07-12 (F9 box-types strip) */
  --boxtype-img-ported: url("../assets/renders/box-type-ported.jpg"); /* Higgsfield render, 2026-07-12 (F9 box-types strip) */
  --boxtype-img-bandpass: url("../assets/renders/box-type-bandpass.jpg"); /* Higgsfield render, 2026-07-12 (F9 box-types strip) */

  /* --- fonts --- */
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --line-height: 1.5;
  --fs-badge: 11px;         /* confidence badges */
  --fs-footer: 12px;        /* page footer */
  --fs-export: 12.5px;      /* checklist/export text box */
  --fs-small: 13px;         /* labels, legends, notes, secondary buttons, tables */
  --fs-base: 14px;          /* inputs, nav links, spec rows, radio labels */
  --fs-med: 15px;           /* ledes, primary buttons, result units */
  --fs-h3: 16px;            /* card headings */
  --fs-h1: 20px;            /* site header title */
  --fs-h2: 22px;            /* page headings */
  --fs-result: 32px;        /* big calculator result numbers */
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;
  --fw-heavy: 800;

  /* --- shape + structural spacing --- */
  --radius: 10px;           /* cards */
  --radius-sm: 8px;         /* inputs, buttons, fieldsets, notes, export box */
  --radius-pill: 999px;     /* badges */
  --content-max-width: 900px;
  --page-pad: 32px 20px 80px;
  --card-pad: 20px;
  --card-gap: 16px;         /* between cards and grid columns */
  --control-pad: 10px 12px; /* inside selects/inputs */
  --button-pad: 12px 20px;  /* inside primary buttons */
}

* { box-sizing: border-box; }

/* ==========================================================================
   MOTION POLICY — one place to kill every looping/ambient animation for
   users who ask for reduced motion (Fable review, 2026-07-12, gap #1: only
   the Shop Ticket's rotation was gated; hero-pulse and hero-cta-glow ran
   forever ungated). Every new infinite/looping animation or scroll-linked
   effect added to this file MUST have its selector added to this list —
   this is meant to stay the single policy block, not grow a second one.
   One-shot interaction feedback (a result count-up, a hover transition)
   does NOT belong here — only things that loop or track scroll/cursor
   indefinitely.

   JS-driven loops (rAF-based auto-rotate/parallax, e.g. box-preview.js's
   auto-rotate) can't be paused by a CSS media query -- they check
   `matchMedia('(prefers-reduced-motion: reduce)')` directly at their own
   call site instead. This comment is the cross-reference so that check
   doesn't get lost/forgotten: grep the repo for `reducedMotion` to find them.
   banner-video.js (F11, 2026-07-12, Box Builder pilot) is one of these --
   it never even inserts the <video> element under reduced-motion, so
   .page-banner__video below is belt-and-suspenders (a `display:none` catch
   in case that JS gate is ever bypassed), not the primary gate.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .hero::before,
  .hero__cta,
  .hero__picker button,
  .port-airflow__dot,
  .page-banner::before {
    animation: none;
  }
  .page-banner__video,
  .hero__video,
  .box-reveal-video {
    /* Belt-and-suspenders like .page-banner__video: banner-video.js,
       hero-video.js, and box-reveal-intro.js all bail under reduced motion
       before creating any element or firing any network request -- that JS
       check is the primary gate, this catches it ever being bypassed. */
    display: none;
  }
}

body {
  margin: 0;
  font-family: var(--font-body);
  background-color: var(--bg);
  background-image: var(--bg-image);
  /* Sizing for the live --bg-image (2026-08-26, per the header-comment
     contract: sizing lands here only once a slot has a real image).
     fixed keeps the surround arc as an ambient frame instead of scrolling
     away; iOS Safari downgrades fixed to scroll on its own — acceptable. */
  background-size: cover;
  background-position: top center;
  background-repeat: no-repeat;
  background-attachment: fixed;
  color: var(--text);
  line-height: var(--line-height);
}

/* Base link color -- there was no rule here at all before this, so any
   anchor not covered by a more specific selector (nav, .support-link,
   .hero__browse, etc.) rendered browser-default blue/purple straight
   against --bg (#0f1115). Low contrast, and the one color on the page not
   drawn from the palette. Cory's call, 2026-08-19: match the brand accent
   instead -- same color already proven readable on this exact background
   by .data-call-banner a / .support-link a, so no new risk, just closing a
   gap the site's own convention already implied. :visited pinned to the
   same color on purpose -- the default visited-purple would reintroduce
   the identical problem for anyone who's clicked a link before. */
a { color: var(--accent); }
a:visited { color: var(--accent); }

.wrap {
  position: relative;
  z-index: 1; /* sits above .sticker-wall (z-index:0) regardless of DOM order */
  max-width: var(--content-max-width);
  margin: 0 auto;
  padding: var(--page-pad);
}

/* Site-wide "paid features coming soon" strip -- full viewport width on
   purpose (sits before .wrap, not inside it), low-key amber notice rather
   than an alarming red error -- nothing free is changing, so this should
   read as anticipation, not a restriction. */
.coming-soon-banner {
  position: relative;
  z-index: 2;
  background: var(--warn-bg);
  border-bottom: 1px solid var(--warn);
  color: var(--text);
  font-size: var(--fs-small);
  text-align: center;
  padding: 8px 20px;
}

/* Data-call banner (2026-08-07). Same full-width, pre-.wrap placement as the
   coming-soon bar above, but deliberately a DIFFERENT colour: this is a request
   for help, not a caution, and stacking two amber bars would read as two
   warnings. Uses the brand accent at low alpha so it's visible without shouting.
   Kept to one line at desktop width -- it sits on every page, so it has to be
   quiet enough to live with. */
.data-call-banner {
  position: relative;
  z-index: 2;
  background: rgba(255, 106, 61, 0.12);
  border-bottom: 1px solid var(--accent);
  color: var(--text);
  font-size: var(--fs-small);
  text-align: center;
  padding: 8px 20px;
}
.data-call-banner a {
  color: var(--accent);
  font-weight: 600;
  white-space: nowrap;
}

header.site {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* keeps the nav off the brand lockup when the link list overfills the row
     (2026-07-14, spotted adding the BMB mark: space-between collapses to a
     zero gap once the nav wraps) */
  column-gap: 16px;
  margin-bottom: 28px;
}

header.site h1 {
  font-size: var(--fs-h1);
  margin: 0;
}

header.site a.brand {
  color: var(--text);
  text-decoration: none;
  font-weight: var(--fw-bold);
  /* BMB mark + wordmark lockup (2026-07-14): the mark is an AI-generated
     vector (recraft_v4_1) but "Build My Box" stays real HTML text on
     purpose -- baked-in wordmarks garble, real type stays crisp at every
     size and keeps the mark reusable standalone. */
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* the lockup never wraps -- the nav links are the flexible part */
  white-space: nowrap;
  flex-shrink: 0;
}
header.site a.brand .brand__mark {
  height: 24px;
  width: auto;
  display: block;
}

header.site nav a {
  color: var(--text-dim);
  text-decoration: none;
  margin-left: 18px;
  font-size: var(--fs-base);
  /* wrap between links, never inside one ("Port / Calculator" was splitting
     mid-label on narrower desktop widths) */
  white-space: nowrap;
  display: inline-block;
}
header.site nav a:hover { color: var(--text); }

h2 { font-size: var(--fs-h2); margin-top: 0; }
h3 { font-size: var(--fs-h3); margin-bottom: 6px; }

p.lede {
  color: var(--text-dim);
  font-size: var(--fs-med);
  max-width: 640px;
}

.card {
  background-color: var(--panel);
  background-image: var(--panel-image);
  background-size: cover; /* 2026-08-26: sizing for the live --panel-image */
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: var(--card-pad);
  margin-bottom: var(--card-gap);
}

.card-link {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: border-color 0.15s ease;
}
.card-link:hover { border-color: var(--accent); }

.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--card-gap);
}
@media (max-width: 640px) {
  .grid-2 { grid-template-columns: 1fr; }

  /* Header/nav has no room to sit side-by-side with the brand text on a
     phone-width screen -- confirmed broken in a real Android emulator
     2026-07-10 (brand text wrapping letter-group by letter-group). Stack
     brand above nav, let nav links wrap in their own row instead of
     fighting the brand for horizontal space. */
  header.site {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
  header.site nav {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
  }
  header.site nav a {
    margin-left: 0;
  }
}

label {
  display: block;
  font-size: var(--fs-small);
  color: var(--text-dim);
  margin-bottom: 4px;
  margin-top: 14px;
}
label:first-child { margin-top: 0; }

select, input[type="number"], input[type="text"], input[type="email"] {
  width: 100%;
  padding: var(--control-pad);
  background: var(--input-bg);
  border: 1px solid var(--panel-border);
  color: var(--text);
  border-radius: var(--radius-sm);
  font-size: var(--fs-base);
  font-family: var(--font-body);
}
select:focus, input:focus {
  outline: none;
  border-color: var(--accent);
}

fieldset {
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  padding: 14px 16px 16px;
  margin: 14px 0 0;
}
legend {
  padding: 0 6px;
  color: var(--text-dim);
  font-size: var(--fs-small);
}

.radio-row {
  display: flex;
  /* flex-wrap added 2026-07-12 (pre-push audit M7): the wiring calculator's
     three long coil-topology labels forced a ~462px min content width,
     making that one page scroll sideways on a 375px phone. Wrapping only
     kicks in when a row genuinely doesn't fit, so the short box-type /
     end-correction rows elsewhere are unaffected. */
  flex-wrap: wrap;
  gap: 18px;
  align-items: center;
}
.radio-row label {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  color: var(--text);
  font-size: var(--fs-base);
  cursor: pointer;
}
.radio-row input { width: auto; }

button {
  background-color: var(--accent);
  background-image: var(--button-image);
  background-size: cover; /* 2026-08-26: sizing for the live --button-image */
  background-position: center;
  color: var(--button-text);
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--button-pad);
  font-size: var(--fs-med);
  font-weight: var(--fw-bold);
  cursor: pointer;
  margin-top: 18px;
}
button:hover { filter: brightness(1.08); }
button.secondary {
  background: transparent; /* shorthand on purpose: also clears --button-image */
  border: 1px solid var(--panel-border);
  color: var(--text);
  font-weight: var(--fw-medium);
  padding: 8px 14px;
  font-size: var(--fs-small);
  margin-top: 10px;
}

.badge {
  display: inline-block;
  font-size: var(--fs-badge);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 3px 8px;
  border-radius: var(--radius-pill);
}
.badge-high { background: var(--ok-bg); color: var(--ok); }
.badge-medium { background: var(--warn-bg); color: var(--warn); }
.badge-low { background: var(--bad-bg); color: var(--bad); }

.spec-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--panel-border);
  font-size: var(--fs-base);
}
.spec-row:last-child { border-bottom: none; }
.spec-row .k { color: var(--text-dim); flex-shrink: 0; }
.spec-row .v { text-align: right; }

/* Result-reveal ceremony (reveal.js, all 4 calculators) -- one-shot, not a
   loop, so it's intentionally outside the MOTION POLICY reduced-motion gate. */
.spec-row.reveal-row {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
.spec-row.reveal-row--in {
  opacity: 1;
  transform: translateY(0);
}

.note {
  font-size: var(--fs-small);
  color: var(--text-dim);
  margin-top: 8px;
  padding: 10px 12px;
  background: var(--note-bg);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--panel-border);
}
.note.warn { border-left-color: var(--warn); }
.note.bad { border-left-color: var(--bad); }

/* Collapsible "why?" explainers (2026-08-23, competitor-analysis rec #2) --
   wraps the LONG static explainer paragraphs under form controls, default
   closed, so the form reads shorter without losing a single word of the
   sourcing/reasoning underneath. Native <details>/<summary>: no JS, works
   with browser find-in-page once opened, keyboard-accessible for free.
   Deliberately NOT used on dynamic/JS-populated notes (warnings, live
   status, empty-state text) -- those stay always-visible; this is for the
   long-form "why" prose only. */
details.note-toggle {
  margin-top: 8px;
}
details.note-toggle > summary {
  cursor: pointer;
  font-size: var(--fs-small);
  color: var(--accent);
  list-style: none;
  user-select: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
details.note-toggle > summary::-webkit-details-marker { display: none; }
details.note-toggle > summary::before {
  content: '▸';
  display: inline-block;
  transition: transform 0.15s ease;
  font-size: 0.85em;
}
details.note-toggle[open] > summary::before {
  transform: rotate(90deg);
}
details.note-toggle > summary:hover {
  text-decoration: underline;
}
details.note-toggle > .note {
  margin-top: 6px;
}

/* ==========================================================================
   LIVE 3D BOX PREVIEW (box-preview.js) — Fable review, 2026-07-12, idea A1.
   A real six-face CSS 3D cube scaled to the user's actual computed box
   dimensions, auto-rotating, drag-to-rotate. No images/credits -- pure
   transforms. Auto-rotate is a JS rAF loop, reduced-motion-aware at its own
   call site (see the MOTION POLICY comment above) -- drag stays available
   either way since it's user-initiated, not ambient.
   ========================================================================== */
.box3d-stage {
  perspective: 900px;
  width: 100%;
  height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 4px 0 14px;
  cursor: grab;
  user-select: none;
  touch-action: none;
}
.box3d-stage:active { cursor: grabbing; }

.box3d {
  position: relative;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
}

.box3d__face {
  position: absolute;
  top: 0;
  left: 0;
  box-sizing: border-box;
  border: 1px solid var(--panel-border);
}
.box3d__face--front, .box3d__face--back { background: var(--panel); }
.box3d__face--side { background: #12151b; }
.box3d__face--top { background: var(--input-bg); }

.box3d__driver {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #2c2c2c 0%, #0d0d0d 55%, #000 100%);
  border: 3px solid var(--accent-dim);
  box-shadow: inset 0 0 12px rgba(0,0,0,0.8);
}
.box3d__port {
  position: absolute;
  top: 68%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: #000;
  border: 2px solid var(--panel-border);
}
.box3d-caption {
  text-align: center;
  color: var(--text-dim);
  font-size: var(--fs-small);
  margin: -6px 0 14px;
}

/* Preview mode switcher (2026-08-23, competitor-analysis rec #3): Solid /
   Transparent / Dimensions are modes of the SAME box3d model
   (box-preview.js), toggled by re-invoking it with a different opts.mode --
   not three separate builds. */
.box3d-mode-toggle {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin: 8px 0 -2px;
}
.box3d-mode-btn {
  font-size: var(--fs-small);
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  padding: 4px 10px;
  cursor: pointer;
}
.box3d-mode-btn:hover { color: var(--text); border-color: var(--accent-dim); }
.box3d-mode-btn.active {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--note-bg);
}

/* Transparent mode: see the openings on the far side of the box, not just
   the near faces. Ports/driver circles stay at their own opacity (set
   inline per-element) so they read clearly against the see-through faces. */
.box3d--transparent .box3d__face {
  opacity: 0.35;
}

/* Dimensions mode: W/H/D callouts as CHILDREN of one real face each (top =
   width, front = height, right side = depth) -- same trick box-preview.js
   already uses for driver/port circles, so the label rotates correctly with
   the model for free instead of needing a second 3D transform computed here. */
.box3d__dim-label {
  position: absolute;
  font-size: 11px;
  font-weight: 600;
  color: #fff;
  background: rgba(0,0,0,0.55);
  padding: 2px 6px;
  border-radius: 3px;
  white-space: nowrap;
  pointer-events: none;
}

/* "Hear your tuning" reactive glow (tuning-audio.js) — Fable review,
   2026-07-12, idea A2. --audio-glow is written directly onto this element
   by the JS module (0 when idle), so the shadow is invisible until a sound
   is actually playing -- no separate idle-state rule needed. Not gated in
   the MOTION POLICY block: it's already bounded to one ~2s play, not a
   loop, and the JS module itself skips the visual write under
   prefers-reduced-motion (see the module's own reducedMotion() check). */
.audio-reactive {
  --audio-glow: 0;
  border-radius: var(--radius-sm);
  box-shadow: 0 0 calc(var(--audio-glow) * 40px) rgba(255, 106, 61, calc(var(--audio-glow) * 0.65));
  transition: box-shadow 0.05s linear;
}

/* Port air-flow visualization (port-airflow.js) — Fable review, 2026-07-12,
   idea A3. Dots flowing smoothly = within the 12-16 in²/ft³ range; amber +
   slight wobble = generous (check box depth); fast red scatter = will
   chuff. A genuine loop while a result is on screen, so .port-airflow__dot
   IS in the MOTION POLICY reduced-motion block below (unlike the audio
   glow above, this one's a real ongoing animation, not a bounded ~2s play). */
.port-airflow {
  margin: 10px 0;
}
.port-airflow__svg {
  width: 100%;
  max-width: 200px;
  height: 44px;
  overflow: visible;
}
.port-airflow__tube {
  fill: none;
  stroke: var(--panel-border);
  stroke-width: 2;
}
.port-airflow__dot {
  animation-duration: var(--flow-duration, 2s);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.port-airflow[data-status="ok"] .port-airflow__dot {
  fill: var(--ok);
  animation-name: port-flow-ok;
}
.port-airflow[data-status="generous"] .port-airflow__dot {
  fill: var(--warn);
  animation-name: port-flow-generous;
}
.port-airflow[data-status="chuff"] .port-airflow__dot {
  fill: var(--bad);
  animation-name: port-flow-chuff;
}
@keyframes port-flow-ok {
  0% { transform: translateX(0px); opacity: 0; }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% { transform: translateX(150px); opacity: 0; }
}
@keyframes port-flow-generous {
  0% { transform: translate(0px, -3px); opacity: 0; }
  10% { opacity: 1; }
  50% { transform: translate(75px, 3px); }
  90% { opacity: 1; }
  100% { transform: translate(150px, -3px); opacity: 0; }
}
@keyframes port-flow-chuff {
  0% { transform: translate(0px, 0px); opacity: 0; }
  8% { opacity: 1; }
  20% { transform: translate(25px, -8px); }
  35% { transform: translate(50px, 6px); }
  50% { transform: translate(75px, -6px); }
  65% { transform: translate(100px, 8px); }
  80% { transform: translate(125px, -4px); }
  92% { opacity: 1; }
  100% { transform: translate(150px, 3px); opacity: 0; }
}

.result-big {
  font-size: var(--fs-result);
  font-weight: var(--fw-heavy);
  color: var(--accent);
  margin: 4px 0 0;
}
.result-unit { font-size: var(--fs-med); color: var(--text-dim); font-weight: var(--fw-medium); margin-left: 6px; }

table.checklist {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-small);
  margin-top: 10px;
}
table.checklist th, table.checklist td {
  text-align: left;
  padding: 6px 8px;
  border-bottom: 1px solid var(--panel-border);
}
table.checklist th { color: var(--text-dim); font-weight: var(--fw-semibold); }

/* Checklist/export output only. Nothing else on the site uses .export-box, so
   the wrapping rules below cannot reach any other <pre> or <code> block.
   QA audit B-4, 2026-07-25: the DS18 affiliate tracking URL is a single
   ~180-char unbreakable token. white-space: pre-wrap alone will not break
   inside a word, so with the default overflow-x: visible that one line pushed
   the WHOLE PAGE sideways (928px scrollWidth vs 573px client at a 588px
   viewport). overflow-wrap: anywhere lets the URL break mid-token;
   overflow-x: auto keeps anything still too wide (long ASCII rules, wide
   fixed-width rows) scrolling inside this box instead of the document. */
pre.export-box {
  background: var(--export-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  padding: 14px;
  font-size: var(--fs-export);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  overflow-x: auto;
  color: var(--text-dim);
}

/* Cut-sheet diagram (cut-sheet.js) — Fable review, 2026-07-12, idea A8.
   Deliberate exception to the site's dark theme: this is meant to be
   screenshotted or printed and taken to the table saw, not read as on-screen
   UI chrome, so it's white/black regardless of --bg/--text everywhere else. */
.cut-sheet {
  background: #ffffff;
  border-radius: var(--radius-sm);
  padding: 12px;
  margin-top: 10px;
}
.cut-sheet__svg {
  width: 100%;
  display: block;
}
.cut-sheet__sheet {
  fill: #ffffff;
  stroke: #1a1a1a;
  stroke-width: 0.15;
}
.cut-sheet__sheet-label {
  font-size: 2px;
  fill: #1a1a1a;
  font-family: var(--font-body);
}
.cut-sheet__panel {
  fill: #f4f1ea;
  stroke: #1a1a1a;
  stroke-width: 0.1;
}
.cut-sheet__label {
  fill: #1a1a1a;
  font-family: var(--font-body);
  font-weight: var(--fw-semibold);
}
.cut-sheet__dims {
  fill: #555555;
  font-family: var(--font-body);
}
.cut-sheet__cutout {
  fill: none;
  stroke: #b8452f;
  stroke-width: 0.12;
  stroke-dasharray: 0.4 0.3;
}
.cut-sheet__cutout--port {
  stroke: #3a6ea5;
}

/* Download buttons injected below the SVG by cut-sheet.js render(). Reuses
   the existing button/button.secondary styles (no new button classes) --
   this row just lays the two out side by side and kills the default
   button margin-top so they sit flush under the diagram. */
.cut-sheet__actions {
  display: flex;
  gap: 10px;
  margin-top: 12px;
}
.cut-sheet__actions button {
  margin-top: 0;
}

/* Print: only the cut-sheet diagram, full-bleed, no buttons. First print
   stylesheet in the app -- there was nothing here before. Hides
   everything else on the page (nav, hero, other cards/footer) via the
   standard visibility-flip trick, then re-shows just .cut-sheet and pulls
   it out of the (hidden, but still laid-out) page flow to fill the sheet. */
@media print {
  body * {
    visibility: hidden;
  }
  .cut-sheet,
  .cut-sheet * {
    visibility: visible;
  }
  .cut-sheet {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    margin: 0;
    padding: 0;
    border-radius: 0;
  }
  .cut-sheet__actions {
    display: none !important;
  }
}

/* ==========================================================================
   BUILD PLAN (build-plan.html) — M1, 2026-07-12. Screen preview of the
   paid $15 build-plan document (no payment code exists anywhere in this
   project yet — Cory picks the processor separately, later; this is the
   free-to-view artifact that WOULD sit behind a paywall once he does).
   Reuses .card/.note/.spec-row/.result-big/etc. from the rest of the app
   for the on-screen look; print-specific overrides live in the @media
   print block below.
   ========================================================================== */

/* Generic screen-only utility -- anything that shouldn't show up on paper
   (nav, action buttons, the "this is a preview" banner) gets this instead
   of a bespoke rule per element. Reusable on any page, not just this one. */
@media print {
  .no-print { display: none !important; }
}

.build-plan__actions {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}
.build-plan__actions button { margin-top: 0; }

.build-plan__cover-title {
  font-size: var(--fs-h2);
  font-weight: var(--fw-heavy);
  margin: 0 0 4px;
}
.build-plan__cover-sub {
  color: var(--text-dim);
  font-size: var(--fs-base);
  margin: 0 0 16px;
}
.build-plan__honesty { margin-top: 16px; }

.build-plan__diagrams {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--card-gap);
}
@media (max-width: 640px) {
  .build-plan__diagrams { grid-template-columns: 1fr; }
  /* The recalibrated sections 5 and 6 (2026-07-28) carry wider tables than
     before -- the brace plan is 7 columns, the cleat options 6 -- and a table
     wider than the viewport pushes the WHOLE PAGE sideways, exactly the bug
     the pre.export-box comment above documents from QA audit B-4. Same fix,
     same reasoning: display:block turns the table element into a scroll
     container (its rows are still laid out by an anonymous table box, so it
     still renders as a table), so anything too wide scrolls inside its own box
     instead of the document. Screen only -- the @media print block resets this
     to display:table, because a scrollbar does nothing on paper and clipping a
     cut list would be worse than a small font. */
  .build-plan table.checklist {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}
/* Deliberate exception to the site's dark theme, same reasoning as
   .cut-sheet above: these diagrams are meant to be printed, not read as
   on-screen UI chrome. */
.bp-diagram-panel {
  background: #ffffff;
  border-radius: var(--radius-sm);
  padding: 12px;
  margin-top: 10px;
}
.bp-diagram-panel__svg {
  width: 100%;
  display: block;
}

.build-plan__assembly-list {
  margin: 12px 0 0;
  padding-left: 22px;
  font-size: var(--fs-base);
}
.build-plan__assembly-list li { margin-bottom: 8px; }
.build-plan__assembly-diagram { margin-top: 14px; }

.build-plan__qr {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}
/* The QR is a <canvas> since 2026-07-28 (was an <img> pointed at a
   third-party QR service -- see build-plan.js's renderQr). Same white
   plate/padding treatment either way; the canvas is drawn at a much higher
   internal resolution than its 180px display size so the printed page still
   resolves the modules cleanly. */
.build-plan__qr img,
.build-plan__qr canvas {
  background: #ffffff;
  border-radius: var(--radius-sm);
  padding: 8px;
  flex-shrink: 0;
  width: 180px;
  height: 180px;
}
.build-plan__qr-url {
  font-size: var(--fs-small);
  color: var(--text-dim);
  word-break: break-all;
  max-width: 360px;
}

/* Print: unlike the cut-sheet-only print rule above (scoped to hiding
   everything except one diagram, for the case where cut-sheet.js is used
   standalone inside box-builder.html), this page's whole point IS to
   print -- so instead of a hide-everything-then-reveal trick, screen-only
   chrome opts OUT via .no-print (see above) and everything else prints as
   laid out on screen, just recolored onto a plain page. */
@media print {
  /* The cut-sheet rule above ("body * { visibility:hidden }") is
     unconditional -- it fires on this page too since both rules live in
     the same stylesheet. Override it back to visible for everything under
     .build-plan; a 2-class selector already outranks that rule's 1-class
     one on specificity, so this wins regardless of source order. */
  .build-plan, .build-plan * {
    visibility: visible;
  }
  body { background: #ffffff; }
  .build-plan {
    color: #1a1a1a;
    background: #ffffff;
  }
  .build-plan h2, .build-plan h3 { color: #1a1a1a; }
  .build-plan .card {
    background: #ffffff;
    border-color: #ccc;
    color: #1a1a1a;
  }
  .build-plan .note {
    background: #f2f2f2;
    color: #333333;
    border-left-color: #999999;
  }
  .build-plan .spec-row { border-bottom-color: #dddddd; }
  .build-plan .spec-row .k { color: #555555; }
  /* Section 5's bracing/gusset tables (2026-07-28) reuse table.checklist,
     whose header colour is var(--text-dim) -- a light grey that's fine on the
     dark UI and invisible on paper. Recolour for print, same as .spec-row
     above. */
  .build-plan table.checklist {
    color: #1a1a1a;
    /* Undo the narrow-screen scroll container above: paper has no scrollbar,
       so a display:block table would silently clip the right-hand columns of
       the cut list. On paper it goes back to being a table and wraps. */
    display: table;
    overflow-x: visible;
  }
  .build-plan table.checklist th { color: #555555; }
  .build-plan table.checklist th,
  .build-plan table.checklist td { border-bottom-color: #dddddd; }
  .build-plan h4 { color: #1a1a1a; }
  .build-plan pre.export-box {
    background: #f7f7f7;
    color: #333333;
    border-color: #cccccc;
  }
  .build-plan .result-big { color: #b8452f; }
  /* Neutralize the cut-sheet-only-page absolute positioning set above --
     this page embeds the cut-sheet inline as one section among several,
     not as the sole page content. Same specificity reasoning as the
     visibility override above. */
  .build-plan .cut-sheet {
    position: static;
    width: auto;
    margin: 10px 0 0;
    padding: 12px;
    border-radius: var(--radius-sm);
  }
  .build-plan__section { page-break-inside: avoid; }
  .build-plan__cover { page-break-after: always; }
  .build-plan__page-break { page-break-before: always; }
}

footer.site {
  margin-top: 40px;
  color: var(--text-dim);
  font-size: var(--fs-footer);
}

.support-link {
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--panel-border);
}
.support-link a {
  color: var(--accent);
  text-decoration: underline;
}

.legal-links {
  margin: 8px 0 0;
  font-size: var(--fs-footer);
}
.legal-links a {
  color: var(--text-dim);
  text-decoration: underline;
}

/* ==========================================================================
   LANDING PAGE — hero, affiliate banners, tool showcase (index.html only)
   ========================================================================== */

.hero {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  padding: 56px 32px;
  margin-bottom: 32px;
  /* Layer order (first = topmost): vignette to keep headline legible over the
     lit workbench, a bottom-heavy darkening pass for the CTA/stats row, a
     cursor-tracked "shop light" sweep (--cursor-x/--cursor-y, written by
     hero-light.js on mousemove -- Fable review, 2026-07-12, idea A5;
     desktop-only, defaults to a fixed 50%/30% spot so it degrades to a
     static warm highlight with no JS/on touch), then the actual render.
     Falls back to the flat gradient look if --hero-image is ever set back
     to `none`. */
  background:
    radial-gradient(ellipse 80% 65% at 50% 30%, rgba(15,17,21,0.4) 0%, rgba(15,17,21,0.18) 45%, rgba(15,17,21,0.02) 75%),
    linear-gradient(180deg, rgba(15,17,21,0.05) 0%, rgba(15,17,21,0.35) 100%),
    radial-gradient(circle 220px at var(--cursor-x, 50%) var(--cursor-y, 30%), rgba(255, 200, 140, 0.22), transparent 70%),
    var(--hero-image),
    radial-gradient(circle at 20% 20%, rgba(255,106,61,0.35), transparent 55%),
    radial-gradient(circle at 80% 80%, rgba(255,106,61,0.18), transparent 50%),
    linear-gradient(160deg, #1a0f0a 0%, #0f1115 60%);
  background-size: cover, cover, cover, cover, auto, auto, auto;
  background-position: center, center, center, center 38%, 20% 20%, 80% 80%, 0% 0%;
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  border: 1px solid var(--panel-border);
  text-align: center;
}
.hero::before {
  content: '';
  position: absolute;
  inset: -40%;
  background: radial-gradient(circle, rgba(255,106,61,0.25) 0%, transparent 60%);
  animation: hero-pulse 6s ease-in-out infinite;
  pointer-events: none;
}
/* Hero intro video layer (hero-video.js, 2026-07-14): absolutely-positioned
   over the .hero background stack, plays once on desktop load, then
   crossfades out (opacity transition below -- duration must stay in sync
   with FADE_LEAD_S in hero-video.js) to reveal the untouched static hero.
   Inserted as .hero's first child so the positioned text/picker children
   paint above it; removed from the DOM entirely after the fade. */
.hero__video {
  position: absolute;
  inset: 0;
  opacity: 1;
  transition: opacity 1.2s ease;
  pointer-events: none;
}
.hero__video--fade { opacity: 0; }
.hero__video video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* same focal band as the .hero background-position for the photo layer */
  object-position: 50% 38%;
  display: block;
}
/* The video paints ABOVE all seven .hero background layers, including the
   two legibility gradients -- so this overlay carries copies of those exact
   two (the vignette + the bottom-heavy darkening pass) or the headline goes
   illegible during playback. Keep in sync with the .hero background stack. */
.hero__video::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 80% 65% at 50% 30%, rgba(15,17,21,0.4) 0%, rgba(15,17,21,0.18) 45%, rgba(15,17,21,0.02) 75%),
    linear-gradient(180deg, rgba(15,17,21,0.05) 0%, rgba(15,17,21,0.35) 100%);
}
@keyframes hero-pulse {
  0%, 100% { transform: scale(0.9); opacity: 0.6; }
  50% { transform: scale(1.15); opacity: 1; }
}
.hero__eyebrow {
  position: relative;
  display: inline-block;
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 12px;
}
.hero h1 {
  position: relative;
  font-size: 40px;
  line-height: 1.15;
  margin: 0 0 16px;
  font-weight: var(--fw-heavy);
  text-shadow: 0 2px 16px rgba(0,0,0,0.85), 0 1px 3px rgba(0,0,0,0.9);
}
@media (max-width: 640px) { .hero h1 { font-size: 28px; } }
.hero p.lede {
  position: relative;
  margin: 0 auto 28px;
  max-width: 560px;
  text-shadow: 0 1px 8px rgba(0,0,0,0.9);
}
.hero__cta {
  position: relative;
  display: inline-block;
  background: var(--accent);
  color: var(--button-text);
  font-weight: var(--fw-bold);
  font-size: var(--fs-med);
  text-decoration: none;
  padding: 16px 32px;
  border-radius: var(--radius-sm);
  animation: hero-cta-glow 2.4s ease-in-out infinite;
}
@keyframes hero-cta-glow {
  0%, 100% { box-shadow: 0 0 0 rgba(255,106,61,0); }
  50% { box-shadow: 0 0 28px rgba(255,106,61,0.55); }
}
.hero__cta:hover { filter: brightness(1.08); }

/* Vehicle-first hero picker (2026-07-14): inline make -> vehicle cascade
   that submits to my-vehicle.html?vehicle=<id>. Desktop-only by design —
   at the 640px breakpoint it swaps for the single .hero__cta--fallback
   link below (two selects in a phone hero is more tap-work than one big
   button; the lookup page opens on the same picker anyway). The fallback
   is also shown by hero-picker.js if the manifest fetch fails. */
.hero__picker {
  position: relative; /* above .hero::before, same as the other hero children */
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  max-width: 660px;
  margin: 0 auto;
}
.hero__picker select {
  flex: 1 1 200px;
  width: auto;
  min-width: 170px;
}
.hero__picker button {
  margin-top: 0;
  white-space: nowrap;
  /* same pulse as the CTA it replaces; gated in the MOTION POLICY block */
  animation: hero-cta-glow 2.4s ease-in-out infinite;
}
.hero__cta--fallback { display: none; }
.hero__browse {
  position: relative;
  margin: 14px 0 0;
  font-size: var(--fs-small);
}
.hero__browse a {
  color: var(--text-dim);
  text-decoration: underline;
}
.hero__browse a:hover { color: var(--text); }
@media (max-width: 640px) {
  .hero__picker { display: none; }
  .hero__cta--fallback { display: inline-block; }
}

.hero__stats {
  position: relative;
  display: flex;
  justify-content: center;
  gap: 32px;
  margin-top: 32px;
  flex-wrap: wrap;
}
.hero__stat { text-align: center; }
.hero__stat-num { display: block; font-size: 24px; font-weight: var(--fw-heavy); color: var(--accent); }
.hero__stat-label { font-size: var(--fs-badge); color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }

.affiliate-banner {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  background: linear-gradient(90deg, rgba(255,106,61,0.14), rgba(255,106,61,0.02));
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius-sm);
  padding: 14px 18px;
  margin-bottom: var(--card-gap);
}
.affiliate-banner__eyebrow {
  font-size: var(--fs-badge);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  background: var(--warn-bg);
  padding: 3px 8px;
  border-radius: var(--radius-pill);
}
.affiliate-banner__name { font-weight: var(--fw-bold); font-size: var(--fs-med); flex: 1; min-width: 120px; }
.affiliate-banner__cta {
  color: var(--accent);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-base);
  text-decoration: none;
  white-space: nowrap;
}
.affiliate-banner__cta:hover { text-decoration: underline; }
.affiliate-banner--hero { padding: 20px 24px; }
.affiliate-banner--footer { margin-top: 8px; margin-bottom: 0; }

/* Brand-spotlight treatment (Sundown Audio, 2026-07-31) -- a real logo +
   generated background for a brand that gave explicit written permission,
   in place of the plain text/gradient look. background-image is set inline
   per-banner (brand-specific asset) by affiliate-banners.js; this block only
   owns layout, the legibility overlay is baked into that inline gradient. */
.affiliate-banner--art {
  background-color: #0a0a0c;
  background-size: cover;
  background-position: center;
  border-color: rgba(255,255,255,0.12);
  min-height: 192px; /* 2026-07-31: doubled per Cory's ask -- reveals roughly
    2x more of the generated scene since the box is cover-scaled by width,
    not height, so a taller box just shows more of the same crop vertically. */
  position: relative;
  align-items: flex-end; /* eyebrow/CTA/disclosure sit as a caption bar along
    the bottom now that the box is tall enough to show real scenery above. */
  /* REAL BUG FIX 2026-08-26 (Cory spotted it on his phone: "the featured
     partner part is right underneath the sundown emblem"). Measured at 375px:
     the eyebrow wasn't just close to the logo, it OVERLAPPED it by 30px.
     Cause: `align-items: flex-end` above only bottom-aligns items WITHIN a
     flex line. At phone width the caption wraps to three lines (eyebrow / CTA /
     disclosure), and multi-line cross-axis placement is governed by
     `align-content`, which was never set -- so it defaulted to stretch and the
     whole caption block floated to the TOP of the box, straight onto the
     absolutely-positioned logo. Desktop never showed it because the caption
     fits on fewer lines there. */
  align-content: flex-end;
  /* The logo is position:absolute, so the flex layout cannot see it and has no
     reason to avoid it. Reserve its footprint explicitly: logo occupies
     16px(top) + 52px(height) = 68px, +24px breathing room = 92px. Measured
     result at 375px: 24px clear gap above the eyebrow (vs the 14px flex gap
     below it), so the eyebrow reads as the caption's label rather than as
     something attached to the logo. */
  padding-top: 92px;
  padding-bottom: 12px; /* 2026-07-31: nudged closer to the true bottom edge
    (was the shared 20px) for more visual gap from the now-top-left logo. */
}
.affiliate-banner__logo { height: 34px; width: auto; display: block; }
/* Corner-badge placement (2026-07-31, Cory's ask): pulled out of the normal
   flex row into a corner (top-left, moved from an initial top-right pass),
   bigger than the old inline size, with a drop-shadow instead of a
   background patch so it stays legible against whatever part of the photo
   ends up behind it. */
.affiliate-banner--art .affiliate-banner__logo {
  position: absolute;
  top: 16px;
  left: 20px;
  height: 52px;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.65));
}
.affiliate-banner--art .affiliate-banner__cta { color: #fff; }
.affiliate-banner--art .affiliate-banner__disclosure { color: rgba(255,255,255,0.55); }
/* FTC disclosure text, 2026-07-28 -- sits on the banner itself (order: 3 =
   always its own line below name/CTA) rather than requiring a click through
   to Terms of Use to find the actual "we earn a commission" wording. */
.affiliate-banner__disclosure { font-size: var(--fs-badge); color: var(--text-dim); width: 100%; order: 3; }

@property --card-wash {
  syntax: '<number>';
  inherits: false;
  initial-value: 0.82;
}

.tool-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--card-gap);
  margin-bottom: 32px;
}
.tool-card {
  --card-bg-image: none; /* set by a .tool-card--* modifier below */
  /* Real renders came back sharper/brighter than the "faint texture" brief
     (2026-07-10 review) -- pushed the mask higher than the hero's to
     compensate: rest ~11% image visible, hover ~20%. */
  --card-wash: 0.89;     /* how much the warm tint/photo is masked; lower = more visible */
  display: block;
  text-decoration: none;
  color: inherit;
  /* Layers (topmost first): a tinted wash that mostly hides the theme photo
     (this is the "really light" ask), the theme photo itself, then a warm
     base gradient that's visible even with no photo set yet -- this is what
     makes the card read as its own surface instead of flat --panel gray. */
  background:
    linear-gradient(rgba(23,26,33,var(--card-wash)), rgba(23,26,33,var(--card-wash))),
    var(--card-bg-image),
    linear-gradient(160deg, #201811 0%, #171a21 65%);
  background-color: var(--panel);
  background-size: cover, cover, cover;
  background-position: center, center, center;
  background-repeat: no-repeat, no-repeat, no-repeat;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 24px 20px;
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease, --card-wash 0.15s ease;
}
.tool-card:hover {
  --card-wash: 0.80; /* lets more of the theme photo show through on hover */
  border-color: var(--accent);
  transform: translateY(-3px);
  /* dark lift-shadow (depth) + an inset ring (reads as a 2px border with zero
     box-sizing reflow, unlike an actual border-width change: Fable review,
     2026-07-12) + a brighter, wider --accent glow (Cory asked for
     brighter/thicker, 2026-07-12) */
  box-shadow: 0 10px 24px rgba(0,0,0,0.35), 0 0 24px rgba(255,106,61,0.45), inset 0 0 0 1px var(--accent);
}

.tool-card--box-builder    { --card-bg-image: var(--card-img-box-builder); }
.tool-card--speaker-panel  { --card-bg-image: var(--card-img-speaker-panel); }
.tool-card--net-volume     { --card-bg-image: var(--card-img-net-volume); }
.tool-card--port-calculator{ --card-bg-image: var(--card-img-port-calculator); }
.tool-card--bandpass       { --card-bg-image: var(--card-img-bandpass); }
.tool-card--troubleshooting{ --card-bg-image: var(--card-img-troubleshooting); }
.tool-card--door-fitment    { --card-bg-image: var(--card-img-door-fitment); }
.tool-card--wiring          { --card-bg-image: var(--card-img-wiring); }
.tool-card--class-calculator{ --card-bg-image: var(--card-img-class-calculator); }
.tool-card--spl-meet-finder { --card-bg-image: var(--card-img-spl-meet-finder); }
.tool-card__icon { font-size: 32px; display: block; margin-bottom: 10px; }
.tool-card__icon-img {
  display: block;
  width: 44px;
  height: 44px;
  margin-bottom: 10px;
  border-radius: 9px;
  border: 1px solid var(--panel-border);
  background-color: var(--input-bg);
  background-image: var(--icon-img, none);
  background-size: cover;
  background-position: center;
}
.tool-card__icon-img--box-builder   { --icon-img: var(--icon-img-box-builder); }
.tool-card__icon-img--speaker-panel { --icon-img: var(--icon-img-speaker-panel); }
.tool-card__icon-img--net-volume    { --icon-img: var(--icon-img-net-volume); }
.tool-card__icon-img--port-calculator{ --icon-img: var(--icon-img-port-calculator); }
.tool-card__icon-img--bandpass      { --icon-img: var(--icon-img-bandpass); }
.tool-card__icon-img--troubleshooting{ --icon-img: var(--icon-img-troubleshooting); }
.tool-card__icon-img--door-fitment    { --icon-img: var(--icon-img-door-fitment); }
.tool-card__icon-img--wiring          { --icon-img: var(--icon-img-wiring); }
.tool-card__icon-img--class-calculator{ --icon-img: var(--icon-img-class-calculator); }
.tool-card__icon-img--spl-meet-finder { --icon-img: var(--icon-img-spl-meet-finder); }
/* Electrical Calculator has no dedicated icon art yet (added to the mobile
   home screen 2026-08-25, never had a web homepage tile at all) — reuses
   the Wiring icon as an honest placeholder until a real Higgsfield pass. */
.tool-card__icon-img--electrical      { --icon-img: var(--icon-img-wiring); }
.tool-card h3 { margin: 0 0 8px; }

/* Per-page banner strip: same photo as the tool's landing-page card, reused
   at full page width above the <h2> title. No text sits on top of it (the
   title stays as a plain heading below), so it only needs a bottom fade into
   --bg to avoid a hard edge — not the heavy legibility darkening .hero uses.
   Photo lives on ::before (Ken Burns zoom, Fable review 2026-07-12 idea A7)
   so the ::after fade gradient can stay static on top of it -- scaling the
   whole .page-banner element would drag the fade along with the photo. */
.page-banner {
  position: relative;
  height: 180px;
  border-radius: var(--radius);
  margin-bottom: 28px;
  border: 1px solid var(--panel-border);
  overflow: hidden;
}
.page-banner::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--banner-image, none);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  animation: page-banner-kenburns 22s ease-in-out infinite alternate;
}
.page-banner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(15,17,21,0) 55%, var(--bg) 100%);
}
@keyframes page-banner-kenburns {
  from { transform: scale(1); }
  to { transform: scale(1.06); }
}
/* F11 pilot (2026-07-12): box-builder's banner cinemagraph. Inserted as a
   real child element by banner-video.js -- NOT present in the static HTML,
   so mobile/reduced-motion visits never even have a <video src> to fetch.
   It paints between ::before (the static photo, now just an instant-load
   fallback under the video) and ::after (the bottom fade, which still needs
   to sit on top of the video the same way it does the photo) -- ::before/
   ::after are both position:absolute with z-index:auto, so a plain in-flow
   child sandwiches between them in DOM-order paint order with no z-index
   juggling needed. See banner-video.js for the desktop+motion gating. */
.page-banner__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.page-banner--box-builder    { --banner-image: var(--card-img-box-builder); }
.page-banner--speaker-panel  { --banner-image: var(--card-img-speaker-panel); }
.page-banner--net-volume     { --banner-image: var(--card-img-net-volume); }
.page-banner--port-calculator{ --banner-image: var(--card-img-port-calculator); }
.page-banner--bandpass       { --banner-image: var(--card-img-bandpass); }
.page-banner--troubleshooting{ --banner-image: var(--card-img-troubleshooting); }
.page-banner--door-fitment    { --banner-image: var(--card-img-door-fitment); }
.page-banner--wiring          { --banner-image: var(--card-img-wiring); }
.page-banner--class-calculator{ --banner-image: var(--card-img-class-calculator); }
.page-banner--spl-meet-finder { --banner-image: var(--card-img-spl-meet-finder); }
@media (max-width: 640px) {
  .page-banner { height: 120px; }
}
.tool-card p { margin: 0; color: var(--text-dim); font-size: var(--fs-base); }

.section-heading { font-size: var(--fs-h2); margin: 0 0 4px; }
.section-sub { color: var(--text-dim); font-size: var(--fs-base); margin: 0 0 20px; }

/* Small group subheading over each themed .tool-grid (landing page,
   2026-07-14) — the 10 cards themselves are untouched, just re-parented
   into 3 groups under these. */
.tool-group-heading {
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin: 0 0 12px;
}

/* ==========================================================================
   BOX TYPES EXPLAINED — landing-page strip (F9). Three cutaway illustrations
   with real visible photography (NOT the faint tool-card wash treatment --
   the whole point here is a beginner can actually read the box structure),
   each linking to its calculator. Own class instead of reusing .tool-card
   because the image needs to render at full strength, not masked to ~11%.
   ========================================================================== */
.boxtype-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--card-gap);
  margin-bottom: 32px;
}
.boxtype-card {
  display: block;
  text-decoration: none;
  color: inherit;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.boxtype-card:hover {
  border-color: var(--accent);
  transform: translateY(-3px);
  box-shadow: 0 10px 24px rgba(0,0,0,0.35), 0 0 24px rgba(255,106,61,0.45), inset 0 0 0 1px var(--accent);
}
.boxtype-card__img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  background-image: var(--boxtype-img, none);
  background-size: cover;
  background-position: center;
  background-color: var(--input-bg);
}
.boxtype-card--sealed   .boxtype-card__img { --boxtype-img: var(--boxtype-img-sealed); }
.boxtype-card--ported   .boxtype-card__img { --boxtype-img: var(--boxtype-img-ported); }
.boxtype-card--bandpass .boxtype-card__img { --boxtype-img: var(--boxtype-img-bandpass); }
.boxtype-card__body { padding: 16px 18px 20px; }
.boxtype-card__body h3 { margin: 0 0 8px; }
.boxtype-card__body p { margin: 0; color: var(--text-dim); font-size: var(--fs-base); }

/* ==========================================================================
   MY VEHICLE LOOKUP (my-vehicle.html) — the vehicle-first flow, 2026-07-14.
   Everything below is composed from existing tokens; the page otherwise
   reuses .card/.note/.spec-row/.badge/.grid-2/.tool-grid/.tool-card as-is.
   ========================================================================== */

/* Neutral confidence badge for reference-build source labels ("forum-
   sourced", "research note", ...) — a sourcing descriptor, not a
   good/bad verdict, so it deliberately avoids the ok/warn/bad colors. */
.badge-neutral {
  background: var(--note-bg);
  color: var(--text-dim);
  border: 1px solid var(--panel-border);
}

.mv-vehicle-header {
  margin: 24px 0 16px;
}
.mv-vehicle-header .section-sub {
  margin-bottom: 0;
}

/* Tier card inside "Recommended starting points" — a lighter inner surface
   (input-bg) so it reads as content within the section card, not a nested
   full card. */
.mv-tier {
  background: var(--input-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
}
.mv-tier .badge {
  margin-bottom: 6px;
}

/* Primary CTA of the build-your-own trio (Box Builder, pre-filled) —
   accent border at rest so it reads first without inventing a new card
   shape; hover behavior stays the shared .tool-card treatment. */
.tool-card--featured {
  border-color: var(--accent);
}

/* ==========================================================================
   TIP JAR — homepage support box (index.html only), between the tool grid
   and the Shop Ticket. The visible instance of the support ask; nav.js still
   appends the quiet footer link on every page (that one stays, this is the
   one people are meant to actually see). Deliberately narrower than the
   content column so it reads as a small aside, not another full section.
   Vocabulary is all borrowed, nothing invented: the tool-card's warm base
   gradient (sans photo, so it sits a touch warmer than the cards at rest —
   it's a personal ask, not a tool), the 44px icon-badge slot, and the Shop
   Ticket's mono stamp for the label. No looping animation and no hover-glow
   on the box itself (only the CTA is clickable; a glowing box would read as
   a banner ad, which is exactly what this must not be).
   ========================================================================== */
.tip-jar {
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; /* same stack as .fb-section */
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 8px 18px;
  max-width: 620px;
  margin: 0 auto;
  padding: 18px 20px;
  background: linear-gradient(160deg, #201811 0%, #171a21 65%); /* .tool-card warm base */
  background-color: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
}

/* Icon badge: same slot language as .tool-card__icon-img, drawn instead of rendered */
.tip-jar__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 9px;
  border: 1px solid var(--panel-border);
  background: var(--input-bg);
}
.tip-jar__coin { fill: var(--accent); }
.tip-jar__lid { fill: var(--text-dim); }
.tip-jar__glass {
  fill: rgba(230, 232, 235, 0.06);
  stroke: var(--text-dim);
  stroke-width: 1.4;
}

.tip-jar__label {
  display: block;
  margin-bottom: 4px;
  font-family: var(--font-mono);
  font-size: var(--fs-badge);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
}
.tip-jar__text {
  margin: 0;
  font-size: var(--fs-base);
  color: var(--text-dim);
  max-width: 46ch;
}
.tip-jar__text strong {
  color: var(--text);
  font-weight: var(--fw-semibold);
}

.tip-jar__action {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.tip-jar__cta {
  display: inline-block;
  background: var(--accent);
  color: var(--button-text);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  text-decoration: none;
  white-space: nowrap;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
}
.tip-jar__cta:hover { filter: brightness(1.08); } /* matches button:hover */
.tip-jar__hint {
  margin-top: 6px;
  font-size: var(--fs-badge);
  color: var(--text-dim);
}

@media (max-width: 640px) {
  .tip-jar {
    grid-template-columns: auto 1fr;
  }
  .tip-jar__action {
    grid-column: 1 / -1;
    align-items: flex-start;
  }
}

/* ==========================================================================
   SHOP TICKET — landing-page feedback/vehicle-request form. Replaces the
   duplicate "Recommended gear" banner that used to sit here (2026-07-12).
   Design chosen via a 3-direction fan-out + judge pass: torn work-order
   ticket beat a plain .card form and a SaaS-style glowing callout band on
   brand distinctiveness, with zero invented colors (built entirely from
   existing tokens + a system monospace stack). Wired by feedback-form.js to
   POST /api/feedback (Vercel function using Resend — see api/feedback.js).
   ========================================================================== */
.fb-section {
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
  overflow-x: hidden;
  padding: 8px 0 4px;
}

/* The ticket itself: rotated a hair, like it's sitting slightly askew on a clipboard */
.ticket {
  position: relative;
  max-width: var(--content-max-width);
  margin: 44px auto 24px;
  transform: rotate(-0.6deg);
  transition: transform 0.25s ease;
}
.ticket:focus-within {
  transform: rotate(0deg);
}
@media (prefers-reduced-motion: reduce) {
  .ticket {
    transition: none;
  }
}

/* Bulldog-clip detail pinning the ticket to the "board" — pure CSS, no image */
.ticket__clip {
  position: absolute;
  top: -20px;
  left: 50%;
  width: 74px;
  height: 34px;
  transform: translateX(-50%) rotate(-2deg);
  z-index: 2;
  border-radius: 6px 6px 3px 3px;
  background: linear-gradient(155deg, #4b5158 0%, #2b2f35 45%, #1a1d21 100%);
  box-shadow: 0 6px 10px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.ticket__clip::before {
  /* metal shine bar */
  content: "";
  position: absolute;
  top: 4px;
  left: 8px;
  right: 8px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.18);
}
.ticket__clip::after {
  /* center rivet */
  content: "";
  position: absolute;
  bottom: 6px;
  left: 50%;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  transform: translateX(-50%);
  background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.5), var(--accent-dim) 60%, #1a1d21 100%);
}

/* The "paper" — snipped top-right corner via clip-path, like a ticket corner cut off */
.ticket__paper {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 28px var(--card-pad) var(--card-pad);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
  clip-path: polygon(0 0, calc(100% - 22px) 0, 100% 22px, 100% 100%, 0 100%);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
/* Same hover-glow treatment as .tool-card (2026-07-12: matched on request).
   Inset ring instead of border-width, same reflow fix as .tool-card above. */
.ticket__paper:hover {
  border-color: var(--accent);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35), 0 0 24px rgba(255, 106, 61, 0.45), inset 0 0 0 1px var(--accent);
}

/* Stub row: ticket number, like a real work order header */
.ticket__stub {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 11px;
  color: var(--text-dim);
}
.ticket__stub-no {
  color: var(--accent);
  font-weight: var(--fw-bold);
}

/* Perforated tear line between the stub and the actual form */
.ticket__perf {
  position: relative;
  margin: 14px calc(-1 * var(--card-pad)) 18px;
  border-top: 2px dashed var(--panel-border);
}
.ticket__perf::before,
.ticket__perf::after {
  content: "";
  position: absolute;
  top: -6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--panel-border);
}
.ticket__perf::before {
  left: -6px;
}
.ticket__perf::after {
  right: -6px;
}

.ticket__title {
  margin: 0 0 6px;
  font-size: var(--fs-h2);
  font-weight: var(--fw-heavy);
  letter-spacing: 0.01em;
}
.ticket__subtitle {
  margin: 0;
  max-width: 56ch;
  color: var(--text-dim);
  font-size: var(--fs-base);
}

.ticket__form label:first-of-type {
  margin-top: 20px;
}

/* Honeypot (security audit M1) -- positioned off-screen rather than
   display:none/visibility:hidden, since some bots specifically skip fields
   with those styles; off-screen positioning still reads as "visible" to a
   naive form-filler while no real user can ever see or reach it. */
.ticket__hp {
  position: absolute;
  left: -9999px;
  top: -9999px;
}

/* Select styled like a stamped "check one" field: mono, uppercase, custom arrow */
.ticket__select {
  -webkit-appearance: none;
  appearance: none;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: var(--fw-semibold);
  background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
    linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
  background-position: right 18px center, right 13px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 36px;
}

/* Textarea rule, consistent with the existing select/input styling (no sitewide <textarea> rule existed before this) */
.ticket__textarea {
  width: 100%;
  min-height: 96px;
  padding: var(--control-pad);
  background: var(--input-bg);
  border: 1px solid var(--panel-border);
  color: var(--text);
  border-radius: var(--radius-sm);
  font-size: var(--fs-base);
  font-family: var(--font-body);
  line-height: 1.5;
  resize: vertical;
}
.ticket__textarea:focus {
  outline: none;
  border-color: var(--accent);
}

.ticket__optional {
  text-transform: none;
  letter-spacing: 0;
  font-weight: var(--fw-medium);
  color: var(--text-dim);
}

.ticket__submit-btn {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  transform: rotate(-1deg);
}
.ticket__submit-btn:active {
  transform: rotate(-1deg) scale(0.97);
}
.ticket__submit-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  filter: none;
  transform: rotate(-1deg);
}

.ticket__status {
  margin-top: 12px;
  font-size: var(--fs-small);
  color: var(--text-dim);
}
.ticket__status:empty {
  display: none;
}
.ticket__status--sending {
  color: var(--text-dim);
}
.ticket__status--success {
  color: var(--accent);
}
.ticket__status--error {
  color: var(--text);
  padding-left: 10px;
  border-left: 3px solid var(--accent-dim);
}

@media (max-width: 640px) {
  .ticket {
    margin: 32px auto 20px;
    transform: rotate(-0.35deg);
  }
  .ticket__clip {
    top: -16px;
    width: 58px;
    height: 28px;
  }
  .ticket__paper {
    padding: 24px 16px 16px;
    clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%);
  }
  .ticket__perf {
    margin-left: -16px;
    margin-right: -16px;
  }
  .ticket__stub {
    font-size: 10px;
  }
  .ticket__title {
    font-size: 19px;
  }
}

/* ==========================================================================
   STICKER WALL — decorative brand-name decals in the page margins only
   (never behind the hero/cards/banners — see .wrap z-index above). Text-only,
   not real logo artwork (no rights to reproduce brand marks). Opacity is
   capped hard per tier; this must never out-contrast real UI. Hidden below
   1200px since there's no margin space to hold it without colliding with
   content. Populated by stickers.js.
   ========================================================================== */

.sticker-wall {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  display: none;
}
@media (min-width: 1200px) {
  .sticker-wall { display: block; }
}

/* One per depth tier (populated by stickers.js), transformed by
   sticker-parallax.js on scroll -- far/mid/near move at different rates
   (0.15x/0.3x/0.5x) for real depth. inset:0 so children's existing
   left/top positioning (written against the old direct-child-of-wall
   layout) resolves identically. Fable review, 2026-07-12, idea A6. */
.sticker-layer {
  position: absolute;
  inset: 0;
}

.sticker {
  position: absolute;
  display: inline-block;
  padding: 0.42em 0.9em;
  border-radius: 11px;
  font-family: inherit;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1.15;
  white-space: nowrap;
  text-align: center;
  background: rgba(230,232,235,0.035);
  color: rgba(230,232,235,0.15);
  border: 1px solid rgba(230,232,235,0.10);
  box-shadow: none;
  transform: rotate(var(--r, 0deg));
}
.sticker--wrap-text { white-space: normal; max-width: 10.5em; }
.sticker--pill { border-radius: 999px; }
.sticker--angled { clip-path: polygon(6% 0%, 100% 0%, 94% 100%, 0% 100%); }

.sticker--accent {
  background: rgba(255,106,61,0.035);
  color: rgba(255,106,61,0.17);
  border-color: rgba(255,106,61,0.12);
}

/* Sizes/opacity bumped up 2026-07-31 per Cory's feedback -- wants these
   bigger and more pronounced than the original ghost-decal ceiling. */
.sticker--far { font-size: 13px; opacity: 0.24; }
.sticker--mid { font-size: 16px; opacity: 0.32; }
.sticker--near {
  font-size: 19px;
  opacity: 0.44;
  box-shadow: 0 1px 3px rgba(0,0,0,0.22);
  z-index: 2;
}

/* Real logo artwork (Sundown Audio only, permission on file 2026-07-31) --
   used in place of a text decal for brands that gave explicit written OK.
   Same opacity ceiling per tier as the text decals so it never reads as more
   prominent than a permitted mark should; the color logo is converted to a
   plain white silhouette (grayscale+invert) to match the ghost-outline look
   of every other decal here rather than popping as the one saturated color
   mark in the wall. */
.sticker--logo {
  padding: 0;
  background: none;
  border: none;
  box-shadow: none;
}
.sticker--logo img {
  display: block;
  width: auto;
  filter: grayscale(1) brightness(0) invert(1);
}
.sticker--far.sticker--logo img { height: 20px; }
.sticker--mid.sticker--logo img { height: 26px; }
.sticker--near.sticker--logo img { height: 34px; }

/* ==========================================================================
   BOX-REVEAL SCROLL-SCRUB (box-reveal-scrub.js) — Fable review, 2026-07-12,
   idea B1. The scroll-scrubbed camera push-in through the hero garage scene
   toward the finished box, built on the rig proven in the B1 phase-1 spike
   (Cory confirmed it feels smooth on a real device 2026-07-12). Desktop-only
   (matches the sticker-wall's existing >=1200px precedent -- 40KB/frame x 30
   frames is real weight, never ship it to mobile data) and skipped under
   prefers-reduced-motion; both cases fall back to a plain static poster
   image, never a blank gap.
   ========================================================================== */
.box-reveal-poster {
  margin: 0 0 32px;
}
.box-reveal-poster img {
  width: 100%;
  display: block;
  border-radius: var(--radius);
  border: 1px solid var(--panel-border);
}
.box-reveal-spacer {
  display: none;
}
@media (min-width: 1200px) {
  .box-reveal-poster { display: none; }
  .box-reveal-spacer { display: block; height: 350vh; position: relative; margin-bottom: 32px; }
}
@media (prefers-reduced-motion: reduce) {
  .box-reveal-spacer { display: none !important; }
  .box-reveal-poster { display: block !important; }
}
.box-reveal-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--bg);
}
#box-reveal-canvas {
  max-width: 92%;
  max-height: 85vh;
  width: auto;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--panel-border);
}
/* Neon-sign intro overlay (box-reveal-intro.js, 2026-07-14): plays once over
   the canvas when the stage first scrolls into view, then fades out to reveal
   the untouched frame-00 resting state. Absolutely positioned inside the
   sticky stage (a containing block) and centered -- the stage flex-centers
   the canvas, so the two centers coincide; box-reveal-intro.js pins the
   overlay's width/height to the canvas's real laid-out size. Border + radius
   mirror #box-reveal-canvas above so the handoff is seamless. Only ever
   exists on desktop + motion-allowed (the JS bails first; see MOTION POLICY). */
.box-reveal-video {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1; /* above the canvas, below .box-reveal-copy (z-index: 2) */
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--panel-border);
  background: #0f1115; /* matches the clip's near-black opening frame */
  opacity: 1;
  transition: opacity 0.6s ease;
  pointer-events: none;
}
.box-reveal-video--fade { opacity: 0; }
.box-reveal-video video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.box-reveal-copy {
  position: absolute;
  top: 12%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  text-align: center;
  pointer-events: none;
  transition: opacity 0.15s linear;
}
.box-reveal-copy__headline {
  margin: 0 0 8px;
  font-size: clamp(1.3rem, 3vw, 2.1rem);
  font-weight: 700;
  color: var(--text);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.65);
}
.box-reveal-copy__cue {
  margin: 0;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.65);
}

/* Cut list table (2026-08-11). The nesting diagram used to double as the parts
   list; this is the table a builder carries to the saw. Printable by intent —
   the plan page is meant to be printed, so borders stay visible on white. */
.bp-cutlist {
  width: 100%;
  border-collapse: collapse;
  margin: 0.75rem 0;
  font-size: 0.95rem;
}
.bp-cutlist th,
.bp-cutlist td {
  border: 1px solid var(--border, #444);
  padding: 0.45rem 0.55rem;
  text-align: left;
  vertical-align: top;
}
.bp-cutlist th {
  background: var(--surface-2, rgba(255, 255, 255, 0.04));
  font-weight: 600;
  white-space: nowrap;
}
/* Dimensions are the column people read at the saw — keep them on one line and
   in a face where 9 9/16" cannot be misread as 99/16". */
.bp-cutlist__dims {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  font-weight: 600;
}
.bp-cutlist__tick {
  text-align: center;
  font-size: 1.15rem;
  line-height: 1;
  width: 3.5em;
}
@media (max-width: 640px) {
  .bp-cutlist { font-size: 0.85rem; }
  .bp-cutlist th, .bp-cutlist td { padding: 0.3rem 0.35rem; }
}
@media print {
  .bp-cutlist th { background: #eee !important; }
  .bp-cutlist th, .bp-cutlist td { border-color: #000 !important; }
}

/* Three-view orthographic drawing (2026-08-11). Full width — it is the primary
   drawing on the plan, not a thumbnail beside another one. White ground because
   the plan page is printed. */
.bp-threeview {
  background: #fff;
  border: 1px solid var(--border, #444);
  border-radius: 6px;
  padding: 0.5rem;
  margin: 0.75rem 0;
  overflow-x: auto;
}
.bp-threeview__svg {
  display: block;
  width: 100%;
  height: auto;
  min-width: 30rem; /* below this the dimension text stops being legible */
}
@media print {
  .bp-threeview { border-color: #000 !important; break-inside: avoid; }
}

/* Assembly steps (2026-08-15). Replaces the old blank-rectangle strip: every step
   now carries a real drawing of the box at that moment, so the picture does the
   teaching and the text stays a caption plus a few craft lines. */
.bp-assembly-step { margin: 0 0 1.4rem; }
.bp-assembly-step__head { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .45rem; }
.bp-assembly-step__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.5rem; height: 1.5rem; border-radius: 50%;
  background: #1a1a1a; color: #fff; font-size: .8rem; font-weight: 700;
}
.bp-assembly-step__pic { max-width: 300px; margin: .3rem 0 .5rem; }
.bp-assembly-step__pic svg { width: 100%; height: auto; display: block; }
.bp-assembly-step__subs { margin: .2rem 0 0 .2rem; padding-left: 1.1rem; }
.bp-assembly-step__subs li { margin: .22rem 0; line-height: 1.45; }
@media print {
  .bp-assembly-step { break-inside: avoid; page-break-inside: avoid; }
}

/* "Before you drill" shop spec under the cut list (2026-08-16). The fastener and
   PPE spec already existed in the joint-spec section further down the page, but
   not on the sheet a builder actually carries to the garage. One block rather than
   a note on every row — seven rows repeating the same pilot size is noise. */
.bp-cutlist__shopspec {
  margin: .75rem 0 0; padding: .7rem .9rem;
  border: 1px solid var(--border, #d8d2c6); border-left: 3px solid #b8452f;
  border-radius: 3px; font-size: .88rem; line-height: 1.5;
}
.bp-cutlist__shopspec ul { margin: .35rem 0 0; padding-left: 1.1rem; }
.bp-cutlist__shopspec li { margin: .25rem 0; }
@media print {
  .bp-cutlist__shopspec { break-inside: avoid; page-break-inside: avoid; }
}
