/**
 * BPS Lite — shape layer
 * ============================================================================
 * Paints the chip classes Lite ALREADY OFFERS in its Elementor controls but
 * never painted, plus the static header/hero shape helpers the ported Base
 * hero sections rely on.
 *
 * Measured 2026-08-13: `class-bps-lite-chip-bar-controls.php` offers 15 chip
 * classes; only 4 of them (bps-anim-slide-up, bps-ghost, bps-glass, bps-pill)
 * had any CSS behind them. A chip that is offered in the UI and paints nothing
 * is worse than one that is absent — the builder picks it, sees no change, and
 * concludes the design system is broken. This file closes that gap for every
 * class that is a STATIC SHAPE concern.
 *
 * 🔴 DELIBERATELY NOT PAINTED HERE — dynamic behaviour, ruled 2026-08-13:
 *   bps-accordion · bps-tabs · bps-counter · bps-parallax · bps-scroll-fade
 * Those need JS/scroll machinery. Mark's ruling: "when it comes to dynamic
 * pieces that's when we simply use third party addons" (eg Elementor Header
 * Footer builder, a premium Elementor pack). Building them here would be a
 * second mechanism duplicating an addon. See OWED entry.
 *
 * 🪤 WHY THIS IS A SEPARATE HAND-WRITTEN FILE, not `build/vocabulary-slice.scss`
 * (which is the Lite-authored source that compiles INTO bps-lite-core.css):
 * `build/compile-skins.php` requires `MOA_SCSS` from an ACTIVE moa-bps-base,
 * and this box's Lite network has no Base. Editing the slice without being able
 * to run the compiler would put the source and the shipped CSS out of sync, and
 * editing the compiled `bps-lite-core.css` directly gets silently wiped by the
 * next compile. A separate stylesheet is neither. Owed: fold these rules into
 * the slice next time a Base-enabled compile is actually run, then delete this.
 *
 * All colour comes from the context tokens the skins map per tone
 * (--bg / --text-color / --accent / --border-color), never a literal hex, so a
 * palette change repaints these automatically.
 */

/* --------------------------------------------------------------------------
 * bps-full-bleed — break a boxed container out to the full viewport width.
 * Uses the margin/width trick rather than 100vw: 100vw includes the scrollbar
 * gutter and causes horizontal overflow (PC-57 row 8 checks for exactly that).
 * ----------------------------------------------------------------------- */
.bps-full-bleed {
  width: 100%;
  max-width: 100%;
  margin-left: 0;
  margin-right: 0;
  padding-left: 0;
  padding-right: 0;
}

.e-con.bps-full-bleed > .e-con-inner {
  max-width: 100%;
  width: 100%;
}

/* A boxed section asked to bleed: cancel the container gutter, keep the inner
   measure so text does not run edge to edge. */
.section.bps-full-bleed {
  padding-left: 0;
  padding-right: 0;
}

/* --------------------------------------------------------------------------
 * bps-center — centre the block and its text.
 * ----------------------------------------------------------------------- */
.bps-center {
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}

.bps-center .elementor-button-wrapper,
.bps-center .wp-block-button {
  justify-content: center;
}

/* --------------------------------------------------------------------------
 * bps-fit — shrink the block to its content and centre it, without forcing
 * its own text or children to centre. bps-center sets auto margins but never
 * a width, so on a full-width/stretched parent the margins resolve to zero
 * and it only centres text. bps-fit is for a list or block that must keep
 * its own left-aligned content while sitting centred as a unit.
 * ----------------------------------------------------------------------- */
.bps-fit {
  width: fit-content;
  margin-inline: auto;
  max-width: 100%;
}

/* --------------------------------------------------------------------------
 * bps-shadow — the mid weight. bps-shadow-sm already existed; the plain one
 * was offered and inert, so any section asking for a normal card shadow got
 * nothing at all.
 * ----------------------------------------------------------------------- */
.bps-shadow {
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}

/* --------------------------------------------------------------------------
 * bps-glow — a soft accent halo. Reads from --accent so it follows the palette
 * per tone (inside a dark section this picks up --dark-accent automatically).
 * ----------------------------------------------------------------------- */
.bps-glow {
  box-shadow: 0 0 32px -6px var(--accent, rgba(0, 0, 0, 0.25));
}

