/* ============================================================
 * Brand-website shared stylesheet.
 * Loaded on every published page at /style.css (per brand bucket).
 *
 * NOT bound to portal design tokens (per CLAUDE.md §17 boundary —
 * portal canonical primitives govern the admin UX; brand websites
 * use their own design vocabulary). Brand vars are injected into
 * <head> by server/lib/page-static.js as :root + [data-theme="dark"].
 *
 * Pure CSS — no preprocessor. Stylesheet ships verbatim to S3.
 * ============================================================ */

/* ---- Reset + base ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-sans, system-ui, -apple-system, 'Segoe UI', sans-serif);
  font-size: 16px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
}
img, svg { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
main { display: block; }

/* ---- Prose container — for naked HTML pages (Privacy / About / FAQ /
        blog-style pages that don't ship their own <section> chrome).
        Wrap content in <article class="prose-content">…</article> and
        get sensible defaults: max readable line length, padding,
        heading scale, paragraph rhythm, list / blockquote styling.
        Pages with their own full-width layout (Haleji landing, etc.)
        skip this wrapper and use their own <style> blocks. ---- */
.prose-content {
  max-width: 760px;
  margin: 0 auto;
  padding: 64px 24px 96px;
  color: var(--text);
}
.prose-content > h1 {
  font-size: clamp(28px, 4vw, 40px);
  font-weight: 700;
  line-height: 1.15;
  margin: 0 0 8px;
  color: var(--text);
  letter-spacing: -0.01em;
}
.prose-content > h2 {
  font-size: clamp(20px, 2.4vw, 24px);
  font-weight: 600;
  line-height: 1.25;
  margin: 40px 0 12px;
  color: var(--text);
}
.prose-content > h3 {
  font-size: 18px;
  font-weight: 600;
  margin: 28px 0 8px;
  color: var(--text);
}
.prose-content > p {
  font-size: 16px;
  line-height: 1.65;
  margin: 0 0 16px;
  color: var(--text);
}
.prose-content > p > em:first-child {
  color: var(--text-muted);
  font-size: 14px;
}
.prose-content > ul,
.prose-content > ol {
  font-size: 16px;
  line-height: 1.65;
  margin: 0 0 16px;
  padding-left: 24px;
  color: var(--text);
}
.prose-content > ul > li,
.prose-content > ol > li { margin-bottom: 6px; }
.prose-content > blockquote {
  border-left: 3px solid var(--border);
  padding-left: 16px;
  margin: 16px 0;
  color: var(--text-muted);
  font-style: italic;
}
.prose-content a {
  color: var(--brand-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.prose-content a:hover { text-decoration: none; }
.prose-content strong { color: var(--text); font-weight: 600; }
.prose-content hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 32px 0;
}

/* ---- Brand chrome — header ---- */
.brand-header {
  display: flex; align-items: center; gap: 32px;
  padding: 14px 32px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.brand-logo { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; font-size: 20px; color: var(--text); }
.brand-logo img { height: 28px; width: auto; }
.brand-nav { display: flex; align-items: center; gap: 24px; flex: 1; }
.brand-nav-item { font-size: 15px; color: var(--text); position: relative; padding: 6px 0; }
.brand-nav-item:hover { color: var(--brand-primary); }
.brand-nav-trigger {
  background: none; border: 0; padding: 6px 0; color: inherit; font-size: 15px;
  display: inline-flex; align-items: center; gap: 4px;
}
.brand-nav-menu {
  position: absolute; top: 100%; left: 0; min-width: 200px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 6px; display: none; box-shadow: 0 8px 24px rgba(0,0,0,0.08);
  z-index: 10;
}
.brand-nav-menu a { display: block; padding: 8px 12px; border-radius: 6px; font-size: 14px; }
.brand-nav-menu a:hover { background: var(--surface); }
.brand-nav-item--has-children:hover .brand-nav-menu,
.brand-nav-item--has-children:focus-within .brand-nav-menu { display: block; }

/* ---- Mega-menu panel ----
   Everything below is additive: a page published before this existed has a
   flat <a> list inside .brand-nav-menu and keeps rendering from the two
   rules above, which must not be touched.

   The panel opens on :hover / :focus-within alone. The shell does ship a
   little JS (page-static.js emits the theme scripts), but the menu must not
   depend on it — CSS opens it, script only improves it. */
.brand-nav-trigger .brand-nav-icon,
.brand-nav-item > .brand-nav-icon { margin-right: 6px; }
.brand-nav-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex: 0 0 18px;
  color: var(--brand-accent);
}
.brand-nav-icon img { width: 18px; height: 18px; object-fit: contain; }

/* The hover bridge. .brand-nav-menu is offset from the trigger by the gap
   below, and without a bridge the pointer crosses dead space on the way
   down and the panel closes under it. The padding belongs to the panel's
   own hover area, so the path is unbroken. */
.brand-nav-item--has-children { padding-bottom: 6px; }

.brand-nav-menu[data-cols] {
  padding: 14px;
  min-width: 260px;
  border-radius: 12px;
}
.brand-nav-groups { display: grid; gap: 4px 22px; }
.brand-nav-menu[data-cols="2"] .brand-nav-groups { grid-template-columns: repeat(2, minmax(210px, 1fr)); }
.brand-nav-menu[data-cols="3"] .brand-nav-groups { grid-template-columns: repeat(3, minmax(190px, 1fr)); }
.brand-nav-menu[data-cols="2"],
.brand-nav-menu[data-cols="3"] { min-width: 0; }

.brand-nav-group { min-width: 0; }
.brand-nav-group-title {
  font-family: var(--font-mono);
  font-size: 11px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-muted);
  padding: 8px 10px 6px;
}
/* Scoped with .brand-nav-menu so it outranks the legacy
   `.brand-nav-menu a` rule above (0,1,1) — which is display:block and
   otherwise pushes the icon onto its own line. The legacy rule must stay
   exactly as it is for pages published before the panel existed. */
.brand-nav-menu .brand-nav-link {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
}
.brand-nav-menu .brand-nav-link:hover { background: var(--surface); }
.brand-nav-link .brand-nav-icon { margin-top: 2px; }
.brand-nav-link-text { min-width: 0; }
.brand-nav-link-label {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  font-size: 14px; font-weight: 500; line-height: 1.3;
  color: var(--text);
}
.brand-nav-link-desc {
  display: block;
  margin-top: 2px;
  font-size: 12px; line-height: 1.45;
  color: var(--text-muted);
}
.brand-nav-badge {
  flex: 0 0 auto;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 9px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--brand-accent);
  border: 1px solid var(--brand-accent);
  border-radius: 3px;
  padding: 1px 5px;
}
.brand-nav-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 18px;
  margin-top: 10px; padding: 10px 10px 2px;
  border-top: 1px solid var(--border);
}
.brand-nav-foot-note {
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-muted);
}
.brand-nav-foot a {
  display: inline; padding: 0;
  font-size: 13px; font-weight: 600; white-space: nowrap;
  color: var(--brand-accent);
}
.brand-nav-foot a:hover { background: none; text-decoration: underline; }
.brand-header-right { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; }
.brand-cta {
  display: inline-flex; align-items: center;
  padding: 8px 16px; border-radius: 999px;
  background: var(--brand-primary); color: #fff;
  font-size: 14px; font-weight: 500;
  transition: opacity .15s;
}
.brand-cta:hover { opacity: .9; }
.brand-theme-toggle, .brand-lang-switcher {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 6px 10px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  font-size: 13px; color: var(--text-muted);
  cursor: pointer;
}

/* ---- Brand chrome — footer ---- */
.brand-footer {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 28px 32px;
  margin: 32px;
}
.brand-footer-top {
  display: flex; align-items: center; justify-content: space-between;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 24px;
}
.brand-footer-top-heading { font-size: 22px; font-weight: 600; color: var(--text); margin: 0; }
.brand-footer-body {
  display: grid;
  grid-template-columns: minmax(120px, 1fr) repeat(auto-fit, minmax(140px, 1fr));
  gap: 32px; align-items: start;
}
.brand-footer-logo img { height: 32px; }
.brand-footer-col-title {
  font-size: 12px; font-weight: 600; color: var(--brand-primary);
  text-transform: uppercase; letter-spacing: .04em;
  margin: 0 0 12px;
}
.brand-footer-col-link {
  display: block; font-size: 14px; color: var(--text-muted);
  padding: 4px 0;
}
.brand-footer-col-link:hover { color: var(--brand-primary); }
.brand-footer-bottom {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 24px; padding-top: 16px;
  border-top: 1px solid var(--border);
  font-size: 13px; color: var(--text-muted);
}
.brand-footer-social { display: inline-flex; gap: 8px; }
.brand-footer-social a {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 8px;
  color: var(--brand-primary);
  transition: background .15s;
}
.brand-footer-social a:hover { background: var(--bg); }
.brand-footer-social svg { width: 16px; height: 16px; }

/* ---- Social icons (.brand-social) ------------------------------------
        One list rendered by server/lib/website-social.js, consumed by the
        header (right | topbar) and the footer (bottom | body).

        The three .brand-footer-social rules ABOVE are deliberately left
        alone: they still serve pages published before this existed, whose
        markup carries that class. New markup emits .brand-social only, so
        the two never meet and there is no specificity contest. The `tile`
        declarations below reproduce those values exactly — tile is the
        default, and a republished footer must look untouched.

        Every colour resolves to a brand variable, so the icons track the
        operator's palette and both themes without a per-style dark rule. */
.brand-social { display: inline-flex; align-items: center; gap: 8px; }
.brand-social a {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 8px;
  color: var(--chrome-glyph);
  transition: background .15s, color .15s;
}
.brand-social a:hover { background: var(--bg); }
.brand-social svg { width: 16px; height: 16px; }

/* bare — a naked glyph, for the header, where a row of bordered boxes
   competes with the CTA. Resting --text-muted; hover --brand-primary
   rather than --brand-accent, matching .brand-footer-col-link:hover
   above. On the Amigo cream canvas the accent gold is 3.00:1 — it clears
   the 3:1 non-text bar for a glyph, but only just, and only on hover. */
.brand-social[data-style="bare"] a {
  width: auto; height: auto;
  border: 0; border-radius: 0;
  padding: 2px;
  background: none;
  color: var(--text-muted);
}
.brand-social[data-style="bare"] a:hover { background: none; color: var(--chrome-glyph); }
.brand-social[data-style="bare"] svg { width: 18px; height: 18px; }

