/**
 * so-components.css
 * Canonical CSS for Editor4 reusable components.
 * Single source of truth — loaded by editor canvas, preview, and shop shell.
 *
 * DO NOT duplicate these rules in component JS files or _preview.php.
 * Add new component styles here as new primitives are built.
 */

/* =====================================================================
   GLOBAL SPACING SCALE — Editor4 design system layout primitives.
   NOT shop-role tokens. NOT user-editable. NOT data-so-role based.
   Pure internal layout spacing units.
   ===================================================================== */
:root {
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;
  --space-xxxl: 64px;
  --space-4xl: 80px;
}


/* =====================================================================
   SHADOW SCALE — Editor4 design system token scale for box-shadow.
   Block authors use via inline `style="box-shadow: var(--shadow-lg);"`.
   Test pending: does GrapesJS Style Manager resolve var() references
   in inline styles? Awaiting verification before committing all blocks
   to token-based authoring.
   ===================================================================== */
:root {
  /* Values in px to match the --space-* token convention above + the JS token
     registry in so-token-bake.js. CSS variables here are primarily for published
     runtime CSS (browser resolves var() cleanly); block-author SM-editable use
     reads from the JS registry via Token Bake-on-Drop.

     CRITICAL: shadow values use 5-part format (X Y Blur Spread Color) — the
     explicit spread '0' is REQUIRED for SM correspondence to work correctly.
     GrapesJS PropertyStack uses positional parsing without smart color
     detection — a 4-part shadow (skipping spread) causes the color value to
     land in the spread slot and the Color subfield defaults to black. See
     EDITOR4_TOKEN_BAKE_ON_DROP_ARCHITECTURE.md §11 for the full investigation. */
  --shadow-xs: 0 1px  2px  0 rgba(0, 0, 0, 0.06);
  --shadow-sm: 0 2px  4px  0 rgba(0, 0, 0, 0.075);
  --shadow-md: 0 8px  16px 0 rgba(0, 0, 0, 0.15);
  --shadow-lg: 0 16px 48px 0 rgba(0, 0, 0, 0.175);
  --shadow-xl: 0 32px 64px 0 rgba(0, 0, 0, 0.2);
}


/* =====================================================================
   BORDER-RADIUS SCALE — Editor4 design system token scale for radius.
   Kept in sync with the JS token registry in so-token-bake.js.
   --radius-full for pills / circles.
   ===================================================================== */
:root {
  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:   16px;
  --radius-xl:   24px;
  --radius-full: 9999px;
}


/* =====================================================================
   SO-CARD — Surface container primitive
   ===================================================================== */

/* Outer wrapper — purely structural after the 2026-06-27 chrome-flip
 * refactor. ALL visual styling (bg, border, radius, padding, shadow)
 * lives on .so-card-body so that the body's hover/select chrome
 * perimeter aligns exactly with the visible card box. Outer remains
 * for: drop-target disambiguation (drops route to body, not outer),
 * select-parent navigation, and equalised-heights equalisation. */
.so-card {
  /* No visuals — body owns them. */
}

/* Equal-heights stretch — only when the card is the SOLE direct child
 * of its column. The common "row of cards, one per col" case still
 * equalises (every card is :only-child). But when a merchant drops
 * another component (image, button, text) into a card-holding col, the
 * card no longer stretches → no wasted whitespace inside the card
 * matching the now-taller col. */
.so-col > .so-card:only-child {
  height: 100%;
}

/* so-card-body — visible card surface. Owns all card visual styling
 * post-chrome-flip. Fills the outer 100% (width + height) so the
 * body's chrome perimeter matches what the merchant sees. */