@supports (background: color-mix(in srgb, red 50%, transparent)) {
  .bps-glow {
    box-shadow: 0 0 36px -4px color-mix(in srgb, var(--accent, #000) 45%, transparent);
  }
}

/* --------------------------------------------------------------------------
 * bps-anim-fade-in / bps-reveal — entrance animations.
 * bps-anim-slide-up already shipped with a keyframe; these two were offered
 * and inert. `bps-reveal` in Base is scroll-triggered; without the scroll
 * machinery (addon territory) the honest Lite behaviour is a plain entrance
 * animation rather than nothing at all. Documented so it is not mistaken for
 * parity with Base's scroll-driven version.
 * ----------------------------------------------------------------------- */
@keyframes bps-lite-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes bps-lite-reveal {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

.bps-anim-fade-in {
  animation: bps-lite-fade-in 0.5s ease-out both;
}

.bps-reveal {
  animation: bps-lite-reveal 0.5s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
  .bps-anim-fade-in,
  .bps-reveal {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
 * SUBMIT BUTTONS IN light3 / dark3 — added 0.2.8.
 *
 * `bps-lite-core.css:169` paints submit buttons for `.light`, `.light2`,
 * `.dark` and `.dark2` — and stops. `light3` / `dark3` are absent, although
 * they appear 21 times elsewhere in that same file and both have been offered
 * in the Tone control since 0.2.1 (they were BUG-480's whole subject).
 *
 * Consequence before this: a form dropped into a light3 or dark3 band rendered
 * its submit button in the browser/theme default while every other band on the
 * same page rendered it in the brand accent. Reaches CF7 and `[bps_lite_form]`
 * alike, because both submit through a real input/button element.
 *
 * `!important` matches the core rule it is completing — core:169 carries it,
 * and a half-tone-set that used a different weight would paint inconsistently
 * across the six tones, which is worse than either choice applied uniformly.
 * ----------------------------------------------------------------------- */
.section.light3 .btn-primary,
.section.light3 button.btn-primary,
.section.light3 button[type="submit"],
.section.light3 input[type="submit"],
.section.dark3 .btn-primary,
.section.dark3 button.btn-primary,
.section.dark3 button[type="submit"],
.section.dark3 input[type="submit"] {
  background-color: var(--button-color) !important;
  color: var(--button-color-text) !important;
  border-color: var(--button-border-color, var(--button-color)) !important;
}

.section.light3 .btn-primary:hover,
.section.light3 button[type="submit"]:hover,
.section.light3 input[type="submit"]:hover,
.section.light3 .btn-primary:focus,
.section.light3 button[type="submit"]:focus,
.section.light3 input[type="submit"]:focus,
.section.dark3 .btn-primary:hover,
.section.dark3 button[type="submit"]:hover,
.section.dark3 input[type="submit"]:hover,
.section.dark3 .btn-primary:focus,
.section.dark3 button[type="submit"]:focus,
.section.dark3 input[type="submit"]:focus {
  background-color: var(--cta-hover) !important;
  color: var(--cta-hover-text, var(--button-color-text)) !important;
}

/* --------------------------------------------------------------------------
 * 🔴 TEXT CAN NEVER COLLAPSE TO ONE CHARACTER PER LINE — added 0.2.8.
 *
 * Several shipped sections use a NARROW FIXED-WIDTH container as an icon slot:
 * `b-features-01` gives each checklist row a `width: 30px` column holding a `✓`,
 * and `b-services-04` in the Base catalogue uses a 40px one. Nothing in the JSON
 * marks those columns as icon-only. Put real text in one — which is an easy and
 * entirely reasonable mistake, because it is a heading slot like any other — and
 * every word renders vertically, one letter per line, **at 1440 and not just on
 * mobile**. It looks like catastrophic CSS failure and it is a filled-in slot.
 *
 * This is the third sighting of the same defect class (salon fleet P4, this
 * fleet's Services pages, and the Base catalogue), so it is fixed here as a
 * PROPERTY of the design system rather than patched per section.
 *
 * `min-width: min-content` = "never narrower than your longest word". For a
 * column holding `✓` that is ~10px, so a correctly-used icon slot is UNCHANGED
 * and still honours its 30px. For a column holding "Casement windows" the column
 * grows to fit "Casement" instead of shredding it. The guard fires only in the
 * case that is already broken.
 *
 * 🪤 Why no gate caught it: placeholder, palette, contrast and overflow gates all
 * pass on vertical text — the words ARE there, in the right colour, inside the
 * viewport. Only looking catches it. A collapsed-text check now lives in
 * `dev/scripts/lite-collapsed-text-gate.py`.
 * ----------------------------------------------------------------------- */
.section .e-con {
  min-width: min-content;
}

/* --------------------------------------------------------------------------
 * 🔴 THE SKIN GRADIENT THAT HIDES THE PALETTE — added 0.2.8.
 *
 * `skin-soft-rounded.css` paints buttons with a HARDCODED pink gradient:
 *     background-image: linear-gradient(135deg, #ec4899 0%, #e0177a 100%)
 * `bps-lite-core.css:169` sets `background-color: var(--button-color)
 * !important` — and never clears the gradient. A background-image paints ON TOP
 * of a background-color, so the palette is applied correctly and is completely
 * invisible: every soft-rounded site ships pink buttons whatever the client's
 * brand colour is.
 *
 * 🪤 This is why it survived so long. `getComputedStyle(btn).backgroundColor`
 * returns the CORRECT palette colour, so every check that reads background-color
 * passes while the page is visibly wrong. It was caught by the LOOK pack's phone
 * strip — by looking — after two rounds of measurement had "confirmed" it was
 * fine. Measure the painted result: on a button that means backgroundImage too.
 *
 * The 0.2.1 changelog claimed the Soft Rounded / Warm Organic gradients follow
 * the palette. That was done for SECTIONS and HEROES; buttons were missed.
 *
 * Fix is deliberately narrow: clear the gradient only where a tone is in play,
 * so the core rule's palette colour is what shows. `!important` matches the
 * declaration it is completing. A palette-derived button gradient would be the
 * nicer answer and belongs in the compiled skin source, not here.
 * ----------------------------------------------------------------------- */
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.elementor-button, .btn-primary, button[type="submit"], input[type="submit"], .wp-block-button__link, a.wp-element-button) {
  background-image: none !important;
  /* 🔴 RESTORING THE COLOUR IS HALF THE FIX, AND IT WAS MISSING — added 2026-09-13.
   * The note above says "clear the gradient so the core rule's palette colour is what
   * shows". It does not show. Soft Rounded sets its gradient with the `background`
   * SHORTHAND, which also resets `background-color` to `initial` — and it does so at
   * (0,3,0), where `bps-lite-core.css`'s `background-color: var(--button-color)` is only
   * (0,2,0). So killing the image left the button with no fill of any kind.
   *
   * Measured on the-circle-sales: `backgroundColor rgba(0,0,0,0)`, `backgroundImage none`,
   * text `#241A14` on a `#241A14` page — a completely INVISIBLE call-to-action, on three
   * of the ten funnel sites (the-circle, palette, cashflow — every soft-rounded one).
   * Warm Organic escapes it only because its own rule uses `background: var(--accent)`,
   * a colour rather than a gradient, so something survives the shorthand; Modern Flat
   * escapes it by declaring no button rule at all. That is why this has sat unnoticed:
   * it needs a gradient-shorthand skin AND the de-hardcoding rule above to appear.
   *
   * 🪤 No `!important` here, deliberately, unlike the line above it. This selector is
   * already (0,3,0) and loads after the skin, so it wins that fight on order alone —
   * while still LOSING to Elementor's own per-element Style-tab CSS at (0,3,1), which is
   * correct: an author who sets a button colour by hand must keep it. The line above
   * needs its `!important` because it is countering a shorthand; this one does not, and
   * adding it would silently break every hand-set button on the estate. */
  background-color: var(--button-color);
}

/* --------------------------------------------------------------------------
 * BUTTON VARIANTS IN ELEMENTOR — OWED-269, added 2026-08-15.
 *
 * `bps-ghost` / `-2` / `-3` and `button-outline` / `-secondary` / `-tertiary`
 * are offered by the chip bar's Variant control on BOTH containers and widgets.
 * Every painting rule for them in bps-lite-core.css is scoped to
 * `.wp-block-button` (:219-248, :326) — a GUTENBERG structure. The chip lands
 * on the Elementor widget wrapper, so on an Elementor page all six paint
 * nothing at all. Measured on this network 2026-08-15: `bps-ghost` is selected
 * on 142 posts across 62 subsites and reaches none of them.
 *
 * 🪤 This is why the 0.2.7 chip audit reported 41 offered / 41 painted / 0
 * inert and was still wrong. "Has a CSS rule somewhere" and "paints on the
 * element the control attaches to" are DIFFERENT TESTS, and only the second
 * one is the truth. Any future chip audit must resolve the selector against
 * the rendered wrapper, not grep the stylesheet for the class name.
 *
 * Scoped to `.section.<tone>` deliberately, matching the Gutenberg rules:
 * `--accent` is a CONTEXT token bound by the tone class, so outside a toned
 * section there is no colour to read and the variant is meaningless. Written
 * with `:is()` to keep six tones legible — `:is()` takes the specificity of
 * its most specific argument, so these land at (0,4,0), which is what it takes
 * to beat Elementor's own per-element CSS at (0,3,1). Do not "simplify" them
 * down to (0,3,0) or Elementor's Style tab silently wins again.
 *
 * 🔴 WHY `!important`, when the CSS ladder says match specificity first.
 * (0,4,0) is enough against the stylesheets alone — measured green on three
 * live pages before these were added. It is NOT enough against Elementor's
 * per-element Style-tab CSS, which is also (0,4,0) and therefore wins or loses
 * purely on load order. A variant chip is an explicit instruction to override
 * the widget's own fill, so beating it unconditionally is the correct SEMANTICS
 * rather than a convenience — and `bps-lite-core.css:169` already does exactly
 * this for submit buttons, so it is the established house pattern for this
 * surface, not a fresh escalation.
 *
 * 🪤 MEASURING THESE: `.elementor-button` carries `transition: 0.3s`. A test
 * that adds a chip at runtime and reads `getComputedStyle` immediately measures
 * the START of the transition and reports a false FAIL. Wait ~500ms, or test a
 * chip that was present at page load. This cost a full debugging cycle on
 * 2026-08-15 and looked exactly like a cascade defect.
 *
 * 🪤 `-2` / `-3` VARIANTS ARE UNDIFFERENTIATED ON LITE. `bps-ghost-2/-3` and
 * `button-outline-secondary/-tertiary` read `--accent-2` / `--accent-3`, which
 * are NOT among the 20 tokens Lite's Palette tab authors (REF-12 §2). They
 * resolve to white and paint an invisible rule on a light section. They now
 * behave correctly; they are simply not usefully distinct until Lite exposes
 * the secondary/tertiary accents. Use `bps-ghost` and `button-outline`.
 * ----------------------------------------------------------------------- */

/* Variable definitions, unscoped — mirroring how core.css:326 defines
 * `--ghost-color`. Harmless in Gutenberg, where the core rules read the
 * `--button-*` chain instead. */
.button-outline {
  --outline-color: var(--button-border-color, var(--button-color, var(--accent)));
  --outline-ink:   var(--button-color-text, var(--accent-text));
}

.button-outline-secondary {
  --outline-color: var(--accent-2);
  --outline-ink:   var(--accent-text-2);
}

.button-outline-tertiary {
  --outline-color: var(--accent-3);
  --outline-ink:   var(--accent-text-3);
}

/* Ghost — transparent fill, accent ink and rule, unchanged on hover.
 * `bps-ghost-text` added 2026-09-23 (Base parity): same shape, but --ghost-color
 * is the band's --text-color (core.css), so it is white on dark and ink on light.
 * It was defined in core but missing here, so on Elementor it painted nothing.
 * The chip sits on the widget wrapper, so the button is a descendant; the
 * second selector is defensive, for a chip written straight onto the link. */
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.bps-ghost, .bps-ghost-2, .bps-ghost-3, .bps-ghost-text) .elementor-button,
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) .elementor-widget a.elementor-button:is(.bps-ghost, .bps-ghost-2, .bps-ghost-3, .bps-ghost-text) {
  background-color: transparent !important;
  background-image: none !important;
  color: var(--ghost-color) !important;
  border-width: 2px !important;
  border-style: solid !important;
  border-color: var(--ghost-color) !important;
}

.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.bps-ghost, .bps-ghost-2, .bps-ghost-3, .bps-ghost-text) .elementor-button:hover,
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.bps-ghost, .bps-ghost-2, .bps-ghost-3, .bps-ghost-text) .elementor-button:focus {
  background-color: transparent !important;
  background-image: none !important;
  color: var(--ghost-color) !important;
  border-color: var(--ghost-color) !important;
}

/* Outline — transparent fill that FILLS on hover, which is the one behavioural
 * difference from ghost and the reason both variants exist. */
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.button-outline, .button-outline-secondary, .button-outline-tertiary) .elementor-button,
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) .elementor-widget a.elementor-button:is(.button-outline, .button-outline-secondary, .button-outline-tertiary) {
  background-color: transparent !important;
  background-image: none !important;
  color: var(--outline-color) !important;
  border-width: 2px !important;
  border-style: solid !important;
  border-color: var(--outline-color) !important;
}

.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.button-outline, .button-outline-secondary, .button-outline-tertiary) .elementor-button:hover,
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.button-outline, .button-outline-secondary, .button-outline-tertiary) .elementor-button:focus {
  background-color: var(--outline-color) !important;
  background-image: none !important;
  color: var(--outline-ink) !important;
  border-color: var(--outline-color) !important;
}

/* Elementor wraps the label in its own spans and its per-element CSS can colour
 * them directly. Without this the wrapper recolours and the text does not. */