/* The legacy footer class carries the SAME defect on every page published
   before .brand-social existed: --brand-primary glyphs at 2.82:1 on the
   dark canvas. One additive override, no rule above is touched. */
[data-theme="dark"] .brand-footer-social a { color: var(--chrome-glyph); }

/* solid — a filled disc. #fff literal, not a variable: the fill is always
   --brand-primary, so the glyph must contrast with that one colour and
   not with whichever canvas the list happens to sit on. */
/* solid — a filled disc. The glyph is --bg rather than a literal #fff so it
   stays legible against the lifted disc in dark mode; on light, --bg is the
   page canvas, which is what a knocked-out glyph should be anyway. */
.brand-social[data-style="solid"] a {
  border: 0; border-radius: 50%;
  background: var(--chrome-glyph);
  color: var(--bg);
}
.brand-social[data-style="solid"] a:hover { background: var(--chrome-glyph); opacity: .85; }

/* ---- Header utility bar (.brand-topbar) ----
        A thin band ABOVE .brand-header. --surface rather than --bg so it
        reads as a separate strip from the header below it, which is --bg.
        Matches the header's 32px inline padding so the icons line up with
        the CTA underneath. */
.brand-topbar {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 16px;
  padding: 6px 32px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* Footer `body` placement — the logo and the icon row share the first
   grid track. The wrapper is emitted ONLY for this placement (see
   renderFooter), so a footer without body socials keeps the bare logo as
   its grid item and every link column stays where it was. */
.brand-footer-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; }

/* ---- Hamburger menu (CSS-only via checkbox + label) ---- */
/* Hidden by default on every width — checkbox lives in the DOM but
   never visible; the label is the visible hamburger button and is
   hidden at desktop widths (see below). */
.brand-menu-checkbox { display: none; }
.brand-menu-burger { display: none; }

/* ---- Responsive ---- */
/* Breakpoint at 1024px because the in-portal preview iframe is
   constrained to 880px max-width at desktop device mode (canonical
   .ae-preview-stage[data-device="desktop"] > * rule). Below 760px
   the original layout collapsed via flex-wrap; we now offer a
   cleaner hamburger toggle for the whole < 1024px range. */
@media (max-width: 1023px) {
  .brand-header {
    position: relative;
    padding: 12px 16px;
    gap: 12px;
  }
  .brand-menu-burger {
    display: inline-flex; flex-direction: column; justify-content: center; gap: 4px;
    width: 36px; height: 36px;
    margin-left: auto;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    cursor: pointer;
    padding: 8px;
  }
  .brand-menu-burger span {
    display: block;
    height: 2px;
    background: var(--text);
    border-radius: 2px;
    transition: transform .15s ease, opacity .15s ease;
  }
  /* When checkbox is checked, hamburger morphs into an X */
  .brand-menu-checkbox:checked ~ .brand-menu-burger span:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
  }
  .brand-menu-checkbox:checked ~ .brand-menu-burger span:nth-child(2) { opacity: 0; }
  .brand-menu-checkbox:checked ~ .brand-menu-burger span:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
  }

  /* Nav + header-right hidden by default; revealed when checkbox is checked */
  .brand-header .brand-nav,
  .brand-header .brand-header-right {
    display: none;
    width: 100%;
    flex: 0 0 100%;
  }
  .brand-menu-checkbox:checked ~ .brand-nav,
  .brand-menu-checkbox:checked ~ .brand-header-right {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 12px 0;
    border-top: 1px solid var(--border);
    margin-top: 12px;
  }
  .brand-menu-checkbox:checked ~ .brand-nav .brand-nav-item,
  .brand-menu-checkbox:checked ~ .brand-nav .brand-nav-item--has-children {
    padding: 8px 4px;
    width: 100%;
  }
  /* Inside the burger panel the dropdown expands in place: it sheds the
     floating chrome it needs on desktop, and any column count flattens.
     display:block matters — the desktop rule only reveals the panel on
     :hover / :focus-within, which a touch device never fires, so submenu
     links were unreachable on a phone. Applies to legacy flat menus too,
     which had the same problem. */
  .brand-menu-checkbox:checked ~ .brand-nav .brand-nav-menu {
    display: block;
    position: static;
    min-width: 0;
    border: 0;
    box-shadow: none;
    background: none;
    padding: 0 0 0 8px;
  }
  .brand-menu-checkbox:checked ~ .brand-nav .brand-nav-menu .brand-nav-groups {
    grid-template-columns: 1fr;
  }
  .brand-menu-checkbox:checked ~ .brand-header-right { margin-left: 0; }
  .brand-menu-checkbox:checked ~ .brand-header-right .brand-cta {
    align-self: flex-start;
  }
  /* The panel turns .brand-header-right into a stretched column, which
     would stand the icon row on end. It is a row at every width. */
  .brand-menu-checkbox:checked ~ .brand-header-right .brand-social {
    flex-direction: row;
    align-self: flex-start;
  }
  /* Topbar tracks the header's mobile inline padding. It stays visible —
     it is one row of glyphs, and hiding it would lose the links entirely
     on the widths where the burger already hides everything else. */
  .brand-topbar { padding: 6px 16px; }
  /* Header needs flex-wrap so the revealed nav + header-right drop
     to new rows below the logo + hamburger. */
  .brand-header { flex-wrap: wrap; }

  .brand-footer { margin: 16px; padding: 20px; border-radius: 12px; }
  .brand-footer-top { flex-direction: column; align-items: flex-start; gap: 12px; }
  .brand-footer-body { grid-template-columns: 1fr; }
  .brand-footer-bottom { flex-direction: column; align-items: flex-start; gap: 12px; }
}

/* ---- Website blocks (.wb-*) ------------------------------------------
        The section archetype library. Operators insert these from the
        page editor; the markup lives in assets/js/ui/wpe-blocks.js and
        every rule below resolves to a brand variable, so changing the
        palette in Site Settings re-skins every block.

        Not bound to portal design tokens — see this file's header.
        Override anything here from Site Settings → Custom CSS, which
        loads after this stylesheet.

        One container (1140px), one inline padding (24px), one type
        scale (13/15/17/20/28/36/44). Breakpoints: 1100 (3-up → 2-up),
        820 (two-column stacks), 720 (single column).             ---- */

.wb {
  padding-block: 88px;
  overflow-wrap: anywhere;
}
.wb-container {
  max-width: 1140px;
  margin-inline: auto;
  padding-inline: 24px;
}
.wb-media {
  width: 100%;
  max-width: 100%;
  border-radius: 14px;
  display: block;
  /* Every one of these resolves to the pre-v1.122.0 value when no control
     has been touched, so a hero published before these existed renders
     byte-identically. See the .wb-media-opt block for why they are custom
     properties rather than descendant selectors. */
  aspect-ratio:    var(--wb-media-ratio, auto);
  height:          var(--wb-media-h, auto);
  object-fit:      var(--wb-media-fit, cover);
  object-position: var(--wb-media-pos, 50% 50%);
}

/* ---- Image sizing controls (.wb-media-opt) ----
   Four separate switches — shape, fit, focal point, exact height — and
   `data-edit` is single-valued, so each needs its own element. They nest as
   display:contents wrappers around the <img>: present in the tree for
   inheritance, absent from layout entirely, exactly as
   .wb-carousel-media does for its own fit switch below.

   They set CUSTOM PROPERTIES rather than styling .wb-media directly, and
   that is load-bearing. The split and full hero styles already set
   height/object-fit on .wb-media at (0,3,0); a descendant selector from
   these wrappers would land at (0,2,0) and lose, so fit and focal point —
   the two controls that matter MOST where the image is cropped to fill —
   would silently do nothing in exactly those two styles. Inherited custom
   properties are not subject to specificity at all, so one declaration in
   each rule below picks them up.

   An explicit height wins over aspect-ratio by CSS rule, which is what
   makes the numeric field an override of the shape preset rather than a
   competitor to it. */
.wb-media-opt { display: contents; }

.wb-media-opt[data-shape="16x9"] { --wb-media-ratio: 16 / 9; }
.wb-media-opt[data-shape="4x3"]  { --wb-media-ratio: 4 / 3; }
.wb-media-opt[data-shape="1x1"]  { --wb-media-ratio: 1 / 1; }
.wb-media-opt[data-shape="3x4"]  { --wb-media-ratio: 3 / 4; }

.wb-media-opt[data-fit="contain"] { --wb-media-fit: contain; }
/* Fit-the-whole-image only reads as deliberate with room around it; without
   this a letterboxed photo sits on whatever is behind, which looks broken
   rather than intentional.
   The mat needs the EDGE as well as the fill. --surface on --bg is
   #f9fafb on #ffffff — a 1.02:1 step, which is no step at all: the mat was
   invisible, so setting a height or a shape appeared to grow a pale empty
   block around the picture instead of framing it. The hairline is the same
   raised-surface vocabulary .wb-card and .wb-tier already use, so a framed
   image reads as one of the page's surfaces rather than a new idea.
   An inset ring rather than a border: a real border on the <img> would
   grow the box the height field just set. */
.wb-media-opt[data-fit="contain"] .wb-media {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border);
}

.wb-media-opt[data-focal="top"]    { --wb-media-pos: 50% 0%; }
.wb-media-opt[data-focal="bottom"] { --wb-media-pos: 50% 100%; }
.wb-media-opt[data-focal="left"]   { --wb-media-pos: 0% 50%; }
.wb-media-opt[data-focal="right"]  { --wb-media-pos: 100% 50%; }

/* Type scale — size AND weight AND colour differentiate the three levels. */
.wb-eyebrow {
  margin: 0 0 14px;
  font-family: var(--font-mono);
  font-size: 13px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-muted);
}
.wb-h1 {
  margin: 0 0 18px;
  font-family: var(--font-display);
  font-size: clamp(32px, 4.4vw, 44px); font-weight: 700; line-height: 1.08;
  letter-spacing: -.02em;
  color: var(--text);
}
.wb-h2 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: clamp(28px, 3.4vw, 36px); font-weight: 700; line-height: 1.15;
  letter-spacing: -.015em;
  color: var(--text);
}
.wb-lede {
  font-size: 17px; line-height: 1.6;
  color: var(--text-muted);
}
.wb-lede > p { margin: 0 0 18px; }
.wb-lede > p:last-child { margin-bottom: 0; }

