{"id":18223,"date":"2020-03-12T05:58:44","date_gmt":"2020-03-12T05:58:44","guid":{"rendered":"https:\/\/radiojingles24.com\/?post_type=product&#038;p=18223"},"modified":"2026-09-08T15:54:08","modified_gmt":"2026-09-08T15:54:08","slug":"sung-audio","status":"publish","type":"product","link":"https:\/\/radiojingles24.com\/fr\/produit\/sung-audio\/","title":{"rendered":"Jingles chant\u00e9s"},"content":{"rendered":"\n\t\t<div id=\"fws_6aa211180a903\"  data-midnight=\"dark\"  data-bg-mobile-hidden=\"\" class=\"wpb_row vc_row-fluid vc_row top-level standard_section   \"  style=\"padding-top: 0px; padding-bottom: 0px; \"><div class=\"row-bg-wrap\" data-bg-animation=\"none\"><div class=\"inner-wrap \"><div class=\"row-bg    \"  style=\"\"><\/div><\/div><div class=\"row-bg-overlay\" ><\/div><\/div><div class=\"col span_12 dark left\">\n\t<div  class=\"vc_col-sm-12 wpb_column column_container vc_column_container col no-extra-padding\"  data-t-w-inherits=\"default\" data-border-radius=\"none\" data-shadow=\"none\" data-border-animation=\"\" data-border-animation-delay=\"\" data-border-width=\"none\" data-border-style=\"solid\" data-border-color=\"\" data-bg-cover=\"\" data-padding-pos=\"all\" data-has-bg-color=\"false\" data-bg-color=\"\" data-bg-opacity=\"1\" data-hover-bg=\"\" data-hover-bg-opacity=\"1\" data-animation=\"\" data-delay=\"0\"><div class=\"column-bg-overlay\"><\/div>\n\t\t<div class=\"vc_column-inner\">\n\t\t\t<div class=\"wpb_wrapper\">\n\t\t\t\t\n\t<div class=\"wpb_raw_code wpb_content_element wpb_raw_html\" >\n\t\t<div class=\"wpb_wrapper\">\n\t\t\t<!-- ==========================================================\n     RADIO JINGLES 24 \u2014 SUNG WIZARD v3.9.6 (multi-jingle via RJ24 plugin AJAX)\n\n     v3.9.6 changes (June 2026):\n       - Real fix for footer-lift not firing. The handler used to run\n         immediately at script parse time, but the wizard's inline script\n         is in the middle of the document \u2014 the WP footer (.rj24-foot)\n         comes AFTER it in source order. So document.querySelector for\n         the footer returned null, the function early-exited, and no\n         scroll listener was ever attached. Result: bar overlapped footer.\n       - Now defers attachment via DOMContentLoaded + window.load + a\n         polling retry, so the handler always finds the footer eventually.\n\n     v3.9.5 changes\n\n     v3.9.5 changes (June 2026):\n       - REAL fix for \"sticky bar disappears under footer\". The bar was\n         being trapped inside .rjsw-screen because the rjswFade animation\n         used transform:translateY(8px) \u2014 any ancestor transform breaks\n         position:fixed (the containing block becomes the transformed\n         ancestor instead of the viewport). The bar was being positioned\n         relative to the tall screen container, putting it off-screen.\n       - rjswFade keyframe: removed translateY, opacity-only fade now.\n       - footer-lift handler: clamps the lift so the bar always stays\n         within the viewport (\"remain at all times\"). When you scroll deep\n         into the footer, the bar pins to the top of the viewport rather\n         than disappearing upward.\n\n     v3.9.4 changes\n\n     v3.9.4 changes (June 2026):\n       - Sticky buy bar is now footer-aware: lifts up smoothly when the\n         page footer enters the bottom of the viewport, instead of\n         floating over the footer content. Uses a passive scroll listener\n         (cheap, no layout thrash) + transform: translateY for GPU compositing.\n\n     v3.9.3 changes\n\n     v3.9.3 changes (June 2026):\n       - Removed -\u00a310 discount from all Kiss beds (Kiss now priced same as\n         every other bed: standard bundle pricing). The discount property has\n         been deleted from each Kiss entry, so the 'SAVE \u00a310' badge will no\n         longer appear on Kiss bed cards in the wizard.\n\n     v3.9.2 changes\n\n     v3.9.2 changes (June 2026):\n       - Added Epic 6, 7, 8 beds\n       - Added Kiss 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20 (each -\u00a310 discount)\n       - Fixed sticky buy bar: was position:sticky;top:0 (collapsing to 0 height)\n         now position:fixed;bottom:0 matching \/product\/commercial-production\/\n       - .rjsw-screen gets padding-bottom:96px so content doesn't sit under bar\n       - Hides sticky bars from inactive screens\n       - Kept Sky & Virgin references per business decision\n\n     ARCHITECTURE:\n       Earlier approaches drove one hidden TMEPO product repeatedly to\n       create multiple cart lines. Every transport (native redirect,\n       wc-ajax, iframe) failed because that fights WooCommerce structurally.\n\n       v3.9.0 uses a proper foundation: a companion plugin (RJ24 Sung\n       Jingles) registers a custom AJAX action `rj24_add_jingle` and a\n       simple \"Sung Jingle\" product (\u00a369, ID 116709). The plugin exposes\n       window.RJ24_CFG = { ajaxUrl, nonce, productId, cartUrl }.\n\n       Flow: pick beds (multi) -> singer -> per-jingle lyrics ->\n       format\/extras -> review -> Add to cart. Each jingle is POSTed\n       individually to rj24_add_jingle (one call = one cart line, with\n       full detail attached as cart_item_data). The loop verifies the\n       returned cart_count grows after each add, then redirects to cart.\n\n       Pricing: bundle by jingle count (1=\u00a369, 2=\u00a3108, 3=\u00a3159, 4=\u00a3199, then \u00a349 each),\n       ENFORCED SERVER-SIDE by the plugin across the whole cart, with\n       singer (+\u00a325) and extras (+\u00a315) premiums on top, and Kiss bed\n       discounts (-\u00a310). The wizard shows estimates that match the plugin.\n       Full Station Package (\u00a3199) is one line item.\n\n       No TMEPO, no wc-ajax, no iframe, no native-redirect. The reliable\n       plugin endpoint is the only cart path.\n\n     NOTE: fillTMEPO\/resetTMEPO remain defined but UNUSED (legacy, harmless).\n     ========================================================== -->\n\n<style>\n\/* ===== KILL ALL REMAINING WOOCOMMERCE CHROME =====\n   NOTE: We do NOT hide .woocommerce-tabs or #tab-description because\n   the sung wizard's Raw HTML block lives INSIDE the description tab\n   on the sung product page. Hiding those would hide the wizard. *\/\n.single-product .product-images,\n.single-product .images,\n.single-product .woocommerce-product-gallery,\n.single-product div.product .images,\n.single-product .product .product_meta,\n.single-product .product-category,\n.single-product .product-on-sale,\n.single-product .woocommerce-product-details__short-description {\n  display: none !important;\n}\n\n\/* Force the description tab to be visible (since our wizard lives in it) *\/\nhtml body .single-product #tab-description,\nhtml body .single-product .panel.entry-content,\nhtml body .single-product .woocommerce-tabs,\nhtml body .single-product .wc-tabs-wrapper {\n  display: block !important;\n  visibility: visible !important;\n  opacity: 1 !important;\n  width: 100% !important;\n  height: auto !important;\n  min-height: 0 !important;\n  max-height: none !important;\n  overflow: visible !important;\n  position: static !important;\n}\n\n\/* Hide the tab navigation row so users don't see the \"Description\" tab label *\/\nhtml body .single-product .wc-tabs,\nhtml body .single-product ul.tabs,\nhtml body .single-product .tabs.wc-tabs,\nhtml body .single-product .qode-shortcodes-product-tabs > ul,\nhtml body .single-product .product-tabs > .tabs {\n  display: none !important;\n}\n\n.single-product .product .row .col_one_third,\n.single-product .product .row .col_two_third {\n  width: 100% !important;\n  max-width: 100% !important;\n  flex: 1 1 100% !important;\n}\n\n\/* Hide PayPal\/share floating widgets *\/\n.single-product .ppcp-button-container,\n.single-product .ppc-button-wrapper,\n.single-product .paypal-button-container,\n.single-product [class*=\"ppcp-\"],\n.single-product [id*=\"paypal-button\"],\n.single-product .a2a_kit,\n.single-product .a2a_floating_style,\n.single-product .addtoany_share_save_container,\n.single-product .share-buttons,\n.single-product .social-share-floating,\n.single-product div[class*=\"share\"][class*=\"float\"],\n.single-product div[class*=\"float\"][class*=\"share\"] {\n  display: none !important;\n  visibility: hidden !important;\n  opacity: 0 !important;\n  pointer-events: none !important;\n}\n\n\/* Hide TMEPO (EXACT match to spoken's working CSS) *\/\n.single-product form.cart .tm-extra-product-options,\n.single-product form.cart .tm-extra-product-options-fields,\n.single-product form.cart .tc-extra-product-options,\n.single-product form.cart > .quantity,\n.single-product form.cart > .single_add_to_cart_button,\n.single-product form.cart > button[name=\"add-to-cart\"] {\n  position: absolute !important;\n  left: -99999px !important;\n  top: -99999px !important;\n  width: 1px !important;\n  height: 1px !important;\n  overflow: hidden !important;\n  opacity: 0 !important;\n  pointer-events: none !important;\n}\n\n\/* CRITICAL: TMEPO containers default to display:none until the description tab\n   becomes active. Since our wizard lives in the description tab (force-shown\n   above), we must ALSO force the TMEPO containers themselves to be visible\n   in the DOM so our JS can find inputs inside them.\n   We use 'block' (not 'none') so getComputedStyle reports them as renderable\n   even though they're positioned offscreen. *\/\nhtml body .single-product #tm-extra-product-options,\nhtml body .single-product #tm-extra-product-options-fields,\nhtml body .single-product .tm-extra-product-options,\nhtml body .single-product .tm-extra-product-options-fields,\nhtml body .single-product .tc-extra-product-options,\nhtml body .single-product .tc-extra-product-options-inner {\n  display: block !important;\n  visibility: visible !important;\n}\n\n\/* Force form.cart visible too, in case the theme hides it inside description tab *\/\nhtml body .single-product form.cart,\nhtml body .single-product form.cart.myadc {\n  display: block !important;\n  visibility: visible !important;\n}\n\n.single-product form.cart {\n  position: static !important;\n  width: 100% !important;\n  height: auto !important;\n  max-height: none !important;\n  overflow: visible !important;\n  opacity: 1 !important;\n  padding: 0 !important;\n  background: transparent !important;\n  box-shadow: none !important;\n  display: block !important;\n  margin: 0 !important;\n}\n\n.single-product .product_title {\n  display: none !important;\n}\n\n\/* ===== WIZARD STYLES ===== *\/\n\/* Force the wizard container to have proper dimensions regardless of\n   parent WPBakery\/Bridge theme wrapping. Some Bridge wrappers render\n   the Raw HTML block inside a flex\/grid that collapses to 0 width. *\/\nhtml body #rj-sung-wizard.rjsw {\n  display: block !important;\n  width: 100% !important;\n  min-width: 100% !important;\n  min-height: 600px !important;\n  position: relative !important;\n  clear: both !important;\n  float: none !important;\n  visibility: visible !important;\n  opacity: 1 !important;\n}\n\n.rjsw {\n  --rj-pink: #EC4899;\n  --rj-pink-dark: #DB2777;\n  --rj-pink-light: #FCE7F3;\n  --rj-pink-lighter: #FDF2F8;\n  --rj-purple: #8B5CF6;\n  --rj-gold: #F59E0B;\n  --rj-gold-light: #FEF3C7;\n  --rj-ink: #1E293B;\n  --rj-text: #475569;\n  --rj-muted: #94A3B8;\n  --rj-line: #E2E8F0;\n  --rj-bg: #FAFAFB;\n  --rj-card: #FFFFFF;\n  --rj-green: #10B981;\n  --rj-green-bg: #D1FAE5;\n  --rj-red: #DC2626;\n  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n  color: var(--rj-ink);\n  max-width: 1080px;\n  margin: 0 auto;\n  padding: 0 16px 40px;\n  -webkit-font-smoothing: antialiased;\n  line-height: 1.5;\n  \/* RJ24-EDIT (Nov 2026): sung-audio page has ~75px of unexplained gap above\n     the wizard from theme chrome. Pull the wizard up tight against the nav. *\/\n  --rjsw-headpad: 0px; padding-top: var(--rjsw-headpad);\n  margin-top: -60px;\n}\n@media (max-width: 1000px) { .rjsw { --rjsw-headpad: 0px; margin-top: -50px; } }\n@media (max-width: 600px) { .rjsw { --rjsw-headpad: 0px; margin-top: -40px; } }\n@import url('https:\/\/fonts.googleapis.com\/css2?family=Anton&display=swap');\n.rjsw * { box-sizing: border-box; }\n\n\/* HERO *\/\n.rjsw-hero{ border-radius:24px; overflow:hidden; background-color:#160F1F;\n  background-image:radial-gradient(120% 140% at 12% 18%, rgba(236,72,153,0.55) 0%, rgba(236,72,153,0) 55%),radial-gradient(120% 140% at 92% 10%, rgba(245,158,11,0.30) 0%, rgba(245,158,11,0) 50%),linear-gradient(135deg,#241733 0%,#0F0A18 100%);\n  padding:46px 32px; text-align:center; margin:6px 0 22px; position:relative; isolation:isolate; }\n.rjsw-hero::after{ content:\"\"; position:absolute; inset:0; background-image:radial-gradient(rgba(255,255,255,0.05) 1px,transparent 1px); background-size:22px 22px; opacity:.5; z-index:-1; }\n.rjsw-hero-eyebrow{ display:inline-block; background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.25); color:#fff; padding:6px 16px; border-radius:999px; font-size:11px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; margin-bottom:16px; }\n.rjsw-hero h1{ font-family:'Anton',sans-serif; text-transform:uppercase; font-size:clamp(38px,7vw,72px); line-height:.92; color:#fff; margin:0; text-shadow:0 6px 40px rgba(236,72,153,.45); }\n.rjsw-hero h1 .pop{ background:linear-gradient(180deg,#FBCFE8 0%,#EC4899 60%,#DB2777 100%); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }\n.rjsw-hero p{ color:rgba(255,255,255,0.82); font-size:15px; margin:14px auto 0; max-width:540px; }\n\n\/* BUNDLE PROMO \u2014 bold version *\/\n.rjsw-promo {\n  background: linear-gradient(135deg, var(--rj-pink) 0%, var(--rj-pink-dark) 100%);\n  border-radius: 16px;\n  padding: 18px 24px; margin: 0 auto 26px;\n  display: flex; flex-direction: column; align-items: center; gap: 12px;\n  text-align: center;\n  box-shadow: 0 12px 30px rgba(236,72,153,0.3);\n}\n\/* The ready-made Full Station Packages were withdrawn in Sept 2026. This strip\n   used to be a bare price list; it now has to carry the argument that replaced\n   them \u2014 build any combination you like, and it gets cheaper as you add. *\/\n.rjsw-promo-title{ font-family:'Anton',sans-serif; font-size:clamp(20px,3vw,28px); color:#fff;\n  text-transform:uppercase; letter-spacing:.02em; line-height:1.1; margin:0; }\n.rjsw-promo-line{ color:rgba(255,255,255,0.92); font-size:14px; line-height:1.55; max-width:660px; margin:0; }\n.rjsw-promo-line b{ color:#fff; }\n.rjsw-promo-tier .e{ font-size:11px; font-weight:600; opacity:.85; margin-top:1px; }\n.rjsw-promo-label{ display:flex; align-items:center; gap:8px; font-family:'Anton',sans-serif; font-size:18px; color:#fff; text-transform:uppercase; letter-spacing:.02em; }\n.rjsw-promo-star { color: #FBBF24; flex-shrink: 0; font-size: 20px; }\n.rjsw-promo-tiers{ display:flex; gap:10px; flex-wrap:wrap; justify-content:center; }\n.rjsw-promo-tier{ display:flex; flex-direction:column; align-items:center; line-height:1.15; background:rgba(255,255,255,0.16); border:1px solid rgba(255,255,255,0.3); border-radius:12px; padding:8px 14px; color:#fff; min-width:74px; }\n.rjsw-promo-tier .q{ font-size:11px; font-weight:600; opacity:.85; text-transform:uppercase; letter-spacing:.04em; }\n.rjsw-promo-tier .p{ font-size:17px; font-weight:800; }\n\n\/* MONTHLY PROMOS row *\/\n\/* ONE COLUMN: the premium promo was withdrawn (Sept 2026) and a 1fr 1fr grid\n   left the surviving offer stranded at half width. *\/\n.rjsw-monthly{ display:grid; grid-template-columns:1fr; gap:14px; margin:0 0 26px; }\n@media (max-width:640px){ .rjsw-monthly{ grid-template-columns:1fr; } }\n.rjsw-mpromo{ display:flex; align-items:center; gap:16px; text-decoration:none; border:2px solid var(--rj-line); border-radius:16px; padding:18px 20px; transition:border-color .15s, transform .12s, box-shadow .15s; background:#fff; }\n.rjsw-mpromo:hover{ border-color:var(--rj-pink); transform:translateY(-3px); box-shadow:0 14px 30px rgba(236,72,153,.16); }\n.rjsw-mpromo.prem{ background:linear-gradient(180deg,var(--rj-gold-light) 0%,#fff 60%); border-color:var(--rj-gold); }\n.rjsw-mpromo-badge{ font-family:'Anton',sans-serif; font-size:24px; color:var(--rj-pink-dark); line-height:1; flex:none; }\n.rjsw-mpromo.prem .rjsw-mpromo-badge{ color:var(--rj-gold); }\n.rjsw-mpromo-body{ flex:1; }\n.rjsw-mpromo-tag{ font-size:10px; font-weight:800; letter-spacing:.1em; text-transform:uppercase; color:var(--rj-muted); }\n.rjsw-mpromo-name{ font-size:16px; font-weight:800; color:var(--rj-ink); margin:2px 0; }\n.rjsw-mpromo-desc{ font-size:12.5px; color:var(--rj-text); line-height:1.45; }\n.rjsw-mpromo-go{ flex:none; font-weight:800; font-size:13px; color:#fff; background:var(--rj-pink); border-radius:999px; padding:9px 16px; white-space:nowrap; }\n.rjsw-mpromo.prem .rjsw-mpromo-go{ background:var(--rj-gold); }\n\n\/* ONE COLUMN since the Full Station Package mode was removed (Sept 2026) \u2014\n   packages are sold from the individual product page. A 1fr 1fr grid left the\n   remaining card sitting at half width looking like something had failed. *\/\n.rjsw-mode-picker {\n  display: grid; grid-template-columns: 1fr; gap: 16px;\n  margin-bottom: 24px;\n}\n@media (max-width: 720px) { .rjsw-mode-picker { grid-template-columns: 1fr; } }\n.rjsw-mode-card {\n  background: white; border: 2px solid var(--rj-line);\n  border-radius: 18px; padding: 28px 24px;\n  text-align: center; cursor: pointer; transition: all 0.2s;\n  position: relative;\n}\n.rjsw-mode-card:hover {\n  border-color: var(--rj-pink); transform: translateY(-3px);\n  box-shadow: 0 12px 30px rgba(236, 72, 153, 0.18);\n}\n.rjsw-mode-card.featured {\n  border: 2px solid var(--rj-gold);\n  background: linear-gradient(180deg, var(--rj-gold-light) 0%, white 50%);\n}\n.rjsw-mode-card.featured:hover { border-color: var(--rj-pink); }\n.rjsw-mode-badge {\n  position: absolute; top: -12px; left: 50%; transform: translateX(-50%);\n  background: var(--rj-gold); color: white;\n  padding: 4px 14px; border-radius: 999px;\n  font-size: 11px; font-weight: 800;\n  text-transform: uppercase; letter-spacing: 0.06em;\n  box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4); white-space: nowrap;\n}\n.rjsw-mode-icon {\n  width: 64px; height: 64px; margin: 0 auto 12px;\n  border-radius: 50%; background: var(--rj-pink-lighter);\n  display: flex; align-items: center; justify-content: center;\n  font-size: 28px;\n}\n.rjsw-mode-card.featured .rjsw-mode-icon { background: var(--rj-gold-light); }\n.rjsw-mode-title {\n  font-size: 22px; font-weight: 800; color: var(--rj-ink);\n  margin: 0 0 6px; letter-spacing: -0.02em;\n}\n.rjsw-mode-desc { font-size: 14px; color: var(--rj-text); margin: 0 0 14px; }\n.rjsw-mode-price {\n  font-size: 16px; font-weight: 800; color: var(--rj-pink-dark);\n  margin: 0 0 18px;\n}\n.rjsw-mode-btn {\n  background: var(--rj-pink); color: white; border: none;\n  padding: 12px 24px; border-radius: 999px;\n  font-weight: 700; font-size: 14px; cursor: pointer;\n  font-family: inherit; transition: all 0.15s;\n  box-shadow: 0 2px 8px rgba(236, 72, 153, 0.25);\n}\n.rjsw-mode-btn:hover { background: var(--rj-pink-dark); transform: translateY(-1px); }\n\n.rjsw-screen { display: none; animation: rjswFade 0.3s ease; padding-bottom: 96px; }\n.rjsw-screen.active { display: block; }\n\/* v3.9.5: opacity-only fade. translateY removed because any transform on\n   .rjsw-screen creates a new containing block, which breaks position:fixed\n   for the .rjsw-sticky-cta inside it (the bar ends up positioned relative\n   to the tall screen container instead of the viewport). *\/\n@keyframes rjswFade {\n  from { opacity: 0; }\n  to   { opacity: 1; }\n}\n.rjsw-screen-head {\n  display: flex; justify-content: space-between; align-items: center;\n  gap: 12px; margin-bottom: 18px; flex-wrap: wrap;\n}\n.rjsw-screen-title {\n  font-size: 24px; font-weight: 800; color: var(--rj-ink);\n  letter-spacing: -0.02em; margin: 0;\n}\n.rjsw-screen-sub { font-size: 14px; color: var(--rj-text); margin: 0; }\n\n\/* v3.9.2: fixed-bottom sticky CTA matching \/product\/commercial-production\/.\n   Only the active screen's bar shows; .rjsw-screen has padding-bottom so\n   content doesn't sit under the bar. *\/\n.rjsw-sticky-cta {\n  position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999;\n  background: white; border-top: 2px solid var(--rj-pink);\n  padding: 14px 20px;\n  margin: 0;\n  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;\n  box-shadow: 0 -6px 20px rgba(236, 72, 153, 0.18);\n  \/* v3.9.4: footer-aware lift uses transform:translateY set by JS *\/\n  transform: translateY(0);\n  transition: transform 0.08s linear;\n  will-change: transform;\n}\n.rjsw-screen:not(.active) .rjsw-sticky-cta { display: none; }\n.rjsw-sticky-cta-info { flex: 1; min-width: 200px; }\n.rjsw-sticky-cta-label {\n  font-size: 10px; color: var(--rj-muted);\n  text-transform: uppercase; letter-spacing: 0.08em;\n  font-weight: 700; margin-bottom: 2px;\n}\n.rjsw-sticky-cta-total {\n  font-size: 22px; font-weight: 800;\n  color: var(--rj-pink-dark); line-height: 1.1;\n}\n.rjsw-sticky-cta-save {\n  font-size: 12px; color: var(--rj-green); font-weight: 700; margin-top: 2px;\n}\n.rjsw-sticky-cta-back {\n  background: white; color: var(--rj-text);\n  border: 2px solid var(--rj-line);\n  padding: 10px 18px; border-radius: 999px;\n  font-weight: 700; font-size: 13px; cursor: pointer;\n  font-family: inherit; transition: all 0.15s; white-space: nowrap;\n}\n.rjsw-sticky-cta-back:hover { border-color: var(--rj-text); color: var(--rj-ink); }\n.rjsw-sticky-cta-btn {\n  background: var(--rj-pink); color: white; border: none;\n  padding: 12px 24px; border-radius: 999px;\n  font-weight: 700; font-size: 15px; cursor: pointer;\n  font-family: inherit; transition: all 0.15s;\n  box-shadow: 0 2px 8px rgba(236, 72, 153, 0.3);\n  white-space: nowrap;\n}\n.rjsw-sticky-cta-btn:hover:not(:disabled) {\n  background: var(--rj-pink-dark); transform: translateY(-1px);\n}\n.rjsw-sticky-cta-btn:disabled { opacity: 0.45; cursor: not-allowed; }\n@media (max-width: 540px) {\n  .rjsw-sticky-cta { padding: 12px 14px; gap: 10px; }\n  .rjsw-sticky-cta-total { font-size: 18px; }\n  .rjsw-sticky-cta-btn { padding: 10px 18px; font-size: 13px; }\n  .rjsw-sticky-cta-back { padding: 8px 14px; font-size: 12px; }\n}\n\n.rjsw-pack-bar {\n  background: var(--rj-bg); border: 1px dashed var(--rj-line);\n  border-radius: 12px; padding: 12px 16px;\n  margin-bottom: 16px;\n  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;\n  min-height: 50px;\n}\n.rjsw-pack-bar.empty {\n  background: white; color: var(--rj-muted);\n  justify-content: center; font-size: 13px;\n}\n.rjsw-pack-bar-label {\n  font-size: 12px; font-weight: 700; color: var(--rj-ink); margin-right: 4px;\n}\n.rjsw-pack-tier-msg {\n  font-size: 11px; color: var(--rj-pink-dark); font-weight: 700;\n  margin-left: auto; padding: 3px 10px;\n  background: var(--rj-pink-lighter); border-radius: 999px;\n}\n.rjsw-pack-tier-msg.unlocked { background: var(--rj-green-bg); color: #047857; }\n.rjsw-pack-chip {\n  background: white; border: 1px solid var(--rj-pink);\n  padding: 4px 6px 4px 10px; border-radius: 999px;\n  font-size: 11px; font-weight: 600; color: var(--rj-ink);\n  display: inline-flex; align-items: center; gap: 4px;\n}\n.rjsw-pack-chip-remove {\n  width: 16px; height: 16px; border-radius: 50%;\n  background: var(--rj-pink-lighter); color: var(--rj-pink-dark);\n  border: none; cursor: pointer;\n  display: flex; align-items: center; justify-content: center;\n  font-size: 9px; font-weight: 700; font-family: inherit;\n  padding: 0; transition: all 0.15s;\n}\n.rjsw-pack-chip-remove:hover { background: var(--rj-pink); color: white; }\n\n.rjsw-filters {\n  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;\n  margin-bottom: 16px;\n}\n.rjsw-filter-label {\n  font-size: 12px; font-weight: 700; color: var(--rj-muted);\n  text-transform: uppercase; letter-spacing: 0.06em; margin-right: 4px;\n}\n.rjsw-filter-pill {\n  background: white; border: 2px solid var(--rj-line);\n  padding: 6px 14px; border-radius: 999px;\n  font-weight: 700; font-size: 12px;\n  cursor: pointer; color: var(--rj-text);\n  font-family: inherit; transition: all 0.15s;\n}\n.rjsw-filter-pill:hover { border-color: var(--rj-pink); color: var(--rj-pink-dark); }\n.rjsw-filter-pill.active {\n  background: var(--rj-pink); color: white; border-color: var(--rj-pink);\n}\n\n.rjsw-bed-grid {\n  display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;\n}\n@media (max-width: 900px) { .rjsw-bed-grid { grid-template-columns: repeat(4, 1fr); } }\n@media (max-width: 720px) { .rjsw-bed-grid { grid-template-columns: repeat(3, 1fr); } }\n@media (max-width: 480px) { .rjsw-bed-grid { grid-template-columns: repeat(2, 1fr); } }\n.rjsw-bed-card {\n  background: white; border: 2px solid var(--rj-line);\n  border-radius: 12px; padding: 12px;\n  text-align: center; transition: all 0.15s;\n  position: relative;\n  display: flex; flex-direction: column; align-items: center; gap: 6px;\n  min-height: 110px;\n}\n.rjsw-bed-card:hover {\n  border-color: var(--rj-pink);\n  box-shadow: 0 4px 12px rgba(236, 72, 153, 0.1);\n}\n.rjsw-bed-card.in-pack { background: var(--rj-pink-lighter); border-color: var(--rj-pink); }\n.rjsw-bed-type-tag {\n  position: absolute; top: 6px; left: 6px;\n  font-size: 9px; font-weight: 700; color: var(--rj-muted);\n  background: var(--rj-bg); padding: 2px 6px;\n  border-radius: 999px; text-transform: uppercase; letter-spacing: 0.04em;\n}\n.rjsw-bed-discount {\n  position: absolute; top: 6px; right: 6px;\n  font-size: 9px; font-weight: 800;\n  color: #047857; background: var(--rj-green-bg);\n  padding: 2px 6px; border-radius: 999px;\n}\n.rjsw-bed-card-name {\n  font-weight: 700; color: var(--rj-ink); font-size: 13px;\n  margin-top: 18px; line-height: 1.3;\n}\n.rjsw-bed-actions { display: flex; gap: 6px; margin-top: auto; }\n.rjsw-bed-btn {\n  width: 32px; height: 32px; border-radius: 50%;\n  border: none; cursor: pointer;\n  display: flex; align-items: center; justify-content: center;\n  font-size: 12px; font-family: inherit; font-weight: 700;\n  padding: 0; transition: all 0.15s;\n}\n.rjsw-bed-btn.play {\n  background: white; border: 1px solid var(--rj-line); color: var(--rj-text);\n}\n.rjsw-bed-btn.play:hover { border-color: var(--rj-pink); color: var(--rj-pink-dark); }\n.rjsw-bed-btn.play.playing { background: var(--rj-purple); color: white; border-color: var(--rj-purple); }\n.rjsw-bed-btn.add { background: var(--rj-pink); color: white; }\n.rjsw-bed-btn.add:hover { background: var(--rj-pink-dark); transform: scale(1.05); }\n.rjsw-bed-btn.remove {\n  background: var(--rj-bg); color: var(--rj-pink-dark);\n  border: 1px solid var(--rj-pink);\n}\n.rjsw-bed-btn.remove:hover { background: var(--rj-pink); color: white; }\n.rjsw-bed-count {\n  position: absolute; bottom: 8px; right: 8px;\n  background: var(--rj-pink); color: white;\n  width: 22px; height: 22px; border-radius: 50%;\n  display: flex; align-items: center; justify-content: center;\n  font-size: 11px; font-weight: 800;\n  box-shadow: 0 2px 4px rgba(236, 72, 153, 0.3);\n}\n\n.rjsw-pkg-grid {\n  display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;\n}\n@media (max-width: 900px) { .rjsw-pkg-grid { grid-template-columns: repeat(2, 1fr); } }\n@media (max-width: 540px) { .rjsw-pkg-grid { grid-template-columns: 1fr; } }\n.rjsw-pkg-card {\n  background: white; border: 2px solid var(--rj-line);\n  border-radius: 12px; padding: 14px;\n  cursor: pointer; transition: all 0.15s;\n  position: relative;\n}\n.rjsw-pkg-card:hover { border-color: var(--rj-pink); }\n.rjsw-pkg-card.selected {\n  border-color: var(--rj-pink); background: var(--rj-pink-lighter);\n}\n.rjsw-pkg-card.selected::after {\n  content: '\u2713'; position: absolute; top: 8px; right: 8px;\n  width: 24px; height: 24px; background: var(--rj-pink); color: white;\n  border-radius: 50%; display: flex; align-items: center;\n  justify-content: center; font-weight: 700; font-size: 13px;\n}\n.rjsw-pkg-thumb {\n  width: 100%; aspect-ratio: 3\/2;\n  background: var(--rj-bg); border-radius: 8px;\n  object-fit: cover; margin-bottom: 10px;\n}\n.rjsw-pkg-name {\n  font-weight: 700; color: var(--rj-ink); font-size: 14px;\n  margin-bottom: 8px; line-height: 1.3;\n}\n.rjsw-pkg-listen {\n  display: inline-flex; align-items: center; gap: 5px;\n  background: white; border: 1px solid var(--rj-line);\n  padding: 5px 12px; border-radius: 999px; font-size: 11px;\n  cursor: pointer; color: var(--rj-text); font-family: inherit;\n  font-weight: 600; transition: all 0.15s;\n}\n.rjsw-pkg-listen:hover { border-color: var(--rj-pink); color: var(--rj-pink-dark); }\n.rjsw-pkg-listen.playing { background: var(--rj-purple); color: white; border-color: var(--rj-purple); }\n\n.rjsw-singers {\n  display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;\n}\n@media (max-width: 640px) { .rjsw-singers { grid-template-columns: 1fr; } }\n.rjsw-singer {\n  background: white; border: 2px solid var(--rj-line);\n  border-radius: 14px; padding: 18px 14px;\n  text-align: center; cursor: pointer; transition: all 0.2s;\n  position: relative;\n}\n.rjsw-singer:hover {\n  border-color: var(--rj-pink); transform: translateY(-2px);\n  box-shadow: 0 6px 16px rgba(236, 72, 153, 0.12);\n}\n.rjsw-singer.selected {\n  border-color: var(--rj-pink);\n  background: linear-gradient(180deg, white 0%, var(--rj-pink-lighter) 100%);\n}\n.rjsw-singer.selected::after {\n  content: '\u2713'; position: absolute; top: 8px; right: 8px;\n  width: 22px; height: 22px; background: var(--rj-pink); color: white;\n  border-radius: 50%; display: flex; align-items: center;\n  justify-content: center; font-weight: 700; font-size: 12px;\n}\n.rjsw-singer-avatar-wrap {\n  position: relative; width: 84px; height: 84px; margin: 4px auto 12px;\n}\n.rjsw-singer-avatar {\n  width: 100%; height: 100%; border-radius: 50%;\n  object-fit: cover; background: var(--rj-pink-light); display: block;\n}\n.rjsw-singer-play {\n  position: absolute; bottom: -2px; right: -2px;\n  width: 28px; height: 28px; background: var(--rj-pink); color: white;\n  border: 3px solid white; border-radius: 50%; cursor: pointer;\n  display: flex; align-items: center; justify-content: center;\n  font-size: 11px; transition: all 0.15s; font-family: inherit;\n  padding: 0; box-shadow: 0 2px 8px rgba(236, 72, 153, 0.4);\n}\n.rjsw-singer-play:hover { background: var(--rj-pink-dark); transform: scale(1.1); }\n.rjsw-singer-play.playing { background: var(--rj-purple); }\n.rjsw-singer-name { font-size: 16px; font-weight: 700; color: var(--rj-ink); margin: 0 0 4px; }\n.rjsw-singer-creds {\n  font-size: 11px; color: var(--rj-muted); line-height: 1.4;\n  margin-bottom: 8px; min-height: 30px;\n}\n.rjsw-singer-delivery {\n  font-size: 10px; color: var(--rj-text); font-weight: 600;\n  text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px;\n}\n.rjsw-singer-price { font-weight: 800; color: var(--rj-pink-dark); font-size: 14px; }\n.rjsw-singer-price.free { color: var(--rj-green); }\n\n.rjsw-lyrics-block {\n  background: white; border: 1px solid var(--rj-line);\n  border-radius: 12px; padding: 14px; margin-bottom: 12px;\n}\n.rjsw-lyrics-block-head {\n  display: flex; align-items: center; gap: 10px;\n  margin-bottom: 10px; font-size: 13px;\n}\n.rjsw-lyrics-block-num {\n  width: 22px; height: 22px; border-radius: 50%;\n  background: var(--rj-pink); color: white;\n  display: flex; align-items: center; justify-content: center;\n  font-weight: 700; font-size: 11px; flex-shrink: 0;\n}\n.rjsw-lyrics-block-name { font-weight: 700; color: var(--rj-ink); flex: 1; }\n.rjsw-lyrics-block-type {\n  font-size: 11px; color: var(--rj-muted); font-weight: 600;\n  text-transform: uppercase; letter-spacing: 0.04em;\n}\n.rjsw-textarea {\n  width: 100%; padding: 12px 14px; border: 2px solid var(--rj-line);\n  border-radius: 10px; font-family: inherit;\n  font-size: 14px; min-height: 70px; resize: vertical;\n  transition: border-color 0.15s; background: white;\n  color: var(--rj-ink); line-height: 1.5;\n}\n.rjsw-textarea:focus {\n  outline: none; border-color: var(--rj-pink);\n  box-shadow: 0 0 0 4px rgba(236, 72, 153, 0.1);\n}\n.rjsw-hint { font-size: 12px; color: var(--rj-muted); margin: 4px 0 0; }\n\n.rjsw-fmt-bar {\n  background: white; border: 1px solid var(--rj-line);\n  border-radius: 12px; padding: 14px 18px; margin-bottom: 14px;\n}\n.rjsw-fmt-head {\n  font-size: 14px; font-weight: 700; color: var(--rj-ink); margin-bottom: 8px;\n}\n.rjsw-fmt-options { display: flex; gap: 6px; flex-wrap: wrap; }\n.rjsw-fmt-opt {\n  background: white; border: 1px solid var(--rj-line);\n  padding: 7px 16px; border-radius: 999px;\n  font-size: 13px; font-weight: 700; cursor: pointer;\n  color: var(--rj-text); transition: all 0.15s; font-family: inherit;\n}\n.rjsw-fmt-opt:hover { border-color: var(--rj-pink); color: var(--rj-pink-dark); }\n.rjsw-fmt-opt.selected {\n  background: var(--rj-pink); color: white; border-color: var(--rj-pink);\n}\n.rjsw-extras {\n  display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 18px;\n}\n@media (max-width: 640px) { .rjsw-extras { grid-template-columns: 1fr; } }\n.rjsw-extra {\n  background: white; border: 2px solid var(--rj-line);\n  border-radius: 10px; padding: 12px 14px;\n  cursor: pointer; transition: all 0.15s;\n  display: flex; align-items: center; gap: 10px;\n}\n.rjsw-extra:hover { border-color: var(--rj-pink); }\n.rjsw-extra.selected {\n  border-color: var(--rj-pink); background: var(--rj-pink-lighter);\n}\n.rjsw-extra-check {\n  width: 20px; height: 20px; border-radius: 5px;\n  border: 2px solid var(--rj-line); flex-shrink: 0;\n  display: flex; align-items: center; justify-content: center;\n  background: white; transition: all 0.15s; font-size: 11px;\n}\n.rjsw-extra.selected .rjsw-extra-check {\n  background: var(--rj-pink); border-color: var(--rj-pink);\n  color: white; font-weight: 700;\n}\n.rjsw-extra-info { flex: 1; min-width: 0; }\n.rjsw-extra-name { font-weight: 700; color: var(--rj-ink); font-size: 13px; }\n.rjsw-extra-desc { font-size: 11px; color: var(--rj-text); line-height: 1.4; }\n.rjsw-extra-price { font-weight: 800; color: var(--rj-pink-dark); font-size: 13px; flex-shrink: 0; }\n\n.rjsw-review-card {\n  background: white; border: 1px solid var(--rj-line);\n  border-radius: 12px; padding: 18px;\n}\n.rjsw-review-row {\n  display: flex; justify-content: space-between; align-items: flex-start;\n  padding: 10px 0; border-bottom: 1px solid var(--rj-line); gap: 12px;\n}\n.rjsw-review-row:first-child { padding-top: 0; }\n.rjsw-review-row:last-child { border-bottom: none; }\n.rjsw-review-label {\n  color: var(--rj-muted); font-size: 11px; font-weight: 700;\n  text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0;\n}\n.rjsw-review-value {\n  color: var(--rj-ink); font-weight: 600; text-align: right;\n  max-width: 65%; word-wrap: break-word; font-size: 13px;\n}\n.rjsw-review-value-muted { color: var(--rj-muted); font-weight: 400; }\n.rjsw-review-tier {\n  background: var(--rj-green-bg); color: #047857;\n  border-radius: 10px; padding: 12px 16px;\n  font-size: 13px; font-weight: 700;\n  margin: 14px 0; text-align: center;\n}\n\n.rjsw-modal-overlay {\n  position: fixed; top: 0; left: 0; right: 0; bottom: 0;\n  background: rgba(15, 23, 42, 0.7); display: none;\n  align-items: center; justify-content: center;\n  z-index: 100000; padding: 16px;\n}\n.rjsw-modal-overlay.open { display: flex; animation: rjswFadeIn 0.2s ease; }\n@keyframes rjswFadeIn { from { opacity: 0; } to { opacity: 1; } }\n.rjsw-modal {\n  background: white; border-radius: 18px;\n  padding: 28px; max-width: 500px; width: 100%;\n  max-height: 90vh; overflow-y: auto;\n  animation: rjswSlideUp 0.3s ease;\n}\n@keyframes rjswSlideUp {\n  from { opacity: 0; transform: translateY(20px); }\n  to { opacity: 1; transform: translateY(0); }\n}\n.rjsw-modal-loader {\n  width: 36px; height: 36px;\n  border: 3px solid var(--rj-line);\n  border-top-color: var(--rj-pink);\n  border-radius: 50%; margin: 0 auto 16px;\n  animation: rjswSpin 0.8s linear infinite;\n}\n@keyframes rjswSpin { to { transform: rotate(360deg); } }\n.rjsw-modal h3 {\n  margin: 0 0 6px; font-size: 22px; color: var(--rj-ink);\n  text-align: center; font-weight: 800;\n}\n.rjsw-modal-sub {\n  color: var(--rj-text); margin: 0 0 18px;\n  font-size: 14px; text-align: center;\n}\n.rjsw-modal-progress {\n  background: var(--rj-bg); border-radius: 10px;\n  padding: 10px; font-size: 13px; color: var(--rj-text);\n  text-align: center; margin: 12px 0;\n}\n\nhtml body .rjsw-toast {\n  position: fixed !important; top: 24px !important; left: 50% !important;\n  transform: translateX(-50%) translateY(-200%) !important;\n  background: #EC4899 !important; color: white !important;\n  padding: 14px 28px !important; border-radius: 999px !important;\n  font-weight: 700 !important; z-index: 100001 !important;\n  transition: transform 0.3s !important;\n  box-shadow: 0 10px 30px rgba(236, 72, 153, 0.5) !important;\n  max-width: 90vw !important; text-align: center !important; font-size: 14px !important;\n}\nhtml body .rjsw-toast.show { transform: translateX(-50%) translateY(0) !important; }\nhtml body .rjsw-toast.success {\n  background: #10B981 !important;\n  box-shadow: 0 10px 30px rgba(16, 185, 129, 0.5) !important;\n}\nhtml body .rjsw-toast.error {\n  background: #DC2626 !important;\n  box-shadow: 0 10px 30px rgba(220, 38, 38, 0.5) !important;\n}\n\n\/* RJ24-EDIT (Nov 2026): trust panel + GBP note *\/\n.rjsw .rjsw-trust{\n  display:grid; grid-template-columns:repeat(5,1fr); gap:10px;\n  margin:0 0 18px; padding:14px;\n  background:#fff; border:1px solid var(--rj-line); border-radius:14px;\n}\n.rjsw .rjsw-trust-item{\n  display:flex; flex-direction:column; align-items:center; text-align:center;\n  padding:8px 6px; gap:4px;\n}\n.rjsw .rjsw-trust-ico{ font-size:22px; line-height:1; margin-bottom:2px; }\n.rjsw .rjsw-trust-h{ font-size:12.5px; font-weight:800; color:var(--rj-ink); letter-spacing:.01em; line-height:1.25; }\n.rjsw .rjsw-trust-d{ font-size:11px; color:var(--rj-text); line-height:1.4; }\n@media (max-width: 880px){\n  .rjsw .rjsw-trust{ grid-template-columns:repeat(2,1fr); }\n}\n@media (max-width: 480px){\n  .rjsw .rjsw-trust{ grid-template-columns:1fr; }\n}\n\n.rjsw .rjsw-gbp{\n  display:flex; align-items:center; gap:10px;\n  background:#FBF7F2; border:1px solid #F3E8DD; border-left:3px solid var(--rj-pink);\n  border-radius:10px; padding:10px 16px; margin:0 auto 22px;\n  max-width:560px; font-size:12.5px; line-height:1.5; color:var(--rj-text);\n}\n.rjsw .rjsw-gbp-ico{ font-size:15px; flex-shrink:0; }\n.rjsw .rjsw-gbp-text strong{ color:var(--rj-ink); font-weight:700; }\n\/* \/RJ24-EDIT *\/\n<\/style>\n\n<div class=\"rjsw\" id=\"rj-sung-wizard\">\n\n  <div class=\"rjsw-hero\">\n    <div class=\"rjsw-hero-eyebrow\">Sung jingles delivered within 48 hours &mdash; yours to use forever<\/div>\n    <h1>Sung Jingles<br><span class=\"pop\">that sound huge.<\/span><\/h1>\n    <p>Your station name, professionally sung and produced. Build your own pack from any beds you like &mdash; or take this month's featured bed for &pound;59.<\/p>\n  <\/div>\n\n  <!-- RJ24-EDIT (Nov 2026): \"What you get\" trust panel -->\n  <div class=\"rjsw-trust\">\n    <div class=\"rjsw-trust-item\"><span class=\"rjsw-trust-ico\">\ud83c\udfa4<\/span><span class=\"rjsw-trust-h\">Real singers, not AI<\/span><span class=\"rjsw-trust-d\">Sung live in a professional studio \u2014 never generated<\/span><\/div>\n    <div class=\"rjsw-trust-item\"><span class=\"rjsw-trust-ico\">\u23f1<\/span><span class=\"rjsw-trust-h\">From 48 hours<\/span><span class=\"rjsw-trust-d\">Rosie sings your station name in 48 hours<\/span><\/div>\n    <div class=\"rjsw-trust-item\"><span class=\"rjsw-trust-ico\">\ud83d\udcc2<\/span><span class=\"rjsw-trust-h\">MP3, WAV &amp; AIFF<\/span><span class=\"rjsw-trust-d\">Studio-quality files in your chosen format<\/span><\/div>\n    <div class=\"rjsw-trust-item\"><span class=\"rjsw-trust-ico\">\u221e<\/span><span class=\"rjsw-trust-h\">Royalty-free forever<\/span><span class=\"rjsw-trust-d\">Yours to broadcast as long as your station runs<\/span><\/div>\n    <div class=\"rjsw-trust-item\"><span class=\"rjsw-trust-ico\">\ud83c\udfb6<\/span><span class=\"rjsw-trust-h\">FREE acapella included<\/span><span class=\"rjsw-trust-d\">Every sung jingle comes with the acapella version too<\/span><\/div>\n  <\/div>\n  <!-- \/RJ24-EDIT -->\n\n  <div class=\"rjsw-promo\">\n    <p class=\"rjsw-promo-title\"><span class=\"rjsw-promo-star\">\u2605<\/span> No fixed packages. Build your own.<\/p>\n    <p class=\"rjsw-promo-line\">Pick <b>as many beds as you like<\/b> and mix them however you want &mdash; AC, CHR, Oldies, Epic, Kiss, all in one order. There is no set list to choose from and nothing you have to take that you did not want. <b>Every jingle you add makes every jingle cheaper.<\/b><\/p>\n    <span class=\"rjsw-promo-tiers\">\n      <span class=\"rjsw-promo-tier\"><span class=\"q\">1 jingle<\/span><span class=\"p\">\u00a369<\/span><span class=\"e\">\u00a369 each<\/span><\/span>\n      <span class=\"rjsw-promo-tier\"><span class=\"q\">2 jingles<\/span><span class=\"p\">\u00a3108<\/span><span class=\"e\">\u00a354 each<\/span><\/span>\n      <span class=\"rjsw-promo-tier\"><span class=\"q\">3 jingles<\/span><span class=\"p\">\u00a3159<\/span><span class=\"e\">\u00a353 each<\/span><\/span>\n      <span class=\"rjsw-promo-tier\"><span class=\"q\">4 jingles<\/span><span class=\"p\">\u00a3199<\/span><span class=\"e\">\u00a349.75 each<\/span><\/span>\n      <span class=\"rjsw-promo-tier\"><span class=\"q\">5 or more<\/span><span class=\"p\">\u00a349<\/span><span class=\"e\">each, no limit<\/span><\/span>\n    <\/span>\n  <\/div>\n\n  <!-- MONTHLY PROMOS \u2014 deliberately vague so they never need editing.\n       The two offer products live at fixed URLs; their price\/content changes\n       monthly but these cards just point to them. -->\n  <div class=\"rjsw-monthly\">\n    <a class=\"rjsw-mpromo\" href=\"\/product\/sung-jingle-offer\/\">\n      <span class=\"rjsw-mpromo-badge\">\u00a359<\/span>\n      <span class=\"rjsw-mpromo-body\">\n        <span class=\"rjsw-mpromo-tag\">This month's offer<\/span>\n        <span class=\"rjsw-mpromo-name\">Sung Jingle Special<\/span>\n        <span class=\"rjsw-mpromo-desc\">A bespoke sung jingle on this month's featured bed &mdash; our best-value sung deal.<\/span>\n      <\/span>\n      <span class=\"rjsw-mpromo-go\">View &rarr;<\/span>\n    <\/a>\n  <\/div>\n\n  <!-- RJ24-EDIT (Nov 2026): GBP currency note -->\n  <div class=\"rjsw-gbp\">\n    <span class=\"rjsw-gbp-ico\">\ud83d\udcb1<\/span>\n    <span class=\"rjsw-gbp-text\">Prices shown in <strong>GBP (\u00a3)<\/strong>. Not in the UK? Your local currency is calculated at checkout, at today's exchange rate.<\/span>\n  <\/div>\n  <!-- \/RJ24-EDIT -->\n\n  <div class=\"rjsw-screen\" id=\"rjsw-screen-mode\">\n    <div class=\"rjsw-mode-picker\">\n      <div class=\"rjsw-mode-card\" data-mode=\"custom\">\n        <div class=\"rjsw-mode-icon\">\ud83c\udfb5<\/div>\n        <h3 class=\"rjsw-mode-title\">Build a Custom Pack<\/h3>\n        <p class=\"rjsw-mode-desc\">Mix any beds from AC, CHR, Oldies or Acapella. Bundle discounts at 2\/3\/4 jingles.<\/p>\n        <div class=\"rjsw-mode-price\">from \u00a369<\/div>\n        <button type=\"button\" class=\"rjsw-mode-btn\">Start building \u2192<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"rjsw-screen active\" id=\"rjsw-screen-browse\">\n    <div class=\"rjsw-sticky-cta\">\n      <div class=\"rjsw-sticky-cta-info\">\n        <div class=\"rjsw-sticky-cta-label\">Pack total<\/div>\n        <div class=\"rjsw-sticky-cta-total\" id=\"rjsw-cta-total-browse\">\u00a30.00<\/div>\n        <div class=\"rjsw-sticky-cta-save\" id=\"rjsw-cta-save-browse\"><\/div>\n      <\/div>\n      <button type=\"button\" class=\"rjsw-sticky-cta-btn\" data-next=\"singer\" id=\"rjsw-cta-btn-browse\" disabled>Continue \u2192<\/button>\n    <\/div>\n    <div class=\"rjsw-screen-head\">\n      <div>\n        <h2 class=\"rjsw-screen-title\">Pick your music beds<\/h2>\n        <p class=\"rjsw-screen-sub\">Tap + to add. Mix any types. Bundle discount applies automatically.<\/p>\n      <\/div>\n    <\/div>\n    <div class=\"rjsw-pack-bar\" id=\"rjsw-pack-bar\"><\/div>\n    <div class=\"rjsw-filters\">\n      <span class=\"rjsw-filter-label\">Style:<\/span>\n      <button type=\"button\" class=\"rjsw-filter-pill active\" data-filter=\"all\">All<\/button>\n      <button type=\"button\" class=\"rjsw-filter-pill\" data-filter=\"ac\">AC \/ Hot AC<\/button>\n      <button type=\"button\" class=\"rjsw-filter-pill\" data-filter=\"chr\">CHR<\/button>\n      <button type=\"button\" class=\"rjsw-filter-pill\" data-filter=\"oldies\">Oldies, Epic &amp; Kiss<\/button>\n      <button type=\"button\" class=\"rjsw-filter-pill\" data-filter=\"acapella\">Acapella<\/button>\n    <\/div>\n    <div class=\"rjsw-bed-grid\" id=\"rjsw-bed-grid\"><\/div>\n  <\/div>\n\n  <div class=\"rjsw-screen\" id=\"rjsw-screen-singer\">\n    <div class=\"rjsw-sticky-cta\">\n      <button type=\"button\" class=\"rjsw-sticky-cta-back\" data-back=\"browse-or-package\">\u2190 Back<\/button>\n      <div class=\"rjsw-sticky-cta-info\">\n        <div class=\"rjsw-sticky-cta-label\">Total<\/div>\n        <div class=\"rjsw-sticky-cta-total\" id=\"rjsw-cta-total-singer\">\u00a30.00<\/div>\n        <div class=\"rjsw-sticky-cta-save\" id=\"rjsw-cta-save-singer\"><\/div>\n      <\/div>\n      <button type=\"button\" class=\"rjsw-sticky-cta-btn\" data-next=\"lyrics\" id=\"rjsw-cta-btn-singer\" disabled>Continue \u2192<\/button>\n    <\/div>\n    <div class=\"rjsw-screen-head\">\n      <div>\n        <h2 class=\"rjsw-screen-title\">Pick a singer<\/h2>\n        <p class=\"rjsw-screen-sub\" id=\"rjsw-singer-sub\">One singer for your whole pack.<\/p>\n      <\/div>\n    <\/div>\n    <div class=\"rjsw-singers\" id=\"rjsw-singers\"><\/div>\n  <\/div>\n\n  <div class=\"rjsw-screen\" id=\"rjsw-screen-lyrics\">\n    <div class=\"rjsw-sticky-cta\">\n      <button type=\"button\" class=\"rjsw-sticky-cta-back\" data-back=\"singer\">\u2190 Back<\/button>\n      <div class=\"rjsw-sticky-cta-info\">\n        <div class=\"rjsw-sticky-cta-label\">Total<\/div>\n        <div class=\"rjsw-sticky-cta-total\" id=\"rjsw-cta-total-lyrics\">\u00a30.00<\/div>\n        <div class=\"rjsw-sticky-cta-save\" id=\"rjsw-cta-save-lyrics\"><\/div>\n      <\/div>\n      <button type=\"button\" class=\"rjsw-sticky-cta-btn\" data-next=\"fmt\" id=\"rjsw-cta-btn-lyrics\" disabled>Continue \u2192<\/button>\n    <\/div>\n    <div class=\"rjsw-screen-head\">\n      <div>\n        <h2 class=\"rjsw-screen-title\">Lyrics<\/h2>\n        <p class=\"rjsw-screen-sub\" id=\"rjsw-lyrics-sub\">Keep each jingle to 5\u20138 words for best quality.<\/p>\n      <\/div>\n    <\/div>\n    <div id=\"rjsw-lyrics-blocks\"><\/div>\n    <div class=\"rjsw-lyrics-block\">\n      <div class=\"rjsw-lyrics-block-head\">\n        <div class=\"rjsw-lyrics-block-name\">Pronunciation \/ Instructions (optional)<\/div>\n      <\/div>\n      <textarea class=\"rjsw-textarea\" id=\"rjsw-instructions\" placeholder=\"e.g. Pronounce KIX like 'kicks'. Upbeat mood.\"><\/textarea>\n    <\/div>\n  <\/div>\n\n  <div class=\"rjsw-screen\" id=\"rjsw-screen-fmt\">\n    <div class=\"rjsw-sticky-cta\">\n      <button type=\"button\" class=\"rjsw-sticky-cta-back\" data-back=\"lyrics\">\u2190 Back<\/button>\n      <div class=\"rjsw-sticky-cta-info\">\n        <div class=\"rjsw-sticky-cta-label\">Total<\/div>\n        <div class=\"rjsw-sticky-cta-total\" id=\"rjsw-cta-total-fmt\">\u00a30.00<\/div>\n        <div class=\"rjsw-sticky-cta-save\" id=\"rjsw-cta-save-fmt\"><\/div>\n      <\/div>\n      <button type=\"button\" class=\"rjsw-sticky-cta-btn\" data-next=\"review\" id=\"rjsw-cta-btn-fmt\">Review \u2192<\/button>\n    <\/div>\n    <div class=\"rjsw-screen-head\">\n      <div>\n        <h2 class=\"rjsw-screen-title\">Format &amp; extras<\/h2>\n        <p class=\"rjsw-screen-sub\">Pick your file format and any optional add-ons.<\/p>\n      <\/div>\n    <\/div>\n    <div class=\"rjsw-fmt-bar\">\n      <div class=\"rjsw-fmt-head\">File format<\/div>\n      <div class=\"rjsw-fmt-options\">\n        <button type=\"button\" class=\"rjsw-fmt-opt selected\" data-format=\"MP3\">MP3<\/button>\n        <button type=\"button\" class=\"rjsw-fmt-opt\" data-format=\"WAVE\">WAV<\/button>\n        <button type=\"button\" class=\"rjsw-fmt-opt\" data-format=\"AIFF\">AIFF<\/button>\n      <\/div>\n    <\/div>\n    <div id=\"rjsw-extras-container\"><\/div>\n  <\/div>\n\n  <div class=\"rjsw-screen\" id=\"rjsw-screen-review\">\n    <div class=\"rjsw-sticky-cta\">\n      <button type=\"button\" class=\"rjsw-sticky-cta-back\" data-back=\"fmt\">\u2190 Back<\/button>\n      <div class=\"rjsw-sticky-cta-info\">\n        <div class=\"rjsw-sticky-cta-label\">Total<\/div>\n        <div class=\"rjsw-sticky-cta-total\" id=\"rjsw-cta-total-review\">\u00a30.00<\/div>\n        <div class=\"rjsw-sticky-cta-save\" id=\"rjsw-cta-save-review\"><\/div>\n      <\/div>\n      <button type=\"button\" class=\"rjsw-sticky-cta-btn\" data-next=\"cart\" id=\"rjsw-cta-btn-review\">Add to cart \ud83d\uded2<\/button>\n    <\/div>\n    <div class=\"rjsw-screen-head\">\n      <div>\n        <h2 class=\"rjsw-screen-title\">Review &amp; add to cart<\/h2>\n        <p class=\"rjsw-screen-sub\">Check everything looks right.<\/p>\n      <\/div>\n    <\/div>\n    <div id=\"rjsw-review-container\"><\/div>\n  <\/div>\n<\/div>\n\n<div class=\"rjsw-modal-overlay\" id=\"rjsw-cart-modal\">\n  <div class=\"rjsw-modal\">\n    <div class=\"rjsw-modal-loader\"><\/div>\n    <h3 id=\"rjsw-modal-title\">Adding to cart\u2026<\/h3>\n    <p class=\"rjsw-modal-sub\" id=\"rjsw-modal-sub\">Please don't close this tab.<\/p>\n    <div class=\"rjsw-modal-progress\" id=\"rjsw-modal-progress\"><\/div>\n  <\/div>\n<\/div>\n\n<div class=\"rjsw-toast\" id=\"rjsw-toast\"><\/div>\n\n<script>\n(function() {\n  'use strict';\n\n  \/\/ ===== DATA =====\n  const BEDS = [\n    { id: 'tameside-1', type: 'ac', name: 'Tameside 1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-tameside-01.mp3' },\n    { id: 'tameside-2', type: 'ac', name: 'Tameside 2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-tameside-02.mp3' },\n    { id: 'tameside-3', type: 'ac', name: 'Tameside 3', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-tameside-03.mp3' },\n    { id: 'tameside-4', type: 'ac', name: 'Tameside 4', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-tameside-04.mp3' },\n    { id: 'giant-1', type: 'ac', name: 'The Giant 1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-giant-01.mp3' },\n    { id: 'giant-2', type: 'ac', name: 'The Giant 2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-giant-02.mp3' },\n    { id: 'hotac-1', type: 'ac', name: 'Hot AC\/AC 1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-01.mp3' },\n    { id: 'hotac-2', type: 'ac', name: 'Hot AC\/AC 2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-02.mp3' },\n    { id: 'hotac-3', type: 'ac', name: 'Hot AC\/AC 3', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-03.mp3' },\n    { id: 'hotac-4', type: 'ac', name: 'Hot AC\/AC 4', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-04.mp3' },\n    { id: 'hotac-5', type: 'ac', name: 'Hot AC\/AC 5', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-05.mp3' },\n    { id: 'ac-hour-opener', type: 'ac', name: 'AC Hour Opener', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-hour-opener.mp3' },\n    { id: 'ac-news', type: 'ac', name: 'AC News Jingle', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-ac-news.mp3' },\n    { id: 'chr-1', type: 'chr', name: 'CHR-1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-01.mp3' },\n    { id: 'chr-2', type: 'chr', name: 'CHR-2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-02.mp3' },\n    { id: 'chr-3', type: 'chr', name: 'CHR-3', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-03.mp3' },\n    { id: 'chr-4', type: 'chr', name: 'CHR-4', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-04.mp3' },\n    { id: 'chr-5', type: 'chr', name: 'CHR-5', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-05.mp3' },\n    { id: 'chr-6', type: 'chr', name: 'CHR-6', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-06.mp3' },\n    { id: 'chr-7', type: 'chr', name: 'CHR-7', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-chr-07.mp3' },\n    { id: 'oldies-1', type: 'oldies', name: 'Oldies-1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-01.mp3' },\n    { id: 'oldies-2', type: 'oldies', name: 'Oldies-2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-02.mp3' },\n    { id: 'oldies-3', type: 'oldies', name: 'Oldies-3', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-03.mp3' },\n    { id: 'oldies-4', type: 'oldies', name: 'Oldies-4', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-04.mp3' },\n    { id: 'oldies-5', type: 'oldies', name: 'Oldies-5', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-05.mp3' },\n    { id: 'oldies-6', type: 'oldies', name: 'Oldies-6', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-06.mp3' },\n    { id: 'oldies-7', type: 'oldies', name: 'Oldies-7', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-07.mp3' },\n    { id: 'oldies-8', type: 'oldies', name: 'Oldies-8', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-08.mp3' },\n    { id: 'oldies-9', type: 'oldies', name: 'Oldies-9', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-09.mp3' },\n    { id: 'oldies-10', type: 'oldies', name: 'Oldies-10', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-10.mp3' },\n    { id: 'oldies-11', type: 'oldies', name: 'Oldies-11', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-11.mp3' },\n    { id: 'oldies-12', type: 'oldies', name: 'Oldies-12', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-12.mp3' },\n    { id: 'oldies-13', type: 'oldies', name: 'Oldies-13', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-13.mp3' },\n    { id: 'oldies-14', type: 'oldies', name: 'Oldies-14', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-14.mp3' },\n    { id: 'oldies-15', type: 'oldies', name: 'Oldies-15', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-15.mp3' },\n    { id: 'oldies-christmas-1', type: 'oldies', name: 'Oldies-Christmas', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-oldies-christmas-01.mp3' },\n    { id: 'epic-1', type: 'oldies', name: 'Epic 1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-epic-01.mp3' },\n    { id: 'epic-2', type: 'oldies', name: 'Epic 2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-epic-02.mp3' },\n    { id: 'epic-3', type: 'oldies', name: 'Epic 3', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-epic-03.mp3' },\n    { id: 'epic-4', type: 'oldies', name: 'Epic 4', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-epic-04.mp3' },\n    { id: 'epic-5', type: 'oldies', name: 'Epic 5', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-epic-05.mp3' },\n    { id: 'kiss-1', type: 'oldies', name: 'Kiss 1', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-kiss-02.mp3' },\n    { id: 'kiss-2', type: 'oldies', name: 'Kiss 2', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-kiss-03.mp3' },\n    { id: 'kiss-3', type: 'oldies', name: 'Kiss 3', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-kiss-05.mp3' },\n    { id: 'kiss-4', type: 'oldies', name: 'Kiss 4', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-kiss-06.mp3' },\n    { id: 'kiss-5', type: 'oldies', name: 'Kiss 5', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-kiss-09.mp3' },\n    { id: 'kiss-6', type: 'oldies', name: 'Kiss 6', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-kiss-10.mp3' },\n    { id: 'acapella', type: 'acapella', name: 'Acapella (no bed)', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2026\/09\/rj24-acapella.mp3' },\n  ];\n\n  const TYPE_LABELS = { ac: 'AC', chr: 'CHR', oldies: 'Oldies', acapella: 'Acap.' };\n  const TYPE_TMEPO = {\n    ac: 'AC \/ Hot AC',\n    chr: 'CHR',\n    oldies: 'Oldies & Country',\n    acapella: 'Acapella Only',\n    package: 'Full Jingle'\n  };\n\n  const PACKAGES = [\n    { id: 'tameside', name: 'Tameside Radio Hot AC', tmepoMatch: 'Tameside Radio Hot AC Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/06\/tameside-300x194.jpg', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/06\/tameside-demo.mp3' },\n    { id: 'giant', name: 'The Giant \u2013 AC', tmepoMatch: 'The Giant - AC Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Original123-300x196.jpg', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Packages_Giant.mp3' },\n    { id: 'nfrs', name: 'NFRS Radio \u2013 AC', tmepoMatch: 'NFRS Radio - AC Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Screenshot-2021-05-18-at-12.33.17-300x198.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Packages_NFRS.mp3' },\n    { id: 'smooth', name: 'Smooth AC', tmepoMatch: 'Smooth AC - AC Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Screenshot-2021-05-18-at-12.36.46-300x198.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Packages_Smooth-AC.mp3' },\n    { id: 'razor', name: 'Razor CHR', tmepoMatch: 'Razor CHR - CHR Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Screenshot-2021-05-18-at-12.37.41-300x198.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2021\/05\/Packages_Razor.mp3' },\n    { id: 'oldies-pkg-1', name: 'Oldies 1', tmepoMatch: 'Oldies 1 - Oldies Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/11\/jpeg-03-300x195.jpg', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/08\/Oldies-1019-Demo.mp3' },\n    { id: 'oldies-pkg-2', name: 'Oldies 2', tmepoMatch: 'Oldies 2 - Oldies Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/11\/jpeg-03-300x195.jpg', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/11\/Sung-Oldies-2.mp3' },\n    { id: 'mellow', name: 'Mellow Mix', tmepoMatch: 'Mellow Mix - Oldies Package', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/03\/Screenshot-2022-03-15-at-09.51.35-300x196.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/03\/Mellow-Demo.mp3' },\n    { id: 'epic-ac-1', name: 'Epic AC 1', tmepoMatch: 'Epic - AC 1', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/6456004ede9f4f989bbbf59b8c06a0c5.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/Epic_AC-1.mp3' },\n    { id: 'epic-wild', name: 'Epic Wild Country', tmepoMatch: 'Epic - Wild Country', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/6456004ede9f4f989bbbf59b8c06a0c5-2.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2023\/09\/Wild-Country-Package-Demo.mp3' },\n    { id: 'epic-ac-3', name: 'Epic AC 3', tmepoMatch: 'Epic - AC 3', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/6456004ede9f4f989bbbf59b8c06a0c5-3.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/Epic_AC-3.mp3' },\n    { id: 'epic-ac-4', name: 'Epic AC 4', tmepoMatch: 'Epic - AC 4', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/6456004ede9f4f989bbbf59b8c06a0c5-4.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/Epic-4-Pack.mp3' },\n    { id: 'epic-ac-5', name: 'Epic AC 5', tmepoMatch: 'Epic - AC 5', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/6456004ede9f4f989bbbf59b8c06a0c5-5.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/Epic-5-Package.mp3' },\n    { id: 'epic-country', name: 'Epic Country', tmepoMatch: 'Epic - Country', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/6456004ede9f4f989bbbf59b8c06a0c5-6.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2022\/11\/Epic_Country-1.mp3' },\n    { id: 'kiss-pkg-1', name: 'Kiss Package 1', tmepoMatch: 'Kiss Package 1', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/03\/THE-BEST-200-x-200-px.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2025\/06\/Kiss-Package_01.mp3' },\n    { id: 'kiss-pkg-2', name: 'Kiss Package 2', tmepoMatch: 'Kiss Package 2', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/03\/THE-BEST-200-x-200-px-2.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2025\/06\/Kiss-Package_02.mp3' },\n    { id: 'kiss-pkg-3', name: 'Kiss Package 3', tmepoMatch: 'Kiss Package 3', image: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/03\/THE-BEST-200-x-200-px-3.png', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2025\/06\/Kiss-Package_03.mp3' }\n  ];\n\n  \/\/ ROSIE IS THE WHOLE ROSTER. Bea left 2026-09 and Mo left 2026-09; both were\n  \/\/ priced upgrades here (+\u00a325 a jingle, +\u00a3100 a package), so until this was\n  \/\/ cut the page took money for a singer who could not record the job.\n  const SINGERS = [\n    { id: 'rosie', name: 'Rosie', creds: 'Bright, versatile, warm', delivery: '48 hours', singlePrice: 0, packagePrice: 0, avatar: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/i.30-300x300.jpg', demo: 'https:\/\/radiojingles24.com\/wp-content\/uploads\/2020\/05\/Rosie-Sung-Demo.mp3', tmepoMatch: 'Rosie' },\n  ];\n\n  const EXTRAS_SINGLE = [\n    { id: 'shotgun', name: 'Add Shotgun ID', desc: 'Quick punchy station ID cut. Per jingle.', price: 15, tmepoMatch: 'Shotgun' },\n    { id: 'ramp', name: 'Add Ramp\/Tag', desc: 'Talk-up ramp or tag cut. Per jingle.', price: 15, tmepoMatch: 'Ramp' }\n  ];\n  const EXTRAS_PACKAGE = [\n    { id: 'shotgun4', name: 'Add 4\u00d7 Shotgun IDs', desc: 'Four quick station ID cuts.', price: 49, tmepoMatch: '4x Shotgun' },\n    { id: 'ramp4', name: 'Add 4\u00d7 Ramp\/Tags', desc: 'Four talk-up ramps or tags.', price: 49, tmepoMatch: '4x Ramp' },\n    { id: 'fullstation', name: 'Upgrade to Full Station', desc: 'News, weather, travel + 6 sweepers. We\\'ll contact you for scripts.', price: 109, tmepoMatch: 'Full Station' }\n  ];\n\n  \/\/ ===== STATE =====\n  const state = {\n    screen: 'mode', mode: null, pack: [],\n    selectedPackage: null, packageLyrics: '',\n    singer: null, format: 'MP3', instructions: '',\n    extras: {}, filter: 'all',\n    currentAudio: null, currentPlayBtn: null\n  };\n\n  \/\/ ===== HELPERS =====\n  const $ = (s, c) => (c || document).querySelector(s);\n  const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));\n  const escapeHtml = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };\n  const isCustom = () => state.mode === 'custom';\n  const isPackage = () => state.mode === 'package';\n  const getExtras = () => isPackage() ? EXTRAS_PACKAGE : EXTRAS_SINGLE;\n\n  \/\/ v3.9.0 multi-jingle bundle pricing. Base bundle by count; the PLUGIN is\n  \/\/ the source of truth at the cart, the wizard shows matching estimates.\n  const BUNDLE = { 1: 69, 2: 108, 3: 159, 4: 199 };\n  const EXTRA_PER_JINGLE = 49; \/\/ each jingle beyond 4\n  function bundleBase(count) {\n    if (count <= 0) return 0;\n    if (count >= 4) return BUNDLE[4] + (count - 4) * EXTRA_PER_JINGLE;\n    return BUNDLE[count];\n  }\n\n  \/\/ Per-jingle premium relative to \u00a369 base: singer upcharge + extras +\n  \/\/ bed discount (e.g. Kiss -\u00a310). This is what each jingle sends as its\n  \/\/ rj24_price (69 + premium), which the plugin uses for the on-top amount.\n  function jinglePremium(j) {\n    let p = 0;\n    if (j && j.bed && j.bed.discount) p += j.bed.discount; \/\/ negative for Kiss\n    return p;\n  }\n  function calcSingerCost() {\n    if (!state.singer) return 0;\n    if (isPackage()) return state.singer.packagePrice;\n    return state.singer.singlePrice * Math.max(1, state.pack.length);\n  }\n  function calcExtrasCost() {\n    let total = 0;\n    const extras = getExtras();\n    extras.forEach(ex => {\n      if (state.extras[ex.id]) {\n        if (isCustom() && (ex.id === 'shotgun' || ex.id === 'ramp')) {\n          total += ex.price * Math.max(1, state.pack.length);\n        } else {\n          total += ex.price;\n        }\n      }\n    });\n    return total;\n  }\n  function calcTotal() {\n    let total = 0;\n    if (isCustom()) {\n      const count = state.pack.length;\n      if (count === 0) return 0;\n      total = bundleBase(count);\n      \/\/ bed-level discounts (Kiss) across the pack\n      state.pack.forEach(j => { total += jinglePremium(j); });\n    } else if (isPackage()) {\n      total = state.selectedPackage ? 199 : 0;\n    }\n    total += calcSingerCost();\n    total += calcExtrasCost();\n    return Math.max(0, total);\n  }\n  function calcSaving() {\n    if (!isCustom()) return 0;\n    const count = state.pack.length;\n    if (count < 2) return 0;\n    const full = 69 * count;\n    return Math.max(0, full - bundleBase(count));\n  }\n  function getTierMessage() {\n    const count = state.pack.length;\n    if (count === 0) return null;\n    if (count === 1) return { msg: 'Add 1 more \u2014 2 jingles for \u00a3108 (save \u00a330)', unlocked: false };\n    if (count === 2) return { msg: 'Add 1 more \u2014 3 jingles for \u00a3159 (save \u00a348)', unlocked: false };\n    if (count === 3) return { msg: '\u2605 1 more \u2014 4 jingles for \u00a3199 (save \u00a377)', unlocked: false };\n    return { msg: '\u2713 Bundle price unlocked \u2014 \u00a3199 for 4, then \u00a349 each', unlocked: true };\n  }\n\n  function showToast(msg, type) {\n    const t = $('#rjsw-toast');\n    t.textContent = msg;\n    t.className = 'rjsw-toast show' + (type ? ' ' + type : '');\n    clearTimeout(t._timer);\n    t._timer = setTimeout(() => t.classList.remove('show'), 3500);\n  }\n  function stopAudio() {\n    if (state.currentAudio) { try { state.currentAudio.pause(); } catch(e){} state.currentAudio = null; }\n    if (state.currentPlayBtn) {\n      state.currentPlayBtn.classList.remove('playing');\n      state.currentPlayBtn.textContent = state.currentPlayBtn.dataset.origText || '\u25b6';\n      state.currentPlayBtn = null;\n    }\n  }\n  function attachAudio(btn, url) {\n    btn.dataset.origText = btn.textContent;\n    btn.addEventListener('click', e => {\n      e.stopPropagation();\n      const wasPlaying = btn === state.currentPlayBtn && state.currentAudio && !state.currentAudio.paused;\n      stopAudio();\n      if (wasPlaying) return;\n      const a = new Audio(url);\n      a.play().catch(() => showToast('Could not play demo', 'error'));\n      state.currentAudio = a;\n      state.currentPlayBtn = btn;\n      btn.classList.add('playing');\n      btn.textContent = '\u23f8';\n      a.addEventListener('ended', () => {\n        btn.classList.remove('playing');\n        btn.textContent = btn.dataset.origText || '\u25b6';\n        state.currentAudio = null; state.currentPlayBtn = null;\n      });\n    });\n  }\n  function scrollUp() {\n    setTimeout(() => {\n      const top = $('#rj-sung-wizard').getBoundingClientRect().top + window.pageYOffset - 20;\n      window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });\n    }, 50);\n  }\n  function showScreen(name) {\n    state.screen = name;\n    $$('.rjsw-screen').forEach(s => s.classList.remove('active'));\n    $('#rjsw-screen-' + name).classList.add('active');\n    updateAllCTAs();\n    scrollUp();\n  }\n  function allLyricsFilled() {\n    if (isCustom()) return state.pack.every(j => j.lyrics && j.lyrics.trim().length > 0);\n    return state.packageLyrics.trim().length > 0;\n  }\n  function updateAllCTAs() {\n    const total = calcTotal();\n    const save = calcSaving();\n    const totalStr = '\u00a3' + total.toFixed(2);\n    const saveStr = save > 0 ? '\u2605 Save \u00a3' + save : '';\n    ['browse','package','singer','lyrics','fmt','review'].forEach(scr => {\n      const tEl = document.getElementById('rjsw-cta-total-' + scr);\n      const sEl = document.getElementById('rjsw-cta-save-' + scr);\n      if (tEl) tEl.textContent = totalStr;\n      if (sEl) sEl.textContent = saveStr;\n    });\n    const browseBtn = $('#rjsw-cta-btn-browse'); if (browseBtn) browseBtn.disabled = state.pack.length === 0;\n    const pkgBtn = $('#rjsw-cta-btn-package'); if (pkgBtn) pkgBtn.disabled = !state.selectedPackage;\n    const singerBtn = $('#rjsw-cta-btn-singer'); if (singerBtn) singerBtn.disabled = !state.singer;\n    const lyricsBtn = $('#rjsw-cta-btn-lyrics'); if (lyricsBtn) lyricsBtn.disabled = !allLyricsFilled();\n  }\n\n  function renderPackBar() {\n    const bar = $('#rjsw-pack-bar');\n    if (state.pack.length === 0) {\n      bar.className = 'rjsw-pack-bar empty';\n      bar.innerHTML = 'Your pack is empty. Tap + on any bed below to start.';\n      return;\n    }\n    bar.className = 'rjsw-pack-bar';\n    let html = '<span class=\"rjsw-pack-bar-label\">' + state.pack.length + ' jingle' + (state.pack.length === 1 ? '' : 's') + ':<\/span>';\n    state.pack.forEach((j, i) => {\n      html += '<span class=\"rjsw-pack-chip\">' + escapeHtml(j.bed.name) +\n        '<button type=\"button\" class=\"rjsw-pack-chip-remove\" data-idx=\"'+i+'\" title=\"Remove\">\u2715<\/button><\/span>';\n    });\n    const tier = getTierMessage();\n    if (tier) html += '<span class=\"rjsw-pack-tier-msg ' + (tier.unlocked ? 'unlocked' : '') + '\">' + tier.msg + '<\/span>';\n    bar.innerHTML = html;\n    $$('.rjsw-pack-chip-remove', bar).forEach(btn => {\n      btn.addEventListener('click', () => {\n        state.pack.splice(parseInt(btn.dataset.idx, 10), 1);\n        renderPackBar(); renderBedGrid(); updateAllCTAs();\n      });\n    });\n  }\n  function renderBedGrid() {\n    const grid = $('#rjsw-bed-grid');\n    const beds = state.filter === 'all' ? BEDS : BEDS.filter(b => b.type === state.filter);\n    grid.innerHTML = beds.map(b => {\n      const count = state.pack.filter(j => j.bed.id === b.id).length;\n      const inPack = count > 0;\n      const typeTag = TYPE_LABELS[b.type] || b.type.toUpperCase();\n      const discount = b.discount ? '<span class=\"rjsw-bed-discount\">SAVE \u00a3' + Math.abs(b.discount) + '<\/span>' : '';\n      const countBadge = count > 1 ? '<div class=\"rjsw-bed-count\">' + count + '<\/div>' : '';\n      return '<div class=\"rjsw-bed-card ' + (inPack ? 'in-pack' : '') + '\" data-id=\"'+b.id+'\">' +\n        '<div class=\"rjsw-bed-type-tag\">' + typeTag + '<\/div>' + discount +\n        '<div class=\"rjsw-bed-card-name\">' + escapeHtml(b.name) + '<\/div>' +\n        '<div class=\"rjsw-bed-actions\">' +\n          '<button type=\"button\" class=\"rjsw-bed-btn play\" data-demo=\"'+b.demo+'\">\u25b6<\/button>' +\n          (inPack ?\n            '<button type=\"button\" class=\"rjsw-bed-btn remove\" data-id=\"'+b.id+'\">\u2212<\/button>' +\n            '<button type=\"button\" class=\"rjsw-bed-btn add\" data-id=\"'+b.id+'\">+<\/button>' :\n            '<button type=\"button\" class=\"rjsw-bed-btn add\" data-id=\"'+b.id+'\">+<\/button>'\n          ) +\n        '<\/div>' + countBadge +\n      '<\/div>';\n    }).join('');\n    $$('.rjsw-bed-btn.play', grid).forEach(btn => attachAudio(btn, btn.dataset.demo));\n    $$('.rjsw-bed-btn.add', grid).forEach(btn => {\n      btn.addEventListener('click', e => {\n        e.stopPropagation();\n        const bed = BEDS.find(b => b.id === btn.dataset.id);\n        state.pack.push({ bed: bed, lyrics: '' });\n        renderPackBar(); renderBedGrid(); updateAllCTAs();\n        showToast('\"' + bed.name + '\" added \u2014 ' + state.pack.length + ' in pack', 'success');\n      });\n    });\n    $$('.rjsw-bed-btn.remove', grid).forEach(btn => {\n      btn.addEventListener('click', e => {\n        e.stopPropagation();\n        const id = btn.dataset.id;\n        for (let i = state.pack.length - 1; i >= 0; i--) {\n          if (state.pack[i].bed.id === id) { state.pack.splice(i, 1); break; }\n        }\n        renderPackBar(); renderBedGrid(); updateAllCTAs();\n      });\n    });\n  }\n  function renderPackages() {\n    const grid = $('#rjsw-pkg-grid');\n    grid.innerHTML = PACKAGES.map(p => {\n      const sel = state.selectedPackage && state.selectedPackage.id === p.id;\n      return '<div class=\"rjsw-pkg-card ' + (sel ? 'selected' : '') + '\" data-id=\"'+p.id+'\">' +\n        '<img decoding=\"async\" class=\"rjsw-pkg-thumb\" src=\"'+p.image+'\" alt=\"'+escapeHtml(p.name)+'\" loading=\"lazy\">' +\n        '<div class=\"rjsw-pkg-name\">' + escapeHtml(p.name) + '<\/div>' +\n        '<button type=\"button\" class=\"rjsw-pkg-listen\" data-demo=\"'+p.demo+'\">\u25b6 Listen<\/button>' +\n      '<\/div>';\n    }).join('');\n    $$('.rjsw-pkg-card', grid).forEach(card => {\n      card.addEventListener('click', e => {\n        if (e.target.closest('.rjsw-pkg-listen')) return;\n        state.selectedPackage = PACKAGES.find(p => p.id === card.dataset.id);\n        renderPackages(); updateAllCTAs();\n      });\n    });\n    $$('.rjsw-pkg-listen', grid).forEach(btn => attachAudio(btn, btn.dataset.demo));\n  }\n  function renderSingers() {\n    const grid = $('#rjsw-singers');\n    const pkg = isPackage();\n    const count = isCustom() ? Math.max(1, state.pack.length) : 1;\n    $('#rjsw-singer-sub').innerHTML = pkg ?\n      'Rosie sings your Full Package \u2014 included in the price.' :\n      'Rosie sings all ' + count + ' jingle' + (count === 1 ? '' : 's') + ' \u2014 included in the price.';\n    grid.innerHTML = SINGERS.map(s => {\n      const sel = state.singer && state.singer.id === s.id;\n      const unitPrice = pkg ? s.packagePrice : s.singlePrice;\n      let priceHtml;\n      if (unitPrice === 0) priceHtml = '<div class=\"rjsw-singer-price free\">Included<\/div>';\n      else if (pkg) priceHtml = '<div class=\"rjsw-singer-price\">+\u00a3' + unitPrice + '<\/div>';\n      else priceHtml = '<div class=\"rjsw-singer-price\">+\u00a3' + unitPrice + (count > 1 ? ' \u00d7 ' + count + ' = \u00a3' + (unitPrice * count) : '') + '<\/div>';\n      return '<div class=\"rjsw-singer ' + (sel ? 'selected' : '') + '\" data-id=\"'+s.id+'\">' +\n        '<div class=\"rjsw-singer-avatar-wrap\">' +\n          '<img decoding=\"async\" class=\"rjsw-singer-avatar\" src=\"'+s.avatar+'\" alt=\"'+escapeHtml(s.name)+'\" loading=\"lazy\">' +\n          '<button class=\"rjsw-singer-play\" type=\"button\" data-demo=\"'+s.demo+'\">\u25b6<\/button>' +\n        '<\/div>' +\n        '<div class=\"rjsw-singer-name\">'+escapeHtml(s.name)+'<\/div>' +\n        '<div class=\"rjsw-singer-creds\">'+s.creds+'<\/div>' +\n        '<div class=\"rjsw-singer-delivery\">\u23f1 '+s.delivery+'<\/div>' +\n        priceHtml +\n      '<\/div>';\n    }).join('');\n    $$('.rjsw-singer', grid).forEach(el => {\n      el.addEventListener('click', e => {\n        if (e.target.closest('.rjsw-singer-play')) return;\n        state.singer = SINGERS.find(x => x.id === el.dataset.id);\n        renderSingers(); updateAllCTAs();\n      });\n    });\n    $$('.rjsw-singer-play', grid).forEach(btn => attachAudio(btn, btn.dataset.demo));\n    \/\/ ONE SINGER MEANS THERE IS NOTHING TO CHOOSE. With Bea and Mo gone this\n    \/\/ screen shows a single card, and leaving it unselected left Continue\n    \/\/ disabled with no way to see why. Pick her and re-render once.\n    if (SINGERS.length === 1 && !state.singer) {\n      state.singer = SINGERS[0];\n      renderSingers(); updateAllCTAs();\n    }\n  }\n  function renderLyrics() {\n    const container = $('#rjsw-lyrics-blocks');\n    if (isPackage()) {\n      $('#rjsw-lyrics-sub').innerHTML = 'For Full Packages, number your lyrics 1\u20134 (one line per cut).';\n      container.innerHTML = '<div class=\"rjsw-lyrics-block\">' +\n        '<div class=\"rjsw-lyrics-block-head\">' +\n          '<div class=\"rjsw-lyrics-block-num\">\ud83d\udcdd<\/div>' +\n          '<div class=\"rjsw-lyrics-block-name\">' + escapeHtml(state.selectedPackage.name) + ' lyrics<\/div>' +\n        '<\/div>' +\n        '<textarea class=\"rjsw-textarea\" id=\"rjsw-pkg-lyrics\" placeholder=\"1. Your station name\\n2. Slogan or tag\\n3. Frequency\\n4. Final shout\">' + escapeHtml(state.packageLyrics) + '<\/textarea>' +\n        '<p class=\"rjsw-hint\">Keep each line to 5\u20138 words for best quality.<\/p>' +\n      '<\/div>';\n      $('#rjsw-pkg-lyrics').addEventListener('input', e => {\n        state.packageLyrics = e.target.value; updateAllCTAs();\n      });\n    } else {\n      $('#rjsw-lyrics-sub').textContent = 'One textbox per jingle. Keep each to 5\u20138 words.';\n      container.innerHTML = state.pack.map((j, i) => {\n        return '<div class=\"rjsw-lyrics-block\">' +\n          '<div class=\"rjsw-lyrics-block-head\">' +\n            '<div class=\"rjsw-lyrics-block-num\">' + (i + 1) + '<\/div>' +\n            '<div class=\"rjsw-lyrics-block-name\">' + escapeHtml(j.bed.name) + '<\/div>' +\n            '<div class=\"rjsw-lyrics-block-type\">' + (TYPE_LABELS[j.bed.type] || '') + '<\/div>' +\n          '<\/div>' +\n          '<textarea class=\"rjsw-textarea\" data-idx=\"'+i+'\" placeholder=\"e.g. KIX 99 \u2014 the hits that move you\">' + escapeHtml(j.lyrics) + '<\/textarea>' +\n        '<\/div>';\n      }).join('');\n      $$('textarea[data-idx]', container).forEach(ta => {\n        ta.addEventListener('input', e => {\n          state.pack[parseInt(e.target.dataset.idx, 10)].lyrics = e.target.value;\n          updateAllCTAs();\n        });\n      });\n    }\n    $('#rjsw-instructions').value = state.instructions;\n  }\n  function renderExtras() {\n    const container = $('#rjsw-extras-container');\n    const items = getExtras();\n    const count = isCustom() ? Math.max(1, state.pack.length) : 1;\n    const perJingle = isCustom();\n    container.innerHTML = '<div style=\"font-size:14px;font-weight:700;color:var(--rj-ink);margin-bottom:10px;\">Optional extras<\/div>' +\n      '<div class=\"rjsw-extras\">' + items.map(ex => {\n        const sel = !!state.extras[ex.id];\n        const isPerJ = perJingle && (ex.id === 'shotgun' || ex.id === 'ramp');\n        const priceLabel = (isPerJ && count > 1) ? '+\u00a3' + ex.price + ' \u00d7 ' + count + ' = \u00a3' + (ex.price * count) : '+\u00a3' + ex.price;\n        return '<div class=\"rjsw-extra '+(sel?'selected':'')+'\" data-id=\"'+ex.id+'\">' +\n          '<div class=\"rjsw-extra-check\">'+(sel?'\u2713':'')+'<\/div>' +\n          '<div class=\"rjsw-extra-info\">' +\n            '<div class=\"rjsw-extra-name\">'+escapeHtml(ex.name)+'<\/div>' +\n            '<div class=\"rjsw-extra-desc\">'+escapeHtml(ex.desc)+'<\/div>' +\n          '<\/div>' +\n          '<div class=\"rjsw-extra-price\">'+priceLabel+'<\/div>' +\n        '<\/div>';\n      }).join('') + '<\/div>';\n    $$('.rjsw-extra', container).forEach(el => {\n      el.addEventListener('click', () => {\n        state.extras[el.dataset.id] = !state.extras[el.dataset.id];\n        renderExtras(); updateAllCTAs();\n      });\n    });\n  }\n  function renderReview() {\n    const container = $('#rjsw-review-container');\n    const extras = getExtras();\n    const selExtras = extras.filter(ex => state.extras[ex.id]);\n    let html = '<div class=\"rjsw-review-card\">';\n    if (isCustom()) {\n      html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Pack<\/span><span class=\"rjsw-review-value\">' +\n        state.pack.length + ' jingle' + (state.pack.length === 1 ? '' : 's') + '<\/span><\/div>';\n      state.pack.forEach((j, i) => {\n        html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">#' + (i+1) + ' ' + (TYPE_LABELS[j.bed.type] || '') + '<\/span><span class=\"rjsw-review-value\">' +\n          escapeHtml(j.bed.name) + ' <span class=\"rjsw-review-value-muted\">\"' + escapeHtml((j.lyrics || '').substring(0, 40)) + (j.lyrics && j.lyrics.length > 40 ? '\u2026' : '') + '\"<\/span><\/span><\/div>';\n      });\n    } else {\n      html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Package<\/span><span class=\"rjsw-review-value\">' +\n        escapeHtml(state.selectedPackage.name) + ' <span class=\"rjsw-review-value-muted\">\u00a3199<\/span><\/span><\/div>';\n      html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Lyrics<\/span><span class=\"rjsw-review-value\" style=\"font-style:italic\">' + escapeHtml(state.packageLyrics) + '<\/span><\/div>';\n    }\n    html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Singer<\/span><span class=\"rjsw-review-value\">' +\n      escapeHtml(state.singer.name) + ' <span class=\"rjsw-review-value-muted\">' + state.singer.delivery + '<\/span><\/span><\/div>';\n    if (state.instructions) html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Notes<\/span><span class=\"rjsw-review-value\">' + escapeHtml(state.instructions) + '<\/span><\/div>';\n    html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Format<\/span><span class=\"rjsw-review-value\">' + state.format + '<\/span><\/div>';\n    if (selExtras.length) html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Extras<\/span><span class=\"rjsw-review-value\">' + selExtras.map(ex => escapeHtml(ex.name)).join(' \u00b7 ') + '<\/span><\/div>';\n    html += '<div class=\"rjsw-review-row\"><span class=\"rjsw-review-label\">Total<\/span><span class=\"rjsw-review-value\" style=\"font-size:18px;color:var(--rj-pink-dark);font-weight:800;\">\u00a3' + calcTotal().toFixed(2) + '<\/span><\/div>';\n    html += '<\/div>';\n    if (isCustom() && calcSaving() > 0) html += '<div class=\"rjsw-review-tier\">\u2605 You save \u00a3' + calcSaving() + ' with the bundle price<\/div>';\n    container.innerHTML = html;\n  }\n\n  \/\/ ==========================================================\n  \/\/ TMEPO \u2014 DIRECT COPY from spoken wizard, plus sung-specific extensions\n  \/\/ ==========================================================\n\n  \/**\n   * Returns true if the element (or its associated label, for radio\/checkbox)\n   * is NOT hidden by display:none on an ancestor.\n   *\n   * TMEPO ALWAYS sets display:none on the native <input> and <span.tc-input-wrap>\n   * because it renders custom styled buttons. So we must check via the LABEL,\n   * which is the visual element TMEPO toggles.\n   *\/\n  function isVisible(el) {\n    if (!el) return false;\n    \/\/ For radio\/checkbox inputs, the input itself is always display:none in TMEPO.\n    \/\/ We check via its associated label instead.\n    let target = el;\n    if (el.tagName === 'INPUT' && (el.type === 'radio' || el.type === 'checkbox')) {\n      const lblFor = el.id ? document.querySelector('label[for=\"' + el.id + '\"]') : null;\n      const lblParent = el.closest('label');\n      target = lblFor || lblParent || el.parentElement;\n    }\n    let cur = target;\n    while (cur && cur.tagName !== 'BODY') {\n      \/\/ Stop walking up when we hit our hidden TMEPO container wrapper\n      if (cur.classList && cur.classList.contains('tm-extra-product-options-fields')) {\n        return true;\n      }\n      const cs = getComputedStyle(cur);\n      if (cs.display === 'none') return false;\n      cur = cur.parentElement;\n    }\n    return true;\n  }\n\n  function findTMEPOInput(sectionHint, optionHint, requireVisible) {\n    \/\/ Search all heading levels (sung product may use h3\/h4 instead of h2)\n    const headings = document.querySelectorAll('.tm-extra-product-options-fields h2, .tm-extra-product-options-fields h3, .tm-extra-product-options-fields h4');\n    \/\/ If requireVisible (default true), collect all matches first, then return the visible one\n    const matches = [];\n    for (const heading of headings) {\n      if (heading.textContent.toLowerCase().indexOf(sectionHint.toLowerCase()) === -1) continue;\n      \/\/ Skip headings that are themselves inside hidden sections\n      if (requireVisible !== false && !isVisible(heading)) continue;\n      let section = heading.parentElement;\n      while (section && section.tagName !== 'BODY') {\n        const inputs = section.querySelectorAll('input[type=\"checkbox\"], input[type=\"radio\"]');\n        if (inputs.length > 0) {\n          for (const inp of inputs) {\n            const lblFor = inp.id ? document.querySelector('label[for=\"' + inp.id + '\"]') : null;\n            const lblParent = inp.closest('label');\n            const lblText = ((lblFor || lblParent || {}).textContent || '').trim().toLowerCase();\n            if (lblText.indexOf(optionHint.toLowerCase()) === -1) continue;\n            \/\/ Visibility filter\n            if (requireVisible !== false && !isVisible(inp)) continue;\n            matches.push(inp);\n          }\n          break;\n        }\n        section = section.parentElement;\n      }\n    }\n    return matches[0] || null;\n  }\n\n  function findTMEPOTextarea(hint, requireVisible) {\n    const allLabels = document.querySelectorAll('.tm-extra-product-options-fields label');\n    for (const lbl of allLabels) {\n      if (lbl.textContent.toLowerCase().indexOf(hint.toLowerCase()) === -1) continue;\n      if (requireVisible !== false && !isVisible(lbl)) continue;\n      const id = lbl.getAttribute('for');\n      if (id) {\n        const ta = document.getElementById(id);\n        if (ta && ta.tagName === 'TEXTAREA' && (requireVisible === false || isVisible(ta))) return ta;\n      }\n      const sibling = lbl.parentElement.querySelector('textarea');\n      if (sibling && (requireVisible === false || isVisible(sibling))) return sibling;\n    }\n    \/\/ Last-resort fallback: any visible textarea by position\n    const textareas = Array.from(document.querySelectorAll('.tm-extra-product-options-fields textarea'))\n      .filter(ta => requireVisible === false || isVisible(ta));\n    if (hint === 'script' || hint === 'type') return textareas[0] || null;\n    if (hint === 'instruction') return textareas[1] || null;\n    return null;\n  }\n\n  function fireChange(inp) {\n    inp.dispatchEvent(new Event('change', { bubbles: true }));\n    if (window.jQuery) try { window.jQuery(inp).trigger('change'); } catch (e) {}\n  }\n\n  function findBedInput(bedName) {\n    const inputs = document.querySelectorAll('.tm-extra-product-options-fields input[type=\"checkbox\"], .tm-extra-product-options-fields input[type=\"radio\"]');\n    for (const inp of inputs) {\n      if (!isVisible(inp)) continue;\n      const lblFor = inp.id ? document.querySelector('label[for=\"' + inp.id + '\"]') : null;\n      const lblText = lblFor ? lblFor.textContent.trim() : '';\n      if (lblText.toLowerCase().indexOf(bedName.toLowerCase()) === 0) {\n        return inp;\n      }\n    }\n    return null;\n  }\n\n  \/**\n   * fillTMEPO for ONE jingle. Returns Promise<missing[]>.\n   * Async because TMEPO conditional UI needs time to render after type selection.\n   *\/\n  async function fillTMEPO(typeKey, bedName, lyrics, packageTmepoMatch) {\n    const missing = [];\n    const tmepoTypeLabel = TYPE_TMEPO[typeKey];\n\n    \/\/ 1. Type \u2014 click and wait for conditional UI\n    if (tmepoTypeLabel) {\n      const t = findTMEPOInput('type of sung jingle', tmepoTypeLabel);\n      if (t) {\n        if (!t.checked) { t.checked = true; fireChange(t); }\n      } else {\n        missing.push('Type: ' + tmepoTypeLabel);\n      }\n    }\n    \/\/ Wait for TMEPO to render the bed\/package section based on type\n    await wait(800);\n\n    \/\/ 2. Bed or Package\n    if (typeKey === 'package' && packageTmepoMatch) {\n      const p = findTMEPOInput('pick a package', packageTmepoMatch);\n      if (p) {\n        if (!p.checked) { p.checked = true; fireChange(p); }\n      } else {\n        missing.push('Package: ' + packageTmepoMatch);\n      }\n      await wait(400);\n    } else if (typeKey !== 'acapella' && bedName) {\n      const b = findBedInput(bedName);\n      if (b) {\n        if (!b.checked) { b.checked = true; fireChange(b); }\n      } else {\n        missing.push('Bed: ' + bedName);\n      }\n      await wait(400);\n    }\n\n    \/\/ 3. Singer\n    if (state.singer) {\n      const s = findTMEPOInput('singer', state.singer.tmepoMatch);\n      if (s) {\n        if (!s.checked) { s.checked = true; fireChange(s); }\n      } else {\n        missing.push('Singer: ' + state.singer.name);\n      }\n    }\n    await wait(200);\n\n    \/\/ 4. Script\n    const script = findTMEPOTextarea('script') || findTMEPOTextarea('type in your');\n    if (script) {\n      script.value = lyrics;\n      script.dispatchEvent(new Event('input', { bubbles: true }));\n      fireChange(script);\n    }\n\n    \/\/ 5. Instructions\n    const instr = findTMEPOTextarea('instruction');\n    if (instr) {\n      instr.value = state.instructions;\n      instr.dispatchEvent(new Event('input', { bubbles: true }));\n      fireChange(instr);\n    }\n\n    \/\/ 6. Format\n    const fmt = findTMEPOInput('format', state.format);\n    if (fmt) {\n      if (!fmt.checked) { fmt.checked = true; fireChange(fmt); }\n    }\n\n    \/\/ 7. Extras\n    const extras = getExtras();\n    extras.forEach(ex => {\n      const inp = findTMEPOInput('extra', ex.tmepoMatch);\n      if (!inp) return;\n      const desired = !!state.extras[ex.id];\n      if (inp.checked !== desired) { inp.checked = desired; fireChange(inp); }\n    });\n\n    \/\/ Final settle before submit\n    await wait(400);\n    return missing;\n  }\n\n  function wait(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n  \/**\n   * Wipe all TMEPO field state. Called between jingles in a multi-jingle\n   * pack so that each submission starts from a clean slate \u2014 otherwise\n   * the previous jingle's hidden-but-checked radios get smuggled into the\n   * next POST and either cause WC to dedupe or produce garbled options.\n   *\/\n  function resetTMEPO() {\n    \/\/ Uncheck every TMEPO radio and checkbox\n    const inputs = document.querySelectorAll(\n      '.tm-extra-product-options-fields input[type=\"radio\"], ' +\n      '.tm-extra-product-options-fields input[type=\"checkbox\"]'\n    );\n    inputs.forEach(inp => {\n      if (inp.checked) {\n        inp.checked = false;\n        try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch(e) {}\n        if (window.jQuery) try { window.jQuery(inp).trigger('change'); } catch(e) {}\n      }\n    });\n\n    \/\/ Blank every TMEPO textarea\n    const textareas = document.querySelectorAll('.tm-extra-product-options-fields textarea');\n    textareas.forEach(ta => {\n      if (ta.value !== '') {\n        ta.value = '';\n        try { ta.dispatchEvent(new Event('input', { bubbles: true })); } catch(e) {}\n        try { ta.dispatchEvent(new Event('change', { bubbles: true })); } catch(e) {}\n        if (window.jQuery) try { window.jQuery(ta).trigger('change'); } catch(e) {}\n      }\n    });\n  }\n\n  \/\/ ==========================================================\n  \/\/ CART SUBMISSION \u2014 EXACT spoken pattern\n  \/\/ Multi-jingle: sessionStorage carries pack across page reloads.\n  \/\/ Each page load = one jingle add = one fresh TMEPO state.\n  \/\/ ==========================================================\n\n  \/\/ Multi-jingle queue\/sessionStorage machinery removed in v3.8.0.\n  \/\/ Each wizard run now adds exactly ONE jingle, so no cross-reload state\n  \/\/ is needed. Only the error key remains, for surfacing add failures.\n  \/\/ ==========================================================\n\n  const SS_ERROR_KEY = 'rj24_last_error';\n\n  function recordError(stage, idx, details) {\n    try {\n      sessionStorage.setItem(SS_ERROR_KEY, JSON.stringify({\n        stage: stage, jingle: idx, details: details, time: Date.now()\n      }));\n    } catch(e) {}\n  }\n  function readRecentError() {\n    try {\n      const raw = sessionStorage.getItem(SS_ERROR_KEY);\n      if (!raw) return null;\n      const err = JSON.parse(raw);\n      if (Date.now() - err.time > 300000) { sessionStorage.removeItem(SS_ERROR_KEY); return null; }\n      return err;\n    } catch(e) { return null; }\n  }\n  function clearError() { sessionStorage.removeItem(SS_ERROR_KEY); }\n\n  \/\/ ============================================================\n  \/\/ v3.8.1 \u2014 SINGLE-JINGLE ADD via NATIVE form submit -> \/cart\/\n  \/\/ ============================================================\n  \/\/ We add exactly ONE jingle per wizard run via WooCommerce's native\n  \/\/ add-to-cart (real button click -> server processes -> redirect to\n  \/\/ \/cart\/). This is the most reliable add path on the site and the only\n  \/\/ one proven to commit the item. wc-ajax and the fragments endpoint are\n  \/\/ broken here (return malformed HTML), so all AJAX\/verify code was removed.\n  \/\/ ============================================================\n  \/\/ ============================================================\n  \/\/ v3.9.0 \u2014 MULTI-JINGLE add via the RJ24 plugin AJAX action.\n  \/\/ ============================================================\n  \/\/ Each jingle is posted to the plugin's rj24_add_jingle endpoint\n  \/\/ (admin-ajax.php). One call = one cart line. No TMEPO, no wc-ajax, no\n  \/\/ iframe. The plugin returns clean JSON {success, cart_count, cart_url}.\n  \/\/ We submit the pack sequentially, verifying cart_count grows each time,\n  \/\/ then redirect to the cart. Bundle pricing is applied server-side by\n  \/\/ the plugin; the wizard's totals are estimates that will match.\n  \/\/ ============================================================\n\n  \/\/ Build the per-jingle payload list from current state.\n  function buildJingleQueue() {\n    const singerName = state.singer ? state.singer.name : '';\n    const extrasList = getExtras().filter(ex => state.extras[ex.id]).map(ex => ex.name).join(', ');\n\n    if (isPackage()) {\n      \/\/ Full Station Package = one line. Price is the \u00a3199 package + singer pkg price + extras.\n      const price = calcTotal(); \/\/ whole package total (single item)\n      return [{\n        type: 'Full Station Package',\n        bed: state.selectedPackage.name,\n        singer: singerName,\n        lyrics: state.packageLyrics,\n        format: state.format,\n        extras: extrasList,\n        instructions: state.instructions,\n        price: price\n      }];\n    }\n\n    \/\/ Custom pack: one payload per jingle. Each jingle's price is the\n    \/\/ \u00a369 base + that bed's discount + singer single price + per-jingle extras.\n    \/\/ (The plugin re-derives bundle pricing across the whole cart; we send a\n    \/\/ per-line price so singer\/extras\/bed-discount premium is captured.)\n    const singerPer = state.singer ? state.singer.singlePrice : 0;\n    let perJingleExtras = 0;\n    getExtras().forEach(ex => {\n      if (state.extras[ex.id] && (ex.id === 'shotgun' || ex.id === 'ramp')) perJingleExtras += ex.price;\n    });\n    return state.pack.map(j => {\n      const bedDisc = (j.bed && j.bed.discount) ? j.bed.discount : 0;\n      const price = 69 + bedDisc + singerPer + perJingleExtras;\n      return {\n        type: TYPE_TMEPO[j.bed.type] || j.bed.type,\n        bed: j.bed.name,\n        singer: singerName,\n        lyrics: j.lyrics,\n        format: state.format,\n        extras: extrasList,\n        instructions: state.instructions,\n        price: price\n      };\n    });\n  }\n\n  \/\/ POST one jingle to the plugin endpoint. Resolves with parsed JSON.\n  async function postJingle(payload) {\n    const cfg = window.RJ24_CFG || {};\n    const body = new URLSearchParams();\n    body.set('action', 'rj24_add_jingle');\n    body.set('nonce', cfg.nonce || '');\n    body.set('rj24_is_sung', '1');\n    if (cfg.productId) body.set('rj24_product_id', String(cfg.productId));\n    body.set('rj24_type', payload.type || '');\n    body.set('rj24_bed', payload.bed || '');\n    body.set('rj24_singer', payload.singer || '');\n    body.set('rj24_lyrics', payload.lyrics || '');\n    body.set('rj24_format', payload.format || 'MP3');\n    body.set('rj24_extras', payload.extras || '');\n    body.set('rj24_instructions', payload.instructions || '');\n    body.set('rj24_price', String(payload.price != null ? payload.price : 69));\n\n    const resp = await fetch(cfg.ajaxUrl, {\n      method: 'POST',\n      credentials: 'same-origin',\n      headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n      body: body.toString()\n    });\n    if (!resp.ok) throw new Error('Server returned HTTP ' + resp.status);\n    const data = await resp.json().catch(() => null);\n    if (!data) throw new Error('Unexpected response from server');\n    if (!data.success) throw new Error(data.message || 'Add to cart was rejected');\n    return data;\n  }\n\n  async function startCartFlow() {\n    const cfg = window.RJ24_CFG || {};\n    if (!cfg.ajaxUrl || !cfg.nonce) {\n      return showCartError('The cart isn\\'t ready (configuration missing). Please refresh the page and try again.');\n    }\n\n    const queue = buildJingleQueue();\n    if (queue.length === 0) {\n      return showCartError('Nothing to add. Please pick at least one jingle.');\n    }\n\n    showCartModal(0, queue.length);\n\n    let lastResp = null;\n    for (let i = 0; i < queue.length; i++) {\n      updateCartModalProgress(i, queue.length);\n      try {\n        \/\/ Diagnostic snapshot\n        try { sessionStorage.setItem('rj24_sent_' + (i + 1), JSON.stringify(queue[i]).substring(0, 600)); } catch(e) {}\n\n        const data = await postJingle(queue[i]);\n        lastResp = data;\n\n        try { sessionStorage.setItem('rj24_resp_' + (i + 1), 'ok cart_count=' + data.cart_count); } catch(e) {}\n\n        \/\/ Verify the cart actually grew to at least (i+1) items.\n        if (typeof data.cart_count === 'number' && data.cart_count < (i + 1)) {\n          throw new Error('Cart did not register jingle ' + (i + 1) + ' (count=' + data.cart_count + ')');\n        }\n      } catch (err) {\n        console.error('[RJSW] Jingle ' + (i + 1) + ' failed:', err);\n        recordError('cart_add_failed', i, [err.message || String(err)]);\n        return showCartError('Jingle ' + (i + 1) + ' of ' + queue.length +\n          ' couldn\\'t be added:<br><code style=\"font-size:11px;background:var(--rj-bg);padding:6px;border-radius:4px;display:block;margin:8px 0;\">' +\n          escapeHtml(err.message || 'Unknown error') +\n          '<\/code>' + (i > 0 ? 'The ' + i + ' jingle' + (i === 1 ? '' : 's') + ' before it ' + (i === 1 ? 'is' : 'are') + ' in your cart. ' : '') +\n          'Please try again or contact us.');\n      }\n    }\n\n    \/\/ All jingles added \u2014 go to the cart.\n    $('#rjsw-modal-title').textContent = 'Added to cart \u2713';\n    $('#rjsw-modal-sub').textContent = 'Taking you to your cart\u2026';\n    const loader = $('#rjsw-cart-modal').querySelector('.rjsw-modal-loader');\n    if (loader) loader.style.display = 'none';\n    window.location.href = (lastResp && lastResp.cart_url) ? lastResp.cart_url : (cfg.cartUrl || '\/cart\/');\n  }\n\n  function showCartModal(currentIdx, totalCount) {\n    const modal = $('#rjsw-cart-modal');\n    $('#rjsw-modal-title').textContent = (totalCount && totalCount > 1) ? 'Adding your jingles\u2026' : 'Adding to cart\u2026';\n    $('#rjsw-modal-sub').textContent = 'Please don\\'t close this tab.';\n    const progress = $('#rjsw-modal-progress');\n    if (totalCount && totalCount > 1) {\n      progress.style.display = 'block';\n      progress.textContent = 'Jingle 1 of ' + totalCount;\n    } else {\n      progress.style.display = 'none';\n      progress.textContent = '';\n    }\n    const loader = modal.querySelector('.rjsw-modal-loader');\n    if (loader) loader.style.display = 'block';\n    modal.classList.add('open');\n  }\n\n  function updateCartModalProgress(idx, total) {\n    const progress = $('#rjsw-modal-progress');\n    if (total > 1) {\n      progress.style.display = 'block';\n      progress.textContent = 'Jingle ' + (idx + 1) + ' of ' + total;\n    }\n  }\n\n  function showCartError(html) {\n    const modal = $('#rjsw-cart-modal');\n    modal.classList.add('open');\n    $('#rjsw-modal-title').textContent = 'Couldn\\'t add to cart';\n    $('#rjsw-modal-sub').innerHTML = html;\n    const progress = $('#rjsw-modal-progress');\n    progress.style.display = 'none';\n    const loader = modal.querySelector('.rjsw-modal-loader');\n    if (loader) loader.style.display = 'none';\n  }\n\n  \/\/ Legacy no-op kept so any stray reference doesn't throw.\n  function checkPendingPack() { return false; }\n\n  \/\/ ===== NAVIGATION =====\n  function goNext(target) {\n    if (target === 'singer') {\n      if (isCustom() && state.pack.length === 0) { showToast('Add at least one bed first'); return; }\n      if (isPackage() && !state.selectedPackage) { showToast('Pick a package first'); return; }\n      showScreen('singer'); renderSingers();\n    } else if (target === 'lyrics') {\n      if (!state.singer) { showToast('Pick a singer first'); return; }\n      showScreen('lyrics'); renderLyrics();\n    } else if (target === 'fmt') {\n      if (!allLyricsFilled()) { showToast('Fill in lyrics for every jingle'); return; }\n      showScreen('fmt'); renderExtras();\n    } else if (target === 'review') {\n      showScreen('review'); renderReview();\n    } else if (target === 'cart') {\n      startCartFlow();\n    }\n  }\n  function goBack(target) {\n    if (target === 'mode') {\n      if (state.pack.length > 0 && !confirm('Going back will clear your selection. Continue?')) return;\n      state.pack = []; state.selectedPackage = null; state.mode = null;\n      showScreen('mode');\n    } else if (target === 'browse-or-package' || target === 'browse' || target === 'package') {\n      showScreen('browse'); renderPackBar(); renderBedGrid();\n    } else if (target === 'singer') { showScreen('singer'); renderSingers(); }\n    else if (target === 'lyrics') { showScreen('lyrics'); renderLyrics(); }\n    else if (target === 'fmt') { showScreen('fmt'); renderExtras(); }\n  }\n\n  function init() {\n    if (checkPendingPack()) return;\n\n    const recentErr = readRecentError();\n    if (recentErr) {\n      const detail = Array.isArray(recentErr.details) ? recentErr.details.join('; ') : String(recentErr.details);\n      const stage = recentErr.stage === 'fill_missing_fields' ? 'Missing TMEPO fields' :\n                    recentErr.stage === 'cart_submit_timeout' ? 'Cart submission timeout' : 'Error';\n      const jingleLabel = recentErr.jingle !== undefined && recentErr.jingle !== null ?\n        'Jingle #' + (recentErr.jingle + 1) + ': ' : '';\n      showToast(jingleLabel + stage + ' \\u2014 ' + detail.substring(0, 100), 'error');\n      clearError();\n    }\n\n    $$('.rjsw-mode-card').forEach(card => {\n      card.addEventListener('click', () => {\n        \/\/ Custom is the only mode since Full Station Package was removed\n        \/\/ (Sept 2026). showScreen('package') would now throw on a null element,\n        \/\/ so every route below goes to browse.\n        state.mode = 'custom';\n        showScreen('browse'); renderPackBar(); renderBedGrid();\n      });\n    });\n    $$('.rjsw-sticky-cta-back').forEach(btn => btn.addEventListener('click', () => goBack(btn.dataset.back)));\n    $$('.rjsw-sticky-cta-btn').forEach(btn => btn.addEventListener('click', () => goNext(btn.dataset.next)));\n    $$('.rjsw-filter-pill').forEach(pill => {\n      pill.addEventListener('click', () => {\n        $$('.rjsw-filter-pill').forEach(p => p.classList.remove('active'));\n        pill.classList.add('active');\n        state.filter = pill.dataset.filter;\n        renderBedGrid();\n      });\n    });\n    $$('.rjsw-fmt-opt').forEach(btn => {\n      btn.addEventListener('click', () => {\n        $$('.rjsw-fmt-opt').forEach(b => b.classList.remove('selected'));\n        btn.classList.add('selected');\n        state.format = btn.dataset.format;\n        updateAllCTAs();\n      });\n    });\n    $('#rjsw-instructions').addEventListener('input', e => { state.instructions = e.target.value; });\n    \/\/ STRAIGHT INTO THE BUILDER. The mode screen offered one card and one route,\n    \/\/ so it was a click that cost visitors and told them nothing. The markup is\n    \/\/ left in place (harmless, hidden) rather than cut out of a live page.\n    state.mode = 'custom';\n    renderPackBar(); renderBedGrid();\n    updateAllCTAs();\n  }\n\n  \/\/ v3.9.0: the wizard no longer drives TMEPO \u2014 it adds via the RJ24 plugin\n  \/\/ AJAX endpoint. So we just initialise as soon as the DOM is ready; there's\n  \/\/ no hidden product form to wait for.\n  function boot() {\n    try { init(); }\n    catch (e) { console.error('[RJSW] init error:', e); }\n  }\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);\n  else boot();\n\n  \/\/ ==========================================================\n  \/\/ v3.9.6 \u2014 Footer-aware lift for the fixed-bottom sticky bar.\n  \/\/ Pushes every visible .rjsw-sticky-cta up by the footer-overlap distance\n  \/\/ so the bar stops covering footer content. Lift is clamped so the bar\n  \/\/ remains visible at all times.\n  \/\/\n  \/\/ IMPORTANT: this script runs at HTML parse time, BEFORE the WP footer\n  \/\/ is in the DOM. So we have to defer attachment. We use a 3-tier retry:\n  \/\/ immediate \/ DOMContentLoaded \/ window.load \/ 200ms polling fallback.\n  \/\/ Once a scroll listener is attached, we never re-attach (idempotent).\n  \/\/ ==========================================================\n  (function () {\n    var attached = false;\n    var pollAttempts = 0;\n    function tryAttach() {\n      if (attached) return true;\n      var footer = document.querySelector('.rj24-foot, #footer, .site-footer');\n      if (!footer) return false; \/\/ footer not in DOM yet \u2014 try again later\n\n      var ticking = false;\n      function adjust() {\n        ticking = false;\n        var footerTop = footer.getBoundingClientRect().top;\n        var vh = window.innerHeight;\n        var overlap = vh - footerTop;\n        var bars = document.querySelectorAll('.rjsw-sticky-cta');\n        var visibleBar = null;\n        for (var j = 0; j < bars.length; j++) {\n          if (getComputedStyle(bars[j]).display !== 'none') { visibleBar = bars[j]; break; }\n        }\n        var barH = visibleBar ? visibleBar.offsetHeight : 64;\n        var maxLift = Math.max(0, vh - barH);\n        var lift = Math.max(0, Math.min(overlap, maxLift));\n        for (var i = 0; i < bars.length; i++) {\n          bars[i].style.transform = lift > 0 ? 'translateY(-' + lift + 'px)' : '';\n        }\n      }\n      function onScroll() {\n        if (ticking) return;\n        ticking = true;\n        window.requestAnimationFrame(adjust);\n      }\n      window.addEventListener('scroll', onScroll, { passive: true });\n      window.addEventListener('resize', onScroll, { passive: true });\n      attached = true;\n      adjust();\n      return true;\n    }\n    function pollUntilAttached() {\n      if (tryAttach()) return;\n      pollAttempts++;\n      if (pollAttempts < 25) setTimeout(pollUntilAttached, 200);\n    }\n    \/\/ 1. Try right now (in case footer is already there \u2014 unlikely but cheap)\n    if (!tryAttach()) {\n      \/\/ 2. Try on DOMContentLoaded\n      if (document.readyState === 'loading') {\n        document.addEventListener('DOMContentLoaded', tryAttach);\n      }\n      \/\/ 3. Try on window.load (most reliable \u2014 all resources are in by then)\n      window.addEventListener('load', tryAttach);\n      \/\/ 4. Polling fallback \u2014 covers themes\/plugins that inject footer late\n      setTimeout(pollUntilAttached, 200);\n    }\n  })();\n\n  window.RJSW = state;\n})();\n<\/script>\n<!--\n  RJ24 \u2014 SUNG JINGLE UPSELL OVERLAY  v2\n  Paste at the END of the Raw HTML block on \/product\/sung-audio\/.\n  Replaces v1 entirely \u2014 delete the old block first.\n\n  CHANGES FROM v1\n  * Ramp\/Tag and Shotgun removed. They are already offered earlier in the\n    wizard, and asking twice reads like the site has lost track of the order.\n  * The bed list was empty. v1 built it when the script parsed, which is BEFORE\n    the wizard has rendered its cards \u2014 so it always found nothing. It is now\n    built each time the overlay OPENS, off whatever is on the page at that\n    moment.\n  * Beds are now browsable rather than a dropdown: filter by AC \/ CHR \/ Oldies \/\n    Acap., play any of them, and show more only when asked. Picking a bed you\n    cannot hear is not a real choice.\n\n  HOW IT GETS ITS DATA\n  Everything comes off the wizard's own bed cards \u2014 name, category and demo URL\n  are already on them:\n      .rjsw-bed-card > .rjsw-bed-type-tag       category\n                     > .rjsw-bed-card-name      name\n                     > .rjsw-bed-btn.play[data-demo]  the MP3\n  So beds added or retired upstream appear here on their own. There is no\n  second list to maintain and nothing to keep in step.\n\n  The price is asked for from the server (rj24_jingle_quote) and printed as\n  given. Nothing here computes a price.\n\n  It hooks the existing button with a capture-phase listener and never edits the\n  wizard. Delete this block and the page is exactly as it was.\n-->\n\n<style>\n#rjsu-ov{position:fixed;inset:0;background:rgba(8,12,20,.74);backdrop-filter:blur(3px);\n  display:none;align-items:center;justify-content:center;z-index:100000;padding:18px;}\n#rjsu-ov.show{display:flex;}\n#rjsu-card{background:#fff;color:#1E293B;max-width:640px;width:100%;border-radius:16px;\n  padding:24px;box-shadow:0 24px 70px rgba(0,0,0,.42);font-family:inherit;\n  max-height:90vh;overflow-y:auto;}\n#rjsu-card h3{margin:0 0 6px;font-size:21px;line-height:1.25;}\n#rjsu-sub{margin:0 0 16px;font-size:14px;color:#475569;line-height:1.5;}\n#rjsu-next{color:#DB2777;font-weight:800;}\n.rjsu-lab{display:block;font-size:13px;font-weight:800;margin:16px 0 8px;}\n\n#rjsu-cats{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:10px;}\n.rjsu-cat{border:2px solid #E2E8F0;background:#fff;border-radius:999px;padding:6px 14px;\n  font:inherit;font-size:13px;font-weight:700;color:#1E293B;cursor:pointer;}\n.rjsu-cat.on{border-color:#EC4899;background:#FDF2F8;color:#DB2777;}\n\n#rjsu-beds{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;}\n.rjsu-bed{display:flex;align-items:center;gap:9px;border:2px solid #E2E8F0;border-radius:10px;\n  padding:9px 11px;cursor:pointer;background:#fff;}\n.rjsu-bed:hover{border-color:#F9A8D4;}\n.rjsu-bed.on{border-color:#EC4899;background:#FDF2F8;}\n.rjsu-bed-play{width:28px;height:28px;flex:none;border-radius:50%;border:0;background:#EC4899;\n  color:#fff;font-size:11px;cursor:pointer;line-height:1;}\n.rjsu-bed-play:hover{background:#DB2777;}\n.rjsu-bed-nm{font-size:13.5px;font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.rjsu-bed-cat{font-size:10.5px;font-weight:800;color:#94A3B8;letter-spacing:.04em;}\n#rjsu-more{margin-top:9px;width:100%;border:2px dashed #E2E8F0;background:#fff;border-radius:10px;\n  padding:10px;font:inherit;font-size:13px;font-weight:700;color:#475569;cursor:pointer;}\n#rjsu-more:hover{border-color:#EC4899;color:#DB2777;}\n\n#rjsu-ly{width:100%;box-sizing:border-box;border:2px solid #E2E8F0;border-radius:10px;\n  padding:10px 12px;font:inherit;font-size:14px;resize:vertical;min-height:70px;color:#1E293B;}\n#rjsu-ly:focus{outline:none;border-color:#EC4899;}\n#rjsu-run{margin-top:14px;font-size:13px;color:#475569;}\n#rjsu-run b{color:#1E293B;}\n#rjsu-act{display:flex;gap:10px;margin-top:18px;flex-wrap:wrap;}\n#rjsu-act button{flex:1 1 auto;min-width:160px;border:0;border-radius:10px;padding:13px 16px;\n  font:inherit;font-size:15px;font-weight:800;cursor:pointer;}\n#rjsu-yes{background:#EC4899;color:#fff !important;}\n#rjsu-yes:disabled{background:#F0AFCC;cursor:not-allowed;}\n#rjsu-no{background:#EEF2F6;color:#3d4d5f !important;}\n@media (max-width:560px){#rjsu-beds{grid-template-columns:1fr;}#rjsu-act button{flex:1 1 100%;}}\n<\/style>\n\n<div id=\"rjsu-ov\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"rjsu-title\">\n  <div id=\"rjsu-card\">\n    <h3 id=\"rjsu-title\">Add another sung jingle for <span id=\"rjsu-next\">\u2014<\/span>?<\/h3>\n    <p id=\"rjsu-sub\">Your bundle price drops the more you order, so the next one costs less than the first.<\/p>\n\n    <span class=\"rjsu-lab\">Pick a music bed \u2014 tap \u25b6 to hear it<\/span>\n    <div id=\"rjsu-cats\"><\/div>\n    <div id=\"rjsu-beds\"><\/div>\n    <button type=\"button\" id=\"rjsu-more\">Show more beds<\/button>\n\n    <label class=\"rjsu-lab\" for=\"rjsu-ly\">Lyrics for this jingle<\/label>\n    <textarea id=\"rjsu-ly\" placeholder=\"e.g. Test Radio \u2014 the greatest hits of all time\"><\/textarea>\n\n    <div id=\"rjsu-run\"><\/div>\n\n    <div id=\"rjsu-act\">\n      <button type=\"button\" id=\"rjsu-no\">No thanks, just checkout<\/button>\n      <button type=\"button\" id=\"rjsu-yes\">Add it to my order<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function () {\n  var CFG = window.RJ24_CFG || {};\n  if (!CFG.ajaxUrl || !CFG.nonce) { return; }\n\n  var ov   = document.getElementById('rjsu-ov');\n  var btn  = document.getElementById('rjsw-cta-btn-review');\n  var ly   = document.getElementById('rjsu-ly');\n  var yes  = document.getElementById('rjsu-yes');\n  var no   = document.getElementById('rjsu-no');\n  var cats = document.getElementById('rjsu-cats');\n  var grid = document.getElementById('rjsu-beds');\n  var more = document.getElementById('rjsu-more');\n  if (!ov || !btn || !grid) { return; }\n\n  var PAGE  = 8;              \/\/ beds shown before \"show more\"\n  var armed = false;\n  var quote = null;\n  var beds  = [];             \/\/ read fresh from the page each time we open\n  var cat   = 'All';\n  var shown = PAGE;\n  var chosen = null;\n  var playing = null, playingBtn = null;\n\n  \/*\n   * Read the beds off the wizard's own cards. This runs on OPEN, not at parse\n   * time: the wizard renders its cards with its own script, so at parse time\n   * there is nothing to read \u2014 which is why the v1 picker was always empty.\n   *\/\n  function readBeds() {\n    var out = [], seen = {};\n    [].forEach.call(document.querySelectorAll('.rjsw-bed-card'), function (c) {\n      var nmEl = c.querySelector('.rjsw-bed-card-name');\n      var name = nmEl ? nmEl.textContent.trim() : '';\n      if (!name || seen[name]) { return; }\n      seen[name] = 1;\n      var tagEl = c.querySelector('.rjsw-bed-type-tag');\n      var play  = c.querySelector('.rjsw-bed-btn.play');\n      out.push({\n        name: name,\n        cat:  tagEl ? tagEl.textContent.trim() : '',\n        demo: play ? (play.getAttribute('data-demo') || '') : ''\n      });\n    });\n    return out;\n  }\n\n  function stopAudio() {\n    if (playing) { playing.pause(); playing.currentTime = 0; }\n    if (playingBtn) { playingBtn.innerHTML = '&#9658;'; }\n    playing = null; playingBtn = null;\n  }\n\n  function renderCats() {\n    var list = ['All'];\n    beds.forEach(function (b) { if (b.cat && list.indexOf(b.cat) === -1) { list.push(b.cat); } });\n    cats.innerHTML = '';\n    list.forEach(function (c) {\n      var b = document.createElement('button');\n      b.type = 'button';\n      b.className = 'rjsu-cat' + (c === cat ? ' on' : '');\n      b.textContent = c;\n      b.addEventListener('click', function () {\n        cat = c; shown = PAGE; stopAudio(); renderCats(); renderBeds();\n      });\n      cats.appendChild(b);\n    });\n  }\n\n  function filtered() {\n    return cat === 'All' ? beds : beds.filter(function (b) { return b.cat === cat; });\n  }\n\n  function renderBeds() {\n    var list = filtered();\n    grid.innerHTML = '';\n    list.slice(0, shown).forEach(function (b) {\n      var row = document.createElement('div');\n      row.className = 'rjsu-bed' + (chosen === b.name ? ' on' : '');\n      row.innerHTML =\n        '<button type=\"button\" class=\"rjsu-bed-play\">&#9658;<\/button>' +\n        '<span class=\"rjsu-bed-nm\"><\/span>' +\n        '<span class=\"rjsu-bed-cat\"><\/span>';\n      row.querySelector('.rjsu-bed-nm').textContent = b.name;\n      row.querySelector('.rjsu-bed-cat').textContent = b.cat;\n\n      row.addEventListener('click', function (e) {\n        if (e.target.closest('.rjsu-bed-play')) { return; }\n        chosen = b.name;\n        renderBeds();\n        paint();\n      });\n\n      var pb = row.querySelector('.rjsu-bed-play');\n      pb.addEventListener('click', function (e) {\n        e.preventDefault(); e.stopPropagation();\n        if (!b.demo) { return; }\n        if (playing && playingBtn === pb) { stopAudio(); return; }\n        stopAudio();\n        var a = new Audio(b.demo);\n        a.play();\n        a.onended = function () { stopAudio(); };\n        playing = a; playingBtn = pb;\n        pb.innerHTML = '&#10073;&#10073;';\n      });\n\n      grid.appendChild(row);\n    });\n\n    var left = list.length - shown;\n    more.style.display = left > 0 ? 'block' : 'none';\n    more.textContent = 'Show ' + Math.min(left, PAGE) + ' more' +\n      (left > PAGE ? ' (' + left + ' left)' : '');\n  }\n\n  more.addEventListener('click', function () { shown += PAGE; renderBeds(); });\n\n  function paint() {\n    if (!quote) { return; }\n    document.getElementById('rjsu-next').textContent = '\u00a3' + quote.next_price;\n    var n = quote.count;\n    document.getElementById('rjsu-sub').textContent = n <= 1\n      ? 'Your bundle price drops the more you order, so the next one costs less than the first.'\n      : 'You have ' + n + ' jingles. The next one is charged at your bundle rate.';\n\n    var ready = chosen && ly.value.trim();\n    document.getElementById('rjsu-run').innerHTML = ready\n      ? 'Adding: <b>' + chosen + '<\/b> for <b>\u00a3' + quote.next_price + '<\/b>'\n      : (chosen ? 'Now add the lyrics for <b>' + chosen + '<\/b>.' : '');\n    yes.disabled = !ready;\n  }\n\n  ly.addEventListener('input', paint);\n\n  function post(action, extra) {\n    var b = new URLSearchParams();\n    b.set('action', action);\n    b.set('nonce', CFG.nonce);\n    Object.keys(extra || {}).forEach(function (k) { b.set(k, extra[k]); });\n    return fetch(CFG.ajaxUrl, {\n      method: 'POST', credentials: 'same-origin',\n      headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n      body: b.toString()\n    }).then(function (r) { return r.json(); });\n  }\n\n  btn.addEventListener('click', function (e) {\n    if (armed) { armed = false; return; }\n    e.preventDefault();\n    e.stopImmediatePropagation();\n\n    post('rj24_jingle_quote', {}).then(function (q) {\n      \/\/ If the server will not quote, do not invent a price - just let the\n      \/\/ normal add-to-cart happen.\n      if (!q || !q.success) { armed = true; btn.click(); return; }\n      quote  = q;\n      beds   = readBeds();\n      if (!beds.length) { armed = true; btn.click(); return; }\n      cat    = 'All'; shown = PAGE; chosen = null;\n      ly.value = '';\n      stopAudio(); renderCats(); renderBeds(); paint();\n      ov.classList.add('show');\n    }).catch(function () { armed = true; btn.click(); });\n  }, true);\n\n  function finish() {\n    stopAudio();\n    ov.classList.remove('show');\n    armed = true;\n    btn.click();                    \/\/ the wizard's own add now runs untouched\n  }\n\n  no.addEventListener('click', finish);\n  ov.addEventListener('click', function (e) { if (e.target === ov) { finish(); } });\n  document.addEventListener('keydown', function (e) {\n    if (e.key === 'Escape' && ov.classList.contains('show')) { finish(); }\n  });\n\n  yes.addEventListener('click', function () {\n    if (yes.disabled) { return; }\n    yes.disabled = true;\n    yes.textContent = 'Adding\u2026';\n    stopAudio();\n\n    post('rj24_add_jingle', {\n      rj24_product_id: CFG.productId || '',\n      rj24_bed:        chosen,\n      rj24_lyrics:     ly.value.trim(),\n      rj24_singer:     'Rosie',\n      rj24_format:     'MP3',\n      rj24_type:       'sung',\n      rj24_is_sung:    '1',\n      \/\/ Base price, so the plugin adds no per-line premium and the whole cart\n      \/\/ simply reprices onto the next bundle tier. The figure the customer was\n      \/\/ shown IS that repricing.\n      rj24_price:      '69'\n    }).then(finish).catch(finish);  \/\/ never strand them: their order still goes in\n  });\n})();\n<\/script>\n\n<!--\n  RJ24 \u2014 KEEP THE PAGE STILL ON \"CONTINUE\"\n\n  The wizard scrolls the new step into view on every Continue, which from\n  halfway down the page looks like it has jumped to the top and broken.\n\n  Rather than edit the wizard's step handling, this suppresses scrolling for a\n  moment after a Continue is pressed. The new content still loads; the page just\n  does not move. Delete this block and the original scrolling returns.\n\n  Deliberately narrow: it only arms on those three buttons, only lasts 900ms,\n  and restores the real functions afterwards, so nothing else on the page loses\n  the ability to scroll.\n-->\n<script>\n(function () {\n  var IDS = ['rjsw-cta-btn-browse', 'rjsw-cta-btn-singer',\n             'rjsw-cta-btn-lyrics', 'rjsw-cta-btn-fmt'];\n  var realScrollTo = window.scrollTo;\n  var realSIV = Element.prototype.scrollIntoView;\n  var until = 0;\n\n  window.scrollTo = function () {\n    if (Date.now() < until) { return; }\n    return realScrollTo.apply(window, arguments);\n  };\n  Element.prototype.scrollIntoView = function () {\n    if (Date.now() < until) { return; }\n    return realSIV.apply(this, arguments);\n  };\n\n  document.addEventListener('click', function (e) {\n    var t = e.target.closest && e.target.closest('button');\n    if (!t || IDS.indexOf(t.id) === -1) { return; }\n    \/\/ Hold the position across the step change and the smooth-scroll that\n    \/\/ would otherwise follow it.\n    until = Date.now() + 900;\n    var y = window.scrollY;\n    var keep = setInterval(function () {\n      if (Date.now() >= until) { clearInterval(keep); return; }\n      if (window.scrollY !== y) { realScrollTo.call(window, 0, y); }\n    }, 16);\n  }, true);\n})();\n<\/script>\n\n<!--\n  RJ24 \u2014 SKIP THE REVIEW STEP\n\n  The wizard runs browse \u2192 singer \u2192 lyrics \u2192 format \u2192 REVIEW \u2192 add to cart.\n  The review screen is one more click between a decided customer and their\n  cart, so this takes it out: the format step's button now goes straight to the\n  add-on offer, and from there into the cart.\n\n  It does NOT delete the review screen. The wizard still advances onto it\n  exactly as before \u2014 this just presses its Add to cart immediately, and the\n  offer overlay covers it while the customer answers. Nothing in the wizard's\n  own validation or state handling is bypassed or reimplemented; the same\n  button does the same job, just without waiting to be asked twice.\n\n  The button label changes with it, because \"Review \u2192\" would now be a lie.\n\n  Delete this block and the review step comes back.\n-->\n<script>\n(function () {\n  var fmt = document.getElementById('rjsw-cta-btn-fmt');\n  if (!fmt) { return; }\n\n  fmt.textContent = 'Add to cart \u2192';\n\n  fmt.addEventListener('click', function () {\n    \/\/ Let the wizard do its own step change first, then press the button it\n    \/\/ was going to show. Two frames is enough for it to render the screen and\n    \/\/ wire the button up; the overlay opens over the top.\n    var tries = 0;\n    var go = setInterval(function () {\n      tries++;\n      var add = document.getElementById('rjsw-cta-btn-review');\n      var onReview = document.querySelector('.rjsw-screen.active');\n      if (add && onReview && onReview.id === 'rjsw-screen-review') {\n        clearInterval(go);\n        add.click();\n      } else if (tries > 40) {\n        \/\/ Something changed in the wizard: stop, and leave the customer on the\n        \/\/ review screen able to finish by hand rather than stuck.\n        clearInterval(go);\n      }\n    }, 25);\n  });\n})();\n<\/script>\n\n\t\t<\/div>\n\t<\/div>\n\n\t\t\t<\/div> \n\t\t<\/div>\n\t<\/div> \n<\/div><\/div>\n","protected":false},"excerpt":{"rendered":"\/* ===== KILL ALL REMAINING WOOCOMMERCE CHROME ===== NOTE: We do NOT hide .woocommerce-tabs or #tab-description because the sung wizard's Raw HTML block lives INSIDE the description tab on the...","protected":false},"featured_media":6045,"comment_status":"closed","ping_status":"closed","template":"","meta":[],"product_brand":[],"product_cat":[89,87,481,557,15],"product_tag":[],"class_list":["post-18223","product","type-product","status-publish","has-post-thumbnail","product_cat-jingles","product_cat-packages","product_cat-radio","product_cat-sale","product_cat-uncategorized","tm-has-options","first","instock","taxable","shipping-taxable","purchasable","product-type-simple"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Sung Jingles - Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/radiojingles24.com\/fr\/produit\/sung-audio\/\" \/>\n<meta property=\"og:locale\" content=\"fr_FR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Sung Jingles - Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles\" \/>\n<meta property=\"og:url\" content=\"https:\/\/radiojingles24.com\/fr\/produit\/sung-audio\/\" \/>\n<meta property=\"og:site_name\" content=\"Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/radiojingles24\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-08T15:54:08+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/7Asset-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"492\" \/>\n\t<meta property=\"og:image:height\" content=\"492\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:site\" content=\"@radiojingles24\" \/>\n<meta name=\"twitter:label1\" content=\"Dur\u00e9e de lecture estim\u00e9e\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/\",\"url\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/\",\"name\":\"Sung Jingles - Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/radiojingles24.com\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/7Asset-1.png\",\"datePublished\":\"2020-03-12T05:58:44+00:00\",\"dateModified\":\"2026-09-08T15:54:08+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/#breadcrumb\"},\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/#primaryimage\",\"url\":\"https:\\\/\\\/radiojingles24.com\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/7Asset-1.png\",\"contentUrl\":\"https:\\\/\\\/radiojingles24.com\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/7Asset-1.png\",\"width\":492,\"height\":492},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/product\\\/sung-audio\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/radiojingles24.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Shop\",\"item\":\"https:\\\/\\\/radiojingles24.com\\\/es\\\/shop\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Sung Jingles\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/#website\",\"url\":\"https:\\\/\\\/radiojingles24.com\\\/\",\"name\":\"Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles\",\"description\":\"Radio Jingles, Sung Jingles, DJ Drops, and Podcast Intros for commercial, community and internet radio stations. UK Based. Prices from \u00a35. Order online.\",\"publisher\":{\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/radiojingles24.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fr-FR\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/#organization\",\"name\":\"Radio Jingles 24\",\"url\":\"https:\\\/\\\/radiojingles24.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/radiojingles24.com\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/RadioJingle1-1.jpg\",\"contentUrl\":\"https:\\\/\\\/radiojingles24.com\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/RadioJingle1-1.jpg\",\"width\":512,\"height\":158,\"caption\":\"Radio Jingles 24\"},\"image\":{\"@id\":\"https:\\\/\\\/radiojingles24.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/radiojingles24\\\/\",\"https:\\\/\\\/x.com\\\/radiojingles24\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Sung Jingles - Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/radiojingles24.com\/fr\/produit\/sung-audio\/","og_locale":"fr_FR","og_type":"article","og_title":"Sung Jingles - Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles","og_url":"https:\/\/radiojingles24.com\/fr\/produit\/sung-audio\/","og_site_name":"Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles","article_publisher":"https:\/\/www.facebook.com\/radiojingles24\/","article_modified_time":"2026-09-08T15:54:08+00:00","og_image":[{"width":492,"height":492,"url":"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/7Asset-1.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_site":"@radiojingles24","twitter_misc":{"Dur\u00e9e de lecture estim\u00e9e":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/radiojingles24.com\/product\/sung-audio\/","url":"https:\/\/radiojingles24.com\/product\/sung-audio\/","name":"Sung Jingles - Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Sung Jingles","isPartOf":{"@id":"https:\/\/radiojingles24.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/radiojingles24.com\/product\/sung-audio\/#primaryimage"},"image":{"@id":"https:\/\/radiojingles24.com\/product\/sung-audio\/#primaryimage"},"thumbnailUrl":"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/7Asset-1.png","datePublished":"2020-03-12T05:58:44+00:00","dateModified":"2026-09-08T15:54:08+00:00","breadcrumb":{"@id":"https:\/\/radiojingles24.com\/product\/sung-audio\/#breadcrumb"},"inLanguage":"fr-FR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/radiojingles24.com\/product\/sung-audio\/"]}]},{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/radiojingles24.com\/product\/sung-audio\/#primaryimage","url":"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/7Asset-1.png","contentUrl":"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/7Asset-1.png","width":492,"height":492},{"@type":"BreadcrumbList","@id":"https:\/\/radiojingles24.com\/product\/sung-audio\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/radiojingles24.com\/"},{"@type":"ListItem","position":2,"name":"Shop","item":"https:\/\/radiojingles24.com\/es\/shop\/"},{"@type":"ListItem","position":3,"name":"Sung Jingles"}]},{"@type":"WebSite","@id":"https:\/\/radiojingles24.com\/#website","url":"https:\/\/radiojingles24.com\/","name":"Radio Jingles 24: Radio Jingles | DJ Drops | Podcast Intros | Jingles chant\u00e9s","description":"Radio Jingles, Sung Jingles, DJ Drops et Podcast Intros pour les stations de radio commerciales, communautaires et Internet. Bas\u00e9 au Royaume-Uni. Prix \u00e0 partir de 5 \u00a3. Commander en ligne.","publisher":{"@id":"https:\/\/radiojingles24.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/radiojingles24.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fr-FR"},{"@type":"Organization","@id":"https:\/\/radiojingles24.com\/#organization","name":"Radio Jingles 24","url":"https:\/\/radiojingles24.com\/","logo":{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/radiojingles24.com\/#\/schema\/logo\/image\/","url":"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/RadioJingle1-1.jpg","contentUrl":"https:\/\/radiojingles24.com\/wp-content\/uploads\/2019\/11\/RadioJingle1-1.jpg","width":512,"height":158,"caption":"Radio Jingles 24"},"image":{"@id":"https:\/\/radiojingles24.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/radiojingles24\/","https:\/\/x.com\/radiojingles24"]}]}},"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/product\/18223","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/product"}],"about":[{"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/types\/product"}],"replies":[{"embeddable":true,"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/comments?post=18223"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/media\/6045"}],"wp:attachment":[{"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/media?parent=18223"}],"wp:term":[{"taxonomy":"product_brand","embeddable":true,"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/product_brand?post=18223"},{"taxonomy":"product_cat","embeddable":true,"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/product_cat?post=18223"},{"taxonomy":"product_tag","embeddable":true,"href":"https:\/\/radiojingles24.com\/fr\/wp-json\/wp\/v2\/product_tag?post=18223"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}