.section:is(.light, .light2, .light3, .dark, .dark2, .dark3) :is(.bps-ghost, .bps-ghost-2, .bps-ghost-3, .bps-ghost-text, .button-outline, .button-outline-secondary, .button-outline-tertiary) .elementor-button :is(.elementor-button-text, .elementor-button-icon, .elementor-button-content-wrapper) {
  color: inherit !important;
}

/* --------------------------------------------------------------------------
 * STATIC header overlay — the "full bleed header" shape.
 *
 * Opt-in via the body class the settings injector adds. This is the SHAPE half
 * only: the header stops occupying layout space so the first section bleeds to
 * the top of the viewport, and the chrome floats over it. Scroll behaviour
 * (sticky, transparent-to-solid, shrink) is deliberately NOT here — that is
 * dynamic, and belongs to header-footer-elementor or a premium pack.
 *
 * Astra selectors are used knowingly. BPS emits no `.ast-` selectors of its own
 * and Lite sites run Astra chrome, so this is theme-surface work by necessity;
 * it is scoped behind the opt-in body class so it can never fire unasked.
 * ----------------------------------------------------------------------- */
body.bps-lite-header-overlay #masthead.site-header {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 99;
  background: transparent;
}

body.bps-lite-header-overlay .ast-main-header-wrap,
body.bps-lite-header-overlay .ast-primary-header-bar,
body.bps-lite-header-overlay .main-header-bar {
  background: transparent;
  border-bottom: 0;
}

/* Ink flips to the DARK tone's tokens, because an overlaid header sits on the
   hero, and the hero shapes that suit an overlay are the dark/photo ones. */