/* Buttons — committed pill radius, accent at full strength. */
.wb-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 14px 28px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 15px; font-weight: 600;
  text-decoration: none;
  transition: opacity .15s ease;
  max-width: 100%;
}
.wb-btn:hover { opacity: .88; }
.wb-btn-primary {
  background: var(--brand-primary);
  color: var(--wb-on-brand);
}
.wb-btn-invert {
  background: var(--wb-on-brand);
  color: var(--brand-primary);
}

/* The Buttons block carries its style as a per-instance ATTRIBUTE rather
   than a class: serializeRepeat rebuilds every repeat instance from the
   first one's outerHTML, so a class that varies per button silently
   reverts on the operator's first keystroke.

   The three classes above are kept exactly as they are — every hero and
   call-to-action published before the Buttons block carries one of them
   inline, and a page published yesterday must render tomorrow. These
   rules are additive: nothing already published has a data-variant. */
.wb-btn-slot[data-variant="primary"] > .wb-btn {
  background: var(--brand-primary);
  color: var(--wb-on-brand);
}
.wb-btn-slot[data-variant="invert"] > .wb-btn {
  background: var(--wb-on-brand);
  color: var(--brand-primary);
}
.wb-btn-slot[data-variant="outline"] > .wb-btn {
  background: transparent;
  border-color: var(--wb-accent);
  color: var(--wb-accent);
}

/* --- hero-split --- */
/* A hero is the first thing on the page and carries the most weight,
   so it gets a little more room above than the standard section rhythm. */
.wb-hero-split { padding-block: 104px 88px; }
.wb-hero-split-inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 56px;
  align-items: center;
}
.wb-hero-split-copy { min-width: 0; }
.wb-hero-split-copy .wb-lede { margin-bottom: 30px; }
.wb-hero-split-media { min-width: 0; }

/* Style options. Everything below is attribute-qualified — a hero
   published before the style family existed carries no data-style /
   -align / -overlay and so matches none of it. Do not lift any of these
   into the base rules above. */

/* split — two halves, no container, media flush to the edge. */
.wb-hero-split[data-style="split"] { padding-block: 0; }
.wb-hero-split[data-style="split"] .wb-hero-split-inner {
  max-width: none;
  padding-inline: 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0;
  align-items: stretch;
  min-height: 68svh;
}
.wb-hero-split[data-style="split"] .wb-hero-split-copy {
  align-self: center;
  padding: 72px clamp(24px, 5vw, 72px);
}
.wb-hero-split[data-style="split"] .wb-hero-split-media { position: relative; border-radius: 0; }
.wb-hero-split[data-style="split"] .wb-media {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  /* var() with the old literal as fallback — the fit and focal controls
     reach this rule by inheritance, which a descendant selector could not
     do at this specificity. Untouched heroes get `cover` / centre exactly
     as before. aspect-ratio is inert here: width and height are both
     explicit. */
  object-fit: var(--wb-media-fit, cover);
  object-position: var(--wb-media-pos, 50% 50%);
  border-radius: 0;
}

/* full — the image is the background, the copy sits over it. */
.wb-hero-split[data-style="full"] {
  position: relative;
  display: grid;
  align-items: center;
  min-height: 78svh;
  padding-block: 120px;
  overflow: hidden;
}
/* The INNER must stay static. Position it and it becomes the containing
   block for the absolute media below, which then sizes to the container
   instead of the section — and, being out of flow, paints over the copy.
   The copy carries the stacking instead. */
.wb-hero-split[data-style="full"] .wb-hero-split-inner {
  grid-template-columns: minmax(0, 720px);
  gap: 0;
}
.wb-hero-split[data-style="full"] .wb-hero-split-copy {
  position: relative;
  z-index: 1;
}
.wb-hero-split[data-style="full"] .wb-hero-split-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: 0;
}
.wb-hero-split[data-style="full"] .wb-media {
  width: 100%; height: 100%;
  object-fit: var(--wb-media-fit, cover);
  object-position: var(--wb-media-pos, 50% 50%);
  border-radius: 0;
}
.wb-hero-split[data-style="full"] .wb-h1,
.wb-hero-split[data-style="full"] .wb-eyebrow { color: var(--wb-on-brand); }
.wb-hero-split[data-style="full"] .wb-lede { color: var(--wb-on-brand); opacity: .88; }

/* ---- Section height ----
   The switch cannot live on the <section>: its data-edit slot is already
   taken by the style family, and a value typed into a descendant cannot
   travel back up — custom properties inherit downward only. So the carrier
   sits inside and the section reads it with :has(), the same decoupling the
   dark-tone rule below documents.

   Only split and full have a height to change; `beside` is sized by its
   content and is deliberately left alone. */
.wb-hero-split[data-style="split"]:has(.wb-media-opt[data-hero-height="short"])  .wb-hero-split-inner { min-height: 52svh; }
.wb-hero-split[data-style="split"]:has(.wb-media-opt[data-hero-height="tall"])   .wb-hero-split-inner { min-height: 88svh; }
.wb-hero-split[data-style="full"]:has(.wb-media-opt[data-hero-height="short"])  { min-height: 58svh; }
.wb-hero-split[data-style="full"]:has(.wb-media-opt[data-hero-height="tall"])   { min-height: 94svh; }

/* Alignment — applies in every style. */
.wb-hero-split[data-align="center"] .wb-hero-split-inner { justify-items: center; }
.wb-hero-split[data-align="center"] .wb-hero-split-copy { text-align: center; }

/* Overlay — a scrim between the image and the copy. Only reaches the copy
   in "full", where the media is the backdrop; in the other styles it just
   darkens the picture, which is a legitimate thing to want.
   The wrapper takes .wb-media's own 14px radius and clips to it, so the
   scrim can't paint square corners over a rounded photo. Split and full
   flatten it back to 0 (their rules win on specificity). */
.wb-hero-split-media[data-overlay="light"],
.wb-hero-split-media[data-overlay="strong"] {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
}
.wb-hero-split-media[data-overlay="light"]::after,
.wb-hero-split-media[data-overlay="strong"]::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.wb-hero-split-media[data-overlay="light"]::after {
  background: color-mix(in srgb, var(--wb-scrim) 28%, transparent);
}
.wb-hero-split-media[data-overlay="strong"]::after {
  background: color-mix(in srgb, var(--wb-scrim) 58%, transparent);
}

/* --- hero-video --- */
/* Poster and video are stacked in the same layer: the poster paints
   first, the video covers it once it has data, and on mobile / reduced
   motion the video is removed so the poster is what ships. */
.wb-hero-video {
  position: relative;
  display: grid;
  align-items: center;
  min-height: 78svh;
  padding-block: 120px;
  overflow: hidden;
}
.wb-hero-video-media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.wb-hero-video-poster,
.wb-hero-video-el {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Poster and video share the crop: a poster focused differently from the
     footage it stands in for would jump on first frame. */
  object-fit: var(--wb-media-fit, cover);
  object-position: var(--wb-media-pos, 50% 50%);
  display: block;
}
.wb-hero-video-media[data-overlay="light"]::after,
.wb-hero-video-media[data-overlay="strong"]::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.wb-hero-video-media[data-overlay="light"]::after {
  background: color-mix(in srgb, var(--wb-scrim) 32%, transparent);
}
.wb-hero-video-media[data-overlay="strong"]::after {
  background: color-mix(in srgb, var(--wb-scrim) 60%, transparent);
}
.wb-hero-video-inner {
  position: relative;
  z-index: 1;
  display: grid;
  gap: 0;
}
/* Both values get a real rule. "Centred" is the authored default, but the
   operator must be able to switch back to left and see it take effect —
   an option that only works in one direction is a broken control. */
.wb-hero-video-inner[data-align="center"] {
  justify-items: center;
  text-align: center;
}
.wb-hero-video-inner[data-align="left"] {
  justify-items: start;
  text-align: left;
}
.wb-hero-video-eyebrow,
.wb-hero-video-h1 { color: var(--wb-on-brand); }
.wb-hero-video-sub {
  color: var(--wb-on-brand);
  opacity: .88;
  max-width: 620px;
  margin-bottom: 30px;
}
/* A background loop is decoration. Anyone who has asked their OS to stop
   animation gets the poster, and so does every narrow screen — this is
   also what stops a phone downloading several MB of video on cellular. */
@media (prefers-reduced-motion: reduce) {
  .wb-hero-video-el { display: none; }
}

/* --- hero-bento --- */
.wb-hero-bento { padding-block: 96px 88px; }
.wb-hero-bento-inner {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: 48px;
  align-items: center;
}
.wb-hero-bento-copy { min-width: 0; }
.wb-hero-bento-copy .wb-lede { margin-bottom: 30px; }
.wb-hero-bento-copy[data-align="center"] { text-align: center; }
.wb-bento-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  min-width: 0;
}
.wb-bento-tile {
  min-width: 0;
  padding: 20px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  overflow: hidden;
}
/* The asymmetry that makes it a bento rather than a plain grid. It lives
   here because every repeat instance is markup-identical by construction. */
.wb-bento-tile:nth-child(1) { grid-column: span 2; }
/* With the first tile double-width, an EVEN number of tiles always leaves
   the last one alone on its own row. Widening it closes the gap and holds
   for any count the operator ends up with — odd counts already fill. */
.wb-bento-tile:last-child:nth-child(even) { grid-column: span 2; }
/* The two figures are a deliberate mosaic: the feature tiles are ~1.36x the
   others (see the :nth-child rule below). The scale control drives ONE
   number and derives the other, so an operator can make the mosaic bigger or
   smaller but cannot make it ragged. */
.wb-bento-img {
  width: 100%;
  height: var(--wb-bento-tile-h, 132px);
  object-fit: var(--wb-media-fit, cover);
  object-position: var(--wb-media-pos, 50% 50%);
  border-radius: 10px;
  display: block;
  margin-bottom: 14px;
}
.wb-bento-tile:nth-child(1) .wb-bento-img,
.wb-bento-tile:last-child:nth-child(even) .wb-bento-img {
  height: calc(var(--wb-bento-tile-h, 132px) * 1.364);
}
.wb-media-opt[data-tile-size="compact"] { --wb-bento-tile-h: 104px; }
.wb-media-opt[data-tile-size="tall"]    { --wb-bento-tile-h: 172px; }
.wb-bento-title { margin: 0 0 6px; font-size: 17px; }
.wb-bento-body {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-muted);
}
.wb-bento-body > p { margin: 0; }