.so-card-body {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  /* #302 relink (8 Sep 2026): the card surface follows the theme (Surface) and the
     theme's card-border utility; no hover — a card body is not an interactive
     surface (ChatGPT #303 ruling G). Fallbacks = the old literals. */
  background-color: var(--theme-surface, #ffffff);
  border: 1px solid var(--so-card-border-color, #dee2e6);
  border-radius: 1rem;
  padding: 1.25rem;
  transition: box-shadow 0.25s ease, transform 0.25s ease;
  /* Clip content (e.g. landscape variant's bleeding image) to the
   * card's rounded corners. Without this, an image that bleeds to
   * the card's edges shows sharp corners poking out of the card's
   * rounded shape. JC 2026-06-27: round corners of the CARD (not
   * the image) — overflow: hidden makes the card's rounded box do
   * the clipping naturally. */
  overflow: hidden;
}

/* Gap classes — added 2026-06-27 for features-icon-cards-cta block.
   Opt-in flex column layout with merchant-editable gap. Without any
   gap class, so-card-body keeps its default block flow (existing
   blocks unchanged). With a gap class, becomes a flex column with
   gap = the chosen space token. Children space via the gap; inline
   margin-top on children becomes UNNECESSARY (single-source-of-
   spacing compliance per PB-002 §6 Rule 1).

   Selector targets only when a gap class is set — bare so-card-body
   stays block-flow as before. */
.so-card-body[class*="so-card-body-gap-"] {
  display: flex;
  flex-direction: column;
}
.so-card-body-gap-xs { gap: var(--space-xs,  4px); }
.so-card-body-gap-sm { gap: var(--space-sm,  8px); }
.so-card-body-gap-md { gap: var(--space-md, 16px); }
.so-card-body-gap-lg { gap: var(--space-lg, 24px); }
.so-card-body-gap-xl { gap: var(--space-xl, 32px); }

/* Zero browser-default margins on direct children of a gap-bearing
   so-card-body so the gap is the SOLE source of inter-element
   spacing (per PB-002 §6 Rule 2 — same pattern used for so-col +
   so-overlay-box). Scoped to margin-bearing elements + registered
   components so it doesn't accidentally override Bootstrap utility
   margin classes on elements without browser defaults. */
.so-card-body[class*="so-card-body-gap-"] > h1,
.so-card-body[class*="so-card-body-gap-"] > h2,
.so-card-body[class*="so-card-body-gap-"] > h3,
.so-card-body[class*="so-card-body-gap-"] > h4,
.so-card-body[class*="so-card-body-gap-"] > h5,
.so-card-body[class*="so-card-body-gap-"] > h6,
.so-card-body[class*="so-card-body-gap-"] > p,
.so-card-body[class*="so-card-body-gap-"] > ul,
.so-card-body[class*="so-card-body-gap-"] > ol,
.so-card-body[class*="so-card-body-gap-"] > blockquote,
.so-card-body[class*="so-card-body-gap-"] > pre,
.so-card-body[class*="so-card-body-gap-"] > figure,
.so-card-body[class*="so-card-body-gap-"] > hr,
.so-card-body[class*="so-card-body-gap-"] > [data-gjs-type] {
  margin-top: 0;
  margin-bottom: 0;
}

/* Prevent images from overflowing the card boundary */
.so-card img {
  max-width: 100%;
  height: auto;
}

/* Preset classes live on the outer (.so-card-elevated etc.) but the
 * visual rules target the body via descendant selector — that way the
 * model semantics stay clean (preset is a property of the card) while
 * the body owns the rendering. Each preset is tuned for clear at-a-
 * glance distinction (JC 27 Jun feedback: previous values were too
 * subtle to tell apart). */

/* Elevated — floating impression: white surface, NO border, generous
 * two-layer shadow. The dual-shadow (close + spread) reads as a
 * material-design-style lifted card rather than a flat one. */
.so-card-elevated > .so-card-body {
  border-color: transparent;
  box-shadow:
    0  2px  4px  rgba(0, 0, 0, 0.06),
    0 12px 28px  rgba(0, 0, 0, 0.14);
}

/* Outline — framed impression: transparent bg lets the page show
 * through, thick darker border defines the boundary. No shadow. */
.so-card-outline > .so-card-body {
  background-color: transparent;
  border: 2px solid #94a3b8;
  box-shadow: none;
}

/* Soft — tinted-fill impression: visible brand-blue-tinted background
 * (replaces the previous near-white #f8f9fa). No border, no shadow. */
.so-card-soft > .so-card-body {
  background-color: #e7f0ff;
  border-color: transparent;
  box-shadow: none;
}

/* Hover effects — CSS-only, target body via descendant selector.
   Pattern mirrors so-badge hover effects (see lower in this file).
   Detected/applied via card.js _applyHoverClassToParent + popover
   dropdown in so-popover.js renderCardBodySettings.
   Transition declared on .so-card-body above animates these. */
.so-card-hover-sm:hover > .so-card-body {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}

.so-card-hover-md:hover > .so-card-body {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  transform: translateY(-4px);
}

/* Pure shadow — no movement. For when lift feels too active. */
.so-card-hover-shadow:hover > .so-card-body {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}

/* Glow — colored halo. Uses the merchant's primary brand color by
   default; falls back to a blue if no token set. */
.so-card-hover-glow:hover > .so-card-body {
  box-shadow: 0 0 24px 4px var(--so-card-glow-color, rgba(13, 110, 253, 0.35));
}

/* Grow — scale up. Subtle (1.03) since cards are larger than badges. */
.so-card-hover-grow:hover > .so-card-body {
  transform: scale(1.03);
}

/* Underline — bottom-border draw-in on hover. A 4px accent bar sits at
   the base of the card body, hidden by default (width:0), and animates
   to full width on hover. Colour follows the merchant's primary theme
   token; can be locally overridden per card via --so-card-underline-color
   (e.g. inline style or SM). Uses a ::before pseudo so the effect adds no
   DOM. The body needs overflow:hidden + position:relative to clip the bar
   inside the border radius; both are safe additions since .so-card-body
   already carries border-radius baked defaults. */
.so-card-hover-underline > .so-card-body {
  position: relative;
  overflow: hidden;
}
.so-card-hover-underline > .so-card-body::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 4px;
  width: 0;
  background-color: var(--so-card-underline-color, var(--theme-primary, #0d6efd));
  transition: width 0.4s ease-in-out;
  pointer-events: none;
}
.so-card-hover-underline:hover > .so-card-body::before {
  width: 100%;
}

/* ---------------------------------------------------------------------
   so-link primitive styles
   ---------------------------------------------------------------------
   Base primitive gets a universal transition so ANY hover values a
   merchant sets via SM Normal/Hover interpolate smoothly instead of
   snapping. Applies to every so-link regardless of block context.

   Hover VALUES are NOT baked in a shared CSS class anymore. Block
   authors use `data-so-hover-*` attributes on the anchor; the
   so-hover-coherence util translates those to per-instance :hover
   styles at init() time so Style Manager can inspect them. See:
     - js/editor4/lib/so-hover-coherence.js — the mechanism
     - memory/lesson-compose-from-primitives-vs-new-type — the doctrine
     - EDITOR4_SM_COHERENCE_MATRIX.md — the coherence register
   --------------------------------------------------------------------- */
.so-link {
  transition: color 0.3s ease-out, letter-spacing 0.3s ease-out,
              text-decoration-color 0.3s ease-out, opacity 0.3s ease-out;
}

/* =====================================================================
   LANDSCAPE VARIANT — now composition-primitive based (so-content-grid)
   =====================================================================
   The Landscape so-card variant uses so-content-grid as its internal
   layout primitive — 2 grid items (image cell + content cell), track-
   resize handles widths via --cg-template-custom. No bespoke CSS Grid
   here, no anonymous Divs, no special Image Position trait.

   Original CSS Grid via variant class was reverted after JC review
   found the inherent limitation: any content beyond a single child
   (e.g. heading + paragraph + button) needs a wrapper, and the wrapper
   becomes an anonymous Div (chrome violation). so-content-grid solves
   it with registered so-content-grid-item children.

   For image bleed inside a landscape card: the merchant uses the
   image cell's own padding controls (Style tab), or the block author
   can pre-set the image cell to padding:0 in the block template.
   The card-level padding (on so-card-body) stays at the standard
   baked default — content cells override individually. */

/* Editor empty-state hint (visible when the card body is empty —
 * i.e. fresh card with nothing in it yet). The hint sits inside the
 * body so it stays inside the card's padding area. Text matches the
 * drag-time placeholder label (so-bootstrap-grid.js sets
 * "Drop in card here" when actively dragging into a card body) so
 * the language is consistent in both states. */
.so-card-body:empty::before {
  content: "Drop in card here";
  display: block;
  padding: 2rem;
  text-align: center;
  color: #adb5bd;
  font-size: 0.875rem;
  pointer-events: none;
}

/* =====================================================================
   COUNTER COMPONENT (so-counter)
   =====================================================================
   Moved here from js/editor4/components/counter.js (24 May 2026) — the
   inline CSS there was only injected into the editor canvas iframe via
   editor.Css.addRules() + direct <style data-counter-css> injection,
   so preview/published pages never got it. Result: counters in editor
   showed at 5rem inline-block (intended), but in preview fell back to
   browser defaults (tiny text, divs stack vertically). Classic Law 11
   (Editor↔Preview Parity) violation. Single source of truth here.

   Display defaults to `block` (24 May 2026 design call) for
   consistency with every other widget-class component (cards, text,
   galleries, etc.) and to honour Constitution Law 1 (Primitive
   Supremacy). Multi-counter side-by-side layouts use sections → rows
   → cols, same as any other component — no per-component layout magic.
   Previous `inline-block` default was a layout hack and is gone. */
.so-counter {
  display: block;
  font-size: 5rem;
  font-weight: bold;
  color: #333;
  padding: 0.5rem;
  min-width: 100px;
  text-align: center;
  line-height: 1.1;
}

.so-counter-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.so-counter-label {
  font-size: 1rem;
  font-weight: normal;
  color: #666;
  margin-top: 0.25rem;
}


/* =====================================================================
   SO-FLIP-CARD — Interactive front/back state machine
   Separate component type (passes all 5 splitting tests vs so-card).
   Rigid 3-level DOM: .so-flip-card → .so-flip-card-inner → front + back
   ===================================================================== */

.so-flip-card {
  perspective: 1000px;
  height: 300px;
}

.so-flip-card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.6s;
}

.so-flip-card-front,
.so-flip-card-back {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  overflow: hidden;
  border-radius: 0.5rem;
  padding: 1.25rem;
}

.so-flip-card-front {
  background-color: #ffffff;
  border: 1px solid #dee2e6;
}

.so-flip-card-back {
  background-color: #f8f9fa;
  border: 1px solid #dee2e6;
  transform: rotateY(180deg);
}

/* Direction: vertical flip (rotateX) — default is horizontal (rotateY) */
.so-flip-card.so-flip-x .so-flip-card-back {
  transform: rotateX(180deg);
}

/* ── Hover trigger (CSS-only, zero JS) ─────────────────────────────── */
.so-flip-card[data-flip-trigger="hover"]:hover .so-flip-card-inner {
  transform: rotateY(180deg);
}
.so-flip-card[data-flip-trigger="hover"].so-flip-x:hover .so-flip-card-inner {
  transform: rotateX(180deg);
}

/* ── Click trigger (toggled by runtime JS via .is-flipped) ─────────── */
.so-flip-card.is-flipped .so-flip-card-inner {
  transform: rotateY(180deg);
}
.so-flip-card.is-flipped.so-flip-x .so-flip-card-inner {
  transform: rotateX(180deg);
}

.so-flip-card[data-flip-trigger="click"] {
  cursor: pointer;
}

/* ── Speed classes ─────────────────────────────────────────────────── */
.so-flip-card.so-flip-fast .so-flip-card-inner {
  transition-duration: 0.3s;
}
.so-flip-card.so-flip-slow .so-flip-card-inner {
  transition-duration: 1s;
}

/* ── Empty-state hints for faces ───────────────────────────────────── */
.so-flip-card-front:empty::before,
.so-flip-card-back:empty::before {
  content: "Drop content here";
  display: block;
  padding: 2rem;
  text-align: center;
  color: #adb5bd;
  font-size: 0.875rem;
  pointer-events: none;
}


/* =====================================================================
   SO-BUTTON — First-class button primitive
   Layers on top of Bootstrap 5. Does not replace .btn.
   Trait-driven: variant, size, shape, width (orthogonal dimensions).
   ===================================================================== */

/* ── Base ─────────────────────────────────────────────────────────── */
.so-button {
  display: inline-block;
  padding: 0.5rem 1.25rem;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
  /* !important because the button can pick up position:absolute + display:block
     from a Free Mode drag, and in that state some browsers fail to propagate
     cursor:pointer to the padding box. Pointer cursor is fundamental to button
     UX — no legitimate rule should override it. (Bootstrap's .btn and Tailwind
     ship the same hardening.) Surfaced May 2026 during the button sweep. */
  cursor: pointer !important;
  user-select: none;
  border: 2px solid transparent;
  border-radius: 0.375rem;
  transition: background-color 0.2s ease, border-color 0.2s ease,
              color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
  /* Cap user-set width at parent — when merchant drags the button
     wider than its column, the rendered width is capped at parent.
     Style.width from GrapesJS resize is preserved as the merchant's
     intent (acts as an upper bound), but rendering can't overflow
     the container. Same pattern as Webflow / Wix.
     overflow-wrap:break-word guards against long unbreakable strings
     (URLs, garbled text) overflowing the button bounds. */
  max-width: 100%;
  overflow-wrap: break-word;
}

/* Drag-to-resize cap for native text components (and any GrapesJS
   resizable that uses data-gjs-type="text"). Identical reasoning to
   .so-button — caps drag at parent width, never overflows the column.
   overflow-wrap:break-word forces long unbreakable strings (URLs,
   garbled text) to wrap when they hit the box edge instead of
   overflowing visually past the column. break-word only kicks in when
   the word would actually overflow — normal text is unaffected. */
[data-gjs-type="text"] {
  max-width: 100%;
  overflow-wrap: break-word;
}

/* Remove default anchor/button resets */
a.so-button:hover { text-decoration: none; }
button.so-button { -webkit-appearance: none; }

/* ── Variant: Filled — solid background + border ─────────────────── */
/* Default fallback colours; overridden by Shop Role tokens or Style tab */
.so-button--filled {
  /* #302 relink (ChatGPT #303 ruling G, 8 Sep 2026): the canvas Button follows the
     theme like the shop buttons do — filled = Primary background / Surface text
     (the ratified pair, 10.2–17.9:1 on all six palettes). Fallbacks = the old
     literals, so a page without theme vars looks exactly as before. */
  background-color: var(--theme-primary, #0d6efd);
  border-color: var(--theme-primary, #0d6efd);
  color: var(--theme-surface, #ffffff);
}
.so-button--filled:hover {
  /* #303 Auto hover (default, class level): the theme's Primary companion —
     resolved per palette by the ONE hover resolver against the Surface text
     (falls back to Primary itself where no companion is emitted). A background-
     only colour change + a 1px lift; NO whole-button brightness filter (ruling E:
     a filter also changes the text, so the resting-pair contrast could not be reused). */
  /* The text keeps its resting colour on hover (JC, 12 Sep 2026): a Button is an <a>,
     and BootstrapSkin's a:hover { color: #0056b3 } (0,1,1) outranks .so-button--filled
     (0,1,0), so hover text turned link-blue on every page. This rule is (0,2,0); a
     colour the merchant sets on the button's #id still wins. */
  color: var(--theme-surface, #ffffff);
  background-color: var(--theme-primary-hover, var(--theme-primary, #0d6efd));
  border-color: var(--theme-primary-hover, var(--theme-primary, #0d6efd));
  transform: translateY(-1px);
}
.so-button--filled:active {
  filter: brightness(0.82);   /* existing active treatment preserved (ruling H) */
  transform: translateY(0);
}

/* ── Variant: Outline — transparent bg, visible border ───────────── */
.so-button--outline {
  background-color: transparent;
  border-color: var(--theme-primary, #0d6efd);
  color: var(--theme-primary, #0d6efd);
}
.so-button--outline:hover {
  /* fills with the ratified Primary/Surface pair (a transparent base has no
     derivable hover — ruling E excludes alpha backgrounds from Auto) */
  background-color: var(--theme-primary, #0d6efd);
  border-color: var(--theme-primary, #0d6efd);
  color: var(--theme-surface, #ffffff);
  transform: translateY(-1px);
}
.so-button--outline:active {
  filter: brightness(0.85);
  color: var(--theme-surface, #ffffff);
  transform: translateY(0);
}

/* ── Variant: Ghost — no background, no border, text only ────────── */
.so-button--ghost {
  background-color: transparent;
  border-color: transparent;
  color: var(--theme-primary, #0d6efd);
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}
.so-button--ghost:hover {
  color: var(--theme-primary, #0d6efd);   /* keeps its text over BootstrapSkin's a:hover link blue (JC, 12 Sep 2026) */
  background-color: rgba(0, 0, 0, 0.05);
  filter: brightness(0.9);
}
.so-button--ghost:active {
  background-color: rgba(0, 0, 0, 0.08);
}

/* ── Size: Small ──────────────────────────────────────────────────── */
.so-button--sm {
  padding: 0.3rem 0.85rem;
  font-size: 0.875rem;
}

/* ── Size: Large ──────────────────────────────────────────────────── */
.so-button--lg {
  padding: 0.65rem 1.75rem;
  font-size: 1.125rem;
}

/* ── Shape: Pill ──────────────────────────────────────────────────── */
.so-button--pill {
  border-radius: 50vh;
}

/* ── Shape: Square ────────────────────────────────────────────────── */
.so-button--square {
  border-radius: 0;
}

/* ── Width: Full-width (block) ────────────────────────────────────── */
.so-button--block {
  display: block;
  width: 100%;
}

/* ── Focus-visible (modern accessible ring) ───────────────────────── */
.so-button:focus-visible {
  /* Keyboard focus (ruling H): a theme-coloured outline OFFSET from the fill so it
     stays visible on hover + focus (the hover changes only the background, never
     the outline) and on every palette; the old literal glow is gone. */
  outline: 2px solid var(--theme-primary, #0d6efd);
  outline-offset: 2px;
  box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
  .so-button:hover, .so-button:active { transform: none; }
}

/* ── Disabled state ───────────────────────────────────────────────── */
.so-button[disabled],
.so-button[aria-disabled="true"] {
  opacity: 0.65;
  pointer-events: none;
  cursor: default;
}

/* ── Shop Role Bridge ─────────────────────────────────────────────── */
/* When a shop role is assigned, token CSS variables override defaults.
   Falls back to the variant's built-in colours if vars are not defined.
   This is additive — safe even when token CSS is not loaded. */
/* Role: Shop Primary */
.so-button--shop-role[data-so-role="button-primary"] {
  background-color: var(--so-button-primary-bg, #0d6efd);
  border-color: var(--so-button-primary-border, var(--so-button-primary-bg, #0d6efd));
  color: var(--so-button-primary-color, #ffffff);
  font-family: var(--so-button-primary-font, inherit);
  font-size: var(--so-button-primary-font-size, 1rem);
}
.so-button--shop-role[data-so-role="button-primary"]:hover {
  background-color: var(--so-button-primary-hover-bg, #0b5ed7);
  border-color: var(--so-button-primary-hover-bg, #0b5ed7);
  color: var(--so-button-primary-hover-color, #ffffff);
}

/* Role: Shop Process */
.so-button--shop-role[data-so-role="button-process"] {
  background-color: var(--so-button-process-bg, #198754);
  border-color: var(--so-button-process-bg, #198754);
  color: var(--so-button-process-color, #ffffff);
  font-family: var(--so-button-process-font, inherit);
  font-size: var(--so-button-process-font-size, 1rem);
}
.so-button--shop-role[data-so-role="button-process"]:hover {
  background-color: var(--so-button-process-hover-bg, #157347);
  border-color: var(--so-button-process-hover-bg, #157347);
  color: var(--so-button-process-hover-color, #ffffff);
}

/* Role: Shop Default */
.so-button--shop-role[data-so-role="button-default"] {
  background-color: var(--so-button-default-bg, #6c757d);
  border-color: var(--so-button-default-border, var(--so-button-default-bg, #6c757d));
  color: var(--so-button-default-color, #ffffff);
  font-family: var(--so-button-default-font, inherit);
  font-size: var(--so-button-default-font-size, 1rem);
}
.so-button--shop-role[data-so-role="button-default"]:hover {
  background-color: var(--so-button-default-hover-bg, #5c636a);
  border-color: var(--so-button-default-hover-bg, #5c636a);
  color: var(--so-button-default-hover-color, #ffffff);
}


/* ═══════════════════════════════════════════════════════════════════
   Icon Component (.so-icon)
   Inline icon primitive — SVG persisted in saved HTML.
   ═══════════════════════════════════════════════════════════════════ */

.so-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  line-height: 1;
  /* #183 (25 Aug 2026): class-level RUNTIME default size. Since the
     B.2 ckpt-3c defaults diet (3a8bb063) so-icon carries no
     defaults.style, so an instance with no authored width/height
     (every icon stored before 7 Aug 2026) had nothing sizing its
     span and the 100%-width SVG filled the card. Authored sizes
     (inline style / #id rule) keep winning by specificity; this only
     catches the unsized case, on every surface (editor/preview/publish). */
  width: 48px;
  height: 48px;
  color: #333333;
  background: transparent;
  border-radius: 0;
}
.so-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ═══════════════════════════════════════════════════════════════════
   so-badge  (Phase 2 — 25 Jun 2026)
   Compact shaped badge holding ONE glyph (icon / number / letter).
   Single-root <span>, no children. The span IS the frame: inline-flex
   centres the glyph; padding gives breathing room; box-sizing keeps
   drag-resize honest. Outer size = the span's own width/height (single
   source, shared with the modal "Badge size" field). Storage doctrine:
   token classes for presets (private), inline --so-badge-* vars for
   values. Supersedes the planned so-frame primitive (no child-dropping).
   ═══════════════════════════════════════════════════════════════════ */
.so-badge {
  box-sizing: border-box;
  display: inline-flex;
  /* #183 addendum (25 Aug 2026): class-level RUNTIME default outer size —
     same defect as so-icon: the B.2 defaults diet moved the 48px seed to
     creation time, so badges stored before 7 Aug 2026 had no size and the
     100%-width glyph filled the card. Authored sizes (inline / #id rule)
     win by specificity; this only catches the unsized case. */
  width: 48px;
  height: 48px;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  line-height: 1;
  background: var(--so-badge-bg, #eef2ff);
  color: var(--so-badge-color, #1e40af);
  padding: var(--so-badge-padding, 12px);
  font-size: var(--so-badge-font-size, 18px);
  font-weight: 600;
}
.so-badge svg { display: block; width: 100%; height: 100%; }
.so-badge-text { display: inline-flex; align-items: center; justify-content: center; line-height: 1; font-size: var(--so-badge-font-size, 24px); font-family: var(--so-badge-font-family, inherit); font-weight: 600; }
/* Shape presets — exactly one at a time */
.so-badge-shape-circle  { border-radius: 50%; }
.so-badge-shape-rounded { border-radius: 12px; }
.so-badge-shape-pill    { border-radius: 999px; }
.so-badge-shape-square  { border-radius: 0; }
.so-badge-shape-blob    { border-radius: 42% 58% 63% 37% / 41% 44% 56% 59%; }
/* Border — opt-in */
.so-badge-bordered {
  border: var(--so-badge-border-width, 1px) solid var(--so-badge-border-color, #d1d5db);
}
/* Shadow presets — "none" = no class */
.so-badge-shadow-soft   { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
.so-badge-shadow-medium { box-shadow: 0 6px 16px rgba(0, 0, 0, 0.14); }

/* Hover effects — added 2026-06-27 for features-icon-cards-cta block.
   Class so-badge-hover-{name} on the badge; the modal's "Hover Effect"
   select writes the class via setHover() in badge.js. All effects use
   the transition declared below so they animate smoothly on enter/leave.
   CSS-only — no JS. */
.so-badge {
  transition: transform 0.4s ease-in-out, border-radius 0.4s ease-in-out,
              box-shadow 0.3s ease-out, background-color 0.3s ease-out,
              color 0.3s ease-out, border-color 0.3s ease-out;
}
.so-badge-hover-lift:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.12);
}
/* Invert: fill bg with the icon color, swap icon to WHITE, clear border.
   NOT a literal --so-badge-bg ↔ --so-badge-color swap — that broke the
   common case (bg starts transparent → swap makes icon transparent =
   invisible on hover, JC repro 2026-06-27). Always-white-on-color is
   the canonical 'icon-circle hover-fill' pattern from Ayro/Bootstrap
   reference designs. If a merchant wants a different hover color, use
   the Style tab to add their own :hover rule. */
.so-badge-hover-invert:hover {
  background-color: var(--so-badge-color, #1e40af);
  color: #ffffff;
  border-color: transparent;
}
.so-badge-hover-invert:hover svg { color: #ffffff; stroke: #ffffff; }
.so-badge-hover-grow:hover {
  transform: scale(1.1);
}
.so-badge-hover-glow:hover {
  box-shadow: 0 0 24px 4px var(--so-badge-color, rgba(30, 64, 175, 0.5));
}

/* Morph — square → circle + full 360° rotation.
   Added 2026-07-01 for features-icon-cards-morph block. Pairs with the
   Trigger-mode dropdown (so-badge-trigger-* classes) so the merchant can
   choose whether the effect fires from direct badge hover, ancestor
   card hover, or both.

   Trigger mode → matching selector:
     default (no trigger class) | 'both'  → direct badge:hover
     'card'                     | 'both'  → .so-card:hover descendant

   Two rules cover all three modes:
     1. Direct badge hover — excludes .so-badge-trigger-card so a
        merchant setting trigger='card' won't get a direct-hover
        reaction (the badge stays passive; only the card hover fires).
     2. Ancestor .so-card:hover — descendant selector picks up any
        morph badge with trigger 'card' or 'both'. This is the
        card→icon propagation pattern JC asked about; pure CSS, no JS,
        no user-side "linking" step — just drop the badge inside a card
        with trigger set to 'card' or 'both' and the coupling is automatic.

   The border-radius + transform declarations animate via the transition
   on .so-badge above (border-radius + transform are both in the list). */
.so-badge-hover-morph:not(.so-badge-trigger-card):hover {
  border-radius: 50%;
  transform: rotate(360deg);
}
.so-card:hover .so-badge-hover-morph.so-badge-trigger-card,
.so-card:hover .so-badge-hover-morph.so-badge-trigger-both {
  border-radius: 50%;
  transform: rotate(360deg);
}


/* (Icon + Emoji picker modal CSS moved VERBATIM to assets/editor4/e4-dialogs.css
   on 31 Aug 2026 — Stage-5 §9 repair: these are EDITOR-CHROME dialogs, and this
   file is published to merchant storefronts via _release_assets.php. No
   merchant-facing selector was removed; the pickers are used only by
   js/editor4/ui/icon-picker-modal.js and emoji-picker-modal.js.) */

/* =====================================================================
   SO-SHAPE-DIVIDER — Decorative SVG section edge overlay
   Position: absolute within parent section (position: relative).
   Pointer-events: none so it never blocks content interaction.
   ===================================================================== */

/* Shape Divider — data-attribute selectors (no GrapesJS classes).
   Avoids getModelToStyle routing styles to a CSS rule instead of inline,
   which broke the built-in GrapesJS resizer. */
[data-so-component="shape-divider"] {
  position: absolute;
  left: 0;
  width: 100%;
  height: 100px;
  line-height: 0;
  pointer-events: auto;
  overflow: hidden;
  z-index: 2;  /* accent=0, rows=1, shape-divider=2 */
}

/* SVG + paths transparent to pointer events — clicks land on the wrapper div
   for GrapesJS selection, while not intercepting via the SVG fill area. */
[data-so-component="shape-divider"] svg,
[data-so-component="shape-divider"] path {
  pointer-events: none;
}

[data-so-component="shape-divider"] svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Position anchoring */
[data-so-sd-position="top"] {
  top: 0;
  bottom: auto;
}
[data-so-sd-position="bottom"] {
  top: auto;
  bottom: 0;
}

/* Flip transforms — applied to WRAPPER DIV (not SVG) to avoid
   CSS specificity conflicts with the inverted sub-pixel fix on SVG. */
[data-so-sd-flip-v="1"] {
  transform: scaleY(-1);
}
[data-so-sd-flip-h="1"] {
  transform: scaleX(-1);
}
[data-so-sd-flip-v="1"][data-so-sd-flip-h="1"] {
  transform: scale(-1, -1);
}

/* Inverted shapes: fill covers full area except the cutout.
   Sub-pixel translateY closes the gap between divider and section edge.
   This is on the SVG element — no conflict with wrapper flip transforms. */
/* Shape dividers are decorative — let pointer events fall through to the
   content underneath in preview/published view. In the editor (body.editor-mode)
   they must remain clickable so merchants can select and edit them. The
   `!important` is required because existing saved tenants may carry an inline
   `pointer-events: auto` from the pre-fix model default. */
[data-gjs-type="so-shape-divider"] {
  pointer-events: none !important;
}
.editor-mode [data-gjs-type="so-shape-divider"] {
  pointer-events: auto !important;
}

[data-so-sd-position="top"] .so-shape-divider-inv {
  transform: translateY(-1px);
}
[data-so-sd-position="bottom"] .so-shape-divider-inv {
  transform: translateY(1px);
}


/* =====================================================================
   SO-ACCENT-LAYER — Decorative background shape for sections.
   Visual-only. Pointer-events: none. No layout effect.
   Type = data-attribute → shape rules (scoped to component identity).
   Position + size + opacity = inline style (per-instance).
   z-index stacking: accent=0, rows=1, shape-divider=2.
   ===================================================================== */

/* Parent section setup when accent is active */
.so-section.so-accent-enabled {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* Rows promoted to z-index:1 inside accent-enabled sections */
.so-section.so-accent-enabled > .so-row {
  position: relative;
  z-index: 1;
}

/* Base accent layer — shared defaults (inline styles handle per-instance values) */
[data-so-component="accent-layer"] {
  position: absolute;
  z-index: 0;
  pointer-events: none;
}

/* Type: circle */
[data-so-component="accent-layer"][data-so-accent-type="circle"] {
  border-radius: 50%;
  background: currentColor;
}

/* Type: square */
[data-so-component="accent-layer"][data-so-accent-type="square"] {
  border-radius: 0;
  background: currentColor;
}

/* Type: blob (organic shape via asymmetric border-radius) */
[data-so-component="accent-layer"][data-so-accent-type="blob"] {
  border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
  background: currentColor;
}

/* Type: blob-2 */
[data-so-component="accent-layer"][data-so-accent-type="blob-2"] {
  border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%;
  background: currentColor;
}

/* Type: blob-3 */
[data-so-component="accent-layer"][data-so-accent-type="blob-3"] {
  border-radius: 40% 60% 70% 30% / 50% 60% 40% 50%;
  background: currentColor;
}

/* Type: blob-4 */
[data-so-component="accent-layer"][data-so-accent-type="blob-4"] {
  border-radius: 72% 28% 47% 53% / 33% 62% 38% 67%;
  background: currentColor;
}

/* Type: blob-soft (smooth, pillow-like) */
[data-so-component="accent-layer"][data-so-accent-type="blob-soft"] {
  border-radius: 45% 55% 60% 40% / 55% 45% 55% 45%;
  background: currentColor;
}

/* Type: blob-sharp (angular organic) */
[data-so-component="accent-layer"][data-so-accent-type="blob-sharp"] {
  border-radius: 20% 80% 30% 70% / 70% 20% 80% 30%;
  background: currentColor;
}

/* Type: organic-1 (irregular polygon) */
[data-so-component="accent-layer"][data-so-accent-type="organic-1"] {
  clip-path: polygon(30% 0%, 70% 10%, 100% 40%, 80% 90%, 30% 100%, 0% 60%);
  background: currentColor;
}

/* Type: organic-2 (amoeba-like) */
[data-so-component="accent-layer"][data-so-accent-type="organic-2"] {
  clip-path: polygon(25% 5%, 65% 0%, 90% 20%, 100% 55%, 85% 85%, 50% 100%, 10% 80%, 0% 45%);
  background: currentColor;
}

/* Type: organic-3 (splat / star-like) */
[data-so-component="accent-layer"][data-so-accent-type="organic-3"] {
  clip-path: polygon(50% 0%, 65% 30%, 100% 35%, 75% 58%, 82% 100%, 50% 75%, 18% 100%, 25% 58%, 0% 35%, 35% 30%);
  background: currentColor;
}

/* Type: organic-4 (pebble) */
[data-so-component="accent-layer"][data-so-accent-type="organic-4"] {
  clip-path: polygon(20% 5%, 80% 0%, 95% 30%, 100% 70%, 75% 95%, 30% 100%, 5% 75%, 0% 30%);
  background: currentColor;
}

/* Type: organic-5 (teardrop / leaf) */
[data-so-component="accent-layer"][data-so-accent-type="organic-5"] {
  clip-path: polygon(50% 0%, 85% 15%, 100% 50%, 85% 85%, 50% 100%, 20% 90%, 0% 55%, 15% 15%);
  background: currentColor;
}

/* Type: image (background-image set via inline style on the component) */
[data-so-component="accent-layer"][data-so-accent-type="image"] {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* Editor-mode override for parallax sections.
 * In preview/published view, jarallax extracts the section's bg image into
 * its own wrapper and applies `imgSize: cover` — so the image scales to
 * fill the entire section. In the editor canvas jarallax doesn't run, so
 * the section's saved `background-size: auto` would render the image at
 * its natural pixel size and only partially fill the section. This rule
 * mirrors jarallax's cover behaviour so editor and preview match. We
 * scope it to .editor-mode so the production output is unchanged. */
body.editor-mode .so-section.jarallax {
  background-size: cover !important;
  background-position: center center !important;
  background-repeat: no-repeat !important;
}

/* GrapesJS Map component — fill the column in BOTH editor and preview.
 * In the editor canvas GrapesJS injects `style="width:100%; height:100%"`
 * on the rendered iframe AND wraps it in a `<div data-gjs-type="map">`,
 * so the map fills its parent. BUT when GrapesJS serializes to HTML it
 * strips both the wrapper div AND the inline width/height, leaving a
 * bare `<iframe>` with no sizing at all. Preview then renders it at the
 * iframe browser-default 300x150 — looks tiny. Target the iframe by its
 * URL (every map embed uses maps.google.com/maps) so the rule applies
 * to the saved output. Also covers the editor case if the data-gjs-type
 * is still present. Height defaults to 350px to match the b-map block's
 * intended size; the merchant can override per-instance via Style Manager. */
iframe[src*="maps.google.com/maps"],
iframe[data-gjs-type="map"] {
  width: 100%;
  height: 350px;
  border: 0;
  display: block;
}


/* =====================================================================
   SO-COL GAP — Vertical rhythm between sibling children of a column.
   UPGRADED 2026-06-16 from margin-based (`> * + * { margin-top }`) to
   real CSS gap via flex container. This means the gap is OWNED BY THE
   COLUMN (parent), not faked via per-child margin — eliminating dead
   space between siblings. Clicking in the gap selects the column.

   Opt-in: only columns with `so-col-gap-*` class become flex containers.
   Columns without the class keep default block flow (no behavioral
   change for existing blocks). Inline styles can still override at
   higher specificity. No !important.

   See EDITOR4_TOKEN_BAKE_ON_DROP_ARCHITECTURE.md §12 for the spacing
   doctrine + chrome model.
   ===================================================================== */
.so-col.so-col-gap-xs,
.so-col.so-col-gap-sm,
.so-col.so-col-gap-md,
.so-col.so-col-gap-lg,
.so-col.so-col-gap-xl,
.so-col.so-col-gap-xxl,
.so-col.so-col-gap-xxxl,
.so-col.so-col-gap-4xl {
  display: flex;
  flex-direction: column;
}

.so-col.so-col-gap-xs   { gap: var(--space-xs); }
.so-col.so-col-gap-sm   { gap: var(--space-sm); }
.so-col.so-col-gap-md   { gap: var(--space-md); }
.so-col.so-col-gap-lg   { gap: var(--space-lg); }
.so-col.so-col-gap-xl   { gap: var(--space-xl); }
.so-col.so-col-gap-xxl  { gap: var(--space-xxl); }
.so-col.so-col-gap-xxxl { gap: var(--space-xxxl); }
.so-col.so-col-gap-4xl  { gap: var(--space-4xl); }


/* =====================================================================
   SO-DIVIDER — Line primitive (Basic family). v2 (2026-06-16) — switched
   from background-image rendering to border-based rendering so dashed /
   dotted variants work natively via border-style. Token Bake border-*
   family (color/width/style) wires directly to the visible line.

   Default: horizontal. .so-divider--vertical class swaps to vertical.
   Vertical dividers live inside an Inline Group with horizontal direction
   (the divider stretches to fill the group's height via flex).

   Editor-mode min-height makes the divider selectable even at 1px.
   ===================================================================== */
/* Use CSS custom properties so editor-mode ::before pseudo + publish border
   share one source of truth for color/thickness/style. Settings popover
   writes to these vars. Avoids the border-shorthand-applies-to-all-4-sides
   problem we hit when writing border-color/width/style longhand sets. */
.so-divider {
  --sd-color:     rgba(0, 0, 0, 0.08);
  --sd-thickness: 1px;
  --sd-style:     solid;

  display: block;
  width: 100%;
  height: 0;
  border: 0;
  border-top: var(--sd-thickness) var(--sd-style) var(--sd-color);
}

.so-divider.so-divider--vertical {
  display: block;
  width: 0;
  height: auto;
  align-self: stretch;
  min-height: 20px;
  border: 0;
  border-left: var(--sd-thickness) var(--sd-style) var(--sd-color);
}

/* Editor mode: hit area expansion + centered ::before line.
   The actual border (border-top for horizontal, border-left for vertical)
   is hidden via color: transparent in editor; ::before paints the visible
   line centered using the same CSS vars (so editor visual matches publish). */
body.editor-mode .so-divider {
  min-height: 16px;
  position: relative;
  border-top-color: transparent;
}
body.editor-mode .so-divider::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 0;
  border-top: var(--sd-thickness) var(--sd-style) var(--sd-color);
  transform: translateY(-50%);
}

body.editor-mode .so-divider.so-divider--vertical {
  min-width: 16px;
  border-top-color: initial;     /* horizontal-mode editor rule doesn't apply here */
  border-left-color: transparent;
}
body.editor-mode .so-divider.so-divider--vertical::before {
  top: 0;
  bottom: 0;
  left: 50%;
  right: auto;
  width: 0;
  height: auto;
  border-top: 0;
  border-left: var(--sd-thickness) var(--sd-style) var(--sd-color);
  transform: translateX(-50%);
}


/* =====================================================================
   SO-SPACER — Intentional blank space primitive (Layout family).
   Added 2026-06-16. Empty block with merchant-controlled height. Used
   for one-off intentional gaps (NOT the primary rhythm system — for
   sibling rhythm in a column, use so-col-gap-* instead).
   Editor-only faint stripe pattern marks it as deliberate space, not
   missing content. Stripes invisible in publish.
   ===================================================================== */
.so-spacer {
  display: block;
  width: 100%;
  /* Height set by inline style; merchant edits via SM Height field */
}

body.editor-mode .so-spacer {
  /* Idle = visually transparent. Spacer keeps its space (height comes
     from inline style) but no chrome marks it. min-height guarantees
     a clickable target even at tiny heights — the stripe affordance
     only appears once the cursor enters its bbox or it's selected. */
  min-height: 8px;
}

body.editor-mode .so-spacer:hover,
body.editor-mode .so-spacer.gjs-hovered,
body.editor-mode .so-spacer.gjs-selected {
  background-image: repeating-linear-gradient(
    45deg,
    rgba(120, 160, 255, 0.13),
    rgba(120, 160, 255, 0.13) 4px,
    transparent 4px,
    transparent 10px
  );
  outline: 1px dashed rgba(120, 160, 255, 0.25);
  outline-offset: -1px;
}


/* =====================================================================
   ROW HORIZONTAL GAP — Controls Bootstrap gutter-x via spacing scale.
   Uses --space-* tokens. Preserves 12-column grid math.
   No !important. No margin hacks. No manual padding changes.
   ===================================================================== */
.so-row.so-row-gap-xs { --bs-gutter-x: var(--space-xs); }
.so-row.so-row-gap-sm { --bs-gutter-x: var(--space-sm); }
.so-row.so-row-gap-md { --bs-gutter-x: var(--space-md); }
.so-row.so-row-gap-lg { --bs-gutter-x: var(--space-lg); }
.so-row.so-row-gap-xl { --bs-gutter-x: var(--space-xl); }
.so-row.so-row-gap-xxl { --bs-gutter-x: var(--space-xxl); }
.so-row.so-row-gap-xxxl { --bs-gutter-x: var(--space-xxxl); }
.so-row.so-row-gap-4xl { --bs-gutter-x: var(--space-4xl); }


/* =====================================================================
   SECTION VERTICAL ROW GAP — Space between sibling rows.
   Applies margin-top to subsequent .so-row only.
   Does not affect section padding or nested rows.

   NOTE (1 Jul 2026): switched from `>` (direct child) to descendant
   selector because many blocks wrap section content in a
   `<div class="container">` for width-constrained content inside a
   `container-fluid` section (common Bootstrap pattern). With the
   `>` selector, .so-row was not a direct child of .so-section and
   the row gap dropdown did nothing visible. Descendant selector is
   safe because `.so-row` is a class emitted only by the grid plugin
   at the top layout level — components never render `.so-row`
   internally (they use their own scoped classes like
   `.so-form-row`, `.so-subscribe-row`, etc.).
   ===================================================================== */
.so-section.so-section-row-gap-xs .so-row + .so-row { margin-top: var(--space-xs); }
.so-section.so-section-row-gap-sm .so-row + .so-row { margin-top: var(--space-sm); }
.so-section.so-section-row-gap-md .so-row + .so-row { margin-top: var(--space-md); }
.so-section.so-section-row-gap-lg .so-row + .so-row { margin-top: var(--space-lg); }
.so-section.so-section-row-gap-xl .so-row + .so-row { margin-top: var(--space-xl); }
.so-section.so-section-row-gap-xxl .so-row + .so-row { margin-top: var(--space-xxl); }
.so-section.so-section-row-gap-xxxl .so-row + .so-row { margin-top: var(--space-xxxl); }
.so-section.so-section-row-gap-4xl .so-row + .so-row { margin-top: var(--space-4xl); }


/* =====================================================================
   EDITOR VISUAL AIDS — Section Row Gap Hatching
   Diagonal stripe overlay shown in editor canvas only, on hover/select.
   Purely cosmetic. No layout shift. No drag interference.
   Selectors updated to match the descendant convention above.
   ===================================================================== */
.editor-mode .so-section[class*="so-section-row-gap-"]:hover .so-row + .so-row,
.editor-mode .so-section[class*="so-section-row-gap-"].gjs-selected .so-row + .so-row {
  position: relative;
}

/* Base pseudo — shared hatching pattern + pointer passthrough */
.editor-mode .so-section[class*="so-section-row-gap-"]:hover > .so-row + .so-row::before,
.editor-mode .so-section[class*="so-section-row-gap-"].gjs-selected > .so-row + .so-row::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  z-index: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    45deg,
    rgba(120, 160, 255, 0.08),
    rgba(120, 160, 255, 0.08) 4px,
    transparent 4px,
    transparent 8px
  );
}

/* Per-size height + top offset (must match the margin-top gap value) */
.editor-mode .so-section.so-section-row-gap-xs:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-xs.gjs-selected > .so-row + .so-row::before {
  height: var(--space-xs); top: calc(-1 * var(--space-xs));
}
.editor-mode .so-section.so-section-row-gap-sm:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-sm.gjs-selected > .so-row + .so-row::before {
  height: var(--space-sm); top: calc(-1 * var(--space-sm));
}
.editor-mode .so-section.so-section-row-gap-md:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-md.gjs-selected > .so-row + .so-row::before {
  height: var(--space-md); top: calc(-1 * var(--space-md));
}
.editor-mode .so-section.so-section-row-gap-lg:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-lg.gjs-selected > .so-row + .so-row::before {
  height: var(--space-lg); top: calc(-1 * var(--space-lg));
}
.editor-mode .so-section.so-section-row-gap-xl:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-xl.gjs-selected > .so-row + .so-row::before {
  height: var(--space-xl); top: calc(-1 * var(--space-xl));
}
.editor-mode .so-section.so-section-row-gap-xxl:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-xxl.gjs-selected > .so-row + .so-row::before {
  height: var(--space-xxl); top: calc(-1 * var(--space-xxl));
}
.editor-mode .so-section.so-section-row-gap-xxxl:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-xxxl.gjs-selected > .so-row + .so-row::before {
  height: var(--space-xxxl); top: calc(-1 * var(--space-xxxl));
}
.editor-mode .so-section.so-section-row-gap-4xl:hover > .so-row + .so-row::before,
.editor-mode .so-section.so-section-row-gap-4xl.gjs-selected > .so-row + .so-row::before {
  height: var(--space-4xl); top: calc(-1 * var(--space-4xl));
}

/* =====================================================================
   FORM COMPONENT (so-form)
   ===================================================================== */

/* Note: rules below also target `.so-form-widget` (the legacy class
   name used in the original form-widget commits) so saved pages still
   pick up the styling after the rename. The runtime in
   so-form-runtime.js applies the CSS variables on either class. */

/* Form component visual styling. Reads CSS custom properties set on the
   wrapper by form.js styleVars() — see ATOMIC_COMPONENT_PATTERN.md.
   Style-only edits in the modal Style tab update those variables
   directly (one inline-style write), no HTML re-parse needed.
   The `<style>` tag we tried previously gets stripped by GrapesJS
   during HTML parse, which is why we use CSS vars instead.
   `!important` on every property to win against stale literals that
   earlier form.js versions serialised into per-tenant CustomStyles.css
   via addStyle() (which under selectorManager.componentFirst writes
   to a #id rule in the editor stylesheet that gets persisted). The
   current code uses setProperty only, so no new literals can be
   written, but legacy tenants may still carry them. */
/* Extra bottom padding reserves vertical space so the form's box and
 * its bg image crop look consistent regardless of whether a success or
 * error message is currently rendered. In editor the auto-typed success
 * preview adds ~40px; this pad gives preview the same visual footprint
 * when neither message is showing. When a real message DOES render via
 * the runtime, it sits inside this reserved zone instead of extending
 * the form box further. */
/* .so-form fills its parent column so the inner can flex to fill height.
 * When merchants set a min-height on the wrapping column (via Style
 * Manager) the form-inner now stretches to match — otherwise the bg
 * image would end at form-inner's content height and leave a gap below
 * where the section bg shows through.
 *
 * 7 Sep 2026 (JC: form dropped beside a fullscreen slider → the slider
 * spilled over the sections below; alone in a column it was fine): the
 * stretch is now scoped to a form that is the SOLE direct child of its
 * column — the same rule the card uses (.so-col > .so-card:only-child).
 * A Bootstrap column is a stretched flex item, so its height counts as
 * definite for its children; an unconditional min-height:100% made the
 * form grow by its SIBLINGS' height (form + slider), overflowing the
 * column — the accordion hit the same trap on 2 Jul 2026. With siblings
 * the form now takes its natural height; the merchant-min-height case
 * (form alone in the column) still stretches. */
.so-form, .so-form-widget {
  display: flex;
  flex-direction: column;
}
.so-col > .so-form:only-child, .so-col > .so-form-widget:only-child {
  min-height: 100%;
}
.so-form .so-form-inner, .so-form-widget .so-form-inner {
  position: relative !important;
  flex: 1 1 auto;
  /* Layer order: image is the BACK layer (rendered directly on the
     inner element), colour sits on TOP as a ::before overlay. The
     colour's own alpha controls blend — opaque colour fully hides
     the image; semi-transparent colour acts as a tint on the photo;
     fully transparent colour lets the image show through unaltered.
     This matches the universal "image with colour overlay" mental
     model used by Figma / hero-section design patterns. */
  background-image: var(--so-form-bg-image, none) !important;
  background-size: var(--so-form-bg-image-size, cover) !important;
  background-repeat: var(--so-form-bg-image-repeat, no-repeat) !important;
  background-position: center !important;
  padding: 24px !important;
  border-radius: 6px !important;
  overflow: hidden !important;
  box-sizing: border-box !important;
}
.so-form .so-form-inner::before, .so-form-widget .so-form-inner::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--so-form-bg, transparent);
  pointer-events: none;
  z-index: 0;
  border-radius: inherit;
}
.so-form .so-form-inner > *, .so-form-widget .so-form-inner > * {
  position: relative;
  z-index: 1;
}
.so-form .so-form-title, .so-form-widget .so-form-title {
  color: var(--so-form-title-color, #212529) !important;
  font-size: var(--so-form-title-size, 28px) !important;
  font-family: var(--so-form-title-font, inherit) !important;
  margin: 0 0 20px 0 !important;
  font-weight: 600 !important;
}
.so-form .so-form-field, .so-form-widget .so-form-field { margin-bottom: 16px !important; }
.so-form .so-form-label, .so-form-widget .so-form-label {
  color: var(--so-form-label-color, #212529) !important;
  font-size: var(--so-form-label-size, 14px) !important;
  font-family: var(--so-form-label-font, inherit) !important;
  display: block !important;
  margin: 0 0 6px 0 !important;
  font-weight: 500 !important;
}
.so-form .so-form-input, .so-form-widget .so-form-input {
  width: 100% !important;
  box-sizing: border-box !important;
  padding: 8px 12px !important;
  background: var(--so-form-field-bg, #ffffff) !important;
  color: var(--so-form-field-text, #212529) !important;
  border: 1px solid var(--so-form-field-border, #d1d5db) !important;
  border-radius: var(--so-form-field-radius, 4px) !important;
  font-size: var(--so-form-label-size, 14px) !important;
  font-family: var(--so-form-label-font, inherit) !important;
}
.so-form .so-form-textarea, .so-form-widget .so-form-textarea { resize: vertical !important; min-height: 96px !important; }
.so-form .so-form-checkbox-row, .so-form-widget .so-form-checkbox-row,
.so-form .so-form-radio-row, .so-form-widget .so-form-radio-row {
  display: inline-flex !important;
  align-items: center !important;
  gap: 8px !important;
  margin-right: 14px !important;
  color: var(--so-form-label-color, #212529) !important;
  font-size: var(--so-form-label-size, 14px) !important;
  font-family: var(--so-form-label-font, inherit) !important;
}
.so-form .so-form-submit, .so-form-widget .so-form-submit {
  background: var(--so-form-button-bg, #0d6efd) !important;
  color: var(--so-form-button-text, #ffffff) !important;
  font-size: var(--so-form-button-size, 14px) !important;
  font-family: var(--so-form-button-font, inherit) !important;
  border: 1px solid var(--so-form-button-border, transparent) !important;
  padding: 10px 24px;
  border-radius: var(--so-form-field-radius, 4px);
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.15s, color 0.15s, border-color 0.15s;
}
.so-form .so-form-submit:hover, .so-form-widget .so-form-submit:hover {
  background: var(--so-form-button-bg-hover, var(--so-form-button-bg, #0b5ed7)) !important;
  color: var(--so-form-button-text-hover, var(--so-form-button-text, #ffffff)) !important;
  border-color: var(--so-form-button-border-hover, var(--so-form-button-border, transparent)) !important;
  /* hover step (7 Sep 2026, JC: "make it more prominent"): the default hover pair stays
     Primary/Surface — never a second colour pairing to audit — the STATE is shown by a
     brightness lift, a 3px ring in the button's own colour and a 1px rise (visible on
     light AND dark themes; the ring uses color-mix, older browsers keep the lift). */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-form-button-bg-hover, var(--so-form-button-bg, #0b5ed7)) 35%, transparent), 0 6px 14px rgba(0, 0, 0, 0.18) !important;
  transform: translateY(-1px) !important;   /* the brightness filter is gone (ChatGPT #303 ruling E, 8 Sep): the hover COLOUR comes from the Auto companion; a filter would recolour the text too */
}
/* Success message — hidden until a successful submit. Runtime swaps
 * display to 'block'. In editor mode it shows by default (the grid
 * auto-typing rule flips display:block !important on non-empty text). */
.so-form .so-form-success, .so-form-widget .so-form-success { display: none; color: #16a34a; margin-top: 16px; }

/* Asterisk inline guarantee (see so-bootstrap-grid.js:2081 — the rule
   that competes). Class-chain specificity 0,4,0 beats it at 0,3,0. */
.so-form .so-form-field [data-gjs-type="text"].so-form-req,
.so-form-widget .so-form-field [data-gjs-type="text"].so-form-req {
  display: inline !important;
  color: #dc2626 !important;
  margin-left: 3px !important;
  padding: 0 !important;
}

/* Busy state shown during the expensive renderForm path (full re-parse).
   Style-only updates hit a fast path and never set this class.
   Strengthened 27 May 2026 — the old 0.6 opacity + 28px spinner was too
   subtle (per user feedback "loading need to be more obvious"). Now:
   high-coverage white overlay + dark centred badge with text + pulse. */
.so-form.so-form-busy,
.so-form-widget.so-form-busy {
  position: relative;
  pointer-events: none;
}
/* Overlay backdrop — dims the form contents so the badge below stands out */
.so-form.so-form-busy::before,
.so-form-widget.so-form-busy::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.92);
  z-index: 10;
  border-radius: inherit;
}
/* Centred badge with label + hourglass icon */
.so-form.so-form-busy::after,
.so-form-widget.so-form-busy::after {
  content: '⏳ Updating form…';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #1f2937;
  color: #ffffff;
  padding: 14px 24px;
  border-radius: 8px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.2px;
  z-index: 11;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  white-space: nowrap;
  animation: so-form-busy-pulse 1.2s ease-in-out infinite alternate;
}
@keyframes so-form-busy-pulse {
  from { opacity: 0.85; transform: translate(-50%, -50%) scale(0.98); }
  to   { opacity: 1.0;  transform: translate(-50%, -50%) scale(1.00); }
}

/* =================================================================
   FEATURED PRODUCTS COMPONENT
   Moved from JS injection (was: ensureFeaturedCssInCanvas in
   js/editor4/components/featured-products.js) + _preview.php heredoc
   to here on 24 May 2026 — single source of truth (Constitution Law 11).
   Editor canvas AND preview/published pages both load this stylesheet,
   so no more drift bugs like the 24 May 2026 3-col-wrap (gap vs
   --bs-gutter-x) or dark-border (phantom var-name) regressions.
   ================================================================= */

.so-shop-featured-products { width: 100%; position: relative; }

/* .so-fp-inner — Ephemeral Preview consumer. The renderer puts all
   customisable values as CSS variables on this element's style
   attribute (see featured-products.js → buildFPInnerStyle). The rules
   below consume them via var(…, fallback). No per-element inline
   styles. See EDITOR4_INTERACTION_DOCTRINE.md §4.4. */
.so-fp-inner {
  width: 100%;
  background: var(--so-fp-bg, transparent);
  padding: var(--so-fp-pad, 0);
}
.so-fp-title {
  /* margin: 0 zeroes top/right/left; margin-bottom is then set by var. */
  margin: 0;
  margin-bottom: var(--so-fp-title-gap, 24px);
  padding: 0;
  line-height: 1.3;
  color: var(--so-fp-title-color, inherit);
  font-family: var(--so-fp-title-font, inherit);
  font-size: var(--so-fp-title-size, 24px);
  text-align: var(--so-fp-title-align, left);
}
.so-fp-title.hidden { display: none; }

.so-fp-product-row { display: flex; flex-wrap: wrap; }
.so-fp-card-col { margin-bottom: 0; }
.so-fp-link { text-decoration: none; color: inherit; display: block; }
.so-fp-link:hover { text-decoration: none; color: inherit; }
.so-fp-card {
  background: var(--so-product-card-bg, #fff);
  border: 1px solid var(--so-product-card-border, transparent);
  border-radius: var(--so-product-card-radius, 6px);
  overflow: hidden;
  transition: box-shadow 0.2s ease, transform 0.2s ease;
  height: 100%;
  display: flex;
  flex-direction: column;
}
.so-fp-card:hover {
  box-shadow: var(--so-product-card-hover-shadow, 0 4px 16px rgba(0,0,0,0.1));
  transform: translateY(-2px);
}
.so-fp-image-wrapper {
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f8f8f8;
}
.so-fp-image { max-width: 100%; max-height: 100%; object-fit: contain; }
.so-fp-info {
  padding: 12px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 4px;
  transition: background 0.2s ease;
}
.so-fp-card:hover .so-fp-info { background: var(--so-product-card-info-hover-bg, transparent); }
.so-fp-name {
  font-family: var(--so-product-title-font, inherit);
  font-size: var(--so-product-title-font-size, 14px);
  color: var(--so-product-title-color, #333);
  font-weight: 500;
  line-height: 1.3;
  display: block;
}
.so-fp-price {
  font-family: var(--so-product-price-font, inherit);
  font-size: var(--so-product-price-font-size, 16px);
  color: var(--so-product-price-color, #333);
  font-weight: 700;
  display: block;
}
.so-fp-was-price { font-size: 13px; text-decoration: line-through; opacity: 0.6; display: block; }

/* Empty + Loading states (editor-only in practice — preview server-renders
   real cards — but harmless to ship since preview never matches these classes). */
.so-fp-empty, .so-fp-loading {
  padding: 60px 20px;
  text-align: center;
  color: #999;
  background: #f9f9f9;
  border: 2px dashed #ddd;
  border-radius: 8px;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.so-fp-empty-text { font-size: 14px; }
.so-fp-loading-text { font-size: 13px; }
.so-fp-spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #ddd;
  border-top-color: #666;
  border-radius: 50%;
  animation: soFpSpin 0.8s linear infinite;
}
@keyframes soFpSpin { to { transform: rotate(360deg); } }
.so-fp-loading-overlay {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(255,255,255,0.85);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  z-index: 10;
  cursor: wait;
  border-radius: 8px;
}

/* Responsive — only at true mobile widths so the merchant's column
   setting wins on tablet AND in editor canvas (which sits around
   700-900px due to side panel chrome and would otherwise trip a
   >=768px breakpoint). Affects both modern (so-fp-card-col) and
   standard ([data-so-role="product-card-col"]) cards. */
@media (max-width: 575.98px) {
  .so-fp-card-col,
  .so-shop-featured-products [data-so-role="product-card-col"] {
    flex: 0 0 50%;
    max-width: 50%;
  }
}
@media (max-width: 400px) {
  .so-fp-card-col,
  .so-shop-featured-products [data-so-role="product-card-col"] {
    flex: 0 0 100%;
    max-width: 100%;
  }
}

/* Standard card structural layout. Tokens (shop-tokens.css) own
   colours/typography/borders/shadows/badges. These rules cover the
   structural layout needed on non-shop pages (where shop-layout.css
   isn't loaded). NOTE: no `border` here — shop-tokens.css owns it
   via [data-so-role="product-card"] so featured-products visually
   matches the categories page (single source of truth). */
.so-shop-featured-products [data-so-role="product-card"] {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  transition: box-shadow 0.2s ease;
}
.so-shop-featured-products [data-so-role="product-image-wrapper"] {
  text-align: center;
  padding: 10px;
  background-color: var(--so-product-card-bg, transparent);
}
.so-shop-featured-products [data-so-role="product-image-cell"] {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--so-product-image-max-height, 180px);
}
.so-shop-featured-products [data-so-role="product-image"] {
  max-height: var(--so-product-image-max-height, 180px);
  max-width: 100%;
  height: auto;
  object-fit: contain;
}
.so-shop-featured-products [data-so-role="product-card-bg"] { padding: 10px; border-radius: 0 0 5px 5px; }
.so-shop-featured-products [data-so-role="product-link"] {
  text-decoration: none;
  color: inherit;
  display: flex;
  flex-direction: column;
  width: 100%;
}
.so-shop-featured-products [data-so-role="product-card-col"] { margin-bottom: 0; display: flex; }
.so-shop-featured-products [data-so-role="product-info-price"] { text-align: center; }

/* Token-consuming rules absent from CustomStyles.css on non-shop
   pages (only loaded for shop shells). */
.so-shop-featured-products [data-so-role="product-grid-title"] {
  color: var(--so-product-title-color, #333);
  font-family: var(--so-product-title-font, inherit);
}
.so-shop-featured-products [data-so-role="product-card"] {
  background: var(--so-product-card-bg, #fff);
  border-radius: var(--so-product-card-radius, 5px);
  overflow: hidden;
}
.so-shop-featured-products [data-so-role="product-card"]:hover [data-so-role="product-card-bg"] {
  background: var(--so-product-card-info-hover-bg, transparent);
}
.so-shop-featured-products [data-so-role="price-current"] {
  color: var(--so-product-price-text, #28a745);
  font-size: var(--so-product-price-size, 1.1rem);
  font-weight: var(--so-product-price-weight, 700);
}
.so-shop-featured-products [data-so-role="price-was"] {
  color: var(--so-product-was-price-text, #999);
  text-decoration: line-through;
  font-size: 0.9em;
}
.so-shop-featured-products [data-so-role="stock-badge"] {
  position: absolute;
  top: 5px;
  right: 5px;
  padding: var(--so-stock-badge-padding, 4px 8px);
  border-radius: var(--so-stock-badge-radius, 3px);
  font-size: var(--so-stock-badge-size, 11px);
  font-weight: 600;
  text-transform: uppercase;
  z-index: 1;
}
.so-shop-featured-products [data-so-role="stock-badge"][data-stock-status="in-stock"] {
  background: var(--so-stock-in-bg, #28a745);
  color: var(--so-stock-in-text, #fff);
}
.so-shop-featured-products [data-so-role="stock-badge"][data-stock-status="out-of-stock"] {
  background: var(--so-stock-out-bg, #dc3545);
  color: var(--so-stock-out-text, #fff);
}
.so-shop-featured-products [data-so-role="product-card"]:hover {
  box-shadow: var(--so-product-card-hover-shadow, 0 4px 12px rgba(0,0,0,0.15));
}

/* =================================================================
   COUNTDOWN COMPONENT
   Moved from JS injection (was: injectCountdownStyles in
   js/editor4/components/countdown.js) to here on 24 May 2026 —
   single source of truth (Constitution Law 11). Previously the
   editor injected via editor.Css.addRules + canvas iframe head,
   so preview/published pages had NO countdown styling and fell
   back to browser defaults.
   ================================================================= */

.countdown {
  text-align: center;
  font-family: Helvetica, serif;
}
.countdown-block {
  display: inline-block;
  margin: 0 10px;
  padding: 10px;
}
/* Font sizes are em-relative + scoped under .so-countdown so the Style-tab
   font-size on the component (its #id rule, specificity 100) scales the whole
   countdown proportionally. The base on .so-countdown keeps the default look
   (80px digits); a merchant's font-size override wins via #id. Scoping
   (.so-countdown .x = specificity 20) also beats any stale unscoped
   .countdown-digit{font-size:rem} baked into older tenant CSS. */
.so-countdown {
  font-size: 16px;
}
.so-countdown .countdown-digit {
  font-size: 5em;
}
.so-countdown .countdown-label {
  font-size: 0.875em;
}
.so-countdown .countdown-endtext {
  font-size: 5em;
}
.countdown-cont {
  display: inline-block;
}

/* =================================================================
   TABS COMPONENT — so-tabs (custom, Free Composed)
   Shared editor + published. Doc: EDITOR4_TABS_ARCHITECTURE.md
   ================================================================= */
.so-tabs { display: block; width: 100%; }
.so-tabs-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  border-bottom: 2px solid rgba(0, 0, 0, 0.10);
}
.so-tabs-tab {
  font-family: var(--theme-font-primary);   /* theme-bound default (JC 6 Sep 2026) — matches tabs.js tabFont default */
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  padding: 10px 18px;
  font: inherit;
  font-size: 15px;
  line-height: 1.2;
  color: #6b7280;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: color 0.15s, border-color 0.15s;
}
.so-tabs-tab:hover { color: #111827; }
.so-tabs-tab.is-active {
  color: var(--theme-primary, #2563eb);               /* was the ghost token theme-color-primary (never emitted) — colour survey, 6 Sep 2026 */
  border-bottom-color: var(--theme-primary, #2563eb);
  font-weight: 600;
}
.so-tabs-panels { padding: 18px 4px; }
.so-tabs-panel { min-height: 40px; }
.so-tabs-panel[hidden] { display: none !important; }
/* Empty panels show NO idle hint (user choice) — the drag-time
   "Inside Tab · Drop Here" placeholder is the only drop affordance. We DO keep a
   comfortable min-height in the editor though: a short empty panel shrinks the
   interior into GrapesJS Sorter's 10px nearBorders band, which retargets the
   FIRST drop up to the parent (content never lands). A tall display:block box
   keeps the drop point clear of the edge bands. Editor-only (published panels
   use the base 40px). See EDITOR4_TABS_ARCHITECTURE.md (nearBorders gotcha). */
.editor-mode .so-tabs-panel:empty {
  display: block;
  min-height: 80px;
}

/* =================================================================
   CONTENT SLIDER COMPONENT — Free Composed (rewritten Phase A, Jun 2026)
   See ADR EDITOR4_CONTENT_SLIDER_ARCHITECTURE_DECISION_JUN2026.md.
   Structure: .so-content-slider.swiper > .so-content-slider-track.swiper-wrapper
              > .so-content-slide.swiper-slide  (free-composed content inside).
   Swiper runs ONLY in preview/published; the editor is static (CSS below).
   Single source of truth (Constitution Law 11) — never inject from JS.
   ================================================================= */

.so-content-slider { position: relative; width: 100%; }

/* Slide is a clean container — NO extra padding of its own (the grid's row/column
   gutters already provide spacing; adding slide padding double-insets content). */
.so-content-slide { box-sizing: border-box; }

/* Editor-only: Swiper is NOT running in the canvas, so neutralise its flex
   track and show ONLY the active slide (the switcher toggles .so-active-slide).
   Scoped to .editor-mode (canvas body) so nothing here ships to published. */
.editor-mode .so-content-slider {
  overflow: visible;        /* chrome sits above; content never clipped */
  height: auto !important;  /* GROW with content — override any stale inline /
                               Swiper height (e.g. a captured 363px) that pins the
                               shell shorter than its slides and clips them */
}
.editor-mode .so-content-slider .so-content-slider-track {
  display: block;
  transform: none !important;
  height: auto !important;       /* never inherit Swiper's height:100% in canvas */
  overflow: visible !important;
}
.editor-mode .so-content-slider .so-content-slide {
  display: none;              /* hidden by default */
  width: 100%;
}
.editor-mode .so-content-slider .so-content-slide.so-active-slide {
  display: block;             /* only the active slide is shown/editable */
  height: auto !important;       /* GROW with content — a tall image stretches the
                                    slide/row, never clipped behind a scrollbar */
  overflow: visible !important;
}

/* Editor-only PREVIEW of the published Swiper arrows + dots (injected in
   view.onRender; never exported). Mirrors how the controls will look on the
   live site so the merchant can see + position them. Pointer-events pass through
   the wrapper to the slide content; only the buttons are clickable. */
.so-cs-navpv {
  position: absolute;
  inset: 0;
  z-index: 15;
  pointer-events: none;
}
/* ── Slider nav ARROWS — SHARED across editor + published ──────────────────────
   The editor preview (.so-cs-arrow) AND the published Swiper buttons (which the
   runtime tags with .so-cs-arrow + the same style/size classes and feeds the
   same SVG) share these rules, so the canvas matches the live site by
   construction. Colour uses Swiper's own custom props so the future Styles tab
   drives both. Style = chevron | arrow | circle | square; size = small/med/large. */
.so-cs-arrow {
  border: none;
  background: transparent;
  color: var(--swiper-navigation-color, var(--swiper-theme-color, #007aff));
  cursor: pointer;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  opacity: 0.95;
  transition: opacity 0.15s, background 0.15s;
}
.so-cs-arrow:hover { opacity: 1; }
.so-cs-arrow svg { display: block; width: 30px; height: 30px; }
/* Editor preview positioning (Swiper isn't running in the canvas). */
.editor-mode .so-content-slider .so-cs-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
.editor-mode .so-content-slider .so-cs-arrow--prev { left: 12px; }
.editor-mode .so-content-slider .so-cs-arrow--next { right: 12px; }
/* Editor preview arrows are a VISUAL MIRROR only — slide navigation in the
   canvas is done via the floating control bar. Keep them click/hover-inert so
   they don't (a) flag the whole slider as hovered (purple overlay) or (b) cover
   the row rail handle sitting on the slide's left/right edge. Published Swiper
   buttons keep pointer-events:auto via the shared .so-cs-arrow rule above. */
.editor-mode .so-content-slider .so-cs-arrow { pointer-events: none; }
/* Published: keep Swiper's button for click logic, but show OUR visual — hide
   Swiper's font-icon ::after and neutralise its margin-based centring. */
.so-content-slider .swiper-button-prev.so-cs-arrow::after,
.so-content-slider .swiper-button-next.so-cs-arrow::after { content: none; display: none; }
.so-content-slider .swiper-button-prev.so-cs-arrow,
.so-content-slider .swiper-button-next.so-cs-arrow { margin-top: 0; transform: translateY(-50%); }
/* Sizes (!important so they beat Swiper's default button width). */
.so-cs-arrow.so-cs-arrow--sz-small  { width: 30px !important; height: 30px !important; }
.so-cs-arrow.so-cs-arrow--sz-small svg  { width: 20px; height: 20px; }
.so-cs-arrow.so-cs-arrow--sz-medium { width: 58px !important; height: 58px !important; }
.so-cs-arrow.so-cs-arrow--sz-medium svg { width: 44px; height: 44px; }
.so-cs-arrow.so-cs-arrow--sz-large  { width: 88px !important; height: 88px !important; }
.so-cs-arrow.so-cs-arrow--sz-large svg  { width: 68px; height: 68px; }
/* Backgrounded styles — chevron in a circle / rounded square. The ARROW COLOUR
   fills the shape (a coloured button); the glyph stays white for contrast. With
   no colour set it falls back to a translucent dark scrim. The `!important` on
   colour beats Swiper's own `.swiper-button { color: var(--swiper-navigation-color) }`
   so the glyph is white in BOTH editor and published. */
.so-cs-arrow--circle, .so-cs-arrow--square {
  color: #fff !important;
  background: var(--swiper-navigation-color, rgba(0, 0, 0, 0.34));
}
.so-cs-arrow--circle { border-radius: 50%; }
.so-cs-arrow--square { border-radius: 9px; }
.so-cs-arrow--circle:hover, .so-cs-arrow--square:hover { filter: brightness(1.12); }
/* Circle/square get a smaller glyph than the box so the scrim has breathing room. */
.so-cs-arrow--circle.so-cs-arrow--sz-small svg, .so-cs-arrow--square.so-cs-arrow--sz-small svg { width: 15px; height: 15px; }
.so-cs-arrow--circle.so-cs-arrow--sz-medium svg, .so-cs-arrow--square.so-cs-arrow--sz-medium svg { width: 32px; height: 32px; }
.so-cs-arrow--circle.so-cs-arrow--sz-large svg, .so-cs-arrow--square.so-cs-arrow--sz-large svg { width: 52px; height: 52px; }
.editor-mode .so-content-slider .so-cs-pagination {
  position: absolute;
  left: 0; right: 0; bottom: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  pointer-events: none;
}
.editor-mode .so-content-slider .so-cs-bullet {
  width: 8px;
  height: 8px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--swiper-pagination-bullet-inactive-color, #000);
  opacity: 0.2;                 /* Swiper default inactive bullet */
  cursor: pointer;
  pointer-events: auto;
  transition: opacity 0.15s, background 0.15s;
}
.editor-mode .so-content-slider .so-cs-bullet.is-active {
  background: var(--swiper-pagination-color, var(--swiper-theme-color, #007aff));
  opacity: 1;
}

/* Editor-only switcher bar (injected in view.onRender; never exported).
   Revealed when the slider is hovered/selected. Sits INSIDE the slider's top
   edge (part of the slider body) so it stays within the spotlight hole when
   editing — floating it ABOVE the slider would either be hidden by the dim or
   force a notch that reveals page content above the slider. */
.so-cs-editbar {
  display: none;
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  background: #fff;
  border: 1px solid #e3e3e3;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
  white-space: nowrap;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* The in-canvas switcher bar (.so-cs-editbar) is REPLACED by the parent-document
   Wix-style control bar (#so-cs-controls, built in content-slider.js's spotlight)
   that floats ABOVE the open slider, over the dim — the dim covered anything in
   the iframe above the slider. The in-canvas bar stays hidden (no show rule); its
   view methods (goTo / applyActive / slides) still drive the slides. */

/* Floating "Edit Slides" overlay — the slider's front door (Facebook-overlay
   lineage). Shown when the slider is NOT entered. On HOVER it shows the hint
   "Double-click to open"; on SELECT it shows the "✎ Edit Slides" button. The
   tinted backdrop is pointer-events:none so single-clicking still SELECTS the
   slider; only the BUTTON is clickable. A double-click anywhere opens it.
   Vanishes once entered. */
.so-cs-open {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(111, 86, 249, 0.35);   /* faded purple — mirrors the Facebook overlay (FB blue → E4 purple) */
  pointer-events: none;
  z-index: 17;
}
/* Show the overlay on hover OR select (closed slider only). */
.editor-mode .so-content-slider:not(.so-cs-entered):hover > .so-cs-open,
.editor-mode .so-content-slider:not(.so-cs-entered).gjs-selected > .so-cs-open {
  display: flex;
}
/* Hover hint (default state of the overlay) — a mostly-opaque purple pill with
   white text, copied from the Facebook overlay's span treatment. Button hidden
   until select. */
.so-cs-hint {
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  color: #fff;
  font: 700 18px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  letter-spacing: 0.3px;
  text-align: center;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.55);
}
.so-cs-hint-icon {
  display: block;
  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));
}
/* On SELECT: swap the hint for the actionable button. */
.editor-mode .so-content-slider:not(.so-cs-entered).gjs-selected > .so-cs-open .so-cs-hint {
  display: none;
}
.so-cs-open-btn { display: none; }
.editor-mode .so-content-slider:not(.so-cs-entered).gjs-selected > .so-cs-open .so-cs-open-btn {
  display: inline-flex;
  pointer-events: auto;
  cursor: pointer;
  border: none;
  background: #6F56F9;   /* E4 component purple — coherent with the faded-purple overlay */
  color: #fff;
  font: 700 17px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  letter-spacing: 0.3px;
  padding: 13px 26px;
  border-radius: 8px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
  transition: background 0.15s, transform 0.1s;
}
.so-cs-open-btn:hover { background: #5a43e0; transform: translateY(-1px); }
.so-cs-editbar button {
  border: none; background: transparent; cursor: pointer;
  font-size: 14px; line-height: 1; color: #333;
  padding: 4px 6px; border-radius: 4px;
}
.so-cs-editbar button:hover { background: #f0f0f0; }
.so-cs-editbar .so-cs-dots { display: inline-flex; align-items: center; gap: 5px; }
.so-cs-editbar .so-cs-dot {
  width: 9px; height: 9px; padding: 0; border-radius: 50%;
  background: #cfcfcf;
}
.so-cs-editbar .so-cs-dot.is-active { background: #2680eb; }
.so-cs-editbar .so-cs-label { font-size: 12px; color: #666; padding: 0 4px; }
.so-cs-editbar .so-cs-sep { width: 1px; height: 18px; background: #e3e3e3; }

/* ── Enter-to-edit ────────────────────────────────────────────────────────
   Until you DOUBLE-CLICK into the slider, its internals are non-interactive, so
   hovering/clicking selects the SLIDER as one unit (no internal row/column
   chrome — the slider is easy to grab and clearly a contained object). The
   switcher bar stays usable so you can switch slides / open settings without
   entering. Double-click enters (blue outline); clicking outside exits. */
.editor-mode .so-content-slider:not(.so-cs-entered) .so-content-slider-track,
.editor-mode .so-content-slider:not(.so-cs-entered) .so-content-slider-track * {
  pointer-events: none !important;
}
.editor-mode .so-content-slider .so-cs-editbar,
.editor-mode .so-content-slider .so-cs-editbar * {
  pointer-events: auto !important;   /* switcher always usable */
}
/* Entered indicator (canvas-side). The dim + exit pill are now a SHELL-document
   spotlight (see updateSliderSpotlight in content-slider.js) — they live in the
   parent document over the iframe, so nothing here positions them. */
.editor-mode .so-content-slider.so-cs-entered {
  /* No blue iframe outline — the shell-document spotlight RING (neutral white rim
     + dark glow) marks the slider boundary. A blue outline here competed with the
     orange ROW / blue COLUMN / purple CONTENT selection outlines inside, making
     them hard to read (esp. the orange row border when a row is selected). */
  outline: none;
  /* Editor-only breathing room WHILE editing inside: insets the slide content so
     child selection outlines pull away from the slider boundary/ring (the orange
     row border was flush against the edge, esp. on the right). Pads the SLIDER —
     not the slide — so the row/column bootstrap gutter math is untouched. Only
     applies when entered, so it never affects the published render. */
  padding: 12px;
  box-sizing: border-box;
}

/* =================================================================
   GRID FOUNDATION — Section / Row / Column Structural Rules
   Moved from JS injection (was: e4-editor-mode-widths + section-baseline
   blocks in js/editor4/plugins/so-bootstrap-grid.js) to here on
   24 May 2026 — single source of truth (Constitution Law 11).
   These rules establish the FOUNDATION layout that BOTH editor canvas
   AND preview/published pages depend on. Editor-only chrome rules
   (e.g. .editor-mode .so-row borders, drop-target affordances, column
   resize handles, GrapesJS selection chrome) remain in so-bootstrap-grid.js
   because they ARE editor-only.

   Class-based selectors (NOT [data-gjs-type=...]) — the data-gjs-type
   attribute is stripped on save so attribute-gated rules never fire
   in preview. Bug surfaced 24 May 2026: shape-divider absolute
   positioning anchored to wrong ancestor in preview because
   .so-section { position: relative } was editor-only AND empty rows
   collapsed to 0 height in preview because the min-height rule used
   [data-gjs-type=so-row] selector.
   ================================================================= */

/* Spacing scale (Visual Grammar §2) */
:root {
  --so-space-xs: 24px;
  --so-space-sm: 40px;
  --so-space-md: 80px;
  --so-space-lg: 120px;
  --so-space-xl: 160px;
}

/* Section spacing presets (class-based, section-level only) */
.so-section.so-spacing-compact  { padding-top: var(--so-space-sm); padding-bottom: var(--so-space-sm); }
.so-section.so-spacing-default  { padding-top: var(--so-space-md); padding-bottom: var(--so-space-md); }
.so-section.so-spacing-spacious { padding-top: var(--so-space-lg); padding-bottom: var(--so-space-lg); }

/* Section establishes its own stacking context — required for
   shape-dividers / accent layers / decorative absolutely-positioned
   children to anchor to the section rather than escape upward to
   <html> or <body>. THIS WAS THE ROOT CAUSE of the 24 May 2026
   shape-divider-over-icons preview bug. */
.so-section {
  position: relative;
}

/* Row layout — flex container with wrap. Required by the grid so
   columns lay out side-by-side and wrap to multiple lines. */
.so-row {
  position: relative;
  display: flex;
  flex-wrap: wrap;
}

/* Column establishes positioned context for column-internal absolutes
   (e.g. resize handles, hover badges in editor; component-internal
   absolutes in published pages). */
.so-col {
  position: relative;
}

/* ── Page-level cascade default (ChatGPT #302 ruling, Tue 8 Sep 2026) ──────────
   Ordinary role-less text receives the page's Theme Text colour through CSS
   inheritance. This is a page-level cascade default, not explicit Theme Role
   membership. Semantic roles (baked per-element `data-so-theme-role` colours),
   inverse-context rules and local authored colours remain higher-precedence
   sources — they are all more specific than inheritance.
   The canonical ordinary-page content wrapper is `#e4-body-zone` on every
   surface (canvas, Preview, published: the SPF composer wraps the page in it
   and the compiler fails closed if the root is anything else). Editor chrome
   and `body` are NOT touched. Shop canvas pages (category / product / auth —
   root `.so-shop-page`, shop-token owned) keep Bootstrap's body colour, as do
   cart / system shell surfaces (no `#e4-body-zone`) and E2.
   JC, 8 Sep: role-less text on Midnight read Bootstrap #212529 → 1.2:1 on the
   background while Midnight's own Text/Background pair is 7.3:1. */
#e4-body-zone {
  color: var(--theme-text, #212529);
}
#e4-body-zone .so-shop-page {
  color: var(--bs-body-color, #212529);   /* shop canvas pages excluded — component/shop ownership stays separate */
}

/* Empty-row min-height: rows without content reserve 100px so absolute-
   positioned siblings (shape dividers, accent layers) don't visually
   collapse onto adjacent rows. The .has-content class is maintained by
   the so-bootstrap-grid plugin on rows with at least one child component.
   Rows with content shrink to natural height. Rows with .manual-height
   are excluded — their height is set by inline style. */
.so-row:not(.has-content):not(.manual-height) {
  min-height: 100px;
}

/* Same rule for columns: empty cols in empty rows reserve space. */
.so-row:not(.has-content):not(.manual-height) .so-col:not(.has-content) {
  min-height: 100px;
}

/* Row vertical alignment primitives — class-based, NO !important so merchant
   Style Manager Flex sector edits cleanly override via #id rule specificity.
   Block authors apply these as layout defaults; merchants tune via Row
   Settings → Vertical Alignment dropdown. Default (no class) is Bootstrap's
   stretch behaviour (cols equal height). Matches the so-row-gap-* family. */
.so-row.so-row-align-top    { align-items: flex-start; }
.so-row.so-row-align-middle { align-items: center; }
.so-row.so-row-align-bottom { align-items: flex-end; }

/* Column content alignment primitives — position children vertically within
   a column. Pairs with row alignment (row positions cols within row; col
   content positions children within col). Default (no class) is normal
   block flow (content stacks from top). Merchant control: Column Settings
   → Content Alignment dropdown. */
.so-col.so-col-content-middle { display: flex; flex-direction: column; justify-content: center; }
.so-col.so-col-content-bottom { display: flex; flex-direction: column; justify-content: flex-end; }

/* Empty-col safety in aligned rows — when a row uses align-items: center
   or flex-end (via the so-row-align-* classes above), the default stretch
   behaviour is overridden and empty cols collapse to 0 (no content to
   align). This 100px floor keeps them visible and droppable. Scoped to
   editor-mode (drop affordance is editor-only) + direct-child (only
   immediate cols, not nested ones inside components). */
body.editor-mode .so-row.so-row-align-top > .so-col:not(.has-content),
body.editor-mode .so-row.so-row-align-middle > .so-col:not(.has-content),
body.editor-mode .so-row.so-row-align-bottom > .so-col:not(.has-content) {
  min-height: 100px;
}

/* #265 (B.10, 3 Sep 2026): a row whose columns are ALL empty collapses to a
   ~15px strip in the editor — the row's .has-content counts its CHILDREN,
   and a row's children are its COLUMNS, so "every column empty" never reads
   as "row empty" and the two conditional floors above it never fire. Floor
   the columns of such a row so the band stays visible and droppable.
   EDITOR-ONLY by design (drop affordance): published all-empty rows keep
   their honest collapsed height (#150 canvas honesty — no blanket floors),
   so this does not touch published output and needs no ruling. Direct-child
   scope and .manual-height exclusion match the sibling rules. */
body.editor-mode .so-row:not(.manual-height):not(:has(> .so-col.has-content)) > .so-col:not(.has-content) {
  min-height: 100px;
}

/* ─────────────────────────────────────────────────────────────────────
   Inline Group — free-composed layout primitive for side-by-side small
   components (CTA rows, stat strips, badge clusters, icon-text pairs,
   social rows, footer links). Row/Column-family sibling — not a card,
   not a Black Box, just a flex container with merchant-tunable layout.

   Base: horizontal, wraps to new line, items vertically centered,
   md (16px) gap. Modifier classes override individual properties
   without !important so Style Manager edits cleanly win.

   Merchant control: Settings popover → Layout
   (direction / gap / justify / align / wrap).

   Authored 15 Jun 2026 — ITEMS_TO_DO #79 (Phase 1 + 2). */
.so-inline-group {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-md);
}

.so-inline-group.so-ig-direction-vertical { flex-direction: column; }

.so-inline-group.so-ig-gap-xs  { gap: var(--space-xs);  }
.so-inline-group.so-ig-gap-sm  { gap: var(--space-sm);  }
.so-inline-group.so-ig-gap-md  { gap: var(--space-md);  }
.so-inline-group.so-ig-gap-lg  { gap: var(--space-lg);  }
.so-inline-group.so-ig-gap-xl  { gap: var(--space-xl);  }
.so-inline-group.so-ig-gap-xxl { gap: var(--space-xxl); }

.so-inline-group.so-ig-justify-center  { justify-content: center;        }
.so-inline-group.so-ig-justify-end     { justify-content: flex-end;      }
.so-inline-group.so-ig-justify-between { justify-content: space-between; }
.so-inline-group.so-ig-justify-around  { justify-content: space-around;  }

.so-inline-group.so-ig-align-start { align-items: flex-start; }
.so-inline-group.so-ig-align-end   { align-items: flex-end;   }

.so-inline-group.so-ig-nowrap { flex-wrap: nowrap; }

/* Always-on editor chrome — communicates "this is a Layout-family
   container" without requiring hover. Visual: native double-line
   outline in a neutral grey-blue so it's clearly distinct from:
     - selection blue (single solid outline)
     - spacing stripes (diagonals, claimed by spacing inspect chrome)
     - selection / hover badges and pills
   Editor-only (selector scoped to .editor-mode). outline: double
   renders as two parallel lines with a 1px gap natively in all modern
   browsers when width ≥ 3px — no overlay or stripe layering needed.
   Stripe-corner / striped-perimeter chrome retired 2026-06-20 per
   ITEMS_TO_DO #84 (vocabulary reassignment to spacing chrome). */
body.editor-mode .so-inline-group[data-gjs-type="so-inline-group"] {
  position: relative;
  outline: 3px double rgba(160, 175, 190, 0.55);
  outline-offset: -2px;
}

/* Identity badges — two corner tabs (top-right + bottom-left) styled
   to look "attached" to the container's double-line border. Each badge
   sits AT the corner overlapping the outline, with a grey-blue
   background that merges with the outline color, plus matching
   double-line borders on its INNER-FACING edges so the outline
   appears to continue around the badge.

   Implementation note (2026-06-20): switched from CSS pseudo-elements
   to real DOM <span> badges injected in the view's onRender. Pseudos
   do NOT fire native browser title tooltips even with pointer-events:
   auto — browsers route the pointer event to the host but the title
   tooltip logic specifically requires a real element. Real spans
   carry their own `title` attribute so native tooltips fire cleanly.
   GrapesJS won't serialize DOM-only additions (they're not in
   model.components()), so saved HTML stays clean. */
body.editor-mode .so-inline-group[data-gjs-type="so-inline-group"] > [data-e4-chrome="inline-structure-badge"] {
  position: absolute;
  padding: 1px 6px 2px;
  background: rgba(160, 175, 190, 0.9);
  color: #fff;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  line-height: 1.3;
  box-sizing: border-box;
  cursor: help;
  white-space: nowrap;
  user-select: none;
}

/* Top-right badge — double border on inner-facing edges (bottom + left)
   so the container's outline visually wraps around it. */
body.editor-mode .so-inline-group[data-gjs-type="so-inline-group"] > [data-e4-chrome="inline-structure-badge"][data-pos="tr"] {
  top: 0;
  right: 0;
  border-bottom: 3px double rgba(160, 175, 190, 0.55);
  border-left:   3px double rgba(160, 175, 190, 0.55);
  border-bottom-left-radius: 2px;
}

/* Bottom-left badge — mirror of top-right. */
body.editor-mode .so-inline-group[data-gjs-type="so-inline-group"] > [data-e4-chrome="inline-structure-badge"][data-pos="bl"] {
  bottom: 0;
  left: 0;
  border-top:   3px double rgba(160, 175, 190, 0.55);
  border-right: 3px double rgba(160, 175, 190, 0.55);
  border-top-right-radius: 2px;
}

/* Outside editor mode — chrome badges shouldn't render even if they
   somehow ended up in the DOM (defensive: belt and suspenders). */
.so-inline-group > [data-e4-chrome="inline-structure-badge"] {
  display: none;
}
body.editor-mode .so-inline-group > [data-e4-chrome="inline-structure-badge"] {
  display: block;
}

/* Instant CSS tooltip on badge hover — same pattern as the H/F zone-
   badge tooltip in zone-badges.js. Native `title` attribute deliber-
   ately not used (~1s browser delay); data-tip drives this `::before`
   layer which appears immediately. Tooltip grows TOWARDS the inline-
   group interior (away from the corner) so it doesn't fly off-edge. */
body.editor-mode .so-inline-group > [data-e4-chrome="inline-structure-badge"][data-tip]:hover::before {
  content: attr(data-tip);
  position: absolute;
  width: 240px;
  white-space: normal;
  background: rgba(20, 20, 20, 0.92);
  color: #fff;
  padding: 6px 8px;
  border-radius: 6px;
  font: 600 12px/1.35 system-ui, -apple-system, "Segoe UI", Roboto, Arial;
  text-transform: none;
  letter-spacing: normal;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
  z-index: 999;
  pointer-events: none;
}

/* Both tooltips RISE UPWARD from their badge — never down. Reason:
   when the inline-group sits at the bottom of a header zone (or any
   ancestor with overflow:hidden), a downward-flowing tooltip from
   the BL badge gets clipped at the boundary line. Up always has
   somewhere to go.
   - TR badge → tooltip rises ABOVE the inline-group (outside top
     edge), anchored at the badge's right.
   - BL badge → tooltip rises ABOVE the BL badge — inside the group's
     bottom-left interior. Inline-group has its own positioning
     context so no ancestor can clip it. */
body.editor-mode .so-inline-group > [data-e4-chrome="inline-structure-badge"][data-pos="tr"][data-tip]:hover::before {
  bottom: calc(100% + 6px);
  right: 0;
}

body.editor-mode .so-inline-group > [data-e4-chrome="inline-structure-badge"][data-pos="bl"][data-tip]:hover::before {
  bottom: calc(100% + 6px);
  left: 0;
}

/* Empty-state — visible drop target when the inline group has no
   children. Uses 'has-content' class (tracked by _updateEmptyState
   in so-bootstrap-grid.js) instead of :empty selector, since GrapesJS
   injects placeholder helpers and whitespace text nodes that defeat
   :empty. Editor-only (publish strips the class). Mirrors the empty-
   column "Drop Content Here" pattern. */
body.editor-mode .so-inline-group[data-gjs-type="so-inline-group"]:not(.has-content) {
  min-height: 80px;
}
body.editor-mode .so-inline-group[data-gjs-type="so-inline-group"]:not(.has-content)::before {
  content: "Inline Group — Drop items here (button, text, image, icon)";
  position: absolute;
  inset: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 12px 16px;
  box-sizing: border-box;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.2px;
  line-height: 1.3;
  color: #ffffff;
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 0.95),
    0 0 8px rgba(0, 0, 0, 0.55);
  background: rgba(20, 28, 45, 0.55);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  border: 2px dashed rgba(255, 255, 255, 0.55);
  border-radius: 6px;
  pointer-events: none;
  z-index: 1;
}
/* During drag, KEEP the empty-state hint visible — unlike empty cols
   (which hide their hint because GrapesJS chrome marks the col clearly),
   an empty Inline Group needs to remain visible as a target inside its
   parent col. Without this, merchants drag toward an invisible area
   and miss the routing.

   When the user IS hovering the inline group during drag, the visible
   placeholder prefix ("INSIDE INLINE GROUP") confirms targeting — both
   the static hint AND the drag placeholder coexist briefly, which is
   fine because the placeholder pops in front. */

/* Global link reset — remove default underline so styled buttons/links
   look clean. Low specificity so user-applied text-decoration via
   Style Manager wins. */
a, a:hover { text-decoration: none; }

/* Published page baseline: strip box-shadow from sections (any shadow
   the merchant wants on a section must be set explicitly via Style
   Manager). Editor mode has its own .so-section visual rules that
   override this. */
body:not(.editor-mode) .so-section {
  box-shadow: none;
}

/* =====================================================
   IMAGE CAROUSEL + 3D CARD CAROUSEL (Team Layout)
   =====================================================
   Migrated from image-carousel.js getImageCarouselCSS() on 28-May-2026.
   Reason: GrapesJS CSS exporter (editor.Css.addRules + editor.getCss())
   silently strips modern CSS functions like color-mix() and
   background-clip from the saved tenant CSS, producing the wrong
   styles in preview/published views. Loading directly from
   so-components.css avoids the parser round-trip.
   ===================================================== */
/* =====================================================
   IMAGE CAROUSEL COMPONENT
   ===================================================== */

.so-image-carousel {
  position: relative;
  width: 100%;
}

/* Editor hover chrome for atomic-image-owner components (carousel +
   galleries + sliders + flip-cards) is centralised in
   editor4-full-ui.js under "ATOMIC IMAGE OWNER OVERLAY" — uses a
   shared ::before pseudo with z-index 999 to paint above the
   component's own internal content. Don't duplicate it here. */

/* Empty State */
.so-carousel-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 300px;
  background: #f8f9fa;
  border: 2px dashed #dee2e6;
  border-radius: 0;
  color: #6c757d;
  text-align: center;
  padding: 40px 20px;
  pointer-events: none;
}

.so-carousel-empty svg {
  opacity: 0.5;
  margin-bottom: 16px;
}

.so-carousel-empty p {
  margin: 0 0 8px;
  font-size: 16px;
}

.so-carousel-empty-hint {
  font-size: 13px;
  opacity: 0.7;
}

/* =====================================================
   SIMPLE IMAGE CAROUSEL — pure CSS, radio + label pattern
   Pattern adapted from https://codepen.io/jh3y/pen/WwVKLN
   Supports up to 12 slides via nth-of-type rules. Each carousel
   instance scopes radio state via a unique name (so-carousel-{uid}).
   ===================================================== */

.so-cs {
  position: relative;
  width: 100%;
  overflow: hidden;
  list-style: none;
  padding: 0;
  margin: 0;
  text-align: center;
  background: var(--so-cs-bg, transparent) !important;
}

/* Hide the radio inputs that drive state */
.so-cs > .so-cs-activator {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/* Slide track translates left as :checked changes. Each slide is a
   flex column inside a horizontal track. */
.so-cs > .so-cs-track {
  position: absolute;
  inset: 0;
  display: flex;
  list-style: none;
  padding: 0;
  margin: 0;
  transition: transform 0.5s ease;
}

.so-cs > .so-cs-track > .so-cs-slide {
  flex: 0 0 100%;
  position: relative;
  height: 100%;
  overflow: hidden;
}

.so-cs > .so-cs-track > .so-cs-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.so-cs-caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
  color: #fff;
  padding: 30px 20px 15px;
  font-size: 16px;
  text-align: center;
}

/* Control pairs — only the matching :checked set is shown. Arrows
   are CSS-only (border triangle), positioned over the slide. */
.so-cs > .so-cs-controls {
  display: none;
}
.so-cs > .so-cs-controls > .so-cs-control {
  position: absolute;
  top: 50%;
  width: 30px;
  height: 30px;
  margin-top: -15px;
  cursor: pointer;
  border-style: solid;
  border-color: var(--so-cs-arrow-color, #fff) !important;
  border-width: 5px 5px 0 0;
  opacity: 0.45;
  z-index: 3;
  transition: opacity 0.2s, border-color 0.2s;
}
.so-cs > .so-cs-controls > .so-cs-control:hover {
  opacity: 1;
  border-color: var(--so-cs-arrow-hover, var(--so-cs-arrow-color, #fff)) !important;
}
.so-cs > .so-cs-controls > .so-cs-control--prev {
  left: 15px;
  transform: rotate(-135deg);
}
.so-cs > .so-cs-controls > .so-cs-control--next {
  right: 15px;
  transform: rotate(45deg);
}
/* Hide all arrows when data-no-arrows is set on the carousel */
.so-cs[data-no-arrows="true"] > .so-cs-controls > .so-cs-control {
  display: none !important;
}

/* Indicator dots — one per slide. Click to jump. */
.so-cs > .so-cs-indicators {
  position: absolute;
  bottom: 16px;
  left: 0;
  right: 0;
  text-align: center;
  z-index: 3;
}
.so-cs > .so-cs-indicators > .so-cs-indicator {
  display: inline-block;
  width: 12px;
  height: 12px;
  margin: 0 4px;
  border-radius: 50%;
  background: var(--so-cs-dot-inactive, rgba(255, 255, 255, 0.45)) !important;
  cursor: pointer;
  transition: background 0.2s, transform 0.2s;
}
.so-cs > .so-cs-indicators > .so-cs-indicator:hover {
  filter: brightness(1.1);
}

/* Generated rules: for slide N, when activator-N is :checked, slide
   the track left by (N-1) * 100%, show controls-N, highlight indicator-N.
   Supports up to 12 slides — extend if you need more. */
.so-cs > .so-cs-activator:nth-of-type(1):checked  ~ .so-cs-track { transform: translateX(0); }
.so-cs > .so-cs-activator:nth-of-type(2):checked  ~ .so-cs-track { transform: translateX(-100%); }
.so-cs > .so-cs-activator:nth-of-type(3):checked  ~ .so-cs-track { transform: translateX(-200%); }
.so-cs > .so-cs-activator:nth-of-type(4):checked  ~ .so-cs-track { transform: translateX(-300%); }
.so-cs > .so-cs-activator:nth-of-type(5):checked  ~ .so-cs-track { transform: translateX(-400%); }
.so-cs > .so-cs-activator:nth-of-type(6):checked  ~ .so-cs-track { transform: translateX(-500%); }
.so-cs > .so-cs-activator:nth-of-type(7):checked  ~ .so-cs-track { transform: translateX(-600%); }
.so-cs > .so-cs-activator:nth-of-type(8):checked  ~ .so-cs-track { transform: translateX(-700%); }
.so-cs > .so-cs-activator:nth-of-type(9):checked  ~ .so-cs-track { transform: translateX(-800%); }
.so-cs > .so-cs-activator:nth-of-type(10):checked ~ .so-cs-track { transform: translateX(-900%); }
.so-cs > .so-cs-activator:nth-of-type(11):checked ~ .so-cs-track { transform: translateX(-1000%); }
.so-cs > .so-cs-activator:nth-of-type(12):checked ~ .so-cs-track { transform: translateX(-1100%); }

.so-cs > .so-cs-activator:nth-of-type(1):checked  ~ .so-cs-controls:nth-of-type(1)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(2):checked  ~ .so-cs-controls:nth-of-type(2)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(3):checked  ~ .so-cs-controls:nth-of-type(3)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(4):checked  ~ .so-cs-controls:nth-of-type(4)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(5):checked  ~ .so-cs-controls:nth-of-type(5)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(6):checked  ~ .so-cs-controls:nth-of-type(6)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(7):checked  ~ .so-cs-controls:nth-of-type(7)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(8):checked  ~ .so-cs-controls:nth-of-type(8)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(9):checked  ~ .so-cs-controls:nth-of-type(9)  { display: block; }
.so-cs > .so-cs-activator:nth-of-type(10):checked ~ .so-cs-controls:nth-of-type(10) { display: block; }
.so-cs > .so-cs-activator:nth-of-type(11):checked ~ .so-cs-controls:nth-of-type(11) { display: block; }
.so-cs > .so-cs-activator:nth-of-type(12):checked ~ .so-cs-controls:nth-of-type(12) { display: block; }

.so-cs > .so-cs-activator:nth-of-type(1):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(1)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(2):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(2)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(3):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(3)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(4):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(4)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(5):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(5)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(6):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(6)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(7):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(7)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(8):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(8)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(9):checked  ~ .so-cs-indicators .so-cs-indicator:nth-of-type(9)  { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(10):checked ~ .so-cs-indicators .so-cs-indicator:nth-of-type(10) { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(11):checked ~ .so-cs-indicators .so-cs-indicator:nth-of-type(11) { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }
.so-cs > .so-cs-activator:nth-of-type(12):checked ~ .so-cs-indicators .so-cs-indicator:nth-of-type(12) { background: var(--so-cs-dot-active, #fff) !important; transform: scale(1.15); }

/* =====================================================
   3D CARD CAROUSEL (Team Layout)
   ===================================================== */

.so-team-carousel-wrapper {
  position: relative;
  width: 100%;
  min-height: 700px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: var(--team-bg-color, #f5f5f5);
  overflow: hidden;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.so-team-about-title {
  /* Font-family / font-size driven by CSS variables set on the wrapper
     (Ephemeral Preview contract — see renderTeamCarousel's rootStyle). */
  font-family: var(--team-title-font, 'Arial Black', Arial, sans-serif);
  font-size: var(--team-title-size, 7.5rem);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  position: absolute;
  top: 80px;
  left: 50%;
  transform: translateX(-50%);
  pointer-events: none;
  white-space: nowrap;
  /* Gradient text-fill: solid-ish for most of the height, with a soft
     fade at the bottom. Constant opacity from 0 to 60%, fade from
     60→85%, fully transparent below that. The 85% endpoint (vs the
     original 76%) means the fade-tail is shorter, so it doesn't reach
     into the cards. */
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--team-title-color, rgb(8, 42, 123)) 45%, transparent) 60%,
    transparent 85%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin: 0;
  z-index: 1;
}

.so-team-carousel-container {
  width: 100%;
  max-width: 1200px;
  height: 450px;
  position: relative;
  perspective: 1000px;
  /* margin-top clears the absolutely-positioned .so-team-about-title
     above. The wrapper has overflow: hidden so we can't move the title
     up — only the cards can move down. Combined with the tightened
     title gradient fade (now 60% instead of 76%), 220px gives a clean
     visual gap between title bottom and card tops. */
  margin-top: 220px;
}

.so-team-carousel-track {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.so-team-card {
  position: absolute;
  width: 280px;
  height: 380px;
  background: white;
  border-radius: 20px;
  overflow: hidden;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
  transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  cursor: pointer;
}

.so-team-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.so-team-card.center {
  z-index: 10;
  transform: scale(1.1) translateZ(0);
}

.so-team-card.center img {
  filter: none;
}

.so-team-card.left-2 {
  z-index: 1;
  transform: translateX(-400px) scale(0.8) translateZ(-300px);
  opacity: 0.7;
}

.so-team-card.left-2 img {
  filter: grayscale(100%);
}

.so-team-card.left-1 {
  z-index: 5;
  transform: translateX(-200px) scale(0.9) translateZ(-100px);
  opacity: 0.9;
}

.so-team-card.left-1 img {
  filter: grayscale(100%);
}

.so-team-card.right-1 {
  z-index: 5;
  transform: translateX(200px) scale(0.9) translateZ(-100px);
  opacity: 0.9;
}

.so-team-card.right-1 img {
  filter: grayscale(100%);
}

.so-team-card.right-2 {
  z-index: 1;
  transform: translateX(400px) scale(0.8) translateZ(-300px);
  opacity: 0.7;
}

.so-team-card.right-2 img {
  filter: grayscale(100%);
}

.so-team-card.hidden {
  opacity: 0;
  pointer-events: none;
}

.so-team-member-info {
  text-align: center;
  margin-top: 40px;
  transition: all 0.5s ease-out;
}

.so-team-member-name {
  color: var(--team-name-color, rgb(8, 42, 123));
  font-family: var(--team-name-font, inherit);
  font-size: var(--team-name-size, 2.5rem);
  font-weight: 700;
  margin-bottom: 10px;
  position: relative;
  display: inline-block;
}

.so-team-member-name::before,
.so-team-member-name::after {
  content: "";
  position: absolute;
  top: 100%;
  width: 100px;
  height: 2px;
  background: var(--team-name-color, rgb(8, 42, 123));
}

.so-team-member-name::before {
  left: -120px;
}

.so-team-member-name::after {
  right: -120px;
}

.so-team-member-role {
  color: var(--team-role-color, #848696);
  font-family: var(--team-role-font, inherit);
  font-size: var(--team-role-size, 1.5rem);
  font-weight: 500;
  opacity: 0.8;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: 10px 0;
  margin-top: -15px;
  position: relative;
}

.so-team-dots {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 60px;
}

.so-team-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--team-dot-inactive, rgba(8, 42, 123, 0.2));
  cursor: pointer;
  transition: all 0.3s ease;
}

.so-team-dot.active {
  background: var(--team-dot-color, rgb(8, 42, 123));
  transform: scale(1.2);
}

.so-team-nav-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: var(--team-arrow-color, rgba(8, 42, 123, 0.6));
  color: white;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 20;
  transition: all 0.3s ease;
  font-size: 1.5rem;
  border: none;
  outline: none;
  padding-bottom: 4px;
}

.so-team-nav-arrow:hover {
  background: var(--team-arrow-hover, rgba(0, 0, 0, 0.8));
  transform: translateY(-50%) scale(1.1);
}

.so-team-nav-left {
  left: 20px;
  padding-right: 3px;
}

.so-team-nav-right {
  right: 20px;
  padding-left: 3px;
}

/* Team Carousel Editor Navigation */
.so-team-editor-nav {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  background: #116dff;
  color: white;
  padding: 6px 12px;
  border-radius: 20px;
  font-size: 13px;
  font-weight: 500;
  z-index: 100;
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  user-select: none;
}

/* 3D Card Carousel: Cards without position class should be hidden */
.so-team-card:not(.center):not(.left-1):not(.left-2):not(.right-1):not(.right-2) {
  opacity: 0;
  pointer-events: none;
}

/* =====================================================
   RESPONSIVE
   ===================================================== */

@media (max-width: 768px) {
  .so-carousel-caption {
    padding: 20px 15px 10px;
    font-size: 14px;
  }

  .so-image-carousel-wrapper .swiper-button-prev,
  .so-image-carousel-wrapper .swiper-button-next {
    width: 36px;
    height: 36px;
  }

  .so-image-carousel-wrapper .swiper-button-prev::after,
  .so-image-carousel-wrapper .swiper-button-next::after {
    font-size: 14px;
  }

  /* 3D Card Carousel Responsive */
  .so-team-about-title {
    font-size: 4.5rem !important;
  }

  .so-team-card {
    width: 200px;
    height: 280px;
  }

  .so-team-card.left-2 {
    transform: translateX(-250px) scale(0.8) translateZ(-300px);
  }

  .so-team-card.left-1 {
    transform: translateX(-120px) scale(0.9) translateZ(-100px);
  }

  .so-team-card.right-1 {
    transform: translateX(120px) scale(0.9) translateZ(-100px);
  }

  .so-team-card.right-2 {
    transform: translateX(250px) scale(0.8) translateZ(-300px);
  }

  .so-team-member-name {
    font-size: 2rem;
  }

  .so-team-member-role {
    font-size: 1.2rem;
  }

  .so-team-member-name::before,
  .so-team-member-name::after {
    width: 50px;
  }

  .so-team-member-name::before {
    left: -70px;
  }

  .so-team-member-name::after {
    right: -70px;
  }
}

/* =====================================================
   FULLSCREEN SLIDER COMPONENT
   =====================================================
   Migrated from fullscreen-slider.js getFullscreenSliderCSS() on
   28-May-2026. Same reason as the carousel migration above:
   GrapesJS CSS exporter strips modern CSS functions (linear-gradient
   etc.) from saved tenant CSS. Loading directly from so-components.css
   avoids the parser round-trip.
   ===================================================== */
/* =====================================================
   FULLSCREEN SLIDER COMPONENT
   ===================================================== */

.so-fullscreen-slider {
  position: relative;
  width: 100%;
}

/* Empty State */
.so-fs-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  color: rgba(255,255,255,0.6);
  text-align: center;
  padding: 40px 20px;
}

.so-fs-empty svg {
  opacity: 0.4;
  margin-bottom: 20px;
}

.so-fs-empty p {
  margin: 0 0 8px;
  font-size: 18px;
}

.so-fs-empty-hint {
  font-size: 14px;
  opacity: 0.6;
}

/* Main Wrapper */
.so-fs-wrapper {
  position: relative;
  width: 100%;
  box-shadow: 0 3px 10px rgba(0,0,0,0.3);
  overflow: hidden;
}

/* Slider List */
.so-fs-slider {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
  width: 100%;
  height: 100%;
}

/* Slide Item */
.so-fs-item {
  width: 200px;
  height: 300px;
  list-style-type: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  background-position: center;
  background-size: cover;
  border-radius: 20px;
  box-shadow: 0 20px 30px rgba(255,255,255,0.3) inset;
  transition: transform 0.1s, left 0.75s, top 0.75s, width 0.75s, height 0.75s;
}

/* First two items (active/background) */
.so-fs-item:nth-child(1),
.so-fs-item:nth-child(2) {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform: none;
  border-radius: 0;
  box-shadow: none;
  opacity: 1;
}

/* Thumbnail positions */
.so-fs-item:nth-child(3) { left: 50%; }
.so-fs-item:nth-child(4) { left: calc(50% + 220px); }
.so-fs-item:nth-child(5) { left: calc(50% + 440px); }
.so-fs-item:nth-child(6) { left: calc(50% + 660px); opacity: 0; }

/* Slides beyond DOM position 6 are queued off-screen. As the user navigates
 * (goToSlide moves the first/last <li> to the opposite end of the <ul>),
 * each queued slide cycles into positions 1-6 and becomes visible. Without
 * this rule, the base .so-fs-item style (width:200px; left:0; top:50%) wins
 * for items 7+ and they paint as 200×300 thumbnails AT THE TOP-LEFT, on top
 * of items 1+2 (later DOM order = higher paint order at the same z-index),
 * overlapping the active slide's text area. Debugged 28-May-2026 via the
 * DOM probe after a user reported "small waterfall image cutting off the
 * title" with 8 slides configured. */
.so-fs-item:nth-child(n+7) {
  opacity: 0 !important;
  visibility: hidden;
  pointer-events: none;
}

/* Content Overlay */
.so-fs-content {
  width: min(30vw, 400px);
  position: absolute;
  top: 50%;
  left: 3rem;
  transform: translateY(-50%);
  font: 400 0.85rem Helvetica, sans-serif;
  color: white;
  text-shadow: 0 3px 8px rgba(0,0,0,0.5);
  opacity: 0;
  display: none;
}

.so-fs-title {
  font-family: 'Arial Black', Arial, sans-serif;
  text-transform: uppercase;
  font-size: 2rem;
  margin: 0 0 1rem;
  line-height: 1.2;
}

.so-fs-description {
  line-height: 1.7;
  margin: 0 0 1.5rem;
  font-size: 0.9rem;
}

.so-fs-button {
  display: inline-block;
  width: fit-content;
  background-color: rgba(0,0,0,0.1);
  color: white;
  border: 2px solid white;
  border-radius: 0.25rem;
  padding: 0.75rem 1.5rem;
  cursor: pointer;
  text-decoration: none;
  font-size: 0.85rem;
  transition: background-color 0.3s, transform 0.2s;
}

/* Hover state colours come from CSS vars on the .so-fs-wrapper (set by
 * renderSlider). Falls back to a tinted-white default if no var is set
 * (legacy slides without hover config). !important needed because the
 * normal state has inline button styles from the rendered HTML. */
.so-fs-wrapper .so-fs-button:hover {
  background-color: var(--fs-btn-bg-hover, rgba(255,255,255,0.2)) !important;
  color: var(--fs-btn-color-hover) !important;
  border-color: var(--fs-btn-border-hover) !important;
  transform: translateY(-2px);
}

/* Show content on second slide (the visible one) */
.so-fs-item:nth-of-type(2) .so-fs-content {
  display: block;
  animation: so-fs-show 0.75s ease-in-out 0.3s forwards;
}

@keyframes so-fs-show {
  0% {
    filter: blur(5px);
    transform: translateY(calc(-50% + 75px));
  }
  100% {
    opacity: 1;
    filter: blur(0);
    transform: translateY(-50%);
  }
}

/* Navigation */
.so-fs-nav {
  position: absolute;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  user-select: none;
  display: flex;
  gap: 0.5rem;
}

.so-fs-btn {
  background-color: var(--nav-bg, rgba(255,255,255,0.5));
  color: var(--nav-icon, rgba(0,0,0,0.7));
  border: 2px solid var(--nav-border, rgba(0,0,0,0.6));
  width: 48px;
  height: 48px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s, transform 0.2s;
}

.so-fs-btn:hover {
  background-color: rgba(255,255,255,0.8);
  transform: scale(1.1);
}

.so-fs-btn svg {
  width: 24px;
  height: 24px;
}

/* Editor Navigation Bar */
.so-fs-editor-nav {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0,0,0,0.85);
  padding: 8px 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  z-index: 10;
}

.so-fs-editor-btn {
  background: rgba(255,255,255,0.15);
  border: none;
  color: white;
  width: 28px;
  height: 28px;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s;
}

.so-fs-editor-btn:hover {
  background: rgba(255,255,255,0.25);
}

.so-fs-editor-indicator {
  color: rgba(255,255,255,0.8);
  font-size: 13px;
}

/* Responsive - Tablet */
@media (min-width: 650px) and (max-width: 900px) {
  .so-fs-content .so-fs-title { font-size: 1.5rem; }
  .so-fs-content .so-fs-description { font-size: 0.8rem; }
  .so-fs-content .so-fs-button { font-size: 0.75rem; }

  .so-fs-item {
    width: 160px;
    height: 270px;
  }
  .so-fs-item:nth-child(3) { left: 50%; }
  .so-fs-item:nth-child(4) { left: calc(50% + 170px); }
  .so-fs-item:nth-child(5) { left: calc(50% + 340px); }
  .so-fs-item:nth-child(6) { left: calc(50% + 510px); opacity: 0; }
}

/* Responsive - Mobile */
@media (max-width: 650px) {
  .so-fs-content {
    width: calc(100% - 2rem);
    left: 1rem;
  }
  .so-fs-content .so-fs-title { font-size: 1.2rem; }
  .so-fs-content .so-fs-description { font-size: 0.75rem; }
  .so-fs-content .so-fs-button { font-size: 0.7rem; padding: 0.5rem 1rem; }

  .so-fs-item {
    width: 130px;
    height: 220px;
  }
  .so-fs-item:nth-child(3) { left: 50%; }
  .so-fs-item:nth-child(4) { left: calc(50% + 140px); }
  .so-fs-item:nth-child(5) { left: calc(50% + 280px); }
  .so-fs-item:nth-child(6) { left: calc(50% + 420px); opacity: 0; }
}

/* ==========================================================
   GALLERY COMPONENTS — all 7 layouts
   ==========================================================
   Migrated 28-May-2026 from gallery-core.getAllCSS() +
   gallery-honeycomb.getCSS() + gallery-organic.getCSS() +
   gallery-slideshow.getCSS(). The latter three were orphaned
   — their getCSS() functions were never called, so honeycomb
   /organic/slideshow layouts rendered as vertical strips.
   Consolidated here so all 7 work in the editor canvas, in
   _preview.php, and on published shop pages — one source of
   truth, no canvas-iframe runtime injection required.
   ========================================================== */

/* ── Gallery core (shared + grid/masonry/justified/mosaic) ── */
/* ============================================
   GALLERY BASE STYLES
   ============================================ */

/* Gallery component container - MUST be full width */
.so-gallery-component {
  width: 100% !important;
  max-width: 100% !important;
  box-sizing: border-box !important;
  display: block !important;
}

/* Gallery wrapper - MUST be full width */
.so-gallery-wrapper {
  width: 100% !important;
  max-width: 100% !important;
  box-sizing: border-box !important;
  margin: 0 !important;
  padding: 0 !important;
}

/* Gallery items */
.so-gallery-item {
  position: relative;
  overflow: hidden;
  cursor: pointer;
  box-sizing: border-box;
}

.so-gallery-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Gallery image padding is intentionally neutralised so per-image ID rules
   auto-saved into CustomStyles.css (e.g. #iuoyc{padding:10px} produced by
   GrapesJS StyleManager) cannot override the gallery's own gap setting.
   Without this, preview and editor diverge: the editor re-renders fresh
   <img> tags with no IDs (no padding applies → tight gaps), while preview
   serves the saved HTML with IDs (padding applies → gaps bloat by ~2x).
   The "gap" setting in the gallery settings modal is the single source of
   truth for spacing. Standalone <img> components are unaffected — this rule
   only targets <img> inside .so-gallery-item. */
.so-gallery-item img {
  padding: 0 !important;
}

/* Gallery wrapper background */
.so-gallery-wrapper {
  background: var(--so-gallery-bg, transparent);
}

/* Captions - use CSS variables for styling */
.so-gallery-caption {
  position: absolute;
  left: 0;
  right: 0;
  padding: 8px 12px;
  background: var(--so-gallery-caption-bg, rgba(0, 0, 0, 0.7));
  color: var(--so-gallery-caption-color, #fff);
  font-size: var(--so-gallery-caption-size, 14px);
  /* Phase 3.6 30-May-2026: caption font family driven by the Styles
     tab Font dropdown (Inherit + theme palette). Default 'inherit'
     means it picks up whatever the gallery's parent element uses. */
  font-family: var(--so-gallery-caption-font, inherit);
  text-align: center;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.so-gallery-item:hover .so-gallery-caption {
  opacity: 1;
}

/* Caption position variants */
.so-gallery-caption-bottom .so-gallery-caption {
  bottom: 0;
  top: auto;
}

.so-gallery-caption-top .so-gallery-caption {
  top: 0;
  bottom: auto;
}

.so-gallery-caption-center .so-gallery-caption {
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
}

/* Below image - caption outside the image */
.so-gallery-caption-below .so-gallery-item {
  display: flex;
  flex-direction: column;
}

.so-gallery-caption-below .so-gallery-caption {
  position: relative;
  opacity: 1;
  bottom: auto;
  top: auto;
  transform: none;
}

/* Hover effects */
.so-gallery-hover-zoom .so-gallery-item img {
  transition: transform 0.3s ease;
}
.so-gallery-hover-zoom .so-gallery-item:hover img {
  transform: scale(1.1);
}

.so-gallery-hover-brighten .so-gallery-item img {
  transition: filter 0.3s ease;
}
.so-gallery-hover-brighten .so-gallery-item:hover img {
  filter: brightness(1.2);
}

.so-gallery-hover-darken .so-gallery-item img {
  transition: filter 0.3s ease;
}
.so-gallery-hover-darken .so-gallery-item:hover img {
  filter: brightness(0.7);
}

.so-gallery-hover-grayscale .so-gallery-item img {
  transition: filter 0.3s ease;
}
.so-gallery-hover-grayscale .so-gallery-item:hover img {
  filter: grayscale(100%);
}

/* Empty state */
.so-gallery-empty {
  width: 100% !important;
  padding: 40px 20px;
  text-align: center;
  background: #f5f5f5;
  border: 2px dashed #ccc;
  border-radius: 8px;
  color: #666;
  box-sizing: border-box;
}

.so-gallery-empty-icon {
  font-size: 48px;
  margin-bottom: 15px;
  opacity: 0.5;
}

/* ============================================
   GRID GALLERY
   ============================================ */

.so-gallery-wrapper.so-gallery-grid {
  display: grid !important;
  grid-template-columns: repeat(var(--so-gallery-columns, 4), 1fr) !important;
  gap: var(--so-gallery-gap, 10px) !important;
  width: 100% !important;
}

.so-gallery-grid .so-gallery-item {
  aspect-ratio: var(--so-gallery-aspect, 1/1);
  width: 100%;
}

.so-gallery-grid .so-gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Grid responsive */
@media (max-width: 992px) {
  .so-gallery-wrapper.so-gallery-grid {
    grid-template-columns: repeat(3, 1fr) !important;
  }
}

@media (max-width: 768px) {
  .so-gallery-wrapper.so-gallery-grid {
    grid-template-columns: repeat(2, 1fr) !important;
  }
}

/* ============================================
   MASONRY GALLERY
   ============================================ */

.so-gallery-wrapper.so-gallery-masonry {
  display: block !important;
  width: 100% !important;
  column-count: var(--so-gallery-columns, 4) !important;
  column-gap: var(--so-gallery-gap, 10px) !important;
}

.so-gallery-masonry .so-gallery-item {
  break-inside: avoid;
  margin-bottom: var(--so-gallery-gap, 10px);
  width: 100%;
}

.so-gallery-masonry .so-gallery-item img {
  width: 100%;
  height: auto;
  display: block;
}

/* Masonry responsive */
@media (max-width: 992px) {
  .so-gallery-wrapper.so-gallery-masonry {
    column-count: 3 !important;
  }
}

@media (max-width: 768px) {
  .so-gallery-wrapper.so-gallery-masonry {
    column-count: 2 !important;
  }
}

/* ============================================
   JUSTIFIED GALLERY
   ============================================ */

.so-gallery-wrapper.so-gallery-justified {
  display: flex !important;
  flex-wrap: wrap !important;
  width: 100% !important;
  gap: var(--so-gallery-gap, 10px) !important;
}

.so-gallery-justified .so-gallery-item {
  flex-grow: 1;
  height: var(--so-gallery-row-height, 200px);
  min-width: 150px;
  max-width: 400px;
}

.so-gallery-justified .so-gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Justified responsive */
@media (max-width: 768px) {
  .so-gallery-justified .so-gallery-item {
    height: 150px;
    min-width: 120px;
  }
}

/* ============================================
   MOSAIC GALLERY
   ============================================ */

.so-gallery-wrapper.so-gallery-mosaic {
  display: grid !important;
  grid-template-columns: repeat(4, 1fr) !important;
  grid-auto-rows: minmax(100px, 150px) !important;
  gap: var(--so-gallery-gap, 10px) !important;
  width: 100% !important;
}

/* Feature pattern - every 5th item starting from 1st is larger */
.so-gallery-mosaic .so-gallery-item:nth-child(5n+1) {
  grid-column: span 2;
  grid-row: span 2;
}

.so-gallery-mosaic .so-gallery-item {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.so-gallery-mosaic .so-gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Mosaic responsive */
@media (max-width: 992px) {
  .so-gallery-wrapper.so-gallery-mosaic {
    grid-template-columns: repeat(3, 1fr) !important;
    grid-auto-rows: minmax(80px, 120px) !important;
  }
}

@media (max-width: 768px) {
  .so-gallery-wrapper.so-gallery-mosaic {
    grid-template-columns: repeat(2, 1fr) !important;
  }
  .so-gallery-mosaic .so-gallery-item:nth-child(5n+1) {
    grid-column: span 2;
    grid-row: span 1;
  }
}

/* ── Gallery honeycomb ── */
/* Honeycomb Gallery Layout - CSS Grid with nth-child positioning */
/* Based on: https://codepen.io/Rafiozoo/pen/rdXqvQ */

.so-gallery-wrapper.so-gallery-honeycomb,
.so-gallery-honeycomb {
  /* Use CSS var from inline style, or fallback */
  --hex-size: var(--so-hex-size, 120px);
  --hex-margin: var(--so-hex-margin, 4px);

  display: grid !important;
  /* 6 half-hex columns for 3-column layout */
  grid-template-columns: repeat(6, calc(var(--hex-size) / 2));
  /* Remove max-width constraint - let it center naturally */
  width: fit-content;
  margin: 0 auto;
  padding: 1rem;
  box-sizing: border-box;
  /* Wrapper background var (Phase 3C-fix, 29-May-2026 — drives the
     Styles tab Gallery Background colour picker added in the same pass) */
  background: var(--so-gallery-bg, transparent);
}

.so-gallery-honeycomb-item {
  width: var(--hex-size);
  height: var(--hex-size);
  position: relative;
  grid-column: span 2;
  margin: 0;
  /* Negative margin creates the tessellation overlap */
  margin-bottom: calc(var(--hex-size) * -0.14);
  cursor: pointer;
  box-sizing: border-box;
}

/* Hexagon outline (border effect) */
.so-gallery-honeycomb-item::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 0; right: 0;
  background-color: #ddd;
  clip-path: polygon(50% 0, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);
  transform: scale(1.05);
  transition: transform 0.3s;
  z-index: 0;
}

.so-gallery-honeycomb-item:hover::before {
  transform: scale(1.1);
}

.so-gallery-honeycomb-item img {
  position: absolute;
  top: 0; bottom: 0; left: 0; right: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  clip-path: polygon(50% 0, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);
  z-index: 1;
}

/* ============================================
   3 COLUMNS (default) - Pattern: 3 top, 2 bottom, repeat
   Grid has 6 half-columns, each hex spans 2
   Row 1: cols 1-2, 3-4, 5-6 (3 hexes)
   Row 2: cols 2-3, 4-5 (2 hexes, offset by half)
   ============================================ */
.so-gallery-honeycomb[data-columns="3"] .so-gallery-honeycomb-item:nth-child(5n+1),
.so-gallery-honeycomb:not([data-columns]) .so-gallery-honeycomb-item:nth-child(5n+1) { grid-column: 1 / span 2; }

.so-gallery-honeycomb[data-columns="3"] .so-gallery-honeycomb-item:nth-child(5n+2),
.so-gallery-honeycomb:not([data-columns]) .so-gallery-honeycomb-item:nth-child(5n+2) { grid-column: 3 / span 2; }

.so-gallery-honeycomb[data-columns="3"] .so-gallery-honeycomb-item:nth-child(5n+3),
.so-gallery-honeycomb:not([data-columns]) .so-gallery-honeycomb-item:nth-child(5n+3) { grid-column: 5 / span 2; }

.so-gallery-honeycomb[data-columns="3"] .so-gallery-honeycomb-item:nth-child(5n+4),
.so-gallery-honeycomb:not([data-columns]) .so-gallery-honeycomb-item:nth-child(5n+4) { grid-column: 2 / span 2; }

.so-gallery-honeycomb[data-columns="3"] .so-gallery-honeycomb-item:nth-child(5n),
.so-gallery-honeycomb:not([data-columns]) .so-gallery-honeycomb-item:nth-child(5n) { grid-column: 4 / span 2; }

/* ============================================
   2 COLUMNS - Pattern: 2 top, 1 bottom, repeat
   Grid has 4 half-columns
   ============================================ */
.so-gallery-honeycomb[data-columns="2"] {
  grid-template-columns: repeat(4, calc(var(--hex-size) / 2));
}
.so-gallery-honeycomb[data-columns="2"] .so-gallery-honeycomb-item:nth-child(3n+1) { grid-column: 1 / span 2; }
.so-gallery-honeycomb[data-columns="2"] .so-gallery-honeycomb-item:nth-child(3n+2) { grid-column: 3 / span 2; }
.so-gallery-honeycomb[data-columns="2"] .so-gallery-honeycomb-item:nth-child(3n+3) { grid-column: 2 / span 2; }

/* ============================================
   4 COLUMNS - Pattern: 4 top, 3 bottom, repeat
   Grid has 8 half-columns
   ============================================ */
.so-gallery-honeycomb[data-columns="4"] {
  grid-template-columns: repeat(8, calc(var(--hex-size) / 2));
}
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n+1) { grid-column: 1 / span 2; }
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n+2) { grid-column: 3 / span 2; }
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n+3) { grid-column: 5 / span 2; }
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n+4) { grid-column: 7 / span 2; }
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n+5) { grid-column: 2 / span 2; }
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n+6) { grid-column: 4 / span 2; }
.so-gallery-honeycomb[data-columns="4"] .so-gallery-honeycomb-item:nth-child(7n) { grid-column: 6 / span 2; }

/* ============================================
   5 COLUMNS - Pattern: 5 top, 4 bottom, repeat
   Grid has 10 half-columns
   ============================================ */
.so-gallery-honeycomb[data-columns="5"] {
  grid-template-columns: repeat(10, calc(var(--hex-size) / 2));
}
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+1) { grid-column: 1 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+2) { grid-column: 3 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+3) { grid-column: 5 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+4) { grid-column: 7 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+5) { grid-column: 9 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+6) { grid-column: 2 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+7) { grid-column: 4 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n+8) { grid-column: 6 / span 2; }
.so-gallery-honeycomb[data-columns="5"] .so-gallery-honeycomb-item:nth-child(9n) { grid-column: 8 / span 2; }

/* ============================================
   6 COLUMNS - Pattern: 6 top, 5 bottom, repeat
   Grid has 12 half-columns
   ============================================ */
.so-gallery-honeycomb[data-columns="6"] {
  grid-template-columns: repeat(12, calc(var(--hex-size) / 2));
}
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+1) { grid-column: 1 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+2) { grid-column: 3 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+3) { grid-column: 5 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+4) { grid-column: 7 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+5) { grid-column: 9 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+6) { grid-column: 11 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+7) { grid-column: 2 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+8) { grid-column: 4 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+9) { grid-column: 6 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n+10) { grid-column: 8 / span 2; }
.so-gallery-honeycomb[data-columns="6"] .so-gallery-honeycomb-item:nth-child(11n) { grid-column: 10 / span 2; }

/* Hover effects */
.so-gallery-hover-zoom .so-gallery-honeycomb-item:hover {
  z-index: 10;
}

.so-gallery-hover-zoom .so-gallery-honeycomb-item:hover img {
  transform: scale(1.05);
}

/* Caption styling for honeycomb (Phase 3C-fix, 29-May-2026 — parametrised
   via CSS vars so the Styles tab Caption Background / Text Color / Font
   Size controls actually drive what the merchant sees. Position remains
   fixed at bottom 25% because the hexagonal clip-path doesn't accommodate
   top/center/below variants the way rectangular galleries do.) */
.so-gallery-honeycomb .so-gallery-caption {
  position: absolute;
  bottom: 25%;
  left: 15%;
  right: 15%;
  text-align: center;
  background: var(--so-gallery-caption-bg, rgba(0, 0, 0, 0.7));
  color: var(--so-gallery-caption-color, white);
  padding: 4px 8px;
  font-size: var(--so-gallery-caption-size, 10px);
  font-family: var(--so-gallery-caption-font, inherit);
  border-radius: 3px;
  z-index: 2;
  clip-path: none;
}

/* Responsive - reduce to 2 columns on small screens */
@media (max-width: 600px) {
  .so-gallery-wrapper.so-gallery-honeycomb,
  .so-gallery-honeycomb {
    --hex-size: 100px;
    grid-template-columns: repeat(4, calc(var(--hex-size) / 2)) !important;
  }
  .so-gallery-honeycomb .so-gallery-honeycomb-item:nth-child(3n+1) { grid-column: 1 / span 2 !important; }
  .so-gallery-honeycomb .so-gallery-honeycomb-item:nth-child(3n+2) { grid-column: 3 / span 2 !important; }
  .so-gallery-honeycomb .so-gallery-honeycomb-item:nth-child(3n+3) { grid-column: 2 / span 2 !important; }
}

/* ── Gallery organic ── */
/* Organic Blob Gallery Layout */
.so-gallery-wrapper.so-gallery-organic,
.so-gallery-organic {
  /* Phase 3C-fix-F 30-May-2026: gallery background parametrised via CSS
     var so the Organic Styles tab Gallery Background colour picker can
     drive it. Default #000814 retained as the visual baseline. */
  background: var(--so-gallery-bg, #000814);
  padding: 1rem;
  display: flex !important;
  justify-content: center;
  overflow: hidden;
  position: relative;
  z-index: 1;
  width: 100% !important;
}

.so-gallery-organic-grid {
  /* Was: `calc(var(--so-organic-size, 100px) - var(--so-organic-gap, 0.5rem) / 2)`
     — legacy formula that math-coupled Image Size with Gap for no
     useful reason. Decoupled 26 Jun 2026. NOTE: this fix is only
     half the story — the IMG rule below uses `max-width: 100%`
     which means images don't actually fill the cell when Image
     Size grows. Visual effect: cells grow but images stay roughly
     fixed → looks like "gap is growing." Real fix tracked in
     ITEMS_TO_DO #97 (non-urgent). */
  --img-size: var(--so-organic-size, 100px);
  display: grid;
  gap: var(--so-organic-gap, 0.5rem);
  grid-template-columns: repeat(6, var(--img-size));
  grid-template-rows: repeat(4, var(--img-size));
  position: relative;
  z-index: 1;
}

.so-gallery-organic-grid img {
  max-width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  grid-column: span 2;
  border-radius: 0.5rem;
  clip-path: path(
    "M 80 20 C 100 0 100 0 120 20 C 140 40 160 60 180 80 C 200 100 200 100 180 120 C 160 140 140 160 120 180 C 100 200 100 200 80 180 C 60 160 40 140 20 120 C 0 100 0 100 20 80 Z"
  );
  transition: filter 500ms, clip-path 500ms;
  cursor: pointer;
}

/* Stagger pattern for 4th and 9th images */
.so-gallery-organic-grid img:nth-of-type(4),
.so-gallery-organic-grid img:nth-of-type(9) {
  grid-column: 2 / span 2;
}

/* Hover shape change - transforms to rectangle */
.so-gallery-organic-grid img:hover {
  z-index: 10;
  clip-path: path(
    "M 0 0 C 100 0 100 0 200 0 C 200 50 200 50 200 80 C 200 100 200 100 200 120 C 200 150 200 150 200 200 C 100 200 100 200 0 200 C 0 150 0 150 0 120 C 0 100 0 100 0 80 Z"
  );
}

/* Reset z-index on non-hover */
.so-gallery-organic-grid img:not(:hover) {
  animation: soGalleryOrganicZIndexFix 500ms;
}

/* Dim non-hovered images when gallery hovered */
.so-gallery-organic-grid:hover > img {
  filter: brightness(0.5) saturate(0.5);
}

/* Highlight hovered image */
.so-gallery-organic-grid > img:hover {
  filter: brightness(1) saturate(1.5);
}

/* Z-index animation keyframe */
@keyframes soGalleryOrganicZIndexFix {
  0%, 100% {
    z-index: 10;
  }
}

/* Responsive — hardcoded mobile/tablet sizes override the merchant's
   Image Size at small breakpoints. Gap subtraction removed to match
   the desktop rule's decoupling (was `calc(70px - gap/2)`). */
@media (max-width: 768px) {
  .so-gallery-organic-grid {
    --img-size: 70px;
  }
}

@media (max-width: 480px) {
  .so-gallery-organic-grid {
    --img-size: 50px;
  }
}

/* =============================================
   GALLERY MODAL — image row + add-row styles
   =============================================
   Used by GalleryCore.renderImagesListWithCaptions when each
   gallery layout opens its SoModal-based settings modal. These
   classes were previously injected inline via getModalCSS() but
   that path was removed during the Phase 3C SoModal migration.
   ============================================= */

      /* Thumbnail-grid image picker (renderImagesGrid) — used by Organic
         gallery (and Honeycomb pre-Phase-3C.5 — now uses the list variant).
         Migrated 29-May-2026 after Organic Phase 3C.6 modal shipped with
         broken styling: the modal moved to SoModal but the inline
         getModalCSS() block that previously styled .so-gallery-images-grid
         was no longer being injected. */
      .so-gallery-images-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
        gap: 12px;
      }
      .so-gallery-image-item {
        position: relative;
        aspect-ratio: 1;
        border-radius: 8px;
        overflow: hidden;
        background: #f0f0f0;
        cursor: grab;
      }
      .so-gallery-image-item img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
      .so-gallery-image-item .remove-btn {
        position: absolute;
        top: 6px;
        right: 6px;
        width: 24px;
        height: 24px;
        background: rgba(255, 0, 0, 0.8);
        color: #fff;
        border: none;
        border-radius: 50%;
        cursor: pointer;
        font-size: 14px;
        line-height: 24px;
        text-align: center;
        opacity: 0;
        transition: opacity 0.2s;
      }
      .so-gallery-image-item:hover .remove-btn {
        opacity: 1;
      }
      .so-gallery-add-btn {
        aspect-ratio: 1;
        border: 2px dashed #ccc;
        border-radius: 8px;
        background: #fafafa;
        cursor: pointer;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        color: #999;
        transition: all 0.2s;
      }
      .so-gallery-add-btn:hover {
        border-color: #4a90d9;
        color: #4a90d9;
        background: #f0f7ff;
      }
      .so-gallery-add-btn i {
        font-size: 28px;
        margin-bottom: 6px;
      }
      .so-gallery-image-item.dragging {
        opacity: 0.5;
      }
      .so-gallery-image-item.drag-over {
        outline: 2px solid #4a90d9;
      }

      /* Inline image editing row layout */
      .so-gallery-images-list {
        display: flex;
        flex-direction: column;
        gap: 12px;
      }
      .so-gallery-image-row {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 12px;
        background: #f8f9fa;
        border-radius: 8px;
        border: 1px solid #e0e0e0;
      }
      .so-gallery-image-row.dragging {
        opacity: 0.5;
        background: #e8f0fe;
      }
      .so-gallery-image-row.drag-over {
        border-color: #4a90d9;
        background: #f0f7ff;
      }
      .so-gallery-row-thumb {
        position: relative;
        width: 80px;
        height: 80px;
        min-width: 80px;
        border-radius: 6px;
        overflow: hidden;
        background: #ddd;
        cursor: pointer; /* Phase 3C-fix: thumb is click-to-change, NOT drag */
      }
      .so-gallery-row-thumb img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        /* Stop the browser's native image drag — the row handle is the
           only legitimate drag affordance. Phase 3C-fix 29-May-2026:
           previously the image alone could be dragged, showing a
           misleading image-only ghost (the row still moved as a whole
           via dragstart bubbling, but the visual was wrong). */
        user-select: none;
        -webkit-user-drag: none;
        pointer-events: none; /* clicks fall through to the parent thumb */
      }
      .so-gallery-row-thumb-overlay {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 4px;
        background: rgba(0, 0, 0, 0.5);
        color: #fff;
        font-size: 11px;
        font-weight: 500;
        opacity: 0;
        transition: opacity 0.15s;
        pointer-events: none;
      }
      .so-gallery-row-thumb:hover .so-gallery-row-thumb-overlay {
        opacity: 1;
      }
      .so-gallery-row-thumb-overlay .fa {
        font-size: 12px;
      }
      .so-gallery-row-drag {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 24px;
        min-width: 24px;
        color: #999;
        cursor: grab;
        font-size: 16px;
      }
      .so-gallery-row-fields {
        flex: 1;
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
      .so-gallery-row-field {
        display: flex;
        align-items: center;
        gap: 8px;
      }
      .so-gallery-row-field label {
        width: 60px;
        min-width: 60px;
        font-size: 12px;
        color: #666;
        text-transform: uppercase;
      }
      .so-gallery-row-field input {
        flex: 1;
        padding: 8px 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 14px;
      }
      .so-gallery-row-field input:focus {
        outline: none;
        border-color: #4a90d9;
      }
      .so-gallery-row-remove {
        width: 32px;
        height: 32px;
        min-width: 32px;
        background: #fee;
        border: 1px solid #fcc;
        border-radius: 6px;
        color: #c00;
        cursor: pointer;
        font-size: 16px;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: all 0.2s;
      }
      .so-gallery-row-remove:hover {
        background: #fdd;
        border-color: #c00;
      }
      .so-gallery-add-row {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        padding: 16px;
        border: 2px dashed #ccc;
        border-radius: 8px;
        background: #fafafa;
        cursor: pointer;
        color: #666;
        font-size: 14px;
        transition: all 0.2s;
      }
      .so-gallery-add-row:hover {
        border-color: #4a90d9;
        color: #4a90d9;
        background: #f0f7ff;
      }
      .so-gallery-add-row i {
        font-size: 18px;
      }

/* =============================================
   SOCIAL BAR ("Follow Us") component CSS
   Migrated 30-May-2026 (Phase 3.5b-CSSdedup) from
   js/editor4/components/social-bar.js getSocialBarCSS() + the duplicate
   <style data-sb-preview-css> heredoc in temp/_preview.php. Single
   source of truth across editor canvas + preview + shop shell — all
   three contexts load so-components.css. See SOCIAL_BAR_COMPONENT.md.
   ============================================= */
.so-social-links {
  width: 100%;
  position: relative;
}
.so-sb-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--so-sb-spacing, 12px);
  padding: 12px;
  align-items: center;
}
.so-sb-horizontal { flex-direction: row; }
.so-sb-vertical   { flex-direction: column; }
.so-sb-align-left   { justify-content: flex-start; }
.so-sb-align-center { justify-content: center; }
.so-sb-align-right  { justify-content: flex-end; }
.so-sb-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  transition: transform 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
  flex-shrink: 0;
  cursor: pointer;
  /* Phase 3.5a-slider — size driven by CSS var (slider 24-128px in editor).
     Inner SVG scales to ~55% of container (matches pre-slider ratio: 22/40,
     26/48, 30/56, 36/64 all ≈ 0.55). */
  width:  var(--so-sb-icon-size, 48px);
  height: var(--so-sb-icon-size, 48px);
}
.so-sb-icon svg {
  display: block;
  width:  calc(var(--so-sb-icon-size, 48px) * 0.55);
  height: calc(var(--so-sb-icon-size, 48px) * 0.55);
}
.so-sb-icon.so-sb-has-custom-icon {
  overflow: hidden;
  background: none !important;
  position: relative;
}
.so-sb-icon.so-sb-has-custom-icon img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.so-sb-shape-none    { background: transparent; }
.so-sb-shape-circle  { border-radius: 50%; background: #f0f0f0; }
.so-sb-shape-rounded { border-radius: 6px;  background: #f0f0f0; }
.so-sb-shape-square  { border-radius: 0;    background: #f0f0f0; }
.so-sb-icon.so-sb-filled { background: none; }
.so-sb-icon.so-sb-hover-lift:hover {
  transform: translateY(-3px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.so-sb-icon.so-sb-hover-grow:hover { transform: scale(1.15); }
.so-sb-icon.so-sb-hover-fade:hover { opacity: 0.7; }
.so-sb-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 30px 20px;
  min-height: 80px;
  color: #999;
}
@media (max-width: 480px) {
  .so-sb-inner { flex-wrap: wrap; justify-content: center; }
}

/* =============================================
   SHARE BAR ("Share This Page") component CSS
   Migrated 30-May-2026 (Phase 3.5b-CSSdedup) from
   js/editor4/components/share-bar.js getShareBarCSS() + the duplicate
   <style data-shb-preview-css> heredoc in temp/_preview.php. Single
   source of truth. See SHARE_BAR_COMPONENT.md.
   ============================================= */
.so-social-share-bar {
  width: 100%;
  position: relative;
}
.so-shb-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--so-shb-spacing, 12px);
  padding: 12px;
  align-items: center;
}
.so-shb-horizontal { flex-direction: row; }
.so-shb-vertical   { flex-direction: column; }
.so-shb-align-left   { justify-content: flex-start; }
.so-shb-align-center { justify-content: center; }
.so-shb-align-right  { justify-content: flex-end; }
.so-shb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: none;
  text-decoration: none;
  transition: transform 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
  flex-shrink: 0;
  cursor: pointer;
  position: relative;
  font-family: inherit;
  /* Phase 3.5b — size driven by CSS var (slider 24-128px in editor).
     Same ~55% inner-SVG ratio as social-bar. */
  min-width: var(--so-shb-icon-size, 48px);
  height:    var(--so-shb-icon-size, 48px);
}
.so-shb-btn svg {
  display: block;
  flex-shrink: 0;
  width:  calc(var(--so-shb-icon-size, 48px) * 0.55);
  height: calc(var(--so-shb-icon-size, 48px) * 0.55);
}
.so-shb-label {
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  color: inherit;
}
.so-shb-btn.so-shb-has-label { padding: 0 16px; }
.so-shb-shape-none    { background: transparent; }
.so-shb-shape-circle  { border-radius: 50%;  background: #f0f0f0; }
.so-shb-shape-rounded { border-radius: 6px;   background: #f0f0f0; }
.so-shb-shape-square  { border-radius: 0;     background: #f0f0f0; }
.so-shb-shape-pill    { border-radius: 50vh; background: #f0f0f0; padding: 0 16px; }
.so-shb-btn.so-shb-filled { background: none; color: #ffffff; }
.so-shb-btn.so-shb-hover-lift:hover {
  transform: translateY(-3px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.so-shb-btn.so-shb-hover-grow:hover { transform: scale(1.15); }
.so-shb-btn.so-shb-hover-fade:hover { opacity: 0.7; }
.so-shb-copied-tooltip {
  position: absolute;
  bottom: -28px;
  left: 50%;
  transform: translateX(-50%);
  background: #333;
  color: #fff;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s;
}
.so-shb-copied-tooltip.show { opacity: 1; }
.so-shb-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 30px 20px;
  min-height: 80px;
  color: #999;
}
@media (max-width: 480px) {
  .so-shb-inner { flex-wrap: wrap; justify-content: center; }
}

/* =============================================
   FACEBOOK PAGE EMBED ("Facebook Page") component CSS
   Migrated 30-May-2026 (Phase 3.5d) from
   js/editor4/components/embed-facebook-page.js getComponentCSS() + the
   duplicate <style data-efp-preview-css> heredoc in temp/_preview.php.
   Single source of truth. See EDITOR4_COMPONENT_NAMING.md.
   ============================================= */
.so-social-facebook-page { width: 100%; position: relative; }

.so-efp-placeholder {
  position: relative;
  max-width: 340px;
  border: 1px solid #ccd0d5;
  background: #fff;
  overflow: hidden;
  font-family: Helvetica, Arial, sans-serif;
}

/* Blue hover overlay — covers entire card on editor hover */
.so-efp-hover-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(24, 119, 242, 0.35);
  color: #fff;
  font-size: 17px;
  font-weight: 700;
  text-align: center;
  opacity: 0;
  transition: opacity 0.2s;
  z-index: 2;
  pointer-events: none;
  padding: 20px;
}
.so-efp-hover-overlay span {
  background: rgba(24, 119, 242, 0.9);
  padding: 12px 20px;
  border-radius: 8px;
  line-height: 1.45;
}
.so-efp-placeholder:hover .so-efp-hover-overlay { opacity: 1; }

.so-efp-skeleton-line {
  height: 10px;
  background: #e4e6eb;
  border-radius: 5px;
  margin-bottom: 8px;
}
.so-efp-skeleton-line:last-child { margin-bottom: 0; }

.so-efp-placeholder--empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  min-height: 160px;
  text-align: center;
  border: 2px dashed #1877F2;
  border-radius: 8px;
  background: linear-gradient(135deg, #f0f6ff 0%, #e8f0fe 100%);
}
.so-efp-logo { margin-bottom: 10px; }
.so-efp-empty-title { font-size: 15px; font-weight: 700; color: #1c1e21; margin-bottom: 4px; }
.so-efp-empty-hint  { font-size: 13px; color: #65676b; }

.so-efp-icon     { width: 20px; height: 20px; display: block; }
.so-efp-verified { width: 13px; height: 13px; vertical-align: middle; margin-left: 3px; }

.so-efp-header {
  display: flex;
  align-items: center;
  padding: 12px;
  gap: 10px;
  border-bottom: 1px solid #e4e6eb;
}
.so-efp-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #1877F2;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.so-efp-avatar .so-efp-icon { width: 24px; height: 24px; }
.so-efp-hinfo { flex: 1; min-width: 0; }
.so-efp-hname {
  font-size: 14px;
  font-weight: 700;
  color: #050505;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.so-efp-hsub { font-size: 12px; color: #65676b; }

.so-efp-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid #e4e6eb;
}
.so-efp-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 16px;
  border-radius: 4px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
}
.so-efp-btn--follow { background: #1877F2; color: #fff; }
.so-efp-btn--share  { background: #e4e6eb; color: #4b4f56; }

.so-efp-tabs {
  display: flex;
  border-bottom: 1px solid #e4e6eb;
  padding: 0 12px;
}
.so-efp-tab {
  padding: 10px 16px;
  font-size: 13px;
  color: #65676b;
  border-bottom: 3px solid transparent;
  margin-bottom: -1px;
}
.so-efp-tab--active {
  color: #1877F2;
  border-bottom-color: #1877F2;
  font-weight: 700;
}

.so-efp-post {
  border-bottom: 8px solid #f0f2f5;
}
.so-efp-post:last-child { border-bottom: none; }

.so-efp-phead {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 12px 0;
}
.so-efp-pav {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: #e4e6eb;
  flex-shrink: 0;
}
.so-efp-pmeta { flex: 1; min-width: 0; }
.so-efp-pname { font-size: 13px; font-weight: 700; color: #050505; }
.so-efp-pdate { font-size: 11px; color: #65676b; }
.so-efp-pfb   { color: #1877F2; flex-shrink: 0; }

.so-efp-pbody {
  margin: 10px 12px;
  border-radius: 8px;
  padding: 16px;
}

.so-efp-preact {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 12px;
  font-size: 12px;
  color: #65676b;
  border-top: 1px solid #e4e6eb;
}
.so-efp-like-dot {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #1877F2;
  flex-shrink: 0;
}
.so-efp-rcount { font-size: 12px; color: #65676b; }
.so-efp-rright { margin-left: auto; display: flex; gap: 10px; }

/* =================================================
   NAVBAR (so-navbar) — Phase 3.5c
   ================================================= */
/* Migrated 31-May-2026 out of navbar.js ensureNavbarCssInManager() +
   editor.Css.addRules. Canonical source: assets/editor4/so-components.css.
   var()-based properties take precedence (via !important) over any stale
   legacy rules that may still live in tenant CSS from pre-migration days.
   See COMPONENT_SPLITTING_DECISION_FRAMEWORK.md §6.1 takeaway #5. */

.so-navbar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  /* !important padding/margin/border/background:transparent — defence
     against stale tenant CSS from pre-3.5c which had
     `padding: 1rem 2rem; background-color: #f8f9fa; border-bottom: 1px solid #dee2e6`
     on the OUTER .so-navbar. Without !important here, the stale gray
     bg leaks through .so-navbar-inner's transparent bg → user sees gray.
     Same Phase 4g pattern. Bar color now lives ONLY on .so-navbar-inner
     (commit 4.5 architectural split — vars baked via render). */
  padding: 0 !important;
  margin: 0 !important;
  /* Per-side border widths (commit 5.14) — replaces position-attr scoped
     rules. Each side reads its own var; 0 = no visible border on that
     side. `border-style: solid !important` is the base so each side just
     needs its width var set. !important on each prop defends against
     stale tenant CSS. */
  border-style: solid !important;
  border-color: var(--so-nav-border-color, #dee2e6) !important;
  border-top-width:    var(--so-nav-border-top, 0)    !important;
  border-right-width:  var(--so-nav-border-right, 0)  !important;
  border-bottom-width: var(--so-nav-border-bottom, 0) !important;
  border-left-width:   var(--so-nav-border-left, 0)   !important;
  background-color: transparent !important;
  border-radius: var(--so-nav-radius-tl, 0) var(--so-nav-radius-tr, 0)
                 var(--so-nav-radius-br, 0) var(--so-nav-radius-bl, 0);
}

/* Phase 3.5c commit 4.5: --so-nav-bar-bg is set on .so-navbar-inner
   by render() (carousel pattern — bake CSS vars into the rendered HTML).
   The OUTER .so-navbar can't read children's vars, so the background
   visual is owned by .so-navbar-inner (which spans full width and
   shows it). */
.so-navbar.so-nav-sticky {
  position: sticky;
  top: 0;
  z-index: 1000;
}

.so-navbar-inner {
  display: flex;
  flex-wrap: wrap;
  /* Stretch (commit 5.18) — child .so-navbar-menu fills full height of
     inner, which lets items stretch vertically and their hover bg cover
     the bar's full height. Was `center` previously, leaving items at
     natural content height with visible bar bg above/below hover area. */
  align-items: stretch;
  /* Padding/margin 0 !important (commit 5.20) — defends against stale
     tenant CSS from pre-3.5c that had `padding: 1rem 2rem` baked in on
     .so-navbar-inner. Without !important the stale rule wins (loads
     AFTER so-components.css with equal specificity → source order wins)
     pushing items inward and breaking the hover-to-edges visual. Same
     Phase 4g pattern as .so-navbar. */
  padding: 0 !important;
  margin: 0 !important;
  /* Default = transparent so column/section bg shows through when merchant
     hasn't picked a bar color. Changed 1-Jun-2026 (commit 5.7) — old default
     #f8f9fa hid column bg images even when picker showed 'transparent'. */
  background-color: var(--so-nav-bar-bg, transparent) !important;
  /* Inherit radius from .so-navbar — without this, inner's square corners
     paint OVER the outer's rounded corners when bg is opaque. Vars cascade
     down from outer (commit 5.9). Updated to per-corner vars in commit 5.12. */
  border-radius: var(--so-nav-radius-tl, 0) var(--so-nav-radius-tr, 0)
                 var(--so-nav-radius-br, 0) var(--so-nav-radius-bl, 0);
  /* !important here because stale .so-navbar-inner rules from pre-3.5c
     tenant CSS (hardcoded justify-content: space-between) load AFTER
     so-components.css and would otherwise win by source order. Same
     defence-in-depth pattern as Phase 4g for carousels. */
  justify-content: var(--so-nav-justify, space-between) !important;
  width: 100%;
}

.so-navbar-brand {
  font-size: 1.5rem;
  font-weight: bold;
  color: #333;
  text-decoration: none;
  margin-right: 2rem;
}

.so-navbar-brand:hover {
  color: #007bff;
}

.so-navbar-toggle {
  display: none;
  padding: 0.5rem;
  background: transparent;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 4px;
  cursor: pointer;
}

.so-navbar-toggle-icon {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: 24px;
  height: 18px;
}

.so-navbar-toggle-icon span {
  display: block;
  width: 100%;
  height: 2px;
  background-color: #333;
}

.so-navbar-menu {
  display: flex;
  flex-direction: row;
  /* Stretch items to fill menu's height (commit 5.19) — so each item's
     hover bg covers the full bar height including the rounded-corner
     curve area. Was `center` which left items at content-height,
     leaving bar bg visible above/below items inside the curves. */
  align-items: stretch;
  list-style: none;
  margin: 0;
  padding: 0;
  /* Commit 5.16: gap = 0 always. Item-spacing is now per-item padding
     (half the gap on each side of each item). This lets hover bg fill
     the full item INCLUDING the gap area, all the way to the divider. */
  gap: 0 !important;
  /* No-wrap so menu items always stay on one row (matches the per-link
     white-space:nowrap rule). Phase 3.5c commit 5.13 narrative. */
  flex-wrap: nowrap;
}

/* When items are stretched, vertically center the link inside each item
   so the text stays in the middle of the now-taller item. */
.so-nav-item {
  display: flex;
  align-items: center;
}
.so-nav-item > .so-nav-link {
  display: flex;
  align-items: center;
}

/* Items — bg states live here (was on .so-nav-link pre-5.16) so hover
   bg covers the full item area including the padding that replaces
   what used to be flex gap. */
.so-nav-item {
  position: relative;
  padding-left:  calc(var(--so-nav-item-gap-x, 0px) / 2);
  padding-right: calc(var(--so-nav-item-gap-x, 0px) / 2);
  background-color: transparent;
  transition: background-color 0.2s ease;
}
/* First/last items: no outer padding so bar fills column edge-to-edge. */
.so-navbar-menu > .so-nav-item:first-child { padding-left:  0 !important; }
.so-navbar-menu > .so-nav-item:last-child  { padding-right: 0 !important; }

/* Hover follows bar corners (commit 5.17) — when toggle ON, first/last
   items inherit the bar's outer corner radii so hover bg paints into
   the rounded corner shape instead of stopping at a rectangle inside it.
   data-hover-to-edges attr set on .so-navbar by handleConfigChange + EPL. */
.so-navbar[data-hover-to-edges="true"] .so-navbar-menu > .so-nav-item:first-child {
  border-top-left-radius:    var(--so-nav-radius-tl, 0);
  border-bottom-left-radius: var(--so-nav-radius-bl, 0);
}
.so-navbar[data-hover-to-edges="true"] .so-navbar-menu > .so-nav-item:last-child {
  border-top-right-radius:    var(--so-nav-radius-tr, 0);
  border-bottom-right-radius: var(--so-nav-radius-br, 0);
}

/* Hover-to-edges + non-left alignment NOT IMPLEMENTED. Attempted in
   commits 5.21/5.22 (data-nav-alignment + flex-grow + justify-content
   with !important) — lost the cascade fight. Reverted in 5.23. Future
   work: diagnose with cascade probe; see todo "hover-to-edges full
   stretch (NICE TO HAVE)". For now, hover-to-edges only fills the bar's
   curved corners visually when alignment=Left. */

.so-nav-item:hover {
  background-color: var(--so-nav-link-hover-bg, transparent) !important;
}
.so-nav-item.so-nav-active {
  background-color: var(--so-nav-link-active-bg, transparent) !important;
}

/* Divider lives in margin-right of each item (except last). When divider
   width = 0 (dividers OFF), margin = 0 so items still touch. When divider
   width > 0, items are separated by divider-width and the divider pseudo
   sits in that space — fully OUTSIDE item's hover-bg area. So hover bg
   fills the item exactly to its edge, never under the divider, regardless
   of divider thickness. Phase 3.5c commit 5.16. */
.so-nav-item:not(:last-child) {
  margin-right: var(--so-nav-divider-width, 0);
}
.so-nav-item:not(:last-child)::after {
  content: '';
  position: absolute;
  top: 50%;
  /* Divider sits in the item's margin-right (right of item bg, full width). */
  right: calc(-1 * var(--so-nav-divider-width, 0px));
  transform: translateY(-50%);
  width: var(--so-nav-divider-width, 0);
  height: var(--so-nav-divider-height, 100%);
  background-color: var(--so-nav-divider-color, #dee2e6);
  pointer-events: none;
}

.so-nav-link {
  display: block;
  /* Padding driven by settings. !important required to beat stale tenant CSS. */
  padding: var(--so-nav-link-padding-y, 0.5rem) var(--so-nav-link-padding-x, 1rem) !important;
  white-space: nowrap;
  color: var(--so-nav-link-color, #333) !important;
  /* Commit 5.16: bg moved to .so-nav-item — link always transparent so
     the item-bg-hover can fill the full item area including its padding
     (which is where the gap used to live). */
  background-color: transparent !important;
  font-family: var(--so-nav-link-font, inherit) !important;
  font-size: var(--so-nav-link-size, 1rem) !important;
  text-decoration: none;
  transition: color 0.3s ease;
}

/* Text color states — driven by hover/active on the ITEM so they
   activate when cursor is anywhere over the item (link + its padding
   area). Pre-5.16 these were on .so-nav-link directly. */
.so-nav-item:hover .so-nav-link {
  color: var(--so-nav-link-hover-color, #007bff) !important;
}
.so-nav-item.so-nav-active .so-nav-link,
.so-nav-link.so-nav-active {
  color: var(--so-nav-link-active-color, #007bff) !important;
}

/* Dropdown indicator */
.so-nav-item.has-children > .so-nav-link::after {
  content: ' ▼';
  font-size: 0.7em;
  margin-left: 0.3em;
}

.so-submenu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  background: var(--so-nav-dropdown-bg, #ffffff) !important;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 0.25rem;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.175);
  min-width: 10rem;
  z-index: 1000;
  margin: 0.125rem 0 0;
  padding: 0.5rem 0;
  list-style: none;
}

.so-nav-item.has-children:hover > .so-submenu {
  display: block;
}

.so-sub-item {
  margin: 0;
}

.so-sub-item .so-nav-link {
  padding: 0.5rem 1.5rem;
  white-space: nowrap;
  color: var(--so-nav-dropdown-link-color, #333) !important;
  font-family: var(--so-nav-dropdown-link-font, inherit) !important;
  font-size: var(--so-nav-dropdown-link-size, 0.875rem) !important;
}

.so-sub-item .so-nav-link:hover {
  background-color: var(--so-nav-dropdown-link-hover-bg, #f8f9fa) !important;
  color: var(--so-nav-dropdown-link-hover-color, var(--so-nav-dropdown-link-color, #333)) !important;
}

.so-sub-item.so-nav-active > .so-nav-link,
.so-sub-item .so-nav-link.so-nav-active {
  background-color: var(--so-nav-dropdown-link-active-bg, transparent) !important;
  color: var(--so-nav-dropdown-link-active-color, var(--so-nav-dropdown-link-color, #333)) !important;
}

/* (Full-width utility removed 31-May-2026 — merchants use the row-level
   container/container-fluid toggle for viewport-wide navbar.) */

/* Mobile breakpoint — Phase 3.5c commit 4. Each breakpoint is scoped to
   navbars that opt into it via data-nav-mobile-bp="N". Default (no attr)
   collapses at 992px to match pre-commit-4 behaviour. The shared
   "collapsed nav" block is defined ONCE and applied via attr selectors
   to avoid CSS duplication across breakpoints. */

/* Default (no attr) + 992 explicit */
@media (max-width: 991.98px) {
  .so-navbar:not([data-nav-mobile-bp]) .so-navbar-toggle,
  .so-navbar[data-nav-mobile-bp="992"] .so-navbar-toggle { display: block; }
  .so-navbar:not([data-nav-mobile-bp]) .so-navbar-menu,
  .so-navbar[data-nav-mobile-bp="992"] .so-navbar-menu {
    display: none;
    flex-direction: column;
    width: 100%;
    margin-top: 1rem;
  }
  .so-navbar:not([data-nav-mobile-bp]).open .so-navbar-menu,
  .so-navbar[data-nav-mobile-bp="992"].open .so-navbar-menu { display: flex; }
  .so-navbar:not([data-nav-mobile-bp]) .so-nav-item,
  .so-navbar[data-nav-mobile-bp="992"] .so-nav-item { width: 100%; }
  .so-navbar:not([data-nav-mobile-bp]) .so-nav-link,
  .so-navbar[data-nav-mobile-bp="992"] .so-nav-link {
    width: 100%;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid #dee2e6;
  }
  .so-navbar:not([data-nav-mobile-bp]) .so-submenu,
  .so-navbar[data-nav-mobile-bp="992"] .so-submenu {
    position: static;
    box-shadow: none;
    border: none;
    padding-left: 1rem;
    background: var(--so-nav-dropdown-bg, #f8f9fa) !important;
  }
}

/* 768 — collapse later */
@media (max-width: 767.98px) {
  .so-navbar[data-nav-mobile-bp="768"] .so-navbar-toggle { display: block; }
  .so-navbar[data-nav-mobile-bp="768"] .so-navbar-menu {
    display: none;
    flex-direction: column;
    width: 100%;
    margin-top: 1rem;
  }
  .so-navbar[data-nav-mobile-bp="768"].open .so-navbar-menu { display: flex; }
  .so-navbar[data-nav-mobile-bp="768"] .so-nav-item { width: 100%; }
  .so-navbar[data-nav-mobile-bp="768"] .so-nav-link {
    width: 100%;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid #dee2e6;
  }
  .so-navbar[data-nav-mobile-bp="768"] .so-submenu {
    position: static;
    box-shadow: none;
    border: none;
    padding-left: 1rem;
    background: var(--so-nav-dropdown-bg, #f8f9fa) !important;
  }
}

/* 1200 — collapse earlier (for sites with lots of links) */
@media (max-width: 1199.98px) {
  .so-navbar[data-nav-mobile-bp="1200"] .so-navbar-toggle { display: block; }
  .so-navbar[data-nav-mobile-bp="1200"] .so-navbar-menu {
    display: none;
    flex-direction: column;
    width: 100%;
    margin-top: 1rem;
  }
  .so-navbar[data-nav-mobile-bp="1200"].open .so-navbar-menu { display: flex; }
  .so-navbar[data-nav-mobile-bp="1200"] .so-nav-item { width: 100%; }
  .so-navbar[data-nav-mobile-bp="1200"] .so-nav-link {
    width: 100%;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid #dee2e6;
  }
  .so-navbar[data-nav-mobile-bp="1200"] .so-submenu {
    position: static;
    box-shadow: none;
    border: none;
    padding-left: 1rem;
    background: var(--so-nav-dropdown-bg, #f8f9fa) !important;
  }
}

/* Navbar settings modal internals (Phase 3.5c commit 2.1) */
.so-navbar-drag-handle {
  display: inline-block;
  width: 24px;
  text-align: center;
  cursor: grab;
  color: #9ca3af;
  font-size: 16px;
  user-select: none;
  padding: 4px;
  border-radius: 4px;
  transition: background 0.15s, color 0.15s;
}
.so-navbar-drag-handle:hover { background: #f3f4f6; color: #4b5563; }
.so-navbar-drag-handle:active { cursor: grabbing; }

.so-navbar-action-btn {
  width: 28px;
  height: 28px;
  padding: 0;
  background: #f3f4f6;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  color: #374151;
  transition: background 0.15s, border-color 0.15s;
}
.so-navbar-action-btn:hover { background: #e5e7eb; border-color: #9ca3af; }
.so-navbar-delete-btn { background: #fee2e2; border-color: #fecaca; color: #dc2626; }
.so-navbar-delete-btn:hover { background: #fecaca; border-color: #fca5a5; }

.so-navbar-input {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  font-size: 13px;
  box-sizing: border-box;
}
.so-navbar-input:focus {
  outline: none;
  border-color: #4a90d9;
  box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.2);
}

.so-navbar-table td { padding: 6px 8px; vertical-align: middle; }
.so-navbar-table tr { border-bottom: 1px solid #e5e7eb; }
.so-navbar-table tr:last-child { border-bottom: none; }
.so-navbar-table tr.sortable-ghost { opacity: 0.4; background: #e0f2fe; }
.so-navbar-table tr.sortable-chosen { background: #f0f9ff; }
.so-navbar-table tr.so-navbar-child-row { background: #fafafa; }
.so-navbar-table tr.so-navbar-child-row td:first-child { padding-left: 28px; }

.so-navbar-btn-success:hover { background: #218838 !important; }

/* ==========================================================================
   so-avatar — avatar list/stack component (Phase A — MVP, 2026-06-23)
   ==========================================================================
   Promoted from PB-GAPS-LOG #9 (the inline avatar-stack workaround in
   cta-social-proof). Black Box atomic component — modal-driven editing
   in Phase B. Phase A renders sample data via inline-styled <img>
   children; CSS-class-driven sizing arrives in Phase B.

   Spec: .claude/docs/EDITOR4_AVATAR_COMPONENT.md
   ========================================================================== */

.so-avatar {
  display: inline-flex;
  align-items: center;
  flex-wrap: nowrap;
  /* Cluster background — set by --so-avatar-bg inline (handleConfigChange).
     Defaults transparent; merchant can change via Styles tab Avatar
     Background subsection. */
  background: var(--so-avatar-bg, transparent);
}

/* Stack variant: overlapping circles via negative margin on siblings.
   Phase A applies the overlap inline on each <img> (margin-left:-18px);
   the CSS variable approach is Phase B when the modal owns the value. */
.so-avatar--stack > .so-avatar-item + .so-avatar-item {
  /* No CSS rule needed for Phase A — overlap is inline on each item.
     Phase B will move to: margin-left: var(--so-avatar-overlap, -18px); */
}

/* Grid variant (Phase C): non-overlapping side-by-side */
.so-avatar--grid {
  gap: 8px;
}

.so-avatar--grid > .so-avatar-item + .so-avatar-item {
  margin-left: 0;
}

/* Per-item: circular, cropped, with white border (default) */
.so-avatar-item {
  display: block;
  border-radius: 50%;
  object-fit: cover;
  /* width / height / border applied inline on each <img> in Phase A
     for sample-data robustness. Phase B moves these to size-variant
     classes once the modal pipeline lands:
       .so-avatar--size-sm .so-avatar-item { width: 48px; height: 48px; }
       .so-avatar--size-md .so-avatar-item { width: 64px; height: 64px; }
       .so-avatar--size-lg .so-avatar-item { width: 80px; height: 80px; }
       .so-avatar--size-xl .so-avatar-item { width: 96px; height: 96px; }
   */
}

/* Editor-mode: keep the cluster compact (avoid Bootstrap row-stretch
   from any inadvertent .row class on a parent expanding it). */
body.editor-mode .so-avatar {
  flex: 0 0 auto;
}

/* Empty state (when all avatars deleted from the modal) */
.so-avatar-empty {
  display: inline-flex;
  align-items: center;
  padding: 12px 18px;
  background: rgba(0, 0, 0, 0.04);
  border: 1px dashed rgba(0, 0, 0, 0.25);
  border-radius: 12px;
  color: rgba(0, 0, 0, 0.55);
  font-size: 13px;
  font-style: italic;
}

/* ──────────────────────────────────────────────────────────────────
   Avatar — variant + justify additions (Phase C, 2026-06-23)
   ────────────────────────────────────────────────────────────────── */

/* Grid variant — gap (no overlap). Inherits display: inline-flex from
   the base .so-avatar rule. */
.so-avatar--grid {
  gap: 8px;
}

/* "+N more" pill — overflow variant. The pill matches avatar size via
   inline styling (width/height set by render); these rules govern its
   typographic + interactive feel. */
.so-avatar-more {
  user-select: none;
  flex: 0 0 auto;
}

/* Justify alignment — makes the cluster a block-level flex container
   with explicit horizontal positioning. Overrides the default
   inline-flex (which hugs content). Useful when the parent column is
   left-aligned and the merchant explicitly wants the avatar centered. */
.so-avatar--justify-start,
.so-avatar--justify-center,
.so-avatar--justify-end {
  display: flex;
  width: 100%;
}
.so-avatar--justify-start  { justify-content: flex-start; }
.so-avatar--justify-center { justify-content: center; }
.so-avatar--justify-end    { justify-content: flex-end; }

/* ──────────────────────────────────────────────────────────────────
   Avatar — Phase D additions: shape + hover effects (2026-06-23)
   ────────────────────────────────────────────────────────────────── */

/* The wrap around each <img> + optional popup. position:relative
   anchors the absolutely-positioned popup. inline-flex matches the
   parent's layout. */
.so-avatar-item-wrap {
  position: relative;
  display: inline-flex;
}

/* Hover popover — appears above the avatar when hover-popover effect
   is active AND the item has a caption. Card matches Twitter-style
   profile-card pattern: white bg, rounded, small drop shadow,
   downward caret. Hidden by default. */
.so-avatar-popup {
  display: none;
  position: absolute;
  bottom: calc(100% + 12px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  background: #ffffff;
  color: #111827;
  padding: 8px 14px;
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(0, 0, 0, 0.08);
  white-space: nowrap;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  pointer-events: none;
}
.so-avatar-popup::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  /* Arrow follows the popup background (set by inline --so-avatar-popup-bg).
     Falls back to white if the variable isn't set. */
  border-top-color: var(--so-avatar-popup-bg, #ffffff);
  filter: drop-shadow(0 2px 1px rgba(0, 0, 0, 0.06));
}

/* Activate the popover on the parent's hover-popover class. Only show
   when hovering the specific avatar wrap (not the whole container). */
.so-avatar--hover-popover .so-avatar-item-wrap:hover {
  z-index: 30;
}
.so-avatar--hover-popover .so-avatar-item-wrap:hover .so-avatar-popup {
  display: block;
}

/* Auto-flip: when the wrap carries .so-avatar-popup-below (set by the
   view's mouseover handler when there's no room above due to an
   overflow:hidden ancestor), flip the popup BELOW the avatar with the
   arrow now pointing UP. Same card styling, just inverted position +
   arrow direction. Added 2026-06-23 — JC repro: cta-social-proof
   section has overflow:hidden for decorative blobs; popup above
   gets clipped at section top. */
.so-avatar-item-wrap.so-avatar-popup-below .so-avatar-popup {
  bottom: auto;
  top: calc(100% + 12px);
}
.so-avatar-item-wrap.so-avatar-popup-below .so-avatar-popup::after {
  top: auto;
  bottom: 100%;
  border-top-color: transparent;
  /* Below variant: arrow points UP, uses the same bg variable */
  border-bottom-color: var(--so-avatar-popup-bg, #ffffff);
  filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.06));
}

/* Spread effect — when the cluster is hovered, the overlap amount
   gets reset (margin-left:0) so avatars spread out evenly. The
   transition on .so-avatar-item is in the inline style. */
.so-avatar--hover-spread:hover .so-avatar-item-wrap {
  margin-left: 4px !important;
}
.so-avatar--hover-spread:hover .so-avatar-item-wrap:first-child {
  margin-left: 0 !important;
}

/* Scale effect — the hovered avatar grows 110%. Other avatars stay
   put. Combine with the inline transition for smooth ease. */
.so-avatar--hover-scale .so-avatar-item-wrap:hover .so-avatar-item {
  transform: scale(1.1);
  z-index: 25;
}

/* Lift effect — hovered avatar slides up + comes to front. */
.so-avatar--hover-lift .so-avatar-item-wrap:hover {
  z-index: 25;
}
.so-avatar--hover-lift .so-avatar-item-wrap:hover .so-avatar-item {
  transform: translateY(-6px);
}

/* Slide-left effect (Phase D refactor, 23 Jun 2026) — adapted from
   prebuiltui.com avatar "slide-on-hover" pattern. On per-avatar
   hover, the item shifts left ~8px revealing a glimpse of the
   neighbour underneath. Pairs naturally with the stack variant
   (where avatars overlap right-to-left). Inline transition on the
   <img> handles the smooth ease. */
.so-avatar--hover-slide-left .so-avatar-item-wrap:hover .so-avatar-item {
  transform: translateX(-8px);
}

/* NOTE: pre-refactor (23 Jun 2026) avatars supported combining
   multiple effects (scale + lift simultaneously). The new model is
   ONE positional effect at a time via the Hover behaviour dropdown,
   so the compound selector is no longer needed and was removed.
   Popover stays as a separate independent toggle. */

/* ==========================================================================
   so-overlay-box — standalone positioned overlay primitive (2026-06-24)
   ==========================================================================
   Refactored from the combined so-image-overlay wrapper (which packaged
   image + overlay-content). Per GPT check-in #3 + JC's testing:
   standalone primitive that drops into any column, anchored via
   9-position class + --ov-x / --ov-y CSS variables.

   Same pattern as so-accent-layer (which anchors to its parent section).
   The "image with overlay" combo lives as a PREBUILT BLOCK template,
   not as a wrapped component.

   Parent column needs position: relative — set via the
   so-overlay-enabled class (the component's init() adds this to the
   parent on add, mirroring so-accent-layer's _ensureParentSetup).
   ========================================================================== */

/* Parent column setup — any so-col containing a so-overlay-box gets
   position:relative so the box's absolute positioning anchors there.
   Also flex-column so the anchor wrapper (when present) can stretch
   to fill the column's height — keeps the image fill-bleed when the
   sibling column is taller via Bootstrap equal-height row stretch.

   JC observation 2026-06-25 ("we were doing it well 2 days ago"):
   the original so-image-overlay (combined wrapper) sized ITSELF to
   the image. The standalone refactor lost that — image became a
   plain col child and Bootstrap stretching exposed the whitespace
   below. This flex-column behaviour on the col + the
   `so-overlay-anchor` flex-fill rule below restores the original
   visual without re-coupling the components. */
.so-col.so-overlay-enabled {
  position: relative;
  display: flex;
  flex-direction: column;
}

/* Anchor wrapper — used when merchant picks "Anchor to: Previous
   element" in Settings. The wrapper persists in the model + DOM
   (NOT a render-time trick per GPT check-in #4 #2026-06-25).
   Contains the overlay's previous sibling + the overlay itself.
   position:relative establishes the containing block for the
   overlay's absolute positioning — overlay now sticks to the
   sibling instead of the whole column. */
.so-overlay-anchor {
  position: relative;
  /* Stretch to fill the column's height when the column is taller
     than the wrapper's intrinsic content height. Lets the image
     inside cover-fill the available space. */
  flex: 1 1 auto;
  min-height: 0;
  /* Inner layout — image stretches to fill; overlay-box is
     absolute-positioned on top. */
  display: flex;
  flex-direction: column;
}

/* Image inside an overlay-anchor wrapper fills the wrapper. Uses
   `object-fit: cover` so the image crops to the available area
   rather than distorting. min-height: 0 lets the image shrink in
   flex contexts where intrinsic content otherwise creates a floor. */
.so-overlay-anchor > img:first-child {
  width: 100%;
  height: 100%;
  flex: 1 1 auto;
  min-height: 0;
  object-fit: cover;
}

/* The overlay box itself. Default styling: blue card, white text,
   rounded corners, drop shadow. Merchant overrides via standard
   Style popover (bg via Token-Bake convention).

   BACKGROUND CONVENTION: use `background-image: linear-gradient(c, c)`
   to match what the Style Manager writes. Otherwise transparent
   edits silently fail (merchant write goes to background-image,
   our default is on background-color, different properties).
   See PB-FACTORY-LOG lesson "Background longhand convention". */
.so-overlay-box {
  position: absolute;
  z-index: 2;
  /* bg expressed as background-image flat gradient — the format the
     grapesjs-style-bg popover Color picker reads + writes. Setting
     `background-color` here instead conflicts with the shim (Color
     picker writes to bg-image, bg-color sticks, merchant edits don't
     reflect, transparent doesn't clear). Single-stop linear-gradient
     renders identical to solid colour while staying shim-compatible.
     Same pattern other so- components use. Same value in
     overlay-box.js defaults (Token Bake also writes here). */
  background-image: linear-gradient(to bottom, #0d6efd 0%, #0d6efd 100%);
  color: #ffffff;
  padding: 16px;
  border-radius: var(--radius-lg, 16px);
  box-shadow: var(--shadow-lg, 0 16px 48px 0 rgba(0, 0, 0, 0.175));
  max-width: 100%;
  /* Flex column with gap support — same model as so-col. Lets the
     merchant control Child Spacing (vertical gap between children)
     via popover Settings, AND lets the spacing chrome paint
     diagonal gap stripes between children (paintColumnGaps in
     so-spacing-chrome.js is generic — works on any flex container
     with gap > 0). Default gap 0 → no gap until merchant picks one. */
  display: flex;
  flex-direction: column;
  gap: 0;
  /* --ov-x and --ov-y default to 24px; the box's own trait listener
     updates these via setStyle on change. */
  --ov-x: 24px;
  --ov-y: 24px;
}

/* Single-source-of-spacing rule — zero browser-default margins on
   direct children that ship with default margins (text elements +
   any registered component). The overlay-box's gap setting is the
   sole source of inter-element spacing inside. Selector scoped to
   margin-bearing elements specifically so we don't accidentally
   zero things that didn't have margins to begin with. See PB-002
   spacing doctrine. */
.so-overlay-box > h1, .so-overlay-box > h2, .so-overlay-box > h3,
.so-overlay-box > h4, .so-overlay-box > h5, .so-overlay-box > h6,
.so-overlay-box > p, .so-overlay-box > ul, .so-overlay-box > ol,
.so-overlay-box > blockquote, .so-overlay-box > pre,
.so-overlay-box > figure, .so-overlay-box > hr,
.so-overlay-box > [data-gjs-type] {
  margin-top: 0;
  margin-bottom: 0;
}

/* Child Spacing — vertical gap between direct children. Same token
   scale as so-col (xs..4xl). Merchant picks via Settings dropdown. */
.so-overlay-box.so-overlay-gap-xs   { gap: var(--space-xs,    4px); }
.so-overlay-box.so-overlay-gap-sm   { gap: var(--space-sm,    8px); }
.so-overlay-box.so-overlay-gap-md   { gap: var(--space-md,   16px); }
.so-overlay-box.so-overlay-gap-lg   { gap: var(--space-lg,   24px); }
.so-overlay-box.so-overlay-gap-xl   { gap: var(--space-xl,   32px); }
.so-overlay-box.so-overlay-gap-xxl  { gap: var(--space-xxl,  48px); }
.so-overlay-box.so-overlay-gap-xxxl { gap: var(--space-xxxl, 64px); }
.so-overlay-box.so-overlay-gap-4xl  { gap: var(--space-4xl,  80px); }

/* 9-position anchors — class drives top/left/right/bottom + transform.
   --ov-x / --ov-y CSS vars carry the px offset values (default 24px). */
.so-overlay-box[data-so-overlay-pos="tl"] { top: var(--ov-y); left: var(--ov-x); }
.so-overlay-box[data-so-overlay-pos="tc"] { top: var(--ov-y); left: 50%; transform: translateX(-50%); }
.so-overlay-box[data-so-overlay-pos="tr"] { top: var(--ov-y); right: var(--ov-x); }
.so-overlay-box[data-so-overlay-pos="ml"] { top: 50%; left: var(--ov-x); transform: translateY(-50%); }
.so-overlay-box[data-so-overlay-pos="mc"] { top: 50%; left: 50%; transform: translate(-50%, -50%); }
.so-overlay-box[data-so-overlay-pos="mr"] { top: 50%; right: var(--ov-x); transform: translateY(-50%); }
.so-overlay-box[data-so-overlay-pos="bl"] { bottom: var(--ov-y); left: var(--ov-x); }
.so-overlay-box[data-so-overlay-pos="bc"] { bottom: var(--ov-y); left: 50%; transform: translateX(-50%); }
.so-overlay-box[data-so-overlay-pos="br"] { bottom: var(--ov-y); right: var(--ov-x); }

/* Editor-mode visibility — faint dashed outline on hover so the box
   boundary is visible even when its bg is transparent. Removed on publish. */
body.editor-mode .so-overlay-box {
  outline: 1px dashed transparent;
}
body.editor-mode .so-overlay-box:hover {
  outline-color: rgba(255, 255, 255, 0.4);
}

/* ──────────────────────────────────────────────────────────────────
   Drop-placeholder UX while DRAGGING an Overlay Box
   ──────────────────────────────────────────────────────────────────
   Per GPT check-in #3 (Option ii): hide the standard insertion line
   (which implies document-flow semantics that don't apply to an
   absolute-positioned primitive), and highlight the target column
   as a whole with a labelled drop-zone treatment.

   Body class `so-dragging-overlay-box` is toggled on both chrome
   AND canvas iframe bodies by overlay-box.js drag-event hooks.

   The column under the cursor gets `so-overlay-drop-target` class
   from a mousemove/dragover tracker (also in overlay-box.js).
   ────────────────────────────────────────────────────────────────── */

/* Hide ALL standard drop indicators during overlay-box drag — the
   standard in-flow placeholder (`.e4-drop-placeholder`) expands
   sibling spacing to insert the dropped component between siblings.
   That's wrong semantics for a positioned primitive: overlay-box
   doesn't go BETWEEN siblings, it OVERLAYS the column. Hide them
   all and let our own overlay-specific UX (so-overlay-drop-target
   class on the hovered column + centered label) communicate the
   correct mental model.

   Suppressed:
   - `.e4-drop-placeholder` — the editor4 drop placeholder (THIS is
     the one that expanded spacing between siblings)
   - `#e4-column-highlight` — standard column blue-border outline
   - `#e4-column-badge` — standard "Column" label
   - `.gjs-placeholder` — GrapesJS native fallback indicator */
body.so-dragging-overlay-box .e4-drop-placeholder,
body.so-dragging-overlay-box #e4-column-highlight,
body.so-dragging-overlay-box #e4-column-badge,
body.so-dragging-overlay-box .gjs-placeholder,
body.so-dragging-overlay-box .gjs-com-no-select {
  display: none !important;
}

/* Drop-target visual style — REUSES the same purple-dashed-border +
   gradient-bg pattern as `.e4-drop-placeholder` (standard "Drop
   Component Here" placeholder in so-bootstrap-grid.js line ~4193).
   Just applied to the WHOLE column instead of an inline between-
   siblings band. Same visual family, different positioning logic.

   Resting state — every column gets a very faint purple tint so the
   merchant sees "these are the drop targets" without overwhelming
   visual noise. position:relative so the indicator can absolute-
   position inside. */
body.so-dragging-overlay-box .so-col {
  background-color: rgba(111, 66, 193, 0.05);
  transition: background-color 0.15s ease, border-color 0.15s ease;
  position: relative !important;
}

/* Active drop target — the column the cursor is over. Purple dashed
   border + gradient background matching the standard placeholder. */
body.so-dragging-overlay-box .so-col.so-overlay-drop-target {
  border: 2px dashed #6f42c1 !important;
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(111, 66, 193, 0.1) 50%,
    transparent 100%) !important;
  border-radius: 6px !important;
}

/* Indicator (download SVG + "Drop Overlay Box on this column")
   injected into the active drop target by overlay-box.js
   _setDropTarget. Centered absolutely; non-interactive so it doesn't
   intercept the drop.

   Wrapped in a white pill with soft shadow so the text stays legible
   on any column content — including images, dark backgrounds, and
   busy patterns. JC 2026-06-25: "difficult to see when you drag
   over images." */
.so-overlay-drop-indicator {
  position: absolute !important;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex !important;
  align-items: center;
  justify-content: center;
  gap: 10px;
  pointer-events: none;
  z-index: 9999;
  background-color: rgba(255, 255, 255, 0.96);
  border: 2px solid #6f42c1;
  border-radius: 8px;
  padding: 10px 18px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22), 0 2px 6px rgba(0, 0, 0, 0.12);
  animation: so-overlay-drop-indicator-in 0.12s ease-out;
}

@keyframes so-overlay-drop-indicator-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.94); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* ==========================================================================
   so-content-grid — CSS-Grid container for repeated content items (2026-06-23)
   ==========================================================================
   Composition primitive per EDITOR4_LAYOUT_EVOLUTION_DECISION_JUN2026.md.
   Replaces the nested-row-in-col instinct for stats / features / services /
   trust points. CSS Grid with responsive column count.

   Per directive:
     - align-items: start (only the item that wraps grows taller —
       no cross-item row equalisation)
     - .so-content-grid-item { min-width: 0 } (normal wrapping)
     - NO horizontal row lines (this is NOT a table — items grow
       independently)
   ========================================================================== */

.so-content-grid {
  display: grid;
  /* 2026-06-26 — Custom-track-width foundation (Commit 1 of the
     track-resize feature). DO NOT write `grid-template-columns`
     directly inline on the element — that breaks responsive
     stacking (inline beats media query). Instead, write
     `--cg-template-custom` inline; the cascade chain below
     resolves it to grid-template-columns through CSS rules only.
     Mobile media query writes `grid-template-columns: 1fr`
     directly via a rule (wins over the desktop rule, beats no
     inline because we never write the property inline).

     Cascade chain:
       1. JS sets inline `--cg-template-custom: 120px 1fr` (custom widths)
       2. CSS rule resolves `var(--cg-template-custom, var(--cg-equal-template))`
          → if custom is set, use it; else use equal-template
       3. `--cg-equal-template` is responsive via the media queries
          at the bottom of this section (tablet caps equal at 2 cols)
       4. Mobile writes `grid-template-columns: 1fr` directly via
          a media-query rule — wins because we never write
          `grid-template-columns` inline

     Existing blocks: no `--cg-template-custom` set → fall through
     to `--cg-equal-template` → same rendering as before this
     refactor. Zero behaviour change for blocks that don't opt in. */
  --cg-equal-template: repeat(var(--cg-cols, 3), minmax(0, 1fr));
  grid-template-columns: var(--cg-template-custom, var(--cg-equal-template));
  /* Single gap shorthand — applies to both column-gap and row-gap.
     If items wrap to multiple rows the same gap sits between rows.
     JC 2026-06-25: no separate vertical control needed at grid
     level; the inside-item gap is the meaningful one. */
  gap: var(--cg-gap, 24px);
  align-items: start;
  width: 100%;
}

.so-content-grid-item {
  min-width: 0;
  /* Flex-column with item-level gap — controlled by the per-item
     so-content-grid-item--gap-* class via --cg-item-gap. Items
     become true containers for their children (heading + paragraph
     + ...) with controllable internal spacing. The flex layout also
     enables paintColumnGaps to paint diagonal stripes between
     children when an item is selected (chrome auto-works). */
  display: flex;
  flex-direction: column;
  gap: var(--cg-item-gap, 0);
}

/* Single-source-of-spacing rule (JC 2026-06-25): zero browser-
   default margins on direct children that ship with default
   margins. The item's gap is the SOLE source of inter-element
   spacing. Without this, h2/p browser defaults (~16-20px each)
   STACK on top of merchant-set gap → "Vertical Spacing: none"
   doesn't actually zero. Block authors can override per-child
   with inline margins if specifically needed (inline beats this
   stylesheet rule). Scoped to specific elements — see PB-002
   spacing doctrine for the full rule. */
.so-content-grid-item > h1, .so-content-grid-item > h2, .so-content-grid-item > h3,
.so-content-grid-item > h4, .so-content-grid-item > h5, .so-content-grid-item > h6,
.so-content-grid-item > p, .so-content-grid-item > ul, .so-content-grid-item > ol,
.so-content-grid-item > blockquote, .so-content-grid-item > pre,
.so-content-grid-item > figure, .so-content-grid-item > hr,
.so-content-grid-item > [data-gjs-type] {
  margin-top: 0;
  margin-bottom: 0;
}

/* Same rule for so-col — column's Horizontal Spacing (gap) is the
   SOLE source of inter-element spacing between its children. Scoped
   to specifically margin-bearing children so we don't override
   Bootstrap utility classes (mb-X / mt-X) on elements that don't
   ship with browser defaults. Block authors can override per-child
   with inline margins. See PB-002 spacing doctrine. */
.so-col > h1, .so-col > h2, .so-col > h3, .so-col > h4, .so-col > h5, .so-col > h6,
.so-col > p, .so-col > ul, .so-col > ol, .so-col > blockquote,
.so-col > pre, .so-col > figure, .so-col > hr,
.so-col > [data-gjs-type] {
  margin-top: 0;
  margin-bottom: 0;
}

/* Column count — desktop. Tablet + mobile responsive behaviour is
   INTRINSIC to the grid (handled by the @media rules on the base
   class below) — no explicit per-breakpoint classes needed. Same
   model as Bootstrap col-md-* (one declaration drives all sizes
   via cascading responsive defaults). */
.so-content-grid--cols-1 { --cg-cols: 1; }
.so-content-grid--cols-2 { --cg-cols: 2; }
.so-content-grid--cols-3 { --cg-cols: 3; }
.so-content-grid--cols-4 { --cg-cols: 4; }
.so-content-grid--cols-5 { --cg-cols: 5; }
.so-content-grid--cols-6 { --cg-cols: 6; }

/* Tablet (≤991px) — cap EQUAL mode at 2 cols. Custom-track templates
   persist (a `120px 1fr 1fr` layout still works at 768px). We only
   override `--cg-equal-template`, not `grid-template-columns`, so
   custom-track grids retain their layout on tablet. */
@media (max-width: 991px) {
  .so-content-grid {
    --cg-equal-template: repeat(min(var(--cg-cols, 3), 2), minmax(0, 1fr));
  }
}

/* Mobile (≤575px) — always stack to 1 col, INCLUDING custom-track
   grids. Writes `grid-template-columns` directly — wins over the
   desktop rule that resolves `var(--cg-template-custom, ...)`. The
   inline `--cg-template-custom` (if set) can't beat this because
   it's a different property in the cascade. */
@media (max-width: 575px) {
  .so-content-grid {
    grid-template-columns: 1fr;
  }
}

/* Gap tokens — match the spacing scale */
/* Horizontal Spacing (grid gap — both column-gap and row-gap via
   CSS `gap` shorthand). xs/sm/md/lg/xl/xxl match --space-* CSS vars
   so gap values are consistent with the rest of the editor's space
   system. */
.so-content-grid--gap-none { --cg-gap: 0; }
.so-content-grid--gap-xs   { --cg-gap: var(--space-xs,   4px); }
.so-content-grid--gap-sm   { --cg-gap: var(--space-sm,   8px); }
.so-content-grid--gap-md   { --cg-gap: var(--space-md,  16px); }
.so-content-grid--gap-lg   { --cg-gap: var(--space-lg,  24px); }
.so-content-grid--gap-xl   { --cg-gap: var(--space-xl,  32px); }
.so-content-grid--gap-xxl  { --cg-gap: var(--space-xxl, 48px); }

/* Vertical Spacing INSIDE each grid item — flex gap between an
   item's direct children. Per-item (each item has its own class)
   so merchants can tune spacing independently per item if needed.
   The chrome (paintColumnGaps in so-spacing-chrome.js) detects
   flex containers and paints stripes between children.

   JC 2026-06-25: "it seems to effect the same change on ALL the
   grid items instead of just the one im on" — moved to per-item.

   Legacy grid-level .so-content-grid--item-gap-* kept as a fallback
   so old saved pages still render with their original item spacing. */
.so-content-grid-item--gap-none { --cg-item-gap: 0; }
.so-content-grid-item--gap-xs   { --cg-item-gap: var(--space-xs,   4px); }
.so-content-grid-item--gap-sm   { --cg-item-gap: var(--space-sm,   8px); }
.so-content-grid-item--gap-md   { --cg-item-gap: var(--space-md,  16px); }
.so-content-grid-item--gap-lg   { --cg-item-gap: var(--space-lg,  24px); }
.so-content-grid-item--gap-xl   { --cg-item-gap: var(--space-xl,  32px); }
.so-content-grid-item--gap-xxl  { --cg-item-gap: var(--space-xxl, 48px); }

/* Legacy grid-level classes (kept for old saved pages — sets
   --cg-item-gap at grid level which cascades to items lacking
   their own class). */
.so-content-grid--item-gap-none { --cg-item-gap: 0; }
.so-content-grid--item-gap-xs   { --cg-item-gap: var(--space-xs,   4px); }
.so-content-grid--item-gap-sm   { --cg-item-gap: var(--space-sm,   8px); }
.so-content-grid--item-gap-md   { --cg-item-gap: var(--space-md,  16px); }
.so-content-grid--item-gap-lg   { --cg-item-gap: var(--space-lg,  24px); }
.so-content-grid--item-gap-xl   { --cg-item-gap: var(--space-xl,  32px); }
.so-content-grid--item-gap-xxl  { --cg-item-gap: var(--space-xxl, 48px); }

/* Text alignment — applies inside each item, not the grid */
.so-content-grid--align-left   .so-content-grid-item { text-align: left; }
.so-content-grid--align-center .so-content-grid-item { text-align: center; }

/* Editor-mode item perimeter — subtle dashed outline on hover so the
   item boundary is visible without overpowering the content. NO row
   lines drawn (per directive) — only per-item perimeter, exactly as
   the directive specifies. */
/* Persistent perimeter on each item in editor mode — makes items
   obviously distinct entities (each can be individually selected /
   styled / dragged-to-reorder). Subtle by default; strengthens on
   hover/select. Removed entirely in published mode. JC feedback
   2026-06-25: items felt invisible without a badge — persistent
   outline answers "what's an item?" visually. */
body.editor-mode .so-content-grid-item {
  outline: 1px dashed rgba(120, 120, 140, 0.28);
  outline-offset: -1px;
  transition: outline-color 0.12s ease;
}
body.editor-mode .so-content-grid-item:hover {
  outline-color: rgba(13, 110, 253, 0.55);
}
body.editor-mode .so-content-grid-item.gjs-selected {
  outline-color: rgba(13, 110, 253, 0.9);
}


/* ═══════════════════════════════════════════════════════════════════
   so-subscribe — single-email signup widget (V1, 2026-06-29)

   Black Box atomic component (data-gjs-type="so-subscribe"). Inner
   HTML rendered programmatically; CSS classes here drive 5 layout
   presets + base styling. CSS variables on the wrapper carry merchant-
   configured colours via per-element CSS rule written by setStyle.

   Layout presets (applied via .so-subscribe-preset-{name} on the form):
     - appended-pill      Single pill wraps field + button (no gap)
     - appended-rounded   Single rounded card wraps field + button
     - detached-card      White card wraps both with visible gap
     - stacked-pill       Pill field above, pill button below (full-width)
     - stacked-square     Square corners version of stacked

   Doctrine refs: PB-002 §4.4 (Token Bake) — colours via CSS vars.
   ═══════════════════════════════════════════════════════════════════ */

.so-subscribe {
  /* 12px wrapper padding gives visible breathing room between the
     editor's selection chrome (which wraps .so-subscribe) and the
     visible pill (.so-subscribe-row inside). Also matches how other
     atomic components show spacing diagonals in GrapesJS. */
  padding: 12px;

  /* Defaults — overridden by merchant via Style tab → CSS vars set
     by setStyle() on the wrapper. Var taxonomy mirrors the modal's
     Style tab subsections (Background / Text / Buttons-Icons). */
  --so-sub-wrapper-bg:           transparent;
  --so-sub-wrapper-border-color: #dee2e6;
  --so-sub-wrapper-border-width: 1px;

  --so-sub-field-text:           #1a202c;
  --so-sub-field-font:           inherit;
  --so-sub-field-size:           15px;
  --so-sub-field-placeholder:    #6b7280;
  --so-sub-placeholder-font:     inherit;
  --so-sub-placeholder-size:     15px;

  --so-sub-button-bg:            #0d6efd;
  --so-sub-button-hover-bg:      #0b5ed7;
  --so-sub-button-text:          #ffffff;
  --so-sub-button-font:          inherit;
  --so-sub-button-size:          14px;
  --so-sub-button-hover-text:    #ffffff;
  --so-sub-button-hover-font:    inherit;
  --so-sub-button-hover-size:    14px;
  --so-sub-button-border:        transparent;
  --so-sub-button-hover-border:  transparent;

  display: block;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
}

.so-subscribe-form {
  display: block;
  width: 100%;
}

.so-subscribe-row {
  display: flex;
  align-items: stretch;
  width: 100%;
  /* Generous baseline height — premium feel per reference designs.
     Mobile drops to 50px via @media below. */
  min-height: 60px;
}

.so-subscribe-field-wrap {
  position: relative;
  flex: 1 1 auto;
  align-self: center;        /* defensive — force vertical centering in row */
  display: flex;
  align-items: center;
  min-width: 0;
}

.so-subscribe-leading-icon {
  position: absolute;
  left: 18px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  pointer-events: none;
  color: var(--so-sub-field-placeholder);
}

.so-subscribe-field {
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  /* Explicit 60px height ensures consistent sizing across all presets
     regardless of row padding / box-sizing inheritance (Bootstrap sets
     box-sizing:border-box globally, which means row min-height + padding
     can squeeze content unexpectedly — see detached-card regression
     30 Jun 2026). 25px horizontal padding from reference CSS. */
  height: 60px;
  padding: 0 25px;
  font-family: var(--so-sub-field-font);
  font-size: var(--so-sub-field-size);
  line-height: 1.4;
  color: var(--so-sub-field-text);
  background: transparent;
  border: none;
  outline: none;
  box-sizing: border-box;
}

/* Focus state — border colour shifts to button bg (treated as the
   "primary" colour of the widget). Subtle, accessible, matches the
   reference newsletter / call-to-action patterns. */
.so-subscribe-field:focus { outline: none; }
.so-subscribe-form:focus-within .so-subscribe-row,
.so-subscribe-form:focus-within .so-subscribe-field-wrap {
  /* Preset-specific selectors override below; this is the baseline */
  border-color: var(--so-sub-button-bg);
}

/* When leading icon is present, indent the placeholder/text */
.so-subscribe-field-wrap:has(.so-subscribe-leading-icon) .so-subscribe-field {
  padding-left: 48px;
}

.so-subscribe-field::placeholder {
  color: var(--so-sub-field-placeholder);
  font-family: var(--so-sub-placeholder-font);
  font-size: var(--so-sub-placeholder-size);
  opacity: 1; /* Firefox default is 0.54 — normalise */
}

.so-subscribe-honeypot {
  /* Hidden anti-spam field — bots fill it; humans never see it.
     Server rejects requests where this field is non-empty. */
  position: absolute !important;
  left: -9999px !important;
  width: 1px !important;
  height: 1px !important;
  opacity: 0 !important;
  pointer-events: none !important;
}

.so-subscribe-button {
  flex: 0 0 auto;
  align-self: center;        /* defensive — force vertical centering in row regardless of row's align-items */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Explicit 60px height matches field — guarantees consistent sizing
     across all presets (same reasoning as field height comment above).
     Appended-pill / -rounded override this to 50px (5px inset inside
     the field pill). */
  height: 60px;
  padding: 0 28px;
  /* All button typography wired to CSS vars so the Style tab Buttons
     subsection can drive font + size in addition to colour. */
  font-family: var(--so-sub-button-font);
  font-size: var(--so-sub-button-size);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--so-sub-button-text);
  background: var(--so-sub-button-bg);
  border: 1px solid var(--so-sub-button-border);
  cursor: pointer;
  white-space: nowrap;
  box-sizing: border-box;
  /* Subtle elevation — symmetric (no Y offset) so the shadow doesn't
     extend asymmetrically into the bottom inset of the appended-pill
     wrapper, which made top/bottom padding perceptually unequal even
     when geometrically equidistant. Diagnosed via canvas inspector
     30 Jun 2026 (commit a89b71 follow-up). */
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.08), 0 0 8px rgba(0, 0, 0, 0.05);
  transition: background-color 0.15s ease-out, color 0.15s ease-out,
              border-color 0.15s ease-out, box-shadow 0.2s ease-out;
}
.so-subscribe-button:hover {
  background: var(--so-sub-button-hover-bg);
  color: var(--so-sub-button-hover-text);
  font-family: var(--so-sub-button-hover-font);
  font-size: var(--so-sub-button-hover-size);
  border-color: var(--so-sub-button-hover-border);
  /* Deeper shadow on hover — still symmetric */
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.12), 0 0 16px rgba(0, 0, 0, 0.08);
  outline: 3px solid color-mix(in srgb, var(--so-sub-button-hover-bg, var(--so-sub-button-bg, #0b5ed7)) 35%, transparent);   /* ring + lift only — no brightness filter (ruling E) */
  outline-offset: 0;
  transform: translateY(-1px);
}
.so-subscribe-button:disabled {
  opacity: 0.7;
  cursor: not-allowed;
  box-shadow: none;
}

.so-subscribe-spinner {
  display: none;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: so-subscribe-spin 0.6s linear infinite;
}
.so-subscribe-form.is-submitting .so-subscribe-spinner { display: inline-block; }
.so-subscribe-form.is-submitting .so-subscribe-button-label { opacity: 0.6; }
@keyframes so-subscribe-spin {
  to { transform: rotate(360deg); }
}

.so-subscribe-msg {
  margin-top: 12px;
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
  min-height: 1em;
}
/* Collapse the msg div to zero height when it's empty (default state
   before any submit). Without this collapse, the empty msg div takes
   ~34px (12px margin-top + ~22px min-height) of space BELOW the pill —
   making the so-subscribe wrapper visibly taller than the pill itself.
   In the editor's selection chrome, that creates the appearance of
   "pill touches top, bottom has space" because the chrome wraps the
   wrapper (not the pill). Diagnosed via 100px padding test 30 Jun 2026
   — confirmed pill is internally symmetric; asymmetry was the empty
   msg div below it. */
.so-subscribe-msg:empty {
  margin: 0;
  min-height: 0;
}
.so-subscribe-msg.is-error { color: #dc2626; }
.so-subscribe-msg.is-success {
  color: #16a34a;
  font-weight: 500;
  padding: 14px 18px;
  background: rgba(22, 163, 74, 0.08);
  border-radius: 8px;
  margin-top: 0;
}

/* Hide the form after successful submission — widget content swaps
   to a permanent success-state per ChatGPT UX recommendation. */
.so-subscribe-form.is-success .so-subscribe-row { display: none; }

/* ── Preset: Appended Pill ──────────────────────────────────────── */
/* Reverted to flex-based layout 30 Jun 2026 after the position:absolute
   approach (which mirrored the reference call-action-two CSS structure
   exactly) produced perceptually-asymmetric top/bottom insets that we
   couldn't fully resolve. Flex-based is more robust + equidistant
   spacing on all four sides falls out naturally from padding + gap.

   Structure: row = pill wrapper (bg, border, border-radius). Padding
   creates the visible inset around field + button. Gap creates the
   space between them. Both field and button stretched to row's
   content area height by align-items:stretch (inherited from global). */
.so-subscribe-preset-appended-pill .so-subscribe-row {
  background: var(--so-sub-wrapper-bg);
  border: var(--so-sub-wrapper-border-width) solid var(--so-sub-wrapper-border-color);
  border-radius: 999px;
  padding: 5px;
  gap: 5px;
  min-height: 0;
  overflow: hidden;
}
.so-subscribe-preset-appended-pill .so-subscribe-field {
  background: transparent;  /* visual bg comes from row */
  border: none;
  border-radius: 999px;
  height: 50px;             /* row total = 50 + 10 padding = 60px */
}
.so-subscribe-preset-appended-pill .so-subscribe-form:focus-within .so-subscribe-row {
  border-color: var(--so-sub-button-bg);
}
.so-subscribe-preset-appended-pill .so-subscribe-button {
  border-radius: 999px;
  height: 50px;
}

/* ── Preset: Appended Rounded ───────────────────────────────────── */
/* Same flex-based structure as appended-pill but with 10px corners. */
.so-subscribe-preset-appended-rounded .so-subscribe-row {
  background: var(--so-sub-wrapper-bg);
  border: var(--so-sub-wrapper-border-width) solid var(--so-sub-wrapper-border-color);
  border-radius: 10px;
  padding: 5px;
  gap: 5px;
  min-height: 0;
  overflow: hidden;
}
.so-subscribe-preset-appended-rounded .so-subscribe-field {
  background: transparent;
  border: none;
  border-radius: 8px;
  height: 50px;
}
.so-subscribe-preset-appended-rounded .so-subscribe-form:focus-within .so-subscribe-row {
  border-color: var(--so-sub-button-bg);
}
.so-subscribe-preset-appended-rounded .so-subscribe-button {
  border-radius: 8px;
  height: 50px;
}

/* Mobile sizing for appended presets — global @media (max-width:767px)
   sets .so-subscribe-field/button to 50px (would overflow the 50px row
   content area). Override here to 40px so they fit inside the row's
   reduced content area (50px total - 10px padding = 40px content). */
@media (max-width: 767px) {
  .so-subscribe-preset-appended-pill .so-subscribe-field,
  .so-subscribe-preset-appended-rounded .so-subscribe-field,
  .so-subscribe-preset-appended-pill .so-subscribe-button,
  .so-subscribe-preset-appended-rounded .so-subscribe-button {
    height: 40px;
  }
}

/* ── Preset: Detached Card ──────────────────────────────────────── */
/* Mirrors the "newsletter-area" reference — white card wraps a flex
   form with input + button side-by-side. Card has subtle elevation;
   field-wrap has its own background (Field background in the Style
   tab) so merchants can paint the inner box independently from the
   card itself (e.g. dark card + lighter input box, as JC showed). */
.so-subscribe-preset-detached-card .so-subscribe-row {
  background: var(--so-sub-wrapper-bg);
  border: var(--so-sub-wrapper-border-width) solid var(--so-sub-wrapper-border-color);
  border-radius: 10px;
  padding: 24px;
  gap: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.so-subscribe-preset-detached-card .so-subscribe-field-wrap {
  background: var(--so-sub-field-bg);
  border: 1px solid var(--so-sub-wrapper-border-color);
  border-radius: 5px;
}
.so-subscribe-preset-detached-card .so-subscribe-field {
  background: transparent; /* inherits visual bg from field-wrap */
  border: none;
  padding: 0 30px;
}
.so-subscribe-preset-detached-card .so-subscribe-button {
  border-radius: 5px;
}

/* ── Preset: Stacked Pill ───────────────────────────────────────── */
.so-subscribe-preset-stacked-pill .so-subscribe-row {
  flex-direction: column;
  gap: 12px;
}
.so-subscribe-preset-stacked-pill .so-subscribe-field-wrap {
  width: 100%;
  background: var(--so-sub-wrapper-bg);
  border: var(--so-sub-wrapper-border-width) solid var(--so-sub-wrapper-border-color);
  border-radius: 999px;
}
.so-subscribe-preset-stacked-pill .so-subscribe-button {
  width: 100%;
  border-radius: 999px;
}

/* ── Preset: Stacked Square ─────────────────────────────────────── */
.so-subscribe-preset-stacked-square .so-subscribe-row {
  flex-direction: column;
  gap: 10px;
}
.so-subscribe-preset-stacked-square .so-subscribe-field-wrap {
  width: 100%;
  background: var(--so-sub-wrapper-bg);
  border: var(--so-sub-wrapper-border-width) solid var(--so-sub-wrapper-border-color);
  border-radius: 6px;
}
.so-subscribe-preset-stacked-square .so-subscribe-button {
  width: 100%;
  border-radius: 6px;
}

/* Responsive: stacked layouts already full-width; appended layouts
   wrap below at narrow widths (button drops below input). Row + field
   + button height ease to 50px on mobile (reference design pattern).
   Appended-pill / -rounded override button height to 50px regardless. */
@media (max-width: 767px) {
  .so-subscribe-row    { min-height: 50px; }
  .so-subscribe-field  { height: 50px; }
  .so-subscribe-button { height: 50px; }
}

@media (max-width: 480px) {
  .so-subscribe-preset-appended-pill .so-subscribe-row,
  .so-subscribe-preset-appended-rounded .so-subscribe-row,
  .so-subscribe-preset-detached-card .so-subscribe-row {
    flex-wrap: wrap;
    gap: 8px;
  }
  .so-subscribe-preset-appended-pill .so-subscribe-field-wrap,
  .so-subscribe-preset-appended-rounded .so-subscribe-field-wrap,
  .so-subscribe-preset-detached-card .so-subscribe-field-wrap {
    flex: 1 1 100%;
  }
  .so-subscribe-preset-appended-pill .so-subscribe-button,
  .so-subscribe-preset-appended-rounded .so-subscribe-button,
  .so-subscribe-preset-detached-card .so-subscribe-button {
    flex: 1 1 100%;
  }
}

/* =====================================================================
   so-accordion — Black Box collapsible-item list (Phase 1, 2 Jul 2026)
   =====================================================================
   Modal-driven Black Box primitive. Phase 1 ships:
     - Base structure (list / item / trigger / panel)
     - Editor-canvas override: ALL panels visible for authoring
     - Bordered style preset (default)
     - Chevron icon with rotate-on-expanded state
   Phases 3-5 layer in the other 4 style presets, 5 more icons, 2-column
   layout, group title, etc. See memory/project-so-accordion-jul02.md
   for the full build plan.

   Design intent (Bordered preset — the default):
     - Each item is a stand-alone card with a thin border
     - Header: bold title on the left, chevron on the right
     - Panel: light background, comfortable padding
     - Active item: primary-colour border + text on the trigger
   ===================================================================== */
.so-accordion {
  /* Content widget — takes its natural height. NOT applying the
     min-height:100% Column-Wrapper Contract because accordion is
     content (like text/image), not a stretch-to-fill widget (like
     gallery/slider). Applying min-height:100% caused overflow into
     the section below (JC repro 2 Jul 2026). */
  display: block;
  width: 100%;
  box-sizing: border-box;   /* selection outline aligns to full box */
  color: inherit;
  position: relative;       /* anchors positioned descendants */
}


/* Optional group title — coloured header above the item list. Only
   renders when config.groupTitle is a non-empty string. Style-preset
   specific colours land in Phase 3; Phase 1 ships a neutral fallback. */
.so-accordion-group-title {
  font-weight: 700;
  font-size: 18px;
  line-height: 1.4;
  padding: 12px 16px;
  background: var(--so-accordion-group-title-bg, transparent);
  color: var(--so-accordion-group-title-color, inherit);
  border-radius: 6px 6px 0 0;
  margin-bottom: 12px;
}

/* List container. Cols-2 lays out items in a CSS grid (Phase 5 setting). */
.so-accordion-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}
.so-accordion-list.so-accordion-cols-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
@media (max-width: 767px) {
  .so-accordion-list.so-accordion-cols-2 {
    grid-template-columns: 1fr;   /* stack on mobile */
  }
}

/* Item — the collapsible unit */
.so-accordion-item {
  position: relative;
  border-radius: 6px;
  overflow: hidden;
  transition: box-shadow 0.3s ease-out, border-color 0.3s ease-out;
}

/* Trigger button — full-width click target for the item header.
   Flex row so the title + icon share the row. Icon-position class
   flips direction so icon-left doesn't need a different DOM tree. */
.so-accordion-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 14px 18px;
  background: transparent;
  color: inherit;
  border: 0;
  font: inherit;
  /* Style-tab font override — '' (var unset) falls back to inherit,
     i.e. the theme font. Per-type rules only set SIZE, never family,
     so this single declaration covers all 6 types. */
  font-family: var(--so-acc-question-font, inherit);
  font-weight: 700;
  font-size: var(--so-acc-question-size, 16px);
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
  transition: color 0.3s ease-out, background 0.3s ease-out;
}
.so-accordion-item.so-accordion-icon-left .so-accordion-trigger {
  flex-direction: row-reverse;    /* icon on left, title fills remaining */
  justify-content: flex-end;
}
.so-accordion-trigger:focus { outline: none; }
.so-accordion-trigger:focus-visible {
  outline: 2px solid var(--theme-primary, #2563eb);
  outline-offset: -2px;
}

.so-accordion-title {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Icon container — the SVG inside rotates via CSS on aria-expanded */
.so-accordion-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: transform 0.3s ease-out;
  /* Sized via the so-accordion-icon-size-* class on the wrapper. Default
     falls back to medium if no size class is present (legacy accordions). */
  width: var(--so-accordion-icon-size, 16px);
  height: var(--so-accordion-icon-size, 16px);
}
.so-accordion-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Icon-size presets — set --so-accordion-icon-size on the wrapper so
   both the .so-accordion-icon box AND any child SVG scale together.
   Sizes chosen to feel proportional against the trigger padding on
   each style preset. */
.so-accordion-icon-size-small  { --so-accordion-icon-size: 12px; }
.so-accordion-icon-size-medium { --so-accordion-icon-size: 16px; }
.so-accordion-icon-size-large  { --so-accordion-icon-size: 22px; }

/* Chevron rotates 180° when expanded */
.so-accordion-trigger[aria-expanded="true"] .so-accordion-icon {
  transform: rotate(180deg);
}

/* Panel — the collapsible content region. Runtime JS (Phase 6) will
   toggle max-height for the expand/collapse animation. Editor canvas
   ALWAYS shows panels expanded (via the .editor-mode override below)
   so merchants can see + edit each item's content. */
.so-accordion-panel {
  overflow: hidden;
  transition: max-height 0.35s ease-out, opacity 0.25s ease-out;
}
.so-accordion-item[data-open="0"] .so-accordion-panel {
  max-height: 0;
  opacity: 0;
}
.so-accordion-item[data-open="1"] .so-accordion-panel {
  max-height: 1000px;   /* generous ceiling — bodies rarely exceed */
  opacity: 1;
}
.so-accordion-content {
  padding: 16px 18px;
  color: var(--so-accordion-body-color, #4b5563);
  font-family: var(--so-acc-panel-font, inherit);
  font-size: var(--so-acc-panel-size, 15px);
  line-height: 1.7;
}
.so-accordion-content > *:first-child { margin-top: 0; }
.so-accordion-content > *:last-child  { margin-bottom: 0; }

/* ═════════════════════════════════════════════════════════════════════
   6 accordion types (5 Jul 2026 rebuild — supersedes the 5-preset
   colour-variant system).
   ─────────────────────────────────────────────────────────────────────
   These are 6 genuinely different accordion DESIGNS, not colour swaps.
   Same DOM, dramatically different treatments — the single-engine
   doctrine holds. Colour customisation lives in the Style tab (Phase 3b
   override layer below).

   Editor-mode override was REMOVED 2 Jul 2026 (Black Box pivot). The
   canvas click-to-expand handler in view.onRender mirrors runtime
   behaviour — merchants click items to expand just like preview.
   ═════════════════════════════════════════════════════════════════════ */

/* ── Type 1 — Card ────────────────────────────────────────────────────
   Individual outlined cards with subtle grey border. On active, the
   border swaps to primary-color at full opacity and a soft primary-tint
   inset shadow appears. Classic "Ayrou-6 / knowledge-base" look.
   Style-tab vars consumed as inline fallbacks. */
.so-accordion-type-card .so-accordion-list { gap: 8px; }
.so-accordion-type-card .so-accordion-item {
  background:    var(--so-acc-question-bg, #ffffff);
  border: 1.5px solid var(--so-acc-border-color, #e2e8f0);
  border-radius: var(--so-acc-border-radius, 8px);
  transition: border-color 0.25s ease-out, box-shadow 0.25s ease-out;
}
.so-accordion-type-card .so-accordion-trigger {
  color: var(--so-acc-question-text, #0f172a);
  padding: 15px 20px;
  font-size: var(--so-acc-question-size, 15px);
  font-weight: 600;
}
.so-accordion-type-card .so-accordion-content {
  background: var(--so-acc-panel-bg, transparent);
  color:      var(--so-acc-panel-text, #475569);
  padding: 4px 20px 18px 20px;
}
.so-accordion-type-card .so-accordion-item[data-open="1"] {
  border-color: var(--theme-primary, #2563eb);
  box-shadow: inset 0 0 0 1px var(--theme-primary, #2563eb),
              0 2px 8px rgba(37, 99, 235, 0.08);
}
/* Open-state question text — Style tab "Text (Panel Open)" row. Falls
   back through the closed override, then the type default. Same
   3-property shape on every type. */
.so-accordion-type-card .so-accordion-item[data-open="1"] .so-accordion-trigger {
  color: var(--so-acc-question-text-open, var(--theme-primary, #2563eb));
  font-family: var(--so-acc-question-font-open, var(--so-acc-question-font, inherit));
  font-size: var(--so-acc-question-size-open, var(--so-acc-question-size, 15px));
}

/* ── Type 2 — Divider ─────────────────────────────────────────────────
   Editorial minimal: no card surface, only a thin bottom rule between
   items. Larger typography with light weight for an airy magazine feel.
   Active state paints the bottom rule in accent + shifts trigger colour. */
.so-accordion-type-divider .so-accordion-list { gap: 0; }
.so-accordion-type-divider .so-accordion-item {
  background: var(--so-acc-question-bg, transparent);
  border: 0;
  border-bottom: 1px solid var(--so-acc-border-color, #e5e7eb);
  border-radius: var(--so-acc-border-radius, 0);
  overflow: visible;
  transition: border-bottom-color 0.25s ease-out;
}
.so-accordion-type-divider .so-accordion-item:last-child { border-bottom: 0; }
.so-accordion-type-divider .so-accordion-trigger {
  color: var(--so-acc-question-text, #4b5563);
  padding: 22px 0;
  font-weight: 400;
  font-size: var(--so-acc-question-size, 18px);
  letter-spacing: 0.01em;
}
.so-accordion-type-divider .so-accordion-content {
  background: var(--so-acc-panel-bg, transparent);
  color:      var(--so-acc-panel-text, #6b7280);
  padding: 0 0 22px 0;
  font-size: var(--so-acc-panel-size, 15px);
  line-height: 1.65;
}
.so-accordion-type-divider .so-accordion-item[data-open="1"] {
  border-bottom-width: 2px;
  border-bottom-color: var(--theme-primary, #06b6d4);
}
.so-accordion-type-divider .so-accordion-item[data-open="1"] .so-accordion-trigger {
  color: var(--so-acc-question-text-open, var(--theme-primary, #06b6d4));
  font-family: var(--so-acc-question-font-open, var(--so-acc-question-font, inherit));
  font-size: var(--so-acc-question-size-open, var(--so-acc-question-size, 18px));
}

/* ── Type 3 — Bold ────────────────────────────────────────────────────
   Solid coloured header block. High-impact FAQ style — the trigger IS
   the coloured bar. Flip-in animation on panel expand for extra pop.
   Reads as "hero-scale FAQ" rather than utility list. */
.so-accordion-type-bold .so-accordion-list { gap: 8px; }
.so-accordion-type-bold .so-accordion-item {
  background: transparent;
  border: 0;
  border-radius: var(--so-acc-border-radius, 6px);
  overflow: hidden;
}
.so-accordion-type-bold .so-accordion-trigger {
  background: var(--so-acc-question-bg, var(--theme-primary, #38cc70));
  color:      var(--so-acc-question-text, #ffffff);
  padding: 24px 28px;         /* hero-scale — matches ref Accordion 3 */
  font-size: var(--so-acc-question-size, 16px);
  font-weight: 700;
  letter-spacing: 0.02em;
  transition: background 0.3s ease-out;
}
.so-accordion-type-bold .so-accordion-trigger[aria-expanded="true"] {
  background: color-mix(in srgb,
              var(--so-acc-question-bg, var(--theme-primary, #38cc70)) 85%, #000);
  color: var(--so-acc-question-text-open, var(--so-acc-question-text, #ffffff));
  font-family: var(--so-acc-question-font-open, var(--so-acc-question-font, inherit));
  font-size: var(--so-acc-question-size-open, var(--so-acc-question-size, 16px));
}
.so-accordion-type-bold .so-accordion-content {
  background: var(--so-acc-panel-bg, rgba(0, 0, 0, 0.03));
  color:      var(--so-acc-panel-text, #374151);
  padding: 20px 24px;
  transform-origin: 50% 0;
  animation: so-accordion-bold-flip-in 0.4s ease-out both;
}
.so-accordion-type-bold .so-accordion-icon { color: var(--so-acc-question-text, #ffffff); }
@keyframes so-accordion-bold-flip-in {
  0%   { opacity: 0; transform: scaleY(0.85) rotateX(-45deg); }
  100% { opacity: 1; transform: scaleY(1)    rotateX(0);      }
}

/* ── Type 4 — Ribbon ──────────────────────────────────────────────────
   Material-shadow stacked ribbon. Items are visually joined into one
   continuous bar (no gap) with a strong shared box-shadow around the
   whole list. Coloured teal-style header with white text. */
.so-accordion-type-ribbon .so-accordion-list {
  gap: 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12),
              0 4px 12px rgba(0, 0, 0, 0.08);
  border-radius: var(--so-acc-border-radius, 6px);
  overflow: hidden;
}
.so-accordion-type-ribbon .so-accordion-item {
  background: var(--so-acc-panel-bg, #ffffff);
  border: 0;
  border-radius: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.15);
}
.so-accordion-type-ribbon .so-accordion-item:first-child { border-top: 0; }
.so-accordion-type-ribbon .so-accordion-trigger {
  background: var(--so-acc-question-bg, var(--theme-primary, #009688));
  color:      var(--so-acc-question-text, #ffffff);
  padding: 15px 20px;
  font-size: var(--so-acc-question-size, 15px);
  font-weight: 400;
  transition: background 0.25s ease-out;
}
.so-accordion-type-ribbon .so-accordion-trigger[aria-expanded="true"] {
  background: color-mix(in srgb,
              var(--so-acc-question-bg, var(--theme-primary, #009688)) 88%, #000);
  color: var(--so-acc-question-text-open, var(--so-acc-question-text, #ffffff));
  font-family: var(--so-acc-question-font-open, var(--so-acc-question-font, inherit));
  font-size: var(--so-acc-question-size-open, var(--so-acc-question-size, 15px));
}
.so-accordion-type-ribbon .so-accordion-content {
  background: var(--so-acc-panel-bg, #ffffff);
  color:      var(--so-acc-panel-text, #374151);
  padding: 18px 20px;
}
.so-accordion-type-ribbon .so-accordion-icon { color: var(--so-acc-question-text, #ffffff); }

/* ── Type 5 — Lift ────────────────────────────────────────────────────
   Cards visibly lift on active — shadow grows dramatically and item
   pushes neighbours away via margin. Feels tactile / clickable, like a
   Material FAB expanding. Best paired with single-mode (only one open). */
.so-accordion-type-lift .so-accordion-list { gap: 4px; }
.so-accordion-type-lift .so-accordion-item {
  background: var(--so-acc-question-bg, #ffffff);
  border: 0;
  border-radius: var(--so-acc-border-radius, 6px);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08),
              0 0 1px rgba(0, 0, 0, 0.10);
  transition:
    margin       0.25s ease-out,
    box-shadow   0.25s ease-out,
    transform    0.25s ease-out;
}
.so-accordion-type-lift .so-accordion-item:hover {
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.so-accordion-type-lift .so-accordion-trigger {
  color: var(--so-acc-question-text, #1f2937);
  padding: 18px 20px;
  font-size: var(--so-acc-question-size, 16px);
  font-weight: 500;
  transition: color 0.2s ease-out;
}
.so-accordion-type-lift .so-accordion-content {
  background: var(--so-acc-panel-bg, transparent);
  color:      var(--so-acc-panel-text, #4b5563);
  border-top: 1px solid var(--so-acc-border-color, #f3f4f6);
  padding: 16px 20px 20px 20px;
  margin-top: 4px;
}
.so-accordion-type-lift .so-accordion-item[data-open="1"] {
  margin: 12px 0;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15),
              0 2px 6px rgba(0, 0, 0, 0.08);
  transform: translateY(-1px);
}
.so-accordion-type-lift .so-accordion-item[data-open="1"] .so-accordion-trigger {
  color: var(--so-acc-question-text-open, var(--theme-primary, #0891b2));
  font-family: var(--so-acc-question-font-open, var(--so-acc-question-font, inherit));
  font-size: var(--so-acc-question-size-open, var(--so-acc-question-size, 16px));
}

/* ── Type 6 — Modern ──────────────────────────────────────────────────
   Sleek SaaS FAQ style — deep neutral background with subtle borders.
   Clean rounded corners, small tight typography, subdued icon colour.
   Reads as premium / product-marketing FAQ. */
.so-accordion-type-modern .so-accordion-list { gap: 6px; }
.so-accordion-type-modern .so-accordion-item {
  background: var(--so-acc-question-bg, #0a0a0a);        /* neutral-950 */
  border: 1px solid var(--so-acc-border-color, #262626); /* neutral-800 */
  border-radius: var(--so-acc-border-radius, 8px);
  transition: background 0.25s ease-out, border-color 0.25s ease-out;
}
.so-accordion-type-modern .so-accordion-item:hover {
  background: color-mix(in srgb, var(--so-acc-question-bg, #0a0a0a) 92%, #fff);
}
.so-accordion-type-modern .so-accordion-trigger {
  color: var(--so-acc-question-text, #f5f5f5);           /* neutral-100 */
  padding: 15px 18px;
  font-size: var(--so-acc-question-size, 14px);
  font-weight: 500;
}
.so-accordion-type-modern .so-accordion-content {
  background: var(--so-acc-panel-bg, transparent);
  color:      var(--so-acc-panel-text, #a3a3a3);         /* neutral-400 */
  padding: 4px 18px 16px 18px;
  font-size: var(--so-acc-panel-size, 14px);
  line-height: 1.6;
}
.so-accordion-type-modern .so-accordion-item[data-open="1"] {
  background: color-mix(in srgb, var(--so-acc-question-bg, #0a0a0a) 88%, #fff);
  border-color: #404040;                                 /* neutral-700 */
}
.so-accordion-type-modern .so-accordion-item[data-open="1"] .so-accordion-trigger {
  color: var(--so-acc-question-text-open, var(--so-acc-question-text, #f5f5f5));
  font-family: var(--so-acc-question-font-open, var(--so-acc-question-font, inherit));
  font-size: var(--so-acc-question-size-open, var(--so-acc-question-size, 14px));
}
.so-accordion-type-modern .so-accordion-icon {
  color: var(--so-acc-question-text, #f5f5f5);
}

/* ═════════════════════════════════════════════════════════════════════
   Style-tab per-instance overrides (Phase 3b — 5 Jul 2026 fix)
   ─────────────────────────────────────────────────────────────────────
   Overrides are consumed by embedding var(--so-acc-X, <type-default>)
   directly inside each type rule above. That pattern is the ONLY one
   that survives the cascade cleanly:
     - Var unset  → falls back to the type default → type paints normally
     - Var set    → merchant colour overrides the type default in place
   The previous "shared layer with `revert` fallback" approach was
   broken: `revert` reverts to the user-agent default (transparent),
   NOT to the earlier CSS rule, so the shared rules over-rode the type
   defaults with transparent whenever the merchant hadn't set an
   override — making the whole accordion disappear (JC's Ribbon repro
   5 Jul 2026 → led to this rewrite).

   Variables consumed:
     --so-acc-question-bg          Question/trigger bar background
     --so-acc-question-text        Question text colour — panel CLOSED
     --so-acc-question-font        Question font family — closed
     --so-acc-question-size        Question font size — closed
     --so-acc-question-text-open   Question text colour — panel OPEN
     --so-acc-question-font-open   Question font family — open
     --so-acc-question-size-open   Question font size — open
     --so-acc-panel-bg             Answer panel background
     --so-acc-panel-text           Answer panel text colour
     --so-acc-panel-font           Answer panel font family
     --so-acc-panel-size           Answer panel font size
     --so-acc-border-color         Item outline colour
     --so-acc-border-radius        Item corner radius

   Open-state vars fall back through the closed override, then the type
   default: var(--x-open, var(--x, <type default>)). The former
   --so-acc-active-color accent (borders / active bg) was removed 5 Jul
   2026 — open-state TEXT styling replaced it in the Style tab; the
   non-text active paints (borders, header-bg darken) are type-fixed.

   Where each var lands per type depends on the type's visual grammar
   (e.g. question-bg = item bg on Card, but trigger bg on Ribbon/Bold).
   See the type rules above for the actual bindings.
   ═════════════════════════════════════════════════════════════════════ */

/* ═════════════════════════════════════════════════════════════════════
   Phase 4 — icon presets (chevron / plus-minus / plus-x / arrow / circle-plus / none)
   Chevron: single SVG, rotates 180°. Plus-X: single "+" rotates 45°.
   Arrow: single "→", rotates 90° base + another 180° on expanded.
   Plus-Minus + Circle-Plus: dual SVGs (closed + open), CSS shows one at a
   time. See renderAccordionIcon() in accordion.js for the SVG output.
   ═════════════════════════════════════════════════════════════════════ */

/* Chevron — already handled above by the [aria-expanded="true"] rotate rule.
   Explicit class scope prevents cross-preset interference. */
.so-accordion-icon-preset-chevron .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon {
  transform: rotate(180deg);
}

/* Plus / X — the same "+" glyph rotates 45° to become × on expand */
.so-accordion-icon-preset-plus-x .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon {
  transform: rotate(45deg);
}

/* Arrow — "→" is emitted right-facing; CSS rotates 90° to point down
   (closed), and to 270° (or -90°) to point up (open). */
.so-accordion-icon-preset-arrow .so-accordion-icon {
  transform: rotate(90deg);
}
.so-accordion-icon-preset-arrow .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon {
  transform: rotate(270deg);
}

/* Plus-Minus + Circle-Plus — dual-SVG swap. Both SVGs render inline;
   CSS shows the appropriate one for the current expanded state. */
.so-accordion-icon-swap-open  { display: none; }
.so-accordion-icon-swap-closed { display: inline-flex; }
.so-accordion-icon-preset-plus-minus .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon-swap-closed,
.so-accordion-icon-preset-circle-plus .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon-swap-closed {
  display: none;
}
.so-accordion-icon-preset-plus-minus .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon-swap-open,
.so-accordion-icon-preset-circle-plus .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon-swap-open {
  display: inline-flex;
}
/* Disable the shared 180° chevron rotation on swap-based icons (they use display swap, not transform) */
.so-accordion-icon-preset-plus-minus .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon,
.so-accordion-icon-preset-circle-plus .so-accordion-trigger[aria-expanded="true"] .so-accordion-icon {
  transform: none;
}

/* ═════════════════════════════════════════════════════════════════════
   Editor-mode adjustment — pivot 2 Jul 2026
   Previously all panels always visible in editor. Now that canvas has
   click-to-expand preview handler (see view.onRender), we let editor
   respect data-open so merchants get a true preview. All editing
   happens in the modal — no need to see collapsed content in canvas.
   ═════════════════════════════════════════════════════════════════════ */
/* No editor-mode overrides — canvas mirrors runtime behaviour. */

/* =====================================================================
   Section video background (Phase B of video-bg rollout, 3 Jul 2026)
   =====================================================================
   Two locked technical children live INSIDE a so-section when its
   data-so-bg-mode attribute equals "video":
     - so-section__bg-video   : the <video> element (absolute, cover)
     - so-section__bg-overlay : the tint <div> above the video (absolute)
   Content children (rows, etc.) sit above both via z-index rule below.
   Scoped to [data-so-bg-mode="video"] so non-video sections are unaffected.

   z-index stack (video mode only):
     0  : bg-video
     1  : bg-overlay
     2  : content (rows, etc.)
   ===================================================================== */

.so-section[data-so-bg-mode="video"] {
  position: relative;
  overflow: hidden;   /* prevents video from bleeding on edges + creates stacking context */
}

.so-section__bg-video,
.so-section__bg-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;   /* clicks pass through to content */
}

.so-section__bg-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  border: 0;
  /* Chrome defaults <video> to solid black/grey background while media is
     loading. Force transparent so the section's CSS bg-image (poster URL,
     baked at setMode time or via runtime paint) shows through the video
     element until the first frame paints. Grey-flash diagnosis 3 Jul 2026. */
  background: transparent !important;
}

/* Same-pattern fix for other "replaceable" elements Chrome paints with a
   solid default background during load. See [[lesson-grapesjs-replaceable-
   element-default-bg]] for the full pattern doctrine.
     - .gjs-comp-map: GrapesJS's built-in map block — an iframe wrapper.
       Grey flashes before the map tiles load.
     - iframe.so-embed: convention for any custom-code / oEmbed iframe
       (YouTube, Facebook Page, custom embeds) — same load-phase grey.
   Setting the iframe transparent lets a parent placeholder (or the
   default page bg) show through until the framed content paints. */
.gjs-comp-map,
.gjs-comp-map iframe,
iframe.so-embed {
  background: transparent !important;
}

.so-section__bg-overlay {
  z-index: 1;
  background: transparent;   /* actual overlay written per-instance via setStyle */
  /* Fade the overlay in only once the video is actually playing. During
     the load phase the overlay would tint the poster ~35% darker, which
     reads as a grey wash on-screen. `is-playing` class is set by the
     view.onRender fadeInHandler in section-bg-video.js when canplay fires. */
  opacity: 0;
  transition: opacity 400ms ease;
}

.so-section[data-so-bg-mode="video"].is-video-playing .so-section__bg-overlay {
  opacity: 1;
}

/* Text-contrast opt-in (added 4 Jul 2026 — ChatGPT review follow-up).
   If the merchant needs the overlay dimmed DURING the poster-load window
   for text contrast (bright poster + white text case), they can set
   data-so-bg-overlay-dim-loading on the section (e.g. via the Video
   settings). Overlay renders at 0.15 opacity during load, then fades to
   full when video plays. Trade-off: slightly less visual polish, but
   text stays readable throughout. Default is opacity:0 during load. */
.so-section[data-so-bg-mode="video"][data-so-bg-overlay-dim-loading]
  .so-section__bg-overlay {
  opacity: 0.15;
}
.so-section[data-so-bg-mode="video"][data-so-bg-overlay-dim-loading].is-video-playing
  .so-section__bg-overlay {
  opacity: 1;
}

/* Content children sit above the video + overlay. Scoped to video-mode
   sections only so non-video sections keep their natural z-index (none).
   Applies to direct children (rows + any other section-level content). */
.so-section[data-so-bg-mode="video"] > *:not(.so-section__bg-video):not(.so-section__bg-overlay) {
  position: relative;
  z-index: 2;
}

/* ═══════════════════════════════════════════════════════════════════════
   so-testimonials — Black Box social-proof component (Phase B/D first pass)
   Added 5 Jul 2026. APPEND-ONLY section — do not reformat rules above.
   One engine, variants driven by wrapper classes:
     .so-testimonials-variant-{quote|grid|list|rating|carousel}
     .so-testimonials-style-{soft|elevated|outline|filled|dark}
     .so-testimonials-align-{left|center}
   Internals are fully locked (GrapesJS) — CSS only styles the opaque body.
   Full Style-tab colour controls land later; this is baseline styling.
   ═══════════════════════════════════════════════════════════════════════ */
.so-testimonials {
  --sot-gap: 24px;
  --sot-card-bg: #ffffff;
  --sot-card-radius: 16px;
  --sot-card-pad: 24px;
  --sot-card-border: transparent;
  --sot-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
  --sot-quote-color: #1f2937;
  --sot-name-color: #111827;
  --sot-muted-color: #6b7280;
  --sot-star-on: #f5a623;
  --sot-star-off: #d7dbe0;
  --sot-avatar-bg: #eef2f7;
  --sot-avatar-color: #44556b;
  display: block;
  box-sizing: border-box;
  background-color: var(--sot-bg-color, transparent);
  background-image: var(--sot-bg-image, none);
  background-size: var(--sot-bg-size, cover);
  background-position: center;
  background-repeat: var(--sot-bg-repeat, no-repeat);
  /* Inset so cards (and their shadows) aren't clipped against the component
     background edges, and content breathes when a background is set. */
  padding: var(--sot-pad, 28px);
}
@media (max-width: 600px) {
  .so-testimonials { padding: var(--sot-pad-mobile, 18px); }
}
.so-testimonials *,
.so-testimonials *::before,
.so-testimonials *::after { box-sizing: border-box; }

/* Heading block (config-managed, not an inline leaf) */
.so-testimonials-head { margin: 0 0 20px; }
.so-testimonials-heading {
  margin: 0 0 6px;
  font-family: var(--sot-heading-font, inherit);
  font-size: var(--sot-heading-size, 1.5rem);
  line-height: 1.25;
  font-weight: 700;
  color: var(--sot-heading-color, #111827);
}
.so-testimonials-subheading {
  margin: 0;
  font-family: var(--sot-sub-font, inherit);
  font-size: var(--sot-sub-size, 1rem);
  color: var(--sot-sub-color, #6b7280);
}

/* Grid / list layout */
.so-testimonials-list {
  display: grid;
  gap: var(--sot-gap);
  grid-template-columns: repeat(1, minmax(0, 1fr));
  align-items: stretch;
}
/* Equal-height toggle (Settings tab): 1fr rows make every card match the
   tallest card across ALL rows (not just within a row); when off, cards size
   to their own content. */
.so-testimonials-list.is-equal-height { grid-auto-rows: 1fr; }
.so-testimonials-list:not(.is-equal-height) { align-items: start; }
.so-testimonials-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.so-testimonials-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.so-testimonials-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 900px) {
  .so-testimonials-cols-3,
  .so-testimonials-cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 600px) {
  .so-testimonials-list { grid-template-columns: repeat(1, minmax(0, 1fr)); }
}

/* Card */
.so-testimonial-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  height: 100%;               /* equal heights in a stretched grid row */
  padding: var(--sot-card-pad);
  background: var(--sot-card-bg);
  border: 1px solid var(--sot-card-border);
  border-radius: var(--sot-card-radius);
  box-shadow: var(--sot-shadow);
}

/* Rating stars (internal — not a standalone so-rating component) */
.so-testimonial-rating {
  display: inline-flex;
  gap: 3px;
  color: var(--sot-star-on);
  line-height: 0;
}
.so-testimonial-star { display: inline-flex; }
.so-testimonial-star.is-empty { color: var(--sot-star-off); }
.so-testimonial-star svg { display: block; width: 16px; height: 16px; }

/* Quote — semantic <blockquote>, attribution sits outside it */
.so-testimonial-quote {
  margin: 0;
  font-family: var(--sot-quote-font, inherit);
  font-size: var(--sot-quote-size, 1.0625rem);
  line-height: 1.6;
  color: var(--sot-quote-color);
  font-style: normal;
}
.so-testimonial-quote::before { content: "\201C"; }
.so-testimonial-quote::after  { content: "\201D"; }

/* Author row (pushed to card bottom for tidy equal-height cards) */
.so-testimonial-author {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: auto;
}
.so-testimonial-avatar {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--sot-avatar-bg);
  color: var(--sot-avatar-color);
  font-weight: 600;
  font-size: 0.95rem;
}
.so-testimonial-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.so-testimonial-author-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.so-testimonial-author-name {
  font-weight: 700;
  color: var(--sot-name-color);
  font-family: var(--sot-name-font, inherit);
  font-size: var(--sot-name-size, 1rem);
}
.so-testimonial-author-role {
  color: var(--sot-muted-color);
  font-family: var(--sot-meta-font, inherit);
  font-size: var(--sot-meta-size, 0.875rem);
}
.so-testimonial-source {
  font-size: 0.8125rem;
  color: var(--sot-muted-color);
  text-decoration: none;
}
a.so-testimonial-source { color: #2563eb; }
a.so-testimonial-source:hover { text-decoration: underline; }
.so-testimonial-date { font-size: 0.8125rem; color: var(--sot-muted-color); }

/* ── Heading alignment (independent of card content alignment) ───────── */
.so-testimonials-head--left { text-align: left; }
.so-testimonials-head--center { text-align: center; }
.so-testimonials-head--right { text-align: right; }

/* ── Card content alignment ─────────────────────────────────────────── */
.so-testimonials-align-center .so-testimonial-card { text-align: center; }
.so-testimonials-align-center .so-testimonial-author { justify-content: center; }
.so-testimonials-align-center .so-testimonial-rating { align-self: center; }
.so-testimonials-align-right .so-testimonial-card { text-align: right; }
.so-testimonials-align-right .so-testimonial-author { justify-content: flex-end; }
.so-testimonials-align-right .so-testimonial-rating { align-self: flex-end; }

/* ── Variant: quote (single, centred, larger) ───────────────────────── */
.so-testimonials-variant-quote .so-testimonials-list {
  max-width: 720px;
  margin-inline: auto;
}
.so-testimonials-variant-quote .so-testimonial-card {
  align-items: center;
  text-align: center;
}
.so-testimonials-variant-quote .so-testimonial-quote { font-size: var(--sot-quote-size, 1.375rem); line-height: 1.5; }
.so-testimonials-variant-quote .so-testimonial-author { margin-top: 8px; justify-content: center; }

/* ── Variant: list (compact, one per row, tighter rhythm) ───────────── */
.so-testimonials-variant-list .so-testimonial-card { padding: 16px 20px; gap: 12px; }
.so-testimonials-variant-list .so-testimonial-quote { font-size: var(--sot-quote-size, 1rem); line-height: 1.55; }
.so-testimonials-variant-list .so-testimonial-quote::before,
.so-testimonials-variant-list .so-testimonial-quote::after { content: ""; }
.so-testimonials-variant-list .so-testimonial-author { margin-top: 4px; }
.so-testimonials-variant-list .so-testimonial-avatar { width: 40px; height: 40px; }

/* ── Variant: rating (stars emphasised, card top-weighted) ──────────── */
.so-testimonials-variant-rating .so-testimonial-star svg { width: 20px; height: 20px; }
.so-testimonials-variant-rating .so-testimonial-rating { margin-bottom: 2px; }
.so-testimonials-variant-rating .so-testimonial-quote { font-size: var(--sot-quote-size, 1rem); }

/* ── Variant: carousel (static track now; runtime nav in Phase E) ────── */
.so-testimonials-carousel { position: relative; }
/* Track = a horizontal flex row of full-width SLIDES; scroll-snaps per slide
   so paging is whole-page (clean jumps, no partial-column shift). */
.so-testimonials-track {
  display: flex;
  gap: var(--sot-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  /* Native scrollbar hidden on preview/published — it's a carousel, nav is
     via arrows/dots/swipe. Kept visible in the editor canvas for authoring
     (body.editor-mode override below). */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.so-testimonials-track::-webkit-scrollbar { width: 0; height: 0; }
body.editor-mode .so-testimonials-track {
  scrollbar-width: auto;
  -ms-overflow-style: auto;
  padding-bottom: 6px;   /* room so the editor scrollbar doesn't clip cards */
}
body.editor-mode .so-testimonials-track::-webkit-scrollbar { height: 8px; }
body.editor-mode .so-testimonials-track::-webkit-scrollbar-thumb {
  background: #c7ccd4;
  border-radius: 4px;
}
/* Each slide is one full-width page: a columns × rows grid of cards. The last
   slide simply has fewer cards (partial grid, empty trailing space). */
.so-testimonials-slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  display: grid;
  grid-template-columns: repeat(var(--sot-cpv, 3), minmax(0, 1fr));
  gap: var(--sot-gap);
  align-content: start;
}
/* Equal-height cards (Settings toggle): 1fr rows force every row to the
   tallest row's height, so all cards on a slide match (not just within a row). */
.so-testimonials-slide.is-equal-height { grid-auto-rows: 1fr; }
.so-testimonials-slide .so-testimonial-card { height: 100%; }
@media (max-width: 700px) {
  /* Single column per slide on mobile (cards stack within the slide). */
  .so-testimonials-slide { grid-template-columns: 1fr; }
}
/* Arrows + dots are injected by so-testimonials-runtime.js (Carousel only,
   preview/published — absent in the editor canvas, which shows a static
   scrollable track). */
.so-testimonials-arrow {
  position: absolute;
  top: 40%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid #e5e7eb;
  background: #ffffff;
  color: #111827;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);
}
.so-testimonials-arrow--prev { left: 6px; }
.so-testimonials-arrow--next { right: 6px; }
.so-testimonials-arrow:hover { background: #f3f4f6; }
.so-testimonials-dots {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 16px;
}
.so-testimonials-dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #d1d5db;
  cursor: pointer;
}
.so-testimonials-dot.is-active { background: #2563eb; }
.so-testimonials-style-dark .so-testimonials-arrow {
  background: #2c3340;
  border-color: #3a4150;
  color: #eef1f6;
}
.so-testimonials-style-dark .so-testimonials-dot { background: #3a4150; }
.so-testimonials-style-dark .so-testimonials-dot.is-active { background: #7db1ff; }

/* ── Style presets ──────────────────────────────────────────────────── */
.so-testimonials-style-elevated { --sot-shadow: 0 16px 40px rgba(0, 0, 0, 0.18); }
.so-testimonials-style-outline {
  --sot-shadow: none;
  --sot-card-border: #e5e7eb;
  --sot-card-bg: #ffffff;
}
.so-testimonials-style-filled {
  --sot-shadow: none;
  --sot-card-bg: #f5f7fa;
}
.so-testimonials-style-dark {
  --sot-card-bg: #1f2430;
  --sot-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  --sot-quote-color: #eef1f6;
  --sot-name-color: #ffffff;
  --sot-muted-color: #9aa4b2;
  --sot-avatar-bg: #2c3340;
  --sot-avatar-color: #cdd5e0;
  --sot-star-off: #3a4150;
}
.so-testimonials-style-dark a.so-testimonial-source { color: #7db1ff; }
/* End so-testimonials section ────────────────────────────────────────── */

/* ═════════════════════════════════════════════════════════════════════
   HEADER BEHAVIOUR — state contract (Phase A0 skeleton, 9 Jul 2026)
   Design: EDITOR4_HEADER_BEHAVIOUR_DESIGN.md. Shared by editor canvas,
   preview and published (single stylesheet = parity by construction).
   Inert without the runtime's attributes. Authored attrs persist;
   data-so-header-active / data-so-header-inset / body vars / .so-scrolled
   are RUNTIME-ONLY (sanitizer strips them from all editor saves).
   ───────────────────────────────────────────────────────────────────── */
:root { --so-z-header: 1000; }

/* Blend engaged: this header section lifts out of flow and floats over
   the page top, transparent, below any in-flow header sections above it. */
.so-section[data-so-header-blend="true"][data-so-header-active="true"] {
  position: absolute;
  left: 0; right: 0;
  top: var(--so-header-flow-h, 0px);
  z-index: var(--so-z-header);
  background: transparent !important; /* runtime state beats authored bg; authored values persist untouched */
}

/* EDITOR CANVAS blend presentation (round-6): the canvas wraps content in
   zone containers (own positioning contexts + chrome paddings), so the
   absolute lift above cannot reach the hero there. In-canvas the holder
   stays IN FLOW (transparent, layered above) and the body zone is pulled
   up underneath it by the runtime-measured natural gap
   (--so-hb-editor-pull) — WYSIWYG blend without fighting zone chrome.
   Published/preview pages have no .editor-mode body: untouched. */
body.editor-mode .so-section[data-so-header-blend="true"][data-so-header-active="true"] {
  position: relative;
  top: auto; left: auto; right: auto;
}
body.editor-mode.so-hb-blend-preview #e4-body-zone {
  /* BFC: the body zone has NO padding, so the hero's negative margin
     otherwise COLLAPSES THROUGH the parent and drags the zone box (and
     its chrome/labels/drop target) up with it — the 12 Jul HB-dump
     finding. flow-root isolates the margin; the zone box stays in flow
     and the hero overflows upward out of it, under the holder. */
  display: flow-root;
}
/* Drag-suspend (JC 12 Jul): while any drag is in flight the blend preview
   lies about zone geometry (the hero overlaps the header's lower drop
   region), so drops aim wrong. The editor toggles so-hb-suspend on
   drag start/stop: pull + hero spacer collapse to honest flow. */
body.editor-mode.so-hb-suspend #e4-body-zone > .so-section:first-child {
  margin-top: 0 !important;
}
body.editor-mode.so-hb-suspend .so-section[data-so-header-inset]::before {
  height: 0 !important;
}

body.editor-mode #e4-body-zone > .so-section:first-child {
  /* Pull the HERO up inside the zone (zone box + chrome stay put — moving
     the zone made the header section read as body content and overlapped
     the drop targets). !important: the canvas carries competing section
     margin rules (PULL-DBG2 found four on the zone alone). */
  margin-top: calc(-1 * var(--so-hb-editor-pull, 0px)) !important;
}

/* Safe inset: spacer pseudo-element on the first body section — additive,
   never mutates merchant style values, composes with any authored padding. */
.so-section[data-so-header-inset]::before {
  content: '';
  display: block;
  height: var(--so-active-header-overlay-h, 0px);
}

/* Pinned state (scrolled past threshold): solid surface ALWAYS — the
   shared invariant: blended appearance only while the hero is the backdrop. */
body.so-scrolled .so-section[data-so-header-scroll="sticky"][data-so-header-status="active"] {
  position: fixed;
  top: var(--so-hb-pin-top, 0px); /* measured pin stack: below visible pinned sections above (round-6) */
  left: 0; right: 0;
  z-index: var(--so-z-header);
  background: var(--so-hbs-bg, #ffffff) !important; /* section-local scrolled surface */
  color: var(--so-hbs-text, inherit);
  box-shadow: var(--so-hbs-shadow, 0 2px 12px 0 rgba(15, 23, 42, 0.10));
}

/* Anchor safety while a pinned stack exists */
html { scroll-padding-top: var(--so-header-pinned-total-h, 0px); }

@media (prefers-reduced-motion: reduce) {
  [data-so-header-scroll] { transition: none !important; animation: none !important; }
}

/* ─────────────────────────────────────────────────────────────────────
   HEADER BEHAVIOUR — Phase C additions (10 Jul 2026)
   sticky-up / fade / pin compensation / mobile-menu solid guardrail.
   All state classes below are RUNTIME-ONLY (sanitizer strips on save).
   ───────────────────────────────────────────────────────────────────── */

/* Pin compensation: a sticky holder leaving the flow would jump the page
   up by its height — body padding fills the vacated slot (header sits at
   page top, so this is exact). Runtime sets the class only for in-flow
   sticky/sticky-up holders (an active blend overlay is already out of flow). */
body.so-scrolled.so-hb-compensate {
  padding-top: var(--so-header-pinned-total-h, 0px);
}

/* Scroll-Up Sticky: pinned base is the SOLID surface (shared invariant —
   it reappears over later content, so blend is never allowed here).
   HIDDEN by default once scrolled (no transition — engaging mid-scroll-down
   must not flash the header back into view and animate it away); slides in
   only while scrolling up. Hiding again on direction change is instant by
   design (the transition lives only on the shown state). */
body.so-scrolled .so-section[data-so-header-scroll="sticky-up"][data-so-header-status="active"] {
  position: fixed;
  top: var(--so-hb-pin-top, 0px); /* slides in BELOW pinned sections above it, never over them */
  left: 0; right: 0;
  z-index: var(--so-z-header);
  background: var(--so-hbs-bg, #ffffff) !important;
  color: var(--so-hbs-text, inherit);
  transform: translateY(calc(-110% - var(--so-hb-pin-top, 0px)));
  box-shadow: none;
}
body.so-scrolled.so-hb-up .so-section[data-so-header-scroll="sticky-up"][data-so-header-status="active"] {
  transform: translateY(0);
  transition: transform 0.28s ease;
  box-shadow: var(--so-hbs-shadow, 0 2px 12px 0 rgba(15, 23, 42, 0.10));
}

/* Fade out: stays in flow; opacity driven continuously by the runtime
   (--so-hb-fade over the first ~120px). Unclickable once faded. */
.so-section[data-so-header-scroll="fade"][data-so-header-status="active"] {
  opacity: var(--so-hb-fade, 1);
}
body.so-hb-faded .so-section[data-so-header-scroll="fade"][data-so-header-status="active"] {
  pointer-events: none;
  visibility: hidden;
}

/* Mobile menu open ⇒ force the solid, visible surface on every
   behaviour-holding header section (guardrail: never a transparent or
   hidden header behind an open menu). */
body.so-mobile-menu-open .so-section[data-so-header-status="active"],
body.so-mobile-menu-open .so-section[data-so-header-active="true"] {
  opacity: 1 !important;
  visibility: visible !important;
  transform: none !important;
  pointer-events: auto !important;
  background: var(--so-hbs-bg, #ffffff) !important;
  color: var(--so-hbs-text, inherit);
}

@media (prefers-reduced-motion: reduce) {
  /* sticky-up behaves as plain sticky (always shown once scrolled);
     fade becomes a binary show/hide. */
  body.so-scrolled .so-section[data-so-header-scroll="sticky-up"][data-so-header-status="active"] {
    transform: none;
    box-shadow: var(--so-hbs-shadow, 0 2px 12px 0 rgba(15, 23, 42, 0.10));
  }
  .so-section[data-so-header-scroll="fade"][data-so-header-status="active"] { opacity: 1; }
  body.so-scrolled .so-section[data-so-header-scroll="fade"][data-so-header-status="active"] {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }
}

/* ============================================================================
   SO-ANNOUNCEMENT-BAR — Black Box promo/notice banner (12 Jul 2026)
   ----------------------------------------------------------------------------
   Doctrine: EDITOR4_BLACK_BOX_COMPONENT_CHECKLIST.md §6 — static component
   CSS lives HERE (single shared stylesheet → editor parent, canvas iframe,
   preview and published all load the same rules; parity by construction).
   Per-instance customisation arrives as --so-annc-* CSS variables committed
   through setStyle() (exports as a #componentId rule) with the hard defaults
   below as fallbacks. Renderer: js/editor4/components/announcement-bar.js.

   Sticky/scroll behaviour is intentionally ABSENT from this component —
   pinning is owned by the HOST header section via the Header Behaviour
   measured pin stack (EDITOR4_HEADER_BEHAVIOUR_DESIGN.md §15). The bar
   owns content/presentation only.

   Display modes (wrapper modifier classes, set by the render function):
     --single     one item, alignment via --align-* modifiers
     --repeat     item duplicated to fill the width, static
     --ticker     item duplicated into TWO identical groups, track animates
                  translateX(-50%) for a seamless CSS-only loop (no JS,
                  no runtime injection). Duplicate count + duration are
                  computed adaptively by the render function from the
                  estimated item width — see estimateAnncItemWidth() in
                  announcement-bar.js for the documented formula.
   ============================================================================ */

.so-announcement-bar {
  position: relative;
  display: block;
  width: 100%;
  box-sizing: border-box;
  overflow: hidden;
  background: var(--so-annc-bg, #111827);
  color: var(--so-annc-text, #f9fafb);
  font-family: var(--so-annc-font, inherit);
  font-size: var(--so-annc-size, 15px);
  font-weight: var(--so-annc-weight, 500);
  letter-spacing: var(--so-annc-letter-spacing, normal);
  border-style: var(--so-annc-border-style, none);
  border-color: var(--so-annc-border-color, transparent);
  border-width: var(--so-annc-border-w, 0);
  border-radius: var(--so-annc-radius, 0);
  box-shadow: var(--so-annc-shadow, none);
  line-height: 1.4;
}

.so-announcement-bar__viewport {
  overflow: hidden;
  display: flex;
  align-items: center;
  width: 100%;
  box-sizing: border-box;
  padding: var(--so-annc-pad-y, 10px) var(--so-annc-pad-x, 16px);
  min-height: var(--so-annc-min-h, 0);
}

/* Contained width — internal VISUAL wrapper only (no so-container child
   model inside a Black Box; approved 12 Jul 2026). Max-widths mirror the
   standard E4/Bootstrap-5 container ladder so "Contained" lines up with
   .container content elsewhere on the page. */
.so-announcement-bar--contained .so-announcement-bar__viewport {
  margin-left: auto;
  margin-right: auto;
}
@media (min-width: 576px)  { .so-announcement-bar--contained .so-announcement-bar__viewport { max-width: 540px; } }
@media (min-width: 768px)  { .so-announcement-bar--contained .so-announcement-bar__viewport { max-width: 720px; } }
@media (min-width: 992px)  { .so-announcement-bar--contained .so-announcement-bar__viewport { max-width: 960px; } }
@media (min-width: 1200px) { .so-announcement-bar--contained .so-announcement-bar__viewport { max-width: 1140px; } }
@media (min-width: 1400px) { .so-announcement-bar--contained .so-announcement-bar__viewport { max-width: 1320px; } }

.so-announcement-bar__track {
  display: flex;
  align-items: center;
  width: 100%;
  min-width: 0;
}

/* Each group carries the item gap INSIDE (column-gap) plus the same gap as
   trailing padding, so the seam between ticker group A and group B is
   exactly one gap wide — this is what makes the translateX(-50%) loop
   seamless (both groups are pixel-identical, seam included). */
.so-announcement-bar__group {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: var(--so-annc-item-gap, 32px);
}
.so-announcement-bar--repeat .so-announcement-bar__group,
.so-announcement-bar--ticker .so-announcement-bar__group {
  padding-right: var(--so-annc-item-gap, 32px);
}

/* Single mode: one group fills the row; alignment via modifiers. */
.so-announcement-bar--single .so-announcement-bar__group {
  flex: 1 1 auto;
  min-width: 0;
  justify-content: center;
}
.so-announcement-bar--align-left  .so-announcement-bar__group { justify-content: flex-start; }
.so-announcement-bar--align-right .so-announcement-bar__group { justify-content: flex-end; }

.so-announcement-bar__item {
  display: inline-flex;
  align-items: center;
  gap: var(--so-annc-icon-gap, 8px);
  white-space: nowrap;
}
/* Single mode must wrap safely (long text, long CTA, mobile). */
.so-announcement-bar--single .so-announcement-bar__item {
  white-space: normal;
  flex-wrap: wrap;
  justify-content: inherit;
  min-width: 0;
  text-align: center;
}
.so-announcement-bar--align-left.so-announcement-bar--single  .so-announcement-bar__item { text-align: left; }
.so-announcement-bar--align-right.so-announcement-bar--single .so-announcement-bar__item { text-align: right; }

.so-announcement-bar__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  font-size: var(--so-annc-icon-size, 1em);
  color: var(--so-annc-icon-color, currentColor);
  line-height: 1;
}
.so-announcement-bar__icon svg {
  width: 1em;
  height: 1em;
  display: block;
}

.so-announcement-bar__text {
  min-width: 0;
}

/* Text CTA */
.so-announcement-bar__cta {
  flex: 0 0 auto;
  color: var(--so-annc-cta-color, inherit);
  font-weight: var(--so-annc-cta-weight, 700);
  text-decoration: var(--so-annc-cta-underline, underline);
  text-underline-offset: 2px;
  margin-left: var(--so-annc-cta-gap, 12px);
  white-space: nowrap;
  cursor: pointer;
}
.so-announcement-bar__cta:hover {
  color: var(--so-annc-cta-hover-color, inherit);
  opacity: 0.85;
}
.so-announcement-bar__cta:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* Button CTA */
.so-announcement-bar__cta--button {
  display: inline-flex;
  align-items: center;
  background: var(--so-annc-btn-bg, #f9fafb);
  color: var(--so-annc-btn-color, #111827);
  font-family: var(--so-annc-btn-font, inherit);
  font-size: var(--so-annc-btn-size, 14px);
  border: var(--so-annc-btn-border-w, 0) solid var(--so-annc-btn-border-color, transparent);
  border-radius: var(--so-annc-btn-radius, 999px);
  padding: var(--so-annc-btn-pad-y, 5px) var(--so-annc-btn-pad-x, 14px);
  text-decoration: none;
  line-height: 1.3;
}
.so-announcement-bar__cta--button:hover {
  background: var(--so-annc-btn-hover-bg, var(--so-annc-btn-bg, #f9fafb));
  color: var(--so-annc-btn-hover-color, var(--so-annc-btn-color, #111827));
  font-family: var(--so-annc-btn-hover-font, var(--so-annc-btn-font, inherit));
  font-size: var(--so-annc-btn-hover-size, var(--so-annc-btn-size, 14px));
  border-color: var(--so-annc-btn-hover-border-color, var(--so-annc-btn-border-color, transparent));
  opacity: 1;
}

/* Separator between repeated items (repeat/ticker only) */
.so-announcement-bar__sep {
  flex: 0 0 auto;
  opacity: 0.6;
}
.so-announcement-bar__sep--line {
  width: 1px;
  height: 1em;
  background: currentColor;
}

/* Whole-bar link — stretched overlay anchor inside the stable <div> root
   (approved 12 Jul 2026: root never swaps between <div> and <a>). Mutually
   exclusive with a CTA anchor by construction in the render function, so
   nested anchors are impossible. */
.so-announcement-bar__bar-link {
  position: absolute;
  inset: 0;
  z-index: 2;
}
.so-announcement-bar__bar-link:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: -2px;
}

/* Repeat + ticker: track sizes to content and clips at the viewport. */
.so-announcement-bar--repeat .so-announcement-bar__track,
.so-announcement-bar--ticker .so-announcement-bar__track {
  width: max-content;
}

/* Ticker animation — CSS only. Duration is emitted per instance by the
   render function as an inline --so-annc-ticker-duration on the track
   (derived from estimated group width ÷ speed px/s, so perceived speed
   is consistent regardless of message length). */
.so-announcement-bar--ticker .so-announcement-bar__track {
  animation: so-annc-scroll var(--so-annc-ticker-duration, 30s) linear infinite;
  will-change: transform;
}
.so-announcement-bar--ticker-right .so-announcement-bar__track {
  animation-direction: reverse;
}
.so-announcement-bar--pause-hover:hover .so-announcement-bar__track {
  animation-play-state: paused;
}
@keyframes so-annc-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Mobile: keep a sane minimum inline padding; allow the single-mode item
   to wrap with a little breathing room between wrapped rows. */
@media (max-width: 575.98px) {
  .so-announcement-bar__viewport {
    padding-left: max(12px, var(--so-annc-pad-x, 16px));
    padding-right: max(12px, var(--so-annc-pad-x, 16px));
  }
  .so-announcement-bar--single .so-announcement-bar__item {
    row-gap: 6px;
  }
}

/* Reduced motion: ticker becomes the static repeated presentation —
   readable, no movement. (House convention — same media block used by
   Header Behaviour + so-animations.) */
@media (prefers-reduced-motion: reduce) {
  .so-announcement-bar--ticker .so-announcement-bar__track {
    animation: none;
  }
}

/* ============================================================================
   SO-SEARCH — Black Box product-search input (12 Jul 2026)
   ----------------------------------------------------------------------------
   Doctrine: EDITOR4_BLACK_BOX_COMPONENT_CHECKLIST.md §6 — static component
   CSS lives HERE (single shared stylesheet; editor/canvas/preview/published
   parity by construction). Per-instance customisation = --so-search-* CSS
   variables committed via setStyle() (#componentId rule) with hard defaults
   below. Renderer: js/editor4/components/search.js. Storefront behaviour:
   assets/editor4/so-search-runtime.js (suggestions + submit normalisation).

   The component SUBMITS to the existing Shell search results page
   (GET q → shop/search) — it owns the interface only, never results
   rendering. Fully namespaced classes; deliberately NO legacy typeahead
   selectors (.typeahead / .widget-search-form-input / .tt-*) so the
   global Bloodhound initialisers on /shop/* shell pages can never attach.

   Layout presets (wrapper modifier classes set by the render function):
     --preset-inside-right / --preset-inside-left   button integrated in field
     --preset-appended-pill / --preset-appended-rounded  attached field+button
     --preset-detached                               gap between field+button
     --preset-stacked-pill / --preset-stacked-square stacked full-width
   ============================================================================ */

.so-search {
  position: relative;
  display: block;
  box-sizing: border-box;
  width: 100%;
  background: var(--so-search-wrapper-bg, transparent);
  font-family: var(--so-search-field-font, inherit);
}
.so-search *, .so-search *::before, .so-search *::after { box-sizing: border-box; }

/* Width + alignment */
.so-search--width-compact { max-width: 320px; }
.so-search--width-custom  { max-width: var(--so-search-max-w, 600px); }
.so-search--align-left    { margin-right: auto; }
.so-search--align-center  { margin-left: auto; margin-right: auto; }
.so-search--align-right   { margin-left: auto; }
/* stretch/full: default 100% block behaviour */

.so-search__form { margin: 0; position: relative; }

/* Screen-reader-only helper (scoped — do not reuse global classes) */
.so-search__sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ── Controls row ── */
.so-search__controls {
  display: flex;
  align-items: stretch;
  width: 100%;
}

.so-search__field {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--so-search-height, 44px);
  padding: 0 var(--so-search-field-padding-x, 16px);
  background: var(--so-search-field-bg, #ffffff);
  color: var(--so-search-field-text, #1a202c);
  font-family: var(--so-search-field-font, inherit);
  font-size: var(--so-search-field-size, 15px);
  font-weight: var(--so-search-field-weight, 400);
  border: var(--so-search-border-width, 1px) solid var(--so-search-border, #dee2e6);
  border-radius: var(--so-search-radius, 999px);
  box-shadow: var(--so-search-shadow, none);
  outline: none;
  appearance: none;
  -webkit-appearance: none;
}
.so-search__field::placeholder {
  color: var(--so-search-placeholder, #6b7280);
  opacity: 1;
}
.so-search__field::-webkit-search-decoration,
.so-search__field::-webkit-search-cancel-button { -webkit-appearance: none; }

.so-search__button {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: var(--so-search-height, 44px);
  padding: 0 var(--so-search-button-padding-x, 16px);
  background: var(--so-search-button-bg, #0d6efd);
  color: var(--so-search-button-text, #ffffff);
  font-family: var(--so-search-button-font, inherit);
  font-size: var(--so-search-button-size, 15px);
  border: var(--so-search-button-border-width, 0) solid var(--so-search-button-border, transparent);
  border-radius: var(--so-search-button-radius, 999px);
  cursor: pointer;
  line-height: 1;
  white-space: nowrap;
}
.so-search__button:hover {
  background: var(--so-search-button-hover-bg, #0b5ed7);
  color: var(--so-search-button-hover-text, #ffffff);
  border-color: var(--so-search-button-hover-border, var(--so-search-button-border, transparent));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-search-button-hover-bg, var(--so-search-button-bg, #0b5ed7)) 35%, transparent), 0 6px 14px rgba(0, 0, 0, 0.18);   /* ring + lift only — no brightness filter (ruling E) */
  transform: translateY(-1px);
}
.so-search__button-icon { display: inline-flex; color: var(--so-search-submit-icon, currentColor); }
.so-search__button-icon svg { width: var(--so-search-icon-size, 18px); height: var(--so-search-icon-size, 18px); display: block; }

.so-search__leading-icon {
  display: inline-flex;
  align-items: center;
  color: var(--so-search-leading-icon, #6b7280);
  flex: 0 0 auto;
}
.so-search__leading-icon svg { width: var(--so-search-icon-size, 18px); height: var(--so-search-icon-size, 18px); display: block; }

/* Focus — a visible keyboard indicator can never be fully removed: the
   committed vars are clamped in styleVars() (zero/transparent → accessible
   fallback), and this rule always paints border + ring from those vars. */
.so-search__field:focus-visible {
  border-color: var(--so-search-focus-border, #0d6efd);
  box-shadow: 0 0 0 var(--so-search-focus-ring-width, 3px) var(--so-search-focus-ring, rgba(13, 110, 253, 0.25));
}
.so-search__button:focus-visible {
  outline: 2px solid var(--so-search-focus-border, #0d6efd);
  outline-offset: 2px;
}

/* ── Preset: inside-right / inside-left (button integrated in the field) ──
   The CONTROLS row carries the field look; the input goes borderless. */
.so-search--preset-inside-right .so-search__controls,
.so-search--preset-inside-left  .so-search__controls {
  background: var(--so-search-field-bg, #ffffff);
  border: var(--so-search-border-width, 1px) solid var(--so-search-border, #dee2e6);
  border-radius: var(--so-search-radius, 999px);
  box-shadow: var(--so-search-shadow, none);
  align-items: center;
  padding: 3px;
}
.so-search--preset-inside-right .so-search__field,
.so-search--preset-inside-left  .so-search__field {
  background: transparent;
  border: none;
  box-shadow: none;
  height: calc(var(--so-search-height, 44px) - 8px);
}
.so-search--preset-inside-right .so-search__field:focus-visible,
.so-search--preset-inside-left  .so-search__field:focus-visible { box-shadow: none; }
.so-search--preset-inside-right .so-search__controls:focus-within,
.so-search--preset-inside-left  .so-search__controls:focus-within {
  border-color: var(--so-search-focus-border, #0d6efd);
  box-shadow: 0 0 0 var(--so-search-focus-ring-width, 3px) var(--so-search-focus-ring, rgba(13, 110, 253, 0.25));
}
.so-search--preset-inside-right .so-search__button,
.so-search--preset-inside-left  .so-search__button {
  height: calc(var(--so-search-height, 44px) - 8px);
}
.so-search--preset-inside-left .so-search__button { order: -1; }
.so-search--preset-inside-left .so-search__leading-icon { order: -2; }
.so-search--preset-inside-right .so-search__leading-icon { margin-left: 12px; }
.so-search--preset-inside-left  .so-search__leading-icon { margin-left: 12px; }

/* ── Preset: appended (attached field + button) ── */
.so-search--preset-appended-pill .so-search__field,
.so-search--preset-appended-rounded .so-search__field {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
}
.so-search--preset-appended-pill .so-search__button,
.so-search--preset-appended-rounded .so-search__button {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.so-search--preset-appended-rounded .so-search__field { border-radius: 8px 0 0 8px; }
.so-search--preset-appended-rounded .so-search__button { border-radius: 0 8px 8px 0; }
.so-search--preset-appended-pill .so-search__field { border-radius: 999px 0 0 999px; }
.so-search--preset-appended-pill .so-search__button { border-radius: 0 999px 999px 0; }
.so-search--preset-appended-pill .so-search__leading-icon,
.so-search--preset-appended-rounded .so-search__leading-icon,
.so-search--preset-detached .so-search__leading-icon,
.so-search--preset-stacked-pill .so-search__leading-icon,
.so-search--preset-stacked-square .so-search__leading-icon {
  /* Anchored to the FORM but vertically centred on the first control row
     (the field), so it also lands correctly in stacked presets where 50%
     of the form would fall between the two rows. */
  position: absolute;
  top: calc(var(--so-search-height, 44px) / 2);
  transform: translateY(-50%);
  pointer-events: none;
}
.so-search--leading-left .so-search__leading-icon { left: 14px; }
.so-search--leading-left .so-search__field {
  padding-left: calc(var(--so-search-field-padding-x, 16px) + var(--so-search-icon-size, 18px) + 10px);
}
/* Right placement is only offered on stacked presets (full-width field row),
   where right:14px is exact. The modal disables it elsewhere; the render
   function also clamps invalid combinations to 'none' defensively. */
.so-search--leading-right .so-search__leading-icon { right: 14px; }
.so-search--leading-right .so-search__field {
  padding-right: calc(var(--so-search-field-padding-x, 16px) + var(--so-search-icon-size, 18px) + 10px);
}

/* ── Preset: detached ── */
.so-search--preset-detached .so-search__controls { gap: 10px; }

/* ── Preset: stacked ── */
.so-search--preset-stacked-pill .so-search__controls,
.so-search--preset-stacked-square .so-search__controls {
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
}
.so-search--preset-stacked-pill .so-search__button,
.so-search--preset-stacked-square .so-search__button { width: 100%; }
.so-search--preset-stacked-square .so-search__field,
.so-search--preset-stacked-square .so-search__button { border-radius: 6px; }

/* ── Button display modes ── */
.so-search--btn-icon .so-search__button-label { display: none; }
.so-search--btn-text .so-search__button-icon { display: none; }
.so-search--btn-none .so-search__button { display: none; }

/* ── Suggestions dropdown ──
   NO dedicated knobs (JC 13 Jul): the panel INHERITS the field's vars
   (background / text / border) and DERIVES its secondary tones from them
   via color-mix, so styling the field once styles the dropdown to match.
   color-mix is safe here — this is a real stylesheet served everywhere
   (the checklist §6 stripping issue only affects GrapesJS-exported CSS). */
.so-search__suggestions {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 6px;
  background: var(--so-search-field-bg, #ffffff);
  border: var(--so-search-border-width, 1px) solid var(--so-search-border, #dee2e6);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
  max-height: 360px;
  overflow-y: auto;
  /* Above sticky headers (--so-z-header: 1000) but far below editor chrome. */
  z-index: calc(var(--so-z-header, 1000) + 100);
}
.so-search__suggestions[hidden] { display: none; }

.so-search__result {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  cursor: pointer;
  border-bottom: 1px solid color-mix(in srgb, var(--so-search-field-text, #1a202c) 12%, transparent);
  color: var(--so-search-field-text, #1a202c);
  font-family: var(--so-search-field-font, inherit);
  font-size: var(--so-search-field-size, 15px);
}
.so-search__result:last-child { border-bottom: none; }
.so-search__result.is-active,
.so-search__result:hover {
  background: color-mix(in srgb, var(--so-search-field-text, #1a202c) 8%, var(--so-search-field-bg, #ffffff));
}
.so-search__result-img {
  width: 36px;
  height: 36px;
  object-fit: cover;
  border-radius: 6px;
  flex: 0 0 36px;
  background: transparent;
}
.so-search__result-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.so-search__result-price {
  flex: 0 0 auto;
  color: var(--so-search-field-text, #1a202c);
  font-weight: 600;
}
.so-search__result--empty {
  cursor: default;
  color: color-mix(in srgb, var(--so-search-field-text, #1a202c) 55%, transparent);
}
.so-search__result--empty:hover { background: transparent; }
.so-search__result--viewall {
  justify-content: center;
  font-weight: 600;
  color: var(--so-search-field-text, #1a202c);
}

/* ── Responsive ── */
@media (max-width: 575.98px) {
  /* Appended + detached collapse to stacked on narrow screens so the
     button never shrinks to an unusable width. */
  .so-search--preset-appended-pill .so-search__controls,
  .so-search--preset-appended-rounded .so-search__controls,
  .so-search--preset-detached .so-search__controls {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  .so-search--preset-appended-pill .so-search__field,
  .so-search--preset-appended-pill .so-search__button { border-radius: 999px; border-right: var(--so-search-border-width, 1px) solid var(--so-search-border, #dee2e6); }
  .so-search--preset-appended-pill .so-search__button { border: var(--so-search-button-border-width, 0) solid var(--so-search-button-border, transparent); }
  .so-search--preset-appended-rounded .so-search__field { border-radius: 8px; border-right: var(--so-search-border-width, 1px) solid var(--so-search-border, #dee2e6); }
  .so-search--preset-appended-rounded .so-search__button { border-radius: 8px; }
  .so-search--width-compact, .so-search--width-custom { max-width: 100%; }
}

/* Commit A2 boot-health sentinel */
:root{--e4-ready-components:1;}