body.bps-lite-header-overlay .site-title a,
body.bps-lite-header-overlay .main-header-menu .menu-link {
  color: var(--dark-text, #fff);
}

body.bps-lite-header-overlay .main-header-menu .current-menu-item > .menu-link {
  color: var(--dark-accent, var(--accent, #fff));
}

/* The first section must clear the floating chrome so the headline does not
   collide with the nav. */
body.bps-lite-header-overlay .site-content .section:first-of-type,
body.bps-lite-header-overlay .site-content .e-con:first-of-type {
  padding-top: calc(var(--moa-y-xl, 120px) + 40px);
}

/* --------------------------------------------------------------------------
 * PALETTE-AWARE THEME CHROME — bridge Astra's colour system to BPS tokens.
 *
 * 🔴 THE ROOT CAUSE, measured 2026-08-13. Across 8 sites with 8 completely
 * different accents, every Lite site rendered the current menu item at
 * rgb(4,92,180) and the rest at rgb(51,65,85). Those are not "a wrong colour"
 * — they are Astra's SHIPPED DEFAULTS (#045cb4, #334155), untouched. Astra
 * runs its own palette in `--ast-global-color-0..8`, BPS never populates it,
 * and Astra's own selectors (0,3,0)/(0,4,0) outrank anything Lite was saying.
 *
 * Fighting that selector-by-selector is a losing game — Astra styles nav,
 * links, headings, buttons and backgrounds off those six variables. Rebinding
 * the VARIABLES fixes every one of those surfaces at once and keeps the theme
 * in charge of its own selectors, which is the correct division of labour
 * (BPS emits no `.ast-` selectors by design; the chrome is theme work).
 *
 * `:root:root` is (0,2,0) and beats Astra's `:root` (0,1,0) by specificity
 * rather than by load order, so this holds regardless of enqueue sequence.
 *
 * Mapping rationale — Astra's slot semantics on the left, BPS token on the right:
 *   0 primary / link          → --light-accent
 *   1 link hover / current     → --light-accent  (same hue; Astra darkens it itself)
 *   2 headings                 → --light-text-highlight
 *   3 body text                → --light-text
 *   4 base background          → --light-bg
 *   5 alternate background     → --light-bg2
 * Fallbacks are Astra's own stock values, so a site with an empty palette
 * degrades to exactly what it renders today rather than to unstyled.
 * ----------------------------------------------------------------------- */
:root:root {
  --ast-global-color-0: var(--light-accent, #046bd2);
  --ast-global-color-1: var(--light-accent, #045cb4);
  --ast-global-color-2: var(--light-text-highlight, #1e293b);
  --ast-global-color-3: var(--light-text, #334155);
  --ast-global-color-4: var(--light-bg, #ffffff);
  --ast-global-color-5: var(--light-bg2, #f0f5fa);
}

/* Under the overlay the chrome sits on the hero, so the same six slots have to
   resolve against the DARK tier instead — otherwise dark-on-dark. */
body.bps-lite-header-overlay #masthead.site-header {
  --ast-global-color-0: var(--dark-accent, #fff);
  --ast-global-color-1: var(--dark-accent, #fff);
  --ast-global-color-2: var(--dark-text, #fff);
  --ast-global-color-3: var(--dark-text, #fff);
}

/* ------------------------------------------------------------------------
 * Mobile overflow fix — Elementor flex/e-con containers default to
 * `min-width: auto`, which resolves to each container's MIN-CONTENT width
 * (the widest un-shrinkable descendant). Nest one full-width flex container
 * inside another (b-hero-06's stat row inside its hero content column is
 * the case that surfaced this — a real mobile user reported the hero text
 * cut off on `ashgrove-heating-air`, confirmed live: body.scrollWidth 830px
 * at a 390px viewport) and that min-content requirement compounds up the
 * tree, forcing the WHOLE hero — not just the row — past the viewport
 * width. `flex-wrap: wrap` alone does not fix it; the container itself
 * still refuses to shrink below its own min-content floor.
 * Confirmed live (2026-08-18) this is the actual initial-value behaviour,
 * not a rule Elementor's own CSS sets — there is no Elementor container
 * control for it, so the fix has to live here, not in a section's JSON.
 * ------------------------------------------------------------------------ */
.section .e-con.e-flex {
  min-width: 0;
}

/* Checklist tick — size and accent colour
 * ------------------------------------------------------------------------
 * Added 2026-08-19. The section library ships the checklist marker as a
 * `heading` widget whose title is a bare "✓", so it inherited body colour
 * and body size: measured `rgb(31,42,46)` (= --light-text) at 16px on
 * ashworth-dental/services. Mark, 2026-08-16 punch list #4: "the checkmarks
 * would look better if bigger and maybe using accent color".
 *
 * 🪤 The descendant selector is load-bearing, not tidiness. A BARE
 * `.bps-lite-tick` on the widget wrapper loses to Elementor's own
 * `.elementor-heading-title.elementor-size-default` on the actual <h*> tag by
 * specificity, so the size never lands and inheritance never gets a look-in.
 * This is design-manners §11's documented trap; the same mistake measured
 * hero H1s at 36px against a declared 56px ceiling. Reach the rendered tag.
 *
 * Colour comes from the palette custom property, never a hex — a hardcoded
 * value desyncs the moment the palette changes.
 */
.bps-lite-tick .elementor-heading-title,
.bps-lite-tick > h1, .bps-lite-tick > h2, .bps-lite-tick > h3,
.bps-lite-tick > h4, .bps-lite-tick > h5, .bps-lite-tick > h6 {
  font-size: 1.35rem; /* was 1.35em — em compounds when nested; ruled rem-only 2026-09-12 */
  line-height: 1;
  color: var(--accent, currentColor);
}

/* Body-copy measure cap — COMMENTED OUT 2026-09-07 (0.5.4), NOT deleted
 * ============================================================================
 * 🛑 RULED by Mark 2026-09-07, superseding the 2026-08-19 ruling that kept it:
 * "the paragraphs in elementor by default should be 100% of the outer
 * container ... this css rule is harmful not useful because we control the
 * outer widget via the container."
 *
 * ⏪ TO PUT IT BACK: uncomment the three rules in the block below and bump
 * BPS_LITE_VERSION (the enqueue uses a static version string, so an unversioned
 * CSS edit keeps serving the stale Bunny-CDN copy). Nothing else is needed.
 *
 * WHY IT WENT — a `max-width` on a child also decides how wide the WIDGET may
 * be. Lite section templates put `align_items: center` on their containers;
 * Elementor `.e-con` is `display:flex`, so in a column that means children are
 * sized `fit-content`, not stretched. `fit-content` needs the widget's
 * max-content width, and a box's max-content contribution is clamped by its own
 * max-width — so the 46ch cap propagated UP and became the width of the
 * text-editor widget itself. Measured on fern-field-vets: a 1000px container
 * painting a 512px widget. Nothing in Elementor exposes this: no control, no
 * class, and the widget's own computed `max-width` reads `100%` — you only find
 * it by inspecting the <p>. 44 of the 93 text-editor widgets in the section
 * library sit directly inside a centre-aligned container and were subject to it.
 *
 * 🪤 AND IT WAS ~12x OVERSIZED FOR ITS JOB. Measured across 16 Lite front pages
 * at 1440 (212 text-editor paragraphs), toggling the cap and comparing:
 *   · 138 of 212 — the cap did nothing at all
 *   ·  74 of 212 — it clamped the paragraph; 72 of those also collapsed the widget
 *   ·  68 of those 74 were ALREADY inside the 45–75ch comfortable band
 *     (typically 56–65ch) and were dragged down to 46ch, the band's floor
 *   ·   6 of 212 genuinely ran long (78–95ch) — the whole benefit
 *
 * 🎁 THE ACCEPTED COST — those 6 now run 78–95ch again:
 *     mulberry-lane-art-studio  .see-all        94.8ch
 *     fern-field-vets           .hero-subtitle  88.1ch
 *     sableford                 (unclassed)     82.7ch
 *     marrow-vance-accounting   .see-all        81.6ch
 *     sableford                 (unclassed)     78.0ch
 *     brightside                (unclassed)     78.0ch
 *   Per the ruling, the fix for those is to NARROW THEIR CONTAINER — the
 *   visible, editable width authority — not a second invisible one.
 *
 * VERIFIED AFTER REMOVAL, both directions (the check 0.4.8 skipped):
 *   212/212 paragraphs now paint exactly their widget's width
 *   74 widened · 138 unchanged · 0 narrowed
 *   0 centred paragraphs off-centre · 0 left-aligned paragraphs indented
 *
 * ⚠️ IF THE LONG-LINE PROBLEM COMES BACK, do not simply restore this. Any
 * character cap on `.section ... p` will size the widget again. It needs a rule
 * that cannot participate in the widget's intrinsic sizing, or a container-width
 * change. The surviving `.lede` cap further down (62ch) has the same shape but
 * is opt-in via an explicit role class, so it stays until it is shown to bite.
 *
 * ---------------------------------------------------------------------------
 * THE ORIGINAL RULES, VERBATIM — uncomment to restore:
 *
 * .section .elementor-widget-text-editor p {
 *   max-width: 46ch;
 * }
 * .section .elementor-widget-text-editor[class*="align-center"] p,
 * .section .bps-text-center .elementor-widget-text-editor p {
 *   margin-left: auto;
 *   margin-right: auto;
 * }
 * .section .elementor-widget-text-editor p {
 *   display: inline-block;
 *   width: 100%;
 *   vertical-align: top;
 * }
 * ========================================================================= */

/* Hero subtitle — stop forcing centre on LEFT-aligned heroes
 * ------------------------------------------------------------------------
 * Added 2026-08-27. All three compiled skins define `.hero-subtitle` with an
 * UNCONDITIONAL `margin: 0 auto 2rem`. Of the 12 hero templates, 5
 * (b-hero-01/04/08/10/11) are authored left-aligned (`align_items: flex-start`
 * on the subtitle's own parent container) — the skin rule fights that and
 * centres the paragraph anyway. Visibility depends on the hero's column
 * width, not on the bug itself: b-hero-11 (full-width column) showed a
 * 214-234px left offset between the heading and the subtitle beneath it;
 * b-hero-08 (asymmetric split) ~38px; b-hero-01/04/10 (narrower split
 * columns) 0-4px — same defect, currently invisible only because the column
 * happens to be narrow enough that centring barely moves anything.
 *
 * Reported by Mark on fern-field-vets (b-hero-11): "I think our structure
 * somehow got mixed up ... we have a known bug in the alignment of body
 * text." Confirmed live via computed styles, not assumed from the
 * screenshot — https://lite.buildprosites.com/fern-field-vets/ measured
 * heading.x=230, subtitle.x=464 (234px offset) before this rule.
 *
 * THE FIX IS UNCONDITIONAL AND NEEDS NO MARKER CLASS. None of the 12 hero
 * templates expose a class distinguishing left vs centre — `align_items` is
 * only ever rendered per-page in Elementor's own generated stylesheet, so
 * there is nothing reusable to select on, and adding one would mean editing
 * 5 section templates plus rebuilding/patching the 10 already-built pages
 * that use them (matches the C4 fix's own reasoning below: prefer a change
 * that reaches already-built pages without a per-page rewrite).
 *
 * Verified live (Playwright, injected before deploying) rather than assumed:
 * dropping the horizontal margin does not touch centred heroes at all —
 * `align-items: center` on their own parent container already centres the
 * subtitle with no margin needed; ashworth-dental (b-hero-02) and
 * mulberry-lane-art-studio (b-hero-05) measured byte-identical before/after
 * (92px / 114px — the ordinary gap between two independently-centred
 * elements of different widths, not a regression). Only `margin-left` and
 * `margin-right` are touched; `margin-bottom: 2rem` from the skin rule is
 * left alone.
 *
 * No `!important`: this file is enqueued after both core and skin (same
 * ordering the D1 Lede role above already relies on), so an equal-specificity
 * `.hero-subtitle` rule here wins on source order alone.
 */
.hero-subtitle {
  margin-left: 0;
  margin-right: 0;
}

/* C4 — dead air: stop vertical padding COMPOUNDING through nested containers
 * ------------------------------------------------------------------------
 * Added 2026-08-19. Measured on ashworth-dental's quote band: a 445px band
 * carrying 169px of text. The band's own padding is only 60/60 — the other
 * ~250px accumulates through FOUR nested containers, each adding a little:
 *
 *   .e-con              60 + 60   the band itself, legitimately
 *   .e-con-inner        10 + 10   Elementor's boxed default
 *   .e-con (only child) 10 + 10   an extra nesting level that holds nothing else
 *   .e-con              32 + 32   plus a 32px margin-top on top of its own padding
 *   trailing <p>              26   default paragraph margin at the very bottom
 *
 * No single value is wrong; the stack is. 🪤 The values live in each page's
 * `_elementor_data`, NOT in the section templates (which ship no padding at all)
 * and NOT in the builder — so there is no single source to correct. Neutralising
 * the redundant levels in CSS is the only fix that reaches 466 already-built
 * pages without a per-page rewrite.
 *
 * Only levels that are genuinely redundant are zeroed:
 *   - vertical padding on `.e-con-inner` (horizontal is kept — it is the gutter)
 *   - vertical padding on a container that is its parent's ONLY child, so no
 *     sibling spacing can depend on it
 *   - a first child's top margin, which duplicates the parent's padding-top
 *   - the last paragraph's trailing margin inside a band
 *
 * Measured effect, three sites, 1440: every band tightened 20–108px; the quote
 * band 445→379 (content fraction 0.44→0.52). Nothing moved horizontally.
 *
 * 🪤 No `!important`. Elementor's per-element rules are `.elementor-element-XXXX`
 * at (0,1,0); these win on specificity. If a future Elementor version raises
 * that, this stops working SILENTLY — verify by paint, not by reading the file.
 *
 * 🔴 CORRECTED 2026-08-25. This note previously claimed these rules are
 * "(0,2,0)/(0,3,0)". Two of the four are (0,4,0) — a pseudo-class carries a full
 * class-weight, which the original count missed. Real values:
 *
 *   .section > .e-con-inner                      (0,2,0)
 *   .section .e-con-inner > .e-con:only-child    (0,4,0)
 *   .section .e-con > .e-con:first-child         (0,4,0)
 *   .section .elementor-widget:last-child p:…    (0,4,1)
 *
 * That undercount mattered. At (0,4,0) the only-child rule outranks BOTH tiers of
 * the size scale — `.moa-sd` (0,1,0) and `.section.moa-sd` (0,2,0) — so nothing an
 * author can express through the BPS Size control could beat it. A styled card that
 * happened to be an only child silently lost its padding and fell back to
 * Elementor's own default. Measured 2026-08-25: 78 containers across 148 published
 * pages on the Lite fleet, including sterling-oak-wealth-advisors' hero card.
 *
 * 🛑 The only-child rule therefore now EXEMPTS any container carrying an explicit
 * size class. `bps_size` defaults to empty, so a structural pass-through wrapper —
 * the case this block was written for — carries no `moa-*` class and is still
 * neutralised. A container someone deliberately gave a Size to keeps its padding.
 * Match the five SIZE classes by name, never `[class*="moa-"]`: `moa-width-narrow`
 * is a WIDTH class and would wrongly opt containers out.
 */
.section > .e-con-inner {
  padding-top: 0;
  padding-bottom: 0;
}
.section .e-con-inner > .e-con:only-child:not(.moa-sm):not(.moa-sd):not(.moa-lg):not(.moa-xl):not(.moa-xxl):not(.moa-pane) {
  padding-top: 0;
  padding-bottom: 0;
}

/* `.moa-pane` — a container whose vertical padding is a SURFACE INSET, not dead air
 * ------------------------------------------------------------------------
 * Added 2026-09-13, from Mark's sales-page review. The only-child rule above is right
 * about a structural pass-through wrapper and wrong about a card: measured on
 * momentum-sales, `sp-offer-01`'s offer card computed `0px/0px/44px/44px` against an
 * authored `56/44/56/44` — vertical eaten, horizontal kept, which is this rule's exact
 * signature. Four sections in the review carried the same fault (offer-01, faq-02,
 * benefits-06, socialproof-02) and every one of Mark's "inner container failed to set
 * padding" notes is one of them.
 *
 * 🔴 THE TEST IS NOT "HAS PADDING" — every candidate has padding. It is **is this
 * container a visible surface**: does it carry a tone, a background or a radius of its
 * own? A hero's inner wrapper has none of those and its 90px genuinely IS dead air on
 * top of the section's own 120px; restoring that would have made Mark's "way too much
 * padding above the fold" worse, not better. Applied across the 16 pilot sections the
 * test separates them perfectly — 4 panes, 9 correctly left zeroed.
 *
 * 🪤 The exemption HAS to live in the `:not()` list. The only-child rule compounds to
 * (0,9,0); no plain class selector can out-specify it by source order, which is why the
 * 2026-08-27 block below had to reach for `!important` on four hardcoded element ids.
 * `.moa-pane` generalises those four away.
 *
 * 🪤 `moa-`, not `bps-`, and deliberately. Mark's 2026-09-13 ruling splits the vocabulary:
 * `bps-*` hook classes are MARKERS that paint nothing and only come alive under a body
 * scope, while spacing and padding classes paint. This one paints — its whole job is
 * vertical padding — so it belongs in the `moa-` spacing family beside the five sizes it
 * sits next to in the selector above. `gate-markers.py` enforces the split.
 *
 * The declaration reapplies Elementor's OWN per-element custom property, already set
 * correctly on the element. It invents no value — same principle as the id block below.
 */
.moa-pane {
  padding-block: var(--padding-top, 0) var(--padding-bottom, 0);
}

/* Only-child rule, exceptions — 4 template elements with real authored padding
 * ------------------------------------------------------------------------
 * Added 2026-08-27. `lite-only-child-padding-gate.py` swept all 354 pages on
 * the network and found the only-child rule above zeroing padding on 4
 * DISTINCT template elements, fleet-wide on the local-business fleet:
 *
 *   .elementor-element-35de438  "In detail" checklist header  wants  90px  30/30 sites
 *   .elementor-element-cf72f14  Services-page closing CTA band wants  80px  30/30 sites
 *   .elementor-element-2c8782c  b-hero-07 boxed hero card       wants  70px   2 sites
 *   .elementor-element-dfb321a  b-hero-12 glass-pane hero card  wants  48px   2 sites
 *
 * 🔴 THE EXEMPTION ABOVE (`:not(.moa-sm)...`) CAN NEVER REACH THESE. Size
 * classes (`moa-lg`/`moa-xl`) are applied to the outer `.section`, never to
 * an inner only-child container — confirmed live: `.elementor-element-35de438`
 * carries no moa-* class of its own, while its ancestor `.section` carries
 * `moa-xl`. That is the normal, universal shape of a BPS section — nearly
 * every section has SOME size class at that outer level — so exempting on
 * the ancestor instead would neuter the original dead-air fix almost
 * entirely, not just for these four. These four elements are the actual
 * exception: a container that carries its own real, deliberately-authored
 * padding despite being an only child.
 *
 * These 4 IDs are TEMPLATE-level and stable everywhere the template is
 * reused (verified: identical `wantTop` value at every one of the 30/30 and
 * 2/2 sites above) — so this reaches every already-built page with no
 * `_elementor_data` rewrite, same principle as the only-child rule itself.
 *
 * 🔴 `!important` IS NECESSARY AND DELIBERATE here, unlike the rest of this
 * file. The only-child rule above compounds to (0,9,0) — `.section` +
 * `.e-con-inner` + `.e-con` + `:only-child` + five `:not(.moa-X)` — and a
 * plain single-class ID selector cannot out-specify that by source order
 * alone. `var(--padding-top)`/`var(--padding-bottom)` reapply Elementor's
 * OWN per-element custom property, already correctly set on the element —
 * this does not invent a value, it restores the one already there.
 *
 * Verified live before deploying (Playwright, injected): all 4 elements
 * restore to their exact authored value (90/80/70/48px); re-measured with
 * the same gate script after deploying — see the script's own header.
 */
.elementor-element-35de438,
.elementor-element-cf72f14,
.elementor-element-2c8782c,
.elementor-element-dfb321a {
  padding-top: var(--padding-top) !important;
  padding-bottom: var(--padding-bottom) !important;
}

.section .e-con > .e-con:first-child {
  margin-top: 0;
}

/* Trailing paragraph margin — Lite never shipped Base's universal version
 * ------------------------------------------------------------------------
 * Added 2026-09-01. moa-bps-base carries `.elementor-widget-text-editor
 * p:last-child { margin: 0; }` at style.scss:1354, unconditionally, for
 * every text-editor widget's last paragraph. Lite is a standalone plugin
 * with no Base loaded (OWED-240), so that rule never reaches a Lite site —
 * only the narrower rule below existed here, which only fires when the
 * WIDGET is the section's last widget. A hero subtitle followed by a CTA
 * button container is not last, so neither rule reached it, and the
 * paragraph fell through to Astra's theme default
 * (`.entry-content p { margin-bottom: 1.6em }`) — 32px on a 20px subtitle,
 * confirmed live via Chrome DevTools protocol on marrow/c2g's hero.
 * Mirrors Base's rule so Lite text-editor widgets behave the same
 * regardless of a widget's position in its section.
 *
 * 🔴 UNSCOPED 2026-09-09 — the 2026-09-01 port above mirrored Base's
 * SELECTOR but not its SCOPE. Base's rule is top-level with no ancestor at
 * all (style.scss:1450, `(0,2,1)`); the port arrived here prefixed
 * `.section `, at `(0,3,1)`. `.section` is not automatic — it comes from the
 * `bps_is_section` switcher, which is OFF by default, or is implied by
 * setting a tone. So every HAND-BUILT container carried neither, the rule
 * never matched, and the paragraph fell through to Astra's
 * `.entry-content p` `(0,1,1)` exactly as before the fix. Measured on
 * barleycorn-bakehouse's hero: 28.8px, while all six `.section` containers
 * on the same page were already 0px. Dropping the prefix is what makes this
 * genuinely mirror Base.
 *
 * 🪤 The SECOND rule below stays `.section`-scoped deliberately. Base never
 * shipped a universal version of it, and unscoped it would zero the last
 * paragraph of ANY last widget anywhere — inside cards, footers and forms.
 * Widening it is not "finishing the job"; it is a different rule.
 */
.elementor-widget-text-editor p:last-child {
  margin-bottom: 0;
}

/* LEADING paragraph margin — the other half of the same asymmetry.
 * ------------------------------------------------------------------------
 * Added 2026-09-16. The rule above resets the TRAILING margin and nothing
 * ever reset the LEADING one, so a text-editor's first <p> kept the browser
 * default `margin-top: 1em`. On a 22px body that is 22px of space above the
 * first line that no author asked for and none can see in the panel.
 *
 * Measured symptom: the tick glyphs in the funnel checklists sat 26px below
 * the text beside them — 22px from this margin plus ~4px of line-height
 * difference (22px vs 30.8px). It reads as a vertical alignment bug and is
 * not one; the glyph is exactly where it should be and the text is pushed
 * down. Chasing it as an alignment problem is how it survived two passes.
 *
 * Scope mirrors the rule above EXACTLY — unscoped, (0,2,1), matching Base's
 * `.elementor-widget-text-editor p:last-child { margin: 0 }`
 * (style.scss:1354), whose `margin` shorthand already zeroes the top on a
 * single-paragraph widget. Lite's `margin-bottom` did not, so Lite carried
 * the worse half of the same defect.
 *
 * 🪤 Do NOT `.section`-scope this. That prefix is what made the trailing
 * rule silently miss every hand-built container in 2026-09-09, documented
 * immediately above.
 */
.elementor-widget-text-editor p:first-child {
  margin-top: 0;
}
.section .elementor-widget:last-child p:last-child {
  margin-bottom: 0;
}

/* Container border-color fallback — Elementor resets the CUSTOM PROPERTY
 * itself, not just the rendered border, on every container.
 * ------------------------------------------------------------------------
 * Added 2026-09-01, found on c2g's three-card "What I Do" section. First
 * attempt at this rule only re-declared the `border-color` PROPERTY
 * (`border-color: var(--border-color)`) and verified live as a no-op —
 * still rendered currentColor on all three cards after deploying.
 *
 * Root cause: Elementor's own frontend.min.css ships
 * `.e-con { --border-color: initial; ... }` unconditionally, on EVERY
 * container — a name collision, Elementor's own internal variable happens
 * to be spelled identically to ours. Every nested container (the row
 * wrapping the cards, and each card itself) carries `.e-con`, so the
 * CUSTOM PROPERTY is reset to invalid there directly — inheritance from
 * the `.light`/`.dark` section root never gets a look-in, because a
 * property declared directly on an element always wins over whatever
 * would have been inherited, regardless of that declaration's own
 * specificity relative to OTHER elements. A rule that only sets the
 * `border-color` property still reads that same broken custom property
 * and inherits the breakage.
 *
 * Fix: re-declare the CUSTOM PROPERTY itself, sourced from
 * `--b-border-color` (confirmed live, via CDP-inherited-styles, to reach
 * every nested container correctly — nothing in Elementor's own CSS
 * touches that name). `.section .e-con` is 2 classes, beating Elementor's
 * bare `.e-con` (1 class) regardless of load order.
 *
 * Still does NOT fix a container whose OWN border-color was explicitly
 * authored as the literal string `var(--border-color)` — Elementor's
 * per-element rule for that specific container carries 3 classes, higher
 * than this rule's 2, so that container's own (broken, self-referential)
 * declaration still wins the cascade for it specifically. That case needs
 * the authored value corrected to `var(--b-border-color)`, not more CSS.
 */
.section .e-con {
  --border-color: var(--b-border-color);
  border-color: var(--border-color);
}

/* P2 — .bps-pill takes its tint from the PALETTE, not a hardcoded blue
 * ------------------------------------------------------------------------
 * Added 2026-08-19. Measured on the painted page across 4 sites: the pill's
 * TEXT is `var(--accent)` and correct, while its background and border are a
 * literal `rgba(0,102,204,…)` / `rgba(77,166,255,…)` — Bootstrap blue — in all
 * three skins. So an amber site paints amber text inside a blue halo:
 *
 *   ironbark      text #E89B3C amber   background rgba(77,166,255,.12)  blue
 *   riverbank     text #0E5C3F forest  background rgba(0,102,204,.10)   blue
 *   fairweather   text #F08A2D orange  background rgba(77,166,255,.12)  blue
 *
 * Used on 32 pages across 30 sites, so it is a live visual defect, not a
 * theoretical one. 🪤 An earlier note claimed `.bps-pill` was "styled only in
 * skin-modern-flat" — stale. All three skins define it; all three hardcode the
 * blue. The defect was never the missing rule, it was the literal colour.
 *
 * `currentColor` rather than `var(--accent)` deliberately: every skin already
 * sets the pill's `color` to the accent and re-sets it under `.dark`, so
 * deriving from `currentColor` follows the tone switch for free and cannot
 * disagree with the text it sits behind.
 *
 * 🪤 Specificity: the skins carry `.dark .bps-pill` at (0,2,0). `.section
 * .bps-pill` matches that weight and wins only because `bps-lite-shapes` is
 * enqueued with the skin as a declared dependency, so it loads later. If that
 * dependency is ever dropped, this reverts to blue SILENTLY.
 */
.bps-pill,
.section .bps-pill {
  background: transparent;
  border-color: currentColor;
}

@supports (background: color-mix(in srgb, red 50%, transparent)) {
  .bps-pill,
  .section .bps-pill {
    background: color-mix(in srgb, currentColor 12%, transparent);
    border-color: color-mix(in srgb, currentColor 38%, transparent);
  }
}

/* ============================================================================
   D1 · Lede — a semantic intro-paragraph role, added 0.4.0
   ============================================================================
   WHY A ROLE AND NOT ANOTHER SIZE STEP. Lite already ships three raw size chips
   (bps-text-xs / -sm / -lg) and they are registered in the picker, so this is
   not a discoverability gap. Measured across all 2,023 published Elementor docs
   on the fleet 2026-08-25: `bps-text-lg` is used ONCE, `-sm` and `-xs` ZERO
   times. Nobody reaches for them, and the reason is visible in the CSS — all
   three are a single unscoped `font-size` that is byte-identical across all
   three skins, so picking one says "20px", not "this is the intro paragraph",
   and a skin can never re-cut it the way it re-cuts .hero-subtitle or
   .section-title. (Base has exactly the same three chips and no lede either —
   there was nothing to port.)

   🔴 THE SELECTOR SHAPE IS THE WHOLE TRICK. The chip class lands on the
   Elementor WIDGET WRAPPER, not on the <p> (OWED-163 / OWED-269). `font-size`
   and `color` inherit down to a bare paragraph, which is why .hero-subtitle
   gets away with being a flat rule — but `max-width`, `margin` and
   `text-wrap` do NOT do anything useful on a wrapper. Applied there, the
   measure would be set on the block rather than on the text, and the rule would
   pass a "does the class have CSS" check while painting nothing anyone asked
   for. So this matches .section-title's shape: reach through to the tag.
   "Has a CSS rule" and "paints on the element the control attaches to" are
   different tests — verify by paint.

   🪤 PER-SKIN SCOPING WITHOUT A RECOMPILE. The per-skin cuts below use the
   `bps-lite-skin-<slug>` BODY class that class-bps-lite-skin-css.php already
   adds and that print_overrides() already scopes to. The skin stylesheets
   themselves are compiled by build/compile-skins.php and cannot be rebuilt on
   this box (needs an active moa-bps-base — OWED-240), so a role defined in the
   skin SCSS is not currently shippable. This file is enqueued after both core
   and skin, so a body-scoped rule here lands correctly at (0,2,1) without
   fighting anything.

   Colour comes from --text-color2, the same token .hero-subtitle uses, so a
   lede tracks the palette and both tones with no tone-specific rule.
   ============================================================================ */
.lede .elementor-widget-text-editor p,
.lede > p,
p.lede {
  font-size: 1.25rem;
  line-height: 1.6;
  color: var(--text-color2);
  max-width: 62ch;
  margin-bottom: 1.25rem;
  text-wrap: pretty;
}

/* A centred parent should carry its lede's measure with it, otherwise the
   max-width above pins the text left inside a centred column — the same class
   of defect as the flex-key bug fixed in 0.3.4. */
.lede.bps-text-center .elementor-widget-text-editor p,
.lede.bps-text-center > p,
p.lede.bps-text-center,
.bps-text-center .lede .elementor-widget-text-editor p,
.bps-text-center .lede > p {
  margin-left: auto;
  margin-right: auto;
}

/* Per-skin cuts. These are the point of the role: modern-flat runs it tighter
   and cooler, warm-organic gives it the serif family's looser leading, and
   soft-rounded sits between. Mirrors how each skin already cuts
   .hero-subtitle differently. */
body.bps-lite-skin-modern-flat .lede .elementor-widget-text-editor p,
body.bps-lite-skin-modern-flat .lede > p,
body.bps-lite-skin-modern-flat p.lede {
  font-size: 1.1875rem;
  line-height: 1.55;
  letter-spacing: -0.005em;
}

body.bps-lite-skin-soft-rounded .lede .elementor-widget-text-editor p,
body.bps-lite-skin-soft-rounded .lede > p,
body.bps-lite-skin-soft-rounded p.lede {
  font-size: 1.25rem;
  line-height: 1.7;
}

body.bps-lite-skin-warm-organic .lede .elementor-widget-text-editor p,
body.bps-lite-skin-warm-organic .lede > p,
body.bps-lite-skin-warm-organic p.lede {
  font-size: 1.3125rem;
  line-height: 1.75;
}

@media (max-width: 768px) {
  .lede .elementor-widget-text-editor p,
  .lede > p,
  p.lede,
  body.bps-lite-skin-modern-flat .lede > p,
  body.bps-lite-skin-soft-rounded .lede > p,
  body.bps-lite-skin-warm-organic .lede > p {
    font-size: 1.0625rem;
  }
}

/* ============================================================================
   D2 · Icons follow the palette accent, added 0.4.0
   ============================================================================
   Ports the moa-bps-base 3.9.11 ruling ("icons follow the accent, for all
   icons") into Lite. Mark, 2026-08-25: "i wanted it for all the icons".

   🔴 THE WIDGET CENSUS DECIDES WHAT THIS RULE IS FOR. Base's rule targets Icon
   and Icon Box. Measured across all 2,023 published Lite docs 2026-08-25:
   ZERO Icon widgets, ZERO Icon Box widgets, and 107 docs using Icon LIST.
   Porting Base's rule alone would have shipped something with no effect on any
   live site. Leg A below is the one that actually does the work; Leg B is
   future-proofing for widgets nobody has used here yet.

   🔴 WHY THE GLYPHS ARE BLACK, which is NOT what it looks like. Measured on
   ironbark/visit, marlow/contact, fellco/contact: the .elementor-icon-list-icon
   WRAPPER computes the inherited body ink (rgb(27,22,17)) while the <svg>
   computes fill: rgb(0,0,0) — pure black — against a #94490D accent. The cause
   is not Elementor's grey and not a missing BPS paint upstream: SVG `fill` does
   NOT inherit and defaults to black. Nothing in Lite sets it, so the glyph is
   black no matter what the wrapper's colour resolves to. Setting `color` alone
   would therefore fix nothing.

   🪤 THE FALLBACK CHAIN IS NOT BASE'S. Base ends its chain at
   rgb(var(--accent-rgb)) because --accent is declared per tone band and never at
   :root. Lite scopes --accent the same way (.light/.dark in the skin CSS) but
   emits NO *-rgb channel triples at all — measured --accent-rgb as an empty
   string on a live page. Copying Base's chain verbatim gives one where BOTH
   links fail outside a tone band, which is the silent no-op that trap warns
   about, one level deeper. Lite's equivalent rescue is the :root literal:
   --light-accent IS at :root in all three skins and is the exact target the
   settings injector writes to.

   🪤 STACKED VIEW IS EXCLUDED, same as Base. Elementor paints
   `.elementor-view-stacked .elementor-icon { background:#69727D; color/fill:#fff }`
   — the glyph is knocked out white on a filled chip, so painting it accent puts
   accent on accent-ish and the icon disappears. Only `default` and `framed`.

   🔴 PAINT THE ELEMENT ELEMENTOR PAINTS — never the `i`. For an icon-font glyph
   Elementor puts the author's Primary Color on the CONTAINER and lets the <i>
   inherit it. A direct declaration beats an inherited value regardless of
   specificity, so a rule on the <i> silently defeats Elementor's (0,5,0) on the
   parent — that is how the first cut of the Base version repainted an icon an
   author had deliberately set to #ff8c00. `color` on the container, `fill` on
   the svg, so specificity genuinely arbitrates and the author still wins.

   ✅ Source order, verified rather than assumed: measured print order on a live
   page puts Elementor at 9-15 and Lite at 18-21, i.e. LITE PRINTS LAST. So the
   (0,2,0) tie against Elementor's own `.elementor-view-framed .elementor-icon`
   is won here, and no !important is needed. The author's per-post Primary Color
   at (0,5,0) still wins on specificity, which is the intent.

   ✅ AN AUTHOR'S OWN ICON COLOUR STILL WINS — verified by paint 2026-08-25, not
   by reading the specificity off the page. Injecting Elementor's own emitted
   shape (`.elementor-element-<id> .elementor-icon-list-icon svg`, (0,2,1))
   against Leg A's (0,1,1) repainted the glyph to the author's #FF8C00. Zero
   published docs on the fleet set an icon colour today, so there is no live
   blast radius either way, but the ordering is right for the first one that does.

   🪤 THAT CHECK READ FALSE THE FIRST TIME, and the reason will bite any future
   probe: `.elementor-icon` carries `transition: all .3s`, so getComputedStyle
   200ms after a change returns a value mid-interpolation — rgb(250,137,1) on the
   way to rgb(255,140,0). It looks like a settled wrong colour and reads as "my
   rule won". Let the transition finish (400ms+) before believing a measurement.

   Opt out per icon with the `bps-icon-ink` chip, which only redefines the custom
   property — no specificity contest with anything below.
   ============================================================================ */
.bps-icon-ink {
  --bps-icon-paint: var(--text-color, currentColor);
}

/* Leg A — Icon List. The live surface: 107 published docs. */
.elementor-icon-list-icon i,
.elementor-icon-list-icon svg {
  color: var(--bps-icon-paint, var(--accent, var(--light-accent)));
  fill: var(--bps-icon-paint, var(--accent, var(--light-accent)));
}

/* Leg B — Icon and Icon Box. No live usage yet; ships so that adding one does
   the palette-correct thing without needing a chip. .elementor-icon is nested
   inside .elementor-icon-box-icon, so this covers both widgets. */
.elementor-view-default .elementor-icon,
.elementor-view-framed .elementor-icon {
  color: var(--bps-icon-paint, var(--accent, var(--light-accent)));
}

.elementor-view-default .elementor-icon svg,
.elementor-view-framed .elementor-icon svg {
  fill: var(--bps-icon-paint, var(--accent, var(--light-accent)));
}

/* 🔴 LEG C — THE SAME TWO LEGS AGAIN, ONE CLASS HEAVIER, BECAUSE ELEMENTOR 4.2.4
   OUT-SPECIFIES THEM. Added 0.7.28, 2026-09-14, measured on `newstead-mould`.

   Legs A and B are (0,1,1) and (0,2,0). Elementor now writes
   `.elementor-widget-icon-list .elementor-icon-list-icon svg { fill: var(--e-global-color-primary) }`
   at (0,2,1) into every post's generated CSS — its stock #6EC1E4 — so every tick in
   every icon-list painted pale blue on a lime-and-ink palette while the element's own
   `color` sat correctly on the accent. `fill` wins for an SVG, so the wrong one showed.

   Same family as the heading and text-editor globals fixed in 0.7.27, and the same
   shape of mistake on our side: the BPS rule was written before anything else claimed
   the property, so it was only as specific as it needed to be then.

   Scoped to `.section` and carrying the widget class, these are (0,3,1) — clear of
   Elementor's (0,2,1), and the `--bps-icon-paint` custom property still governs, so the
   `bps-icon-ink` opt-out keeps working with no specificity contest. */
.section .elementor-widget-icon-list .elementor-icon-list-icon i,
.section .elementor-widget-icon-list .elementor-icon-list-icon svg {
  color: var(--bps-icon-paint, var(--accent, var(--light-accent)));
  fill: var(--bps-icon-paint, var(--accent, var(--light-accent)));
}

.section .elementor-widget-icon .elementor-icon,
.section .elementor-widget-icon-box .elementor-icon {
  color: var(--bps-icon-paint, var(--accent, var(--light-accent)));
}

.section .elementor-widget-icon .elementor-icon svg,
.section .elementor-widget-icon-box .elementor-icon svg {
  fill: var(--bps-icon-paint, var(--accent, var(--light-accent)));
}


/* --------------------------------------------------------------------------
 * Hand back Text Align on opener / closer sections — added 2026-08-30.
 *
 * All three skins declare `text-align: center` on .section-opener and
 * .section-closer. That is (0,1,0) and BPS Lite's stylesheet loads AFTER
 * Elementor's, so it beat Elementor's own `.e-con-boxed { text-align: initial }`
 * reset and forced centring on the whole section subtree — with no way to turn
 * it off from the editor.
 *
 * Elementor's Text Align control writes `--text-align`, so routing through it
 * hands the control back: unset resolves to the fallback and nothing changes,
 * set and it now wins. Elementor declares `--text-align: initial` on `.e-con`,
 * which for an unregistered custom property is the guaranteed-invalid value, so
 * the `center` fallback applies exactly as before.
 *
 * bps-lite-core.css already carries the same shape for two sibling properties
 * (`min-height: var(--min-height, auto)` and `justify-content:
 * var(--justify-content, center)`); text-align was simply missed. This rule
 * lives HERE rather than beside them because core.css is compiled output and,
 * per this file's own header, hand edits to it are silently wiped by the next
 * compile-skins run.
 *
 * Symptom it fixes: content stuck centred whatever Text Align is set to, and
 * appearing to be "caused" by editing the section padding — padding only
 * changes the box, which makes the pre-existing forced centring newly obvious.
 * ----------------------------------------------------------------------- */
.section-opener.e-con,
.section-closer.e-con {
  text-align: var(--text-align, center);
}

/* Accent Bold — restored 2026-08-30, mirrors moa-bps-base style.scss:824-848.
 * ------------------------------------------------------------------------
 * Colours <b>/<strong> text inside a chip-marked element/container with a
 * palette accent, instead of the element's own text colour. Ruled back in
 * by Mark — used across his own designs to call out a phrase mid-heading or
 * mid-paragraph without a separate widget.
 *
 * `accent_bold` was previously in BPS_Lite_Design_Vocabulary::INERT — the
 * chip existed in the vocabulary array but was pruned before it ever reached
 * Elementor/Gutenberg, and no CSS backed it either way. Un-inerted in the
 * same change that adds this block (build/verify-vocabulary.php fails
 * otherwise).
 *
 * Lite carries real --accent/--accent-2/--accent-3 tokens (see any
 * skin-*.css :root block), so all three tiers resolve to genuinely distinct
 * colours when the palette sets them — this is not a 1-colour stand-in.
 * `highlight-gold` was deliberately left out of Lite's curated vocabulary
 * (see class-bps-lite-design-vocabulary.php default_buckets() comment) —
 * that variant stays Base-only.
 * ----------------------------------------------------------------------- */
.highlight b,
.highlight strong {
  color: var(--accent, var(--text-color));
}
.highlight-2 b,
.highlight-2 strong {
  color: var(--accent-2, var(--accent, var(--text-color)));
}
.highlight-3 b,
.highlight-3 strong {
  color: var(--accent-3, var(--accent-2, var(--accent, var(--text-color))));
}

/* Stat block — give the SKINS their own type back
 * ============================================================================
 * Added 2026-09-07. Mark: "stat-label css is too aggressively large."
 *
 * 🔴 It is not a taste problem — the skin's own values were never painting.
 * `bps-lite-core.css` declares its role-class defaults as
 * `.stat-label:not(.bps-bypass)` = (0,2,0); every skin declares the same class
 * bare, `.stat-label` = (0,1,0). Core therefore beats all three skins on
 * specificity no matter what the load order is, and a skin cannot re-cut a role
 * it is supposed to own. Measured live on 8 sites across all three skins: every
 * one painted core's 16px uppercase label over a 64px number — including the
 * three modern-flat sites whose skin asks for 13px sentence case over a 42px
 * number. c2g (Mark's page) is modern-flat, so it was 52% oversized.
 *
 * 🪤 This is systemic, not local: 12 core role classes carry the
 * `:not(.bps-bypass)` guard and 23 skin declarations across 5 of them are dead
 * the same way — `prehead` (size + tracking), `hero-title` (weight + leading),
 * `hero-subtitle` (size + leading), `stat-number`, `stat-label`. Only the stat
 * pair is restored here, because that is what was reported and because the
 * hero/prehead deltas change every hero on 92 sites and want Mark's eye first.
 *
 * The guard itself is kept — `.bps-bypass` still opts out — and the values below
 * are copied verbatim from each skin, not invented. `body.<skin> .<role>` is
 * (0,2,1), so it wins the cascade without touching the generated files.
 * (core and the skins are compiled from Base SCSS by build/compile-skins.php;
 * hand edits there are wiped by the next compile. This file is the seam.)
 * ========================================================================= */
body.bps-lite-skin-modern-flat .stat-label:not(.bps-bypass) {
  font-size: 0.8125rem;
  text-transform: none;
  letter-spacing: 0.02em;
}
body.bps-lite-skin-modern-flat .stat-number:not(.bps-bypass) {
  font-size: clamp(2rem, 3vw, 2.625rem);
  font-weight: 700;
}
body.bps-lite-skin-soft-rounded .stat-number:not(.bps-bypass),
body.bps-lite-skin-warm-organic .stat-number:not(.bps-bypass) {
  font-weight: 700;
}

/* Sticky funnel chrome — .moa-sticky and .moa-stickybar
 * ============================================================================
 * Added 2026-09-13 (0.7.9) for the funnel nav family (`sp-nav-*`) and the
 * mobile CTA bar. Mark's framing: *"BPS Lite as the floor, not the ceiling"* —
 * so this is a plain CSS primitive with no plugin behind it. Elementor's own
 * sticky is a Pro feature and the estate runs free Elementor 4.2.4, which is
 * why the library has never had a nav band.
 *
 * 🪤 THE TRAP, and it is the whole reason this needed measuring rather than
 * asserting: `position: sticky` is cancelled — silently, with no error and no
 * visual hint — by ANY ancestor carrying `overflow: hidden`, `auto` or
 * `scroll`. It is not inherited and it is not a specificity fight, so no
 * amount of `!important` recovers it. Elementor's own reset does not set
 * overflow on the page wrapper, but a bleed section will: `.moa-bleed` and the
 * boxed-section cancel at the top of this file both clip. A sticky nav placed
 * under one of those does nothing and looks exactly like a class that never
 * loaded. Measured on a built page before shipping — see the handoff.
 *
 * 🪤 Second trap: the sticky element's parent must be TALLER than the sticky
 * element or there is nothing to travel through. That is satisfied here
 * because the section's parent is the page body, but it breaks the moment
 * someone nests one of these inside another container.
 *
 * `--moa-sticky-top` exists so an overlaid header can push the nav down
 * without a second class; it defaults to 0.
 * ========================================================================= */
.moa-sticky {
  position: -webkit-sticky;
  position: sticky;
  top: var(--moa-sticky-top, 0px);
  z-index: 50;
}

/* The bottom-anchored CTA bar. Same primitive, opposite edge. `bottom` rather
 * than `top`, and a higher stack order because it overlays content rather than
 * leading it. */
.moa-stickybar {
  position: -webkit-sticky;
  position: sticky;
  bottom: var(--moa-stickybar-bottom, 0px);
  z-index: 60;
}

/* 🔴 A sticky band MUST be opaque or the page scrolls visibly through it.
 * `bps_tone` paints `background-color` on the section, so a toned nav is
 * already covered; an untoned one is not, and that is the easy mistake. The
 * fallback below is the page ground, so an author who forgets a tone gets a
 * legible bar rather than a transparent smear. */
.moa-sticky:not([class*="light"]):not([class*="dark"]),
.moa-stickybar:not([class*="light"]):not([class*="dark"]) {
  background-color: var(--background-color, #fff);
}

/* Hairline under a sticky nav, so the band has an edge when content passes
 * beneath it. Uses the same token the section borders use. */
.moa-sticky {
  border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
}

/* 🪤 A sticky band must not inherit the vertical rhythm of a content band —
 * `moa-sd` at the Compact scale is 20px top and bottom, which reads as a
 * 90px-tall nav. The chrome carries `moa-sm` (a hardcoded 10px) and tops it up
 * by 8 here, giving the 18px the wireframes use — measured off the mockups'
 * `padding:18px 24px`, not chosen by eye. */
.moa-sticky > .e-con-inner,
.moa-stickybar > .e-con-inner {
  padding-block: 8px;
}
@media (max-width: 767px) {
  .moa-sticky > .e-con-inner,
  .moa-stickybar > .e-con-inner {
    padding-block: 4px;
  }
}

/* 🪤 The chrome row must WRAP rather than overflow at 390. A wordmark plus a
 * button plus three links is wider than a phone, and `space-between` on a
 * nowrap row pushes the button off-screen instead of stacking it — which the
 * horizontal-overflow gate catches only if something actually paints past the
 * edge. Wrapping is the fix; centring keeps it tidy once it has wrapped. */
/* 🪤 The wrap belongs on the ROW, not on `.e-con-inner`. The chrome's flex row
 * is a CHILD of the inner wrapper — `section > .e-con-inner > .e-con[row]` —
 * so wrapping the wrapper does nothing to the items that actually overflow.
 * First cut targeted the wrapper and the nav still stacked ragged at 390:
 * wordmark left, links centred, button hard right on three separate lines. */
@media (max-width: 767px) {
  .moa-sticky .e-con-inner > .e-con,
  .moa-stickybar .e-con-inner > .e-con {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 6px;
  }
}

/* Reduced-motion and print: a bar that follows the reader is motion, and it is
 * meaningless on paper. */
@media print {
  .moa-sticky,
  .moa-stickybar {
    position: static;
  }
}

/* Chrome type — .moa-wordmark and .moa-barmsg
 * ============================================================================
 * The library had no nav band until 0.7.9, so it has never had a type role for
 * a wordmark either. The first build of `sp-nav-01` reached for `section-title`
 * because that is the nearest existing role, and rendered a 40px "Your Brand"
 * in a 115px-tall bar — structurally correct and visually absurd. Measured on
 * the chrome lab before shipping.
 *
 * 🔴 These are DELIBERATELY NOT `section-title` with an override. Core declares
 * its role classes as `.section-title:not(.bps-bypass)` = (0,2,0); a bare
 * `.moa-wordmark` is (0,1,0) and would lose to it on any element wearing both.
 * The fix is a class that stands alone, not one that fights — the same trap
 * that made 23 skin declarations dead across five role classes (see the stat
 * block note above). So the nav headings wear ONE of these and no role class.
 *
 * Sized off the wireframes rather than by eye: `Sales - Momentum Course.dc.html`
 * sets the nav wordmark at 22px/700 and Flowline's at 18px/800.
 * ========================================================================= */
.moa-wordmark {
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.01em;
  margin: 0;
}

/* The bottom rail's message is a sentence, not a name — it reads at body size
 * and must not compete with the button beside it. */
.moa-barmsg {
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.25;
  margin: 0;
}

@media (max-width: 767px) {
  .moa-wordmark { font-size: 1.125rem; }
  .moa-barmsg   { font-size: 0.875rem; }
}

/* 🪤 ELEMENTOR'S DEFAULT 10px CONTAINER PADDING, AGAIN.
 * Every `e-con` gets 10px all round unless something says otherwise. In a
 * content band that is invisible; in a chrome bar it is 20px added to the
 * tallest child in EVERY nested box, and it compounds. Measured on the chrome
 * lab: a 22px wordmark and a 46px button produced a 103px nav — the wordmark's
 * own box was 42px and the button's 66px, both purely from this default.
 * (Same default cost an avatar 20px of its 56px earlier in this review; it is
 * the single most reliable way to be 20px wrong in Elementor.)
 *
 * Zeroed for boxes INSIDE the chrome only. The chrome's own rhythm stays with
 * `moa-sm` on the section and the 8px top-up on `.e-con-inner` above. */
.moa-sticky .e-con-inner .e-con,
.moa-stickybar .e-con-inner .e-con {
  padding: 0;
}

/* 🪤 A CHILD CONTAINER INSIDE THE CHROME MUST SIZE TO ITS CONTENT.
 * Elementor gives `content_width: full` a literal `width: 100%`, so three boxes
 * in a wrap row each claim a whole line — the wordmark, the links and the
 * button stacked as three full-width rows at 390 instead of a bar. It is the
 * same defect that made the trust badges render as full-width stripes; there it
 * was fixed by putting the widgets straight in the row, here the boxes are
 * needed for grouping, so they are told to size themselves.
 *
 * `width: auto` + `flex: 0 1 auto` keeps `space-between` meaningful: the
 * wordmark sits left, the CTA right, and neither grows into the gap. */
.moa-sticky .e-con-inner > .e-con > .e-con,
.moa-stickybar .e-con-inner > .e-con > .e-con {
  width: auto;
  flex: 0 1 auto;
}

/* In-page links are a desktop convenience. At phone width they turn a two-item
 * bar into a three-row block that eats the fold — the one thing a funnel hero
 * cannot afford ("above the fold is gold"). The bar keeps the wordmark and the
 * CTA, which are the only two things it exists for. */
@media (max-width: 767px) {
  .moa-chrome-links {
    display: none !important;
  }
}