/* --- metric-tiles --- */
.wb-metrics-head { margin-bottom: 28px; }
.wb-metrics-status {
  margin: 6px 0 0;
  font-family: var(--font-mono);
  font-size: 12px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-muted);
}
.wb-metrics {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
}
.wb-metric {
  min-width: 0;
  padding: 20px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: 12px;
  background: var(--bg);
}
.wb-metric-label {
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-muted);
}
.wb-metric-value {
  margin: 10px 0 4px;
  font-family: var(--font-mono);
  font-size: clamp(28px, 3vw, 36px); font-weight: 700; line-height: 1;
  color: var(--text);
}
.wb-metric-sub { font-size: 13px; line-height: 1.45; color: var(--text-muted); }
/* State is per instance and arrives as an edit value, so it selects here. */
.wb-metric[data-state="warning"] { border-left-color: var(--wb-state-warn); }
.wb-metric[data-state="warning"] .wb-metric-value { color: var(--wb-state-warn); }
.wb-metric[data-state="alert"] { border-left-color: var(--wb-state-alert); }
.wb-metric[data-state="alert"] .wb-metric-value { color: var(--wb-state-alert); }

/* --- audience-cards --- */
.wb-aud-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 24px;
}
.wb-aud-card {
  min-width: 0;
  display: flex; flex-direction: column;
  padding: 32px 30px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--bg);
}
.wb-aud-title { margin: 0 0 20px; }
.wb-aud-list { list-style: none; margin: 0 0 24px; padding: 0; flex: 1; }
.wb-aud-item {
  position: relative;
  padding: 10px 0 10px 26px;
  font-size: 15px; line-height: 1.5;
  color: var(--text);
  border-bottom: 1px solid var(--border);
}
.wb-aud-item:last-child { border-bottom: 0; }
/* The tick is drawn, not typed: a character would land in the operator's
   editable text and could be deleted by accident. */
.wb-aud-item::before {
  content: "";
  position: absolute;
  left: 4px; top: 17px;
  width: 9px; height: 5px;
  border-left: 2px solid var(--wb-accent);
  border-bottom: 2px solid var(--wb-accent);
  transform: rotate(-45deg);
}
.wb-aud-cta { align-self: flex-start; }

/* --- spec-table --- */
.wb-spec-inner { max-width: 860px; }
.wb-spec-heading { margin-bottom: 26px; }
.wb-spec-list { margin: 0; }
.wb-spec-row {
  display: grid;
  grid-template-columns: minmax(140px, 220px) minmax(0, 1fr);
  gap: 16px 28px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}
.wb-spec-row:last-child { border-bottom: 0; }
.wb-spec-key {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-muted);
  align-self: center;
}
.wb-spec-val { margin: 0; font-size: 15px; line-height: 1.5; color: var(--text); }

/* --- ticker --- */
.wb-ticker { padding-block: 0; }
.wb-tick-rail {
  overflow: hidden;
  border-block: 1px solid var(--border);
}
.wb-ticks {
  display: flex;
  align-items: center;
  gap: 40px;
  padding: 14px 24px;
  white-space: nowrap;
}
.wb-tick { display: inline-flex; align-items: baseline; gap: 10px; }
.wb-tick-label {
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-muted);
}
.wb-tick-value {
  font-family: var(--font-mono);
  font-size: 15px; font-weight: 700;
  color: var(--wb-accent);
}
/* Scrolling is opt-in and never traps content: the strip reads the same
   whether it moves or not, which is why reduced-motion stops the animation
   rather than hiding the element — hiding would take the content with it. */
.wb-tick-rail[data-motion="scroll"] .wb-ticks {
  width: max-content;
  animation: wb-tick-scroll 32s linear infinite;
}
.wb-tick-rail[data-motion="scroll"]:hover .wb-ticks { animation-play-state: paused; }
@keyframes wb-tick-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .wb-tick-rail[data-motion="scroll"] .wb-ticks { animation: none; width: auto; }
  .wb-tick-rail[data-motion="scroll"] { overflow-x: auto; }
}

/* --- feature-grid numbering --- */
/* A CSS counter, so every repeat instance stays markup-identical. A typed
   number would be an edit value the operator has to renumber by hand every
   time a card moves. */
.wb-cards[data-numbering="numbered"] .wb-cards-inner { counter-reset: wb-feature; }
.wb-cards[data-numbering="numbered"] .wb-card { counter-increment: wb-feature; }
.wb-cards[data-numbering="numbered"] .wb-card::before {
  content: counter(wb-feature, decimal-leading-zero);
  display: block;
  margin-bottom: 12px;
  font-family: var(--font-mono);
  font-size: 12px; letter-spacing: .16em;
  color: var(--wb-accent);
}

/* --- cta-band --- */
.wb-cta-band { background: var(--brand-primary); }
.wb-cta-band-inner { text-align: center; }
.wb-cta-band-heading { color: var(--wb-on-brand); }
.wb-cta-band-sub {
  margin: 0 auto 30px;
  max-width: 620px;
  font-size: 17px; line-height: 1.6;
  color: var(--wb-on-brand);
  opacity: .82;
}
.wb-cta-band-sub > p { margin: 0 0 12px; }
.wb-cta-band-sub > p:last-child { margin-bottom: 0; }

/* --- buttons --- */
/* One row of buttons, wrapping rather than overflowing: the labels are
   operator-written and a two-word button next to an eight-word one must
   still fit its container (CLAUDE.md §15). The row element carries the
   alignment control and the list is what actually lays the buttons out,
   because data-edit is single-valued and the list's is already "repeat".
   The instance wrapper is display:contents for the same reason
   .wb-media-opt is: a control carrier must not occupy layout, or the gap
   below would measure between wrappers instead of between buttons. */
.wb-buttons-list {
  display: flex; flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}
.wb-btn-slot { display: contents; }
.wb-buttons-row[data-align="center"] .wb-buttons-list { justify-content: center; }
.wb-buttons-row[data-align="right"] .wb-buttons-list { justify-content: flex-end; }

/* --- prose --- */
/* Prose is reading material rather than a display band — it sits
   tighter than the sections around it so a run of text doesn't
   acquire display-sized gaps between its parts. */
/* Every fallback below is the literal this rule carried before v1.122.0, so
   a text block published earlier — which has none of the wrappers and so
   sets none of the properties — renders byte-identically. */
.wb-prose { padding-block: var(--wb-prose-pad, 64px); }
.wb-prose-inner {
  max-width: var(--wb-prose-w, 760px);
  margin-inline: auto;
  padding-inline: 24px;
  font-size: var(--wb-prose-fs, 17px); line-height: 1.7;
  color: var(--wb-prose-color, var(--text));
  text-align: var(--wb-prose-align, left);
}

/* ---- Text block controls ----
   Custom properties again, for the same reason the hero uses them: several
   of these have to reach .wb-prose-inner past rules that already target it,
   and inheritance is not subject to specificity.

   COLOUR IS THE ONE TO READ CAREFULLY. Brand and accent map to the LIFTED
   variants, not to --brand-primary / --brand-accent. The comment above
   .wb[data-tone="dark"] records why: brand-primary as text on a dark
   surface lands about 2.1:1. --chrome-glyph and --wb-accent are the same
   colours already corrected for that, per theme and per tone, so a text
   block set to "Brand" stays legible in light, in dark, and inside a
   dark-tone band without the operator checking three times. A free hex
   picker could not do this, which is why the control offers none. */
.wb-media-opt[data-align="center"] { --wb-prose-align: center; }
.wb-media-opt[data-align="right"]  { --wb-prose-align: right; }

.wb-media-opt[data-color="muted"]  { --wb-prose-color: var(--text-muted); }
.wb-media-opt[data-color="brand"]  { --wb-prose-color: var(--chrome-glyph); }
.wb-media-opt[data-color="accent"] { --wb-prose-color: var(--wb-accent); }

/* Headings scale WITH the body rather than staying fixed: a 24px body under
   a 28px h2 reads as a mistake. em keeps the ratio the block was designed
   with (28/17 and 20/17). */
.wb-media-opt[data-size="small"]  { --wb-prose-fs: 15px; }
.wb-media-opt[data-size="large"]  { --wb-prose-fs: 19px; }
.wb-media-opt[data-size="xlarge"] { --wb-prose-fs: 22px; }

.wb-media-opt[data-width="narrow"] { --wb-prose-w: 580px; }
.wb-media-opt[data-width="wide"]   { --wb-prose-w: 980px; }
.wb-media-opt[data-width="full"]   { --wb-prose-w: 100%; }

.wb-media-opt[data-space="compact"] { --wb-prose-pad: 32px; }
.wb-media-opt[data-space="roomy"]   { --wb-prose-pad: 104px; }

/* ---- Text block image ----
   A separate layout element, not a display:contents wrapper: this one has
   to BE the grid when the image sits beside the copy. Published text blocks
   have no wrapper at all and so match none of this. */
.wb-prose-layout { display: flex; flex-direction: column; gap: 28px; }
/* Switched off means GONE, not an empty frame (Step 0.5 marker 6). It also
   leaves the reading order clean for a screen reader. */
.wb-prose-layout[data-pos="none"] .wb-prose-figure { display: none; }
.wb-prose-figure { margin: 0; max-width: var(--wb-prose-w, 760px); margin-inline: auto; padding-inline: 24px; width: 100%; }
/* Stated rather than implied. Above IS what the base flex column already
   does, but a choice that works only by coincidence is one an edit to the
   base silently breaks — and website-blocks.test.js fails any choice value
   with no rule of its own, which is how this was caught. */
.wb-prose-layout[data-pos="above"] .wb-prose-figure { order: 0; }
.wb-prose-layout[data-pos="below"] .wb-prose-figure { order: 2; }

/* Side by side. auto-fit + minmax so the two columns become one on a narrow
   screen instead of forcing a horizontal scrollbar. */
.wb-prose-layout[data-pos="left"],
.wb-prose-layout[data-pos="right"] {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  align-items: start;
  gap: 40px;
  max-width: 1140px;
  margin-inline: auto;
  padding-inline: 24px;
}
.wb-prose-layout[data-pos="right"] .wb-prose-figure { order: 2; }
/* In a column the reading-width cap is the column, so release it — leaving
   760px inside a 480px track would just centre a narrow ribbon. */
.wb-prose-layout[data-pos="left"] > *,
.wb-prose-layout[data-pos="right"] > * { max-width: none; padding-inline: 0; }
.wb-prose-inner > h2 {
  margin: 40px 0 14px;
  font-size: 1.647em; font-weight: 700; line-height: 1.2;
  letter-spacing: -.015em;
}
.wb-prose-inner > h2:first-child { margin-top: 0; }
.wb-prose-inner > h3 { margin: 30px 0 10px; font-size: 1.176em; font-weight: 600; }
.wb-prose-inner > p  { margin: 0 0 18px; }
.wb-prose-inner > ul,
.wb-prose-inner > ol { margin: 0 0 18px; padding-left: 24px; }
.wb-prose-inner > ul > li,
.wb-prose-inner > ol > li { margin-bottom: 8px; }
.wb-prose-inner a {
  color: var(--wb-accent);
  text-decoration: underline; text-underline-offset: 2px;
}

/* --- shared section head --- */
.wb-head {
  max-width: 680px;
  margin: 0 auto 48px;
  text-align: center;
}
.wb-head-sub { margin: 0; }

/* --- feature-grid --- */
.wb-h3 {
  margin: 0 0 8px;
  font-size: 20px; font-weight: 600; line-height: 1.3;
  color: var(--text);
}
.wb-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}
/* .wb-cards is the grid on every page published before the numbering option
   existed — its cards are direct children. Newer markup nests them in
   .wb-cards-inner so the wrapper has a free data-edit slot for the choice,
   and only THEN does .wb-cards stop being the grid. Removing this rule
   would silently stack every existing feature grid into one column. */
.wb-cards[data-numbering] { display: block; }
.wb-cards-inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}
.wb-card {
  min-width: 0;
  padding: 28px 26px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
  transition: border-color .15s ease;
}
.wb-card:hover { border-color: var(--wb-accent); }
/* ---- Card media: glyph, image, or nothing (per card) ----
   The image occupies EXACTLY the glyph's box — same 44px square, same 14px
   radius. With a per-card switch a grid can legitimately mix the two, and
   two different media heights in one row of cards reads as a broken render
   rather than a deliberate mix. Sharing the box is what makes mixing safe.

   The switch WRAPS both branches, which is also what lets the publish
   pre-flight skip the unused one: lib/placeholders.js strips the subtree of
   a switch in an off state, so a card set to Glyph does not block publish on
   the image slot it is not using. */
.wb-card-media[data-media="icon"]  .wb-card-figure { display: none; }
.wb-card-media[data-media="image"] .wb-card-icon   { display: none; }
.wb-card-media[data-media="none"]  > *             { display: none; }
.wb-card-figure { margin: 0; }
.wb-card-img {
  width: 44px; height: 44px;
  margin-bottom: 18px;
  display: block;
  border-radius: 14px;
  background: var(--surface);
  object-fit: var(--wb-media-fit, cover);
  object-position: var(--wb-media-pos, 50% 50%);
}

.wb-card-icon {
  width: 44px; height: 44px;
  margin-bottom: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 14px;
  background: var(--surface);
  color: var(--wb-accent);
  font-size: 20px; line-height: 1;
}
.wb-card-body {
  font-size: 15px; line-height: 1.6;
  color: var(--text-muted);
}
.wb-card-body > p { margin: 0 0 10px; }
.wb-card-body > p:last-child { margin-bottom: 0; }

/* --- stat-band --- */
.wb-stat-band { background: var(--surface); }
.wb-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 40px 24px;
  text-align: center;
}
.wb-stat { min-width: 0; }
.wb-stat-value {
  font-family: var(--font-mono);
  font-size: clamp(36px, 4vw, 44px); font-weight: 700; line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--wb-accent);
}
.wb-stat-label {
  margin-top: 10px;
  font-size: 15px; line-height: 1.5;
  color: var(--text-muted);
}

/* --- logo-strip --- */
.wb-logo-strip { padding-block: 56px; }
/* Text below the logos. Attribute-qualified via :has() so a strip published
   before this control existed carries no attribute, matches nothing, and
   keeps its text on top. Reordering rather than restructuring: the markup
   order stays lead → sub → logos, so the reading order for a screen reader
   is unchanged and only the visual order moves. */
.wb-logo-strip:has([data-textpos="bottom"]) .wb-container {
  display: flex;
  flex-direction: column;
}
.wb-logo-strip:has([data-textpos="bottom"]) .wb-logos { order: -1; margin-bottom: 28px; }
.wb-logo-strip:has([data-textpos="bottom"]) .wb-logo-strip-lead { margin-bottom: 0; }
.wb-logo-strip-lead { text-align: center; margin-bottom: 28px; }
.wb-logo-strip-sub {
  margin: -6px 0 0;
  font-size: 14px;
  color: var(--text-muted);
}
.wb-logos {
  display: flex; flex-wrap: wrap;
  align-items: center; justify-content: center;
  gap: 24px 48px;
}
.wb-logo {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 180px;
}
/* Height cap so one oversized upload cannot set the row height
   (CLAUDE.md §15 — containers must contain their content). */
.wb-logo-img {
  max-width: 100%; max-height: 40px;
  width: auto; height: auto;
  object-fit: contain;
  opacity: .72;
  transition: opacity .15s ease;
}
.wb-logo-img:hover { opacity: 1; }

/* --- columns container ---
   The only block that holds other blocks. It draws no border and no
   background of its own: grouping is whitespace, so a column of cards
   doesn't read as a card inside a card. Track counts are named
   explicitly rather than auto-fit — the operator picked a number, and
   auto-fit would silently hand back fewer. minmax(0, 1fr) for the same
   reason the 3-up grids use it: a long unbroken string must not push a
   track past its share. Step-downs live with the other breakpoints. */
/* FLEX, not grid, and the reason is the per-column width control: Grid
   cannot express "column 1 is 70% and column 2 takes the rest" without a
   per-column track definition, which a static stylesheet cannot generate.
   Flex can say exactly that in two rules. */
.wb-columns-grid {
  display: flex;
  flex-wrap: nowrap;
  gap: 24px;
  align-items: start;
}
/* No width set → share whatever is left, equally with the other unset
   columns. This is the pre-existing behaviour, so every container
   published before the width control renders exactly as it did. */
/* Section width. The container matches every other block at 1140px by
   default — a row that ignored the site's measure would be the only
   section on the page that did. Wide and Full are opt-in, reusing the
   prose block's vocabulary.

   Read from the control element as a DIRECT CHILD, never :has(): a Form
   block dropped into a column carries its own data-width, and a :has()
   here would let that silently widen the whole row. Same class of leak
   as the tone rule above. Padding is kept even at Full so text never
   touches the viewport edge. */
.wb-media-opt[data-width="wide"] > .wb-columns-grid { max-width: 1440px; }
.wb-media-opt[data-width="full"] > .wb-columns-grid { max-width: none; }

.wb-col { min-width: 0; flex: 1 1 0; }
/* Width set → be that percentage. flex-shrink stays 1 so the gap deficit
   is absorbed PROPORTIONALLY: 70% + 30% + one 24px gap comes out as a
   true 70/30 of the content width instead of overflowing by 24px.
   The selector reads the style attribute because clearing the field
   REMOVES the declaration rather than blanking it (see the data-edit=
   "length" contract in page-edit-form.js) — so its presence is exactly
   the auto/explicit distinction, with no second attribute to keep in
   sync. */
.wb-col[style*="--wb-col-w"] { flex: 0 1 var(--wb-col-w); }
.wb-col > .wb-nested + .wb-nested { margin-top: 24px; }

/* A container the operator inserted but never filled renders as NOTHING,
   not as 176px of section padding around empty tracks. An empty band on a
   live page reads as a broken render; this is the same "empty states are
   placeholders, not visible rows" rule the rest of the design canon
   applies. The editor's form view still shows the block and its columns,
   which is where it gets filled. */
.wb-columns:not(:has(.wb-nested)) { display: none; }

/* The same rule one tier down, for the named slots a hero or a call to
   action carries. An operator who removes the button from a hero must get
   a hero with no button — not a hero with an empty band where one was.
   :not(:has()) rather than :empty because an edit leaves the slot holding
   whitespace, which :empty would read as filled. */
.wb-slot:not(:has(.wb-nested)) { display: none; }

/* A block inside a column is still a .wb, so it would otherwise re-apply
   the 88px section padding its container already provides and re-impose
   the 1140px container width inside a ~260px track. Both resets are
   written at (0,2,0) DELIBERATELY: the rules they have to beat
   (.wb padding-block at the 820/720 breakpoints, .wb-container
   padding-inline at 720) sit LATER in this file, so a (0,1,0) selector
   here would lose below 820px. */
.wb.wb-nested { padding-block: 0; }
.wb-nested > .wb-container { padding-inline: 0; max-width: none; }

/* --- pricing-3col --- */
.wb-tiers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
  align-items: stretch;
}
.wb-tier {
  display: flex; flex-direction: column;
  min-width: 0;
  padding: 32px 28px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
}
.wb-tier-name {
  font-size: 15px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-muted);
}
.wb-tier-tagline {
  margin-bottom: 14px;
  font-size: 13px; line-height: 1.45;
  color: var(--text-muted);
}
.wb-tier-price {
  margin-top: 12px;
  font-size: clamp(36px, 3.6vw, 44px); font-weight: 700; line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--text);
}
.wb-tier-period {
  margin-top: 6px;
  font-size: 13px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-muted);
}
.wb-tier-features {
  margin: 26px 0 28px;
  padding: 0;
  list-style: none;
}
.wb-tier-feature {
  padding: 11px 0;
  border-bottom: 1px solid var(--border);
  font-size: 15px; line-height: 1.5;
  color: var(--text);
}
.wb-tier-feature:last-child { border-bottom: 0; }
.wb-tier-cta { margin-top: auto; width: 100%; }
/* The middle plan reads as the recommended one — a flat 3-up with no
   focal point looks unfinished. The emphasis lives HERE rather than in
   the instance markup because serializeRepeat rebuilds every instance
   from the first one's template, so a per-instance class would revert
   on the operator's first keystroke. Operators retune or move it from
   Site Settings → Custom CSS. */
.wb-tier:nth-child(2) {
  border-color: var(--wb-accent);
  box-shadow: 0 0 0 1px var(--wb-accent);
}
.wb-tier:nth-child(2) .wb-tier-cta {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
  color: var(--wb-on-brand);
}
.wb-btn-outline {
  background: transparent;
  border-color: var(--wb-accent);
  color: var(--wb-accent);
}

/* --- faq-accordion --- */
.wb-faq-inner { max-width: 820px; }
.wb-faq-heading { margin-bottom: 32px; text-align: center; }
.wb-faq-list { border-top: 1px solid var(--border); }
.wb-faq-item { border-bottom: 1px solid var(--border); }
/* Closed state is designed, not left to the browser: the default
   disclosure triangle is removed and replaced by a chevron that
   rotates on open (Step 0.5 marker 7). */
.wb-faq-q {
  position: relative;
  display: block;
  padding: 22px 44px 22px 0;
  font-size: 17px; font-weight: 600; line-height: 1.4;
  color: var(--text);
  cursor: pointer;
  list-style: none;
  transition: color .15s ease;
}
.wb-faq-q::-webkit-details-marker { display: none; }
.wb-faq-q::after {
  content: "";
  position: absolute; top: 50%; right: 6px;
  width: 9px; height: 9px;
  margin-top: -6px;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform .18s ease, border-color .15s ease;
}
.wb-faq-q:hover { color: var(--wb-accent); }
.wb-faq-q:hover::after { border-color: var(--wb-accent); }
.wb-faq-q:focus-visible {
  outline: 2px solid var(--wb-accent);
  outline-offset: 2px;
  border-radius: 14px;
}
.wb-faq-item[open] .wb-faq-q { color: var(--wb-accent); }
.wb-faq-item[open] .wb-faq-q::after {
  transform: rotate(-135deg);
  margin-top: -2px;
  border-color: var(--wb-accent);
}
.wb-faq-a {
  padding: 0 44px 24px 0;
  font-size: 17px; line-height: 1.65;
  color: var(--text-muted);
}
.wb-faq-a > p { margin: 0 0 12px; }
.wb-faq-a > p:last-child { margin-bottom: 0; }

/* --- carousel ---------------------------------------------------------
   CSS-only. A published page is a static file and the sanitizer strips
   <script>, so movement is the visitor's: swipe, drag or trackpad, with
   scroll-snap doing the landing. No autoplay, no arrows, no dots — those
   all need script. The peeking next slide plus a thin scrollbar are the
   "there is more" affordance.

   Three operator options, each a data-* attribute written by a
   data-edit="choice" dropdown in the form view:
     [data-style] on .wb-carousel      filmstrip | slider | grid
     [data-align] on .wb-carousel-frame  center  | left
     [data-size]  on .wb-carousel-rail   sm | md | lg
   Size stays meaningful in every style — it never becomes a dead
   control (Step 9 item 5).                                          --- */
.wb-carousel-head { max-width: 680px; margin: 0 0 40px; }
.wb-carousel-sub  { margin: 0; }

.wb-carousel-rail {
  /* Scroll viewport. padding-bottom reserves room for the scrollbar so
     it never sits on top of a caption. */
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 14px;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
  /* Bleed to the viewport edge on narrow screens so a slide can sit
     flush rather than stranded inside the container padding. */
  margin-inline: calc(-1 * var(--wb-rail-bleed, 0px));
  padding-inline: var(--wb-rail-bleed, 0px);
}
.wb-carousel-track {
  display: flex;
  gap: 20px;
  width: max-content;
  min-width: 100%;
}
.wb-slide {
  flex: 0 0 auto;
  min-width: 0;
  margin: 0;
  scroll-snap-align: start;
  width: 320px;
}
.wb-slide-img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 14px;
  background: var(--surface);
}
.wb-slide-cap {
  margin-top: 12px;
  font-size: 15px; line-height: 1.5;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* Alignment. Centring the track only bites when the slides are narrower
   than the rail; otherwise flex-start is what a scroll container needs. */
.wb-carousel-frame[data-align="center"] .wb-carousel-head { margin-inline: auto; text-align: center; }
.wb-carousel-frame[data-align="center"] .wb-carousel-track { justify-content: center; }
.wb-carousel-frame[data-align="left"]   .wb-carousel-head { margin-inline: 0; text-align: left; }
.wb-carousel-frame[data-align="left"]   .wb-carousel-track { justify-content: flex-start; }

/* Slide size — per style, because "large" means something different when
   one slide fills the frame than when six are in a row. */
.wb-carousel-rail[data-size="sm"] .wb-slide { width: 220px; }
.wb-carousel-rail[data-size="md"] .wb-slide { width: 320px; }
.wb-carousel-rail[data-size="lg"] .wb-slide { width: 460px; }

/* Slider — one at a time, centred in the frame, snapping to the middle.
   `slider-auto` is the same layout that advances itself; the width each
   rule sets is also published as --wb-slide-w so the animation below can
   step by exactly one slide rather than guessing a percentage. Adding the
   second selector changes nothing for pages already published as
   `slider` — they still match the first. */
.wb-carousel[data-style="slider"] .wb-slide,
.wb-carousel[data-style="slider-auto"] .wb-slide { scroll-snap-align: center; }
.wb-carousel[data-style="slider"] .wb-carousel-rail[data-size="sm"] .wb-slide,
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail[data-size="sm"] .wb-slide { width: min(520px, 82vw); }
.wb-carousel[data-style="slider"] .wb-carousel-rail[data-size="md"] .wb-slide,
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail[data-size="md"] .wb-slide { width: min(720px, 88vw); }
.wb-carousel[data-style="slider"] .wb-carousel-rail[data-size="lg"] .wb-slide,
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail[data-size="lg"] .wb-slide { width: min(920px, 92vw); }
.wb-carousel[data-style="slider"] .wb-slide-img,
.wb-carousel[data-style="slider-auto"] .wb-slide-img { aspect-ratio: 16 / 9; }
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail[data-size="sm"] { --wb-slide-w: min(520px, 82vw); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail[data-size="md"] { --wb-slide-w: min(720px, 88vw); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail[data-size="lg"] { --wb-slide-w: min(920px, 92vw); }

/* ---- Image fit (.wb-carousel-media[data-fit]) ----
   The slide box is a fixed shape, so `cover` crops every image to it —
   correct for a uniform filmstrip, wrong when the picture matters. The
   wrapper is display:contents, so it carries the attribute without
   existing in the layout at all: the rail stays a direct flex/grid child
   of the frame exactly as before. Pages published before this exists have
   no wrapper, match nothing here, and keep cropping. */
.wb-carousel-media { display: contents; }
.wb-carousel-media[data-fit="contain"] .wb-slide-img { object-fit: contain; }

/* ---- Auto-advance (data-style="slider-auto") ----
   A published page runs NO JavaScript — it is a static file on S3 and the
   sanitizer strips <script> — so this is a pure CSS animation. The track
   steps one slide at a time and ping-pongs back rather than jumping, which
   avoids the hard reset a forward-only loop would show.

   The step distance must be exact, so it is computed from the slide width
   the rules above publish plus the 20px track gap; a percentage would drift,
   because the track is n*width + (n-1)*gap, not n*width.

   The slide COUNT cannot be read in CSS, so :has() sets --wb-n per count.
   Two to eight slides animate; outside that range --wb-n stays 1, the
   transform resolves to translateX(0) and nothing moves. That is a
   deliberate silent no-op, not a failure.

   Accessibility, stated plainly: WCAG 2.2.2 wants a pause control for
   anything moving longer than five seconds. Without JavaScript there is no
   button to offer, so this pauses on hover, on focus-within and on :active
   (which covers a finger held on the rail), and prefers-reduced-motion
   disables it outright. A touch user who does not hold the rail still has
   no control — which is why this is opt-in and not the slider default. */
@keyframes wb-carousel-advance {
  to { transform: translateX(calc(-1 * (var(--wb-n) - 1) * (var(--wb-slide-w, 720px) + 20px))); }
}
.wb-carousel[data-style="slider-auto"] .wb-carousel-track {
  --wb-n: 1;
  animation-name: wb-carousel-advance;
  animation-duration: calc(var(--wb-n) * 4s);
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-fill-mode: both;
}
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(2):last-child) { --wb-n: 2; animation-timing-function: steps(1, end); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(3):last-child) { --wb-n: 3; animation-timing-function: steps(2, end); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(4):last-child) { --wb-n: 4; animation-timing-function: steps(3, end); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(5):last-child) { --wb-n: 5; animation-timing-function: steps(4, end); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(6):last-child) { --wb-n: 6; animation-timing-function: steps(5, end); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(7):last-child) { --wb-n: 7; animation-timing-function: steps(6, end); }
.wb-carousel[data-style="slider-auto"] .wb-carousel-track:has(> :nth-child(8):last-child) { --wb-n: 8; animation-timing-function: steps(7, end); }
/* Hand-over to the first-party behaviour script. It sets data-js="on" only
   once it has actually built the controls, so this rule can never strand a
   carousel: no script, no attribute, CSS animation stands. */
.wb-carousel[data-js="on"] .wb-carousel-track { animation: none; }
.wb-carousel-ctrls {
  display: flex; justify-content: center; gap: 8px;
  margin-top: 18px;
}
.wb-carousel-btn {
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg); color: var(--text);
  font-size: 15px; line-height: 1;
  transition: background .15s, border-color .15s;
}
.wb-carousel-btn:hover { background: var(--surface); border-color: var(--chrome-glyph); }
.wb-carousel-btn:focus-visible { outline: 2px solid var(--chrome-glyph); outline-offset: 2px; }

.wb-carousel[data-style="slider-auto"] .wb-carousel-rail:hover .wb-carousel-track,
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail:focus-within .wb-carousel-track,
.wb-carousel[data-style="slider-auto"] .wb-carousel-rail:active .wb-carousel-track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .wb-carousel[data-style="slider-auto"] .wb-carousel-track { animation: none; }
}

/* Grid — nothing scrolls. The rail stops being a viewport and the track
   stops being a row, so scroll-snap and max-content must both be undone. */
.wb-carousel[data-style="grid"] .wb-carousel-rail {
  overflow: visible;
  padding-bottom: 0;
  scroll-snap-type: none;
  margin-inline: 0;
  padding-inline: 0;
}
.wb-carousel[data-style="grid"] .wb-carousel-track {
  display: grid;
  width: auto;
  grid-template-columns: repeat(auto-fit, minmax(var(--wb-grid-min, 260px), 1fr));
}
.wb-carousel[data-style="grid"] .wb-slide { width: auto; }
.wb-carousel[data-style="grid"] .wb-carousel-rail[data-size="sm"] { --wb-grid-min: 180px; }
.wb-carousel[data-style="grid"] .wb-carousel-rail[data-size="md"] { --wb-grid-min: 260px; }
.wb-carousel[data-style="grid"] .wb-carousel-rail[data-size="lg"] { --wb-grid-min: 360px; }

/* --- Section tone -----------------------------------------------------
   A tone is a scoped custom-property override, NOT a set of new rules.
   Custom properties inherit, so re-pointing them here re-skins every
   descendant without a single block rule knowing that tone exists.

   Two things this rule must keep doing:

     1. --bg is the RAISED colour. In this layer var(--bg) is only ever
        .wb-card and .wb-tier — raised cards, never a canvas. The section
        paints its own background instead. Point --bg at the canvas and
        every card disappears into the section behind it.

     2. --wb-accent is re-derived. The existing lift lives under
        [data-theme="dark"], which does NOT match a dark-tone section in a
        light-theme page — brand-primary as TEXT there lands about 2.1:1.

   Two selectors because the switch and the styled element are decoupled:
   data-edit is single-valued, so a block whose <section> slot is already
   taken (hero-split, carousel) carries the switch on a descendant. Either
   way the SECTION is what gets styled.

   .wb-columns is excluded from the :has() half because it is the one
   block that CONTAINS other blocks: a dark block dropped into column 1
   would otherwise satisfy :has() on the container and darken the whole
   band, column 2 included. The container carries its own tone switch in
   its section slot, so it matches the first selector directly and needs
   the second not at all.

   :not([data-slot] *) is the same exclusion generalised, and it is why a
   hero can hold blocks at all. A hero's own switch sits on -copy, so the
   :has() half must keep matching it; but a dark block dropped into the
   hero's slot must NOT darken the hero around it. Matching only switches
   that are not inside a slot separates the two exactly. The .wb-columns
   exclusion stays as well: a legacy .wb-col carries no data-slot for a
   selector to see.                                                     */
.wb[data-tone="dark"],
.wb:not(.wb-columns):has([data-tone="dark"]:not([data-slot] *)) {
  background: var(--tone-dark-canvas);
  --bg:         var(--tone-dark-raised);
  --surface:    var(--tone-dark-inset);
  --text:       var(--tone-dark-text);
  --text-muted: var(--tone-dark-muted);
  --border:     var(--tone-dark-border);
  --wb-accent:  color-mix(in srgb, var(--brand-accent) 70%, var(--tone-dark-lift));
  --wb-state-warn:  var(--wb-state-warn-lift);
  --wb-state-alert: var(--wb-state-alert-lift);
}

[data-theme="dark"] .wb[data-tone="dark"],
[data-theme="dark"] .wb:not(.wb-columns):has([data-tone="dark"]:not([data-slot] *)) {
  background: var(--tone-dark-canvas-on-dark);
  --bg: var(--tone-dark-inset);
}

/* The 3-up grids drop to 2-up here. This has to name the column count
   explicitly: lowering the auto-fit floor would make three tracks MORE
   likely to fit, not fewer. minmax(0, 1fr) rather than 1fr so a long
   unbroken string can't push a track past its share. */
@media (max-width: 1100px) {
  .wb-cards,
  .wb-cards-inner,
  .wb-tiers,
  .wb-metrics,
  .wb-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* 3-up and 4-up containers drop to 2-up. This deliberately OVERRIDES an
     operator's per-column percentages: a 70/10/10/10 split is a decision
     about a wide canvas, and it stops meaning anything in 900px. Written
     at (0,3,0) so it beats the explicit-width rule whatever the order. */
  .wb-columns-grid[data-cols="3"],
  .wb-columns-grid[data-cols="4"] { flex-wrap: wrap; }
  .wb-columns-grid[data-cols="3"] > .wb-col,
  .wb-columns-grid[data-cols="4"] > .wb-col { flex: 1 1 calc(50% - 12px); }
}
@media (max-width: 820px) {
  .wb { padding-block: 64px; }
  .wb-hero-split-inner { grid-template-columns: 1fr; gap: 36px; }
  /* Split stacks: copy first, then the image as a fixed-height band. The
     column count is named explicitly — lowering an auto-fit floor would
     fit MORE tracks, not fewer. */
  .wb-hero-split[data-style="split"] .wb-hero-split-inner {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .wb-hero-split[data-style="split"] .wb-hero-split-copy { padding: 56px 24px; }
  .wb-hero-split[data-style="split"] .wb-hero-split-media { min-height: 320px; }
  .wb-hero-bento-inner { grid-template-columns: 1fr; gap: 36px; }
  .wb-head { margin-bottom: 36px; }
  .wb-logos { gap: 20px 32px; }
}
@media (max-width: 720px) {
  .wb { padding-block: 52px; }
  .wb-container { padding-inline: 20px; }
  .wb-prose-inner { padding-inline: 20px; }
  .wb-btn { width: 100%; }
  .wb-cards,
  .wb-cards-inner { grid-template-columns: 1fr; }
  .wb-stats { grid-template-columns: 1fr; gap: 32px; }
  .wb-metrics { grid-template-columns: 1fr; }
  /* The key/value pairs stack rather than squeeze into two narrow columns. */
  .wb-spec-row { grid-template-columns: 1fr; gap: 4px; }
  .wb-logo { max-width: 140px; }
  .wb-tiers { grid-template-columns: 1fr; }
  /* Every container is single-column on a phone, whatever the operator
     chose — side-by-side stops meaning anything below one track, and so
     do the percentages. */
  .wb-columns-grid[data-cols] { flex-direction: column; }
  .wb-columns-grid[data-cols] > .wb-col { flex: 0 0 auto; width: 100%; }
  .wb-faq-q { font-size: 16px; padding-right: 36px; }
  .wb-faq-a { font-size: 16px; padding-right: 0; }
  /* Let the rail run to the viewport edge so a slide sits flush and the
     next one peeks, instead of both being boxed in by the container. */
  .wb-carousel-rail { --wb-rail-bleed: 20px; }
  .wb-carousel-head { margin-bottom: 28px; }
  .wb-carousel-rail[data-size="lg"] .wb-slide { width: 300px; }
  .wb-carousel[data-style="grid"] .wb-carousel-rail { --wb-rail-bleed: 0px; }
  /* Both backdrop heroes give up the immersive height at the same width —
     they are the same visual idea and must not diverge on a phone. */
  .wb-hero-video-el { display: none; }
  .wb-hero-video,
  .wb-hero-split[data-style="full"] { min-height: 0; padding-block: 88px; }
  .wb-bento-grid { grid-template-columns: 1fr; }
  .wb-bento-tile:nth-child(1),
  .wb-bento-tile:last-child:nth-child(even) { grid-column: span 1; }
}

/* ---- Form block -----------------------------------------------------
   A lead form rendered in the site's own vocabulary. Inputs are RAISED
   surfaces, so they take the same background/border/radius trio .wb-card
   takes (var(--bg) / var(--border) / 14px) — which is also what makes
   them follow a dark tone band automatically, since .wb[data-tone] re-
   points those variables and every rule here reads them.

   Focus ring is --wb-accent, never --brand-primary: the unlifted brand
   colour as a thin stroke on the dark tone measures ~2.1:1. A focus ring
   that fails contrast is an accessibility defect, not a cosmetic one. */

.wb-form-inner { display: block; }

.wb-form-head { margin-bottom: 40px; max-width: 640px; }
.wb-form-sub { margin-top: 12px; }

/* Width and Frame are choice controls, and data-edit is single-valued —
   .wb-form-inner already carries data-edit="source" for the form picker,
   so these two live on their own nested .wb-media-opt wrappers (display:
   contents, see that block below) rather than on .wb-form-inner itself:

     .wb-form-inner            (data-form=…)
       └─ .wb-media-opt        (data-width=…)
            └─ .wb-media-opt   (data-frame=…)
                 └─ .wb-form-slot

   Selectors therefore target the .wb-media-opt that actually carries the
   attribute, scoped under .wb-form: .wb-media-opt[data-width=…] alone
   would also match the prose block's own reading-width control (same
   attribute name, different meaning — see the .wb-prose-inner rules).
   Attribute-qualified for the same reason every hero rule is: pages
   published before this block existed carry none of these attributes and
   match nothing new. */
.wb-form .wb-media-opt[data-width="default"] .wb-form-slot { max-width: 720px; }
.wb-form .wb-media-opt[data-width="narrow"]  .wb-form-slot { max-width: 520px; }
.wb-form .wb-media-opt[data-width="wide"]    .wb-form-slot { max-width: 100%; }

/* Width/Frame resize .wb-form-slot itself, not the viewport — so a
   viewport media query can't see a Narrow+Card slot squeezed to ~456px
   on an otherwise wide screen. container-type: inline-size makes the
   slot a query container on its own measured width; container-name is
   explicit (not the nearest anonymous container) so a future container
   nested inside the form — a product tile, a field wrapper — can't
   silently capture the @container rules below instead of the slot.
   Containment here is layout-neutral: .wb-form-slot is a block box whose
   width already comes from its parent/max-width, never from its
   children, so establishing inline-size containment changes nothing
   about how it renders — confirmed byte-identical against the no-preset
   case via measure.js. */
.wb-form-slot {
  container-type: inline-size;
  container-name: wb-form-slot;
}

/* The frame is the ONLY border in the block. Field groups separate with
   whitespace and one hairline; fields separate with whitespace alone.
   A bordered group inside a bordered form inside a bordered section is
   the dated-admin tell this avoids. */
.wb-form .wb-media-opt[data-frame="card"] .wb-form-slot {
  padding: 36px 32px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
}

.wb-form-group + .wb-form-group { margin-top: 40px; }
.wb-form-group-head {
  padding-bottom: 12px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border);
}
.wb-form-group-title {
  margin: 0;
  font-size: 15px; font-weight: 600; line-height: 1.3;
  color: var(--text);
}
.wb-form-group-sub {
  margin: 6px 0 0;
  font-size: 14px; color: var(--text-muted);
}

/* Explicit two-column repeat, NOT auto-fit: lowering an auto-fit minmax
   floor in a media query makes MORE columns fit, not fewer. */
.wb-form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
}
.wb-form-field { min-width: 0; }
.wb-form-field[data-span="full"] { grid-column: 1 / -1; }

.wb-form-label {
  display: block;
  margin-bottom: 8px;
  font-size: 13px; font-weight: 500; line-height: 1.3;
  color: var(--text-muted);
}
.wb-form-req { margin-left: 3px; color: var(--wb-accent); }

.wb-form-input {
  display: block; width: 100%;
  padding: 12px 14px;
  font: inherit; font-size: 15px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  transition: border-color .15s ease;
}
.wb-form-input:hover { border-color: var(--wb-accent); }
.wb-form-input:focus-visible {
  outline: 2px solid var(--wb-accent);
  outline-offset: 2px;
  border-color: var(--wb-accent);
}
textarea.wb-form-input { min-height: 120px; resize: vertical; }

.wb-form-radios { display: flex; flex-wrap: wrap; gap: 8px 20px; }
.wb-form-radios label {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 15px; color: var(--text);
}

.wb-form-help {
  margin: 6px 0 0;
  font-size: 13px; line-height: 1.4;
  color: var(--text-muted);
}

.wb-form-products {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.wb-form-tile {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
  cursor: pointer;
  transition: border-color .15s ease;
}
.wb-form-tile:hover { border-color: var(--wb-accent); }
.wb-form-tile:focus-within { outline: 2px solid var(--wb-accent); outline-offset: 2px; }
.wb-form-tile img { width: 32px; height: 32px; object-fit: contain; flex: 0 0 auto; }
.wb-form-tile-name { font-size: 15px; font-weight: 500; color: var(--text); }
.wb-form-tile-desc { margin: 2px 0 0; font-size: 13px; color: var(--text-muted); }

.wb-form-pblock { margin-top: 20px; }

/* Honeypot. Off-screen rather than display:none — some bots skip hidden
   fields, and the whole point is that they fill it in. */
.wb-form-hp {
  position: absolute; left: -9999px;
  width: 1px; height: 1px; overflow: hidden;
}

.wb-form-error {
  margin: 24px 0 0;
  padding: 12px 14px;
  font-size: 14px;
  color: var(--text);
  border: 1px solid var(--wb-accent);
  border-radius: 14px;
  background: var(--bg);
}

.wb-form-foot {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 16px;
  margin-top: 32px;
}
.wb-form-consent {
  margin: 0; flex: 1 1 260px;
  font-size: 13px; line-height: 1.5;
  color: var(--text-muted);
}

/* The success state is its own composition, not the form minus the form:
   same vertical box so the page does not jump, its own centred rhythm. */
.wb-form-done {
  padding: 48px 32px;
  text-align: center;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
}
.wb-form-done:focus-visible { outline: 2px solid var(--wb-accent); outline-offset: 2px; }
.wb-form-done > .wb-lede { margin-top: 12px; }

/* Editor-only. A page carrying this cannot be published — the pre-flight
   blocks it — so it never reaches a visitor. Dashed, muted and explicit
   so it reads as an unfinished block rather than a broken render. */
.wb-form-empty {
  padding: 40px 24px;
  text-align: center;
  font-size: 14px;
  color: var(--text-muted);
  border: 1px dashed var(--border);
  border-radius: 14px;
}

.wb-form-noscript { margin: 24px 0 0; font-size: 14px; color: var(--text-muted); }
.wb-form-noscript a { color: var(--wb-accent); text-decoration: underline; }

@media (max-width: 720px) {
  .wb-form-grid,
  .wb-form-products { grid-template-columns: minmax(0, 1fr); }
  .wb-form-foot { flex-direction: column; align-items: stretch; }
  .wb-form-foot .wb-btn { width: 100%; }
}

/* Container-query counterpart to the viewport media query above: the
   Width/Frame presets shrink .wb-form-slot's own box independently of
   the viewport (see container-type on .wb-form-slot), so a slot can be
   narrow at any viewport width. Kept ALONGSIDE the @media block, not
   instead of it — a genuinely narrow viewport still needs to collapse
   even on a page with no width preset at all (already-published pages
   carry no data-width/data-frame attribute and never enter a container
   query context that would otherwise handle it).

   Two thresholds, not one, because the two grids tolerate a squeeze
   differently:
   - .wb-form-products tiles carry a logo plus a two-line description,
     so they need materially more per-column room. 600px sits strictly
     between the narrowest container that must stay 2-up (720px Default,
     656px Default+Card) and the widest one that must go 1-up (520px
     Narrow, 456px Narrow+Card) — both Narrow combinations collapse,
     both Default/Wide combinations don't, regardless of viewport.
   - .wb-form-grid fields are a label plus a single-line input, and
     survive the same squeeze the products grid can't — the measured
     Narrow+Card case (456px, 221px/column) is tight but legible, and
     the brief for this fix explicitly keeps it 2-up. 420px sits below
     every width/frame combination that exists today, so no current
     preset trips it; it's there so a future narrower preset (or extra
     chrome placed around the slot) still degrades safely instead of
     silently reproducing this bug for fields. */
@container wb-form-slot (max-width: 600px) {
  .wb-form-products { grid-template-columns: minmax(0, 1fr); }
}
@container wb-form-slot (max-width: 420px) {
  .wb-form-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Default tokens — fallback values used only if the inline
        :root block from page-static.js doesn't override them.
        Operators configure these via Site Settings.                ---- */
:root {
  /* Social glyph colour. --brand-primary is a FILL colour: as a glyph on a
     dark canvas the Amigo purple measures 2.82:1, under the 3:1 non-text
     bar (WCAG 1.4.11). Light needs no lift, dark does — derived rather than
     hard-coded so it tracks whatever palette the operator picks, exactly
     like --wb-accent below. */
  --chrome-glyph: var(--brand-primary);
  --bg: #ffffff;
  --surface: #f9fafb;
  --text: #0f172a;
  --text-muted: #64748b;
  --border: #e2e8f0;
  --brand-primary: #4b47a8;
  /* A second accent, independent of the structural brand colour: the
     highlight a palette uses for figures, ticks and small emphasis.
     Defaults to --brand-primary, so a site that never sets it is
     unchanged. */
  --brand-accent: var(--brand-primary);
  --wb-on-brand: #ffffff;
  /* Scrim behind hero copy that sits over an image or video. A fixed dark
     ink in BOTH themes: the text above it is --wb-on-brand either way, so
     tracking --text would invert it and destroy the contrast. */
  --wb-scrim: #0f172a;
  /* Section tone — a dark band inside a light page. The canvas is what the
     section paints; raised is one step lighter, for the cards that sit ON
     the canvas; inset is for chips and wells inside those cards. */
  --tone-dark-canvas: #1a1525;
  --tone-dark-raised: #221c30;
  --tone-dark-inset:  #2d2640;
  --tone-dark-text:   #f5f1e8;
  --tone-dark-muted:  #9d96a8;
  --tone-dark-border: #3a3348;
  /* What the accent is mixed TOWARDS to stay legible on the dark canvas.
     A token rather than a literal because the .wb-* layer forbids colour
     literals — and because an operator with a near-white canvas can
     retune it without touching the rule. */
  --tone-dark-lift:   #ffffff;
  /* A dark band inside a DARK theme has nothing to contrast against — the
     page is already this colour — so the band lifts instead of deepening.
     Without it the alternating rhythm silently disappears in dark mode. */
  --tone-dark-canvas-on-dark: #241e33;
  /* Tile states. Semantic, not brand — an alert must read as an alert even
     if the operator's accent happens to be red. */
  --wb-state-ok:    #2f855a;
  --wb-state-warn:  #b7791f;
  --wb-state-alert: #c53030;
  /* Lifted variants for a dark canvas. Measured: #c53030 on the dark tone
     lands ~2.96:1, just under the 3:1 large-text threshold. Same trap that
     --wb-accent has, so it gets the same treatment. */
  --wb-state-warn-lift:  #f0b429;
  --wb-state-alert-lift: #fc8181;
  /* Accent used as TEXT or a BORDER, as opposed to a fill. In light
     mode that is the brand colour itself; the dark override below
     lifts it so it stays legible on a dark background. */
  --wb-accent: var(--brand-accent);
  --font-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  /* Display + mono default TO the sans stack, so a site that sets neither
     renders exactly as it did before these existed. Set them in Site
     Settings and load the webfont through head_extras. */
  --font-display: var(--font-sans);
  --font-mono:    var(--font-sans);
}

/* `:root[data-theme]`, not bare `[data-theme]`. Both `:root` and
   `[data-theme="dark"]` compute to specificity (0,1,0), so the winner was
   decided by document order — and page-static.js injects the operator's
   palette in a <style> AFTER this file's <link>. That meant this block's
   dark defaults lost to the operator's LIGHT `:root` values, and any site
   whose operator had not set a dark palette rendered light in dark mode:
   the toggle flipped data-theme and nothing changed colour. Observed on
   production 2026-08-13, where website.brand.dark did not exist at all.

   At (0,2,0) this beats the injected `:root` regardless of order, while the
   operator's own dark block — emitted at the same (0,2,0) but later — still
   wins over it, key by key. Unset keys now fall back here, which is what
   these defaults were written for. */
:root[data-theme="dark"] {
  /* --brand-primary is a fill colour and stays as the operator set it;
     as TEXT on a dark background it lands around 2:1, which fails.
     Derived rather than hard-coded so it tracks whatever palette the
     operator picks — including a dark-specific brand colour. */
  --wb-accent: color-mix(in srgb, var(--brand-accent) 55%, #ffffff);
  --chrome-glyph: color-mix(in srgb, var(--brand-primary) 55%, #ffffff);
  --bg: #0f172a;
  --surface: #1e293b;
  --text: #f1f5f9;
  --text-muted: #94a3b8;
  --border: #334155;
}
